tmgmt.entity.job_item.inc 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. <?php
  2. /*
  3. * @file
  4. * Contains job item entity class.
  5. */
  6. /**
  7. * Entity class for the tmgmt_job entity.
  8. *
  9. * @ingroup tmgmt_job
  10. */
  11. class TMGMTJobItem extends Entity {
  12. /**
  13. * The source plugin that provides the item.
  14. *
  15. * @var varchar
  16. */
  17. public $plugin;
  18. /**
  19. * The identifier of the translation job.
  20. *
  21. * @var integer
  22. */
  23. public $tjid;
  24. /**
  25. * The identifier of the translation job item.
  26. *
  27. * @var integer
  28. */
  29. public $tjiid;
  30. /**
  31. * Type of this item, used by the plugin to identify it.
  32. *
  33. * @var string
  34. */
  35. public $item_type;
  36. /**
  37. * Id of the item.
  38. *
  39. * @var integer
  40. */
  41. public $item_id;
  42. /**
  43. * The time when the job item was changed as a timestamp.
  44. *
  45. * @var integer
  46. */
  47. public $changed;
  48. /**
  49. * Can be used by the source plugin to store the data instead of creating it
  50. * on demand.
  51. *
  52. * If additional information is added in the UI, like adding comments, it will
  53. * also be saved here.
  54. *
  55. * Always use TMGMTJobItem::getData() to load the data, which will use
  56. * this property if present and otherwise get it from the source.
  57. *
  58. * @var array
  59. */
  60. public $data = array();
  61. /**
  62. * Counter for all data items waiting for translation.
  63. *
  64. * @var integer
  65. */
  66. public $count_pending = 0;
  67. /**
  68. * Counter for all translated data items.
  69. *
  70. * @var integer
  71. */
  72. public $count_translated = 0;
  73. /**
  74. * Counter for all accepted data items.
  75. *
  76. * @var integer
  77. */
  78. public $count_accepted = 0;
  79. /**
  80. * Counter for all reviewed data items.
  81. *
  82. * @var integer
  83. */
  84. public $count_reviewed = 0;
  85. /**
  86. * Amount of words in this job item.
  87. *
  88. * @var integer
  89. */
  90. public $word_count = 0;
  91. /**
  92. * {@inheritdoc}
  93. */
  94. public function __construct(array $values = array()) {
  95. parent::__construct($values, 'tmgmt_job_item');
  96. if (!isset($this->state)) {
  97. $this->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
  98. }
  99. }
  100. /**
  101. * Clones as active.
  102. */
  103. public function cloneAsActive() {
  104. $clone = clone $this;
  105. $clone->data = NULL;
  106. $clone->tjid = NULL;
  107. $clone->tjiid = NULL;
  108. $clone->changed = NULL;
  109. $clone->word_count = NULL;
  110. $clone->count_accepted = NULL;
  111. $clone->count_pending = NULL;
  112. $clone->count_translated = NULL;
  113. $clone->count_reviewed = NULL;
  114. $clone->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
  115. return $clone;
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function defaultLabel() {
  121. if ($controller = $this->getSourceController()) {
  122. $label = $controller->getLabel($this);
  123. }
  124. else {
  125. $label = parent::defaultLabel();
  126. }
  127. if (strlen($label) > TMGMT_JOB_LABEL_MAX_LENGTH) {
  128. $label = truncate_utf8($label, TMGMT_JOB_LABEL_MAX_LENGTH, TRUE);
  129. }
  130. return $label;
  131. }
  132. /**
  133. * {@inheritdoc}
  134. *
  135. * @see _tmgmt_ui_breadcrumb()
  136. */
  137. public function defaultUri() {
  138. // The path of a job item is not directly below the job that it belongs to.
  139. // Having to maintain two unknowns / wildcards (job and job item) in the
  140. // path is more complex than it has to be. Instead we just append the
  141. // additional breadcrumb pieces manually with _tmgmt_ui_breadcrumb().
  142. return array('path' => 'admin/tmgmt/items/' . $this->tjiid);
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. public function buildContent($view_mode = 'full', $langcode = NULL) {
  148. $content = array();
  149. if (module_exists('tmgmt_ui')) {
  150. $content = tmgmt_ui_job_item_review($this);
  151. }
  152. return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
  153. }
  154. /**
  155. * Add a log message for this job item.
  156. *
  157. * @param $message
  158. * The message to store in the log. Keep $message translatable by not
  159. * concatenating dynamic values into it! Variables in the message should be
  160. * added by using placeholder strings alongside the variables argument to
  161. * declare the value of the placeholders. See t() for documentation on how
  162. * $message and $variables interact.
  163. * @param $variables
  164. * (Optional) An array of variables to replace in the message on display.
  165. * @param $type
  166. * (Optional) The type of the message. Can be one of 'status', 'error',
  167. * 'warning' or 'debug'. Messages of the type 'debug' will not get printed
  168. * to the screen.
  169. */
  170. public function addMessage($message, $variables = array(), $type = 'status') {
  171. // Save the job item if it hasn't yet been saved.
  172. if (!empty($this->tjiid) || $this->save()) {
  173. $message = tmgmt_message_create($message, $variables, array(
  174. 'tjid' => $this->tjid,
  175. 'tjiid' => $this->tjiid,
  176. 'uid' => $GLOBALS['user']->uid,
  177. 'type' => $type,
  178. ));
  179. if ($message->save()) {
  180. return $message;
  181. }
  182. }
  183. return FALSE;
  184. }
  185. /**
  186. * Retrieves the label of the source object via the source controller.
  187. *
  188. * @return
  189. * The label of the source object.
  190. */
  191. public function getSourceLabel() {
  192. if ($controller = $this->getSourceController()) {
  193. return $controller->getLabel($this);
  194. }
  195. return FALSE;
  196. }
  197. /**
  198. * Retrieves the path to the source object via the source controller.
  199. *
  200. * @return
  201. * The path to the source object.
  202. */
  203. public function getSourceUri() {
  204. if ($controller = $this->getSourceController()) {
  205. return $controller->getUri($this);
  206. }
  207. return FALSE;
  208. }
  209. /**
  210. * Returns the user readable type of job item.
  211. *
  212. * @param string
  213. * A type that describes the job item.
  214. */
  215. public function getSourceType() {
  216. if ($controller = $this->getSourceController()) {
  217. return $controller->getType($this);
  218. }
  219. return ucfirst($this->item_type);
  220. }
  221. /**
  222. * Loads the job entity that this job item is attached to.
  223. *
  224. * @return TMGMTJob
  225. * The job entity that this job item is attached to or FALSE if there was
  226. * a problem.
  227. */
  228. public function getJob() {
  229. if (!empty($this->tjid)) {
  230. return tmgmt_job_load($this->tjid);
  231. }
  232. return FALSE;
  233. }
  234. /**
  235. * Returns the translator for this job item.
  236. *
  237. * @return TMGMTTranslator
  238. * The translator entity or FALSE if there was a problem.
  239. */
  240. public function getTranslator() {
  241. if ($job = $this->getJob()) {
  242. return $job->getTranslator();
  243. }
  244. return FALSE;
  245. }
  246. /**
  247. * Returns the translator plugin controller of the translator of this job item.
  248. *
  249. * @return TMGMTTranslatorPluginControllerInterface
  250. * The controller of the translator plugin or FALSE if there was a problem.
  251. */
  252. public function getTranslatorController() {
  253. if ($job = $this->getJob()) {
  254. return $job->getTranslatorController();
  255. }
  256. return FALSE;
  257. }
  258. /**
  259. * Array of the data to be translated.
  260. *
  261. * The structure is similar to the form API in the way that it is a possibly
  262. * nested array with the following properties whose presence indicate that the
  263. * current element is a text that might need to be translated.
  264. *
  265. * - #text: The text to be translated.
  266. * - #label: (Optional) The label that might be shown to the translator.
  267. * - #comment: (Optional) A comment with additional information.
  268. * - #translate: (Optional) If set to FALSE the text will not be translated.
  269. * - #translation: The translated data. Set by the translator plugin.
  270. * - #escape: (Optional) List of arrays with a required string key, keyed by
  271. * the position key. Translators must use this list to prevent translation
  272. * of these strings if possible.
  273. *
  274. *
  275. * @todo: Move data item documentation to a new, separate api group.
  276. *
  277. * The key can be an alphanumeric string.
  278. * @param $key
  279. * If present, only the subarray identified by key is returned.
  280. * @param $index
  281. * Optional index of an attribute below $key.
  282. *
  283. * @return array
  284. * A structured data array.
  285. */
  286. public function getData(array $key = array(), $index = NULL) {
  287. if (empty($this->data) && !empty($this->tjid)) {
  288. // Load the data from the source if it has not been set yet.
  289. $this->data = $this->getSourceData();
  290. $this->save();
  291. }
  292. if (empty($key)) {
  293. return $this->data;
  294. }
  295. if ($index) {
  296. $key = array_merge($key, array($index));
  297. }
  298. return drupal_array_get_nested_value($this->data, $key);
  299. }
  300. /**
  301. * Loads the structured source data array from the source.
  302. */
  303. public function getSourceData() {
  304. if ($controller = $this->getSourceController()) {
  305. return $controller->getData($this);
  306. }
  307. return array();
  308. }
  309. /**
  310. * Returns the plugin controller of the configured plugin.
  311. *
  312. * @return TMGMTSourcePluginControllerInterface
  313. */
  314. public function getSourceController() {
  315. if (!empty($this->plugin)) {
  316. return tmgmt_source_plugin_controller($this->plugin);
  317. }
  318. return FALSE;
  319. }
  320. /**
  321. * Count of all pending data items
  322. *
  323. * @return
  324. * Pending counts
  325. */
  326. public function getCountPending() {
  327. return $this->count_pending;
  328. }
  329. /**
  330. * Count of all translated data items.
  331. *
  332. * @return
  333. * Translated count
  334. */
  335. public function getCountTranslated() {
  336. return $this->count_translated;
  337. }
  338. /**
  339. * Count of all accepted data items.
  340. *
  341. * @return
  342. * Accepted count
  343. */
  344. public function getCountAccepted() {
  345. return $this->count_accepted;
  346. }
  347. /**
  348. * Count of all accepted data items.
  349. *
  350. * @return
  351. * Accepted count
  352. */
  353. public function getCountReviewed() {
  354. return $this->count_reviewed;
  355. }
  356. /**
  357. * Word count of all data items.
  358. *
  359. * @return
  360. * Word count
  361. */
  362. public function getWordCount() {
  363. return (int)$this->word_count;
  364. }
  365. /**
  366. * Sets the state of the job item to 'needs review'.
  367. */
  368. public function needsReview($message = NULL, $variables = array(), $type = 'status') {
  369. if (!isset($message)) {
  370. $uri = $this->getSourceUri();
  371. $message = 'The translation for !source needs to be reviewed.';
  372. $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
  373. }
  374. $return = $this->setState(TMGMT_JOB_ITEM_STATE_REVIEW, $message, $variables, $type);
  375. // Auto accept the trganslation if the translator is configured for it.
  376. if ($this->getTranslator()->getSetting('auto_accept')) {
  377. $this->acceptTranslation();
  378. }
  379. return $return;
  380. }
  381. /**
  382. * Sets the state of the job item to 'accepted'.
  383. */
  384. public function accepted($message = NULL, $variables = array(), $type = 'status') {
  385. if (!isset($message)) {
  386. $uri = $this->getSourceUri();
  387. $message = 'The translation for !source has been accepted.';
  388. $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
  389. }
  390. $return = $this->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED, $message, $variables, $type);
  391. // Check if this was the last unfinished job item in this job.
  392. if (tmgmt_job_check_finished($this->tjid) && $job = $this->getJob()) {
  393. // Mark the job as finished.
  394. $job->finished();
  395. }
  396. return $return;
  397. }
  398. /**
  399. * Sets the state of the job item to 'active'.
  400. */
  401. public function active($message = NULL, $variables = array(), $type = 'status') {
  402. if (!isset($message)) {
  403. $uri = $this->getSourceUri();
  404. $message = 'The translation for !source is now being processed.';
  405. $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
  406. }
  407. return $this->setState(TMGMT_JOB_ITEM_STATE_ACTIVE, $message, $variables, $type);
  408. }
  409. /**
  410. * Updates the state of the job item.
  411. *
  412. * @param $state
  413. * The new state of the job item. Has to be one of the job state constants.
  414. * @param $message
  415. * (Optional) The log message to be saved along with the state change.
  416. * @param $variables
  417. * (Optional) An array of variables to replace in the message on display.
  418. *
  419. * @return int
  420. * The updated state of the job if it could be set.
  421. *
  422. * @see TMGMTJob::addMessage()
  423. */
  424. public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
  425. // Return TRUE if the state could be set. Return FALSE otherwise.
  426. if (array_key_exists($state, tmgmt_job_item_states()) && $this->state != $state) {
  427. $this->state = $state;
  428. $this->save();
  429. // If a message is attached to this state change add it now.
  430. if (!empty($message)) {
  431. $this->addMessage($message, $variables, $type);
  432. }
  433. }
  434. return $this->state;
  435. }
  436. /**
  437. * Returns the state of the job item. Can be one of the job item state
  438. * constants.
  439. *
  440. * @return integer
  441. * The state of the job item.
  442. */
  443. public function getState() {
  444. // We don't need to check if the state is actually set because we always set
  445. // it in the constructor.
  446. return $this->state;
  447. }
  448. /**
  449. * Checks whether the passed value matches the current state.
  450. *
  451. * @param $state
  452. * The value to check the current state against.
  453. *
  454. * @return boolean
  455. * TRUE if the passed state matches the current state, FALSE otherwise.
  456. */
  457. public function isState($state) {
  458. return $this->getState() == $state;
  459. }
  460. /**
  461. * Checks whether the state of this transaction is 'accepted'.
  462. *
  463. * @return boolean
  464. * TRUE if the state is 'accepted', FALSE otherwise.
  465. */
  466. public function isAccepted() {
  467. return $this->isState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
  468. }
  469. /**
  470. * Checks whether the state of this transaction is 'active'.
  471. *
  472. * @return boolean
  473. * TRUE if the state is 'active', FALSE otherwise.
  474. */
  475. public function isActive() {
  476. return $this->isState(TMGMT_JOB_ITEM_STATE_ACTIVE);
  477. }
  478. /**
  479. * Checks whether the state of this transaction is 'needs review'.
  480. *
  481. * @return boolean
  482. * TRUE if the state is 'needs review', FALSE otherwise.
  483. */
  484. public function isNeedsReview() {
  485. return $this->isState(TMGMT_JOB_ITEM_STATE_REVIEW);
  486. }
  487. /**
  488. * Checks whether the state of this transaction is 'aborted'.
  489. *
  490. * @return boolean
  491. * TRUE if the state is 'aborted', FALSE otherwise.
  492. */
  493. public function isAborted() {
  494. return $this->isState(TMGMT_JOB_ITEM_STATE_ABORTED);
  495. }
  496. /**
  497. * Recursively writes translated data to the data array of a job item.
  498. *
  499. * While doing this the #status of each data item is set to
  500. * TMGMT_DATA_ITEM_STATE_TRANSLATED.
  501. *
  502. * @param $translation
  503. * Nested array of translated data. Can either be a single text entry, the
  504. * whole data structure or parts of it.
  505. * @param $key
  506. * (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
  507. * one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
  508. * means that it will replace the whole translated data array.
  509. */
  510. protected function addTranslatedDataRecursive($translation, $key = array()) {
  511. if (isset($translation['#text'])) {
  512. $data = $this->getData(tmgmt_ensure_keys_array($key));
  513. if (empty($data['#status']) || $data['#status'] != TMGMT_DATA_ITEM_STATE_ACCEPTED) {
  514. // In case the origin is not set consider it to be remote.
  515. if (!isset($translation['#origin'])) {
  516. $translation['#origin'] = 'remote';
  517. }
  518. // If we already have a translation text and it hasn't changed, don't
  519. // update anything unless the origin is remote.
  520. if (!empty($data['#translation']['#text']) && $data['#translation']['#text'] == $translation['#text'] && $translation['#origin'] != 'remote') {
  521. return;
  522. }
  523. // In case the timestamp is not set consider it to be now.
  524. if (!isset($translation['#timestamp'])) {
  525. $translation['#timestamp'] = REQUEST_TIME;
  526. }
  527. // If we have a translation text and is different from new one create
  528. // revision.
  529. if (!empty($data['#translation']['#text']) && $data['#translation']['#text'] != $translation['#text']) {
  530. // Copy into $translation existing revisions.
  531. if (!empty($data['#translation']['#text_revisions'])) {
  532. $translation['#text_revisions'] = $data['#translation']['#text_revisions'];
  533. }
  534. // If current translation was created locally and the incoming one is
  535. // remote, do not override the local, just create a new revision.
  536. if (isset($data['#translation']['#origin']) && $data['#translation']['#origin'] == 'local' && $translation['#origin'] == 'remote') {
  537. $translation['#text_revisions'][] = array(
  538. '#text' => $translation['#text'],
  539. '#origin' => $translation['#origin'],
  540. '#timestamp' => $translation['#timestamp'],
  541. );
  542. $this->addMessage('Translation for customized @key received. Revert your changes if you wish to use it.', array('@key' => tmgmt_ensure_keys_string($key)));
  543. // Unset text and origin so that the current translation does not
  544. // get overridden.
  545. unset($translation['#text'], $translation['#origin'], $translation['#timestamp']);
  546. }
  547. // Do the same if the translation was already reviewed and origin is
  548. // remote.
  549. elseif ($translation['#origin'] == 'remote' && !empty($data['#status']) && $data['#status'] == TMGMT_DATA_ITEM_STATE_REVIEWED) {
  550. $translation['#text_revisions'][] = array(
  551. '#text' => $translation['#text'],
  552. '#origin' => $translation['#origin'],
  553. '#timestamp' => $translation['#timestamp'],
  554. );
  555. $this->addMessage('Translation for already reviewed @key received and stored as a new revision. Revert to it if you wish to use it.', array('@key' => tmgmt_ensure_keys_string($key)));
  556. // Unset text and origin so that the current translation does not
  557. // get overridden.
  558. unset($translation['#text'], $translation['#origin'], $translation['#timestamp']);
  559. }
  560. else {
  561. $translation['#text_revisions'][] = array(
  562. '#text' => $data['#translation']['#text'],
  563. '#origin' => isset($data['#translation']['#origin']) ? $data['#translation']['#origin'] : 'remote',
  564. '#timestamp' => isset($data['#translation']['#timestamp']) ? $data['#translation']['#timestamp'] : $this->changed,
  565. );
  566. // Add a message if the translation update is from remote.
  567. if ($translation['#origin'] == 'remote') {
  568. $diff = drupal_strlen($translation['#text']) - drupal_strlen($data['#translation']['#text']);
  569. $this->addMessage('Updated translation for key @key, size difference: @diff characters.', array('@key' => tmgmt_ensure_keys_string($key), '@diff' => $diff));
  570. }
  571. }
  572. }
  573. $values = array(
  574. '#translation' => $translation,
  575. '#status' => TMGMT_DATA_ITEM_STATE_TRANSLATED,
  576. );
  577. $this->updateData($key, $values);
  578. }
  579. return;
  580. }
  581. foreach (element_children($translation) as $item) {
  582. $this->addTranslatedDataRecursive($translation[$item], array_merge($key, array($item)));
  583. }
  584. }
  585. /**
  586. * Reverts data item translation to the latest existing revision.
  587. *
  588. * @param array $key
  589. * Data item key that should be reverted.
  590. *
  591. * @return bool
  592. * Result of the revert action.
  593. */
  594. public function dataItemRevert(array $key) {
  595. $data = $this->getData($key);
  596. if (!empty($data['#translation']['#text_revisions'])) {
  597. $prev_revision = end($data['#translation']['#text_revisions']);
  598. $data['#translation']['#text_revisions'][] = array(
  599. '#text' => $data['#translation']['#text'],
  600. '#timestamp' => $data['#translation']['#timestamp'],
  601. '#origin' => $data['#translation']['#origin'],
  602. );
  603. $data['#translation']['#text'] = $prev_revision['#text'];
  604. $data['#translation']['#origin'] = $prev_revision['#origin'];
  605. $data['#translation']['#timestamp'] = $prev_revision['#timestamp'];
  606. $this->updateData($key, $data);
  607. $this->addMessage('Translation for @key reverted to the latest version.', array('@key' => tmgmt_ensure_keys_string($key)));
  608. return TRUE;
  609. }
  610. return FALSE;
  611. }
  612. /**
  613. * Updates the values for a specific substructure in the data array.
  614. *
  615. * The values are either set or updated but never deleted.
  616. *
  617. * @param $key
  618. * Key pointing to the item the values should be applied.
  619. * The key can be either be an array containing the keys of a nested array
  620. * hierarchy path or a string with '][' or '|' as delimiter.
  621. * @param $values
  622. * Nested array of values to set.
  623. */
  624. public function updateData($key, $values = array()) {
  625. foreach ($values as $index => $value) {
  626. // In order to preserve existing values, we can not aplly the values array
  627. // at once. We need to apply each containing value on its own.
  628. // If $value is an array we need to advance the hierarchy level.
  629. if (is_array($value)) {
  630. $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
  631. }
  632. // Apply the value.
  633. else {
  634. drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
  635. }
  636. }
  637. }
  638. /**
  639. * Adds translated data to a job item.
  640. *
  641. * This function calls for TMGMTJobItem::addTranslatedDataRecursive() which
  642. * sets the status of each added data item to TMGMT_DATA_ITEM_STATE_TRANSLATED.
  643. *
  644. * Following rules apply while adding translated data:
  645. *
  646. * 1) Updated are only items that are changed. In case there is local
  647. * modification the translation is added as a revision with a message stating
  648. * this fact.
  649. *
  650. * 2) Merging happens at the data items level, so updating only those that are
  651. * changed. If a data item is in review/reject status and is being updated
  652. * with translation originating from remote the status is updated to
  653. * 'translated' no matter if it is changed or not.
  654. *
  655. * 3) Each time a data item is updated the previous translation becomes a
  656. * revision.
  657. *
  658. * If all data items are translated, the status of the job item is updated to
  659. * needs review.
  660. *
  661. * @todo
  662. * To update the job item status to needs review we could take advantage of
  663. * the TMGMTJobItem::getCountPending() and TMGMTJobItem::getCountTranslated().
  664. * The catch is, that this counter gets updated while saveing which not yet
  665. * hapened.
  666. *
  667. * @param $translation
  668. * Nested array of translated data. Can either be a single text entry, the
  669. * whole data structure or parts of it.
  670. * @param $key
  671. * (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
  672. * one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
  673. * means that it will replace the whole translated data array.
  674. */
  675. public function addTranslatedData($translation, $key = array()) {
  676. $this->addTranslatedDataRecursive($translation, $key);
  677. // Check if the job item has all the translated data that it needs now.
  678. // Only attempt to change the status to needs review if it is currently
  679. // active.
  680. if ($this->isActive()) {
  681. $data = tmgmt_flatten_data($this->getData());
  682. $data = array_filter($data, '_tmgmt_filter_data');
  683. $finished = TRUE;
  684. foreach ($data as $item) {
  685. if (empty($item['#status']) || $item['#status'] == TMGMT_DATA_ITEM_STATE_PENDING) {
  686. $finished = FALSE;
  687. break;
  688. }
  689. }
  690. if ($finished) {
  691. // There are no unfinished elements left.
  692. if ($this->getJob()->getTranslator()->getSetting('auto_accept')) {
  693. // If the job item is going to be auto-accepted, set to review without
  694. // a message.
  695. $this->needsReview(FALSE);
  696. }
  697. else {
  698. // Otherwise, create a message that contains source label, target
  699. // language and links to the review form.
  700. $uri = $this->uri();
  701. $job_uri = $this->getJob()->uri();
  702. $variables = array(
  703. '!source' => l($this->getSourceLabel(), $uri['path']),
  704. '@language' => entity_metadata_wrapper('tmgmt_job', $this->getJob())->target_language->label(),
  705. '!review_url' => url($uri['path'], array('query' => array('destination' => $job_uri['path']))),
  706. );
  707. $this->needsReview('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $variables);
  708. }
  709. }
  710. }
  711. $this->save();
  712. }
  713. /**
  714. * Propagates the returned job item translations to the sources.
  715. *
  716. * @return boolean
  717. * TRUE if we were able to propagate the translated data and the item could
  718. * be saved, FALSE otherwise.
  719. */
  720. public function acceptTranslation() {
  721. if (!$this->isNeedsReview() || !$controller = $this->getSourceController()) {
  722. return FALSE;
  723. }
  724. // We don't know if the source plugin was able to save the translation after
  725. // this point. That means that the plugin has to set the 'accepted' states
  726. // on its own.
  727. $controller->saveTranslation($this);
  728. }
  729. /**
  730. * Returns all job messages attached to this job item.
  731. *
  732. * @return array
  733. * An array of translation job messages.
  734. */
  735. public function getMessages($conditions = array()) {
  736. $query = new EntityFieldQuery();
  737. $query->entityCondition('entity_type', 'tmgmt_message');
  738. $query->propertyCondition('tjiid', $this->tjiid);
  739. foreach ($conditions as $key => $condition) {
  740. if (is_array($condition)) {
  741. $operator = isset($condition['operator']) ? $condition['operator'] : '=';
  742. $query->propertyCondition($key, $condition['value'], $operator);
  743. }
  744. else {
  745. $query->propertyCondition($key, $condition);
  746. }
  747. }
  748. $results = $query->execute();
  749. if (!empty($results['tmgmt_message'])) {
  750. return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
  751. }
  752. return array();
  753. }
  754. /**
  755. * Retrieves all siblings of this job item.
  756. *
  757. * @return array
  758. * An array of job items that are the siblings of this job item.
  759. */
  760. public function getSiblings() {
  761. $query = new EntityFieldQuery();
  762. $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
  763. ->propertyCondition('tjiid', $this->tjiid, '<>')
  764. ->propertyCondition('tjid', $this->tjid)
  765. ->execute();
  766. if (!empty($result['tmgmt_job_item'])) {
  767. return entity_load('tmgmt_job_item', array_keys($result['tmgmt_job_item']));
  768. }
  769. return FALSE;
  770. }
  771. /**
  772. * Returns all job messages attached to this job item with timestamp newer
  773. * than $time.
  774. *
  775. * @param $timestamp
  776. * (Optional) Messages need to have a newer timestamp than $time. Defaults
  777. * to REQUEST_TIME.
  778. *
  779. * @return array
  780. * An array of translation job messages.
  781. */
  782. public function getMessagesSince($time = NULL) {
  783. $time = isset($time) ? $time : REQUEST_TIME;
  784. $conditions = array('created' => array('value' => $time, 'operator' => '>='));
  785. return $this->getMessages($conditions);
  786. }
  787. /**
  788. * Adds remote mapping entity to this job item.
  789. *
  790. * @param string $data_item_key
  791. * Job data item key.
  792. * @param int $remote_identifier_1
  793. * Array of remote identifiers. In case you need to save
  794. * remote_identifier_2/3 set it into $mapping_data argument.
  795. * @param array $mapping_data
  796. * Additional data to be added.
  797. *
  798. * @return int|bool
  799. * @throws TMGMTException
  800. */
  801. public function addRemoteMapping($data_item_key = NULL, $remote_identifier_1 = NULL, $mapping_data = array()) {
  802. if (empty($remote_identifier_1) && !isset($mapping_data['remote_identifier_2']) && !isset($remote_mapping['remote_identifier_3'])) {
  803. throw new TMGMTException('Cannot create remote mapping without remote identifier.');
  804. }
  805. $data = array(
  806. 'tjid' => $this->tjid,
  807. 'tjiid' => $this->tjiid,
  808. 'data_item_key' => $data_item_key,
  809. 'remote_identifier_1' => $remote_identifier_1,
  810. );
  811. if (!empty($mapping_data)) {
  812. $data += $mapping_data;
  813. }
  814. $remote_mapping = entity_create('tmgmt_remote', $data);
  815. return entity_get_controller('tmgmt_remote')->save($remote_mapping);
  816. }
  817. /**
  818. * Gets remote mappings for current job item.
  819. *
  820. * @return array
  821. * List of TMGMTRemote entities.
  822. */
  823. public function getRemoteMappings() {
  824. $query = new EntityFieldQuery();
  825. $query->entityCondition('entity_type', 'tmgmt_remote');
  826. $query->propertyCondition('tjiid', $this->tjiid);
  827. $result = $query->execute();
  828. if (isset($result['tmgmt_remote'])) {
  829. return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
  830. }
  831. return array();
  832. }
  833. /**
  834. * Gets language code of the job item source.
  835. *
  836. * @return string
  837. * Language code.
  838. */
  839. public function getSourceLangCode() {
  840. return $this->getSourceController()->getSourceLangCode($this);
  841. }
  842. /**
  843. * Gets existing translation language codes of the job item source.
  844. *
  845. * @return array
  846. * Array of language codes.
  847. */
  848. public function getExistingLangCodes() {
  849. return $this->getSourceController()->getExistingLangCodes($this);
  850. }
  851. /**
  852. * Recalculate statistical word-data: pending, translated, reviewed, accepted.
  853. */
  854. public function recalculateStatistics() {
  855. // Set translatable data from the current entity to calculate words.
  856. if (empty($this->data)) {
  857. $this->data = $this->getSourceData();
  858. }
  859. // Consider everything accepted when the job item is accepted.
  860. if ($this->isAccepted()) {
  861. $this->count_pending = 0;
  862. $this->count_translated = 0;
  863. $this->count_reviewed = 0;
  864. $this->count_accepted = count(array_filter(tmgmt_flatten_data($this->data), '_tmgmt_filter_data'));
  865. }
  866. // Count the data item states.
  867. else {
  868. // Reset counter values.
  869. $this->count_pending = 0;
  870. $this->count_translated = 0;
  871. $this->count_reviewed = 0;
  872. $this->count_accepted = 0;
  873. $this->word_count = 0;
  874. $this->count($this->data);
  875. }
  876. }
  877. /**
  878. * Parse all data items recursively and sums up the counters for
  879. * accepted, translated and pending items.
  880. *
  881. * @param $item
  882. * The current data item.
  883. */
  884. protected function count(&$item) {
  885. if (!empty($item['#text'])) {
  886. if (_tmgmt_filter_data($item)) {
  887. // Count words of the data item.
  888. $this->word_count += tmgmt_word_count($item['#text']);
  889. // Set default states if no state is set.
  890. if (!isset($item['#status'])) {
  891. // Translation is present.
  892. if (!empty($item['#translation'])) {
  893. $item['#status'] = TMGMT_DATA_ITEM_STATE_TRANSLATED;
  894. }
  895. // No translation present.
  896. else {
  897. $item['#status'] = TMGMT_DATA_ITEM_STATE_PENDING;
  898. }
  899. }
  900. switch ($item['#status']) {
  901. case TMGMT_DATA_ITEM_STATE_REVIEWED:
  902. $this->count_reviewed++;
  903. break;
  904. case TMGMT_DATA_ITEM_STATE_TRANSLATED:
  905. $this->count_translated++;
  906. break;
  907. default:
  908. $this->count_pending++;
  909. break;
  910. }
  911. }
  912. }
  913. elseif (is_array($item)) {
  914. foreach (element_children($item) as $key) {
  915. $this->count($item[$key]);
  916. }
  917. }
  918. }
  919. }