tripal_bulk_loader.constants.inc 21 KB

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