tripal_bulk_loader.constants.inc 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <?php
  2. /**
  3. * @file
  4. * Manages the constants form added to the tripal bulk loader node form
  5. *
  6. * @ingroup tripal_bulk_loader
  7. */
  8. /**
  9. * Inserts/Updates a tripal bulk loading job constant
  10. *
  11. * @param $nid
  12. * The node ID of the the tripal bulk loading job the constant is associated
  13. * with
  14. * @param $table
  15. * The chado table the constant is associated with
  16. * @param $field
  17. * The chado field the constant is associated with
  18. * @param $record_id
  19. * The index in the template array for this record
  20. * @param $field_id
  21. * The index in the template array for this field
  22. *
  23. * NOTE: $template_array[$record_id]['table'] = $table and
  24. * $template_array[$record_id]['fields'][$field_id]['field'] = $field both
  25. * are included as a means of double-checking the constant still is still in
  26. * thesame place in the template array. For example, that the template was
  27. * not edited and the records moved around after the job was submitted but
  28. * before it was run.
  29. *
  30. * @return
  31. * On success it returns the object (with primary key if inserted);
  32. * on failure it returns FALSE
  33. *
  34. * @ingroup tripal_bulk_loader
  35. */
  36. function tripal_bulk_loader_update_constant($nid, $group_id, $table, $field, $record_id, $field_id, $value) {
  37. $record = [
  38. 'nid' => $nid,
  39. 'group_id' => $group_id,
  40. 'chado_table' => $table,
  41. 'chado_field' => $field,
  42. 'record_id' => $record_id,
  43. 'field_id' => $field_id,
  44. 'value' => $value,
  45. ];
  46. // Check to see if already exists
  47. $exists = db_select('tripal_bulk_loader_constants', 'c')
  48. ->fields('c', ['constant_id'])
  49. ->condition('nid', $record['nid'])
  50. ->condition('record_id', $record['record_id'])
  51. ->condition('field_id', $record['field_id'])
  52. ->condition('group_id', $record['group_id'])
  53. ->execute();
  54. $exists = $exists->fetchObject();
  55. if (!isset($exists)) {
  56. $record['constant_id'] = $exists->constant_id;
  57. $status = drupal_write_record('tripal_bulk_loader_constants', $record, 'constant_id');
  58. if ($status) {
  59. return $record;
  60. }
  61. else {
  62. return FALSE;
  63. }
  64. }
  65. else {
  66. $status = drupal_write_record('tripal_bulk_loader_constants', $record);
  67. if ($status) {
  68. return $record;
  69. }
  70. else {
  71. return FALSE;
  72. }
  73. }
  74. }
  75. /**
  76. * Check if a bulk loading job has exposed constants
  77. *
  78. * @ingroup tripal_bulk_loader
  79. */
  80. function tripal_bulk_loader_has_exposed_fields($node) {
  81. // exposed fields isn't set
  82. if (!isset($node->exposed_fields)) {
  83. return FALSE;
  84. }
  85. // exposed fields has at least one element
  86. if (sizeof($node->exposed_fields) == 1) {
  87. // need to check if single element is an empty array
  88. $element = reset($node->exposed_fields);
  89. if ($element) {
  90. return TRUE;
  91. }
  92. else {
  93. return FALSE;
  94. }
  95. }
  96. elseif (sizeof($node->exposed_fields) > 1) {
  97. return TRUE;
  98. }
  99. else {
  100. return FALSE;
  101. }
  102. return FALSE;
  103. }
  104. ///////////////////////////////////////////////////////////
  105. // Set Constants Form (on Bulk Loader Node)
  106. ///////////////////////////////////////////////////////////
  107. /**
  108. * Set constants (exposed fields in template)
  109. *
  110. * @param $form_state
  111. * The current state of the form
  112. * @param $node
  113. * The node to set constants for
  114. *
  115. * @return
  116. * A form array to be rendered by drupal_get_form()
  117. *
  118. * @ingroup tripal_bulk_loader
  119. */
  120. function tripal_bulk_loader_set_constants_form($form, &$form_state, $node) {
  121. $form = [];
  122. $form['nid'] = [
  123. '#type' => 'hidden',
  124. '#value' => $node->nid,
  125. ];
  126. $form['#attached']['css'] = [
  127. drupal_get_path('module', 'tripal_bulk_loader') . '/theme/tripal_bulk_loader.css',
  128. ];
  129. $form_state['node'] = $node;
  130. if (!tripal_bulk_loader_has_exposed_fields($node)) {
  131. return $form;
  132. }
  133. // Check to see if the template has changed since this node was last updated.
  134. // If so we need to remove the constant set since it's no longer valid.
  135. if ($node->template->changed >= $node->changed AND !empty($node->constants)) {
  136. // Save all constants for display to the user.
  137. $form['old_constants'] = [
  138. '#type' => 'fieldset',
  139. '#title' => 'Previous Constant Set',
  140. '#description' => 'This constant set is no longer valid due to changes in
  141. the bulk loading template. As such you will need to re-enter it below.
  142. <strong>Please copy this information somewhere before leaving this page
  143. (including entering a constant set) because it will NOT BE SAVED.</strong>',
  144. '#prefix' => '<div id="expired-constants">',
  145. '#suffix' => '</div>',
  146. ];
  147. $form['old_constants']['table'] = [
  148. '#type' => 'markup',
  149. '#theme' => 'tripal_bulk_loader_constant_set',
  150. '#nid' => $node->nid,
  151. '#constants' => $node->constants,
  152. '#template' => $node->template,
  153. '#options' => ['display_operations' => FALSE],
  154. ];
  155. drupal_set_message('Template has been changed since the constant set was added; therefore,
  156. your constants are no longer valid. Please re-enter them before loading.',
  157. 'warning'
  158. );
  159. // Remove all constants for this node.
  160. db_delete('tripal_bulk_loader_constants')
  161. ->condition('nid', $node->nid)
  162. ->execute();
  163. $node->constants = [];
  164. }
  165. $form['exposed_array'] = [
  166. '#type' => 'hidden',
  167. '#value' => serialize($node->exposed_fields),
  168. ];
  169. $form['exposed_fields'] = [
  170. '#type' => 'fieldset',
  171. '#title' => t('Constant Values'),
  172. '#collapsible' => TRUE,
  173. '#collapsed' => ($node->template_id) ? FALSE : TRUE,
  174. '#prefix' => '<div id="set-constants">',
  175. '#suffix' => '</div>',
  176. ];
  177. // Display table of already added constant sets with the ability to re-arrange and delete
  178. $first_constant = reset($node->constants);
  179. if (sizeof($node->constants) > 0) {
  180. $form['exposed_fields']['explanation-1'] = [
  181. '#type' => 'item',
  182. '#markup' => t('You have already added constants to this bulk loading job. Each '
  183. . 'row in the following table represents a set of constants. Each set will be used '
  184. . 'to load your data file with the specified template resulting in the each record '
  185. . 'in the template to be loaded x number of times where there are x sets of '
  186. . 'constants (rows in the following table).'),
  187. ];
  188. $form['exposed_fields']['existing'] = [
  189. '#type' => 'markup',
  190. '#theme' => 'tripal_bulk_loader_constant_set',
  191. '#nid' => $node->nid,
  192. '#constants' => $node->constants,
  193. '#template' => $node->template,
  194. ];
  195. }
  196. $form['exposed_fields']['new'] = [
  197. '#type' => 'fieldset',
  198. '#title' => t('New set of Constants'),
  199. ];
  200. $form['exposed_fields']['new']['explanation-2'] = [
  201. '#type' => 'item',
  202. '#markup' => t('The following fields are constants in the selected template that you need to set values for.'),
  203. ];
  204. // Add textifelds for exposed fields of the current template
  205. $exposed_fields = FALSE;
  206. $indexes = [];
  207. if (tripal_bulk_loader_has_exposed_fields($node)) {
  208. foreach ($node->exposed_fields as $exposed_id => $exposed_index) {
  209. $record_id = $exposed_index['record_id'];
  210. $field_id = $exposed_index['field_id'];
  211. $field = $node->template->template_array[$record_id]['fields'][$field_id];
  212. if ($field['exposed']) {
  213. $exposed_fields = TRUE;
  214. $indexes[$record_id][] = $field_id;
  215. $default_value = '';
  216. if (isset($node->constants[$record_id])) {
  217. if (isset($node->constants[$record_id][$field_id])) {
  218. $default_value = $node->constants[$exposed_id][$record_id][$field_id]['value'];
  219. }
  220. }
  221. elseif (isset($field['constant value'])) {
  222. $default_value = $field['constant value'];
  223. }
  224. elseif (isset($field['spreadsheet column'])) {
  225. $default_value = $field['spreadsheet column'];
  226. }
  227. switch ($field['type']) {
  228. case 'table field':
  229. $form['exposed_fields']['new'][$record_id . '-' . $field_id] = [
  230. '#type' => 'textfield',
  231. '#title' => t('@title', ['@title' => $field['title']]),
  232. '#description' => t('%exposed_description', ['%exposed_description' => $field['exposed_description']]),
  233. '#default_value' => $default_value,
  234. ];
  235. break;
  236. case 'constant':
  237. $form['exposed_fields']['new'][$record_id . '-' . $field_id] = [
  238. '#type' => 'textfield',
  239. '#title' => t('@title', ['@title' => $field['title']]),
  240. '#description' => t('Enter the case-sensitive value of this constant for your data file'),
  241. '#default_value' => $default_value,
  242. ];
  243. break;
  244. }
  245. $form['exposed_fields']['new'][$record_id . '-' . $field_id . '-table'] = [
  246. '#type' => 'hidden',
  247. '#value' => $node->template->template_array[$record_id]['table'],
  248. ];
  249. $form['exposed_fields']['new'][$record_id . '-' . $field_id . '-field'] = [
  250. '#type' => 'hidden',
  251. '#value' => $field['field'],
  252. ];
  253. $form['exposed_fields']['new'][$record_id . '-' . $field_id . '-type'] = [
  254. '#type' => 'hidden',
  255. '#value' => $field['type'],
  256. ];
  257. }
  258. }
  259. }
  260. $form['template'] = [
  261. '#type' => 'hidden',
  262. '#value' => serialize($node->template->template_array),
  263. ];
  264. $form['exposed_fields']['new']['indexes'] = [
  265. '#type' => 'hidden',
  266. '#value' => serialize($indexes),
  267. ];
  268. if (!$exposed_fields) {
  269. $form['exposed_fields']['new']['explanation'] = [
  270. '#type' => 'item',
  271. '#value' => t('There are no exposed fields for this template.'),
  272. ];
  273. }
  274. $form['exposed_fields']['new']['submit-2'] = [
  275. '#type' => 'submit',
  276. '#name' => 'add_constant',
  277. '#value' => t('Add Constant Set'),
  278. ];
  279. return $form;
  280. }
  281. /**
  282. * Validate that the values entered exist in the database
  283. * if indicated in hte template array
  284. *
  285. * @ingroup tripal_bulk_loader
  286. */
  287. function tripal_bulk_loader_set_constants_form_validate($form, $form_state) {
  288. $template = unserialize($form_state['values']['template']);
  289. $indexes = unserialize($form_state['values']['indexes']);
  290. $op = $form_state['values'][$form_state['clicked_button']['#name']];
  291. if (strcmp('Add Constant Set', $op) == 0) {
  292. foreach ($indexes as $record_id => $array) {
  293. foreach ($array as $field_id) {
  294. if (isset($template[$record_id]['fields'][$field_id]['exposed_validate'])) {
  295. if ($template[$record_id]['fields'][$field_id]['exposed_validate']) {
  296. $result = chado_query(
  297. 'SELECT 1 as valid FROM {' . $template[$record_id]['table'] . '} WHERE ' . $template[$record_id]['fields'][$field_id]['field'] . '=:value',
  298. [':value' => $form_state['values'][$record_id . '-' . $field_id]]
  299. )->fetchField();
  300. if (!$result) {
  301. $msg = 'A ' . $form['exposed_fields']['new'][$record_id . '-' . $field_id]['#title'] . ' of "' . $form['exposed_fields']['new'][$record_id . '-' . $field_id]['#value'] . '" must already exist!';
  302. form_set_error($record_id . '-' . $field_id, $msg);
  303. }
  304. else {
  305. drupal_set_message(
  306. t(
  307. 'Confirmed a %title of "%value" already exists.',
  308. [
  309. '%title' => $form['exposed_fields']['new'][$record_id . '-' . $field_id]['#title'],
  310. '%value' => $form['exposed_fields']['new'][$record_id . '-' . $field_id]['#value'],
  311. ]
  312. )
  313. );
  314. }
  315. }
  316. }
  317. }
  318. }
  319. }
  320. }
  321. /**
  322. * Insert/update the constants associated with this node
  323. *
  324. * @ingroup tripal_bulk_loader
  325. */
  326. function tripal_bulk_loader_set_constants_form_submit($form, $form_state) {
  327. // Insert/Update constants
  328. $template = unserialize($form_state['values']['template']);
  329. $indexes = unserialize($form_state['values']['indexes']);
  330. $op = $form_state['values'][$form_state['clicked_button']['#name']];
  331. if (strcmp('Add Constant Set', $op) == 0) {
  332. $max_group = db_query("SELECT max(group_id) as value FROM {tripal_bulk_loader_constants} WHERE nid=:nid", [':nid' => $form_state['values']['nid']])->fetchObject();
  333. foreach ($indexes as $record_id => $array) {
  334. foreach ($array as $field_id) {
  335. tripal_bulk_loader_update_constant(
  336. $form_state['values']['nid'],
  337. $max_group->value + 1,
  338. $form_state['values'][$record_id . '-' . $field_id . '-table'],
  339. $form_state['values'][$record_id . '-' . $field_id . '-field'],
  340. $record_id,
  341. $field_id,
  342. $form_state['values'][$record_id . '-' . $field_id]
  343. );
  344. }
  345. }
  346. // Update the node so that the constant set isn't immediatly erased...
  347. $node = $form_state['node'];
  348. $node->has_header = $node->file_has_header;
  349. if ($node = node_submit($node)) {
  350. node_save($node);
  351. }
  352. }
  353. }
  354. ///////////////////////////////////////////////////////////
  355. // Set Constants Form (on Bulk Loader Node)
  356. ///////////////////////////////////////////////////////////
  357. /**
  358. * Edit a constant set (exposed fields in template)
  359. *
  360. * @param $form_state
  361. * The current state of the form
  362. * @param $node
  363. * The node to set constants for
  364. * @param $group_id
  365. * The constant set to edit
  366. *
  367. * @return
  368. * A form array to be rendered by drupal_get_form()
  369. *
  370. * @ingroup tripal_bulk_loader
  371. */
  372. function tripal_bulk_loader_edit_constant_set_form($form, &$form_state, $node, $group_id) {
  373. $form = [];
  374. $form['nid'] = [
  375. '#type' => 'hidden',
  376. '#value' => $node->nid,
  377. ];
  378. $form['group_id'] = [
  379. '#type' => 'hidden',
  380. '#value' => $group_id,
  381. ];
  382. $form['explanation'] = [
  383. '#type' => 'item',
  384. '#value' => t('The following fields are constants in the selected template that you need to set values for.'),
  385. ];
  386. // Add textifelds for exposed fields of the current template
  387. $exposed_fields = FALSE;
  388. $indexes = [];
  389. if (tripal_bulk_loader_has_exposed_fields($node)) {
  390. foreach ($node->exposed_fields as $exposed_index) {
  391. $record_id = $exposed_index['record_id'];
  392. $record = $node->template->template_array[$record_id];
  393. $field_id = $exposed_index['field_id'];
  394. $field = $node->template->template_array[$record_id]['fields'][$field_id];
  395. if ($field['exposed']) {
  396. $exposed_fields = TRUE;
  397. $indexes[$record_id][] = $field_id;
  398. switch ($field['type']) {
  399. case 'table field':
  400. $form[$record_id . '-' . $field_id] = [
  401. '#type' => 'textfield',
  402. '#title' => t('%title', ['%title' => $field['title']]),
  403. '#description' => t('%exposed_description', ['%exposed_description' => $field['exposed_description']]),
  404. '#default_value' => (isset($node->constants[$group_id][$record_id][$field_id]['value'])) ? $node->constants[$group_id][$record_id][$field_id]['value'] : $field['constant value'],
  405. ];
  406. break;
  407. case 'constant':
  408. $form[$record_id . '-' . $field_id] = [
  409. '#type' => 'textfield',
  410. '#title' => t('%title', ['%title' => $field['title']]),
  411. '#description' => t('Enter the case-sensitive value of this constant for your data file'),
  412. '#default_value' => (isset($node->constants[$group_id][$record_id][$field_id]['value'])) ? $node->constants[$group_id][$record_id][$field_id]['value'] : $field['constant value'],
  413. ];
  414. break;
  415. }
  416. $form[$record_id . '-' . $field_id . '-table'] = [
  417. '#type' => 'hidden',
  418. '#value' => $record['table'],
  419. ];
  420. $form[$record_id . '-' . $field_id . '-field'] = [
  421. '#type' => 'hidden',
  422. '#value' => $field['field'],
  423. ];
  424. $form[$record_id . '-' . $field_id . '-type'] = [
  425. '#type' => 'hidden',
  426. '#value' => $field['type'],
  427. ];
  428. }
  429. }
  430. }
  431. $form['template'] = [
  432. '#type' => 'hidden',
  433. '#value' => serialize($node->template->template_array),
  434. ];
  435. $form['indexes'] = [
  436. '#type' => 'hidden',
  437. '#value' => serialize($indexes),
  438. ];
  439. $form['save'] = [
  440. '#type' => 'submit',
  441. '#value' => 'Save',
  442. ];
  443. $form['cancel'] = [
  444. '#type' => 'link',
  445. '#title' => 'Cancel',
  446. '#href' => 'node/' . $node->nid,
  447. ];
  448. return $form;
  449. }
  450. /**
  451. * Edit constants in the current constant set
  452. *
  453. * @ingroup tripal_bulk_loader
  454. */
  455. function tripal_bulk_loader_edit_constant_set_form_submit($form, &$form_state) {
  456. // Update constants
  457. $template = unserialize($form_state['values']['template']);
  458. $indexes = unserialize($form_state['values']['indexes']);
  459. $nid = $form_state['values']['nid'];
  460. $op = $form_state['values'][$form_state['clicked_button']['#name']];
  461. if (strcmp('Save', $op) == 0) {
  462. $form_state['redirect'] = 'node/' . $nid;
  463. $form_state['rebuild'] = FALSE;
  464. foreach ($indexes as $record_id => $array) {
  465. foreach ($array as $field_id) {
  466. tripal_bulk_loader_update_constant(
  467. $form_state['values']['nid'],
  468. $form_state['values']['group_id'],
  469. $form_state['values'][$record_id . '-' . $field_id . '-table'],
  470. $form_state['values'][$record_id . '-' . $field_id . '-field'],
  471. $record_id,
  472. $field_id,
  473. $form_state['values'][$record_id . '-' . $field_id]
  474. );
  475. }
  476. }
  477. drupal_set_message(t('The constant set was successfully updated.'));
  478. }
  479. }
  480. /**
  481. * Delete a constant set (exposed fields in template)
  482. *
  483. * @param $form_state
  484. * The current state of the form
  485. * @param $node
  486. * The node to set constants for
  487. * @param $group_id
  488. * The constant set to delete
  489. *
  490. * @return
  491. * A form array to be rendered by drupal_get_form()
  492. *
  493. * @ingroup tripal_bulk_loader
  494. */
  495. function tripal_bulk_loader_delete_constant_set_form($form, &$form_state, $node, $group_id) {
  496. $form = [];
  497. $form['nid'] = [
  498. '#type' => 'value',
  499. '#value' => $node->nid,
  500. ];
  501. $form['group_id'] = [
  502. '#type' => 'hidden',
  503. '#value' => $group_id,
  504. ];
  505. return confirm_form($form,
  506. t('Are you sure you want to delete this constant set?'),
  507. 'node/' . $node->nid,
  508. t('This action cannot be undone.'),
  509. t('Delete'),
  510. t('Cancel')
  511. );
  512. }
  513. /**
  514. * Delete the current constant set
  515. *
  516. * @ingroup tripal_bulk_loader
  517. */
  518. function tripal_bulk_loader_delete_constant_set_form_submit($form, &$form_state) {
  519. $group_id = $form_state['values']['group_id'];
  520. $nid = $form_state['values']['nid'];
  521. if ($nid && $form_state['values']['confirm']) {
  522. $form_state['redirect'] = 'node/' . $nid;
  523. $form_state['rebuild'] = FALSE;
  524. db_delete('tripal_bulk_loader_constants')
  525. ->condition('nid', $nid)
  526. ->condition('group_id', $group_id)
  527. ->execute();
  528. drupal_set_message(t('Constant set successfully deleted.'));
  529. }
  530. }
  531. /**
  532. * Display a constant set.
  533. *
  534. * @param $varaibles
  535. * An array of variables that are available:
  536. * -nid: the NID of the bulk loading job node the constants are for.
  537. * -constants: An array of constants as loaded by tripal_bulk_loader_load().
  538. * -template: An object containing the template record with unserialized
  539. * template_array.
  540. * -options: An optional array of options.
  541. *
  542. * @return
  543. * Rendered HTML.
  544. */
  545. function theme_tripal_bulk_loader_constant_set($variables) {
  546. // Check that there are constants to render.
  547. if (empty($variables['constants'])) {
  548. return '';
  549. }
  550. // Set Defaults.
  551. $variables['options'] = (isset($variables['options'])) ? $variables['options'] : [];
  552. $variables['options']['display_operations'] = (isset($variables['options']['display_operations'])) ?
  553. $variables['options']['display_operations'] : TRUE;
  554. // Get all the rows.
  555. $rows = [];
  556. foreach ($variables['constants'] as $group_id => $set) {
  557. $row = [];
  558. $row['group_id'] = $group_id;
  559. foreach ($set as $record) {
  560. foreach ($record as $field) {
  561. $index = $field['record_id'] . '-' . $field['field_id'];
  562. $row[$index] = $field['value'];
  563. }
  564. }
  565. if ($variables['options']['display_operations']) {
  566. $row['operations'] = filter_xss(l(t('Edit'), 'node/' . $variables['nid'] . '/constants/' . $group_id . '/edit') . ' | ' .
  567. l(t('Delete'), 'node/' . $variables['nid'] . '/constants/' . $group_id . '/delete'));
  568. }
  569. $rows[] = $row;
  570. }
  571. // Get the header using the last constant set.
  572. $header = [];
  573. $header[0]['group_id'] = [
  574. 'data' => t('Group'),
  575. 'header' => TRUE,
  576. 'rowspan' => 2,
  577. ];
  578. foreach ($set as $record) {
  579. foreach ($record as $field) {
  580. $index = $field['record_id'] . '-' . $field['field_id'];
  581. $header[0][$field['record_id']] = [
  582. 'data' => $variables['template']->template_array[$field['record_id']]['record_id'],
  583. 'header' => TRUE,
  584. 'colspan' => sizeof($record),
  585. ];
  586. $header[1][$index] = [
  587. 'data' => $variables['template']->template_array[$field['record_id']]['fields'][$field['field_id']]['title'],
  588. 'header' => TRUE,
  589. ];
  590. }
  591. }
  592. if ($variables['options']['display_operations']) {
  593. $header[0]['operations'] = [
  594. 'data' => t('Operations'),
  595. 'header' => TRUE,
  596. 'rowspan' => 2,
  597. ];
  598. }
  599. return theme(
  600. 'table',
  601. [
  602. 'header' => [],
  603. 'rows' => array_merge($header, $rows),
  604. ]
  605. );
  606. }