From d5f9ff4aa91c7c968de7cd2d4aff9555a6e3b67d Mon Sep 17 00:00:00 2001 From: reynold tan Date: Wed, 22 Dec 2021 14:00:46 -0600 Subject: [PATCH 01/16] Creates rawphenotypes germplasm field --- .../ncit__raw_data/ncit__raw_data.inc | 276 ++ .../ncit__raw_data_formatter.inc | 157 + .../ncit__raw_data/ncit__raw_data_widget.inc | 44 + .../ncit__raw_data/theme/icon-download.jpg | Bin 0 -> 19226 bytes .../ncit__raw_data/theme/icon-export.png | Bin 0 -> 48259 bytes .../ncit__raw_data/theme/icon-export2.png | Bin 0 -> 47587 bytes .../ncit__raw_data/theme/icon-leaf.png | Bin 0 -> 47614 bytes .../ncit__raw_data/theme/icon-raw.jpg | Bin 0 -> 14860 bytes includes/TripalFields/rawpheno.fields.inc | 123 + {include => includes}/rawpheno.admin.form.inc | 2 +- .../rawpheno.backup.form.inc | 0 .../rawpheno.download.form.inc | 770 ++--- .../rawpheno.function.measurements.inc | 0 .../rawpheno.instructions.form.inc | 1012 +++---- .../rawpheno.rawdata.form.inc | 256 +- .../rawpheno.tripaldownload.inc | 4 +- .../rawpheno.upload.excel.inc | 2542 ++++++++--------- .../rawpheno.upload.form.inc | 2520 ++++++++-------- .../rawpheno.upload.helpers.inc | 138 +- {include => includes}/rawpheno.validation.inc | 0 rawpheno.install | 2 +- rawpheno.module | 56 +- theme/css/rawpheno.germplasm.field.css | 152 + theme/js/rawpheno.germplasm.field.js | 70 + 24 files changed, 4502 insertions(+), 3622 deletions(-) create mode 100644 includes/TripalFields/ncit__raw_data/ncit__raw_data.inc create mode 100644 includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc create mode 100644 includes/TripalFields/ncit__raw_data/ncit__raw_data_widget.inc create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-download.jpg create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-export.png create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-export2.png create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-leaf.png create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg create mode 100644 includes/TripalFields/rawpheno.fields.inc rename {include => includes}/rawpheno.admin.form.inc (99%) rename {include => includes}/rawpheno.backup.form.inc (100%) rename {include => includes}/rawpheno.download.form.inc (89%) rename {include => includes}/rawpheno.function.measurements.inc (100%) rename {include => includes}/rawpheno.instructions.form.inc (97%) mode change 100755 => 100644 rename {include => includes}/rawpheno.rawdata.form.inc (97%) mode change 100755 => 100644 rename {include => includes}/rawpheno.tripaldownload.inc (99%) rename {include => includes}/rawpheno.upload.excel.inc (97%) mode change 100755 => 100644 rename {include => includes}/rawpheno.upload.form.inc (97%) mode change 100755 => 100644 rename {include => includes}/rawpheno.upload.helpers.inc (96%) mode change 100755 => 100644 rename {include => includes}/rawpheno.validation.inc (100%) create mode 100644 theme/css/rawpheno.germplasm.field.css create mode 100644 theme/js/rawpheno.germplasm.field.js diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc new file mode 100644 index 0000000..adb4fe4 --- /dev/null +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -0,0 +1,276 @@ + 'tripal_no_storage', + // It is expected that all fields set a 'value' in the load() function. + // In many cases, the value may be an associative array of key/value pairs. + // In order for Tripal to provide context for all data, the keys should + // be a controlled vocabulary term (e.g. rdfs:type). Keys in the load() + // function that are supported by the query() function should be + // listed here. + 'browseable_keys' => array(), + ); + + // Provide a list of instance specific settings. These can be access within + // the instanceSettingsForm. When the instanceSettingsForm is submitted + // then Drupal with automatically change these settings for the instance. + // It is recommended to put settings at the instance level whenever possible. + // If you override this variable in a child class be sure to replicate the + // term_name, term_vocab, term_accession and term_fixed keys as these are + // required for all TripalFields. + public static $default_instance_settings = array( + // The short name for the vocabulary (e.g. schema, SO, GO, PATO, etc.). + 'term_vocabulary' => 'NCIT', + // The name of the term. + 'term_name' => 'Raw Data', + // The unique ID (i.e. accession) of the term. + 'term_accession' => 'C142663', + // Set to TRUE if the site admin is not allowed to change the term + // type, otherwise the admin can change the term mapped to a field. + 'term_fixed' => FALSE, + // Indicates if this field should be automatically attached to display + // or web services or if this field should be loaded separately. This + // is convenient for speed. Fields that are slow should for loading + // should have auto_attach set to FALSE so tha their values can be + // attached asynchronously. + 'auto_attach' => FALSE, + // The table where the options for this specific field are stored. + // This can be one of trpfancy_browse_options or trpfancy_browse_options_per_entity + // based on admin configuration. Default: trpfancy_browse_options. + 'option_storage' => '', + // A list of browser types this field intends to provide. + 'browser_types' => '', + ); + + // A boolean specifying that users should not be allowed to create + // fields and instances of this field type through the UI. Such + // fields can only be created programmatically with field_create_field() + // and field_create_instance(). + public static $no_ui = FALSE; + // A boolean specifying that the field will not contain any data. This + // should exclude the field from web services or downloads. An example + // could be a quick browse field that appears on the page that redirects + // the user but otherwise provides no data. + public static $no_data = TRUE; + + /** + * Loads the field values from the underlying data store. + * + * @param $entity + * + * @return + * An array of the following format: + * $entity->{$field_name}['und'][0]['value'] = $value; + * where: + * - $entity is the entity object to which this field is attached. + * - $field_name is the name of this field + * - 'und' is the language code (in this case 'und' == undefined) + * - 0 is the cardinality. Increment by 1 when more than one item is + * available. + * - 'value' is the key indicating the value of this field. It should + * always be set. The value of the 'value' key will be the contents + * used for web services and for downloadable content. The value + * should be of the follow format types: 1) A single value (text, + * numeric, etc.) 2) An array of key value pair. 3) If multiple entries + * then cardinality should incremented and format types 1 and 2 should + * be used for each item. + * The array may contain as many other keys at the same level as 'value' + * but those keys are for internal field use and are not considered the + * value of the field. + * + * + */ + public function load($entity) { + // Arrays to hold phenotypes related to this germplasm. + $germplasm = array(); + $icons = array(); + $summary = array(); + $current_user = array(); + $traits = array(); + + // User profile. + global $user; + $current_user['id'] = $user->uid; + + // User permissions. + $rawpheno_permission = array('access rawpheno', 'download rawpheno'); + $count_permission = 0; + foreach($rawpheno_permission as $permission) { + if (user_access($permission, $user)) { + $count_permission++; + } + } + + // If user has no permission to begin with, skip all and report + // raw phenotypes not available. + $field_name = $this->instance['field_name']; + $entity->{$field_name}['und'][0]['value'] = array(); + + if ($count_permission == 2 || user_is_logged_in()) { + // User appointed experiments. + // See api library in includes directory for this function definition. + $user_experiment = rawpheno_function_user_project($user->uid); + $user_experiment = array_keys($user_experiment); + + // Current user. + $current_user['permission'] = TRUE; + $current_user['experiments'] = $user_experiment; + + // Germplasm. + $germplasm['id'] = $entity->chado_record->stock_id; + $germplasm['name'] = $entity->chado_record->name; + + // Icons. + $icon_path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno') . '/includes/TripalFields/ncit__raw_data/theme/'; + + $icons['leaf'] = $icon_path . 'icon-leaf.png'; + $icons['export'] = $icon_path . 'icon-export.png'; + $icons['raw'] = $icon_path . 'icon-raw.jpg'; + $icons['download'] = $icon_path . 'icon-download.jpg'; + + $traits = []; + // This query is identical to the rawphenotypes download page. + // Get all experiments (by plant id) where germplasm was used. + $all_experiment_locations = chado_query(" + SELECT p2.project_id, p2.name, value AS location + FROM pheno_plantprop + INNER JOIN pheno_plant_project AS p1 USING (plant_id) + INNER JOIN {project} AS p2 ON p1.project_id = p2.project_id + WHERE + type_id = (SELECT cvterm_id FROM {cvterm} cvt LEFT JOIN {cv} cv ON cv.cv_id = cvt.cv_id + WHERE cvt.name = 'Location' AND cv.name = 'phenotype_plant_property_types') AND + plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :germplasm GROUP BY plant_id) + GROUP BY p2.project_id, p2.name, value + ", array(':germplasm' => $germplasm['id'])); + $experiment_locations = $all_experiment_locations->fetchAll(); + + // All traits in experiment and location. + $sql_cvterm = " + SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( + SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( + SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( + SELECT string_agg(DISTINCT all_traits, ',') AS all_traits + FROM {rawpheno_rawdata_mview} + WHERE + location IN(:location) + AND plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) + ) AS list_id + )::int[]) + ) AS c_j + WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') + ORDER BY c_j.cvterm_json->>'name' ASC + "; + + $trait_experiment_location = array(); + $cache_exp = array(); + $cache_loc = array(); + foreach($experiment_locations as $item) { + $cache_exp[] = $item->project_id; + $cache_loc[] = $item->location; + + $trait_set = chado_query($sql_cvterm, array(':location' => $item->location, ':project_id' => $item->project_id)) + ->fetchAllKeyed(0, 1); + + if ($trait_set) { + foreach($trait_set as $trait_id => $trait_name) { + // Trait id, project id and name + location: + $trait_experiment_location[ $trait_id . '_' . $trait_name ][] = $trait_id . '#' . $item->project_id . '#' . $item->name . '#' . $item->location; + } + } + } + + ksort($trait_experiment_location); + + // Summarize. + $summary['traits'] = count($trait_experiment_location); + $summary['experiments'] = count(array_unique($cache_exp)); + $summary['locations'] = count(array_unique($cache_loc)); + + $entity->{$field_name}['und'][0]['value']['NCIT:Raw Data'] = array( + 'user' => $current_user, + 'germplasm' => $germplasm, + 'summary' => $summary, + 'traits' => $trait_experiment_location, + 'icons' => $icons + ); + } + } + + /** + * Provides a form for the 'Field Settings' of an instance of this field. + * + * This function corresponds to the hook_field_instance_settings_form() + * function of the Drupal Field API. + * + * Validation of the instance settings form is not supported by Drupal, but + * the TripalField class does provide a mechanism for supporting validation. + * To allow for validation of your setting form you must call the parent + * in your child class: + * + * @code + * $element = parent::instanceSettingsForm(); + * @endcode + * + * Please note, the form generated with this function does not easily + * support AJAX calls in the same way that other Drupal forms do. If you + * need to use AJAX you must manually alter the $form in your ajax call. + * The typical way to handle updating the form via an AJAX call is to make + * the changes in the form function itself but that doesn't work here. + */ + public function instanceSettingsForm() { + + // Retrieve the current settings. + // If this field was just created these will contain the default values. + $settings = $this->instance['settings']; + + // Allow the parent Tripal Field to set up the form element for us. + $element = parent::instanceSettingsForm(); + + return $element; + } + + /** + * @see ChadoField::elementInfo() + * + */ + public function elementInfo() { + $field_term = $this->getFieldTermID(); + return array( + $field_term => array( + 'operations' => array('eq', 'ne', 'contains', 'starts'), + 'sortable' => TRUE, + 'searchable' => TRUE, + ), + ); + } +} \ No newline at end of file diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc new file mode 100644 index 0000000..5f420de --- /dev/null +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -0,0 +1,157 @@ + +
+
+   What are Raw Phenotypes? +
+
+ +
+
 
+
+ + + +

' . $germplasm_raw_phenotypes['germplasm']['name'] . ': %d Traits / %d Experiments / %d Locations

+ +
Please note that some experiments appear disabled. Please contact KnowPulse if you need access.
+ +
+
+
%s
+
+
+ +
*Data export will launch a new window
+ '; + $icon_img = 'Download Raw Phenotypic Data'; + + $response = ''; + + if ($germplasm_raw_phenotypes['user']['permission']) { + // Export summary table. + $table_row = array(); + + $id = 0; + foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { + list($trait_id, $trait_name) = explode('_', $trait); + + $select = $this->create_select($exp_loc, $germplasm_raw_phenotypes['user']['experiments']); + $table_row[ $id ] = array(sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $germplasm_raw_phenotypes['icons']['export'])); + $id++; + } + + // Create markup. + $summary_table = theme('table', array( + 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['download'])), + 'rows' => $table_row, + 'sticky' => FALSE, + 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table') + )); + + $response = sprintf($markup, + $germplasm_raw_phenotypes['summary']['traits'], + $germplasm_raw_phenotypes['summary']['experiments'], + $germplasm_raw_phenotypes['summary']['locations'], + $summary_table + ); + + // Render germplasm raw phenotypes. + $element[0] = array( + '#type' => 'markup', + '#markup' => $response, + ); + } + } + + return $element; + } + + /** + * Create select field. + * + * @param $id + * Id attribute of the final select field. + * @param $items + * Associative array, where each item will be rendered as an option + * with key as the value and value as text. + * @param $disable + * Array of items to match an item if it should be disabled. + */ + public function create_select($items, $disable) { + $option = array(); + $cache_exp = []; + foreach($items as $loc_exp) { + list($trait_id, $project_id, $project_name, $location) = explode('#', $loc_exp); + $select_value = $trait_id . '#' . $project_id . '#' . $location; + $cache_exp[] = $project_id; + + $disabled = (in_array($project_id, $disable)) ? '' : 'disabled'; + $option[] = ''; + } + + $select = ''; + + return sprintf($select, implode('', $option)); + } +} + diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_widget.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_widget.inc new file mode 100644 index 0000000..923fd48 --- /dev/null +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_widget.inc @@ -0,0 +1,44 @@ +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 !{rD!yW*w95J~kt2?T|% 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 Q|LK#o*H-HeC+C&^1GQ`Rng9R* literal 0 HcmV?d00001 diff --git a/includes/TripalFields/ncit__raw_data/theme/icon-export2.png b/includes/TripalFields/ncit__raw_data/theme/icon-export2.png new file mode 100644 index 0000000000000000000000000000000000000000..f00439646b6141165219fb4d93e3d85268639072 GIT binary patch literal 47587 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 z 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 AhyVZp literal 0 HcmV?d00001 diff --git a/includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg b/includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66b3cec5899dc9dd30142e3dfd8ce7ffbdb291e2 GIT binary patch literal 14860 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 literal 0 HcmV?d00001 diff --git a/includes/TripalFields/rawpheno.fields.inc b/includes/TripalFields/rawpheno.fields.inc new file mode 100644 index 0000000..b5eeef4 --- /dev/null +++ b/includes/TripalFields/rawpheno.fields.inc @@ -0,0 +1,123 @@ +data_table) AND ($bundle->data_table == 'stock')) { + // Number of traits. + tripal_insert_cvterm(array( + 'id' => 'NCIT:C142663', + 'name' => 'Raw Data', + 'cv_name' => 'NCIT', + 'definition' => 'The original information, collected from the primary source. Used in Germplasm Raw Phenotypes Field.', + )); + $field_name = 'ncit__raw_data'; + $field_type = 'ncit__raw_data'; + $fields[$field_name] = array( + 'field_name' => $field_name, + 'type' => $field_type, + 'cardinality' => 1, + 'locked' => FALSE, + 'storage' => array( + 'type' => 'field_chado_storage', + ), + ); + } + + return $fields; +} + +/** + * Implements hook_bundle_instances_info(). + * + * This hook tells Drupal/Tripal to create a field instance of a given field type on a + * specific Tripal Content type (otherwise known as the bundle). Make sure to implement + * hook_bundle_create_fields() to create your field type before trying to create an + * instance of that field. + * + * @param $entity_type + * This should be 'TripalEntity' for all Tripal Content. + * @param $bundle + * This object describes the Type of Tripal Entity (e.g. Organism or Gene) the field + * instances are being created for. Thus this hook is called once per Tripal Content Type on your + * site. The name of the bundle is the machine name of the type (e.g. bio_data_1) and + * the label of the bundle (e.g. Organism) is what you see in the interface. Since the + * label can be changed by site admin, we suggest checking the data_table to determine + * if this is the entity you want to add field instances to. + * @return + * An array of field instance definitions. This is where you can define the defaults + * for any settings you use in your field. Each entry in this array will be used to + * create an instance of an already existing field. + */ +function rawpheno_bundle_instances_info($entity_type, $bundle) { + $instances = array(); + + // IN GERMPLASM PAGE ONLY: + if (isset($bundle->data_table) AND ($bundle->data_table == 'stock')) { + // Number of Values Recorded. + $field_name = 'ncit__raw_data'; + $field_type = 'ncit__raw_data'; + $instances[$field_name] = array( + 'field_name' => $field_name, + 'entity_type' => $entity_type, + 'bundle' => $bundle->name, + 'label' => 'Germplasm Raw Phenotypes', + 'description' => 'Field to add interface to raw phenotypes available to a germplasm.', + 'required' => FALSE, + 'settings' => array( + 'term_vocabulary' => 'NCIT', + 'term_name' => 'Raw Data', + 'term_accession' => 'C142663', + 'auto_attach' => FALSE, + 'chado_table' => $bundle->data_table, + 'chado_column' => $bundle->data_table . '_id', + 'base_table' => $bundle->data_table, + ), + 'widget' => array( + 'type' => 'ncit__raw_data_widget', + 'settings' => array(), + ), + 'display' => array( + 'default' => array( + 'label' => 'hidden', + 'type' => 'ncit__raw_data_formatter', + 'settings' => array(), + ), + ), + ); + } + + return $instances; +} \ No newline at end of file diff --git a/include/rawpheno.admin.form.inc b/includes/rawpheno.admin.form.inc similarity index 99% rename from include/rawpheno.admin.form.inc rename to includes/rawpheno.admin.form.inc index ce5dbe3..206c3f3 100644 --- a/include/rawpheno.admin.form.inc +++ b/includes/rawpheno.admin.form.inc @@ -129,7 +129,7 @@ function rawpheno_admin_page($form, &$form_state) { // Include function to manage column headers, cv terms and variable names. -module_load_include('inc', 'rawpheno', 'include/rawpheno.function.measurements'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.function.measurements'); /** diff --git a/include/rawpheno.backup.form.inc b/includes/rawpheno.backup.form.inc similarity index 100% rename from include/rawpheno.backup.form.inc rename to includes/rawpheno.backup.form.inc diff --git a/include/rawpheno.download.form.inc b/includes/rawpheno.download.form.inc similarity index 89% rename from include/rawpheno.download.form.inc rename to includes/rawpheno.download.form.inc index c48bce2..b786474 100644 --- a/include/rawpheno.download.form.inc +++ b/includes/rawpheno.download.form.inc @@ -1,366 +1,404 @@ - 'markup', - '#markup' => t('View Summary ❯'), - ); - - // PROJECT SELECT BOX. - if (isset($form_state['values']['sel_project'])) { - // Project selected. - $project_selected = $form_state['values']['sel_project']; - } - - // Sort the project names according to Planting Date. - // Put project with recently uploaded data/ based on planting year - // first in the list. - $sql = "SELECT project_id, name - FROM {project} AS t1 - RIGHT JOIN pheno_plant_project AS t2 USING (project_id) - LEFT JOIN pheno_measurements AS t3 USING (plant_id) - WHERE t3.type_id = (SELECT cvterm_id FROM {cvterm} WHERE name = 'Planting Date (date)' LIMIT 1) - GROUP BY project_id, name, t3.value - ORDER BY t3.value DESC"; - - $opt_project = chado_query($sql) - ->fetchAllKeyed(); - - // Project options: - if (count($opt_project) <= 0) { - // Module has no projects w/ data yet. - return $form; - } - else { - // Remove any duplicates from the sorted list. - $opt_project = array_unique($opt_project, SORT_REGULAR); - } - - // AJAX wrapper. - // Main wrapper - $form['ajax_container'] = array( - '#type' => 'markup', - '#prefix' => '
', - '#suffix' => '
', - ); - - // This a hidden field containing all project id. - // This field will allow callback functions to get all project ids which is - // the equivalent of the option select all project from the project select box. - $form['ajax_container']['txt_project'] = array( - '#type' => 'hidden', - '#value' => implode(',', array_keys($opt_project)), - ); - - $form['ajax_container']['sel_project'] = array( - '#type' => 'select', - '#title' => t('Experiment'), - '#options' => $opt_project, - '#multiple' => FALSE, - '#id' => 'download-sel-project', - '#ajax' => array( - 'event' => 'change', - 'callback' => 'rawpheno_download_get_locations_traits', - 'wrapper' => 'download-ajax-wrapper', - 'progress' => array('type' => '', 'message' => '') - ), - ); - - // This will reset the project select box on load and page refresh. - drupal_add_js('jQuery(document).ready(function() { - jQuery("#download-sel-project").val(0); - })', 'inline'); - - // Define the project ids required by the next field. - if (isset($project_selected)) { - // When a project is selected. Default to the project selected. - $project_id = $project_selected; - } - else { - // No project select. This is the default to the first project. - $p = array_keys($opt_project); - $project_id = reset($p); - } - - // All Locations for the default project above. Default project is the - // first project in the list. - $sql = "SELECT DISTINCT value, value AS prj_location - FROM pheno_plantprop - WHERE - type_id = (SELECT cvterm_id FROM {cvterm} cvt LEFT JOIN {cv} cv ON cv.cv_id = cvt.cv_id - WHERE cvt.name = 'Location' AND cv.name = 'phenotype_plant_property_types') AND - plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id IN (:project_id)) - ORDER BY value ASC"; - - $opt_location = chado_query($sql, array(':project_id' => $project_id)) - ->fetchAllKeyed(); - - $form['ajax_container']['sel_location'] = array( - '#type' => 'select', - '#title' => t('Location'), - '#options' => $opt_location, - '#multiple' => TRUE, - '#size' => 7, - '#id' => 'download-sel-location', - '#ajax' => array( - 'event' => 'change', - 'callback' => 'rawpheno_download_get_traits', - 'wrapper' => 'download-ajax-wrapper-traits', - 'progress' => array('type' => '', 'message' => '') - ), - ); - - $form['ajax_container']['chk_select_all_locations'] = array( - '#title' => t('Select all Locations'), - '#type' => 'checkbox', - '#default_value' => 0, - '#ajax' => array( - 'event' => 'change', - 'callback' => 'rawpheno_download_get_locations_traits', - 'wrapper' => 'download-ajax-wrapper', - 'progress' => array('type' => '', 'message' => '') - ), - '#id' => 'chk-select-all-locations', - ); - - $location_id = $opt_location; - - // Manage environment data file option. - // Allow option when a project is selected and project and location combination - // returns an environment data file. - $add_option = FALSE; - - if (isset($project_selected) && $project_selected > 0) { - if (isset($form_state['values']['sel_location']) - && count($form_state['values']['sel_location']) > 0) { - - $location = $form_state['values']['sel_location']; - - $envfile = rawpheno_function_getenv($project_selected, $location); - if ($envfile) { - $add_option = TRUE; - } - } - } - - drupal_add_js(array('rawpheno' => array('envdata_option' => $add_option)), array('type' => 'setting')); - - - // TRAITS. - // Select traits wrapper. - $form['ajax_container']['ajax_container_traits'] = array( - '#type' => 'markup', - '#prefix' => '
', - '#suffix' => '
', - ); - - // Get traits given a location and project. - if (isset($project_selected) && isset($location)) { - $project_id = $project_selected; - $location_id = $location; - } - - // The summarized list of cvterm_ids from MVIEW returned by inner most query will be passed to function that converts - // comma separated values into individual values (cvterm_id numbers) and the result is the parameter of ANY clause - // that will filter cvterms to only those in the list. Final rows are in JSON object and sorted alphabetically by name - // that will be passed on to the select field of rawdata form. - $sql_cvterm = " - SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( - SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( - SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( - SELECT string_agg(DISTINCT all_traits, ',') AS all_traits - FROM {rawpheno_rawdata_mview} - WHERE - location IN(:location) - AND plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) - ) AS list_id - )::int[]) - ) AS c_j - WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') - ORDER BY c_j.cvterm_json->>'name' ASC - "; - - $trait_set = chado_query($sql_cvterm, array(':location' => $location_id, ':project_id' => $project_id)) - ->fetchAllKeyed(); - - $opt_trait = array_unique($trait_set); - - $form['ajax_container']['ajax_container_traits']['sel_trait'] = array( - '#type' => 'select', - '#title' => t('@trait_count Traits available', array('@trait_count' => count($opt_trait))), - '#options' => $opt_trait, - '#multiple' => TRUE, - '#size' => 15, - '#id' => 'download-sel-trait', - ); - - $form['ajax_container']['chk_select_all_traits'] = array( - '#title' => t('Select all Traits'), - '#type' => 'checkbox', - '#default_value' => 0, - '#id' => 'chk-select-all-traits', - ); - - $form['div_buttons'] = array( - '#prefix' => '
', - '#suffix' => '
', - ); - - $form['div_buttons']['chk_envdata'] = array( - '#title' => t('Include Environment Data (Include Environment Data)', array('@img' => '../../' . $path . 'img/env.gif')), - '#type' => 'checkbox', - '#default_value' => 0, - '#id' => 'chk-envdata', - ); - - $form['div_buttons']['chk_rfriendly'] = array( - '#title' => t('Make R Friendly (Make R Friendly)', array('@img' => '../../' . $path . 'img/r.gif')), - '#type' => 'checkbox', - '#default_value' => 0, - ); - - - $form['div_buttons']['download_submit_download'] = array( - '#type' => 'submit', - '#value' => 'Download', - ); - - $form['#attached']['js'] = array($path . 'js/rawpheno.download.script.js'); - - - return $form; -} - - -/** - * Function callback: AJAX update location and traits select boxes when project is selected. - */ -function rawpheno_download_get_locations_traits($form, $form_state) { - return $form['ajax_container']; -} - - -/** - * Function callback: AJAX update trait select box. - */ -function rawpheno_download_get_traits($form, $form_state) { - /* - $location = $form_state['values']['sel_location']; - $project = $form_state['values']['sel_project']; - - // Determine if the selected project is all project. - if ($project == 0) { - // Yes, then read the value of the hidden field containing project ids. - $t = $form_state['values']['txt_project']; - $project = explode(',', $t); - } - - // Get all traits given a location and project. - $opt_trait = rawpheno_download_load_traits($location, $project); - - // Update the #options value of select a trait select box. - $form['ajax_container']['ajax_container_traits']['sel_trait']['#options'] = $opt_trait; - // Update the title. - $form['ajax_container']['ajax_container_traits']['sel_trait']['#title'] = t('@count_trait Traits available', array('@count_trait' => count($opt_trait))); -*/ - - return $form['ajax_container']['ajax_container_traits']; -} - - -/** - * Implements hook_form_submit(). - * - * Generate a comma separated values (csv) file based on the location and trait set selected. - */ -function rawpheno_download_submit($form, &$form_state) { - // Project select field. - // Project by default is 0 - all projects then we want all project id field. - // This is field is never an array. - $prj = $form_state['values']['sel_project']; - $all_prj = $form_state['values']['txt_project']; - $prj = ($prj == 0) ? $all_prj : $prj; - - // Location select field. - // Location select field is an empty array - all locations. - // Otherwise, it will be an associative array where location is both key and value. - // Convert this to comma separated string when there's anything else set to 0 - for all locations. - $loc = $form_state['values']['sel_location']; - // Location 1 + (and) Location 2 + ..... - $loc = (count($loc) > 0) ? implode('+', $loc) : 0; - - // Trait select field. - // Trait select field is an empty array - all traits. - // Otherwise, it will be an associative array where trait is both key and value. - // Convert this to comma separated string when there's anything else set to 0 - for all traits. - $trt = $form_state['values']['sel_trait']; - $trt = (count($trt) > 0) ? implode(',', $trt) : 0; - - // Lastly, if user wants Environment Data and R version. - $env = $form_state['values']['chk_envdata']; - $rvr = $form_state['values']['chk_rfriendly']; - - // Construct environment data files archive. - $env_filename = 0; - - if (isset($env) && $env == 1) { - // Ensure that project and location combination return an environment data file. - $project = explode(',', $prj); - $location = explode('+', $loc); - - $files = rawpheno_function_getenv($project, $location); - - if (count($files) > 0) { - // Env file available. - $envs = array(); - - foreach($files as $file) { - $envs[] = $file->filename; - } - - if (count($envs) == 1) { - // Single env file found. Fetch the file (xlsx usually) and submit to tripal download. - $env_filename = reset($envs); - } - else { - // Multiple env files found. Fetch all files, tar (archive) and submit to tripal download. - $public = drupal_realpath('public://'); - $tar_filename = 'environment_data_' . date('ymdis') . '.tar'; - $tar_file = $public . '/' . $tar_filename; - - $tar_cmd = 'tar -cf ' . escapeshellarg($tar_file) . ' -C ' . escapeshellarg($public) . ' '; - $tar_cmd .= implode(' ', $envs) . ' 2>&1'; - - // Package everything... - shell_exec($tar_cmd); - $env_filename = $tar_filename; - } - } - } - - // Contain all query parameters/string into one string. - // Decode first when reading this string using base64_decode() function. - $url = 'p=' . $prj . '&l=' . $loc . '&t=' . $trt . '&r=' . $rvr . '&e=' . $env . '&file=' . $env_filename; - - // Format url for redirect. - $form_state['redirect'] = array( - '/phenotypes/raw/csv', - array( - 'query' => array( - 'code' => base64_encode($url), - ), - ), - ); -} +uid); + $user_experiment = array_keys($user_experiment); + + if (in_array($param_experiment, $user_experiment)) { + $param_location = $query_vars['l']; + $param_trait = (int) $query_vars['t']; + + if ($param_experiment > 0 && $param_location && $param_trait > 0) { + // Create query string. + $query_string = 'p=' . $param_experiment . '&l=' . $param_location . '&t=' . $param_trait . '&r=0&e=0&file=0'; + drupal_goto('/phenotypes/raw/csv', array('query' => array('code' => base64_encode($query_string)))); + } + } + } + } + + // Attach CSS and JavaScript + $path = drupal_get_path('module', 'rawpheno') . '/theme/'; + $form['#attached']['css'] = array($path . 'css/rawpheno.download.style.css'); + + // Navigation button. Related page of download page is rawdata/summary page. + $form['page_button'] = array( + '#type' => 'markup', + '#markup' => t('View Summary ❯'), + ); + + // PROJECT SELECT BOX. + if (isset($form_state['values']['sel_project'])) { + // Project selected. + $project_selected = $form_state['values']['sel_project']; + } + + // Sort the project names according to Planting Date. + // Put project with recently uploaded data/ based on planting year + // first in the list. + $sql = "SELECT project_id, name + FROM {project} AS t1 + RIGHT JOIN pheno_plant_project AS t2 USING (project_id) + LEFT JOIN pheno_measurements AS t3 USING (plant_id) + WHERE t3.type_id = (SELECT cvterm_id FROM {cvterm} WHERE name = 'Planting Date (date)' LIMIT 1) + GROUP BY project_id, name, t3.value + ORDER BY t3.value DESC"; + + $opt_project = chado_query($sql) + ->fetchAllKeyed(); + + // Project options: + if (count($opt_project) <= 0) { + // Module has no projects w/ data yet. + return $form; + } + else { + // Remove any duplicates from the sorted list. + $opt_project = array_unique($opt_project, SORT_REGULAR); + } + + // AJAX wrapper. + // Main wrapper + $form['ajax_container'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + ); + + // This a hidden field containing all project id. + // This field will allow callback functions to get all project ids which is + // the equivalent of the option select all project from the project select box. + $form['ajax_container']['txt_project'] = array( + '#type' => 'hidden', + '#value' => implode(',', array_keys($opt_project)), + ); + + $form['ajax_container']['sel_project'] = array( + '#type' => 'select', + '#title' => t('Experiment'), + '#options' => $opt_project, + '#multiple' => FALSE, + '#id' => 'download-sel-project', + '#ajax' => array( + 'event' => 'change', + 'callback' => 'rawpheno_download_get_locations_traits', + 'wrapper' => 'download-ajax-wrapper', + 'progress' => array('type' => '', 'message' => '') + ), + ); + + // This will reset the project select box on load and page refresh. + drupal_add_js('jQuery(document).ready(function() { + jQuery("#download-sel-project").val(0); + })', 'inline'); + + // Define the project ids required by the next field. + if (isset($project_selected)) { + // When a project is selected. Default to the project selected. + $project_id = $project_selected; + } + else { + // No project select. This is the default to the first project. + $p = array_keys($opt_project); + $project_id = reset($p); + } + + // All Locations for the default project above. Default project is the + // first project in the list. + $sql = "SELECT DISTINCT value, value AS prj_location + FROM pheno_plantprop + WHERE + type_id = (SELECT cvterm_id FROM {cvterm} cvt LEFT JOIN {cv} cv ON cv.cv_id = cvt.cv_id + WHERE cvt.name = 'Location' AND cv.name = 'phenotype_plant_property_types') AND + plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id IN (:project_id)) + ORDER BY value ASC"; + + $opt_location = chado_query($sql, array(':project_id' => $project_id)) + ->fetchAllKeyed(); + + $form['ajax_container']['sel_location'] = array( + '#type' => 'select', + '#title' => t('Location'), + '#options' => $opt_location, + '#multiple' => TRUE, + '#size' => 7, + '#id' => 'download-sel-location', + '#ajax' => array( + 'event' => 'change', + 'callback' => 'rawpheno_download_get_traits', + 'wrapper' => 'download-ajax-wrapper-traits', + 'progress' => array('type' => '', 'message' => '') + ), + ); + + $form['ajax_container']['chk_select_all_locations'] = array( + '#title' => t('Select all Locations'), + '#type' => 'checkbox', + '#default_value' => 0, + '#ajax' => array( + 'event' => 'change', + 'callback' => 'rawpheno_download_get_locations_traits', + 'wrapper' => 'download-ajax-wrapper', + 'progress' => array('type' => '', 'message' => '') + ), + '#id' => 'chk-select-all-locations', + ); + + $location_id = $opt_location; + + // Manage environment data file option. + // Allow option when a project is selected and project and location combination + // returns an environment data file. + $add_option = FALSE; + + if (isset($project_selected) && $project_selected > 0) { + if (isset($form_state['values']['sel_location']) + && count($form_state['values']['sel_location']) > 0) { + + $location = $form_state['values']['sel_location']; + + $envfile = rawpheno_function_getenv($project_selected, $location); + if ($envfile) { + $add_option = TRUE; + } + } + } + + drupal_add_js(array('rawpheno' => array('envdata_option' => $add_option)), array('type' => 'setting')); + + + // TRAITS. + // Select traits wrapper. + $form['ajax_container']['ajax_container_traits'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + ); + + // Get traits given a location and project. + if (isset($project_selected) && isset($location)) { + $project_id = $project_selected; + $location_id = $location; + } + + // The summarized list of cvterm_ids from MVIEW returned by inner most query will be passed to function that converts + // comma separated values into individual values (cvterm_id numbers) and the result is the parameter of ANY clause + // that will filter cvterms to only those in the list. Final rows are in JSON object and sorted alphabetically by name + // that will be passed on to the select field of rawdata form. + $sql_cvterm = " + SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( + SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( + SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( + SELECT string_agg(DISTINCT all_traits, ',') AS all_traits + FROM {rawpheno_rawdata_mview} + WHERE + location IN(:location) + AND plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) + ) AS list_id + )::int[]) + ) AS c_j + WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') + ORDER BY c_j.cvterm_json->>'name' ASC + "; + + $trait_set = chado_query($sql_cvterm, array(':location' => $location_id, ':project_id' => $project_id)) + ->fetchAllKeyed(); + + $opt_trait = array_unique($trait_set); + + $form['ajax_container']['ajax_container_traits']['sel_trait'] = array( + '#type' => 'select', + '#title' => t('@trait_count Traits available', array('@trait_count' => count($opt_trait))), + '#options' => $opt_trait, + '#multiple' => TRUE, + '#size' => 15, + '#id' => 'download-sel-trait', + ); + + $form['ajax_container']['chk_select_all_traits'] = array( + '#title' => t('Select all Traits'), + '#type' => 'checkbox', + '#default_value' => 0, + '#id' => 'chk-select-all-traits', + ); + + $form['div_buttons'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['div_buttons']['chk_envdata'] = array( + '#title' => t('Include Environment Data (Include Environment Data)', array('@img' => '../../' . $path . 'img/env.gif')), + '#type' => 'checkbox', + '#default_value' => 0, + '#id' => 'chk-envdata', + ); + + $form['div_buttons']['chk_rfriendly'] = array( + '#title' => t('Make R Friendly (Make R Friendly)', array('@img' => '../../' . $path . 'img/r.gif')), + '#type' => 'checkbox', + '#default_value' => 0, + ); + + + $form['div_buttons']['download_submit_download'] = array( + '#type' => 'submit', + '#value' => 'Download', + ); + + $form['#attached']['js'] = array($path . 'js/rawpheno.download.script.js'); + + return $form; +} + + +/** + * Function callback: AJAX update location and traits select boxes when project is selected. + */ +function rawpheno_download_get_locations_traits($form, $form_state) { + return $form['ajax_container']; +} + + +/** + * Function callback: AJAX update trait select box. + */ +function rawpheno_download_get_traits($form, $form_state) { + /* + $location = $form_state['values']['sel_location']; + $project = $form_state['values']['sel_project']; + + // Determine if the selected project is all project. + if ($project == 0) { + // Yes, then read the value of the hidden field containing project ids. + $t = $form_state['values']['txt_project']; + $project = explode(',', $t); + } + + // Get all traits given a location and project. + $opt_trait = rawpheno_download_load_traits($location, $project); + + // Update the #options value of select a trait select box. + $form['ajax_container']['ajax_container_traits']['sel_trait']['#options'] = $opt_trait; + // Update the title. + $form['ajax_container']['ajax_container_traits']['sel_trait']['#title'] = t('@count_trait Traits available', array('@count_trait' => count($opt_trait))); +*/ + + return $form['ajax_container']['ajax_container_traits']; +} + + +/** + * Implements hook_form_submit(). + * + * Generate a comma separated values (csv) file based on the location and trait set selected. + */ +function rawpheno_download_submit($form, &$form_state) { + // Project select field. + // Project by default is 0 - all projects then we want all project id field. + // This is field is never an array. + $prj = $form_state['values']['sel_project']; + $all_prj = $form_state['values']['txt_project']; + $prj = ($prj == 0) ? $all_prj : $prj; + + // Location select field. + // Location select field is an empty array - all locations. + // Otherwise, it will be an associative array where location is both key and value. + // Convert this to comma separated string when there's anything else set to 0 - for all locations. + $loc = $form_state['values']['sel_location']; + // Location 1 + (and) Location 2 + ..... + $loc = (count($loc) > 0) ? implode('+', $loc) : 0; + + // Trait select field. + // Trait select field is an empty array - all traits. + // Otherwise, it will be an associative array where trait is both key and value. + // Convert this to comma separated string when there's anything else set to 0 - for all traits. + $trt = $form_state['values']['sel_trait']; + $trt = (count($trt) > 0) ? implode(',', $trt) : 0; + + // Lastly, if user wants Environment Data and R version. + $env = $form_state['values']['chk_envdata']; + $rvr = $form_state['values']['chk_rfriendly']; + + // Construct environment data files archive. + $env_filename = 0; + + if (isset($env) && $env == 1) { + // Ensure that project and location combination return an environment data file. + $project = explode(',', $prj); + $location = explode('+', $loc); + + $files = rawpheno_function_getenv($project, $location); + + if (count($files) > 0) { + // Env file available. + $envs = array(); + + foreach($files as $file) { + $envs[] = $file->filename; + } + + if (count($envs) == 1) { + // Single env file found. Fetch the file (xlsx usually) and submit to tripal download. + $env_filename = reset($envs); + } + else { + // Multiple env files found. Fetch all files, tar (archive) and submit to tripal download. + $public = drupal_realpath('public://'); + $tar_filename = 'environment_data_' . date('ymdis') . '.tar'; + $tar_file = $public . '/' . $tar_filename; + + $tar_cmd = 'tar -cf ' . escapeshellarg($tar_file) . ' -C ' . escapeshellarg($public) . ' '; + $tar_cmd .= implode(' ', $envs) . ' 2>&1'; + + // Package everything... + shell_exec($tar_cmd); + $env_filename = $tar_filename; + } + } + } + + // Contain all query parameters/string into one string. + // Decode first when reading this string using base64_decode() function. + $url = 'p=' . $prj . '&l=' . $loc . '&t=' . $trt . '&r=' . $rvr . '&e=' . $env . '&file=' . $env_filename; + + // Format url for redirect. + $form_state['redirect'] = array( + '/phenotypes/raw/csv', + array( + 'query' => array( + 'code' => base64_encode($url), + ), + ), + ); +} diff --git a/include/rawpheno.function.measurements.inc b/includes/rawpheno.function.measurements.inc similarity index 100% rename from include/rawpheno.function.measurements.inc rename to includes/rawpheno.function.measurements.inc diff --git a/include/rawpheno.instructions.form.inc b/includes/rawpheno.instructions.form.inc old mode 100755 new mode 100644 similarity index 97% rename from include/rawpheno.instructions.form.inc rename to includes/rawpheno.instructions.form.inc index 513483c..a095565 --- a/include/rawpheno.instructions.form.inc +++ b/includes/rawpheno.instructions.form.inc @@ -1,506 +1,506 @@ - 'markup', - '#markup' => t('Upload Data ❯'), - ); - - // Attach CSS. - $path = drupal_get_path('module', 'rawpheno') . '/theme/'; - $form['#attached']['css'] = array($path . 'css/rawpheno.instructions.style.css'); - - // Create a select box containing projects available - these are projects - // that have associated column header set and must have at least 1 essential column header. - // The projects are filtered to show only projects assigned to user. - $all_project = rawpheno_function_user_project($GLOBALS['user']->uid); - - // No projects assined to the user. - if (count($all_project) < 1) { - return $form; - } - - // Ensure that project is valid. - if ($project_id) { - if (!in_array($project_id, array_keys($all_project))) { - // Project does not exist. - $form['message_invalid_project'] = array( - '#markup' => '
Experiment does not exist.
' - ); - } - else { - // Project is valid. Null this. - $form['message_invalid_project'] = array(); - } - } - else { - // When no project is supplied, then default this page to the most recent project - // available from the projects defined by admin. - $project_id = array_keys($all_project)[0]; - } - - // Project Name. - $project_name = $all_project[$project_id]; - - // Given a project id, construct the table required for each trait type set. - // All trait types, need to remove type plantproperty. - $trait_set = rawpheno_function_trait_types(); - - $sql = "SELECT project_cvterm_id AS id FROM {pheno_project_cvterm} - WHERE project_id = :project_id AND type <> :plantprop - ORDER BY type, cvterm_id ASC"; - - $args = array(':project_id' => $project_id, ':plantprop' => $trait_set['type4']); - $cvterm_id = db_query($sql, $args); - - // Array to hold trait row. - $arr_cvterm = array(); - - foreach($cvterm_id as $id) { - $cvterm = rawpheno_function_header_properties($id->id); - // Create an array that will translate to a row in table. - $arr_cvterm[ $cvterm['type'] ][] = array( - '
' . $cvterm['name'] . '
', - $cvterm['method'], - $cvterm['definition'] - ); - } - - // Construct table. - $arr_tbl_args['empty'] = '0 Column Header'; - $arr_tbl_args['header'] = array(t('Column Header/Trait'), t('Collection Method'), t('Definition')); - - unset($trait_set['type4']); - - foreach($trait_set as $type) { - $arr_tbl_args['rows'] = isset($arr_cvterm[$type]) ? $arr_cvterm[$type] : array(); - - $form['tbl_project_headers_' . $type] = array( - '#markup' => (count($arr_tbl_args['rows']) <= 0) ? 'no-trait' : theme('table', $arr_tbl_args), - ); - } - - // Project and project select box. - $form['project_panel'] = array( - '#markup' => $project_name - ); - - $form['sel_project'] = array( - '#type' => 'select', - '#options' => array(0 => 'Please select an experiment') + $all_project, - '#id' => 'rawpheno-ins-sel-project' - ); - - $ins_path = base_path() . 'phenotypes/raw/instructions/'; - // Make the project select box into a jump menu and add the project id number to the url. - drupal_add_js('jQuery(document).ready(function() { - jQuery("#rawpheno-ins-sel-project").change(function(){ - if (jQuery(this).val() > 0) { - window.location.href = "'. $ins_path .'" + jQuery(this).val(); - } - }); - })', 'inline'); - - - // NOTE: When project is AGILE, download data collection spreadsheet link points to - // pre-made AGILE spreadsheet, otherwise, instructions page will generate the spreadsheet file. To ensure that - // the column headers matched for AGILE project, a check (when no new column header added tru admin then download - // pre-made, else generate) is required before downloading the file. - - // Provide user with link to download project specific data collection spreadsheet file. - // When the project is the default project (AGILE) then supply a ready made spreadsheet. - - $file_path = ($tmp_project == null) ? 'instructions/spreadsheet/' : 'spreadsheet/'; - $link_to_xls = $file_path . $project_id; - - if ($project_name == 'AGILE: Application of Genomic Innovation in the Lentil Economy') { - // Compare the AGILE-specific column header to check if they match - $AGILE_column_headers = rawpheno_function_headers('expected'); - $AGILE_essential_headers = rawpheno_function_headers('essential'); - $AGILE_required_headers = rawpheno_function_headers('required'); - $AGILE_essential_headers = array_diff($AGILE_essential_headers, $AGILE_required_headers); - - // Query the AGILE project column headers. - $sql = "SELECT name, type FROM {cvterm} RIGHT JOIN pheno_project_cvterm USING(cvterm_id) WHERE project_id = :project_id"; - $args = array(':project_id' => $project_id); - $h = chado_query($sql, $args) - ->fetchAllKeyed(); - - // Manually add Name, since Name is not added to project. - $h['Name'] = 'plantproperty'; - - $header_found_count = 0; - $essential_found_count = 0; - $essential_count = 0; - $header_count = 0; - - foreach($h as $header => $type) { - // Ignore trait contributed. - if ($type == $trait_set['type5']) { - continue; - } - - $header_count++; - - // Use ready made AGILE data collection spreadsheet. - // Total number of headers, excluding contributed matches AGILE traits. - if (in_array($header, $AGILE_column_headers)) { - $header_found_count++; - } - - // Same essential traits as in AGILE traits. - if ($type == $trait_set['type1'] AND in_array($header, $AGILE_essential_headers)) { - $essential_found_count++; - } - - // Same number of essential traits as in AGILE traits. - if ($type == $trait_set['type1']) { - $essential_count++; - } - - // Finally, name must be AGILE: Application of Genomic Innovation in the Lentil Economy as condition for this block. - } - - // Check if AGILE trait set was not altered. - if ($header_count == count($AGILE_column_headers) - && $header_found_count == count($AGILE_column_headers) - && $essential_found_count == count($AGILE_essential_headers) - && $essential_count == count($AGILE_essential_headers)) { - // AGILE Project match the original trait set. Download ready-made data collection spreadsheet file. - $link_to_xls = file_create_url('public://AGILE-PhenotypeDataCollection-v5.xlsx.zip'); - } - } - - $form['download_data_collection'] = array( - '#markup' => t('Download Data Collection Spreadsheet', array('@link' => $link_to_xls)) - ); - - // Search field with autocomplete feature. - $form['txt_search'] = array( - '#title' => '', - '#type' => 'textfield', - '#maxlength' => 65, - '#size' => 65, - '#default_value' => t('Search Trait'), - '#autocomplete_path' => 'phenotypes/raw/instructions/autocomplete/' . $project_id, - ); - - $form['btn_search'] = array( - '#type' => 'markup', - '#markup' => '', - ); - - // Hidden field containing url to json. - $form['json_url'] = array( - '#type' => 'hidden', - '#value' => $GLOBALS['base_url'] . '/phenotypes/raw/instructions/autocomplete/' . $project_id, - '#attributes' => array('id' => array('traits-json')) - ); - - // Attach JQuery UI library and JavaScript. - $form['#attached']['library'][] = array('system', 'ui.tabs'); - $form['#attached']['js'] = array($path . 'js/rawpheno.instructions.script.js'); - - return $form; -} - - -/** - * Function create a spreadsheet file. - * - */ -function rawpheno_instructions_create_spreadsheet($project_id) { - // Query column headers specific to a project, given a project id. - if (isset($project_id) AND $project_id > 0) { - // Array to hold all trait types. - $trait_type = rawpheno_function_trait_types(); - - // Exclude the plant property from the set of headers. They are pre-inserted to the array - // of column headers passed to the spreadsheet writer. - $sql = "SELECT project_cvterm_id, name, type - FROM {cvterm} RIGHT JOIN pheno_project_cvterm USING(cvterm_id) - WHERE project_id = :project_id AND type NOT IN ( :exclude_property ) - ORDER BY type ASC"; - - $args = array(':project_id' => $project_id, ':exclude_property' => array($trait_type['type4'], $trait_type['type5'])); - $cvterm = chado_query($sql, $args); - - // Only when project has headers. - if ($cvterm->rowCount() > 0) { - // Array to hold the column headers passed to the excel writer. - $col_headers = array(); - // Array to hold standard procedure, which basically is - // the traits definition and collection method. - $instructions_data = array(); - - // Get the data type per unit. This type will be the cell type in the spreadsheet. - $data_type = rawpheno_function_default_unit('type'); - - // Prepend the array with plant property column headers. - $col_headers = array( - 'Plot' => 'integer', - 'Entry' => 'integer', - 'Name' => 'string', - 'Rep' => 'integer', - 'Location' => 'string' - ); - - // Start at F column taking into account plant properties. - // A for Plot, B for Entry and so on (A-E is 5 cols). - $l = 'F'; - $cell_i = array(); - - // Assign the data type for each header based on the unit it contains. - $h = array('name' => 'Trait', 'definition' => 'Definition', 'method' => 'Collection Method'); - - foreach($cvterm as $trait) { - // Get the unit. - $u = rawpheno_function_header_unit($trait->name); - $unit = isset($data_type[$u]) ? $data_type[$u] : 'string'; - - $col_headers[$trait->name] = $unit; - - // Highlight the cells when it is essential trait. - if ($trait->type == $trait_type['type1']) { - array_push($cell_i, $l . '1'); - // Increment F column. - $l++; - } - - // Get header method and definition information. - $t = rawpheno_function_header_properties($trait->project_cvterm_id); - - foreach($h as $m_i => $m) { - $star = ($m_i == 'name') ? '*' : ''; - - if (strlen($t[$m_i]) < 80) { - // Short text, save it. - array_push($instructions_data, array($star . $m . ':', $t[$m_i])); - } - else { - // Hard-wrap long lines into shorter line and put each - // line into a cell/row. - $wrapped_string = wordwrap($t[$m_i], 100, "\n"); - $chunks = explode("\n", $wrapped_string); - - foreach($chunks as $i => $chunk) { - $ins_text = ($i == 0) ? array($star . $m . ':', $chunk) : array('', $chunk); - array_push($instructions_data, $ins_text); - } - } - } - - // Add extra new line. - array_push($instructions_data, array('' , '')); - } - - // Load spreadsheet writer library. - $xlsx_writer = libraries_load('spreadsheet_writer'); - include_once $xlsx_writer['library path'] . '/'. $xlsx_writer['files'][0]; - - $writer = new XLSXWriter(); - // Measurement tab. - @$writer->writeSheet(array(), 'Measurements', $col_headers, - array( - // The entire header row apply these styles. - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '000000', - 'bold' => false, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'verticalAlign' => 'top', - 'horizontalAlign' => 'center', - 'fill' => array('color' => 'F7F7F7'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'rows' => array('0') - ), - // Once the styles above have been applied, style the plant property headers. - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '000000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'verticalAlign' => 'bottom', - 'horizontalAlign' => 'center', - 'fill' => array('color' => 'EAEAEA'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'cells' => array('A1', 'B1', 'C1', 'D1', 'E1') - ), - // Make sure to style the essential trait/header. - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '008000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'verticalAlign' => 'top', - 'horizontalAlign' => 'center', - 'fill' => array('color' => 'F5FFDF'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'cells' => $cell_i - ) - ) - ); - - // Standard procedure tab. - // Load trait definition and data collection method to this sheet. - $instructions_header = array(); - @$writer->writeSheet($instructions_data, 'Instructions', $instructions_header, - array( - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '000000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'columns' => '0', - ), - array( - 'font' => - array( - 'size' => '12', - ), - 'wrapText' => false, - 'columns' => '1', - ), - ) - ); - - // Calculator tab. - $calc_header = array('CALCULATE DAYS TO' => 'string'); - $calc_data = - array( - array('Planting Date', '2015-10-06'), - array('Current Date', date('Y-m-d')), - array('Current "Days till"', '=B3 - B2'), - array('',''), - array('Instructions', ''), - array('', ''), - array('Fill out the planting date indicated in the measurements tab, as well as, the current date.', ''), - array('The "Days till" date will then be calculated for you.', '') - ); - - @$writer->writeSheet($calc_data, 'Calculate Days to', $calc_header, - array( - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '20', - 'color' => '000000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => false, - 'rows' => array('0'), - ), - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => 'FFFFFF', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'fill' => array('color' => '305673'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'rows' => array('1'), - ), - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '000000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'fill' => array('color' => 'F7F7F7'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'rows' => array('2'), - ), - array( - 'font' => - array( - 'name' => 'Arial', - 'size' => '11', - 'color' => '000000', - 'bold' => true, - 'italic' => false, - 'underline' => false - ), - 'wrapText' => true, - 'fill' => array('color' => '79a183'), - 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), - 'rows' => array('3'), - ) - ) - ); - - // Data collection spreadsheet name contains the following: - // Project id, name of the user, date and time. - $filename = 'datacollection_' . $project_id . '_' . str_replace(' ', '_', $GLOBALS['user']->name) .'_'. date('YMd') .'_'. time() . '.xlsx'; - $file = file_save_data($writer->writeToString(), 'public://' . $filename); - - // Launch save file window and ask user to save file. - $http_headers = array( - 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - ); - - if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) { - $http_headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0'; - $http_headers['Pragma'] = 'public'; - } - else { - $http_headers['Pragma'] = 'no-cache'; - } - - file_transfer($file->uri, $http_headers); - - // Just in case the auto download fails, provide a link to manually download the file. - print 'Download Data Collection Spreadsheet'; - } - else { - // Here project not found. - print 'Experiment not found.'; - } - } - else { - // Project id is not valid. - print 'Experiment ID number is invalid.'; - } -} + 'markup', + '#markup' => t('Upload Data ❯'), + ); + + // Attach CSS. + $path = drupal_get_path('module', 'rawpheno') . '/theme/'; + $form['#attached']['css'] = array($path . 'css/rawpheno.instructions.style.css'); + + // Create a select box containing projects available - these are projects + // that have associated column header set and must have at least 1 essential column header. + // The projects are filtered to show only projects assigned to user. + $all_project = rawpheno_function_user_project($GLOBALS['user']->uid); + + // No projects assined to the user. + if (count($all_project) < 1) { + return $form; + } + + // Ensure that project is valid. + if ($project_id) { + if (!in_array($project_id, array_keys($all_project))) { + // Project does not exist. + $form['message_invalid_project'] = array( + '#markup' => '
Experiment does not exist.
' + ); + } + else { + // Project is valid. Null this. + $form['message_invalid_project'] = array(); + } + } + else { + // When no project is supplied, then default this page to the most recent project + // available from the projects defined by admin. + $project_id = array_keys($all_project)[0]; + } + + // Project Name. + $project_name = $all_project[$project_id]; + + // Given a project id, construct the table required for each trait type set. + // All trait types, need to remove type plantproperty. + $trait_set = rawpheno_function_trait_types(); + + $sql = "SELECT project_cvterm_id AS id FROM {pheno_project_cvterm} + WHERE project_id = :project_id AND type <> :plantprop + ORDER BY type, cvterm_id ASC"; + + $args = array(':project_id' => $project_id, ':plantprop' => $trait_set['type4']); + $cvterm_id = db_query($sql, $args); + + // Array to hold trait row. + $arr_cvterm = array(); + + foreach($cvterm_id as $id) { + $cvterm = rawpheno_function_header_properties($id->id); + // Create an array that will translate to a row in table. + $arr_cvterm[ $cvterm['type'] ][] = array( + '
' . $cvterm['name'] . '
', + $cvterm['method'], + $cvterm['definition'] + ); + } + + // Construct table. + $arr_tbl_args['empty'] = '0 Column Header'; + $arr_tbl_args['header'] = array(t('Column Header/Trait'), t('Collection Method'), t('Definition')); + + unset($trait_set['type4']); + + foreach($trait_set as $type) { + $arr_tbl_args['rows'] = isset($arr_cvterm[$type]) ? $arr_cvterm[$type] : array(); + + $form['tbl_project_headers_' . $type] = array( + '#markup' => (count($arr_tbl_args['rows']) <= 0) ? 'no-trait' : theme('table', $arr_tbl_args), + ); + } + + // Project and project select box. + $form['project_panel'] = array( + '#markup' => $project_name + ); + + $form['sel_project'] = array( + '#type' => 'select', + '#options' => array(0 => 'Please select an experiment') + $all_project, + '#id' => 'rawpheno-ins-sel-project' + ); + + $ins_path = base_path() . 'phenotypes/raw/instructions/'; + // Make the project select box into a jump menu and add the project id number to the url. + drupal_add_js('jQuery(document).ready(function() { + jQuery("#rawpheno-ins-sel-project").change(function(){ + if (jQuery(this).val() > 0) { + window.location.href = "'. $ins_path .'" + jQuery(this).val(); + } + }); + })', 'inline'); + + + // NOTE: When project is AGILE, download data collection spreadsheet link points to + // pre-made AGILE spreadsheet, otherwise, instructions page will generate the spreadsheet file. To ensure that + // the column headers matched for AGILE project, a check (when no new column header added tru admin then download + // pre-made, else generate) is required before downloading the file. + + // Provide user with link to download project specific data collection spreadsheet file. + // When the project is the default project (AGILE) then supply a ready made spreadsheet. + + $file_path = ($tmp_project == null) ? 'instructions/spreadsheet/' : 'spreadsheet/'; + $link_to_xls = $file_path . $project_id; + + if ($project_name == 'AGILE: Application of Genomic Innovation in the Lentil Economy') { + // Compare the AGILE-specific column header to check if they match + $AGILE_column_headers = rawpheno_function_headers('expected'); + $AGILE_essential_headers = rawpheno_function_headers('essential'); + $AGILE_required_headers = rawpheno_function_headers('required'); + $AGILE_essential_headers = array_diff($AGILE_essential_headers, $AGILE_required_headers); + + // Query the AGILE project column headers. + $sql = "SELECT name, type FROM {cvterm} RIGHT JOIN pheno_project_cvterm USING(cvterm_id) WHERE project_id = :project_id"; + $args = array(':project_id' => $project_id); + $h = chado_query($sql, $args) + ->fetchAllKeyed(); + + // Manually add Name, since Name is not added to project. + $h['Name'] = 'plantproperty'; + + $header_found_count = 0; + $essential_found_count = 0; + $essential_count = 0; + $header_count = 0; + + foreach($h as $header => $type) { + // Ignore trait contributed. + if ($type == $trait_set['type5']) { + continue; + } + + $header_count++; + + // Use ready made AGILE data collection spreadsheet. + // Total number of headers, excluding contributed matches AGILE traits. + if (in_array($header, $AGILE_column_headers)) { + $header_found_count++; + } + + // Same essential traits as in AGILE traits. + if ($type == $trait_set['type1'] AND in_array($header, $AGILE_essential_headers)) { + $essential_found_count++; + } + + // Same number of essential traits as in AGILE traits. + if ($type == $trait_set['type1']) { + $essential_count++; + } + + // Finally, name must be AGILE: Application of Genomic Innovation in the Lentil Economy as condition for this block. + } + + // Check if AGILE trait set was not altered. + if ($header_count == count($AGILE_column_headers) + && $header_found_count == count($AGILE_column_headers) + && $essential_found_count == count($AGILE_essential_headers) + && $essential_count == count($AGILE_essential_headers)) { + // AGILE Project match the original trait set. Download ready-made data collection spreadsheet file. + $link_to_xls = file_create_url('public://AGILE-PhenotypeDataCollection-v5.xlsx.zip'); + } + } + + $form['download_data_collection'] = array( + '#markup' => t('Download Data Collection Spreadsheet', array('@link' => $link_to_xls)) + ); + + // Search field with autocomplete feature. + $form['txt_search'] = array( + '#title' => '', + '#type' => 'textfield', + '#maxlength' => 65, + '#size' => 65, + '#default_value' => t('Search Trait'), + '#autocomplete_path' => 'phenotypes/raw/instructions/autocomplete/' . $project_id, + ); + + $form['btn_search'] = array( + '#type' => 'markup', + '#markup' => '', + ); + + // Hidden field containing url to json. + $form['json_url'] = array( + '#type' => 'hidden', + '#value' => $GLOBALS['base_url'] . '/phenotypes/raw/instructions/autocomplete/' . $project_id, + '#attributes' => array('id' => array('traits-json')) + ); + + // Attach JQuery UI library and JavaScript. + $form['#attached']['library'][] = array('system', 'ui.tabs'); + $form['#attached']['js'] = array($path . 'js/rawpheno.instructions.script.js'); + + return $form; +} + + +/** + * Function create a spreadsheet file. + * + */ +function rawpheno_instructions_create_spreadsheet($project_id) { + // Query column headers specific to a project, given a project id. + if (isset($project_id) AND $project_id > 0) { + // Array to hold all trait types. + $trait_type = rawpheno_function_trait_types(); + + // Exclude the plant property from the set of headers. They are pre-inserted to the array + // of column headers passed to the spreadsheet writer. + $sql = "SELECT project_cvterm_id, name, type + FROM {cvterm} RIGHT JOIN pheno_project_cvterm USING(cvterm_id) + WHERE project_id = :project_id AND type NOT IN ( :exclude_property ) + ORDER BY type ASC"; + + $args = array(':project_id' => $project_id, ':exclude_property' => array($trait_type['type4'], $trait_type['type5'])); + $cvterm = chado_query($sql, $args); + + // Only when project has headers. + if ($cvterm->rowCount() > 0) { + // Array to hold the column headers passed to the excel writer. + $col_headers = array(); + // Array to hold standard procedure, which basically is + // the traits definition and collection method. + $instructions_data = array(); + + // Get the data type per unit. This type will be the cell type in the spreadsheet. + $data_type = rawpheno_function_default_unit('type'); + + // Prepend the array with plant property column headers. + $col_headers = array( + 'Plot' => 'integer', + 'Entry' => 'integer', + 'Name' => 'string', + 'Rep' => 'integer', + 'Location' => 'string' + ); + + // Start at F column taking into account plant properties. + // A for Plot, B for Entry and so on (A-E is 5 cols). + $l = 'F'; + $cell_i = array(); + + // Assign the data type for each header based on the unit it contains. + $h = array('name' => 'Trait', 'definition' => 'Definition', 'method' => 'Collection Method'); + + foreach($cvterm as $trait) { + // Get the unit. + $u = rawpheno_function_header_unit($trait->name); + $unit = isset($data_type[$u]) ? $data_type[$u] : 'string'; + + $col_headers[$trait->name] = $unit; + + // Highlight the cells when it is essential trait. + if ($trait->type == $trait_type['type1']) { + array_push($cell_i, $l . '1'); + // Increment F column. + $l++; + } + + // Get header method and definition information. + $t = rawpheno_function_header_properties($trait->project_cvterm_id); + + foreach($h as $m_i => $m) { + $star = ($m_i == 'name') ? '*' : ''; + + if (strlen($t[$m_i]) < 80) { + // Short text, save it. + array_push($instructions_data, array($star . $m . ':', $t[$m_i])); + } + else { + // Hard-wrap long lines into shorter line and put each + // line into a cell/row. + $wrapped_string = wordwrap($t[$m_i], 100, "\n"); + $chunks = explode("\n", $wrapped_string); + + foreach($chunks as $i => $chunk) { + $ins_text = ($i == 0) ? array($star . $m . ':', $chunk) : array('', $chunk); + array_push($instructions_data, $ins_text); + } + } + } + + // Add extra new line. + array_push($instructions_data, array('' , '')); + } + + // Load spreadsheet writer library. + $xlsx_writer = libraries_load('spreadsheet_writer'); + include_once $xlsx_writer['library path'] . '/'. $xlsx_writer['files'][0]; + + $writer = new XLSXWriter(); + // Measurement tab. + @$writer->writeSheet(array(), 'Measurements', $col_headers, + array( + // The entire header row apply these styles. + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '000000', + 'bold' => false, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'verticalAlign' => 'top', + 'horizontalAlign' => 'center', + 'fill' => array('color' => 'F7F7F7'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'rows' => array('0') + ), + // Once the styles above have been applied, style the plant property headers. + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '000000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'verticalAlign' => 'bottom', + 'horizontalAlign' => 'center', + 'fill' => array('color' => 'EAEAEA'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'cells' => array('A1', 'B1', 'C1', 'D1', 'E1') + ), + // Make sure to style the essential trait/header. + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '008000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'verticalAlign' => 'top', + 'horizontalAlign' => 'center', + 'fill' => array('color' => 'F5FFDF'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'cells' => $cell_i + ) + ) + ); + + // Standard procedure tab. + // Load trait definition and data collection method to this sheet. + $instructions_header = array(); + @$writer->writeSheet($instructions_data, 'Instructions', $instructions_header, + array( + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '000000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'columns' => '0', + ), + array( + 'font' => + array( + 'size' => '12', + ), + 'wrapText' => false, + 'columns' => '1', + ), + ) + ); + + // Calculator tab. + $calc_header = array('CALCULATE DAYS TO' => 'string'); + $calc_data = + array( + array('Planting Date', '2015-10-06'), + array('Current Date', date('Y-m-d')), + array('Current "Days till"', '=B3 - B2'), + array('',''), + array('Instructions', ''), + array('', ''), + array('Fill out the planting date indicated in the measurements tab, as well as, the current date.', ''), + array('The "Days till" date will then be calculated for you.', '') + ); + + @$writer->writeSheet($calc_data, 'Calculate Days to', $calc_header, + array( + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '20', + 'color' => '000000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => false, + 'rows' => array('0'), + ), + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => 'FFFFFF', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'fill' => array('color' => '305673'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'rows' => array('1'), + ), + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '000000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'fill' => array('color' => 'F7F7F7'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'rows' => array('2'), + ), + array( + 'font' => + array( + 'name' => 'Arial', + 'size' => '11', + 'color' => '000000', + 'bold' => true, + 'italic' => false, + 'underline' => false + ), + 'wrapText' => true, + 'fill' => array('color' => '79a183'), + 'border' => array('style' => 'thin', 'color' => 'A0A0A0'), + 'rows' => array('3'), + ) + ) + ); + + // Data collection spreadsheet name contains the following: + // Project id, name of the user, date and time. + $filename = 'datacollection_' . $project_id . '_' . str_replace(' ', '_', $GLOBALS['user']->name) .'_'. date('YMd') .'_'. time() . '.xlsx'; + $file = file_save_data($writer->writeToString(), 'public://' . $filename); + + // Launch save file window and ask user to save file. + $http_headers = array( + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ); + + if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) { + $http_headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0'; + $http_headers['Pragma'] = 'public'; + } + else { + $http_headers['Pragma'] = 'no-cache'; + } + + file_transfer($file->uri, $http_headers); + + // Just in case the auto download fails, provide a link to manually download the file. + print 'Download Data Collection Spreadsheet'; + } + else { + // Here project not found. + print 'Experiment not found.'; + } + } + else { + // Project id is not valid. + print 'Experiment ID number is invalid.'; + } +} diff --git a/include/rawpheno.rawdata.form.inc b/includes/rawpheno.rawdata.form.inc old mode 100755 new mode 100644 similarity index 97% rename from include/rawpheno.rawdata.form.inc rename to includes/rawpheno.rawdata.form.inc index 8d92588..e4e0c56 --- a/include/rawpheno.rawdata.form.inc +++ b/includes/rawpheno.rawdata.form.inc @@ -1,128 +1,128 @@ - 'markup', - '#markup' => t('Download Data ❯'), - ); - - // Query project that has data saved to it. - $sql = "SELECT DISTINCT t1.project_id, t1.name - FROM {project} AS t1 RIGHT JOIN pheno_plant_project AS t2 USING(project_id) - WHERE plant_id IS NOT NULL - ORDER BY t1.project_id ASC"; - - $project = chado_query($sql) - ->fetchAllKeyed(); - - $form['rawdata_txt_project'] = array( - '#type' => 'hidden', - '#value' => implode(',', array_keys($project)) - ); - - // Select project select box. - $form['rawdata_sel_project'] = array( - '#type' => 'select', - '#title' => t('Select experiment and trait:'), - '#options' => $project, - '#id' => 'rawdata-sel-project', - ); - - // The summarized list of cvterm_ids from MVIEW returned by inner most query will be passed to function that converts - // comma separated values into individual values (cvterm_id numbers) and the result is the parameter of ANY clause - // that will filter cvterms to only those in the list. Final rows are in JSON object and sorted alphabetically by name - // that will be passed on to the select field of rawdata form. - $sql_cvterm = " - SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( - SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( - SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( - SELECT string_agg(DISTINCT all_traits, ',') AS all_traits - FROM {rawpheno_rawdata_mview} - WHERE plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) - ) AS list_id - )::int[]) - ) AS c_j - WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') - ORDER BY c_j.cvterm_json->>'name' ASC - "; - - // Add first option as instruction to this field. - $default_option = array(0 => 'Select a trait to hightlight in the chart'); - - // Create the select field and populate it with traits specific to a project. - foreach(array_keys($project) as $p_id) { - // Trait id numbers. - $trait_ids = chado_query($sql_cvterm, array(':project_id' => $p_id)) - ->fetchAllKeyed(); - - $traits_array = array_unique($trait_ids); - $traits = $default_option + $traits_array; - - $form['sel_' . $p_id] = array( - '#type' => 'select', - '#title' => ' ', - '#options' => $traits, - '#default_value' => reset($traits), - '#attributes' => array( - 'name' => 'rawdata-sel-trait' - ), - '#states' => array( - 'visible' => array(':input[name="rawdata_sel_project"]' => array('value' => $p_id)), - ), - ); - } - - // SVG elements. - // SVG canvas. - $form['page_content'] = array( - '#type' => 'markup', - '#markup' => ' - - - - - - - - - - ', - ); - - // Hidden field containing url to JSON (summary data). - $form['json_url'] = array( - '#type' => 'hidden', - // Update this line if this module is in a different directory structure. - '#value' => url('/'), - '#attributes' => array('id' => array('rawdata-json')) - ); - - // Attach D3 JS library. - $d3_lib = libraries_load('d3js'); - - if (isset($d3_lib) && !empty($d3_lib['loaded'])) { - $form['d3lib']['#attached']['libraries_load'][] = array('d3js'); - } - - // Attach CSS and JavaScript. - $path = drupal_get_path('module', 'rawpheno') . '/theme/'; - $form['#attached']['css'] = array($path . 'css/rawpheno.rawdata.style.css'); - $form['#attached']['js'] = array($path . 'js/rawpheno.rawdata.script.js'); - - return $form; -} + 'markup', + '#markup' => t('Download Data ❯'), + ); + + // Query project that has data saved to it. + $sql = "SELECT DISTINCT t1.project_id, t1.name + FROM {project} AS t1 RIGHT JOIN pheno_plant_project AS t2 USING(project_id) + WHERE plant_id IS NOT NULL + ORDER BY t1.project_id ASC"; + + $project = chado_query($sql) + ->fetchAllKeyed(); + + $form['rawdata_txt_project'] = array( + '#type' => 'hidden', + '#value' => implode(',', array_keys($project)) + ); + + // Select project select box. + $form['rawdata_sel_project'] = array( + '#type' => 'select', + '#title' => t('Select experiment and trait:'), + '#options' => $project, + '#id' => 'rawdata-sel-project', + ); + + // The summarized list of cvterm_ids from MVIEW returned by inner most query will be passed to function that converts + // comma separated values into individual values (cvterm_id numbers) and the result is the parameter of ANY clause + // that will filter cvterms to only those in the list. Final rows are in JSON object and sorted alphabetically by name + // that will be passed on to the select field of rawdata form. + $sql_cvterm = " + SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( + SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( + SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( + SELECT string_agg(DISTINCT all_traits, ',') AS all_traits + FROM {rawpheno_rawdata_mview} + WHERE plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) + ) AS list_id + )::int[]) + ) AS c_j + WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') + ORDER BY c_j.cvterm_json->>'name' ASC + "; + + // Add first option as instruction to this field. + $default_option = array(0 => 'Select a trait to hightlight in the chart'); + + // Create the select field and populate it with traits specific to a project. + foreach(array_keys($project) as $p_id) { + // Trait id numbers. + $trait_ids = chado_query($sql_cvterm, array(':project_id' => $p_id)) + ->fetchAllKeyed(); + + $traits_array = array_unique($trait_ids); + $traits = $default_option + $traits_array; + + $form['sel_' . $p_id] = array( + '#type' => 'select', + '#title' => ' ', + '#options' => $traits, + '#default_value' => reset($traits), + '#attributes' => array( + 'name' => 'rawdata-sel-trait' + ), + '#states' => array( + 'visible' => array(':input[name="rawdata_sel_project"]' => array('value' => $p_id)), + ), + ); + } + + // SVG elements. + // SVG canvas. + $form['page_content'] = array( + '#type' => 'markup', + '#markup' => ' + + + + + + + + + + ', + ); + + // Hidden field containing url to JSON (summary data). + $form['json_url'] = array( + '#type' => 'hidden', + // Update this line if this module is in a different directory structure. + '#value' => url('/'), + '#attributes' => array('id' => array('rawdata-json')) + ); + + // Attach D3 JS library. + $d3_lib = libraries_load('d3js'); + + if (isset($d3_lib) && !empty($d3_lib['loaded'])) { + $form['d3lib']['#attached']['libraries_load'][] = array('d3js'); + } + + // Attach CSS and JavaScript. + $path = drupal_get_path('module', 'rawpheno') . '/theme/'; + $form['#attached']['css'] = array($path . 'css/rawpheno.rawdata.style.css'); + $form['#attached']['js'] = array($path . 'js/rawpheno.rawdata.script.js'); + + return $form; +} diff --git a/include/rawpheno.tripaldownload.inc b/includes/rawpheno.tripaldownload.inc similarity index 99% rename from include/rawpheno.tripaldownload.inc rename to includes/rawpheno.tripaldownload.inc index d55777c..41b7fb5 100644 --- a/include/rawpheno.tripaldownload.inc +++ b/includes/rawpheno.tripaldownload.inc @@ -30,7 +30,7 @@ function rawpheno_register_trpdownload_type() { 'get_format' => 'rawpheno_trpdownload_get_readable_format', ), ); - + return $types; } @@ -191,7 +191,7 @@ function rawpheno_trpdownload_get_readable_format($vars) { /** * Function callback: generate csv file. */ -function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { +function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { // Get query string and filename. $code = ''; foreach($variables as $l => $v) { diff --git a/include/rawpheno.upload.excel.inc b/includes/rawpheno.upload.excel.inc old mode 100755 new mode 100644 similarity index 97% rename from include/rawpheno.upload.excel.inc rename to includes/rawpheno.upload.excel.inc index ef4fbd8..9c6fdd9 --- a/include/rawpheno.upload.excel.inc +++ b/includes/rawpheno.upload.excel.inc @@ -1,1271 +1,1271 @@ - $fid), - array('print' => TRUE) - ); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 100] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(1); - } - - $xls_file = drupal_realpath($file->uri); - - // INFO: - // Keep a record of which file we are loading in the logs. - // print "\nXLSX File: " . $xls_file . "\n"; - - // Add the libraries needed to parse excel files. - rawpheno_add_parsing_libraries(); - - // Open the file for reading - $xls_obj = rawpheno_open_file($file); - if (!$xls_obj) { - tripal_report_error( - 'rawpheno', - TRIPAL_CRITICAL, - 'Uploading Phenoypic Data: Unable to open file. File=@file', - array('@file' => print_r($file, TRUE)), - array('print' => TRUE) - ); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 101] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(2); - } - - // Change to the correct spreadsheet. - rawpheno_change_sheet($xls_obj, 'measurements'); - - // Compute the total number of rows parsed. - $row_count = rawpheno_count_rows($xls_obj); - - // File to write progress status. - $tmp = file_directory_temp(); - $filename = $tmp . '/' . 'job-progress' . $job_id . '.txt'; - - // Variations of Not Applicable. - $not_applicable = array('na', 'n/a', 'n.a.'); - - // Start Transaction. - $TRANSACTION = db_transaction(); - try { - - // Read each row. - // INFO: - // print "\nNow parsing each row and saving it to the database...\nNumber of rows saved: \n"; - $i = 0; - - // Skip columns. - $skip = array(); - // Project name. - $project_name = rawpheno_function_getproject($project_id); - // Calling all modules implementing hook_rawpheno_ignorecols_valsave_alter(): - drupal_alter('rawpheno_ignorecols_valsave', $skip, $project_name); - - // Each row in the spreadsheet. - foreach ($xls_obj as $row) { - // Create progress update by computing the number of rows saved in percent. - // Write to file and progress bar API reads the content and pass it JSON generator - // in file rawpheno.module function: rawpheno_upload_job_progress_json(). - // Echo to terminal. - - $percent = round(($i / $row_count) * 100); - if ($percent % 10 == 0) { - print $percent . '% complete...' . "\n"; - } - - // To file. - file_unmanaged_save_data($percent, $filename, FILE_EXISTS_REPLACE); - - // HEADER! - // This is the header. - if ($i == 0) { - $header = $row; - - // Find the index number of name header in the spreadsheet. - $name_index = array_search('name', array_map('rawpheno_function_delformat', $header)); - if ($name_index === FALSE) { - tripal_report_error( - 'rawpheno', - TRIPAL_CRITICAL, - 'Uploading Phenoypic Data: Unable to determine the name column.', - array(), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 102] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(31); - } - - $i++; - continue; - } - - // NEXT ROW IF EMPTY! - // Don't continue processing if this row is empty. - if (strlen(trim(implode('', $row))) <= 5) { - break; - } - - // VALID EACH ROW! - // Process the name column first since we need a plant_id before we can insert any more data. - // Name column header goes into pheno_plant. - // Check if stock name exists. - unset($stock_id); - - // Prior to saving, Remove non-breaking whitespace by converting it to a blank space instead of removing it, - // in case user intends a space between words/values. - // trim() implementation below should drop unecessary leading and trailing spaces. - if (preg_match('/\xc2\xa0/', $row[$name_index])) { - $row[$name_index] = preg_replace('/\xc2\xa0/', ' ', $row[$name_index]); - } - - $stock_id = rawpheno_function_getstockid(trim($row[$name_index]), $project_name); - // print $stock_id . ' -- ' . $row[$name_index] . "\n"; - - // Determine if name has a stock id number. - if (isset($stock_id) && $stock_id > 0) { - $p_id = 0; - - // Test if stock was measured in the active project, if not, insert as a new record. - // Otherwise, do more check (plot) to see if plant_id should be re-used. - $sql = " - SELECT plant_id - FROM pheno_plant AS t1 INNER JOIN pheno_plant_project AS t2 USING(plant_id) - WHERE t1.stock_id = :stock_id AND t2.project_id = :project_id LIMIT 1"; - - $args = array(':stock_id' => $stock_id, ':project_id' => $project_id); - $p = chado_query($sql, $args); - - if ($p->rowCount()) { - // Found a stock record in the project. Do more test. - // Array to hold plot headers. - // Plot, Rep, Location, Planting Date (date) - $arr_plot_cols = rawpheno_function_headers('plot'); - - // Construct query string. - // String : stock_id - plot - rep - location - year - // eg. 147-5-2-Saskatoon-2015 - $plot = $stock_id; - - // Given a row, construct the search string (format) above and use it to search if - // the such combination matched any record in the database. - foreach($arr_plot_cols as $plot_col) { - $plot_col = rawpheno_function_delformat($plot_col); - - // Cell value of plot property header. - $col_index = array_search(strtolower($plot_col), array_map('rawpheno_function_delformat', $header)); - $cell_val = trim($row[$col_index]); - - // If planting date - extract the year value. - // Support NA and YYYY in planting date. Use this value when - // value cannot be split by -. - if ($plot_col == 'plantingdate(date)') { - $y = explode('-', $cell_val); - - if (is_array($y)) { - // Extract the year only from planting date. - $cell_val = $y[0]; - } - - // Else use the NA or YYYY. - } - - $plot .= '-' . $cell_val; - } - - // Search the query string. - $p_id = rawpheno_function_plot_exists($plot, $project_id); - } - - - if ($p_id) { - // Plot found - re-use the plant_id. - $pheno_plantid = $p_id; - // INFO: - // print 'FOUND PLOT: ' . $plot . ' [re-using plot id #' . $pheno_plantid . '] ~ '; - } - else { - // Plot not found - insert as new row. - $pheno_plantid = db_insert('pheno_plant') - ->fields(array('stock_id' => $stock_id)) - ->execute(); - - // Map this record/stock to a project. - db_insert('pheno_plant_project') - ->fields(array('project_id' => $project_id, - 'plant_id' => $pheno_plantid)) - ->execute(); - - // INFO: - // print 'NEW STOCK: #' . $stock_id . ' [adding plot id #' . $pheno_plantid . '] ~ '; - } - } - else { - // Warn the admin that germplasm is not available... - // We want to stop loading if this is the case. - tripal_report_error( - 'rawpheno', - TRIPAL_CRITICAL, - 'Uploading Phenoypic Data: Germplasm doesn\'t exist (name=!name; row=!row)', - array('!name' => $row[$name_index], '!row' => $i), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 103] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(32); - } - - // Read each row and each cell. - // Each row will be an array where name is always the first element. - foreach($row as $cell_index => $cell_entry) { - // Skip this cell when col from durpal_alter hook matches the col. - $h = rawpheno_function_delformat($header[$cell_index]); - - if (count($skip) > 0 && in_array($h, $skip)) { - // print 'skipping value : ' . $h . '=' . $cell_entry . "\n"; - continue; - } - - // For consistency, convert all variations of not applicable to NA. - if (is_string($cell_entry) && in_array(strtolower($cell_entry), $not_applicable)) { - $cell_entry = 'NA'; - } - - // We don't want to insert empty data. - // That said, while PHP thinks 0 is empty, we do not. - if (!empty($cell_entry) OR (strval($cell_entry) === '0')) { - - // Get the column header of a cell. - $cell_colheader = trim(str_replace(array("\n", "\r", " "), ' ', $header[$cell_index])); - // Remove additional spaces from column headers. - $cell_colheader = preg_replace('/\s+/', ' ', $cell_colheader); - - - // Determine if user wants to save this trait. - if (count($arr_newheaders) > 0 AND array_key_exists($cell_colheader, $arr_newheaders)) { - if ($arr_newheaders[$cell_colheader]['flag'] == 0) { - // Skip this cell if it is a new column header and user does not want to save - // this new trait; - continue; - } - elseif ($arr_newheaders[$cell_colheader]['flag'] == 1) { - // Get the cvterm name for this new header. - $alt_name = $arr_newheaders[$cell_colheader]['alt_header']; - $n = array('cvterm_id' => $alt_name, 'cv_id' => array('name' => 'phenotype_measurement_types')); - - if (function_exists('chado_get_cvterm')) { - $name = chado_get_cvterm($n); - } - else { - $name = tripal_get_cvterm($n); - } - - $cell_colheader = $name->name; - } - } - - // Prior to saving, Remove non-breaking whitespace by converting it to a blank space instead of removing it, - // in case user intends a space between words/values. - // trim() implementation below should drop unecessary leading and trailing spaces. - if (preg_match('/\xc2\xa0/', $cell_entry)) { - $cell_entry = preg_replace('/\xc2\xa0/', ' ', $cell_entry); - } - - // We always want to strip flanking white space. - // FYI: This is done when the data is validated as well. - $cell_entry = trim($cell_entry); - $cell_colheader = trim($cell_colheader); - - // Determine which table to insert a column header. - // If this is the name column then doing nothing since we've already delt with it above. - if ($cell_index == $name_index) { continue; } - - // PLOT, ENTRY, REP and LOCATION - // Cells containing column headers that are required. - // Traits: plot, entry, rep, location into pheno_plantprop. - elseif (in_array($cell_colheader, $plantprop_headers) && !empty($cell_colheader)) { - $t = array('name' => $cell_colheader, 'cv_id' => array('name' => 'phenotype_plant_property_types')); - - if (function_exists('chado_get_cvterm')) { - $type = chado_get_cvterm($t); - } - else { - $type = tripal_get_cvterm($t); - } - - $type_id = $type->cvterm_id; - - // Ensure that cvterm_id is present before inserting to table - if(isset($type_id)) { - $tmp = db_insert('pheno_plantprop') - ->fields(array('plant_id' => $pheno_plantid, - 'type_id' => $type_id, - 'value' => $cell_entry)) - ->execute(); - - if (!$tmp) { - tripal_report_error( - 'rawpheno', - TRIPAL_ERROR, - 'Uploading Phenoypic Data: Unable to insert plant property. Values=@values', - array('@values' => print_r(array('plant_id' => $pheno_plantid, 'type_id' => $type_id, 'value' => $cell_entry),TRUE)), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 104] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(33); - } - } - - else { - tripal_report_error( - 'rawpheno', - TRIPAL_ERROR, - 'Uploading Phenoypic Data: Plant Property type !type does\'t exist.', - array('!type' => $cell_colheader), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 105] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(34); - } - - } - // THE REST OF THE COLUMN HEADERS - // Everything else into pheno_measurements. - elseif ((!empty($cell_colheader) && $cell_entry != 'NA') - || ($cell_colheader == 'Planting Date (date)' && $cell_entry == 'NA')) { - - // Allow NA only when header is planting date. - - $c_h = rawpheno_function_delformat($cell_colheader); - if (in_array($c_h, $skip)) { - // print 'skipping header : ' . $cell_colheader . "\n"; - continue; - } - - // Get the cvterm_id for the trait measurement. - $type_id = rawpheno_get_trait_id($cell_colheader); - - if (!$type_id) { - tripal_report_error( - 'rawpheno', - TRIPAL_ERROR, - 'Uploading Phenoypic Data: Missing Plant Measurement Type (Header=!colheader).', - array('!colheader' => $cell_colheader), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 106] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(37); - } - - // Retrieve the unit for this trait. - $cv_unit = rawpheno_get_trait_unit($cell_colheader, $type_id); - - if ($cv_unit) { - $unit_id = $cv_unit['id']; - $unit = $cv_unit['name']; - } - else { - tripal_report_error( - 'rawpheno', - TRIPAL_ERROR, - 'Uploading Phenoypic Data: Unable to find unit for Plant Measurement Type (Term=!name; Type ID=!id).', - array('!name' => $cell_colheader, '!id' => $type_id), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 107] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(37); - } - - // Determine if cell requires scale member code. - // When unit is scale, find code equivalent in pheno_scale_member table. - if ($unit == 'scale') { - // Get pheno scale member code - $cvalue_id = db_query("SELECT member_id FROM {pheno_scale_member} - WHERE code = :code LIMIT 1", - array(':code' => trim($cell_entry))) - ->fetchField(); - // We want to report an error if we can't find the scale memeber - // but only if there are any in the first place! - $num_members = db_query('SELECT count(*) FROM {pheno_scale_member} WHERE scale_id=:unit_id', - array(':unit_id' => $unit_id))->fetchField(); - if (!$cvalue_id AND !empty($num_members)) { - tripal_report_error( - 'rawpheno', - TRIPAL_WARNING, - 'Uploading Phenoypic Data: Unable to find scale id for Plant Measurement Type (Trait=!trait; Term=!name; Type ID=!id; Scale Value=!scale).', - array('!trait' => $cell_colheader, '!name' => $unit, '!id' => $unit_id, '!scale' => $cell_entry), - array('print' => TRUE) - ); - } - - // Use default value in the cell if query to find scale member code - // has no equivalent value. - $cvalue_id = (isset($cvalue_id) && $cvalue_id > 0) ? $cvalue_id : $cell_entry; - } - else { - // No scale member value for the rest of traits. - $cvalue_id = ''; - } - - // Insert trait only when type_id and unit_id are not null. - if (isset($type_id) && isset($unit_id)) { - - $temp = db_insert('pheno_measurements') - ->fields(array('plant_id' => $pheno_plantid, - 'type_id' => $type_id, - 'unit_id' => $unit_id, - 'cvalue_id' => $cvalue_id, - 'value' => $cell_entry, - 'modified' => date("D M d, Y h:i:s a", time()))) - ->execute(); - - if (!$temp) { - tripal_report_error( - 'rawpheno', - TRIPAL_ERROR, - 'Uploading Phenoypic Data: Unable to insert measurement. Values=@values.', - array('@values' => print_r(array('plant_id' => $pheno_plantid, - 'type_id' => $type_id, - 'unit_id' => $unit_id, - 'cvalue_id' => $cvalue_id, - 'value' => $cell_entry, - 'modified' => date("D M d, Y h:i:s a", time())),TRUE)), - array('print' => TRUE) - ); - $TRANSACTION->rollback(); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 108] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(38); - } - } - } - } - } - - $i++; - } - } - catch (Exception $e) { - $TRANSACTION->rollback(); - watchdog_exception('rawpheno', $e); - tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 109] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); - exit(4); - } - - unset($TRANSACTION); //Commit - print "Upload complete.\n"; - - print "\nUpdating the materialized view summarizing phenotypic data.\n"; - $mview_id = tripal_get_mview_id('rawpheno_rawdata_summary'); - if ($mview_id) tripal_populate_mview($mview_id); -} - - -/** - * Validates an excel file using any validators registered with rawpheno. - * - * @param $file - * A drupal managed_file object describing the uploaded spreadsheet. - * @param $project_id - * An integer containing project id selected in the project select box. - * This will map the data submitted to a project. - * @param $source - * A string containing the source of the file upload - Upload Data or Backup File. - * - * @return - * An array containing the validation result from each validator. - */ -function rawpheno_validate_excel_file($file, $project_id, $source) { - $status = array(); - - // Process the validators to make them easier to use. - // Specifically, sort them by their scope. - $validators = array(); - $all_validators = module_invoke_all('rawpheno_validators'); - foreach($all_validators as $k => $v) { - $validators[ $v['scope'] ][ $k ] = $v; - } - - // Todo list. - $all_scope_validators = array('project', 'file', 'all', 'header', 'subset'); - - // Add the libraries needed to parse excel files. - rawpheno_add_parsing_libraries(); - - // Before performing any validation to the excel file. Ensure first that a project is selected. - foreach ($validators['project'] as $prj_validator_name => $prj_validator) { - if (isset($prj_validator['validation callback']) AND function_exists($prj_validator['validation callback'])) { - $status[ $prj_validator_name ] = call_user_func($prj_validator['validation callback'], $project_id); - - // If returned false then halt validation. - if ($status[ $prj_validator_name ] === FALSE) { - // Fail the project and set the rest to TODO. - $status[ $prj_validator_name ] = FALSE; - - // Todo the rest of validators. - // Since this is project scope and it got falsed - remove the project. - unset($all_scope_validators[0]); - foreach($all_scope_validators as $v) { - foreach($validators[ $v ] as $v_name => $validator) { - $status[ $v_name ] = 'todo'; - } - } - - return $status; - } - } - } - - // First validate the whole file. If any of these fail then halt validation. - foreach ($validators['file'] as $validator_name => $validator) { - if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { - $status[ $validator_name ] = call_user_func($validator['validation callback'], $file); - - // If returned false then halt validation. - if ($status[ $validator_name ] === FALSE) { - // Fail the file and set the rest to TODO but set the project to passed - // first since it is assumed that project validator returned a passed value. - $status[ 'project_selected' ] = TRUE; - $status[ $validator_name ] = FALSE; - - // Todo the rest of validators. - // Since project is completed. skip this scope. - unset($all_scope_validators[0]); - foreach($all_scope_validators as $v) { - foreach($validators[ $v ] as $v_name => $validator) { - if ($status[ $v_name ] === TRUE) { - $status[ $v_name ] = TRUE; - } - elseif ($status[ $v_name ] === FALSE) { - $status[ $v_name ] = FALSE; - } - else { - $status[ $v_name ] = 'todo'; - } - } - } - - return $status; - } - } - } - - // Open the file for reading - $xls_obj = rawpheno_open_file($file); - - // Change to the correct spreadsheet. - rawpheno_change_sheet($xls_obj, 'measurements'); - - // This increment variable $i is required since xls and xlsx - // parsers assign array index differently. - // XLS starts at 1, while XLSX at 0; - $i = 0; - - // Variations of Not Applicable. - $not_applicable = array('na', 'n/a', 'n.a.'); - - // Skip columns. - $skip = array(); - // Project name. - $project_name = rawpheno_function_getproject($project_id); - // Calling all modules implementing hook_rawpheno_ignorecols_valsave_alter(): - drupal_alter('rawpheno_ignorecols_valsave', $skip, $project_name); - - // Iterate though each row. - $num_errored_rows = 0; - $storage = array(); - foreach($xls_obj as $row) { - $i++; - - // Convert row into a string and check the length. - // This will exclude empty rows. - if (strlen(trim(implode('', $row))) >= 5) { - - // VALIDATE THE HEADER. - if ($i == 1) { - // Save the header for later. - $header = array(); - $new_header = array(); - // Checking plot value requires cell value in Planting Date (date) and Location. - // Store index numbers of these two traits. - $plot_req = array(); - - $o = 0; - foreach ($row as $r) { - $without_format = rawpheno_function_delformat($r); - - // To maintain index of both cells and header, tag either to skip or process - // based on headers in drupal_alter hook. - $s = (in_array($without_format, $skip)) ? 1 : 0; - - // Remove new lines. - $rem_newline = str_replace(array("\n", "\r"), ' ', $r); - // Remove extra spaces. - $rem_spaces = preg_replace('/\s+/', ' ', $rem_newline); - // Remove leading and trailing spaces. - $r = trim($rem_spaces); - $no_units = rawpheno_get_trait_name($r); - - $header[] = array( - 'no format' => $without_format, - 'original' => $r, - 'units' => rawpheno_function_unit($without_format), - 'no units' => $no_units, - 'skip' => $s, - ); - - // Store index number of Plot trait requirements. - if (!isset($plot_req['planting date (date)']) && $without_format == 'plantingdate(date)') { - $plot_req['planting date (date)'] = $o; - } - elseif (!isset($plot_req['location']) && $without_format == 'location') { - $plot_req['location'] = $o; - } - - $o++; - } - - // Foreach validator with a scope of header, execute the validation callback & save the results. - foreach($validators['header'] as $validator_name => $validator) { - if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { - $result = call_user_func($validator['validation callback'], $header, $project_id); - - // The status needs to keep track of which rows failed for a given header. - if ($result === FALSE) { - $status[ $validator_name ] = $i; - } - elseif (is_array($result)) { - $status[ $validator_name ] = $result; - } - } - } - } - // VALIDATE THE ROW. - else { - - $row_has_error = FALSE; - foreach ($row as $column_index => $cell) { - if ($header[$column_index]['skip'] == 1) continue; - - $column_name = $header[$column_index]['no units']; - if (empty($column_name)) continue; - - // Prior to validating, Remove non-breaking whitespace by converting it to a blank space instead of removing it, - // in case user intends a space between words/values. - // trim() implementation below should drop unecessary leading and trailing spaces. - if (preg_match('/\xc2\xa0/', $cell)) { - $cell = preg_replace('/\xc2\xa0/', ' ', $cell); - } - - // We always want to strip flanking white space. - // FYI: This is done when the data is loaded as well. - $cell = trim($cell); - - // For consistency, convert all variations of not applicable to NA. - if (is_string($cell) && in_array(strtolower($cell), $not_applicable)) { - $cell = 'NA'; - } - - // Foreach validator: - foreach (array('all','subset') as $scope) { - foreach($validators[$scope] as $validator_name => $validator) { - - // Only validate if there is a validation callback. - if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { - - // Only validate if the current validator applies to the current column. - // Specifically, if there are no defined headers it's applicable to - // OR if the current header is in the list of applicable headers. - if (!isset($validator['headers']) OR in_array($column_name, $validator['headers'])) { - - // Execute the validation callback & save the results. - $tmp_storage = (isset($storage[$validator_name])) ? $storage[$validator_name] : array(); - $context = array( - 'row index' => $i, - 'column index' => $column_index, - 'row' => $row, - 'header' => $header - ); - - // If column header is Plot, attach Plot validation requirement to - // $context array. The indexes will be used to fetch the cell value in context row. - if ($column_name == 'Plot') { - $context['plot_req'] = $plot_req; - } - - $result = $validator['validation callback']($cell, $context, $tmp_storage, $project_id); - - // Note: we use tmp storage b/c passing $storage[$validator_name] directly - // doesn't seem to work. - $storage[$validator_name] = $tmp_storage; - - // The status needs to keep track of which rows failed for a given header. - if (is_array($result)) { - $status[ $validator_name ][ $column_name ][$i] = $result; - $row_has_error = TRUE; - } - elseif ($result !== TRUE) { - $status[ $validator_name ][ $column_name ][$i] = $i; - $row_has_error = TRUE; - } - } - } - } - } - } - - if ($row_has_error) $num_errored_rows++; - } - - // Only check until you have 10 rows with errors. - if ($num_errored_rows >= 10) { - // We only want to present the warning if this is not the end of the file ;-) - $has_next = $xls_obj->next(); - if ($has_next AND strlen(trim(implode('', $has_next))) >= 1) { - $check_limit_message = "We have only checked the first $i lines of your file. Please fix the errors reported below and then upload the fixed file."; - - if ($source == 'upload') { - drupal_set_message($check_limit_message, 'error'); - return $status; - } - elseif ($source == 'backup') { - return array('status' => $status, 'check_limit' => $check_limit_message); - } - } - } - } - } - - // Make sure all validators are represented in status. - // If they are not already then a failure wasn't recorded -thus they passed :-). - foreach($all_validators as $validator_name => $validator) { - if (!isset($status[$validator_name])) { - $status[$validator_name] = TRUE; - } - } - - return $status; -} - -/** - * Open the Excel file using the spreadsheet reader. - * - * @param $file - * A Drupal managed file object. - * @return - * An object representing the Excel file. - */ -function rawpheno_open_file($file) { - // Grab the path and extension from the file. - $xls_file = drupal_realpath($file->uri); - $xls_extension = pathinfo($file->filename, PATHINFO_EXTENSION); - - // Validate that the spreadsheet is either xlsx or xls and open the spreadsheet using - // the correct class. - // XLSX: - if ($xls_extension == 'xlsx') { - $xls_obj = new SpreadsheetReader_XLSX($xls_file); - } - // XLS: - elseif ($xls_extension == 'xls') { - // PLS INCLUDE THIS FILE ONLY FOR XLS TYPE. - $xls_lib = libraries_load('spreadsheet_reader'); - $lib_path = $xls_lib['path']; - - include_once $lib_path . 'SpreadsheetReader_XLS.php'; - $xls_obj = new SpreadsheetReader_XLS($xls_file); - } - - return $xls_obj; -} - -/** - * Changes the worksheet in the Excel Object. - * - * @param $xls_obj - * The object describing this Excel workbook. - * @param $tab_name - * The name of the tab you would like to switch to. - * @return - * TRUE if it found the tab and FALSE otherwise. - */ -function rawpheno_change_sheet(&$xls_obj, $tab_name) { - // Get all the sheets in the workbook. - $xls_sheets = $xls_obj->Sheets(); - - // Locate the measurements sheet. - foreach($xls_sheets as $sheet_key => $sheet_value) { - $xls_obj->ChangeSheet($sheet_key); - - // Only process the measurements worksheet. - if (rawpheno_function_delformat($sheet_value) == 'measurements') { - return TRUE; - } - } - - return FALSE; -} - - -/** - * Adds the necessary files for EXCEL parsing. - */ -function rawpheno_add_parsing_libraries($file_type = 'XLSX') { - // Function call libraries_load() base on the implementation - // of hook_libraries_info() in rawpheno.module. - $xls_lib = libraries_load('spreadsheet_reader'); - // Library path information returned will be used - // to include individual library files required. - $lib_path = $xls_lib['path']; - - // Include parser library. PLS DO NOT ALTER ORDER!!! - // To stop parser from auto formatting date to MM/DD/YY, - // suggest a new date format YYYY-mm-dd in: - // line 678 in excel_reader2.php - // 0xe => "m/d/Y", to 0xe => "Y-m-d", - // line 834 in SpreadsheetReader_XLSX.php - // $Value = $Value -> format($Format['Code']); to $Value = $Value -> format('Y-m-d'); - // - include_once $lib_path . 'php-excel-reader/excel_reader2.php'; - include_once $lib_path . 'SpreadsheetReader_XLSX.php'; - include_once $lib_path . 'SpreadsheetReader.php'; - - if ($file_type == 'XLS') { - // PLS INCLUDE THIS FILE ONLY FOR XLS TYPE. - include_once $lib_path . 'SpreadsheetReader_XLS.php'; - } -} - - -/** - * Function to remove all formatting from a cell value. - * - * @param $xls_cell_value - * Contains a value of a cell. - * @return - * Contains a cell value with all formatting removed. - */ -function rawpheno_function_delformat($xls_cell_value) { - // Remove any extra spaces, new lines, leading and trainling spaces - // and covert the final result to lowercase. - return trim(strtolower(preg_replace('!\s+!', '', $xls_cell_value))); -} - - -/** - * Function to extract the unit from the column header. - * - * @param $xls_header_cell - * A string containing a column header. - * @return - * A string containing the unit found from the column header. - */ -function rawpheno_function_unit($xls_header_cell) { - // Remove all formatting. - $temp_value = rawpheno_function_delformat($xls_header_cell); - - // If this is a scale then return that. - if (preg_match('/\(scale/',$temp_value)) { - return 'scale'; - } - - // Remove the following characters. - $cell_value = str_replace(array(';', '1st', '2nd', 'r1', 'r3', 'r5', 'r7', ': 1-5'), '', $temp_value); - - // Extract text information inside the parenthesis. - preg_match("/.*\(([^)]*)\)/", $cell_value, $match); - - // Return unit found, or default to text if no unit. - return (isset($match[1])) ? trim($match[1]) : 'text'; -} - - -/** - * Function to determine additional column headers in the spreadsheet. Additional column headers are - * headers that are no part of the predefined headers set of the project. - * - * @param $file - * The full path to the excel file containing data. - * @param $project_id - * Project id number the spreadsheet is specific to. - * @return - * An array containing all additional column headers detected. - */ -function rawpheno_indicate_new_headers($file, $project_id) { - // Retrieve the header for the indicated file. - rawpheno_add_parsing_libraries(); - $xls_obj = rawpheno_open_file($file); - rawpheno_change_sheet($xls_obj, 'measurements'); - - // Note: we use the foreach here - // because the library documentation doesn't have a single fetch function. - foreach ($xls_obj as $xls_headers) { break; } - - // Array to hold epected column headers specific to a given project. - $expected_headers = rawpheno_project_traits($project_id); - - // Remove any formatting in each column headers. - $expected_headers = array_map('rawpheno_function_delformat', $expected_headers); - - // Array to hold new column headers. - $new_headers = array(); - - // Assuming the file actually has a non-empty header row... - if (count($xls_headers) > 0) { - // Read each column header and compare against expected column headers. - foreach($xls_headers as $value) { - $temp_value = rawpheno_function_delformat($value); - - // Determine if column header exists in the expected column headers. - if (!in_array($temp_value, $expected_headers) && !empty($value)) { - // Not in expected column headers, save it as new header. - $value = preg_replace('/\s+/', ' ', $value); - $new_headers[] = $value; - } - } - } - - return $new_headers; -} - - -/** - * Get all column headers. - * - * @param $file - * The full path to the excel file containing data. - * @return - * An array of headers. - */ -function rawpheno_all_headers($file) { - // Retrieve the header for the indicated file. - rawpheno_add_parsing_libraries(); - $xls_obj = rawpheno_open_file($file); - rawpheno_change_sheet($xls_obj, 'measurements'); - // Note: we use the foreach here - // because the library documentation doesn't have a single fetch function. - - $arr_headers = array(); - foreach ($xls_obj as $xls_headers) { - foreach($xls_headers as $h) { - if (strlen($h) > 2) { - $arr_headers[] = trim($h); - } - } - break; - } - - return $arr_headers; -} - - -/** - * Count all rows in a spreadsheet. - * - * @param $file - * The full path to the excel file containing data. - * @return - * An integer value of the total rows. - */ -function rawpheno_count_rows($xls_obj) { - // Row of 5 chars or more long is a row. - $count_rows = 0; - foreach ($xls_obj as $row) { - if (strlen(implode('', $row)) > 5) { - $count_rows++; - } - } - - // Less header row. - return $count_rows - 1; -} - - -/** - * Retrieve the cvterm_id for a given header. - * - * @param $header - * The unchanged/original header text for the trait. - * @return - * The cvterm_id for the trait. - */ -function rawpheno_get_trait_id($header) { - // New lines. - $header = str_replace(array("\n", "\r"), ' ', $header); - // Extra spaces. - $header = preg_replace('!\s+!', ' ', $header); - - // Query trait. Module stores unit in lowercase but user can use any case - // in the spreadsheet. eg Planting Date (date) and Planting Date (Date). - // @note cvterm.name + cv.name + not obsolete combination is unique (constraints). - $sql = "SELECT t2.cvterm_id - FROM {cv} AS t1 INNER JOIN {cvterm} AS t2 USING(cv_id) - WHERE lower(t2.name) = :cvterm_name AND t1.name = :cv_name AND is_obsolete = 0"; - - $args = array(':cvterm_name' => trim(strtolower($header)), ':cv_name' => 'phenotype_measurement_types'); - $type = chado_query($sql, $args) - ->fetchObject(); - - if ($type->cvterm_id) { - return $type->cvterm_id; - } - - return FALSE; -} - - -/** - * Retrieve the unit for the trait. - * - * @param $trait_name - * The name of the trait as found in the column header. - * @param $trait_id - * The cvterm_id of the trait if you have it (OPTIONAL). - * @return - * Returns an array with the cvterm_id and name of the unit. - */ -function rawpheno_get_trait_unit($trait_name, $trait_id = NULL) { - // Get the trait id if that is not provided to us. - if ($trait_id == NULL) { - $trait_id = rawpheno_get_trait_id($trait_name); - } - - // First we try to get the unit through relationships since that avoids making assumptions. - // in chado.cvterm_relationship. - // @todo make this more specific (restrict relationship by type?) - // @todo rather then limit, check if there are 1+ and warn the admin. - $sql = "SELECT cvterm_id, name FROM {cvterm} WHERE cvterm_id = - (SELECT subject_id FROM {cvterm_relationship} WHERE object_id = :trait LIMIT 1)"; - - $args = array(':trait' => $trait_id); - $unit = chado_query($sql, $args); - - if ($unit->rowCount() > 0) { - $r = $unit->fetchObject(); - return array('id' => $r->cvterm_id, 'name' => $r->name); - } - - // If that doesn't work then we try to extract it from the name. - // Note: if the following function is unable to extract the unit then it will default to text. - $unit_name = rawpheno_function_unit($trait_name); - - // Column header does not contain unit, use text as default - if (function_exists('chado_get_cvterm')) { - $cvterm = chado_get_cvterm(array('name' => $unit_name, 'cv_id' => array('name' => 'phenotype_measurement_units'))); - } - else { - $cvterm = tripal_get_cvterm(array('name' => $unit_name, 'cv_id' => array('name' => 'phenotype_measurement_units'))); - } - - if ($cvterm) { - return array('id' => $cvterm->cvterm_id, 'name' => $cvterm->name); - } - - return FALSE; -} - - -/** - * Remove the unit part from a trait. - * - * @param $trait_name - * A string containing the trait name as formatted in cvterm name. - * @return - * A string containing the trait name without the unit. - */ -function rawpheno_get_trait_name($trait_name) { - $t = explode('(', $trait_name); - - // Given a trait as defined in cvterm name in the following format: - // Trait name (Trait Rep; Unit), extract the trait name only and return - // the extracted name. - return (count($t) > 1) ? trim(preg_replace('/\(.*/', ' ', $trait_name)) : $trait_name; -} - - -/** - * Get all the essential traits in a project. - * - * @param $project_id - * An integer containing the project ID number. - * @return - * An array containing all essential traits in a project. - */ -function rawpheno_project_essential_traits($project_id) { - if (isset($project_id) AND $project_id > 0) { - // Get array of trait types - $trait_type = rawpheno_function_trait_types(); - - // Array to hold trait names. - $arr_essential_traits = array(); - - // Query essential traits in a project. - $sql = "SELECT TRIM(t1.name) AS cvterm - FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) - WHERE - t2.project_id = :project_id - AND t2.type IN (:essential) - ORDER BY t1.name ASC"; - - $args = array(':project_id' => $project_id, ':essential' => array($trait_type['type1'], $trait_type['type4'])); - $trait = chado_query($sql, $args); - - foreach($trait as $t) { - $m = rawpheno_get_trait_name($t->cvterm); - $arr_essential_traits[] = $m; - } - - // Add Name column header to the traits returned. - $arr_essential_traits[] = 'Name'; - - return $arr_essential_traits; - } -} - - -/** - * Get all the plant property traits in a project selected. - * - * @param $project_id - * An integer containing the project ID number. - * @return - * An array containing all essential traits in a project. - */ -function rawpheno_project_plantproperty_traits($project_id) { - if (isset($project_id) AND $project_id > 0) { - // Get array of trait types - $trait_type = rawpheno_function_trait_types(); - - // Array to hold trait names. - $arr_plantproperty_traits = array(); - - // Query plant property traits in a project. - $sql = "SELECT TRIM(t1.name) AS cvterm - FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) - WHERE t2.project_id = :project_id AND t2.type = :plantproperty - ORDER BY t1.name ASC"; - - // traits of type plantproperty. - $args = array(':project_id' => $project_id, ':plantproperty' => $trait_type['type4']); - $trait = chado_query($sql, $args); - - foreach($trait as $t) { - // Remove the trait rep and unit from the trait. - $m = rawpheno_get_trait_name($t->cvterm); - $arr_plantproperty_traits[] = $m; - } - - return $arr_plantproperty_traits; - } -} - - -/** - * Get all traits available in a project whether essential or not. - * - * @param $project_id - * An integer containing the project ID number. - * @return - * An array containing all traits available in a project. - */ -function rawpheno_project_traits($project_id) { - if (isset($project_id) AND $project_id > 0) { - $arr_trait = array(); - - // Query column headers in a project. - $sql = "SELECT TRIM(t1.name) AS cvterm - FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) - WHERE t2.project_id = :project_id - ORDER BY t1.name ASC"; - - $args = array(':project_id' => $project_id); - $trait = chado_query($sql, $args); - - foreach($trait as $t) { - $arr_trait[] = $t->cvterm; - } - - // Add Name column header to the traits returned. - $arr_trait[] = 'Name'; - - return $arr_trait; - } -} - - -/** - * Function to fetch information about a unit (Describe method) when available. - * - * @param $cvterm_id - * An integer containing the cvterm id of a trait. - */ -function rawpheno_function_cvterm_properties($cvterm_id) { - // Narrow the search to cvterm of type measurement units. - if (function_exists('chado_get_cv')) { - $cv_unit = chado_get_cv(array('name' => 'phenotype_measurement_units')); - } - else { - $cv_unit = tripal_get_cv(array('name' => 'phenotype_measurement_units')); - } - - // In form state 2, describe header, user has the opportunity to describe the unit (Describe method field). - // This information is stored in cvterm relationship together with the cvterm id of the unit as the subject_id - // and cvterm_id of the header as the object_id. Given a header cvterm id, get the subject id and use it to - // get the information required from cvtermprop table. - // @todo check if there is more then one unit for a given trait and if so, warn the admin. - $sql = "SELECT subject_id FROM {cvterm_relationship} WHERE object_id = :cvterm_id AND type_id = :cv_unit LIMIT 1"; - $args = array(':cvterm_id' => $cvterm_id, ':cv_unit' => $cv_unit->cv_id); - - $d = chado_query($sql, $args) - ->fetchField(); - - // @note cvterm_id + type_id + rank is unique (constraint). - $sql = "SELECT value FROM {cvtermprop} WHERE type_id = :cv_unit AND cvterm_id = :cvterm_id AND rank = 0"; - $args = array(':cv_unit' => $cv_unit->cv_id, ':cvterm_id' => $d); - - $d = chado_query($sql, $args); - - if ($d->rowCount() == 1) { - return $d->fetchField(); - } - else { - return 'Describe the method used not available'; - } -} + $fid), + array('print' => TRUE) + ); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 100] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(1); + } + + $xls_file = drupal_realpath($file->uri); + + // INFO: + // Keep a record of which file we are loading in the logs. + // print "\nXLSX File: " . $xls_file . "\n"; + + // Add the libraries needed to parse excel files. + rawpheno_add_parsing_libraries(); + + // Open the file for reading + $xls_obj = rawpheno_open_file($file); + if (!$xls_obj) { + tripal_report_error( + 'rawpheno', + TRIPAL_CRITICAL, + 'Uploading Phenoypic Data: Unable to open file. File=@file', + array('@file' => print_r($file, TRUE)), + array('print' => TRUE) + ); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 101] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(2); + } + + // Change to the correct spreadsheet. + rawpheno_change_sheet($xls_obj, 'measurements'); + + // Compute the total number of rows parsed. + $row_count = rawpheno_count_rows($xls_obj); + + // File to write progress status. + $tmp = file_directory_temp(); + $filename = $tmp . '/' . 'job-progress' . $job_id . '.txt'; + + // Variations of Not Applicable. + $not_applicable = array('na', 'n/a', 'n.a.'); + + // Start Transaction. + $TRANSACTION = db_transaction(); + try { + + // Read each row. + // INFO: + // print "\nNow parsing each row and saving it to the database...\nNumber of rows saved: \n"; + $i = 0; + + // Skip columns. + $skip = array(); + // Project name. + $project_name = rawpheno_function_getproject($project_id); + // Calling all modules implementing hook_rawpheno_ignorecols_valsave_alter(): + drupal_alter('rawpheno_ignorecols_valsave', $skip, $project_name); + + // Each row in the spreadsheet. + foreach ($xls_obj as $row) { + // Create progress update by computing the number of rows saved in percent. + // Write to file and progress bar API reads the content and pass it JSON generator + // in file rawpheno.module function: rawpheno_upload_job_progress_json(). + // Echo to terminal. + + $percent = round(($i / $row_count) * 100); + if ($percent % 10 == 0) { + print $percent . '% complete...' . "\n"; + } + + // To file. + file_unmanaged_save_data($percent, $filename, FILE_EXISTS_REPLACE); + + // HEADER! + // This is the header. + if ($i == 0) { + $header = $row; + + // Find the index number of name header in the spreadsheet. + $name_index = array_search('name', array_map('rawpheno_function_delformat', $header)); + if ($name_index === FALSE) { + tripal_report_error( + 'rawpheno', + TRIPAL_CRITICAL, + 'Uploading Phenoypic Data: Unable to determine the name column.', + array(), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 102] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(31); + } + + $i++; + continue; + } + + // NEXT ROW IF EMPTY! + // Don't continue processing if this row is empty. + if (strlen(trim(implode('', $row))) <= 5) { + break; + } + + // VALID EACH ROW! + // Process the name column first since we need a plant_id before we can insert any more data. + // Name column header goes into pheno_plant. + // Check if stock name exists. + unset($stock_id); + + // Prior to saving, Remove non-breaking whitespace by converting it to a blank space instead of removing it, + // in case user intends a space between words/values. + // trim() implementation below should drop unecessary leading and trailing spaces. + if (preg_match('/\xc2\xa0/', $row[$name_index])) { + $row[$name_index] = preg_replace('/\xc2\xa0/', ' ', $row[$name_index]); + } + + $stock_id = rawpheno_function_getstockid(trim($row[$name_index]), $project_name); + // print $stock_id . ' -- ' . $row[$name_index] . "\n"; + + // Determine if name has a stock id number. + if (isset($stock_id) && $stock_id > 0) { + $p_id = 0; + + // Test if stock was measured in the active project, if not, insert as a new record. + // Otherwise, do more check (plot) to see if plant_id should be re-used. + $sql = " + SELECT plant_id + FROM pheno_plant AS t1 INNER JOIN pheno_plant_project AS t2 USING(plant_id) + WHERE t1.stock_id = :stock_id AND t2.project_id = :project_id LIMIT 1"; + + $args = array(':stock_id' => $stock_id, ':project_id' => $project_id); + $p = chado_query($sql, $args); + + if ($p->rowCount()) { + // Found a stock record in the project. Do more test. + // Array to hold plot headers. + // Plot, Rep, Location, Planting Date (date) + $arr_plot_cols = rawpheno_function_headers('plot'); + + // Construct query string. + // String : stock_id - plot - rep - location - year + // eg. 147-5-2-Saskatoon-2015 + $plot = $stock_id; + + // Given a row, construct the search string (format) above and use it to search if + // the such combination matched any record in the database. + foreach($arr_plot_cols as $plot_col) { + $plot_col = rawpheno_function_delformat($plot_col); + + // Cell value of plot property header. + $col_index = array_search(strtolower($plot_col), array_map('rawpheno_function_delformat', $header)); + $cell_val = trim($row[$col_index]); + + // If planting date - extract the year value. + // Support NA and YYYY in planting date. Use this value when + // value cannot be split by -. + if ($plot_col == 'plantingdate(date)') { + $y = explode('-', $cell_val); + + if (is_array($y)) { + // Extract the year only from planting date. + $cell_val = $y[0]; + } + + // Else use the NA or YYYY. + } + + $plot .= '-' . $cell_val; + } + + // Search the query string. + $p_id = rawpheno_function_plot_exists($plot, $project_id); + } + + + if ($p_id) { + // Plot found - re-use the plant_id. + $pheno_plantid = $p_id; + // INFO: + // print 'FOUND PLOT: ' . $plot . ' [re-using plot id #' . $pheno_plantid . '] ~ '; + } + else { + // Plot not found - insert as new row. + $pheno_plantid = db_insert('pheno_plant') + ->fields(array('stock_id' => $stock_id)) + ->execute(); + + // Map this record/stock to a project. + db_insert('pheno_plant_project') + ->fields(array('project_id' => $project_id, + 'plant_id' => $pheno_plantid)) + ->execute(); + + // INFO: + // print 'NEW STOCK: #' . $stock_id . ' [adding plot id #' . $pheno_plantid . '] ~ '; + } + } + else { + // Warn the admin that germplasm is not available... + // We want to stop loading if this is the case. + tripal_report_error( + 'rawpheno', + TRIPAL_CRITICAL, + 'Uploading Phenoypic Data: Germplasm doesn\'t exist (name=!name; row=!row)', + array('!name' => $row[$name_index], '!row' => $i), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 103] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(32); + } + + // Read each row and each cell. + // Each row will be an array where name is always the first element. + foreach($row as $cell_index => $cell_entry) { + // Skip this cell when col from durpal_alter hook matches the col. + $h = rawpheno_function_delformat($header[$cell_index]); + + if (count($skip) > 0 && in_array($h, $skip)) { + // print 'skipping value : ' . $h . '=' . $cell_entry . "\n"; + continue; + } + + // For consistency, convert all variations of not applicable to NA. + if (is_string($cell_entry) && in_array(strtolower($cell_entry), $not_applicable)) { + $cell_entry = 'NA'; + } + + // We don't want to insert empty data. + // That said, while PHP thinks 0 is empty, we do not. + if (!empty($cell_entry) OR (strval($cell_entry) === '0')) { + + // Get the column header of a cell. + $cell_colheader = trim(str_replace(array("\n", "\r", " "), ' ', $header[$cell_index])); + // Remove additional spaces from column headers. + $cell_colheader = preg_replace('/\s+/', ' ', $cell_colheader); + + + // Determine if user wants to save this trait. + if (count($arr_newheaders) > 0 AND array_key_exists($cell_colheader, $arr_newheaders)) { + if ($arr_newheaders[$cell_colheader]['flag'] == 0) { + // Skip this cell if it is a new column header and user does not want to save + // this new trait; + continue; + } + elseif ($arr_newheaders[$cell_colheader]['flag'] == 1) { + // Get the cvterm name for this new header. + $alt_name = $arr_newheaders[$cell_colheader]['alt_header']; + $n = array('cvterm_id' => $alt_name, 'cv_id' => array('name' => 'phenotype_measurement_types')); + + if (function_exists('chado_get_cvterm')) { + $name = chado_get_cvterm($n); + } + else { + $name = tripal_get_cvterm($n); + } + + $cell_colheader = $name->name; + } + } + + // Prior to saving, Remove non-breaking whitespace by converting it to a blank space instead of removing it, + // in case user intends a space between words/values. + // trim() implementation below should drop unecessary leading and trailing spaces. + if (preg_match('/\xc2\xa0/', $cell_entry)) { + $cell_entry = preg_replace('/\xc2\xa0/', ' ', $cell_entry); + } + + // We always want to strip flanking white space. + // FYI: This is done when the data is validated as well. + $cell_entry = trim($cell_entry); + $cell_colheader = trim($cell_colheader); + + // Determine which table to insert a column header. + // If this is the name column then doing nothing since we've already delt with it above. + if ($cell_index == $name_index) { continue; } + + // PLOT, ENTRY, REP and LOCATION + // Cells containing column headers that are required. + // Traits: plot, entry, rep, location into pheno_plantprop. + elseif (in_array($cell_colheader, $plantprop_headers) && !empty($cell_colheader)) { + $t = array('name' => $cell_colheader, 'cv_id' => array('name' => 'phenotype_plant_property_types')); + + if (function_exists('chado_get_cvterm')) { + $type = chado_get_cvterm($t); + } + else { + $type = tripal_get_cvterm($t); + } + + $type_id = $type->cvterm_id; + + // Ensure that cvterm_id is present before inserting to table + if(isset($type_id)) { + $tmp = db_insert('pheno_plantprop') + ->fields(array('plant_id' => $pheno_plantid, + 'type_id' => $type_id, + 'value' => $cell_entry)) + ->execute(); + + if (!$tmp) { + tripal_report_error( + 'rawpheno', + TRIPAL_ERROR, + 'Uploading Phenoypic Data: Unable to insert plant property. Values=@values', + array('@values' => print_r(array('plant_id' => $pheno_plantid, 'type_id' => $type_id, 'value' => $cell_entry),TRUE)), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 104] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(33); + } + } + + else { + tripal_report_error( + 'rawpheno', + TRIPAL_ERROR, + 'Uploading Phenoypic Data: Plant Property type !type does\'t exist.', + array('!type' => $cell_colheader), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 105] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(34); + } + + } + // THE REST OF THE COLUMN HEADERS + // Everything else into pheno_measurements. + elseif ((!empty($cell_colheader) && $cell_entry != 'NA') + || ($cell_colheader == 'Planting Date (date)' && $cell_entry == 'NA')) { + + // Allow NA only when header is planting date. + + $c_h = rawpheno_function_delformat($cell_colheader); + if (in_array($c_h, $skip)) { + // print 'skipping header : ' . $cell_colheader . "\n"; + continue; + } + + // Get the cvterm_id for the trait measurement. + $type_id = rawpheno_get_trait_id($cell_colheader); + + if (!$type_id) { + tripal_report_error( + 'rawpheno', + TRIPAL_ERROR, + 'Uploading Phenoypic Data: Missing Plant Measurement Type (Header=!colheader).', + array('!colheader' => $cell_colheader), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 106] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(37); + } + + // Retrieve the unit for this trait. + $cv_unit = rawpheno_get_trait_unit($cell_colheader, $type_id); + + if ($cv_unit) { + $unit_id = $cv_unit['id']; + $unit = $cv_unit['name']; + } + else { + tripal_report_error( + 'rawpheno', + TRIPAL_ERROR, + 'Uploading Phenoypic Data: Unable to find unit for Plant Measurement Type (Term=!name; Type ID=!id).', + array('!name' => $cell_colheader, '!id' => $type_id), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 107] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(37); + } + + // Determine if cell requires scale member code. + // When unit is scale, find code equivalent in pheno_scale_member table. + if ($unit == 'scale') { + // Get pheno scale member code + $cvalue_id = db_query("SELECT member_id FROM {pheno_scale_member} + WHERE code = :code LIMIT 1", + array(':code' => trim($cell_entry))) + ->fetchField(); + // We want to report an error if we can't find the scale memeber + // but only if there are any in the first place! + $num_members = db_query('SELECT count(*) FROM {pheno_scale_member} WHERE scale_id=:unit_id', + array(':unit_id' => $unit_id))->fetchField(); + if (!$cvalue_id AND !empty($num_members)) { + tripal_report_error( + 'rawpheno', + TRIPAL_WARNING, + 'Uploading Phenoypic Data: Unable to find scale id for Plant Measurement Type (Trait=!trait; Term=!name; Type ID=!id; Scale Value=!scale).', + array('!trait' => $cell_colheader, '!name' => $unit, '!id' => $unit_id, '!scale' => $cell_entry), + array('print' => TRUE) + ); + } + + // Use default value in the cell if query to find scale member code + // has no equivalent value. + $cvalue_id = (isset($cvalue_id) && $cvalue_id > 0) ? $cvalue_id : $cell_entry; + } + else { + // No scale member value for the rest of traits. + $cvalue_id = ''; + } + + // Insert trait only when type_id and unit_id are not null. + if (isset($type_id) && isset($unit_id)) { + + $temp = db_insert('pheno_measurements') + ->fields(array('plant_id' => $pheno_plantid, + 'type_id' => $type_id, + 'unit_id' => $unit_id, + 'cvalue_id' => $cvalue_id, + 'value' => $cell_entry, + 'modified' => date("D M d, Y h:i:s a", time()))) + ->execute(); + + if (!$temp) { + tripal_report_error( + 'rawpheno', + TRIPAL_ERROR, + 'Uploading Phenoypic Data: Unable to insert measurement. Values=@values.', + array('@values' => print_r(array('plant_id' => $pheno_plantid, + 'type_id' => $type_id, + 'unit_id' => $unit_id, + 'cvalue_id' => $cvalue_id, + 'value' => $cell_entry, + 'modified' => date("D M d, Y h:i:s a", time())),TRUE)), + array('print' => TRUE) + ); + $TRANSACTION->rollback(); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 108] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(38); + } + } + } + } + } + + $i++; + } + } + catch (Exception $e) { + $TRANSACTION->rollback(); + watchdog_exception('rawpheno', $e); + tripal_report_error('rawpheno', TRIPAL_CRITICAL, '[CODE 109] Failed to load phenoypic data (job !id)', array('!id' => $job_id), array('print' => TRUE)); + exit(4); + } + + unset($TRANSACTION); //Commit + print "Upload complete.\n"; + + print "\nUpdating the materialized view summarizing phenotypic data.\n"; + $mview_id = tripal_get_mview_id('rawpheno_rawdata_summary'); + if ($mview_id) tripal_populate_mview($mview_id); +} + + +/** + * Validates an excel file using any validators registered with rawpheno. + * + * @param $file + * A drupal managed_file object describing the uploaded spreadsheet. + * @param $project_id + * An integer containing project id selected in the project select box. + * This will map the data submitted to a project. + * @param $source + * A string containing the source of the file upload - Upload Data or Backup File. + * + * @return + * An array containing the validation result from each validator. + */ +function rawpheno_validate_excel_file($file, $project_id, $source) { + $status = array(); + + // Process the validators to make them easier to use. + // Specifically, sort them by their scope. + $validators = array(); + $all_validators = module_invoke_all('rawpheno_validators'); + foreach($all_validators as $k => $v) { + $validators[ $v['scope'] ][ $k ] = $v; + } + + // Todo list. + $all_scope_validators = array('project', 'file', 'all', 'header', 'subset'); + + // Add the libraries needed to parse excel files. + rawpheno_add_parsing_libraries(); + + // Before performing any validation to the excel file. Ensure first that a project is selected. + foreach ($validators['project'] as $prj_validator_name => $prj_validator) { + if (isset($prj_validator['validation callback']) AND function_exists($prj_validator['validation callback'])) { + $status[ $prj_validator_name ] = call_user_func($prj_validator['validation callback'], $project_id); + + // If returned false then halt validation. + if ($status[ $prj_validator_name ] === FALSE) { + // Fail the project and set the rest to TODO. + $status[ $prj_validator_name ] = FALSE; + + // Todo the rest of validators. + // Since this is project scope and it got falsed - remove the project. + unset($all_scope_validators[0]); + foreach($all_scope_validators as $v) { + foreach($validators[ $v ] as $v_name => $validator) { + $status[ $v_name ] = 'todo'; + } + } + + return $status; + } + } + } + + // First validate the whole file. If any of these fail then halt validation. + foreach ($validators['file'] as $validator_name => $validator) { + if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { + $status[ $validator_name ] = call_user_func($validator['validation callback'], $file); + + // If returned false then halt validation. + if ($status[ $validator_name ] === FALSE) { + // Fail the file and set the rest to TODO but set the project to passed + // first since it is assumed that project validator returned a passed value. + $status[ 'project_selected' ] = TRUE; + $status[ $validator_name ] = FALSE; + + // Todo the rest of validators. + // Since project is completed. skip this scope. + unset($all_scope_validators[0]); + foreach($all_scope_validators as $v) { + foreach($validators[ $v ] as $v_name => $validator) { + if ($status[ $v_name ] === TRUE) { + $status[ $v_name ] = TRUE; + } + elseif ($status[ $v_name ] === FALSE) { + $status[ $v_name ] = FALSE; + } + else { + $status[ $v_name ] = 'todo'; + } + } + } + + return $status; + } + } + } + + // Open the file for reading + $xls_obj = rawpheno_open_file($file); + + // Change to the correct spreadsheet. + rawpheno_change_sheet($xls_obj, 'measurements'); + + // This increment variable $i is required since xls and xlsx + // parsers assign array index differently. + // XLS starts at 1, while XLSX at 0; + $i = 0; + + // Variations of Not Applicable. + $not_applicable = array('na', 'n/a', 'n.a.'); + + // Skip columns. + $skip = array(); + // Project name. + $project_name = rawpheno_function_getproject($project_id); + // Calling all modules implementing hook_rawpheno_ignorecols_valsave_alter(): + drupal_alter('rawpheno_ignorecols_valsave', $skip, $project_name); + + // Iterate though each row. + $num_errored_rows = 0; + $storage = array(); + foreach($xls_obj as $row) { + $i++; + + // Convert row into a string and check the length. + // This will exclude empty rows. + if (strlen(trim(implode('', $row))) >= 5) { + + // VALIDATE THE HEADER. + if ($i == 1) { + // Save the header for later. + $header = array(); + $new_header = array(); + // Checking plot value requires cell value in Planting Date (date) and Location. + // Store index numbers of these two traits. + $plot_req = array(); + + $o = 0; + foreach ($row as $r) { + $without_format = rawpheno_function_delformat($r); + + // To maintain index of both cells and header, tag either to skip or process + // based on headers in drupal_alter hook. + $s = (in_array($without_format, $skip)) ? 1 : 0; + + // Remove new lines. + $rem_newline = str_replace(array("\n", "\r"), ' ', $r); + // Remove extra spaces. + $rem_spaces = preg_replace('/\s+/', ' ', $rem_newline); + // Remove leading and trailing spaces. + $r = trim($rem_spaces); + $no_units = rawpheno_get_trait_name($r); + + $header[] = array( + 'no format' => $without_format, + 'original' => $r, + 'units' => rawpheno_function_unit($without_format), + 'no units' => $no_units, + 'skip' => $s, + ); + + // Store index number of Plot trait requirements. + if (!isset($plot_req['planting date (date)']) && $without_format == 'plantingdate(date)') { + $plot_req['planting date (date)'] = $o; + } + elseif (!isset($plot_req['location']) && $without_format == 'location') { + $plot_req['location'] = $o; + } + + $o++; + } + + // Foreach validator with a scope of header, execute the validation callback & save the results. + foreach($validators['header'] as $validator_name => $validator) { + if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { + $result = call_user_func($validator['validation callback'], $header, $project_id); + + // The status needs to keep track of which rows failed for a given header. + if ($result === FALSE) { + $status[ $validator_name ] = $i; + } + elseif (is_array($result)) { + $status[ $validator_name ] = $result; + } + } + } + } + // VALIDATE THE ROW. + else { + + $row_has_error = FALSE; + foreach ($row as $column_index => $cell) { + if ($header[$column_index]['skip'] == 1) continue; + + $column_name = $header[$column_index]['no units']; + if (empty($column_name)) continue; + + // Prior to validating, Remove non-breaking whitespace by converting it to a blank space instead of removing it, + // in case user intends a space between words/values. + // trim() implementation below should drop unecessary leading and trailing spaces. + if (preg_match('/\xc2\xa0/', $cell)) { + $cell = preg_replace('/\xc2\xa0/', ' ', $cell); + } + + // We always want to strip flanking white space. + // FYI: This is done when the data is loaded as well. + $cell = trim($cell); + + // For consistency, convert all variations of not applicable to NA. + if (is_string($cell) && in_array(strtolower($cell), $not_applicable)) { + $cell = 'NA'; + } + + // Foreach validator: + foreach (array('all','subset') as $scope) { + foreach($validators[$scope] as $validator_name => $validator) { + + // Only validate if there is a validation callback. + if (isset($validator['validation callback']) AND function_exists($validator['validation callback'])) { + + // Only validate if the current validator applies to the current column. + // Specifically, if there are no defined headers it's applicable to + // OR if the current header is in the list of applicable headers. + if (!isset($validator['headers']) OR in_array($column_name, $validator['headers'])) { + + // Execute the validation callback & save the results. + $tmp_storage = (isset($storage[$validator_name])) ? $storage[$validator_name] : array(); + $context = array( + 'row index' => $i, + 'column index' => $column_index, + 'row' => $row, + 'header' => $header + ); + + // If column header is Plot, attach Plot validation requirement to + // $context array. The indexes will be used to fetch the cell value in context row. + if ($column_name == 'Plot') { + $context['plot_req'] = $plot_req; + } + + $result = $validator['validation callback']($cell, $context, $tmp_storage, $project_id); + + // Note: we use tmp storage b/c passing $storage[$validator_name] directly + // doesn't seem to work. + $storage[$validator_name] = $tmp_storage; + + // The status needs to keep track of which rows failed for a given header. + if (is_array($result)) { + $status[ $validator_name ][ $column_name ][$i] = $result; + $row_has_error = TRUE; + } + elseif ($result !== TRUE) { + $status[ $validator_name ][ $column_name ][$i] = $i; + $row_has_error = TRUE; + } + } + } + } + } + } + + if ($row_has_error) $num_errored_rows++; + } + + // Only check until you have 10 rows with errors. + if ($num_errored_rows >= 10) { + // We only want to present the warning if this is not the end of the file ;-) + $has_next = $xls_obj->next(); + if ($has_next AND strlen(trim(implode('', $has_next))) >= 1) { + $check_limit_message = "We have only checked the first $i lines of your file. Please fix the errors reported below and then upload the fixed file."; + + if ($source == 'upload') { + drupal_set_message($check_limit_message, 'error'); + return $status; + } + elseif ($source == 'backup') { + return array('status' => $status, 'check_limit' => $check_limit_message); + } + } + } + } + } + + // Make sure all validators are represented in status. + // If they are not already then a failure wasn't recorded -thus they passed :-). + foreach($all_validators as $validator_name => $validator) { + if (!isset($status[$validator_name])) { + $status[$validator_name] = TRUE; + } + } + + return $status; +} + +/** + * Open the Excel file using the spreadsheet reader. + * + * @param $file + * A Drupal managed file object. + * @return + * An object representing the Excel file. + */ +function rawpheno_open_file($file) { + // Grab the path and extension from the file. + $xls_file = drupal_realpath($file->uri); + $xls_extension = pathinfo($file->filename, PATHINFO_EXTENSION); + + // Validate that the spreadsheet is either xlsx or xls and open the spreadsheet using + // the correct class. + // XLSX: + if ($xls_extension == 'xlsx') { + $xls_obj = new SpreadsheetReader_XLSX($xls_file); + } + // XLS: + elseif ($xls_extension == 'xls') { + // PLS INCLUDE THIS FILE ONLY FOR XLS TYPE. + $xls_lib = libraries_load('spreadsheet_reader'); + $lib_path = $xls_lib['path']; + + include_once $lib_path . 'SpreadsheetReader_XLS.php'; + $xls_obj = new SpreadsheetReader_XLS($xls_file); + } + + return $xls_obj; +} + +/** + * Changes the worksheet in the Excel Object. + * + * @param $xls_obj + * The object describing this Excel workbook. + * @param $tab_name + * The name of the tab you would like to switch to. + * @return + * TRUE if it found the tab and FALSE otherwise. + */ +function rawpheno_change_sheet(&$xls_obj, $tab_name) { + // Get all the sheets in the workbook. + $xls_sheets = $xls_obj->Sheets(); + + // Locate the measurements sheet. + foreach($xls_sheets as $sheet_key => $sheet_value) { + $xls_obj->ChangeSheet($sheet_key); + + // Only process the measurements worksheet. + if (rawpheno_function_delformat($sheet_value) == 'measurements') { + return TRUE; + } + } + + return FALSE; +} + + +/** + * Adds the necessary files for EXCEL parsing. + */ +function rawpheno_add_parsing_libraries($file_type = 'XLSX') { + // Function call libraries_load() base on the implementation + // of hook_libraries_info() in rawpheno.module. + $xls_lib = libraries_load('spreadsheet_reader'); + // Library path information returned will be used + // to include individual library files required. + $lib_path = $xls_lib['path']; + + // Include parser library. PLS DO NOT ALTER ORDER!!! + // To stop parser from auto formatting date to MM/DD/YY, + // suggest a new date format YYYY-mm-dd in: + // line 678 in excel_reader2.php + // 0xe => "m/d/Y", to 0xe => "Y-m-d", + // line 834 in SpreadsheetReader_XLSX.php + // $Value = $Value -> format($Format['Code']); to $Value = $Value -> format('Y-m-d'); + // + include_once $lib_path . 'php-excel-reader/excel_reader2.php'; + include_once $lib_path . 'SpreadsheetReader_XLSX.php'; + include_once $lib_path . 'SpreadsheetReader.php'; + + if ($file_type == 'XLS') { + // PLS INCLUDE THIS FILE ONLY FOR XLS TYPE. + include_once $lib_path . 'SpreadsheetReader_XLS.php'; + } +} + + +/** + * Function to remove all formatting from a cell value. + * + * @param $xls_cell_value + * Contains a value of a cell. + * @return + * Contains a cell value with all formatting removed. + */ +function rawpheno_function_delformat($xls_cell_value) { + // Remove any extra spaces, new lines, leading and trainling spaces + // and covert the final result to lowercase. + return trim(strtolower(preg_replace('!\s+!', '', $xls_cell_value))); +} + + +/** + * Function to extract the unit from the column header. + * + * @param $xls_header_cell + * A string containing a column header. + * @return + * A string containing the unit found from the column header. + */ +function rawpheno_function_unit($xls_header_cell) { + // Remove all formatting. + $temp_value = rawpheno_function_delformat($xls_header_cell); + + // If this is a scale then return that. + if (preg_match('/\(scale/',$temp_value)) { + return 'scale'; + } + + // Remove the following characters. + $cell_value = str_replace(array(';', '1st', '2nd', 'r1', 'r3', 'r5', 'r7', ': 1-5'), '', $temp_value); + + // Extract text information inside the parenthesis. + preg_match("/.*\(([^)]*)\)/", $cell_value, $match); + + // Return unit found, or default to text if no unit. + return (isset($match[1])) ? trim($match[1]) : 'text'; +} + + +/** + * Function to determine additional column headers in the spreadsheet. Additional column headers are + * headers that are no part of the predefined headers set of the project. + * + * @param $file + * The full path to the excel file containing data. + * @param $project_id + * Project id number the spreadsheet is specific to. + * @return + * An array containing all additional column headers detected. + */ +function rawpheno_indicate_new_headers($file, $project_id) { + // Retrieve the header for the indicated file. + rawpheno_add_parsing_libraries(); + $xls_obj = rawpheno_open_file($file); + rawpheno_change_sheet($xls_obj, 'measurements'); + + // Note: we use the foreach here + // because the library documentation doesn't have a single fetch function. + foreach ($xls_obj as $xls_headers) { break; } + + // Array to hold epected column headers specific to a given project. + $expected_headers = rawpheno_project_traits($project_id); + + // Remove any formatting in each column headers. + $expected_headers = array_map('rawpheno_function_delformat', $expected_headers); + + // Array to hold new column headers. + $new_headers = array(); + + // Assuming the file actually has a non-empty header row... + if (count($xls_headers) > 0) { + // Read each column header and compare against expected column headers. + foreach($xls_headers as $value) { + $temp_value = rawpheno_function_delformat($value); + + // Determine if column header exists in the expected column headers. + if (!in_array($temp_value, $expected_headers) && !empty($value)) { + // Not in expected column headers, save it as new header. + $value = preg_replace('/\s+/', ' ', $value); + $new_headers[] = $value; + } + } + } + + return $new_headers; +} + + +/** + * Get all column headers. + * + * @param $file + * The full path to the excel file containing data. + * @return + * An array of headers. + */ +function rawpheno_all_headers($file) { + // Retrieve the header for the indicated file. + rawpheno_add_parsing_libraries(); + $xls_obj = rawpheno_open_file($file); + rawpheno_change_sheet($xls_obj, 'measurements'); + // Note: we use the foreach here + // because the library documentation doesn't have a single fetch function. + + $arr_headers = array(); + foreach ($xls_obj as $xls_headers) { + foreach($xls_headers as $h) { + if (strlen($h) > 2) { + $arr_headers[] = trim($h); + } + } + break; + } + + return $arr_headers; +} + + +/** + * Count all rows in a spreadsheet. + * + * @param $file + * The full path to the excel file containing data. + * @return + * An integer value of the total rows. + */ +function rawpheno_count_rows($xls_obj) { + // Row of 5 chars or more long is a row. + $count_rows = 0; + foreach ($xls_obj as $row) { + if (strlen(implode('', $row)) > 5) { + $count_rows++; + } + } + + // Less header row. + return $count_rows - 1; +} + + +/** + * Retrieve the cvterm_id for a given header. + * + * @param $header + * The unchanged/original header text for the trait. + * @return + * The cvterm_id for the trait. + */ +function rawpheno_get_trait_id($header) { + // New lines. + $header = str_replace(array("\n", "\r"), ' ', $header); + // Extra spaces. + $header = preg_replace('!\s+!', ' ', $header); + + // Query trait. Module stores unit in lowercase but user can use any case + // in the spreadsheet. eg Planting Date (date) and Planting Date (Date). + // @note cvterm.name + cv.name + not obsolete combination is unique (constraints). + $sql = "SELECT t2.cvterm_id + FROM {cv} AS t1 INNER JOIN {cvterm} AS t2 USING(cv_id) + WHERE lower(t2.name) = :cvterm_name AND t1.name = :cv_name AND is_obsolete = 0"; + + $args = array(':cvterm_name' => trim(strtolower($header)), ':cv_name' => 'phenotype_measurement_types'); + $type = chado_query($sql, $args) + ->fetchObject(); + + if ($type->cvterm_id) { + return $type->cvterm_id; + } + + return FALSE; +} + + +/** + * Retrieve the unit for the trait. + * + * @param $trait_name + * The name of the trait as found in the column header. + * @param $trait_id + * The cvterm_id of the trait if you have it (OPTIONAL). + * @return + * Returns an array with the cvterm_id and name of the unit. + */ +function rawpheno_get_trait_unit($trait_name, $trait_id = NULL) { + // Get the trait id if that is not provided to us. + if ($trait_id == NULL) { + $trait_id = rawpheno_get_trait_id($trait_name); + } + + // First we try to get the unit through relationships since that avoids making assumptions. + // in chado.cvterm_relationship. + // @todo make this more specific (restrict relationship by type?) + // @todo rather then limit, check if there are 1+ and warn the admin. + $sql = "SELECT cvterm_id, name FROM {cvterm} WHERE cvterm_id = + (SELECT subject_id FROM {cvterm_relationship} WHERE object_id = :trait LIMIT 1)"; + + $args = array(':trait' => $trait_id); + $unit = chado_query($sql, $args); + + if ($unit->rowCount() > 0) { + $r = $unit->fetchObject(); + return array('id' => $r->cvterm_id, 'name' => $r->name); + } + + // If that doesn't work then we try to extract it from the name. + // Note: if the following function is unable to extract the unit then it will default to text. + $unit_name = rawpheno_function_unit($trait_name); + + // Column header does not contain unit, use text as default + if (function_exists('chado_get_cvterm')) { + $cvterm = chado_get_cvterm(array('name' => $unit_name, 'cv_id' => array('name' => 'phenotype_measurement_units'))); + } + else { + $cvterm = tripal_get_cvterm(array('name' => $unit_name, 'cv_id' => array('name' => 'phenotype_measurement_units'))); + } + + if ($cvterm) { + return array('id' => $cvterm->cvterm_id, 'name' => $cvterm->name); + } + + return FALSE; +} + + +/** + * Remove the unit part from a trait. + * + * @param $trait_name + * A string containing the trait name as formatted in cvterm name. + * @return + * A string containing the trait name without the unit. + */ +function rawpheno_get_trait_name($trait_name) { + $t = explode('(', $trait_name); + + // Given a trait as defined in cvterm name in the following format: + // Trait name (Trait Rep; Unit), extract the trait name only and return + // the extracted name. + return (count($t) > 1) ? trim(preg_replace('/\(.*/', ' ', $trait_name)) : $trait_name; +} + + +/** + * Get all the essential traits in a project. + * + * @param $project_id + * An integer containing the project ID number. + * @return + * An array containing all essential traits in a project. + */ +function rawpheno_project_essential_traits($project_id) { + if (isset($project_id) AND $project_id > 0) { + // Get array of trait types + $trait_type = rawpheno_function_trait_types(); + + // Array to hold trait names. + $arr_essential_traits = array(); + + // Query essential traits in a project. + $sql = "SELECT TRIM(t1.name) AS cvterm + FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) + WHERE + t2.project_id = :project_id + AND t2.type IN (:essential) + ORDER BY t1.name ASC"; + + $args = array(':project_id' => $project_id, ':essential' => array($trait_type['type1'], $trait_type['type4'])); + $trait = chado_query($sql, $args); + + foreach($trait as $t) { + $m = rawpheno_get_trait_name($t->cvterm); + $arr_essential_traits[] = $m; + } + + // Add Name column header to the traits returned. + $arr_essential_traits[] = 'Name'; + + return $arr_essential_traits; + } +} + + +/** + * Get all the plant property traits in a project selected. + * + * @param $project_id + * An integer containing the project ID number. + * @return + * An array containing all essential traits in a project. + */ +function rawpheno_project_plantproperty_traits($project_id) { + if (isset($project_id) AND $project_id > 0) { + // Get array of trait types + $trait_type = rawpheno_function_trait_types(); + + // Array to hold trait names. + $arr_plantproperty_traits = array(); + + // Query plant property traits in a project. + $sql = "SELECT TRIM(t1.name) AS cvterm + FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) + WHERE t2.project_id = :project_id AND t2.type = :plantproperty + ORDER BY t1.name ASC"; + + // traits of type plantproperty. + $args = array(':project_id' => $project_id, ':plantproperty' => $trait_type['type4']); + $trait = chado_query($sql, $args); + + foreach($trait as $t) { + // Remove the trait rep and unit from the trait. + $m = rawpheno_get_trait_name($t->cvterm); + $arr_plantproperty_traits[] = $m; + } + + return $arr_plantproperty_traits; + } +} + + +/** + * Get all traits available in a project whether essential or not. + * + * @param $project_id + * An integer containing the project ID number. + * @return + * An array containing all traits available in a project. + */ +function rawpheno_project_traits($project_id) { + if (isset($project_id) AND $project_id > 0) { + $arr_trait = array(); + + // Query column headers in a project. + $sql = "SELECT TRIM(t1.name) AS cvterm + FROM {cvterm} AS t1 RIGHT JOIN pheno_project_cvterm AS t2 USING (cvterm_id) + WHERE t2.project_id = :project_id + ORDER BY t1.name ASC"; + + $args = array(':project_id' => $project_id); + $trait = chado_query($sql, $args); + + foreach($trait as $t) { + $arr_trait[] = $t->cvterm; + } + + // Add Name column header to the traits returned. + $arr_trait[] = 'Name'; + + return $arr_trait; + } +} + + +/** + * Function to fetch information about a unit (Describe method) when available. + * + * @param $cvterm_id + * An integer containing the cvterm id of a trait. + */ +function rawpheno_function_cvterm_properties($cvterm_id) { + // Narrow the search to cvterm of type measurement units. + if (function_exists('chado_get_cv')) { + $cv_unit = chado_get_cv(array('name' => 'phenotype_measurement_units')); + } + else { + $cv_unit = tripal_get_cv(array('name' => 'phenotype_measurement_units')); + } + + // In form state 2, describe header, user has the opportunity to describe the unit (Describe method field). + // This information is stored in cvterm relationship together with the cvterm id of the unit as the subject_id + // and cvterm_id of the header as the object_id. Given a header cvterm id, get the subject id and use it to + // get the information required from cvtermprop table. + // @todo check if there is more then one unit for a given trait and if so, warn the admin. + $sql = "SELECT subject_id FROM {cvterm_relationship} WHERE object_id = :cvterm_id AND type_id = :cv_unit LIMIT 1"; + $args = array(':cvterm_id' => $cvterm_id, ':cv_unit' => $cv_unit->cv_id); + + $d = chado_query($sql, $args) + ->fetchField(); + + // @note cvterm_id + type_id + rank is unique (constraint). + $sql = "SELECT value FROM {cvtermprop} WHERE type_id = :cv_unit AND cvterm_id = :cvterm_id AND rank = 0"; + $args = array(':cv_unit' => $cv_unit->cv_id, ':cvterm_id' => $d); + + $d = chado_query($sql, $args); + + if ($d->rowCount() == 1) { + return $d->fetchField(); + } + else { + return 'Describe the method used not available'; + } +} diff --git a/include/rawpheno.upload.form.inc b/includes/rawpheno.upload.form.inc old mode 100755 new mode 100644 similarity index 97% rename from include/rawpheno.upload.form.inc rename to includes/rawpheno.upload.form.inc index d03a742..b33378b --- a/include/rawpheno.upload.form.inc +++ b/includes/rawpheno.upload.form.inc @@ -1,1260 +1,1260 @@ - 'markup', - '#markup' => t('Standard Procedure ❯'), - ); - - // If the stage is not set then default to the first stage (i.e. 'check' ) - if (!isset($form_state['stage'])) { - $form_state['stage'] = 'check'; - } - - // If a job_id was provided in the URL then the user wants information - // on a prvious bulk loading job. Thus we should show them the last step. - if (isset($form_state['build_info']['args'][0])) { - $form_state['stage'] = 'save'; - } - - // Add the stage tracker/header. - $form = rawpheno_get_header($form, $form_state); - // Holds the current stage. - $form_stage = $form_state['stage']; - - // Stage indicator for theme function. - $form['current_stage'] = array( - '#type' => 'value', - '#value' => $form_stage, - ); - - // Add the next button for all but the last step. - if ($form_stage != 'save') { - $form['next_step'] = array( - '#type' => 'submit', - '#value' => 'Next Step', - '#weight' => 100 - ); - } - - // Create a select box containing projects available - these are projects - // that have associated column header set and must have at least 1 essential column header. - // The projects are filtered to show only projects assigned to user. - if ($form_stage != 'save') { - $my_project = rawpheno_function_user_project($GLOBALS['user']->uid); - - if (count($my_project) > 0) { - // When there is more than 1 project assigned to user, tell user to select a project - // otherwise default to the only project available. - if (count($my_project) > 1) { - $my_project = array(0 => 'Please select an experiment') + $my_project; - } - - // Default Project in project selector field. - if (isset($_POST['sel_project']) AND $_POST['sel_project'] > 0) { - // HTTP method post has the project id. - $default_value = $_POST['sel_project']; - } - elseif (isset($form_state['values']['sel_project'])) { - // Form state has the project id. - $default_value = $form_state['values']['sel_project']; - } - else { - // Neither has the project id number default to the first project in the project list. - $default_value = 0; - } - - // Determine if the project select box should be enabled. - // Disabled in all stages but stage 01. - $disabled = ($form_stage == 'check') ? FALSE : TRUE; - - $form['sel_project'] = array( - '#type' => 'select', - '#options' => $my_project, - '#default_value' => $default_value, - '#disabled' => $disabled, - '#id' => 'rawpheno-select-project-field', - ); - - // This block is to ensure select project select box is always default to - // "please select a project" or to the first project option in the beginning of the upload process. - // It will also default to this option when user refreshes the page. - if ($form_stage == 'check') { - drupal_add_js('jQuery(document).ready(function() { - jQuery("#rawpheno-select-project-field").val(0); - })', 'inline'); - } - } - else { - // No project is assigned to user. - $form['no_project'] = array( - '#markup' => '
' . t('No experiment is assigned to this account. Please contact the administrator of this website.') . '
', - ); - } - } - - // Note: - // When there is no project defined in the module, the error message is handled by the theme. - - // Get the directory path of rawpheno module. - $path = drupal_get_path('module', 'rawpheno') . '/theme/'; - - // Load corresponding function callback together with JavaScript and CSS. - switch($form_stage) { - case 'check': - // Stage 01 - upload and check spreadsheet. - $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage01.css'); - $form['#attached']['js'] = array($path . 'js/rawpheno.upload.stage01.js', - $path . 'js/rawpheno.upload.script.js'); - - $form = rawpheno_upload_form_stage_check($form, $form_state); - break; - - case 'review': - // Stage 02 - describe form (only when there is additional trait). - $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage02.css'); - $form['#attached']['js'] = array($path . 'js/rawpheno.upload.script.js'); - - $form = rawpheno_upload_form_stage_review($form, $form_state); - break; - - case 'save': - // Stage 03 - save to databse and success page. - $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage03.css'); - $form['#attached']['js'] = array($path . 'js/rawpheno.upload.stage03.js', - $path . 'js/rawpheno.upload.script.js'); - - $form = rawpheno_upload_form_stage_save($form, $form_state); - break; - } - - return $form; -} - - -/** - * Function callback: Construct form for Stage 01. - * - * Stage 01 form allows user to upload data collection spreadsheet and perform basic compliance test. - */ -function rawpheno_upload_form_stage_check($form, &$form_state) { - // Create an instance of DragNDrop Upload. - // SETTINGS: - // #file_upload_max_size: max file size allowed - // #upload_location: destination of file - // #upload_event: manual - show an upload button or auto - uploads after drag drop - // #upload_validators: allowed file extensions - // #upload_button_text: label of upload button - // #droppable_area_text: text in drop area - // #progress_indicator: none, throbber or bar - // #progress_message: message to display while processing - // #allow_replace: allow user to replace file by drag and drop another file - // #standard_upload: show browse button or not - // #upload_button_text: submit button text (not required when auto submit is auto) - - $form['dnd'] = array( - '#type' => 'dragndrop_upload', - '#file_upload_max_size' => '10M', - '#upload_location' => 'public://', - '#upload_event' => 'auto', - // NOTE: Accept the listed file extension and let the spreadsheet reader tell if file is valid to generate an an error message. - // No silent treatment. - '#upload_validators' => array( - 'file_validate_extensions' => array('xlsx xls jpg jpeg gif png txt doc pdf ppt pps odt ods odp csv'), - ), - '#droppable_area_text' => t('Drag your Microsoft Excel Spreadsheet file here'), - '#progress_indicator' => 'throbber', - '#progress_message' => 'Validating your spreadsheet file. Please wait...', - '#allow_replace' => 1, - '#standard_upload' => 1, - '#upload_button_text' => '', - - // We are adding our own element process function so that we can make a successfully - // uploaded/validated file permanent during the AJAX process rather than waiting for - // them to click the "Next" button. - '#process' => array( - 'file_managed_file_process', - 'dragndrop_upload_element_element_process', - 'rawpheno_phenotype_upload_file_element_process', - ), - ); - - return $form; -} - - -/** - * Function callback: Construct form for Stage 02. - * - * Stage 02 form allows user to describe and save a additional trait/s found in the spreadsheet submitted in Stage 01. - * - * Assuming the file uploaded properly, we have access to an excel file and need to - * ensure that all traits have been described. If there are any that haven't then - * we need to ask the the user to define them now. - * - * NOTE: User has the option to skip this stage by not checking any of the new traits found and clicking next step. - */ -function rawpheno_upload_form_stage_review(&$form, &$form_state) { - // Array to hold new headers. - $new_header = array(); - - // The project id number the spreadsheet and column headers are specific to. - $project_id = $form_state['values']['sel_project']; - $project_name = rawpheno_function_getproject($project_id); - - // FIND NEW HEADERS. - // First step, determine which headers/traits need to be described. - if (isset($form_state['multistep_values']['fid'])) { - // Get Drupal file object. - $file = file_load($form_state['multistep_values']['fid']); - - // Ensure that the file exits and project id is selected. - // The form will unset the project id upon page refresh, this will catch the condition - // when no project id is selected, user will be stopped and is requested to retry the process. - if ($file AND !empty($project_id)) { - $new_header = rawpheno_indicate_new_headers($file, $project_id); - - // Calling all modules implementing hook_rawpheno_AGILE_stock_name_alter(): - drupal_alter('rawpheno_ignorecols_newcolumn', $new_header, $project_name); - - $form_state['multistep_values']['new_headers'] = $new_header; - } - else { - drupal_set_message(t('Unable to access your file. Please try uploading again.'), 'error'); - } - } - else { - drupal_set_message(t('We have no record of your uploaded file. Please try uploading it again.'), 'error'); - } - - // If we were unable to access the file or project is not selected then don't let them proceed. - if (!isset($file) OR empty($file) OR empty($project_id)) { - $form['notice'] = array( - '#type' => 'markup', - '#markup' => '
' - . t('Unable to access uploaded file. Please attempt to upload your file again on the previous page. If the problem persists then contact the administrator.', - array('@upload-page' => url('phenotypes/raw/upload'))) - . '
', - ); - - // No submit button as well. - unset($form['next_step']); - - return $form; - } - - - // NO NEW HEADER. - // If there are no new headers then they don't have to do anything. The module will display a summary - // showing the number of parsed column headers in the spreadsheet. - if (empty($new_header)) { - $all_headers = rawpheno_all_headers($file); - - $markup = ' -
    -
  • - ' . count($all_headers) . ' Column Headers. -
  • - -
  • - No Additional Column Headers Found. -
  • -
- '; - - $form['xls_summary_fldset'] = array( - '#type' => 'fieldset', - '#title' => t('Describe new trait'), - ); - - $form['xls_summary_fldset']['information'] = array( - '#type' => 'markup', - '#markup' => $markup, - ); - - $form['notice'] = array( - '#type' => 'markup', - '#markup' => '
No new traits were detected in the spreadsheet. Please click "Next Step".
', - ); - - return $form; - } - - - // NEW HEADERS FOUND. - if (function_exists('chado_get_cv')) { - $cv = chado_get_cv(array('name' => 'phenotype_measurement_types')); - } - else { - $cv = tripal_get_cv(array('name' => 'phenotype_measurement_types')); - } - - $cv_id = $cv->cv_id; - - // Where clause that form part of the SQL below. - $where = array( - 'yes' => "TRIM(LOWER(name)) = :cvterm LIMIT 1", - 'no' => "TRIM(LOWER(SPLIT_PART(name, '(', 1))) LIKE :cvterm" - ); - - $sel = "SELECT * FROM {cvterm} WHERE - cvterm_id NOT IN (SELECT cvterm_id FROM pheno_project_cvterm WHERE project_id = :project_id) - AND cv_id = :cv_id AND %s"; - - // Otherwise, we need a form! - // Main fieldset container for form elements. - $form['xls_review_fldset'] = array( - '#type' => 'fieldset', - '#title' => t('Check the traits that you want to describe and save'), - ); - - $headers_no_format = array_map('rawpheno_function_delformat', $new_header); - - // Array to hold all checked headers. - $arr_checked_header = array(); - - foreach($new_header as $i => $k) { - if (isset($k) AND !empty($k)) { - // To prevent spills of information to other form set, reset this variable that holds - // query result object. - if (isset($cvterm_info)) { - unset($cvterm_info); - } - - // CHECKBOX to let user select a trait to describe and save. If left unchecked, system will not save it. - $form['xls_review_fldset']['chk_' . $i] = array( - '#type' => 'checkbox', - '#title' => t(ucwords($k)), - '#ajax' => array( - 'callback' => 'ajax_rawpheno_upload_form_step2_expand_trait_callback', - 'wrapper' => 'trait-description-' . $i, - 'effect' => 'fade', - 'trait_index' => $i, - ), - ); - - // Container div that holds form elements. - $form['xls_review_fldset']['fldset_' . $i] = array( - '#type' => 'markup', - '#prefix' => '
', - '#suffix' => '
', - ); - - // By default, show the describe form. - $show_form = 'yes'; - - // If the checkbox is checked then show the fields user want to described. - if (isset($form_state['values']['chk_' . $i]) AND ($form_state['values']['chk_' . $i] == TRUE)) { - // TERM NAME/TRAIT/HEADER - $form['xls_review_fldset']['fldset_' . $i]['txt_header_' . $i] = array( - '#type' => 'hidden', - '#value' => $k, - ); - - // Clean up the current header. - $name = trim(strtolower(preg_replace('!\s+!', ' ', $k))); - $arr_checked_header[] = $name; - - // Test if the header has a unit component and set the variable accordingly. - $has_unit = (strpbrk($name, '()')) ? 'yes' : 'no'; - - // Format the name to be used in the following SQL. When the name has a unit component, - // we just feed the name to the SQL using equal operator, otherwise, we use like operator - // to find all similar headers and suggest it. - $cvterm = ($has_unit == 'yes') ? $name : '%' . $name . '%'; - - // Construct the query statement. - $sql = sprintf($sel, $where[$has_unit]); - $args = array(':project_id' => $project_id, ':cv_id' => $cv_id, ':cvterm' => $cvterm); - $h = chado_query($sql, $args); - - if ($h->rowCount() > 0) { - if ($has_unit == 'yes') { - // Load information about the header. - $cvterm_info = $h->fetchObject(); - - // Tell user that the column header exists already. - $form['xls_review_fldset']['fldset_' . $i]['notice_' . $i] = array( - '#markup' => '
The system has detected this column header in the database. - All form fields are disabled to prevent alteration to the original version. - To save this header and data associated to it, please keep the checkbox checked.
', - ); - - $show_form = 'yes'; - } - else { - // Suggest similar header. - // Ensure that the list of headers to be suggested is not in the list of headers detected, - // that way we can avoid duplicate headers. - $header_options = array(); - foreach($h as $m) { - $this_header = trim(strtolower($m->name)); - - if (!in_array($this_header, $headers_no_format)) { - $header_options[$m->cvterm_id] = $m->name; - } - } - - if (count($header_options) > 0) { - $form['xls_review_fldset']['fldset_' . $i]['sel_header_' . $i] = array( - '#type' => 'select', - '#title' => t('Did you mean?'), - '#options' => array('-1' => '---', 0 => 'None of these apply') + $header_options, - '#ajax' => array( - 'callback' => 'ajax_rawpheno_upload_form_step2_load_header_info', - 'wrapper' => 'trait-description-' . $i, - 'effect' => 'fade', - 'trait_index' => $i, - ), - '#element_validate' => array('rawpheno_newheader_didyoumean_validate'), - '#attributes' => array('class' => array('sel-header')), - '#description' => t('The system has detected a similar header in the database. - It is recommended that you select the header from the select box that best describes your data. - If the header is not listed, please select None of these apply option and use the form below to describe this column header.'), - ); - - $show_form = 'no'; - } - else { - $show_form = 'yes'; - } - - if (isset($form_state['values']['sel_header_' . $i])) { - if ($form_state['values']['sel_header_' . $i] > 0) { - $cvterm_id = $form_state['values']['sel_header_' . $i]; - - if (function_exists('chado_get_cvterm')) { - $cvterm_info = chado_get_cvterm(array('cvterm_id' => $cvterm_id)); - } - else { - $cvterm_info = tripal_get_cvterm(array('cvterm_id' => $cvterm_id)); - } - - $show_form = 'yes'; - } - elseif ($form_state['values']['sel_header_' . $i] == 0) { - $show_form = 'yes'; - } - } - } - } - - - if ($show_form == 'yes') { - // TERM DEFINITION - $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i] = array( - '#type' => 'textarea', - '#title' => t('Definition'), - '#required' => TRUE, - '#description' => t('A human-readable text definition'), - ); - - if (isset($cvterm_info) && isset($cvterm_info->definition)) { - $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i]['#value'] = $cvterm_info->definition; - $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i]['#disabled'] = TRUE; - } - - - // UNIT - $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i] = array( - '#type' => 'textfield', - '#title' => t('Unit'), - '#required' => TRUE, - '#maxlength' => 100, - '#element_validate' => array('rawpheno_newheader_unit_validate'), - '#description' => t('Unit of measurement used'), - ); - - if (isset($cvterm_info)) { - $unit_val = strpbrk($cvterm_info->name, '()'); - // Remove any parenthesis making its way to the final value. - $unit_val = str_replace(array('(', ')'), '', $unit_val); - - $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#value'] = trim($unit_val); - $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#disabled'] = TRUE; - } - else { - if ($has_unit == 'yes') { - $u = strpbrk($name, '()'); - // Remove any parenthesis making its way to the final value. - $u = str_replace(array('(', ')'), '', $u); - - $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#value'] = trim($u); - $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#disabled'] = FALSE; - } - } - - - // DESCRIPTION - describe the trait. - $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i] = array( - '#type' => 'textarea', - '#title' => t('Describe the method used'), - '#required' => TRUE, - '#description' => t('Describe the method used to collect this data if you used a scale, be specific'), - ); - - if (isset($cvterm_info)) { - $cvterm_describe_unit = rawpheno_function_cvterm_properties($cvterm_info->cvterm_id); - - $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i]['#value'] = $cvterm_describe_unit; - $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i]['#disabled'] = TRUE; - } - - - // Note fields are required - $form['xls_review_fldset']['fldset_' . $i]['required_' . $i] = array( - '#markup' => '
* means field is required
' - ); - } - } - } - } - - // Hidden field containing all the checked new headers - if (isset($arr_checked_header) AND count($arr_checked_header) > 0) { - $form['all_header_checked'] = array( - '#type' => 'hidden', - '#value' => implode(',', $arr_checked_header), - ); - } - - // Indicator to user of how many of the new traits found has been described. - $form['traits_checked'] = array( - '#type' => 'markup', - '#markup' => '
You have described 0 trait. Please click "Next Step".
' - ); - - return $form; -} - - -/** - * Function validate the unit field when new header is detected in the spreadsheet. - * Validation includes ensuring that user does not use parenthesis ( and ) in the unit. - */ -function rawpheno_newheader_unit_validate($element, &$form_state) { - $unit_value = trim($element['#value']); - $project_id = $form_state['values']['sel_project']; - - // strpbrk() Returns a string starting from the character found, or FALSE if it is not found. - if (strpbrk($unit_value, '()')) { - form_set_error($element['#name'], 'The value in the unit field contains characters "(" and/or ")". Please remove these characters and try again.'); - } - else { - // Test the name plus the unit combination if it is in the project. - $header_field = str_replace('unit', 'header', $element['#name']); - $header_value = $form_state['values'][$header_field]; - - if (!strpbrk($header_value, '()')) { - $name_value = trim(strtolower($header_value . ' (' . strtolower($unit_value) . ')')); - - $sql = "SELECT cvterm_id - FROM {cvterm} INNER JOIN pheno_project_cvterm USING(cvterm_id) - WHERE project_id = :project_id AND TRIM(LOWER(name)) = :cvterm LIMIT 1"; - - $args = array(':project_id' => $project_id, ':cvterm' => $name_value); - $h = chado_query($sql, $args); - - if ($h->rowCount() == 1) { - form_set_error($element['#name'], 'Cannot save column header and unit. ' . ucfirst($name_value) . ' exists in this project.'); - } - - // Test if user is about to save same headers. - if (isset($form_state['values']['all_header_checked'])) { - $h = $form_state['values']['all_header_checked']; - $header_validation = explode(',', $h); - - if (in_array($name_value, $header_validation)) { - form_set_error($element['#name'], 'Cannot save multiple entries of the same column header and unit combination.'); - } - } - } - } -} - - -/** - * Function callback: validate Did you mean? select box - */ -function rawpheno_newheader_didyoumean_validate($element, &$form_state) { - if ($element['#value'] < 0) { - form_set_error($element['#name'], 'Please select an option and try again.'); - } -} - - -/** - * Function load column header information. - */ -function ajax_rawpheno_upload_form_step2_load_header_info($form, $form_state) { - $i = $form_state['triggering_element']['#ajax']['trait_index']; - - return $form['xls_review_fldset']['fldset_' . $i]; -} - - -/* - * Selects the piece of the form we want to use as replacement text and returns it as a form (renderable array). - * - * @return renderable array (the trait description elements) - */ -function ajax_rawpheno_upload_form_step2_expand_trait_callback($form, $form_state) { - // Unique id of each form set. - $i = $form_state['triggering_element']['#ajax']['trait_index']; - - return $form['xls_review_fldset']['fldset_' . $i]; -} - - -/** - * Function callback: Construct form for Stage 03. - * - * Stage 03 form is the final stage that displays a status message - * and a navigation button to direct user after a successful file upload. - */ -function rawpheno_upload_form_stage_save($form, &$form_state) { - $job_id = NULL; - global $user; - - if (isset($form_state['build_info']['args'][0])) { - $job_id = $form_state['build_info']['args'][0]; - - // We only want to run jobs that has phenotypic data in it. Otherwise we tell user - // job is not valid (in case user will hack the url containing the job id). - // Retrieve the tripal job and determine the percent complete. - $job = tripal_get_job($job_id); - - // If job is valid. - if ($job) { - if ($job->uid == $user->uid) { - // Job id belongs to the user. Authorized. - $job_status = trim(strtolower($job->status)); - - // Check if it is a valid job and not a BLAST or other job type. - if ($job->callback == 'rawpheno_load_spreadsheet') { - if ($job_status == 'completed') { - // Is completed some time ago. - $form['notice'] = array( - '#markup' => '
It appears that you are attempting to submit a spreadsheet that has been processed already
' - ); - } - elseif ($job_status == 'error') { - // Has error. - $form['notice'] = array( - '#markup' => '
It appears that you are attempting to submit a spreadsheet that has errors.
' - ); - } - elseif ($job_status == 'cancelled') { - // Is cancelled. - $form['notice'] = array( - '#markup' => '
It appears that you are attempting to submit a spreadsheet that has been cancelled.
' - ); - } - else { - // Not processed yet - show the progress bar. - // A valid job - work on it. - $form['notice'] = array( - '#type' => 'markup', - '#markup' => - '
Your spreadsheet has been successfully submitted and will not be interupted if you choose to leave this page.
' - . '
The progress bar below indicates our progress updating ' . strtoupper($_SERVER['SERVER_NAME']) . '. Your data will not be available until the progress bar below completes.
' - ); - - // Add Progress JS Library. - drupal_add_js('misc/progress.js'); - - // This is the link passed to the JavaScript Progress.js as the parameter to a function - // that monitors a link. The link is a function callback that generates a JSON object - // containing the number of rows save in percent. See file: rawpheno.module. - $form['tripal_job_id'] = array( - '#type' => 'hidden', - '#value' => $GLOBALS['base_url'] . '/phenotypes/raw/upload/job_summary/' . $job->job_id, - '#attributes' => array('id' => 'tripal-job-id'), - ); - - // We make a DIV which the progress bar can occupy. You can see this in use - // in ajax_example_progressbar_callback(). - $form['status'] = array( - '#type' => 'markup', - '#markup' => '
' - ); - } - } - else { - // Job not supported by this module. - $form['notice'] = array( - '#markup' => '
It appears that you are attempting to request a process that is not supported by this module.
' - ); - } - } - else { - // Not authorized. - $form['notice'] = array( - '#markup' => '
It appears that you are attempting to submit a spreadsheet that is not in your account.
' - ); - } - } - else { - // Job is not valid or does not exists. - $form['notice'] = array( - '#markup' => '
The job request to save spreadsheet file does not exist.
' - ); - } - } - - return $form; -} - - -/** - * Implements hook_file_insert(). - * Save file information when file is saved (backup). - * - * @param $file - * Drupal file opbject. - */ -function rawpheno_file_insert($file) { - // Process file only when there is a request to save a file and that request is coming from backup page. - if (isset($file->source)) { - if ($file->source == 'bdnd') { - // User id of the currently logged in user. - $user_id = $GLOBALS['user']->uid; - // The project id field. - $project_id = $_POST['backup_sel_project']; - // The notes field. - $notes = trim(strip_tags($_POST['backup_txt_description'])); - - // Query the record id of the project to user record. - // The result id will be used to map a backup file to user and to project. - $sql = "SELECT project_user_id FROM pheno_project_user WHERE project_id = :project_id AND uid = :user_id LIMIT 1"; - $args = array(':project_id' => $project_id, ':user_id' => $user_id); - $prj_usr_id = db_query($sql, $args) - ->fetchField(); - - // Get the validation result performed to the spreadsheet file and store the result along with the file information. - // The same validation process performed in upload data page is carried out to backup file. However, the result - // is stored as plain text and passed and failed icons are replaced by words passed and failed, respectively. - $status = rawpheno_validate_excel_file($file, $project_id, 'backup'); - - $s = (isset($status['status'])) ? $status['status'] : $status; - - // Express the validation result array into human readable non-html content format. - $validation_result = ''; - // Call the same validator function used in upload data. - $validators = module_invoke_all('rawpheno_validators'); - - // For each status result, convert it to text based and add a unique text indicator to be used - // as key to explode the entire text and create a list using the
tag. - foreach($s as $key => $result) { - $flag = ($result === TRUE) ? 'passed' : 'failed'; - - // The item keyword will be used to create a list of validation entries when displaying validaiton result to user. - $validation_result .= '#item: (' . $flag . ') ' . $validators[$key]['label'] . "\n"; - if ($result !== TRUE) { - $message = call_user_func($validators[$key]['message callback'], $result); - if (!empty($message)) { - $validation_result .= implode("\n", $message); - } - } - } - - // Compute the version number of this file. - $sql = "SELECT MAX(t2.version) + 1 AS version - FROM {pheno_project_user} AS t1 RIGHT JOIN {pheno_backup_file} AS t2 USING(project_user_id) - WHERE t1.project_id = :project_id AND t1.uid = :user_id LIMIT 1"; - - $args = array(':project_id' => $project_id, ':user_id' => $user_id); - $version = db_query($sql, $args) - ->fetchField(); - - // On initial upload the file version is null, in this case - // version is set to 1. Version is incremented by 1 (+1) in - // subsequent uploads. - $version = ($version === null) ? 1 : $version; - - // Insert a record of this file. - // File version, which is a sequential order (integer) is handled by the rdbms as it is set to serial type. - if (isset($status['check_limit'])) { - $validation_result .= "#item: (failed) NOTICE : " . $status['check_limit'] . "\n"; - } - - db_insert('pheno_backup_file') - ->fields(array('fid' => $file->fid, - 'notes' => $notes, - 'version' => $version, - 'project_user_id' => $prj_usr_id, - 'validation_result' => $validation_result)) - ->execute(); - - // Finally, make the file permanent. - rawpheno_upload_make_file_permanent($file->fid); - - // Make the validation result available to frontend. - $_SESSION['rawpheno']['backup_file_validation_result'] = $status; - } - } -} - - -/** - * Upload validators callback: - * Basic compliance test to spreadsheet submitted. - * - * @param $file - * Drupal file opbject. - */ -function rawpheno_file_validate($file) { - // 10 mins (60 * 10). - $max_time = 600; - - if ($file->source == 'dnd') { - // Set processing time to 10 mins. - ini_set('max_execution_time', $max_time); - - // Upload data page. - - // Project id number the spreadsheet and column headers are specific to. - $project_id = (int)$_POST['sel_project']; - - // Validate the file. - // The following function will return an array specifying which of the validation - // steps passed and providing infomration for those that failed. - $status = rawpheno_validate_excel_file($file, $project_id, 'upload'); - - // We want to show the user which steps passed/failed even if all of them passed, - // so lets do that now. We use drupal_set_message() because returning from this function - // creates an error message and halts file upload, whereas, using drupal_set_message() - // allows us to print to the screen regardless of failure/success. - drupal_set_message(theme('rawpheno_upload_validation_report', array('status' => $status)), 'rawpheno-validate-progress'); - - // Now we want to determine if validation passed or failed as a whole. - // To do that we have to look at each step and only if all steps passed - // did the file pass validation and can be uploaded. - $all_passed = TRUE; - foreach ($status as $test_result) { - if ($test_result !== TRUE) { - $all_passed = FALSE; - break; break; - } - } - - // hook_file_validate() expects an array of error messages if validation failed and - // and empty array if there are no errors. We don't want this system to print the errors - // for us since we are using our more friendly theme (see drupal_set_message() above). - // The work-around is to pass FALSE if validation failed. - if ($all_passed) { - drupal_set_message('Your file uploaded successfully. Please click "Next" to continue.'); - return array(); - } - else { - return FALSE; - } - } - elseif ($file->source == 'bdnd') { - // Set processing time to 10 mins. - ini_set('max_execution_time', $max_time); - - // Backup file page. - - // Array to hold the validation result. - $status = array(); - - // Project id number the spreadsheet and column headers are specific to. - $project_id = (int)$_POST['backup_sel_project']; - - // Perform basic compliance test: - // - A project is selected. - // - File is Microsoft Excel Spreadhseet file. - // - Measurement tab exists. - // - Essential column headers defined in the project are present. - $flag_index = array('project_selected', 'is_excel', 'tab_exists', 'column_exists'); - - // Validate file. - $flags = rawpheno_validate_excel_file($file, $project_id, 'backup'); - - $s = (isset($flags['status'])) ? $flags['status'] : $flags; - - // If DND backup, missing measurements, skip all validator but - // save/backup the file anyway. - if ($s['tab_exists'] === FALSE) { - return array(); - } - - // Read only the status from test listed above. - foreach($s as $i => $v) { - if (in_array($i, $flag_index) AND ($v === FALSE || $v === 'todo')) { - $status[$i] = $v; - } - } - - // When any of the mentioned test failed, show them to user. - if (count($status) > 0) { - if (isset($flags['check_limit'])) { - drupal_set_message($flags['check_limit'], 'error'); - } - - drupal_set_message(theme('rawpheno_upload_validation_report', array('status' => $status)), 'rawpheno-validate-progress'); - return FALSE; - } - - // Else, proceed to hook_file_insert(). - } - else { - // File source is one that is not of interest to us. - // Do not return anything or it will trigger validation errors for other modules. - } -} - - -/** - * Make the phenotype excel file permanent on successful upload. - * - * This is an additional process handler used by the form API to generate the form array - * for a given element. Usually it is used to make a custom form element or enhance a - * standard form element. - * - * We are using it to capture the just uploaded file within the AJAX call by checking - * when the element is rendered if it has a fid (ie: has been saved). - */ -function rawpheno_phenotype_upload_file_element_process($element, &$form_state, $form) { - if (isset($element['#value']['fid']) AND !empty($element['#value']['fid'])) { - $file_id = $element['#value']['fid']; - rawpheno_upload_make_file_permanent($file_id); - } - - return $element; -} - - -/** - * Make the file uploaded permanent and make a record indicating that file is used by the module. - * - * @param $file_id - * File id in Drupal file object. - */ -function rawpheno_upload_make_file_permanent($file_id) { - // Get the file object. - $file = file_load($file_id); - - if ($file) { - // Make the file permanent. - $file->status = FILE_STATUS_PERMANENT; - file_save($file); - - // Also, point out that we are using it ;-) - // Note, the file_usage_add() function expects a numerical unique id which we don't have. - // We have gotten around this by using the uid concatenated with the timestamp using - // the assumption that a single user cannot upload more than one phenotype file within a second. - file_usage_add($file, 'rawpheno', 'rawphenotypes-file', $file->uid . $file->timestamp); - } -} - - -/** - * Implements hook_validate(). - */ -function rawpheno_upload_form_master_validate($form, &$form_state) { } - - -/** - * Implements hook_submit(). - * - * Master submit to handle form submit. - */ -function rawpheno_upload_form_master_submit(&$form, &$form_state) { - // Which button triggers a submit action. - $btn_submit = $form_state['triggering_element']['#value']; - - // Save any additional traits and then submit a job to save the spreadsheet. - if ($form_state['stage'] == 'review') { - $job_id = rawpheno_submit_review($form, $form_state); - - // Then we need to add the job_id to the path so the system can keep track of it. - if ($job_id) { - drupal_goto(current_path() . '/' . $job_id); - } - } - - // If we just uploaded the file then we want to save the fid for easy access. - if (isset($form_state['values']['dnd'])) { - $form_state['multistep_values']['fid'] = $form_state['values']['dnd']; - } - - // If the next step button was pressed then iterate to the next step. - if ($btn_submit == 'Next Step') { - // Definitely save the form id. - if(isset($form_state['multistep_values']['form_build_id'])) { - $form_state['values']['form_build_id'] = $form_state['multistep_values']['form_build_id']; - } - - // Save the values from the current step. - $form_state['multistep_values'][$form_state['stage']] = $form_state['values']; - - // Iterate to the next step. - $form_state['new_stage'] = rawpheno_next_page($form, $form_state); - - // Ensure the form state is saved and the form is rebuilt. - $form_state['multistep_values']['form_build_id'] = $form_state['values']['form_build_id']; - $form_state['stage'] = $form_state['new_stage']; - $form_state['rebuild'] = TRUE; - } -} - - -/** - * Save spreadsheet to database. - */ -function rawpheno_submit_review($form, &$form_state) { - // Project id number the spreadsheet and column headers are specific to. - $project_id = $form_state['values']['sel_project']; - - // Save spreadsheet data in the following order. - // 1. New column headers. - // 2. The entire spreadsheet. - - // cvterm id of controlled vocabulary. - if (function_exists('chado_get_cv')) { - $cvid = chado_get_cv(array('name' => 'phenotype_measurement_units')); - } - else { - $cvid = tripal_get_cv(array('name' => 'phenotype_measurement_units')); - } - - $cv_measurements_unit = $cvid->cv_id; - - // 1. Save new headers. - // Read variable that holds new column headers. - $new_header = $form_state['multistep_values']['new_headers']; - - // Create an array of new hearders with flag/status if user wants to save it. - // This array will be passed to rawpheno_load_spreadsheet. - $arr_newheaders = array(); - - // Determine if there is new header. - if (count($new_header) > 0) { - $trait_type = rawpheno_function_trait_types(); - - // Read each column header. - foreach($new_header as $i => $header) { - // For each new header store information provided in the interface. - // Indicates if user has check this header for saving. - $header = trim(str_replace(array("\n", "\r", " "), ' ', $header)); - $header = preg_replace('/\s+/', ' ', $header); - - $arr_newheaders[$header]['flag'] = ($form_state['values']['chk_' . $i] == 1) ? 1 : 0; - - // Determine if the form in review traits has been filled out and checkbox - // has been checked by user. If it has been checked then save the trait. - if ($form_state['values']['chk_' . $i] === 1 && !empty($form_state['values']['txt_header_' . $i])) { - // Before save, we need to tell if the header is present in the database and - // user just wants to reuse them. Otherwise, add a new header. - // Reuse header - set to OPTIONAL. - if ((isset($form_state['values']['sel_header_' . $i]) AND $form_state['values']['sel_header_' . $i] > 0) OR - (isset($form_state['values']['txt_header_cvterm_id_' . $i]))) { - - // User selected from a list of similar headers. - $cvterm_id = (isset($form_state['values']['sel_header_' . $i])) - ? $form_state['values']['sel_header_' . $i] - : $form_state['values']['txt_header_cvterm_id_' . $i]; - - // Map this header to the project. - $sql = "SELECT cvterm_id FROM {pheno_project_cvterm} WHERE project_id = :project_id AND cvterm_id = :cvterm_id LIMIT 1"; - $args = array(':project_id' => $project_id, ':cvterm_id' => $cvterm_id); - - $h = db_query($sql, $args); - if ($h->rowCount() <= 0) { - // Add to project only when it is not in the project. - // Set the trait type to contributed. - db_insert('pheno_project_cvterm') - ->fields(array( - 'project_id' => $project_id, - 'cvterm_id' => $cvterm_id, - 'type' => $trait_type['type2']) - ) - ->execute(); - } - - // When saving this data for this header, use the cvterm_id. - $arr_newheaders[$header]['alt_header'] = $cvterm_id; - continue; - } - - // Check if the trait exists in the database, then it is likely - // that the user is reusing the trait - threfore it is not contributed and just map - // the cvterm id to a project. - - // Add the as contributed - set to CONTRIBUTED. - // Construct the column header name. - $name = trim($form_state['values']['txt_header_' . $i]); - $name = preg_replace('/\s+/', ' ', $name); - - $unit = trim($form_state['values']['txt_unit_' . $i]); - $method = trim($form_state['values']['txtarea_describe_' . $i]); - $def = trim($form_state['values']['txt_def_' . $i]); - - // Format the header. - if (strpbrk($name, '()')) { - // Header has a unit part. - $name = trim(str_replace(array("\n", "\r", " "), ' ', $name)); - } - else { - // Construct header plus the unit. - $name = $name . ' (' . strtolower($unit) . ')'; - } - - // Trait properties - use when inserting the cterm and reference to other property. - $m_cvterm = array( - 'id' => 'rawpheno_tripal:' . $name, - 'name' => $name, - 'definition' => $def, - 'cv_name' => 'phenotype_measurement_types' - ); - - // Search the name in cvterm and decide if trait should be considered optional or contributed. - $sql = "SELECT t2.cvterm_id - FROM {cv} AS t1 INNER JOIN {cvterm} AS t2 USING (cv_id) - WHERE - trim(lower(t2.name)) = trim(lower(:cvterm_name)) - AND t1.name = :cv_name LIMIT 1"; - - $args = array(':cvterm_name' => $m_cvterm['name'], ':cv_name' => $m_cvterm['cv_name']); - $result = chado_query($sql, $args) - ->fetchObject(); - - if ($result) { - // Found use the id. - $m_cvterm_id = $result->cvterm_id; - // Trait is optional. - $type = $trait_type['type2']; - } - else { - // Not found, insert and get the inserted id. - $m = tripal_insert_cvterm($m_cvterm); - $m_cvterm_id = $m->cvterm_id; - // Trait is contributed. - $type = $trait_type['type5']; - } - - // When saving this data for this header, use the cvterm_id. - $arr_newheaders[$header]['alt_header'] = $m_cvterm_id; - db_insert('pheno_project_cvterm') - ->fields(array('project_id' => $project_id, - 'cvterm_id' => $m_cvterm_id, - 'type' => $type)) - ->execute(); - - // Create a R Friendly version. - $r_version = rawpheno_function_make_r_compatible($m_cvterm['name']); - - if (function_exists('chado_get_cv')) { - $cv_rfriendly = chado_get_cv(array('name' => 'phenotype_r_compatible_version')); - } - else { - $cv_rfriendly = tripal_get_cv(array('name' => 'phenotype_r_compatible_version')); - } - - $values = array( - 'cvterm_id' => $m_cvterm_id, - 'type_id' => $cv_rfriendly->cv_id, - 'value' => $r_version, - 'rank' => 0 - ); - - chado_insert_record('cvtermprop', $values); - - // Then save the Unit. - $u_cvterm = array( - 'id' => 'rawpheno_tripal:' . strtolower($unit), - 'name' => strtolower($unit), - 'definition' => $unit, - 'cv_name' => 'phenotype_measurement_units' - ); - - $u_cvterm_id = chado_select_record('cvterm',array('cvterm_id'), - array('name' => $u_cvterm['name'], - 'cv_id' => array('name' => $u_cvterm['cv_name']))); - if (!$u_cvterm_id) { - $u_cvterm_id = tripal_insert_cvterm($u_cvterm); - } - - // Grab just the id. - if (is_array($u_cvterm_id)) { - $u_cvterm_id = $u_cvterm_id[0]->cvterm_id; - } - elseif (is_object($u_cvterm_id)) { - $u_cvterm_id = $u_cvterm_id->cvterm_id; - } - - // Don't forget the method description. - $prop = array( - 'cvterm_id' => $m_cvterm_id, - 'type_id' => $cv_measurements_unit, - 'value' => $method, - 'rank' => 0, - ); - - $prop_id = chado_select_record('cvtermprop', array('cvtermprop_id'), $prop); - if (!$prop_id) { - $prop = chado_insert_record('cvtermprop', $prop); - } - - // Finally relate the measurement and unit. - $rel = array( - 'subject_id' => $u_cvterm_id, - 'type_id' => $cv_measurements_unit, - 'object_id' => $m_cvterm_id, - ); - $rel_id = chado_select_record('cvterm_relationship', array('cvterm_relationship_id'), $rel); - if (!$rel_id) { - chado_insert_record('cvterm_relationship', $rel); - } - } - } - } - - // 2. The entire spreadsheet. - // Get the variable that holds the path to the spreadsheet file in the server. - $file = file_load($form_state['multistep_values']['fid']); - $xls_file = drupal_realpath($file->uri); - - // Array of required traits excluding Name. - $plantprop_headers = rawpheno_project_plantproperty_traits($project_id); - - // Drupal user object. - global $user; - - if (isset($xls_file) && !empty($xls_file)) { - $job_id = tripal_add_job( - "Upload Phenoypic data: " . $xls_file, - 'rawpheno', - 'rawpheno_load_spreadsheet', - array( - $project_id, - serialize($arr_newheaders), - $form_state['multistep_values']['fid'], - serialize($plantprop_headers) - ), - $user->uid - ); - - return $job_id; - } -} + 'markup', + '#markup' => t('Standard Procedure ❯'), + ); + + // If the stage is not set then default to the first stage (i.e. 'check' ) + if (!isset($form_state['stage'])) { + $form_state['stage'] = 'check'; + } + + // If a job_id was provided in the URL then the user wants information + // on a prvious bulk loading job. Thus we should show them the last step. + if (isset($form_state['build_info']['args'][0])) { + $form_state['stage'] = 'save'; + } + + // Add the stage tracker/header. + $form = rawpheno_get_header($form, $form_state); + // Holds the current stage. + $form_stage = $form_state['stage']; + + // Stage indicator for theme function. + $form['current_stage'] = array( + '#type' => 'value', + '#value' => $form_stage, + ); + + // Add the next button for all but the last step. + if ($form_stage != 'save') { + $form['next_step'] = array( + '#type' => 'submit', + '#value' => 'Next Step', + '#weight' => 100 + ); + } + + // Create a select box containing projects available - these are projects + // that have associated column header set and must have at least 1 essential column header. + // The projects are filtered to show only projects assigned to user. + if ($form_stage != 'save') { + $my_project = rawpheno_function_user_project($GLOBALS['user']->uid); + + if (count($my_project) > 0) { + // When there is more than 1 project assigned to user, tell user to select a project + // otherwise default to the only project available. + if (count($my_project) > 1) { + $my_project = array(0 => 'Please select an experiment') + $my_project; + } + + // Default Project in project selector field. + if (isset($_POST['sel_project']) AND $_POST['sel_project'] > 0) { + // HTTP method post has the project id. + $default_value = $_POST['sel_project']; + } + elseif (isset($form_state['values']['sel_project'])) { + // Form state has the project id. + $default_value = $form_state['values']['sel_project']; + } + else { + // Neither has the project id number default to the first project in the project list. + $default_value = 0; + } + + // Determine if the project select box should be enabled. + // Disabled in all stages but stage 01. + $disabled = ($form_stage == 'check') ? FALSE : TRUE; + + $form['sel_project'] = array( + '#type' => 'select', + '#options' => $my_project, + '#default_value' => $default_value, + '#disabled' => $disabled, + '#id' => 'rawpheno-select-project-field', + ); + + // This block is to ensure select project select box is always default to + // "please select a project" or to the first project option in the beginning of the upload process. + // It will also default to this option when user refreshes the page. + if ($form_stage == 'check') { + drupal_add_js('jQuery(document).ready(function() { + jQuery("#rawpheno-select-project-field").val(0); + })', 'inline'); + } + } + else { + // No project is assigned to user. + $form['no_project'] = array( + '#markup' => '
' . t('No experiment is assigned to this account. Please contact the administrator of this website.') . '
', + ); + } + } + + // Note: + // When there is no project defined in the module, the error message is handled by the theme. + + // Get the directory path of rawpheno module. + $path = drupal_get_path('module', 'rawpheno') . '/theme/'; + + // Load corresponding function callback together with JavaScript and CSS. + switch($form_stage) { + case 'check': + // Stage 01 - upload and check spreadsheet. + $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage01.css'); + $form['#attached']['js'] = array($path . 'js/rawpheno.upload.stage01.js', + $path . 'js/rawpheno.upload.script.js'); + + $form = rawpheno_upload_form_stage_check($form, $form_state); + break; + + case 'review': + // Stage 02 - describe form (only when there is additional trait). + $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage02.css'); + $form['#attached']['js'] = array($path . 'js/rawpheno.upload.script.js'); + + $form = rawpheno_upload_form_stage_review($form, $form_state); + break; + + case 'save': + // Stage 03 - save to databse and success page. + $form['#attached']['css'] = array($path . 'css/rawpheno.upload.stage03.css'); + $form['#attached']['js'] = array($path . 'js/rawpheno.upload.stage03.js', + $path . 'js/rawpheno.upload.script.js'); + + $form = rawpheno_upload_form_stage_save($form, $form_state); + break; + } + + return $form; +} + + +/** + * Function callback: Construct form for Stage 01. + * + * Stage 01 form allows user to upload data collection spreadsheet and perform basic compliance test. + */ +function rawpheno_upload_form_stage_check($form, &$form_state) { + // Create an instance of DragNDrop Upload. + // SETTINGS: + // #file_upload_max_size: max file size allowed + // #upload_location: destination of file + // #upload_event: manual - show an upload button or auto - uploads after drag drop + // #upload_validators: allowed file extensions + // #upload_button_text: label of upload button + // #droppable_area_text: text in drop area + // #progress_indicator: none, throbber or bar + // #progress_message: message to display while processing + // #allow_replace: allow user to replace file by drag and drop another file + // #standard_upload: show browse button or not + // #upload_button_text: submit button text (not required when auto submit is auto) + + $form['dnd'] = array( + '#type' => 'dragndrop_upload', + '#file_upload_max_size' => '10M', + '#upload_location' => 'public://', + '#upload_event' => 'auto', + // NOTE: Accept the listed file extension and let the spreadsheet reader tell if file is valid to generate an an error message. + // No silent treatment. + '#upload_validators' => array( + 'file_validate_extensions' => array('xlsx xls jpg jpeg gif png txt doc pdf ppt pps odt ods odp csv'), + ), + '#droppable_area_text' => t('Drag your Microsoft Excel Spreadsheet file here'), + '#progress_indicator' => 'throbber', + '#progress_message' => 'Validating your spreadsheet file. Please wait...', + '#allow_replace' => 1, + '#standard_upload' => 1, + '#upload_button_text' => '', + + // We are adding our own element process function so that we can make a successfully + // uploaded/validated file permanent during the AJAX process rather than waiting for + // them to click the "Next" button. + '#process' => array( + 'file_managed_file_process', + 'dragndrop_upload_element_element_process', + 'rawpheno_phenotype_upload_file_element_process', + ), + ); + + return $form; +} + + +/** + * Function callback: Construct form for Stage 02. + * + * Stage 02 form allows user to describe and save a additional trait/s found in the spreadsheet submitted in Stage 01. + * + * Assuming the file uploaded properly, we have access to an excel file and need to + * ensure that all traits have been described. If there are any that haven't then + * we need to ask the the user to define them now. + * + * NOTE: User has the option to skip this stage by not checking any of the new traits found and clicking next step. + */ +function rawpheno_upload_form_stage_review(&$form, &$form_state) { + // Array to hold new headers. + $new_header = array(); + + // The project id number the spreadsheet and column headers are specific to. + $project_id = $form_state['values']['sel_project']; + $project_name = rawpheno_function_getproject($project_id); + + // FIND NEW HEADERS. + // First step, determine which headers/traits need to be described. + if (isset($form_state['multistep_values']['fid'])) { + // Get Drupal file object. + $file = file_load($form_state['multistep_values']['fid']); + + // Ensure that the file exits and project id is selected. + // The form will unset the project id upon page refresh, this will catch the condition + // when no project id is selected, user will be stopped and is requested to retry the process. + if ($file AND !empty($project_id)) { + $new_header = rawpheno_indicate_new_headers($file, $project_id); + + // Calling all modules implementing hook_rawpheno_AGILE_stock_name_alter(): + drupal_alter('rawpheno_ignorecols_newcolumn', $new_header, $project_name); + + $form_state['multistep_values']['new_headers'] = $new_header; + } + else { + drupal_set_message(t('Unable to access your file. Please try uploading again.'), 'error'); + } + } + else { + drupal_set_message(t('We have no record of your uploaded file. Please try uploading it again.'), 'error'); + } + + // If we were unable to access the file or project is not selected then don't let them proceed. + if (!isset($file) OR empty($file) OR empty($project_id)) { + $form['notice'] = array( + '#type' => 'markup', + '#markup' => '
' + . t('Unable to access uploaded file. Please attempt to upload your file again on the previous page. If the problem persists then contact the administrator.', + array('@upload-page' => url('phenotypes/raw/upload'))) + . '
', + ); + + // No submit button as well. + unset($form['next_step']); + + return $form; + } + + + // NO NEW HEADER. + // If there are no new headers then they don't have to do anything. The module will display a summary + // showing the number of parsed column headers in the spreadsheet. + if (empty($new_header)) { + $all_headers = rawpheno_all_headers($file); + + $markup = ' +
    +
  • + ' . count($all_headers) . ' Column Headers. +
  • + +
  • + No Additional Column Headers Found. +
  • +
+ '; + + $form['xls_summary_fldset'] = array( + '#type' => 'fieldset', + '#title' => t('Describe new trait'), + ); + + $form['xls_summary_fldset']['information'] = array( + '#type' => 'markup', + '#markup' => $markup, + ); + + $form['notice'] = array( + '#type' => 'markup', + '#markup' => '
No new traits were detected in the spreadsheet. Please click "Next Step".
', + ); + + return $form; + } + + + // NEW HEADERS FOUND. + if (function_exists('chado_get_cv')) { + $cv = chado_get_cv(array('name' => 'phenotype_measurement_types')); + } + else { + $cv = tripal_get_cv(array('name' => 'phenotype_measurement_types')); + } + + $cv_id = $cv->cv_id; + + // Where clause that form part of the SQL below. + $where = array( + 'yes' => "TRIM(LOWER(name)) = :cvterm LIMIT 1", + 'no' => "TRIM(LOWER(SPLIT_PART(name, '(', 1))) LIKE :cvterm" + ); + + $sel = "SELECT * FROM {cvterm} WHERE + cvterm_id NOT IN (SELECT cvterm_id FROM pheno_project_cvterm WHERE project_id = :project_id) + AND cv_id = :cv_id AND %s"; + + // Otherwise, we need a form! + // Main fieldset container for form elements. + $form['xls_review_fldset'] = array( + '#type' => 'fieldset', + '#title' => t('Check the traits that you want to describe and save'), + ); + + $headers_no_format = array_map('rawpheno_function_delformat', $new_header); + + // Array to hold all checked headers. + $arr_checked_header = array(); + + foreach($new_header as $i => $k) { + if (isset($k) AND !empty($k)) { + // To prevent spills of information to other form set, reset this variable that holds + // query result object. + if (isset($cvterm_info)) { + unset($cvterm_info); + } + + // CHECKBOX to let user select a trait to describe and save. If left unchecked, system will not save it. + $form['xls_review_fldset']['chk_' . $i] = array( + '#type' => 'checkbox', + '#title' => t(ucwords($k)), + '#ajax' => array( + 'callback' => 'ajax_rawpheno_upload_form_step2_expand_trait_callback', + 'wrapper' => 'trait-description-' . $i, + 'effect' => 'fade', + 'trait_index' => $i, + ), + ); + + // Container div that holds form elements. + $form['xls_review_fldset']['fldset_' . $i] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + ); + + // By default, show the describe form. + $show_form = 'yes'; + + // If the checkbox is checked then show the fields user want to described. + if (isset($form_state['values']['chk_' . $i]) AND ($form_state['values']['chk_' . $i] == TRUE)) { + // TERM NAME/TRAIT/HEADER + $form['xls_review_fldset']['fldset_' . $i]['txt_header_' . $i] = array( + '#type' => 'hidden', + '#value' => $k, + ); + + // Clean up the current header. + $name = trim(strtolower(preg_replace('!\s+!', ' ', $k))); + $arr_checked_header[] = $name; + + // Test if the header has a unit component and set the variable accordingly. + $has_unit = (strpbrk($name, '()')) ? 'yes' : 'no'; + + // Format the name to be used in the following SQL. When the name has a unit component, + // we just feed the name to the SQL using equal operator, otherwise, we use like operator + // to find all similar headers and suggest it. + $cvterm = ($has_unit == 'yes') ? $name : '%' . $name . '%'; + + // Construct the query statement. + $sql = sprintf($sel, $where[$has_unit]); + $args = array(':project_id' => $project_id, ':cv_id' => $cv_id, ':cvterm' => $cvterm); + $h = chado_query($sql, $args); + + if ($h->rowCount() > 0) { + if ($has_unit == 'yes') { + // Load information about the header. + $cvterm_info = $h->fetchObject(); + + // Tell user that the column header exists already. + $form['xls_review_fldset']['fldset_' . $i]['notice_' . $i] = array( + '#markup' => '
The system has detected this column header in the database. + All form fields are disabled to prevent alteration to the original version. + To save this header and data associated to it, please keep the checkbox checked.
', + ); + + $show_form = 'yes'; + } + else { + // Suggest similar header. + // Ensure that the list of headers to be suggested is not in the list of headers detected, + // that way we can avoid duplicate headers. + $header_options = array(); + foreach($h as $m) { + $this_header = trim(strtolower($m->name)); + + if (!in_array($this_header, $headers_no_format)) { + $header_options[$m->cvterm_id] = $m->name; + } + } + + if (count($header_options) > 0) { + $form['xls_review_fldset']['fldset_' . $i]['sel_header_' . $i] = array( + '#type' => 'select', + '#title' => t('Did you mean?'), + '#options' => array('-1' => '---', 0 => 'None of these apply') + $header_options, + '#ajax' => array( + 'callback' => 'ajax_rawpheno_upload_form_step2_load_header_info', + 'wrapper' => 'trait-description-' . $i, + 'effect' => 'fade', + 'trait_index' => $i, + ), + '#element_validate' => array('rawpheno_newheader_didyoumean_validate'), + '#attributes' => array('class' => array('sel-header')), + '#description' => t('The system has detected a similar header in the database. + It is recommended that you select the header from the select box that best describes your data. + If the header is not listed, please select None of these apply option and use the form below to describe this column header.'), + ); + + $show_form = 'no'; + } + else { + $show_form = 'yes'; + } + + if (isset($form_state['values']['sel_header_' . $i])) { + if ($form_state['values']['sel_header_' . $i] > 0) { + $cvterm_id = $form_state['values']['sel_header_' . $i]; + + if (function_exists('chado_get_cvterm')) { + $cvterm_info = chado_get_cvterm(array('cvterm_id' => $cvterm_id)); + } + else { + $cvterm_info = tripal_get_cvterm(array('cvterm_id' => $cvterm_id)); + } + + $show_form = 'yes'; + } + elseif ($form_state['values']['sel_header_' . $i] == 0) { + $show_form = 'yes'; + } + } + } + } + + + if ($show_form == 'yes') { + // TERM DEFINITION + $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i] = array( + '#type' => 'textarea', + '#title' => t('Definition'), + '#required' => TRUE, + '#description' => t('A human-readable text definition'), + ); + + if (isset($cvterm_info) && isset($cvterm_info->definition)) { + $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i]['#value'] = $cvterm_info->definition; + $form['xls_review_fldset']['fldset_' . $i]['txt_def_' . $i]['#disabled'] = TRUE; + } + + + // UNIT + $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i] = array( + '#type' => 'textfield', + '#title' => t('Unit'), + '#required' => TRUE, + '#maxlength' => 100, + '#element_validate' => array('rawpheno_newheader_unit_validate'), + '#description' => t('Unit of measurement used'), + ); + + if (isset($cvterm_info)) { + $unit_val = strpbrk($cvterm_info->name, '()'); + // Remove any parenthesis making its way to the final value. + $unit_val = str_replace(array('(', ')'), '', $unit_val); + + $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#value'] = trim($unit_val); + $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#disabled'] = TRUE; + } + else { + if ($has_unit == 'yes') { + $u = strpbrk($name, '()'); + // Remove any parenthesis making its way to the final value. + $u = str_replace(array('(', ')'), '', $u); + + $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#value'] = trim($u); + $form['xls_review_fldset']['fldset_' . $i]['txt_unit_' . $i]['#disabled'] = FALSE; + } + } + + + // DESCRIPTION - describe the trait. + $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i] = array( + '#type' => 'textarea', + '#title' => t('Describe the method used'), + '#required' => TRUE, + '#description' => t('Describe the method used to collect this data if you used a scale, be specific'), + ); + + if (isset($cvterm_info)) { + $cvterm_describe_unit = rawpheno_function_cvterm_properties($cvterm_info->cvterm_id); + + $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i]['#value'] = $cvterm_describe_unit; + $form['xls_review_fldset']['fldset_' . $i]['txtarea_describe_' . $i]['#disabled'] = TRUE; + } + + + // Note fields are required + $form['xls_review_fldset']['fldset_' . $i]['required_' . $i] = array( + '#markup' => '
* means field is required
' + ); + } + } + } + } + + // Hidden field containing all the checked new headers + if (isset($arr_checked_header) AND count($arr_checked_header) > 0) { + $form['all_header_checked'] = array( + '#type' => 'hidden', + '#value' => implode(',', $arr_checked_header), + ); + } + + // Indicator to user of how many of the new traits found has been described. + $form['traits_checked'] = array( + '#type' => 'markup', + '#markup' => '
You have described 0 trait. Please click "Next Step".
' + ); + + return $form; +} + + +/** + * Function validate the unit field when new header is detected in the spreadsheet. + * Validation includes ensuring that user does not use parenthesis ( and ) in the unit. + */ +function rawpheno_newheader_unit_validate($element, &$form_state) { + $unit_value = trim($element['#value']); + $project_id = $form_state['values']['sel_project']; + + // strpbrk() Returns a string starting from the character found, or FALSE if it is not found. + if (strpbrk($unit_value, '()')) { + form_set_error($element['#name'], 'The value in the unit field contains characters "(" and/or ")". Please remove these characters and try again.'); + } + else { + // Test the name plus the unit combination if it is in the project. + $header_field = str_replace('unit', 'header', $element['#name']); + $header_value = $form_state['values'][$header_field]; + + if (!strpbrk($header_value, '()')) { + $name_value = trim(strtolower($header_value . ' (' . strtolower($unit_value) . ')')); + + $sql = "SELECT cvterm_id + FROM {cvterm} INNER JOIN pheno_project_cvterm USING(cvterm_id) + WHERE project_id = :project_id AND TRIM(LOWER(name)) = :cvterm LIMIT 1"; + + $args = array(':project_id' => $project_id, ':cvterm' => $name_value); + $h = chado_query($sql, $args); + + if ($h->rowCount() == 1) { + form_set_error($element['#name'], 'Cannot save column header and unit. ' . ucfirst($name_value) . ' exists in this project.'); + } + + // Test if user is about to save same headers. + if (isset($form_state['values']['all_header_checked'])) { + $h = $form_state['values']['all_header_checked']; + $header_validation = explode(',', $h); + + if (in_array($name_value, $header_validation)) { + form_set_error($element['#name'], 'Cannot save multiple entries of the same column header and unit combination.'); + } + } + } + } +} + + +/** + * Function callback: validate Did you mean? select box + */ +function rawpheno_newheader_didyoumean_validate($element, &$form_state) { + if ($element['#value'] < 0) { + form_set_error($element['#name'], 'Please select an option and try again.'); + } +} + + +/** + * Function load column header information. + */ +function ajax_rawpheno_upload_form_step2_load_header_info($form, $form_state) { + $i = $form_state['triggering_element']['#ajax']['trait_index']; + + return $form['xls_review_fldset']['fldset_' . $i]; +} + + +/* + * Selects the piece of the form we want to use as replacement text and returns it as a form (renderable array). + * + * @return renderable array (the trait description elements) + */ +function ajax_rawpheno_upload_form_step2_expand_trait_callback($form, $form_state) { + // Unique id of each form set. + $i = $form_state['triggering_element']['#ajax']['trait_index']; + + return $form['xls_review_fldset']['fldset_' . $i]; +} + + +/** + * Function callback: Construct form for Stage 03. + * + * Stage 03 form is the final stage that displays a status message + * and a navigation button to direct user after a successful file upload. + */ +function rawpheno_upload_form_stage_save($form, &$form_state) { + $job_id = NULL; + global $user; + + if (isset($form_state['build_info']['args'][0])) { + $job_id = $form_state['build_info']['args'][0]; + + // We only want to run jobs that has phenotypic data in it. Otherwise we tell user + // job is not valid (in case user will hack the url containing the job id). + // Retrieve the tripal job and determine the percent complete. + $job = tripal_get_job($job_id); + + // If job is valid. + if ($job) { + if ($job->uid == $user->uid) { + // Job id belongs to the user. Authorized. + $job_status = trim(strtolower($job->status)); + + // Check if it is a valid job and not a BLAST or other job type. + if ($job->callback == 'rawpheno_load_spreadsheet') { + if ($job_status == 'completed') { + // Is completed some time ago. + $form['notice'] = array( + '#markup' => '
It appears that you are attempting to submit a spreadsheet that has been processed already
' + ); + } + elseif ($job_status == 'error') { + // Has error. + $form['notice'] = array( + '#markup' => '
It appears that you are attempting to submit a spreadsheet that has errors.
' + ); + } + elseif ($job_status == 'cancelled') { + // Is cancelled. + $form['notice'] = array( + '#markup' => '
It appears that you are attempting to submit a spreadsheet that has been cancelled.
' + ); + } + else { + // Not processed yet - show the progress bar. + // A valid job - work on it. + $form['notice'] = array( + '#type' => 'markup', + '#markup' => + '
Your spreadsheet has been successfully submitted and will not be interupted if you choose to leave this page.
' + . '
The progress bar below indicates our progress updating ' . strtoupper($_SERVER['SERVER_NAME']) . '. Your data will not be available until the progress bar below completes.
' + ); + + // Add Progress JS Library. + drupal_add_js('misc/progress.js'); + + // This is the link passed to the JavaScript Progress.js as the parameter to a function + // that monitors a link. The link is a function callback that generates a JSON object + // containing the number of rows save in percent. See file: rawpheno.module. + $form['tripal_job_id'] = array( + '#type' => 'hidden', + '#value' => $GLOBALS['base_url'] . '/phenotypes/raw/upload/job_summary/' . $job->job_id, + '#attributes' => array('id' => 'tripal-job-id'), + ); + + // We make a DIV which the progress bar can occupy. You can see this in use + // in ajax_example_progressbar_callback(). + $form['status'] = array( + '#type' => 'markup', + '#markup' => '
' + ); + } + } + else { + // Job not supported by this module. + $form['notice'] = array( + '#markup' => '
It appears that you are attempting to request a process that is not supported by this module.
' + ); + } + } + else { + // Not authorized. + $form['notice'] = array( + '#markup' => '
It appears that you are attempting to submit a spreadsheet that is not in your account.
' + ); + } + } + else { + // Job is not valid or does not exists. + $form['notice'] = array( + '#markup' => '
The job request to save spreadsheet file does not exist.
' + ); + } + } + + return $form; +} + + +/** + * Implements hook_file_insert(). + * Save file information when file is saved (backup). + * + * @param $file + * Drupal file opbject. + */ +function rawpheno_file_insert($file) { + // Process file only when there is a request to save a file and that request is coming from backup page. + if (isset($file->source)) { + if ($file->source == 'bdnd') { + // User id of the currently logged in user. + $user_id = $GLOBALS['user']->uid; + // The project id field. + $project_id = $_POST['backup_sel_project']; + // The notes field. + $notes = trim(strip_tags($_POST['backup_txt_description'])); + + // Query the record id of the project to user record. + // The result id will be used to map a backup file to user and to project. + $sql = "SELECT project_user_id FROM pheno_project_user WHERE project_id = :project_id AND uid = :user_id LIMIT 1"; + $args = array(':project_id' => $project_id, ':user_id' => $user_id); + $prj_usr_id = db_query($sql, $args) + ->fetchField(); + + // Get the validation result performed to the spreadsheet file and store the result along with the file information. + // The same validation process performed in upload data page is carried out to backup file. However, the result + // is stored as plain text and passed and failed icons are replaced by words passed and failed, respectively. + $status = rawpheno_validate_excel_file($file, $project_id, 'backup'); + + $s = (isset($status['status'])) ? $status['status'] : $status; + + // Express the validation result array into human readable non-html content format. + $validation_result = ''; + // Call the same validator function used in upload data. + $validators = module_invoke_all('rawpheno_validators'); + + // For each status result, convert it to text based and add a unique text indicator to be used + // as key to explode the entire text and create a list using the
tag. + foreach($s as $key => $result) { + $flag = ($result === TRUE) ? 'passed' : 'failed'; + + // The item keyword will be used to create a list of validation entries when displaying validaiton result to user. + $validation_result .= '#item: (' . $flag . ') ' . $validators[$key]['label'] . "\n"; + if ($result !== TRUE) { + $message = call_user_func($validators[$key]['message callback'], $result); + if (!empty($message)) { + $validation_result .= implode("\n", $message); + } + } + } + + // Compute the version number of this file. + $sql = "SELECT MAX(t2.version) + 1 AS version + FROM {pheno_project_user} AS t1 RIGHT JOIN {pheno_backup_file} AS t2 USING(project_user_id) + WHERE t1.project_id = :project_id AND t1.uid = :user_id LIMIT 1"; + + $args = array(':project_id' => $project_id, ':user_id' => $user_id); + $version = db_query($sql, $args) + ->fetchField(); + + // On initial upload the file version is null, in this case + // version is set to 1. Version is incremented by 1 (+1) in + // subsequent uploads. + $version = ($version === null) ? 1 : $version; + + // Insert a record of this file. + // File version, which is a sequential order (integer) is handled by the rdbms as it is set to serial type. + if (isset($status['check_limit'])) { + $validation_result .= "#item: (failed) NOTICE : " . $status['check_limit'] . "\n"; + } + + db_insert('pheno_backup_file') + ->fields(array('fid' => $file->fid, + 'notes' => $notes, + 'version' => $version, + 'project_user_id' => $prj_usr_id, + 'validation_result' => $validation_result)) + ->execute(); + + // Finally, make the file permanent. + rawpheno_upload_make_file_permanent($file->fid); + + // Make the validation result available to frontend. + $_SESSION['rawpheno']['backup_file_validation_result'] = $status; + } + } +} + + +/** + * Upload validators callback: + * Basic compliance test to spreadsheet submitted. + * + * @param $file + * Drupal file opbject. + */ +function rawpheno_file_validate($file) { + // 10 mins (60 * 10). + $max_time = 600; + + if ($file->source == 'dnd') { + // Set processing time to 10 mins. + ini_set('max_execution_time', $max_time); + + // Upload data page. + + // Project id number the spreadsheet and column headers are specific to. + $project_id = (int)$_POST['sel_project']; + + // Validate the file. + // The following function will return an array specifying which of the validation + // steps passed and providing infomration for those that failed. + $status = rawpheno_validate_excel_file($file, $project_id, 'upload'); + + // We want to show the user which steps passed/failed even if all of them passed, + // so lets do that now. We use drupal_set_message() because returning from this function + // creates an error message and halts file upload, whereas, using drupal_set_message() + // allows us to print to the screen regardless of failure/success. + drupal_set_message(theme('rawpheno_upload_validation_report', array('status' => $status)), 'rawpheno-validate-progress'); + + // Now we want to determine if validation passed or failed as a whole. + // To do that we have to look at each step and only if all steps passed + // did the file pass validation and can be uploaded. + $all_passed = TRUE; + foreach ($status as $test_result) { + if ($test_result !== TRUE) { + $all_passed = FALSE; + break; break; + } + } + + // hook_file_validate() expects an array of error messages if validation failed and + // and empty array if there are no errors. We don't want this system to print the errors + // for us since we are using our more friendly theme (see drupal_set_message() above). + // The work-around is to pass FALSE if validation failed. + if ($all_passed) { + drupal_set_message('Your file uploaded successfully. Please click "Next" to continue.'); + return array(); + } + else { + return FALSE; + } + } + elseif ($file->source == 'bdnd') { + // Set processing time to 10 mins. + ini_set('max_execution_time', $max_time); + + // Backup file page. + + // Array to hold the validation result. + $status = array(); + + // Project id number the spreadsheet and column headers are specific to. + $project_id = (int)$_POST['backup_sel_project']; + + // Perform basic compliance test: + // - A project is selected. + // - File is Microsoft Excel Spreadhseet file. + // - Measurement tab exists. + // - Essential column headers defined in the project are present. + $flag_index = array('project_selected', 'is_excel', 'tab_exists', 'column_exists'); + + // Validate file. + $flags = rawpheno_validate_excel_file($file, $project_id, 'backup'); + + $s = (isset($flags['status'])) ? $flags['status'] : $flags; + + // If DND backup, missing measurements, skip all validator but + // save/backup the file anyway. + if ($s['tab_exists'] === FALSE) { + return array(); + } + + // Read only the status from test listed above. + foreach($s as $i => $v) { + if (in_array($i, $flag_index) AND ($v === FALSE || $v === 'todo')) { + $status[$i] = $v; + } + } + + // When any of the mentioned test failed, show them to user. + if (count($status) > 0) { + if (isset($flags['check_limit'])) { + drupal_set_message($flags['check_limit'], 'error'); + } + + drupal_set_message(theme('rawpheno_upload_validation_report', array('status' => $status)), 'rawpheno-validate-progress'); + return FALSE; + } + + // Else, proceed to hook_file_insert(). + } + else { + // File source is one that is not of interest to us. + // Do not return anything or it will trigger validation errors for other modules. + } +} + + +/** + * Make the phenotype excel file permanent on successful upload. + * + * This is an additional process handler used by the form API to generate the form array + * for a given element. Usually it is used to make a custom form element or enhance a + * standard form element. + * + * We are using it to capture the just uploaded file within the AJAX call by checking + * when the element is rendered if it has a fid (ie: has been saved). + */ +function rawpheno_phenotype_upload_file_element_process($element, &$form_state, $form) { + if (isset($element['#value']['fid']) AND !empty($element['#value']['fid'])) { + $file_id = $element['#value']['fid']; + rawpheno_upload_make_file_permanent($file_id); + } + + return $element; +} + + +/** + * Make the file uploaded permanent and make a record indicating that file is used by the module. + * + * @param $file_id + * File id in Drupal file object. + */ +function rawpheno_upload_make_file_permanent($file_id) { + // Get the file object. + $file = file_load($file_id); + + if ($file) { + // Make the file permanent. + $file->status = FILE_STATUS_PERMANENT; + file_save($file); + + // Also, point out that we are using it ;-) + // Note, the file_usage_add() function expects a numerical unique id which we don't have. + // We have gotten around this by using the uid concatenated with the timestamp using + // the assumption that a single user cannot upload more than one phenotype file within a second. + file_usage_add($file, 'rawpheno', 'rawphenotypes-file', $file->uid . $file->timestamp); + } +} + + +/** + * Implements hook_validate(). + */ +function rawpheno_upload_form_master_validate($form, &$form_state) { } + + +/** + * Implements hook_submit(). + * + * Master submit to handle form submit. + */ +function rawpheno_upload_form_master_submit(&$form, &$form_state) { + // Which button triggers a submit action. + $btn_submit = $form_state['triggering_element']['#value']; + + // Save any additional traits and then submit a job to save the spreadsheet. + if ($form_state['stage'] == 'review') { + $job_id = rawpheno_submit_review($form, $form_state); + + // Then we need to add the job_id to the path so the system can keep track of it. + if ($job_id) { + drupal_goto(current_path() . '/' . $job_id); + } + } + + // If we just uploaded the file then we want to save the fid for easy access. + if (isset($form_state['values']['dnd'])) { + $form_state['multistep_values']['fid'] = $form_state['values']['dnd']; + } + + // If the next step button was pressed then iterate to the next step. + if ($btn_submit == 'Next Step') { + // Definitely save the form id. + if(isset($form_state['multistep_values']['form_build_id'])) { + $form_state['values']['form_build_id'] = $form_state['multistep_values']['form_build_id']; + } + + // Save the values from the current step. + $form_state['multistep_values'][$form_state['stage']] = $form_state['values']; + + // Iterate to the next step. + $form_state['new_stage'] = rawpheno_next_page($form, $form_state); + + // Ensure the form state is saved and the form is rebuilt. + $form_state['multistep_values']['form_build_id'] = $form_state['values']['form_build_id']; + $form_state['stage'] = $form_state['new_stage']; + $form_state['rebuild'] = TRUE; + } +} + + +/** + * Save spreadsheet to database. + */ +function rawpheno_submit_review($form, &$form_state) { + // Project id number the spreadsheet and column headers are specific to. + $project_id = $form_state['values']['sel_project']; + + // Save spreadsheet data in the following order. + // 1. New column headers. + // 2. The entire spreadsheet. + + // cvterm id of controlled vocabulary. + if (function_exists('chado_get_cv')) { + $cvid = chado_get_cv(array('name' => 'phenotype_measurement_units')); + } + else { + $cvid = tripal_get_cv(array('name' => 'phenotype_measurement_units')); + } + + $cv_measurements_unit = $cvid->cv_id; + + // 1. Save new headers. + // Read variable that holds new column headers. + $new_header = $form_state['multistep_values']['new_headers']; + + // Create an array of new hearders with flag/status if user wants to save it. + // This array will be passed to rawpheno_load_spreadsheet. + $arr_newheaders = array(); + + // Determine if there is new header. + if (count($new_header) > 0) { + $trait_type = rawpheno_function_trait_types(); + + // Read each column header. + foreach($new_header as $i => $header) { + // For each new header store information provided in the interface. + // Indicates if user has check this header for saving. + $header = trim(str_replace(array("\n", "\r", " "), ' ', $header)); + $header = preg_replace('/\s+/', ' ', $header); + + $arr_newheaders[$header]['flag'] = ($form_state['values']['chk_' . $i] == 1) ? 1 : 0; + + // Determine if the form in review traits has been filled out and checkbox + // has been checked by user. If it has been checked then save the trait. + if ($form_state['values']['chk_' . $i] === 1 && !empty($form_state['values']['txt_header_' . $i])) { + // Before save, we need to tell if the header is present in the database and + // user just wants to reuse them. Otherwise, add a new header. + // Reuse header - set to OPTIONAL. + if ((isset($form_state['values']['sel_header_' . $i]) AND $form_state['values']['sel_header_' . $i] > 0) OR + (isset($form_state['values']['txt_header_cvterm_id_' . $i]))) { + + // User selected from a list of similar headers. + $cvterm_id = (isset($form_state['values']['sel_header_' . $i])) + ? $form_state['values']['sel_header_' . $i] + : $form_state['values']['txt_header_cvterm_id_' . $i]; + + // Map this header to the project. + $sql = "SELECT cvterm_id FROM {pheno_project_cvterm} WHERE project_id = :project_id AND cvterm_id = :cvterm_id LIMIT 1"; + $args = array(':project_id' => $project_id, ':cvterm_id' => $cvterm_id); + + $h = db_query($sql, $args); + if ($h->rowCount() <= 0) { + // Add to project only when it is not in the project. + // Set the trait type to contributed. + db_insert('pheno_project_cvterm') + ->fields(array( + 'project_id' => $project_id, + 'cvterm_id' => $cvterm_id, + 'type' => $trait_type['type2']) + ) + ->execute(); + } + + // When saving this data for this header, use the cvterm_id. + $arr_newheaders[$header]['alt_header'] = $cvterm_id; + continue; + } + + // Check if the trait exists in the database, then it is likely + // that the user is reusing the trait - threfore it is not contributed and just map + // the cvterm id to a project. + + // Add the as contributed - set to CONTRIBUTED. + // Construct the column header name. + $name = trim($form_state['values']['txt_header_' . $i]); + $name = preg_replace('/\s+/', ' ', $name); + + $unit = trim($form_state['values']['txt_unit_' . $i]); + $method = trim($form_state['values']['txtarea_describe_' . $i]); + $def = trim($form_state['values']['txt_def_' . $i]); + + // Format the header. + if (strpbrk($name, '()')) { + // Header has a unit part. + $name = trim(str_replace(array("\n", "\r", " "), ' ', $name)); + } + else { + // Construct header plus the unit. + $name = $name . ' (' . strtolower($unit) . ')'; + } + + // Trait properties - use when inserting the cterm and reference to other property. + $m_cvterm = array( + 'id' => 'rawpheno_tripal:' . $name, + 'name' => $name, + 'definition' => $def, + 'cv_name' => 'phenotype_measurement_types' + ); + + // Search the name in cvterm and decide if trait should be considered optional or contributed. + $sql = "SELECT t2.cvterm_id + FROM {cv} AS t1 INNER JOIN {cvterm} AS t2 USING (cv_id) + WHERE + trim(lower(t2.name)) = trim(lower(:cvterm_name)) + AND t1.name = :cv_name LIMIT 1"; + + $args = array(':cvterm_name' => $m_cvterm['name'], ':cv_name' => $m_cvterm['cv_name']); + $result = chado_query($sql, $args) + ->fetchObject(); + + if ($result) { + // Found use the id. + $m_cvterm_id = $result->cvterm_id; + // Trait is optional. + $type = $trait_type['type2']; + } + else { + // Not found, insert and get the inserted id. + $m = tripal_insert_cvterm($m_cvterm); + $m_cvterm_id = $m->cvterm_id; + // Trait is contributed. + $type = $trait_type['type5']; + } + + // When saving this data for this header, use the cvterm_id. + $arr_newheaders[$header]['alt_header'] = $m_cvterm_id; + db_insert('pheno_project_cvterm') + ->fields(array('project_id' => $project_id, + 'cvterm_id' => $m_cvterm_id, + 'type' => $type)) + ->execute(); + + // Create a R Friendly version. + $r_version = rawpheno_function_make_r_compatible($m_cvterm['name']); + + if (function_exists('chado_get_cv')) { + $cv_rfriendly = chado_get_cv(array('name' => 'phenotype_r_compatible_version')); + } + else { + $cv_rfriendly = tripal_get_cv(array('name' => 'phenotype_r_compatible_version')); + } + + $values = array( + 'cvterm_id' => $m_cvterm_id, + 'type_id' => $cv_rfriendly->cv_id, + 'value' => $r_version, + 'rank' => 0 + ); + + chado_insert_record('cvtermprop', $values); + + // Then save the Unit. + $u_cvterm = array( + 'id' => 'rawpheno_tripal:' . strtolower($unit), + 'name' => strtolower($unit), + 'definition' => $unit, + 'cv_name' => 'phenotype_measurement_units' + ); + + $u_cvterm_id = chado_select_record('cvterm',array('cvterm_id'), + array('name' => $u_cvterm['name'], + 'cv_id' => array('name' => $u_cvterm['cv_name']))); + if (!$u_cvterm_id) { + $u_cvterm_id = tripal_insert_cvterm($u_cvterm); + } + + // Grab just the id. + if (is_array($u_cvterm_id)) { + $u_cvterm_id = $u_cvterm_id[0]->cvterm_id; + } + elseif (is_object($u_cvterm_id)) { + $u_cvterm_id = $u_cvterm_id->cvterm_id; + } + + // Don't forget the method description. + $prop = array( + 'cvterm_id' => $m_cvterm_id, + 'type_id' => $cv_measurements_unit, + 'value' => $method, + 'rank' => 0, + ); + + $prop_id = chado_select_record('cvtermprop', array('cvtermprop_id'), $prop); + if (!$prop_id) { + $prop = chado_insert_record('cvtermprop', $prop); + } + + // Finally relate the measurement and unit. + $rel = array( + 'subject_id' => $u_cvterm_id, + 'type_id' => $cv_measurements_unit, + 'object_id' => $m_cvterm_id, + ); + $rel_id = chado_select_record('cvterm_relationship', array('cvterm_relationship_id'), $rel); + if (!$rel_id) { + chado_insert_record('cvterm_relationship', $rel); + } + } + } + } + + // 2. The entire spreadsheet. + // Get the variable that holds the path to the spreadsheet file in the server. + $file = file_load($form_state['multistep_values']['fid']); + $xls_file = drupal_realpath($file->uri); + + // Array of required traits excluding Name. + $plantprop_headers = rawpheno_project_plantproperty_traits($project_id); + + // Drupal user object. + global $user; + + if (isset($xls_file) && !empty($xls_file)) { + $job_id = tripal_add_job( + "Upload Phenoypic data: " . $xls_file, + 'rawpheno', + 'rawpheno_load_spreadsheet', + array( + $project_id, + serialize($arr_newheaders), + $form_state['multistep_values']['fid'], + serialize($plantprop_headers) + ), + $user->uid + ); + + return $job_id; + } +} diff --git a/include/rawpheno.upload.helpers.inc b/includes/rawpheno.upload.helpers.inc old mode 100755 new mode 100644 similarity index 96% rename from include/rawpheno.upload.helpers.inc rename to includes/rawpheno.upload.helpers.inc index 3bc05be..e86f45e --- a/include/rawpheno.upload.helpers.inc +++ b/includes/rawpheno.upload.helpers.inc @@ -1,69 +1,69 @@ - 1, 'review' => 2, 'save' => 3); - $current_step = (isset($form_stages[$form_state['stage']])) ? $form_stages[$form_state['stage']] : 1; - - // Array of stage indicators. - $stages = array(1 => '1. Validate Spreadsheet', - 2 => '2. Describe New Trait', - 3 => '3. Save Spreadsheet'); - - $markup = ''; - foreach($stages as $k => $v) { - $class = ($k <= $current_step) ? '' : ' progress-stage-todo'; - $markup .= '
-  ' . $v . '  -
'; - } - - // Add header to each stage with corresponding - // stage information defined above. - $form['header_upload'] = array( - '#type' => 'markup', - '#markup' => $markup, - ); - - return $form; -} - - -/** - * Function to calculate the next stage. - * - * @param $form - * @param $form_state - * - * @return - * A string containing the stage name. - */ -function rawpheno_next_page($form, &$form_state) { - // Get the address/name of the next page based on the current stage. - switch($form_state['stage']) { - case 'check': - // In stage check, next is stage 03 or stage 02. - $btn_submit = $form_state['triggering_element']['#value']; - return ($btn_submit == 'Save spreadheet') ? 'save' : 'review'; - break; - - case 'review': - // In stage review, next is stage 03. - return 'save'; - break; - } -} + 1, 'review' => 2, 'save' => 3); + $current_step = (isset($form_stages[$form_state['stage']])) ? $form_stages[$form_state['stage']] : 1; + + // Array of stage indicators. + $stages = array(1 => '1. Validate Spreadsheet', + 2 => '2. Describe New Trait', + 3 => '3. Save Spreadsheet'); + + $markup = ''; + foreach($stages as $k => $v) { + $class = ($k <= $current_step) ? '' : ' progress-stage-todo'; + $markup .= '
+  ' . $v . '  +
'; + } + + // Add header to each stage with corresponding + // stage information defined above. + $form['header_upload'] = array( + '#type' => 'markup', + '#markup' => $markup, + ); + + return $form; +} + + +/** + * Function to calculate the next stage. + * + * @param $form + * @param $form_state + * + * @return + * A string containing the stage name. + */ +function rawpheno_next_page($form, &$form_state) { + // Get the address/name of the next page based on the current stage. + switch($form_state['stage']) { + case 'check': + // In stage check, next is stage 03 or stage 02. + $btn_submit = $form_state['triggering_element']['#value']; + return ($btn_submit == 'Save spreadheet') ? 'save' : 'review'; + break; + + case 'review': + // In stage review, next is stage 03. + return 'save'; + break; + } +} diff --git a/include/rawpheno.validation.inc b/includes/rawpheno.validation.inc similarity index 100% rename from include/rawpheno.validation.inc rename to includes/rawpheno.validation.inc diff --git a/rawpheno.install b/rawpheno.install index 7fe9d26..81a245f 100755 --- a/rawpheno.install +++ b/rawpheno.install @@ -283,7 +283,7 @@ function rawpheno_schema() { } // Include function to manage column headers, cv terms and variable names. -module_load_include('inc', 'rawpheno', 'include/rawpheno.function.measurements'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.function.measurements'); /** * Function to manage terms used by this module. diff --git a/rawpheno.module b/rawpheno.module index c85d488..15e56e3 100755 --- a/rawpheno.module +++ b/rawpheno.module @@ -12,13 +12,15 @@ */ // Include function to manage column headers. -module_load_include('inc', 'rawpheno', 'include/rawpheno.function.measurements'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.function.measurements'); // Include functions required in processing spreadsheet file. -module_load_include('inc', 'rawpheno', 'include/rawpheno.upload.excel'); -module_load_include('inc', 'rawpheno', 'include/rawpheno.validation'); -module_load_include('inc', 'rawpheno', 'include/rawpheno.upload.form'); -module_load_include('inc', 'rawpheno', 'include/rawpheno.tripaldownload'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.upload.excel'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.validation'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.upload.form'); +module_load_include('inc', 'rawpheno', 'includes/rawpheno.tripaldownload'); +// Prepare term used by Germplasm Raw Phenotypes Field. +module_load_include('inc', 'rawpheno', 'includes/TripalFields/rawpheno.fields'); /** * Implements hook_menu(). @@ -37,7 +39,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_rawdata'), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.rawdata.form.inc', + 'file' => 'includes/rawpheno.rawdata.form.inc', 'type' => MENU_NORMAL_ITEM, ); // Menu callback which generates the summary data in JSON, @@ -74,7 +76,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_instructions'), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.instructions.form.inc', + 'file' => 'includes/rawpheno.instructions.form.inc', 'type' => MENU_NORMAL_ITEM, 'weight' => 0, ); @@ -84,7 +86,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_instructions', 3), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.instructions.form.inc', + 'file' => 'includes/rawpheno.instructions.form.inc', 'type' => MENU_CALLBACK, ); // Menu callback which generates a list of headers in JSON, @@ -101,7 +103,7 @@ function rawpheno_menu() { 'page callback' => 'rawpheno_instructions_create_spreadsheet', 'page arguments' => array(4), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.instructions.form.inc', + 'file' => 'includes/rawpheno.instructions.form.inc', 'type' => MENU_CALLBACK, ); @@ -114,7 +116,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_download'), 'access arguments' => array('download rawpheno'), - 'file' => 'include/rawpheno.download.form.inc', + 'file' => 'includes/rawpheno.download.form.inc', 'type' => MENU_NORMAL_ITEM, 'weight' => 6, ); @@ -134,7 +136,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_upload_form_master'), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.upload.form.inc', + 'file' => 'includes/rawpheno.upload.form.inc', 'type' => MENU_NORMAL_ITEM, 'weight' => 3, ); @@ -154,7 +156,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_backup'), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.backup.form.inc', + 'file' => 'includes/rawpheno.backup.form.inc', 'type' => MENU_NORMAL_ITEM, 'weight' => 7, ); @@ -164,7 +166,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_backup', 3, 4, 5), 'access arguments' => array('access rawpheno'), - 'file' => 'include/rawpheno.backup.form.inc', + 'file' => 'includes/rawpheno.backup.form.inc', 'type' => MENU_CALLBACK, ); @@ -176,7 +178,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_admin_main_page'), 'access arguments' => array('access administration pages'), - 'file' => 'include/rawpheno.admin.form.inc', + 'file' => 'includes/rawpheno.admin.form.inc', 'type' => MENU_NORMAL_ITEM, ); @@ -186,7 +188,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_admin_page'), 'access arguments' => array('access administration pages'), - 'file' => 'include/rawpheno.admin.form.inc', + 'file' => 'includes/rawpheno.admin.form.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); @@ -198,7 +200,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_admin_rheaders'), 'access arguments' => array('access administration pages'), - 'file' => 'include/rawpheno.admin.form.inc', + 'file' => 'includes/rawpheno.admin.form.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); @@ -211,7 +213,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_admin_all_projects'), 'access arguments' => array('access administration pages'), - 'file' => 'include/rawpheno.admin.form.inc', + 'file' => 'includes/rawpheno.admin.form.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 3, ); @@ -220,7 +222,7 @@ function rawpheno_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('rawpheno_admin_project_management', 5, 6, 7), 'access arguments' => array('access administration pages'), - 'file' => 'include/rawpheno.admin.form.inc', + 'file' => 'includes/rawpheno.admin.form.inc', 'type' => MENU_CALLBACK, ); @@ -1177,3 +1179,21 @@ function rawpheno_preprocess_block(&$vars) { return $vars; } + + +// RAWPHENOTYPES IN GERMPLASM FIELD. + + +/** + * Implements hook_field_group_build_pre_render_alter(). + * Apply css and js. + */ +function rawpheno_field_group_build_pre_render_alter(&$element) { + $rp_path = drupal_get_path('module', 'rawpheno') . '/theme/'; + + $element['#attached']['js'][] = $rp_path . 'js/rawpheno.germplasm.field.js'; + $element['#attached']['css'][] = $rp_path . 'css/rawpheno.germplasm.field.css'; + + // Rawphenotypes download link: + drupal_add_js(array('rawpheno' => array('exportLink' => $GLOBALS['base_url'] . '/phenotypes/raw/download')), array('type' => 'setting')); +} diff --git a/theme/css/rawpheno.germplasm.field.css b/theme/css/rawpheno.germplasm.field.css new file mode 100644 index 0000000..821c9b5 --- /dev/null +++ b/theme/css/rawpheno.germplasm.field.css @@ -0,0 +1,152 @@ +/** + * @file + * Style rawphenotypes in Germplasm field. + */ + +#rawphenotypes-germplasm-raw-phenotypes-field img { + margin: 0 !important; +} + +#rawphenotypes-germplasm-table-wrapper { + -moz-box-shadow: inset 0 -10px 20px -10px #EAEAEA; + -webkit-box-shadow: inset 0 -10px 20px -10px #EAEAEA; + box-shadow: inset 0 -10px 20px -10px #EAEAEA; + z-index: 1000; +} + +#rawphenotypes-germplasm-field-header { + position: relative; + height: 45px; +} + +#rawphenotypes-germplasm-field-header div:first-child { + float: left; + height: inherit; +} + +#rawphenotypes-germplasm-field-header div:nth-child(2) { + float: right; + height: inherit; +} + +#rawphenotypes-germplasm-field-header div:nth-child(2) div { + background-color: #314355; + height: 40px; + width: 190px; + text-align:center; + line-height: 40px; +} + +.rawphenotypes-germplasm-nav a, +.rawphenotypes-germplasm-nav a:link, +.rawphenotypes-germplasm-nav a:active, +.rawphenotypes-germplasm-nav a:visited { + font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, sans-serif; + color: #FFFFFF; + display: inline-block; + white-space: nowrap; + font-size: 0.9em; + text-decoration: none; +} + +#rawphenotypes-germplasm-field-header div:nth-child(2) div:hover { + color: #FFFFFF; + text-decoration: none; + + -moz-box-shadow: 1px 1px 5px 1px #CCCCCC; + -webkit-box-shadow: 1px 1px 5px 1px #CCCCCC; + box-shadow: 1px 1px 5px 1px #CCCCCC; +} + +#rawphenotypes-define-raw-container { + clear: both; +} + +#rawphenotypes-germplasm-field-header div:last-child { + clear: both; + height: 1px; +} + +#rawphenotypes-germplasm-raw-phenotypes-field h1 span { + color: #3A7F21; +} + +#rawphenotypes-germplasm-field-table { + padding: 0; + margin: 0 !important; +} + +#rawphenotypes-germplasm-field-table td select { + min-width: 250px; + width: 300px; +} + +#rawphenotypes-germplasm-field-table td { font-size: 1.2em; } +#rawphenotypes-germplasm-field-header img, +#rawphenotypes-germplasm-field-table th img, +#rawphenotypes-germplasm-field-table td img { + border: none !important +} + +#rawphenotypes-germplasm-field-table th:last-child, +#rawphenotypes-germplasm-field-table td:last-child { + text-align: center ; + width: 10px; +} + +#rawphenotypes-germplasm-field-table td:last-child img { + opacity: 0.5; +} + +#rawphenotypes-germplasm-field-table td:first-child { + width: 100%; +} + +#rawphenotypes-germplasm-export-table { + padding: 0; + margin: 0; + height: 238px; + overflow-y: scroll; + border: none; +} + +#rawphenotypes-germplasm-export-table tr:nth-child(2n+2) { + background: #efefef; + background: rgba(0, 0, 0, 0.063); +} + +#rawphenotypes-germplasm-export-table tbody tr { + cursor: pointer; +} + +#rawphenotypes-germplasm-export-table tbody tr:hover { + background-color: rgba(0, 0, 0, 0.1); + + -webkit-transition: background-color 0.9s ease-out; + -moz-transition: background-color 0.9s ease-out; + -o-transition: background-color 0.9s ease-out; + transition: background-color 0.9s ease-out; + + border-left: 2px solid #314355; +} + +#rawphenotypes-germplasm-raw-phenotypes-field small { + float: right; + margin: 10px 0 0 0; +} + +#rawphenotypes-define-raw-container { + border-bottom: 1px solid #CCCCCC; + cursor: pointer; + margin: 20px 0; + padding: 0 0 15px 0; + text-align: center; + + -moz-box-shadow: 0 5px 7px -9px #333333; + -webkit-box-shadow: 0 5px 7px -9px #333333; + box-shadow: 0 5px 7px -9px #333333; +} + +#rawphenotypes-germplasm-warning { + margin: 8px 0; +} \ No newline at end of file diff --git a/theme/js/rawpheno.germplasm.field.js b/theme/js/rawpheno.germplasm.field.js new file mode 100644 index 0000000..4727617 --- /dev/null +++ b/theme/js/rawpheno.germplasm.field.js @@ -0,0 +1,70 @@ +/** + * Rawphenotypes in Germplasm Field. + */ +(function ($) { + Drupal.behaviors.rawphenotypesGermplasm = { + attach: function (context, settings) { + // Raw phenotype definition. + $('#rawphenotypes-germplasm-field-header div').eq(0).find('a').click(function(e) { + e.preventDefault(); + $('#rawphenotypes-define-raw-container').slideDown(200); + }); + + // Okay - definition. + if ($('#rawphenotypes-define-raw-container')) { + $('#rawphenotypes-define-raw-container a').click(function(e) { + e.preventDefault(); + $('#rawphenotypes-define-raw-container').slideUp(200); + }); + } + + + var imgOpacity = 0.5; + + // Select box event - prepare link to download selection. + var selects = $('#rawphenotypes-germplasm-field-table select'); + var downloadLink = ''; + + selects.change(function(e) { + var selectValue = e.target.value; + var selectId = e.target.id; + downloadLink = ''; + + // Reset other select to allow only one select field + // at a time to export. + selects.each(function() { + if ($(this).attr('id') != selectId) { + $('#' + $(this).attr('id') + '-img').css('opacity', '0.5'); + this.selectedIndex = 0; + } + }); + + if (selectValue == '0') { + // None selected - default to select an option. + imgOpacity = '0.5'; + // Detach event created. + } + else { + // Selected, prepare query string for export. + imgOpacity = '1'; + var params = selectValue.split('#'); + // Project id & location & trait. + downloadLink = 'p=' + params[1] + '&l=' + params[2] + '&t=' + params[0]; + } + + $('#' + selectId + '-img').css('opacity', imgOpacity); + }); + + // Listen to images clicked to launch data download. + $('#rawphenotypes-germplasm-field-table td:last-child img').click(function(e) { + var imgId = e.target.id; + var imgOpacity = $(this).css('opacity'); + + if (imgOpacity == 1) { + window.open( + Drupal.settings.rawpheno.exportLink + '?code=' + btoa(downloadLink), + '_blank' + ); + } + }); +}};}(jQuery)); \ No newline at end of file From fe6bc13de586342516dc3a3e4151c0e529147c56 Mon Sep 17 00:00:00 2001 From: reynold tan Date: Wed, 22 Dec 2021 15:26:53 -0600 Subject: [PATCH 02/16] Filters resultset to match stock id --- .../ncit__raw_data_formatter.inc | 10 +++---- includes/rawpheno.download.form.inc | 5 ++-- includes/rawpheno.tripaldownload.inc | 28 +++++++++++++++---- theme/js/rawpheno.germplasm.field.js | 2 +- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 5f420de..32ca6ab 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -92,7 +92,7 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { list($trait_id, $trait_name) = explode('_', $trait); - $select = $this->create_select($exp_loc, $germplasm_raw_phenotypes['user']['experiments']); + $select = $this->create_select($germplasm_raw_phenotypes['germplasm']['id'], $exp_loc, $germplasm_raw_phenotypes['user']['experiments']); $table_row[ $id ] = array(sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $germplasm_raw_phenotypes['icons']['export'])); $id++; } @@ -126,20 +126,20 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { /** * Create select field. * - * @param $id - * Id attribute of the final select field. + * @param $germplasm + * Stock id number. * @param $items * Associative array, where each item will be rendered as an option * with key as the value and value as text. * @param $disable * Array of items to match an item if it should be disabled. */ - public function create_select($items, $disable) { + public function create_select($germplasm, $items, $disable) { $option = array(); $cache_exp = []; foreach($items as $loc_exp) { list($trait_id, $project_id, $project_name, $location) = explode('#', $loc_exp); - $select_value = $trait_id . '#' . $project_id . '#' . $location; + $select_value = $trait_id . '#' . $project_id . '#' . $location . '#' . $germplasm; $cache_exp[] = $project_id; $disabled = (in_array($project_id, $disable)) ? '' : 'disabled'; diff --git a/includes/rawpheno.download.form.inc b/includes/rawpheno.download.form.inc index b786474..3de7765 100644 --- a/includes/rawpheno.download.form.inc +++ b/includes/rawpheno.download.form.inc @@ -37,10 +37,11 @@ function rawpheno_download($form, &$form_state) { if (in_array($param_experiment, $user_experiment)) { $param_location = $query_vars['l']; $param_trait = (int) $query_vars['t']; + $param_stock = (int) $query_vars['g']; if ($param_experiment > 0 && $param_location && $param_trait > 0) { // Create query string. - $query_string = 'p=' . $param_experiment . '&l=' . $param_location . '&t=' . $param_trait . '&r=0&e=0&file=0'; + $query_string = 'p=' . $param_experiment . '&l=' . $param_location . '&t=' . $param_trait . '&r=0&e=0&file=0&g=' . $param_stock; drupal_goto('/phenotypes/raw/csv', array('query' => array('code' => base64_encode($query_string)))); } } @@ -390,7 +391,7 @@ function rawpheno_download_submit($form, &$form_state) { // Contain all query parameters/string into one string. // Decode first when reading this string using base64_decode() function. - $url = 'p=' . $prj . '&l=' . $loc . '&t=' . $trt . '&r=' . $rvr . '&e=' . $env . '&file=' . $env_filename; + $url = 'p=' . $prj . '&l=' . $loc . '&t=' . $trt . '&r=' . $rvr . '&e=' . $env . '&file=' . $env_filename . '&g=0'; // Format url for redirect. $form_state['redirect'] = array( diff --git a/includes/rawpheno.tripaldownload.inc b/includes/rawpheno.tripaldownload.inc index 41b7fb5..1e6184e 100644 --- a/includes/rawpheno.tripaldownload.inc +++ b/includes/rawpheno.tripaldownload.inc @@ -209,7 +209,7 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { } $q = base64_decode($code); - list($project, $location, $traits, $r_version,,) = explode('&', $q); + list($project, $location, $traits, $r_version,,,$germplasm) = explode('&', $q); // Projects: $tmp = trim(str_replace('p=', '', $project)); @@ -261,14 +261,30 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { } } - // Sub-query to select plant_id given a location and project. - // NOTE: leading and trailing spaces are required. - $sub_sql = " (SELECT plant_id + // Germplasm/Stock id - request from germplasm field. + $germplasm = trim(str_replace('g=', '', $germplasm)); + + if ($germplasm != '' && $gemplasm > 0) { + // Sub-query to select plant_id given a location and project and germplasm. + // NOTE: leading and trailing spaces are required. + $sub_sql = " (SELECT plant_id + FROM {pheno_plantprop} INNER JOIN {pheno_plant_project} USING(plant_id) + WHERE value IN (:location) AND project_id IN (:project)) + AND plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :stock_id) "; + + // Query values required by sub query. + $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits, ':stock_id' => $germplasm); + } + else { + // Sub-query to select plant_id given a location and project. + // NOTE: leading and trailing spaces are required. + $sub_sql = " (SELECT plant_id FROM {pheno_plantprop} INNER JOIN {pheno_plant_project} USING(plant_id) WHERE value IN (:location) AND project_id IN (:project)) "; - // Query values required by sub query. - $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); + // Query values required by sub query. + $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); + } // First we need to get the header. This will allow us to ensure that the data // downloaded all matches up with the trait it is associated with. Furthermore, diff --git a/theme/js/rawpheno.germplasm.field.js b/theme/js/rawpheno.germplasm.field.js index 4727617..dedcc47 100644 --- a/theme/js/rawpheno.germplasm.field.js +++ b/theme/js/rawpheno.germplasm.field.js @@ -49,7 +49,7 @@ imgOpacity = '1'; var params = selectValue.split('#'); // Project id & location & trait. - downloadLink = 'p=' + params[1] + '&l=' + params[2] + '&t=' + params[0]; + downloadLink = 't=' + params[0] + '&p=' + params[1] + '&l=' + params[2] + '&g=' + params[3]; } $('#' + selectId + '-img').css('opacity', imgOpacity); From 5541a672712d8360fab16d2820eb94179cc38dde Mon Sep 17 00:00:00 2001 From: reynold tan Date: Wed, 22 Dec 2021 15:53:23 -0600 Subject: [PATCH 03/16] Filters resultset to match stock id --- includes/rawpheno.tripaldownload.inc | 37 +++++++++++++--------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/includes/rawpheno.tripaldownload.inc b/includes/rawpheno.tripaldownload.inc index 1e6184e..16b4339 100644 --- a/includes/rawpheno.tripaldownload.inc +++ b/includes/rawpheno.tripaldownload.inc @@ -262,29 +262,21 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { } // Germplasm/Stock id - request from germplasm field. - $germplasm = trim(str_replace('g=', '', $germplasm)); - - if ($germplasm != '' && $gemplasm > 0) { - // Sub-query to select plant_id given a location and project and germplasm. - // NOTE: leading and trailing spaces are required. - $sub_sql = " (SELECT plant_id - FROM {pheno_plantprop} INNER JOIN {pheno_plant_project} USING(plant_id) - WHERE value IN (:location) AND project_id IN (:project)) - AND plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :stock_id) "; - - // Query values required by sub query. - $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits, ':stock_id' => $germplasm); + $stock_id = trim(str_replace('g=', '', $germplasm)); + $stock_name = ''; + if ($stock_id != '' && $stock_id > 0) { + $stock_name = chado_query("SELECT name FROM {stock} WHERE stock_id = :stock_id", array(':stock_id' => $stock_id)) + ->fetchField(); } - else { - // Sub-query to select plant_id given a location and project. - // NOTE: leading and trailing spaces are required. - $sub_sql = " (SELECT plant_id + + // Sub-query to select plant_id given a location and project. + // NOTE: leading and trailing spaces are required. + $sub_sql = " (SELECT plant_id FROM {pheno_plantprop} INNER JOIN {pheno_plant_project} USING(plant_id) WHERE value IN (:location) AND project_id IN (:project)) "; - // Query values required by sub query. - $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); - } + // Query values required by sub query. + $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); // First we need to get the header. This will allow us to ensure that the data // downloaded all matches up with the trait it is associated with. Furthermore, @@ -378,7 +370,12 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { // Note: this first array will not have any missing data cells filled in. $rows = array(); foreach($results as $r) { - $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; + if ($stock_name != '' && $r->value == $stock_name) { + $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; + } + else { + $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; + } } // Total lines; From 158c27e0f2bce2e089de2ff464078e60a6bf837a Mon Sep 17 00:00:00 2001 From: reynold tan Date: Wed, 22 Dec 2021 18:30:10 -0600 Subject: [PATCH 04/16] Filters resultset to match stock id --- includes/rawpheno.tripaldownload.inc | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/includes/rawpheno.tripaldownload.inc b/includes/rawpheno.tripaldownload.inc index 16b4339..6400999 100644 --- a/includes/rawpheno.tripaldownload.inc +++ b/includes/rawpheno.tripaldownload.inc @@ -263,21 +263,24 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { // Germplasm/Stock id - request from germplasm field. $stock_id = trim(str_replace('g=', '', $germplasm)); - $stock_name = ''; - if ($stock_id != '' && $stock_id > 0) { - $stock_name = chado_query("SELECT name FROM {stock} WHERE stock_id = :stock_id", array(':stock_id' => $stock_id)) - ->fetchField(); - } + $limit_stock = ''; // Sub-query to select plant_id given a location and project. // NOTE: leading and trailing spaces are required. $sub_sql = " (SELECT plant_id FROM {pheno_plantprop} INNER JOIN {pheno_plant_project} USING(plant_id) - WHERE value IN (:location) AND project_id IN (:project)) "; + WHERE value IN (:location) AND project_id IN (:project) %s) "; // Query values required by sub query. $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); + if ($stock_id != '' && (int) $stock_id > 0) { + $limit_stock = "AND plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :stock_id)"; + $arr_q_string[':stock_id'] = $stock_id; + } + + $sub_sql = sprintf($sub_sql, $limit_stock); + // First we need to get the header. This will allow us to ensure that the data // downloaded all matches up with the trait it is associated with. Furthermore, // it will allow us to handle missing data. @@ -370,12 +373,7 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { // Note: this first array will not have any missing data cells filled in. $rows = array(); foreach($results as $r) { - if ($stock_name != '' && $r->value == $stock_name) { - $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; - } - else { - $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; - } + $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; } // Total lines; From 6437f8297fb0f37333764fda3455d03042e1d9ca Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Tue, 22 Feb 2022 09:29:54 -0600 Subject: [PATCH 05/16] Adds experiment column header --- includes/rawpheno.tripaldownload.inc | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/includes/rawpheno.tripaldownload.inc b/includes/rawpheno.tripaldownload.inc index 6400999..afa4fc7 100644 --- a/includes/rawpheno.tripaldownload.inc +++ b/includes/rawpheno.tripaldownload.inc @@ -215,6 +215,10 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { $tmp = trim(str_replace('p=', '', $project)); $project = explode(',', $tmp); + if ($project[0]) { + $project_name = rawpheno_function_getproject($project[0]); + } + // Locations: $location = trim(str_replace('l=', '', $location)); @@ -274,9 +278,16 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { // Query values required by sub query. $arr_q_string = array(':project' => $project, ':location' => $location, ':traits' => $traits); + // Array to hold column headers. + // Add Name/Stock name column headers array. + $header = array('A0' => 'Name'); + if ($stock_id != '' && (int) $stock_id > 0) { $limit_stock = "AND plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :stock_id)"; $arr_q_string[':stock_id'] = $stock_id; + + // Add experiment to header if request comes from field. + $header['D0'] = 'Experiment'; } $sub_sql = sprintf($sub_sql, $limit_stock); @@ -297,10 +308,6 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { $result = db_query($sql, $arr_q_string); - // Array to hold column headers. - //Add Name/Stock name column headers array. - $header = array('A0' => 'Name'); - foreach ($result as $r) { $def = $r->name; @@ -374,6 +381,11 @@ function rawpheno_trpdownload_generate_file($variables, $job_id = NULL) { $rows = array(); foreach($results as $r) { $rows[ $r->id ][ $r->grp . $r->tid ] = $r->value; + + if ($stock_id != '' && (int) $stock_id > 0) { + // Add experiment name when request comes from field. + $rows[ $r->id ][ 'D0' ] = $project_name; + } } // Total lines; From 6fa0ccc91f484d2c0db4938577e737f1adfb161d Mon Sep 17 00:00:00 2001 From: Reynold Tan Date: Wed, 9 Mar 2022 14:41:39 -0600 Subject: [PATCH 06/16] Update includes/TripalFields/ncit__raw_data/ncit__raw_data.inc excellent documentation Co-authored-by: Lacey-Anne Sanderson --- includes/TripalFields/ncit__raw_data/ncit__raw_data.inc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index adb4fe4..94884e0 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -138,7 +138,9 @@ class ncit__raw_data extends TripalField { if ($count_permission == 2 || user_is_logged_in()) { // User appointed experiments. - // See api library in includes directory for this function definition. + // See includes/rawpheno.function.measurements.inc file for function definition + // Given the user id this function returns an array of chado projects keyed by project_id + // that the user has permission to see. $user_experiment = rawpheno_function_user_project($user->uid); $user_experiment = array_keys($user_experiment); From 5dfbe83b78ccf02acc2765fc1cf6dd11b9c4daf1 Mon Sep 17 00:00:00 2001 From: Rey Tan Date: Wed, 9 Mar 2022 15:47:08 -0600 Subject: [PATCH 07/16] @todo: replace markup with a template file --- .../ncit__raw_data/ncit__raw_data.inc | 18 +++++------- .../ncit__raw_data_formatter.inc | 29 ++++++++++++------- rawpheno.module | 18 ------------ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index 94884e0..c073f1d 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -152,14 +152,6 @@ class ncit__raw_data extends TripalField { $germplasm['id'] = $entity->chado_record->stock_id; $germplasm['name'] = $entity->chado_record->name; - // Icons. - $icon_path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno') . '/includes/TripalFields/ncit__raw_data/theme/'; - - $icons['leaf'] = $icon_path . 'icon-leaf.png'; - $icons['export'] = $icon_path . 'icon-export.png'; - $icons['raw'] = $icon_path . 'icon-raw.jpg'; - $icons['download'] = $icon_path . 'icon-download.jpg'; - $traits = []; // This query is identical to the rawphenotypes download page. // Get all experiments (by plant id) where germplasm was used. @@ -206,7 +198,12 @@ class ncit__raw_data extends TripalField { if ($trait_set) { foreach($trait_set as $trait_id => $trait_name) { // Trait id, project id and name + location: - $trait_experiment_location[ $trait_id . '_' . $trait_name ][] = $trait_id . '#' . $item->project_id . '#' . $item->name . '#' . $item->location; + $trait_experiment_location[ $trait_id . '_' . $trait_name ][] = array( + 'trait_id' => $trait_id, + 'project_id' => $item->project_id, + 'project_name' => $item->name, + 'location' => $item->location, + ); } } } @@ -218,12 +215,11 @@ class ncit__raw_data extends TripalField { $summary['experiments'] = count(array_unique($cache_exp)); $summary['locations'] = count(array_unique($cache_loc)); - $entity->{$field_name}['und'][0]['value']['NCIT:Raw Data'] = array( + $entity->{$field_name}['und'][0]['value'] = array( 'user' => $current_user, 'germplasm' => $germplasm, 'summary' => $summary, 'traits' => $trait_experiment_location, - 'icons' => $icons ); } } diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 32ca6ab..039ad15 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -44,8 +44,17 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * hook_field_formatter_view() function. */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { - if ($items[0]['value']['NCIT:Raw Data']) { - $germplasm_raw_phenotypes = $items[0]['value']['NCIT:Raw Data']; + if ($items[0]['value']) { + // Icons. + $icon_path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno') . '/includes/TripalFields/ncit__raw_data/theme/'; + + $icons = array(); + $icons['leaf'] = $icon_path . 'icon-leaf.png'; + $icons['export'] = $icon_path . 'icon-export.png'; + $icons['raw'] = $icon_path . 'icon-raw.jpg'; + $icons['download'] = $icon_path . 'icon-download.jpg'; + + $germplasm_raw_phenotypes = $items[0]['value']; // Create markup. // Refer to this ID for CSS styling. @@ -54,7 +63,7 @@ class ncit__raw_data_formatter extends TripalFieldFormatter {
@@ -93,13 +102,13 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { list($trait_id, $trait_name) = explode('_', $trait); $select = $this->create_select($germplasm_raw_phenotypes['germplasm']['id'], $exp_loc, $germplasm_raw_phenotypes['user']['experiments']); - $table_row[ $id ] = array(sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $germplasm_raw_phenotypes['icons']['export'])); + $table_row[ $id ] = array(sprintf($icon_img, '', $icons['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $icons['export'])); $id++; } // Create markup. $summary_table = theme('table', array( - 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['download'])), + 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $icons['download'])), 'rows' => $table_row, 'sticky' => FALSE, 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table') @@ -139,14 +148,14 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $cache_exp = []; foreach($items as $loc_exp) { list($trait_id, $project_id, $project_name, $location) = explode('#', $loc_exp); - $select_value = $trait_id . '#' . $project_id . '#' . $location . '#' . $germplasm; - $cache_exp[] = $project_id; + $select_value = $loc_exp['trait_id'] . '#' . $loc_exp['project_id'] . '#' . $loc_exp['location'] . '#' . $loc_exp['germplasm']; + $cache_exp[] = $loc_exp['project_id']; - $disabled = (in_array($project_id, $disable)) ? '' : 'disabled'; - $option[] = ''; + $disabled = (in_array($loc_exp['project_id'], $disable)) ? '' : 'disabled'; + $option[] = ''; } - $select = ' %s '; diff --git a/rawpheno.module b/rawpheno.module index 15e56e3..0829b20 100755 --- a/rawpheno.module +++ b/rawpheno.module @@ -1179,21 +1179,3 @@ function rawpheno_preprocess_block(&$vars) { return $vars; } - - -// RAWPHENOTYPES IN GERMPLASM FIELD. - - -/** - * Implements hook_field_group_build_pre_render_alter(). - * Apply css and js. - */ -function rawpheno_field_group_build_pre_render_alter(&$element) { - $rp_path = drupal_get_path('module', 'rawpheno') . '/theme/'; - - $element['#attached']['js'][] = $rp_path . 'js/rawpheno.germplasm.field.js'; - $element['#attached']['css'][] = $rp_path . 'css/rawpheno.germplasm.field.css'; - - // Rawphenotypes download link: - drupal_add_js(array('rawpheno' => array('exportLink' => $GLOBALS['base_url'] . '/phenotypes/raw/download')), array('type' => 'setting')); -} From 1f872cc9b3f7338fa53f7a25cac3a4c5f6a0702b Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Fri, 18 Mar 2022 15:35:40 -0600 Subject: [PATCH 08/16] Implements hook_theme --- .../ncit__raw_data/ncit__raw_data.inc | 3 + .../ncit__raw_data_formatter.inc | 92 +++--------------- rawpheno.module | 9 +- ....field.css => rawpheno.germplasmfield.css} | 1 + .../theme => theme/img/fields}/icon-raw.jpg | Bin theme/rawpheno_germplasm_field.tpl.php | 47 +++++++++ 6 files changed, 73 insertions(+), 79 deletions(-) rename theme/css/{rawpheno.germplasm.field.css => rawpheno.germplasmfield.css} (99%) rename {includes/TripalFields/ncit__raw_data/theme => theme/img/fields}/icon-raw.jpg (100%) create mode 100644 theme/rawpheno_germplasm_field.tpl.php diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index c073f1d..0fe234a 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -111,6 +111,9 @@ class ncit__raw_data extends TripalField { * */ public function load($entity) { + $path_module = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno'); + drupal_add_css($path_module . '/theme/css/abc.css', 'file'); + // Arrays to hold phenotypes related to this germplasm. $germplasm = array(); $icons = array(); diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 039ad15..2f47a02 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -45,88 +45,24 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { if ($items[0]['value']) { - // Icons. - $icon_path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno') . '/includes/TripalFields/ncit__raw_data/theme/'; + $path_module = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno'); - $icons = array(); - $icons['leaf'] = $icon_path . 'icon-leaf.png'; - $icons['export'] = $icon_path . 'icon-export.png'; - $icons['raw'] = $icon_path . 'icon-raw.jpg'; - $icons['download'] = $icon_path . 'icon-download.jpg'; + // Add style and script. - $germplasm_raw_phenotypes = $items[0]['value']; + drupal_add_css($path_module . '/theme/css/rawpheno.germplasmfield.css'); + drupal_add_js($path_module . '/theme/js/rawpheno.germplasm.field.js'); - // Create markup. - // Refer to this ID for CSS styling. - $id = 'rawphenotypes-germplasm-raw-phenotypes-field'; - $markup = ' -
- - - - -

' . $germplasm_raw_phenotypes['germplasm']['name'] . ': %d Traits / %d Experiments / %d Locations

- -
Please note that some experiments appear disabled. Please contact KnowPulse if you need access.
- -
-
-
%s
-
-
- -
*Data export will launch a new window
-
'; - $icon_img = 'Download Raw Phenotypic Data'; - - $response = ''; - - if ($germplasm_raw_phenotypes['user']['permission']) { - // Export summary table. - $table_row = array(); - - $id = 0; - foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { - list($trait_id, $trait_name) = explode('_', $trait); - - $select = $this->create_select($germplasm_raw_phenotypes['germplasm']['id'], $exp_loc, $germplasm_raw_phenotypes['user']['experiments']); - $table_row[ $id ] = array(sprintf($icon_img, '', $icons['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $icons['export'])); - $id++; - } - - // Create markup. - $summary_table = theme('table', array( - 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $icons['download'])), - 'rows' => $table_row, - 'sticky' => FALSE, - 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table') - )); - - $response = sprintf($markup, - $germplasm_raw_phenotypes['summary']['traits'], - $germplasm_raw_phenotypes['summary']['experiments'], - $germplasm_raw_phenotypes['summary']['locations'], - $summary_table - ); + // Render germplasm raw phenotypes. + $variables = array( + 'element_id' => 'rawphenotypes-germplasm-raw-phenotypes-field', + 'path_img' => $path_module . '/theme/img/fields/' + ); - // Render germplasm raw phenotypes. - $element[0] = array( - '#type' => 'markup', - '#markup' => $response, - ); - } + $markup = theme('rawpheno_germplasm_field', $variables); + $element[0] = array( + '#type' => 'markup', + '#markup' => $markup, + ); } return $element; diff --git a/rawpheno.module b/rawpheno.module index 0829b20..aaf2beb 100755 --- a/rawpheno.module +++ b/rawpheno.module @@ -487,6 +487,13 @@ function rawpheno_theme($existing, $type, $theme, $path) { ), ); + // Raw data field. + $items['rawpheno_germplasm_field'] = array( + 'template' => 'rawpheno_germplasm_field', + 'path' => $path . '/theme', + 'variables' => array('raw_data' => '') + ); + return $items; } @@ -1178,4 +1185,4 @@ function rawpheno_preprocess_block(&$vars) { } return $vars; -} +} \ No newline at end of file diff --git a/theme/css/rawpheno.germplasm.field.css b/theme/css/rawpheno.germplasmfield.css similarity index 99% rename from theme/css/rawpheno.germplasm.field.css rename to theme/css/rawpheno.germplasmfield.css index 821c9b5..009d4e2 100644 --- a/theme/css/rawpheno.germplasm.field.css +++ b/theme/css/rawpheno.germplasmfield.css @@ -17,6 +17,7 @@ #rawphenotypes-germplasm-field-header { position: relative; height: 45px; + border: 10px solid blue; } #rawphenotypes-germplasm-field-header div:first-child { diff --git a/includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg b/theme/img/fields/icon-raw.jpg similarity index 100% rename from includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg rename to theme/img/fields/icon-raw.jpg diff --git a/theme/rawpheno_germplasm_field.tpl.php b/theme/rawpheno_germplasm_field.tpl.php new file mode 100644 index 0000000..deff46e --- /dev/null +++ b/theme/rawpheno_germplasm_field.tpl.php @@ -0,0 +1,47 @@ + + +
+
+ + + +
 
+
+ + + +
+

GERMPLASM NAME: 5 Traits / 5 Experiments / 5 Locations

+
+ +
+ Please note that some experiments appear disabled. Please contact KnowPulse if you need access. +
+ +
+
+
%s
+
+
+ +
*Data export will launch a new window
+
\ No newline at end of file From 6dc7913b5b62d9c6e7254174145f0c5784cc6d51 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Fri, 18 Mar 2022 17:03:00 -0600 Subject: [PATCH 09/16] Finally, add css works --- .../ncit__raw_data_formatter.inc | 42 +++++++++++++++---- ....css => rawpheno.germplasmfield.style.css} | 1 - ...d.js => rawpheno.germplasmfield.script.js} | 0 theme/rawpheno_germplasm_field.tpl.php | 4 +- 4 files changed, 36 insertions(+), 11 deletions(-) rename theme/css/{rawpheno.germplasmfield.css => rawpheno.germplasmfield.style.css} (99%) rename theme/js/{rawpheno.germplasm.field.js => rawpheno.germplasmfield.script.js} (100%) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 2f47a02..69ef2ac 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -44,18 +44,46 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * hook_field_formatter_view() function. */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { - if ($items[0]['value']) { - $path_module = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno'); + if ($items[0]['value'] && $items[0]['value']['user']['permission']) { + $path_base = $GLOBALS['base_url'] . '/'; + $path_module = drupal_get_path('module', 'rawpheno') . '/'; // Add style and script. + drupal_add_css($path_module . 'theme/css/rawpheno.germplasmfield.style.css'); + drupal_add_js($path_base . $path_module . 'theme/js/rawpheno.germplasmfield.script.js'); - drupal_add_css($path_module . '/theme/css/rawpheno.germplasmfield.css'); - drupal_add_js($path_module . '/theme/js/rawpheno.germplasm.field.js'); + $germplasm_raw_phenotypes = $items[0]['value']; + // Export summary table. + $table_row = array(); + + $id = 0; + foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { + list($trait_id, $trait_name) = explode('_', $trait); + + $select = $this->create_select($exp_loc, $germplasm_raw_phenotypes['user']['experiments']); + $table_row[ $id ] = array(sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $germplasm_raw_phenotypes['icons']['export'])); + $id++; + } + + // Create markup. + $summary_table = theme('table', array( + 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['download'])), + 'rows' => $table_row, + 'sticky' => FALSE, + 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table') + )); + // Render germplasm raw phenotypes. $variables = array( 'element_id' => 'rawphenotypes-germplasm-raw-phenotypes-field', - 'path_img' => $path_module . '/theme/img/fields/' + 'path_img' => $path_base . $path_module . 'theme/img/fields/', + 'summary_table' => $summary_table, + 'header' => array( + 'traits' => $germplasm_raw_phenotypes['summary']['traits'], + 'experiments' => $germplasm_raw_phenotypes['summary']['experiments'], + 'locations' => $germplasm_raw_phenotypes['summary']['locations'], + ) ); $markup = theme('rawpheno_germplasm_field', $variables); @@ -71,15 +99,13 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { /** * Create select field. * - * @param $germplasm - * Stock id number. * @param $items * Associative array, where each item will be rendered as an option * with key as the value and value as text. * @param $disable * Array of items to match an item if it should be disabled. */ - public function create_select($germplasm, $items, $disable) { + public function create_select($items, $disable) { $option = array(); $cache_exp = []; foreach($items as $loc_exp) { diff --git a/theme/css/rawpheno.germplasmfield.css b/theme/css/rawpheno.germplasmfield.style.css similarity index 99% rename from theme/css/rawpheno.germplasmfield.css rename to theme/css/rawpheno.germplasmfield.style.css index 009d4e2..821c9b5 100644 --- a/theme/css/rawpheno.germplasmfield.css +++ b/theme/css/rawpheno.germplasmfield.style.css @@ -17,7 +17,6 @@ #rawphenotypes-germplasm-field-header { position: relative; height: 45px; - border: 10px solid blue; } #rawphenotypes-germplasm-field-header div:first-child { diff --git a/theme/js/rawpheno.germplasm.field.js b/theme/js/rawpheno.germplasmfield.script.js similarity index 100% rename from theme/js/rawpheno.germplasm.field.js rename to theme/js/rawpheno.germplasmfield.script.js diff --git a/theme/rawpheno_germplasm_field.tpl.php b/theme/rawpheno_germplasm_field.tpl.php index deff46e..20a3d8b 100644 --- a/theme/rawpheno_germplasm_field.tpl.php +++ b/theme/rawpheno_germplasm_field.tpl.php @@ -30,7 +30,7 @@
-

GERMPLASM NAME: 5 Traits / 5 Experiments / 5 Locations

+

GERMPLASM NAME: Traits / Experiments / Locations

@@ -39,7 +39,7 @@
-
%s
+
From b6405487bf5aa38b4a43df2689f46cf7751b4ef8 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Mon, 21 Mar 2022 14:33:05 -0600 Subject: [PATCH 10/16] Revises rawphenotypes germplasm field formatter to use template file --- .../ncit__raw_data/ncit__raw_data.inc | 39 +++---- .../ncit__raw_data_formatter.inc | 96 +++++++++++------- .../ncit__raw_data/theme/icon-raw.jpg | Bin 0 -> 14860 bytes theme/css/rawpheno.germplasmfield.style.css | 2 +- theme/js/rawpheno.germplasmfield.script.js | 2 +- theme/rawpheno_germplasm_field.tpl.php | 44 ++++++-- 6 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index 0fe234a..177d7c0 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -107,24 +107,10 @@ class ncit__raw_data extends TripalField { * The array may contain as many other keys at the same level as 'value' * but those keys are for internal field use and are not considered the * value of the field. - * - * */ public function load($entity) { - $path_module = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'rawpheno'); - drupal_add_css($path_module . '/theme/css/abc.css', 'file'); - - // Arrays to hold phenotypes related to this germplasm. - $germplasm = array(); - $icons = array(); - $summary = array(); - $current_user = array(); - $traits = array(); - - // User profile. global $user; - $current_user['id'] = $user->uid; - + // User permissions. $rawpheno_permission = array('access rawpheno', 'download rawpheno'); $count_permission = 0; @@ -146,16 +132,20 @@ class ncit__raw_data extends TripalField { // that the user has permission to see. $user_experiment = rawpheno_function_user_project($user->uid); $user_experiment = array_keys($user_experiment); - - // Current user. + + // # USER PROFILE: + // user id, has permission to rawphenotypes and list of experiments user is aactive. + $current_user = array(); $current_user['permission'] = TRUE; $current_user['experiments'] = $user_experiment; - // Germplasm. + // # GERMPLASM: + $germplasm = array(); $germplasm['id'] = $entity->chado_record->stock_id; $germplasm['name'] = $entity->chado_record->name; - $traits = []; + // # TRAITS/EXPERIMENT/LOCATION: + $traits = array(); // This query is identical to the rawphenotypes download page. // Get all experiments (by plant id) where germplasm was used. $all_experiment_locations = chado_query(" @@ -201,7 +191,7 @@ class ncit__raw_data extends TripalField { if ($trait_set) { foreach($trait_set as $trait_id => $trait_name) { // Trait id, project id and name + location: - $trait_experiment_location[ $trait_id . '_' . $trait_name ][] = array( + $trait_experiment_location[ $trait_name . '_' . $trait_id ][] = array( 'trait_id' => $trait_id, 'project_id' => $item->project_id, 'project_name' => $item->name, @@ -213,16 +203,17 @@ class ncit__raw_data extends TripalField { ksort($trait_experiment_location); - // Summarize. + // # SUMMARY COUNT: + $summary = array(); $summary['traits'] = count($trait_experiment_location); $summary['experiments'] = count(array_unique($cache_exp)); $summary['locations'] = count(array_unique($cache_loc)); $entity->{$field_name}['und'][0]['value'] = array( - 'user' => $current_user, + 'user' => $current_user, 'germplasm' => $germplasm, - 'summary' => $summary, - 'traits' => $trait_experiment_location, + 'summary' => $summary, + 'traits' => $trait_experiment_location, ); } } diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 69ef2ac..c46851e 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -44,52 +44,70 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * hook_field_formatter_view() function. */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { - if ($items[0]['value'] && $items[0]['value']['user']['permission']) { - $path_base = $GLOBALS['base_url'] . '/'; - $path_module = drupal_get_path('module', 'rawpheno') . '/'; - - // Add style and script. - drupal_add_css($path_module . 'theme/css/rawpheno.germplasmfield.style.css'); - drupal_add_js($path_base . $path_module . 'theme/js/rawpheno.germplasmfield.script.js'); + // If germplasm has phenotypes and user has permission to access raw phenotypes. + if ($items[0]['value'] && $items[0]['value']['user']['permission']) { + // All field values are accessible through this var. $germplasm_raw_phenotypes = $items[0]['value']; - // Export summary table. + // Reference directory path. + $base_path = $GLOBALS['base_url'] . '/'; + $module_path = drupal_get_path('module', 'rawpheno') . '/'; + $theme_path = $base_path . $module_path . '/includes/TripalFields/ncit__raw_data/theme/'; + + // Append image as bullet points, header icon and export button or link. + $img = 'Download Raw Phenotypic Data'; + + // CONSTRUCT SUMMARY TABLE: + // # TABLE HEADER: + $table_header = array('Trait', 'LOCATION/Experiment', sprintf($img, '', $theme_path . 'icon-download.jpg')); + + // # TABLE ROWS: + $id = 0; $table_row = array(); - - $id = 0; + foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { - list($trait_id, $trait_name) = explode('_', $trait); + list($trait_name, $trait_id) = explode('_', $trait); - $select = $this->create_select($exp_loc, $germplasm_raw_phenotypes['user']['experiments']); - $table_row[ $id ] = array(sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['leaf']) . ucfirst($trait_name), $select, sprintf($icon_img, $trait_id, $germplasm_raw_phenotypes['icons']['export'])); + $select = $this->create_select($germplasm_raw_phenotypes['germplasm']['id'], $exp_loc, $germplasm_raw_phenotypes['user']['experiments']); + $table_row[ $id ] = array(sprintf($img, '', $theme_path . 'icon-leaf.png') . ucfirst($trait_name), $select, sprintf($img, $trait_id, $theme_path . 'icon-export.png')); $id++; } - - // Create markup. - $summary_table = theme('table', array( - 'header' => array('Trait', 'LOCATION/Experiment', sprintf($icon_img, '', $germplasm_raw_phenotypes['icons']['download'])), - 'rows' => $table_row, - 'sticky' => FALSE, - 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table') - )); - // Render germplasm raw phenotypes. - $variables = array( - 'element_id' => 'rawphenotypes-germplasm-raw-phenotypes-field', - 'path_img' => $path_base . $path_module . 'theme/img/fields/', - 'summary_table' => $summary_table, - 'header' => array( - 'traits' => $germplasm_raw_phenotypes['summary']['traits'], - 'experiments' => $germplasm_raw_phenotypes['summary']['experiments'], - 'locations' => $germplasm_raw_phenotypes['summary']['locations'], - ) + // # THEME TABLE: + $summary_table = theme('table', array( + 'header' => $table_header, + 'rows' => $table_row, + 'sticky' => FALSE, + 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table')) ); + + // Make field elements generated by formatter avaiable to the template as template vars. + // @see template file for this field in rawphenotypes/theme directory. + $markup = theme('rawpheno_germplasm_field', array( + 'element_id' => 'rawphenotypes-germplasm-raw-phenotypes-field', + 'summary_table' => array( + 'table' => $summary_table, + 'headers' => array( + 'experiments' => $germplasm_raw_phenotypes['summary']['experiments'], + 'locations' => $germplasm_raw_phenotypes['summary']['locations'], + 'germplasm' => $germplasm_raw_phenotypes['germplasm']['name'], + 'traits' => $germplasm_raw_phenotypes['summary']['traits'], + ) + ) + )); + + // Rawphenotypes download link: + drupal_add_js(array('rawpheno' => array('exportLink' => $base_path . '/phenotypes/raw/download')), array('type' => 'setting')); - $markup = theme('rawpheno_germplasm_field', $variables); + // Construct field render array. $element[0] = array( '#type' => 'markup', '#markup' => $markup, + '#attached' => array( + 'css' => array($module_path . 'theme/css/rawpheno.germplasmfield.style.css'), + 'js' => array($module_path . 'theme/js/rawpheno.germplasmfield.script.js') + ) ); } @@ -99,21 +117,23 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { /** * Create select field. * + * @param $germplasm + * Stock id number. * @param $items * Associative array, where each item will be rendered as an option * with key as the value and value as text. - * @param $disable - * Array of items to match an item if it should be disabled. + * @param $user_experiments + * Array of experiments user has access to. This will be used to cross check + * if experiment should be enabled or disabled to a user */ - public function create_select($items, $disable) { + public function create_select($germplasm, $items, $user_experiments) { $option = array(); $cache_exp = []; foreach($items as $loc_exp) { - list($trait_id, $project_id, $project_name, $location) = explode('#', $loc_exp); - $select_value = $loc_exp['trait_id'] . '#' . $loc_exp['project_id'] . '#' . $loc_exp['location'] . '#' . $loc_exp['germplasm']; + $select_value = $loc_exp['trait_id'] . '#' . $loc_exp['project_id'] . '#' . $loc_exp['location'] . '#' . $germplasm; $cache_exp[] = $loc_exp['project_id']; - $disabled = (in_array($loc_exp['project_id'], $disable)) ? '' : 'disabled'; + $disabled = (in_array($loc_exp['project_id'], $user_experiments)) ? '' : 'disabled'; $option[] = ''; } diff --git a/includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg b/includes/TripalFields/ncit__raw_data/theme/icon-raw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66b3cec5899dc9dd30142e3dfd8ce7ffbdb291e2 GIT binary patch literal 14860 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 literal 0 HcmV?d00001 diff --git a/theme/css/rawpheno.germplasmfield.style.css b/theme/css/rawpheno.germplasmfield.style.css index 821c9b5..43e89e3 100644 --- a/theme/css/rawpheno.germplasmfield.style.css +++ b/theme/css/rawpheno.germplasmfield.style.css @@ -32,7 +32,7 @@ #rawphenotypes-germplasm-field-header div:nth-child(2) div { background-color: #314355; height: 40px; - width: 190px; + width: 270px; text-align:center; line-height: 40px; } diff --git a/theme/js/rawpheno.germplasmfield.script.js b/theme/js/rawpheno.germplasmfield.script.js index dedcc47..aa25f4c 100644 --- a/theme/js/rawpheno.germplasmfield.script.js +++ b/theme/js/rawpheno.germplasmfield.script.js @@ -54,7 +54,7 @@ $('#' + selectId + '-img').css('opacity', imgOpacity); }); - + // Listen to images clicked to launch data download. $('#rawphenotypes-germplasm-field-table td:last-child img').click(function(e) { var imgId = e.target.id; diff --git a/theme/rawpheno_germplasm_field.tpl.php b/theme/rawpheno_germplasm_field.tpl.php index 20a3d8b..9a546f7 100644 --- a/theme/rawpheno_germplasm_field.tpl.php +++ b/theme/rawpheno_germplasm_field.tpl.php @@ -1,36 +1,54 @@
-

GERMPLASM NAME: Traits / Experiments / Locations

+

+ : + + Traits / + Experiments / + Locations + +

@@ -39,9 +57,13 @@
-
+
-
*Data export will launch a new window
+
+ *Data export will launch a new window +
+ +
 
\ No newline at end of file From 5699d362febc4cff48c85f30870e76c6bde6dd88 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Fri, 15 Apr 2022 16:34:38 -0600 Subject: [PATCH 11/16] Revises field array keys to use cvterms @todo: test --- .../ncit__raw_data/ncit__raw_data.inc | 51 ++++++++----------- .../ncit__raw_data_formatter.inc | 45 ++++++++-------- includes/TripalFields/rawpheno.fields.inc | 19 ++++++- 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index 177d7c0..8c1a813 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -132,17 +132,6 @@ class ncit__raw_data extends TripalField { // that the user has permission to see. $user_experiment = rawpheno_function_user_project($user->uid); $user_experiment = array_keys($user_experiment); - - // # USER PROFILE: - // user id, has permission to rawphenotypes and list of experiments user is aactive. - $current_user = array(); - $current_user['permission'] = TRUE; - $current_user['experiments'] = $user_experiment; - - // # GERMPLASM: - $germplasm = array(); - $germplasm['id'] = $entity->chado_record->stock_id; - $germplasm['name'] = $entity->chado_record->name; // # TRAITS/EXPERIMENT/LOCATION: $traits = array(); @@ -181,6 +170,7 @@ class ncit__raw_data extends TripalField { $trait_experiment_location = array(); $cache_exp = array(); $cache_loc = array(); + foreach($experiment_locations as $item) { $cache_exp[] = $item->project_id; $cache_loc[] = $item->location; @@ -190,30 +180,29 @@ class ncit__raw_data extends TripalField { if ($trait_set) { foreach($trait_set as $trait_id => $trait_name) { - // Trait id, project id and name + location: - $trait_experiment_location[ $trait_name . '_' . $trait_id ][] = array( - 'trait_id' => $trait_id, - 'project_id' => $item->project_id, - 'project_name' => $item->name, - 'location' => $item->location, - ); + // Trait id, project id and project name + location: + $entity->{$field_name}['und'][0]['value']['hydra:member'][] = array( + 'phenotype_rawdatafield_terms:id' => $trait_id, + 'phenotype_rawdatafield_terms:name' => $trait_name, + 'phenotype_rawdatafield_terms:experiment' => array( + 'phenotype_rawdatafield_terms:id' => $item->project_id, + 'phenotype_rawdatafield_terms:name' => $item->name, + 'phenotype_rawdatafield_terms:location' => $item->location + ) + ); } } } - - ksort($trait_experiment_location); - - // # SUMMARY COUNT: - $summary = array(); - $summary['traits'] = count($trait_experiment_location); - $summary['experiments'] = count(array_unique($cache_exp)); - $summary['locations'] = count(array_unique($cache_loc)); - + $entity->{$field_name}['und'][0]['value'] = array( - 'user' => $current_user, - 'germplasm' => $germplasm, - 'summary' => $summary, - 'traits' => $trait_experiment_location, + 'phenotype_rawdatafield_terms:permission' => TRUE, + 'phenotype_rawdatafield_terms:experiment' => $user_experiment, + + 'phenotype_rawdatafield_terms:summary' => array( + 'phenotype_rawdatafield_terms:trait' => count($entity->{$field_name}['und'][0]['value']['hydra:member']), + 'phenotype_rawdatafield_terms:location' => count(array_unique($cache_loc)), + 'phenotype_rawdatafield_terms:experiment' => count(array_unique($cache_exp)), + } ); } } diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index c46851e..996b593 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -45,10 +45,12 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { // If germplasm has phenotypes and user has permission to access raw phenotypes. - - if ($items[0]['value'] && $items[0]['value']['user']['permission']) { - // All field values are accessible through this var. - $germplasm_raw_phenotypes = $items[0]['value']; + if ($items[0]['value'] && $items[0]['value']['phenotype_rawdatafield_terms:permission']) { + $user_experiment = $items[0]['value']['phenotype_rawdatafield_terms:experiment']; + $summary_values = $items[0]['value']['phenotype_rawdatafield_terms:summary']; + + // All trait and experiment+location values are accessible through this var. + $germplasm_raw_phenotypes = $items[0]['value']['hydra:member']; // Reference directory path. $base_path = $GLOBALS['base_url'] . '/'; @@ -63,15 +65,13 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $table_header = array('Trait', 'LOCATION/Experiment', sprintf($img, '', $theme_path . 'icon-download.jpg')); // # TABLE ROWS: - $id = 0; - $table_row = array(); - - foreach($germplasm_raw_phenotypes['traits'] as $trait => $exp_loc) { - list($trait_name, $trait_id) = explode('_', $trait); + $table_row = array(); + foreach($germplasm_raw_phenotypes as $id => $exp_loc) { + $trait_name = $exp_loc['phenotype_rawdatafield_terms:id']; + $trait_id = $exp_loc['phenotype_rawdatafield_terms:name']; - $select = $this->create_select($germplasm_raw_phenotypes['germplasm']['id'], $exp_loc, $germplasm_raw_phenotypes['user']['experiments']); + $select = $this->create_select($entity->chado_record->stock_id, $exp_loc, $user_experiment); $table_row[ $id ] = array(sprintf($img, '', $theme_path . 'icon-leaf.png') . ucfirst($trait_name), $select, sprintf($img, $trait_id, $theme_path . 'icon-export.png')); - $id++; } // # THEME TABLE: @@ -89,10 +89,10 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { 'summary_table' => array( 'table' => $summary_table, 'headers' => array( - 'experiments' => $germplasm_raw_phenotypes['summary']['experiments'], - 'locations' => $germplasm_raw_phenotypes['summary']['locations'], - 'germplasm' => $germplasm_raw_phenotypes['germplasm']['name'], - 'traits' => $germplasm_raw_phenotypes['summary']['traits'], + 'experiments' => $summary_values['phenotype_rawdatafield_terms:experiment'], + 'locations' => $summary_values['phenotype_rawdatafield_terms:location'], + 'germplasm' => $entity->chado_record->name, + 'traits' => $summary_values['phenotype_rawdatafield_terms:trait'], ) ) )); @@ -128,16 +128,19 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { */ public function create_select($germplasm, $items, $user_experiments) { $option = array(); - $cache_exp = []; + $cache_exp = array(); + foreach($items as $loc_exp) { - $select_value = $loc_exp['trait_id'] . '#' . $loc_exp['project_id'] . '#' . $loc_exp['location'] . '#' . $germplasm; - $cache_exp[] = $loc_exp['project_id']; + $trait_experiment = $loc_exp['phenotype_rawdatafield_terms:experiment']; + + $select_value = $loc_exp['phenotype_rawdatafield_terms:id'] . '#' . $trait_experiment['phenotype_rawdatafield_terms:id'] . '#' . $trait_experiment['phenotype_rawdatafield_terms:location'] . '#' . $germplasm; + $cache_exp[] = $trait_experiment['phenotype_rawdatafield_terms:id']; - $disabled = (in_array($loc_exp['project_id'], $user_experiments)) ? '' : 'disabled'; - $option[] = ''; + $disabled = (in_array($trait_experiment['phenotype_rawdatafield_terms:id'], $user_experiments)) ? '' : 'disabled'; + $option[] = ''; } - $select = ' %s '; diff --git a/includes/TripalFields/rawpheno.fields.inc b/includes/TripalFields/rawpheno.fields.inc index b5eeef4..899b065 100644 --- a/includes/TripalFields/rawpheno.fields.inc +++ b/includes/TripalFields/rawpheno.fields.inc @@ -36,13 +36,30 @@ function rawpheno_bundle_fields_info($entity_type, $bundle) { // IN GERMPLASM PAGE ONLY: if (isset($bundle->data_table) AND ($bundle->data_table == 'stock')) { - // Number of traits. + // Insert auxiliary terms used by Raw Data field. + // Create cv to hold terms. + $cv = 'phenotype_rawdatafield_terms'; + chado_insert_cv($cv, 'vocabulary term to hold terms used by Raw Data field'); + + // Insert terms to cv above. + $terms = array('experiment', 'id', 'location', 'name', 'permission', 'summary', 'trait'); + foreach($terms as $term) { + tripal_insert_cvterm(array( + 'id' => 'rawpheno_tripal:' . $term, + 'name' => $term, + 'definition' => $term, + 'cv_name' => $cv + )); + } + + // Raw Data term. tripal_insert_cvterm(array( 'id' => 'NCIT:C142663', 'name' => 'Raw Data', 'cv_name' => 'NCIT', 'definition' => 'The original information, collected from the primary source. Used in Germplasm Raw Phenotypes Field.', )); + $field_name = 'ncit__raw_data'; $field_type = 'ncit__raw_data'; $fields[$field_name] = array( From c666964cb6319caa0e3cc9ff2e40c0b1525f7370 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Mon, 18 Apr 2022 15:04:18 -0600 Subject: [PATCH 12/16] Revises field instance and formatter to use cvterms as array keys --- .../ncit__raw_data/ncit__raw_data.inc | 113 +++++++++--------- .../ncit__raw_data_formatter.inc | 84 ++++++++----- includes/TripalFields/rawpheno.fields.inc | 4 +- 3 files changed, 109 insertions(+), 92 deletions(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index 8c1a813..93cbd96 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -125,14 +125,7 @@ class ncit__raw_data extends TripalField { $field_name = $this->instance['field_name']; $entity->{$field_name}['und'][0]['value'] = array(); - if ($count_permission == 2 || user_is_logged_in()) { - // User appointed experiments. - // See includes/rawpheno.function.measurements.inc file for function definition - // Given the user id this function returns an array of chado projects keyed by project_id - // that the user has permission to see. - $user_experiment = rawpheno_function_user_project($user->uid); - $user_experiment = array_keys($user_experiment); - + if (user_is_logged_in() && $count_permission == 2) { // # TRAITS/EXPERIMENT/LOCATION: $traits = array(); // This query is identical to the rawphenotypes download page. @@ -147,63 +140,67 @@ class ncit__raw_data extends TripalField { WHERE cvt.name = 'Location' AND cv.name = 'phenotype_plant_property_types') AND plant_id IN (SELECT plant_id FROM pheno_plant WHERE stock_id = :germplasm GROUP BY plant_id) GROUP BY p2.project_id, p2.name, value - ", array(':germplasm' => $germplasm['id'])); + ", array(':germplasm' => $entity->chado_record->stock_id)); $experiment_locations = $all_experiment_locations->fetchAll(); - - // All traits in experiment and location. - $sql_cvterm = " - SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( - SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( - SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( - SELECT string_agg(DISTINCT all_traits, ',') AS all_traits - FROM {rawpheno_rawdata_mview} - WHERE - location IN(:location) - AND plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) - ) AS list_id - )::int[]) - ) AS c_j - WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') - ORDER BY c_j.cvterm_json->>'name' ASC - "; - $trait_experiment_location = array(); - $cache_exp = array(); - $cache_loc = array(); - - foreach($experiment_locations as $item) { - $cache_exp[] = $item->project_id; - $cache_loc[] = $item->location; + // Only when germplasm returned rawphenotypic data. + if (count($experiment_locations) > 0) { + // User appointed experiments. + // See includes/rawpheno.function.measurements.inc file for function definition + // Given the user id this function returns an array of chado projects keyed by project_id + // that the user has permission to see. + $user_experiment = rawpheno_function_user_project($user->uid); + $user_experiment = array_keys($user_experiment); + // Save user experiments. + $entity->{$field_name}['und'][0]['value']['phenotype_customfield_terms:user_experiment'] = $user_experiment; - $trait_set = chado_query($sql_cvterm, array(':location' => $item->location, ':project_id' => $item->project_id)) - ->fetchAllKeyed(0, 1); + // All traits in experiment and location. + $sql_cvterm = " + SELECT c_j.cvterm_json->>'id', c_j.cvterm_json->>'name' FROM ( + SELECT JSON_BUILD_OBJECT('id', cvterm_id, 'name', name) AS cvterm_json FROM {cvterm} WHERE cvterm_id = ANY (( + SELECT STRING_TO_ARRAY(list_id.all_traits, ',') FROM ( + SELECT string_agg(DISTINCT all_traits, ',') AS all_traits + FROM {rawpheno_rawdata_mview} + WHERE + location IN(:location) + AND plant_id IN (SELECT plant_id FROM pheno_plant_project WHERE project_id = :project_id) + ) AS list_id + )::int[]) + ) AS c_j + WHERE c_j.cvterm_json->>'name' NOT IN ('Rep', 'Entry', 'Location', 'Name', 'Plot', 'Planting Date (date)', '# of Seeds Planted (count)') + ORDER BY c_j.cvterm_json->>'name' ASC + "; - if ($trait_set) { - foreach($trait_set as $trait_id => $trait_name) { - // Trait id, project id and project name + location: - $entity->{$field_name}['und'][0]['value']['hydra:member'][] = array( - 'phenotype_rawdatafield_terms:id' => $trait_id, - 'phenotype_rawdatafield_terms:name' => $trait_name, - 'phenotype_rawdatafield_terms:experiment' => array( - 'phenotype_rawdatafield_terms:id' => $item->project_id, - 'phenotype_rawdatafield_terms:name' => $item->name, - 'phenotype_rawdatafield_terms:location' => $item->location - ) - ); + $trait_experiment_location = array(); + $cache_exp = array(); + $cache_loc = array(); + + foreach($experiment_locations as $item) { + $cache_exp[] = $item->project_id; + $cache_loc[] = $item->location; + + $trait_set = chado_query($sql_cvterm, array(':location' => $item->location, ':project_id' => $item->project_id)) + ->fetchAllKeyed(0, 1); + + if ($trait_set) { + foreach($trait_set as $trait_id => $trait_name) { + // Save basic information about the trait (name + id key) and all experiment + location it was measured. + $entity->{$field_name}['und'][0]['value']['hydra:member'][ $trait_name . '_' . $trait_id ][] = array( + 'phenotype_customfield_terms:id' => $item->project_id, // Project id number. + 'phenotype_customfield_terms:name' => $item->name, // Project name. + 'phenotype_customfield_terms:location' => $item->location // Location in a project trait was measured. + ); + } } } - } - - $entity->{$field_name}['und'][0]['value'] = array( - 'phenotype_rawdatafield_terms:permission' => TRUE, - 'phenotype_rawdatafield_terms:experiment' => $user_experiment, - 'phenotype_rawdatafield_terms:summary' => array( - 'phenotype_rawdatafield_terms:trait' => count($entity->{$field_name}['und'][0]['value']['hydra:member']), - 'phenotype_rawdatafield_terms:location' => count(array_unique($cache_loc)), - 'phenotype_rawdatafield_terms:experiment' => count(array_unique($cache_exp)), - } - ); + // Save a complete summary count as a quick raw phenotypic data summary related to the germplasm. + $entity->{$field_name}['und'][0]['value']['phenotype_customfield_terms:summary'] = array( + 'phenotype_customfield_terms:experiment' => count(array_unique($cache_exp)), // Summary count of experiments. + 'phenotype_customfield_terms:location' => count(array_unique($cache_loc)), // Summary count of locations. + 'phenotype_customfield_terms:trait' => count($entity->{$field_name}['und'][0]['value']['hydra:member']), // Summary count of traits. + ); + } } } diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 996b593..1a61a3d 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -44,34 +44,48 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * hook_field_formatter_view() function. */ public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { - // If germplasm has phenotypes and user has permission to access raw phenotypes. - if ($items[0]['value'] && $items[0]['value']['phenotype_rawdatafield_terms:permission']) { - $user_experiment = $items[0]['value']['phenotype_rawdatafield_terms:experiment']; - $summary_values = $items[0]['value']['phenotype_rawdatafield_terms:summary']; - + // Only when germplasm has raw phenotypic data under experiment/s that user + // has permission to access or export data. This value is an empty array when + // user has neither permission nor active experiments. + if ($items[0]['value']) { // All trait and experiment+location values are accessible through this var. $germplasm_raw_phenotypes = $items[0]['value']['hydra:member']; + // Experiment array user has permission to. + $user_experiment = $items[0]['value']['phenotype_customfield_terms:user_experiment']; + // Overall summary count of raw phenotypic data by experiment, location and trait. + $summary_values = $items[0]['value']['phenotype_customfield_terms:summary']; + // Id (stock id) of the current germplasm. + $germplasm_id = $entity->chado_record->stock_id; + // Name (stock name) of the current germplasm. + $germplasm_name = $entity->chado_record->name; // Reference directory path. - $base_path = $GLOBALS['base_url'] . '/'; + $base_path = $GLOBALS['base_url'] . '/'; $module_path = drupal_get_path('module', 'rawpheno') . '/'; - $theme_path = $base_path . $module_path . '/includes/TripalFields/ncit__raw_data/theme/'; + $theme_path = $base_path . $module_path . '/includes/TripalFields/ncit__raw_data/theme/'; // Append image as bullet points, header icon and export button or link. $img = 'Download Raw Phenotypic Data'; + // CONSTRUCT SUMMARY TABLE: + // Each row will contain the trait name (trait header), location and experiment combination (LOCATION/Experiment header) + // as a select box and download link (download icon header). + // # TABLE HEADER: $table_header = array('Trait', 'LOCATION/Experiment', sprintf($img, '', $theme_path . 'icon-download.jpg')); // # TABLE ROWS: $table_row = array(); - foreach($germplasm_raw_phenotypes as $id => $exp_loc) { - $trait_name = $exp_loc['phenotype_rawdatafield_terms:id']; - $trait_id = $exp_loc['phenotype_rawdatafield_terms:name']; + foreach($germplasm_raw_phenotypes as $trait => $exp_loc) { + $tmp = explode('_', $trait); + $trait = array('id' => $tmp[1], 'name' => $tmp[0]); - $select = $this->create_select($entity->chado_record->stock_id, $exp_loc, $user_experiment); - $table_row[ $id ] = array(sprintf($img, '', $theme_path . 'icon-leaf.png') . ucfirst($trait_name), $select, sprintf($img, $trait_id, $theme_path . 'icon-export.png')); + $table_row[] = array( + sprintf($img, '', $theme_path . 'icon-leaf.png') . ucfirst($trait['name']), + $this->create_select($germplasm_id, $trait, $exp_loc, $user_experiment), + sprintf($img, $trait['id'], $theme_path . 'icon-export.png') + ); } // # THEME TABLE: @@ -82,6 +96,7 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { 'attributes' => array('id' => 'rawphenotypes-germplasm-field-table')) ); + // Make field elements generated by formatter avaiable to the template as template vars. // @see template file for this field in rawphenotypes/theme directory. $markup = theme('rawpheno_germplasm_field', array( @@ -89,10 +104,10 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { 'summary_table' => array( 'table' => $summary_table, 'headers' => array( - 'experiments' => $summary_values['phenotype_rawdatafield_terms:experiment'], - 'locations' => $summary_values['phenotype_rawdatafield_terms:location'], - 'germplasm' => $entity->chado_record->name, - 'traits' => $summary_values['phenotype_rawdatafield_terms:trait'], + 'germplasm' => $germplasm_name, + 'experiments' => $summary_values['phenotype_customfield_terms:experiment'], + 'locations' => $summary_values['phenotype_customfield_terms:location'], + 'traits' => $summary_values['phenotype_customfield_terms:trait'], ) ) )); @@ -119,29 +134,34 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * * @param $germplasm * Stock id number. - * @param $items - * Associative array, where each item will be rendered as an option - * with key as the value and value as text. - * @param $user_experiments + * @param $trait + * Associative array, with keys id and name that represent + * the cvterm id and cvterm name of a trait respectively. + * @param $experiment + * An array containing all experiment the trait+germplasm + * was measured with location information besides the experiment. + * @param $user_experiment * Array of experiments user has access to. This will be used to cross check - * if experiment should be enabled or disabled to a user + * if experiment should be enabled or disabled to a user. */ - public function create_select($germplasm, $items, $user_experiments) { + public function create_select($germplasm, $trait, $experiment, $user_experiment) { $option = array(); - $cache_exp = array(); - - foreach($items as $loc_exp) { - $trait_experiment = $loc_exp['phenotype_rawdatafield_terms:experiment']; + $cache_exp = []; + + foreach($experiment as $exp_loc) { + $experiment_name = $exp_loc['phenotype_customfield_terms:name']; + $experiment_id = $exp_loc['phenotype_customfield_terms:id']; + $experiment_loc = $exp_loc['phenotype_customfield_terms:location']; - $select_value = $loc_exp['phenotype_rawdatafield_terms:id'] . '#' . $trait_experiment['phenotype_rawdatafield_terms:id'] . '#' . $trait_experiment['phenotype_rawdatafield_terms:location'] . '#' . $germplasm; - $cache_exp[] = $trait_experiment['phenotype_rawdatafield_terms:id']; + $select_value = $trait['id'] . '#' . $experiment_id . '#' . $experiment_loc . '#' . $germplasm; + $cache_exp[] = $experiment_id; - $disabled = (in_array($trait_experiment['phenotype_rawdatafield_terms:id'], $user_experiments)) ? '' : 'disabled'; - $option[] = ''; + $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; + $option[] = ''; } - $select = ' + %s '; diff --git a/includes/TripalFields/rawpheno.fields.inc b/includes/TripalFields/rawpheno.fields.inc index 899b065..3f8adfe 100644 --- a/includes/TripalFields/rawpheno.fields.inc +++ b/includes/TripalFields/rawpheno.fields.inc @@ -38,11 +38,11 @@ function rawpheno_bundle_fields_info($entity_type, $bundle) { if (isset($bundle->data_table) AND ($bundle->data_table == 'stock')) { // Insert auxiliary terms used by Raw Data field. // Create cv to hold terms. - $cv = 'phenotype_rawdatafield_terms'; + $cv = 'phenotype_customfield_terms'; chado_insert_cv($cv, 'vocabulary term to hold terms used by Raw Data field'); // Insert terms to cv above. - $terms = array('experiment', 'id', 'location', 'name', 'permission', 'summary', 'trait'); + $terms = array('experiment', 'id', 'location', 'name', 'summary', 'trait', 'user_experiment'); foreach($terms as $term) { tripal_insert_cvterm(array( 'id' => 'rawpheno_tripal:' . $term, From 3af2452ae5f5f6c57aec76d3a707fa670cb2d765 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Fri, 22 Apr 2022 17:04:55 -0600 Subject: [PATCH 13/16] Revises raw data field - allow more options to export data --- .../ncit__raw_data_formatter.inc | 96 +++++++++++++++--- .../theme/icon-download-all.jpg | Bin 0 -> 13703 bytes .../ncit__raw_data/theme/icon-export2.png | Bin 47587 -> 0 bytes .../ncit__raw_data/theme/search.png | Bin 0 -> 713 bytes theme/css/rawpheno.germplasmfield.style.css | 75 +++++++++++++- theme/js/rawpheno.germplasmfield.script.js | 83 +++++++++++++++ theme/rawpheno_germplasm_field.tpl.php | 10 ++ 7 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-download-all.jpg delete mode 100644 includes/TripalFields/ncit__raw_data/theme/icon-export2.png create mode 100644 includes/TripalFields/ncit__raw_data/theme/search.png diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index 1a61a3d..d96da27 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -65,7 +65,7 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $theme_path = $base_path . $module_path . '/includes/TripalFields/ncit__raw_data/theme/'; // Append image as bullet points, header icon and export button or link. - $img = 'Download Raw Phenotypic Data'; + $img = ''; // CONSTRUCT SUMMARY TABLE: @@ -73,7 +73,12 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { // as a select box and download link (download icon header). // # TABLE HEADER: - $table_header = array('Trait', 'LOCATION/Experiment', sprintf($img, '', $theme_path . 'icon-download.jpg')); + $table_header = array( + sprintf($img, '', $theme_path . 'icon-download-all.jpg', 'Download all for this trait'), + 'Trait', + 'LOCATION + Experiment', + sprintf($img, '', $theme_path . 'icon-download.jpg', 'Download Location + Experiment') + ); // # TABLE ROWS: $table_row = array(); @@ -82,9 +87,10 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $trait = array('id' => $tmp[1], 'name' => $tmp[0]); $table_row[] = array( - sprintf($img, '', $theme_path . 'icon-leaf.png') . ucfirst($trait['name']), - $this->create_select($germplasm_id, $trait, $exp_loc, $user_experiment), - sprintf($img, $trait['id'], $theme_path . 'icon-export.png') + sprintf($img, $trait['id'] . '-all', $theme_path . 'icon-export.png', 'Download all for this trait'), + ucfirst($trait['name']), + $this->create_select($germplasm_id, $trait, $exp_loc, $user_experiment), + sprintf($img, $trait['id'], $theme_path . 'icon-export.png', 'Download Location + Experiment') ); } @@ -114,6 +120,8 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { // Rawphenotypes download link: drupal_add_js(array('rawpheno' => array('exportLink' => $base_path . '/phenotypes/raw/download')), array('type' => 'setting')); + // Autocomplete UI. + drupal_add_library('system', 'ui.autocomplete'); // Construct field render array. $element[0] = array( @@ -145,27 +153,85 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { * if experiment should be enabled or disabled to a user. */ public function create_select($germplasm, $trait, $experiment, $user_experiment) { - $option = array(); - $cache_exp = []; - + // Array to hold select option for select field by location + experiment. + $option_loc_exp = array(); + // Array to hold select option for select field by experiment. + $option_exp = array(); + // Array to hold sorted values for select field by experiment. + $experiments = array(); + // Store all experiments. + $cache_exp = array(); + + // Create select by location + experiment at the same time + // prepare values for the other select (by experiment). foreach($experiment as $exp_loc) { $experiment_name = $exp_loc['phenotype_customfield_terms:name']; $experiment_id = $exp_loc['phenotype_customfield_terms:id']; $experiment_loc = $exp_loc['phenotype_customfield_terms:location']; + + $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; + + // Select by Experiment. + if (!in_array($experiment_id, $cache_exp)) { + $experiments[ $experiment_id ] = array( + 'disabled' => $disabled, + 'text' => $experiment_name, + 'value' => $trait['id'] . '#' . $experiment_id . '#' . $germplasm . '#' + ); + } - $select_value = $trait['id'] . '#' . $experiment_id . '#' . $experiment_loc . '#' . $germplasm; + $experiments[ $experiment_id ]['locations'][] = $experiment_loc; $cache_exp[] = $experiment_id; - - $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; - $option[] = ''; + + // Select by Location + Experiment. + $select_value = $trait['id'] . '#' . $experiment_id . '#' . $germplasm . '#' . $experiment_loc; + $option_loc_exp[] = ''; } - - $select = ' %s '; - return sprintf($select, implode('', $option)); + + // Construct select by experiment. + foreach($experiments as $experiment) { + $locations = implode('+', $experiment['locations']); + $select_value = $experiment['value'] . $locations; + + $option_exp[] = ''; + } + + $select_experiment = ''; + + return sprintf($select_location_experiment, implode('', $option_loc_exp)) . sprintf($select_experiment, implode('', $option_exp)); } } + +/** + +foreach($experiment as $exp_loc) { + $experiment_name = $exp_loc['phenotype_customfield_terms:name']; + $experiment_id = $exp_loc['phenotype_customfield_terms:id']; + $experiment_loc = $exp_loc['phenotype_customfield_terms:location']; + + $select_value = $trait['id'] . '#' . $experiment_id . '#' . $experiment_loc . '#' . $germplasm; + $cache_exp[] = $experiment_id; + + $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; + $option_loc_exp[] = ''; + + + + + } + + + + + * + */ \ No newline at end of file diff --git a/includes/TripalFields/ncit__raw_data/theme/icon-download-all.jpg b/includes/TripalFields/ncit__raw_data/theme/icon-download-all.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94f2472cfc0334da794cf1a3730652d6c6020a76 GIT binary patch literal 13703 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 z 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 literal 0 HcmV?d00001 diff --git a/theme/css/rawpheno.germplasmfield.style.css b/theme/css/rawpheno.germplasmfield.style.css index 43e89e3..59513d5 100644 --- a/theme/css/rawpheno.germplasmfield.style.css +++ b/theme/css/rawpheno.germplasmfield.style.css @@ -90,7 +90,7 @@ #rawphenotypes-germplasm-field-table th:last-child, #rawphenotypes-germplasm-field-table td:last-child { - text-align: center ; + text-align: center; width: 10px; } @@ -98,11 +98,18 @@ opacity: 0.5; } +#rawphenotypes-germplasm-field-table th:first-child, #rawphenotypes-germplasm-field-table td:first-child { + text-align: center; + width: 35px; +} + +#rawphenotypes-germplasm-field-table td:nth-child(2) { width: 100%; } #rawphenotypes-germplasm-export-table { + position: relative; padding: 0; margin: 0; height: 238px; @@ -121,6 +128,7 @@ #rawphenotypes-germplasm-export-table tbody tr:hover { background-color: rgba(0, 0, 0, 0.1); + text-decoration: underline; -webkit-transition: background-color 0.9s ease-out; -moz-transition: background-color 0.9s ease-out; @@ -149,4 +157,69 @@ #rawphenotypes-germplasm-warning { margin: 8px 0; +} + +#rawphenotypes-germplasm-controls { + border-bottom-width: 2px; + border-bottom-style: solid; + border-bottom-color: #FFFFFF; + margin: 7px 0 0 0; + padding-bottom: 10px; + position: relative; +} + +#rawphenotypes-germplasm-controls span:first-child a { + padding-left: 17px; + padding-right: 3px; + background: url(../../includes/TripalFields/ncit__raw_data/theme/search.png) no-repeat left center; + background-size: 13px 16px; +} + +#rawphenotypes-germplasm-controls-search-window { + display: none; + background-color: #EAEAEA; + height: 60px; + margin-top: 10px; + padding-top: 10px; + padding-left: 20px; + padding-bottom: 5px; + position: absolute; + left: 45px; + width: 160px; + z-index: 1; + + -moz-box-shadow: 1px 1px 5px 1px #CCCCCC; + -webkit-box-shadow: 1px 1px 5px 1px #CCCCCC; + box-shadow: outset 1px 1px 5px 1px #CCCCCC; +} + +#rawphenotypes-germplasm-controls-search-window::after { + content: " "; + position: absolute; + bottom: 100%; /* At the top of the tooltip */ + left: 50%; + margin-left: -10px; + border-width: 10px; + border-style: solid; + border-color: transparent transparent #EAEAEA transparent; +} + +#rawphenotypes-germplasm-controls-search-window input { + display: block; + font-weight: 300; + font-size: 0.7em; + padding: 2px !important; + margin: 10px 0 5px 0 !important; + width: 80%; +} + +#rawphenotypes-germplasm-controls-search-window a { + font-size: 0.9em; + font-family: Arial, Helvetica, sans-serif; + font-weight: 300; +} + +.ui-autocomplete { + font-family:Arial, Helvetica, sans-serif !important; + font-size: 0.8em !important; } \ No newline at end of file diff --git a/theme/js/rawpheno.germplasmfield.script.js b/theme/js/rawpheno.germplasmfield.script.js index aa25f4c..de84ae6 100644 --- a/theme/js/rawpheno.germplasmfield.script.js +++ b/theme/js/rawpheno.germplasmfield.script.js @@ -67,4 +67,87 @@ ); } }); + + // Listen to controls search, expand and select by. + var tableWindow = $('#rawphenotypes-germplasm-export-table'); + // Create border when scrolling. + tableWindow.scroll(function() { + var c = ($(this).scrollTop() <= 0) ? '#FFFFFF' : '#314355'; + $('#rawphenotypes-germplasm-controls').css('border-bottom-color', c); + }); + + // Expand. + $('#rawphenotypes-germplasm-controls-expand').click(function(){ + var h = ($(this).is(':checked')) ? 460 : 230; + tableWindow.css('height', h); + }); + + // Search. + $('#rawphenotypes-germplasm-controls-search').click(function(e) { + e.preventDefault(); + + $('#rawphenotypes-germplasm-controls-search-window') + .fadeIn('fast') + .find('input').val('').focus(); + + tableWindow.scrollTop(-1); + }); + + // Close search window. + $('#rawphenotypes-germplasm-controls-search-window a').click(function(e) { + e.preventDefault(); + + $(this) + .parent().fadeOut('fast') + .find('input').val(''); + }); + + // Search field when selected/on focus. + $('#rawphenotypes-germplasm-controls-search-window input') + .click(function() { + if ($(this).val()) { + $(this).select(); + } + }); + + // Prepare search items (all traits currently displayed in the table). + var searchTerms = new Array(); + $('#rawphenotypes-germplasm-export-table table td:nth-child(2)').each(function() { + searchTerms.push($(this).text()); + }); + + $('#rawphenotypes-germplasm-controls-search-window input') + .autocomplete({ + select: function(event, ui) { + var foundIndex = searchTerms.indexOf(ui.item.value.trim()); + var foundRow = $('#rawphenotypes-germplasm-export-table table tbody tr').eq(foundIndex); + tableWindow.scrollTop(foundRow.position().top); + + var t = 0; + var timer = setInterval(function() { + if (t < 5) { + var o = (t%2 == 0) ? 0 : 1; + foundRow.css('opacity', o); + } + else { + foundRow.css('opacity', 1); + clearInterval(timer); + } + + t++; + }, 250); + + $(this).parent().fadeOut(); + }, + source: function(request, response) { + var results = $.ui.autocomplete.filter(searchTerms, request.term); + // Fist 5 results. + response(results.slice(0, 5)); + } + }); + + // Select by type. + + + }};}(jQuery)); \ No newline at end of file diff --git a/theme/rawpheno_germplasm_field.tpl.php b/theme/rawpheno_germplasm_field.tpl.php index 9a546f7..a597531 100644 --- a/theme/rawpheno_germplasm_field.tpl.php +++ b/theme/rawpheno_germplasm_field.tpl.php @@ -54,6 +54,16 @@
Please note that some experiments appear disabled. Please contact KnowPulse if you need access.
+ +
+ Expand Table | Search + Select by: Location + Experiment | Experiment + +
+ + Close +
+
From 3ef128bc8e994684684c6486968c527622ab5ea2 Mon Sep 17 00:00:00 2001 From: "reynoldltan@gmail.com" Date: Sat, 30 Apr 2022 17:00:14 -0600 Subject: [PATCH 14/16] Adds more download options. @TODO: prepare export data --- .../ncit__raw_data/ncit__raw_data.inc | 15 +- .../ncit__raw_data_formatter.inc | 157 ++++++++++-------- theme/css/rawpheno.germplasmfield.style.css | 10 ++ theme/js/rawpheno.germplasmfield.script.js | 126 +++++++++++++- theme/rawpheno_germplasm_field.tpl.php | 2 +- 5 files changed, 223 insertions(+), 87 deletions(-) diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc index 93cbd96..aa1df10 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data.inc @@ -149,10 +149,8 @@ class ncit__raw_data extends TripalField { // See includes/rawpheno.function.measurements.inc file for function definition // Given the user id this function returns an array of chado projects keyed by project_id // that the user has permission to see. - $user_experiment = rawpheno_function_user_project($user->uid); + $user_experiment = rawpheno_function_user_project($user->uid); $user_experiment = array_keys($user_experiment); - // Save user experiments. - $entity->{$field_name}['und'][0]['value']['phenotype_customfield_terms:user_experiment'] = $user_experiment; // All traits in experiment and location. $sql_cvterm = " @@ -183,12 +181,15 @@ class ncit__raw_data extends TripalField { ->fetchAllKeyed(0, 1); if ($trait_set) { - foreach($trait_set as $trait_id => $trait_name) { + foreach($trait_set as $trait_id => $trait_name) { + $allow = (in_array($item->project_id, $user_experiment)) ? 1 : 0; + // Save basic information about the trait (name + id key) and all experiment + location it was measured. $entity->{$field_name}['und'][0]['value']['hydra:member'][ $trait_name . '_' . $trait_id ][] = array( - 'phenotype_customfield_terms:id' => $item->project_id, // Project id number. - 'phenotype_customfield_terms:name' => $item->name, // Project name. - 'phenotype_customfield_terms:location' => $item->location // Location in a project trait was measured. + 'phenotype_customfield_terms:id' => $item->project_id, // Project id number. + 'phenotype_customfield_terms:name' => $item->name, // Project name. + 'phenotype_customfield_terms:location' => $item->location, // Location in a project trait was measured. + 'phenotype_customfield_terms:user_experiment' => $allow // Does user have permission? ); } } diff --git a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc index d96da27..89b52d1 100644 --- a/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc +++ b/includes/TripalFields/ncit__raw_data/ncit__raw_data_formatter.inc @@ -50,8 +50,6 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { if ($items[0]['value']) { // All trait and experiment+location values are accessible through this var. $germplasm_raw_phenotypes = $items[0]['value']['hydra:member']; - // Experiment array user has permission to. - $user_experiment = $items[0]['value']['phenotype_customfield_terms:user_experiment']; // Overall summary count of raw phenotypic data by experiment, location and trait. $summary_values = $items[0]['value']['phenotype_customfield_terms:summary']; // Id (stock id) of the current germplasm. @@ -65,7 +63,7 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $theme_path = $base_path . $module_path . '/includes/TripalFields/ncit__raw_data/theme/'; // Append image as bullet points, header icon and export button or link. - $img = ''; + $img = ''; // CONSTRUCT SUMMARY TABLE: @@ -76,10 +74,15 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $table_header = array( sprintf($img, '', $theme_path . 'icon-download-all.jpg', 'Download all for this trait'), 'Trait', - 'LOCATION + Experiment', + 'Filter by:', sprintf($img, '', $theme_path . 'icon-download.jpg', 'Download Location + Experiment') ); + // 2 select fields are required: + // A. Select field to filter by Location + Experiment. + // B. Select field to filter by Experiment (All location included). + // Depending on which Filter by option user selects, load corresponding select. + // # TABLE ROWS: $table_row = array(); foreach($germplasm_raw_phenotypes as $trait => $exp_loc) { @@ -89,8 +92,8 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $table_row[] = array( sprintf($img, $trait['id'] . '-all', $theme_path . 'icon-export.png', 'Download all for this trait'), ucfirst($trait['name']), - $this->create_select($germplasm_id, $trait, $exp_loc, $user_experiment), - sprintf($img, $trait['id'], $theme_path . 'icon-export.png', 'Download Location + Experiment') + $this->create_select($trait['id']), + sprintf($img, $trait['id'], $theme_path . 'icon-export.png', 'Download') ); } @@ -122,6 +125,9 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { drupal_add_js(array('rawpheno' => array('exportLink' => $base_path . '/phenotypes/raw/download')), array('type' => 'setting')); // Autocomplete UI. drupal_add_library('system', 'ui.autocomplete'); + // All datapoints available to JS to populate select field. + drupal_add_js(array('rawpheno' => array('germRawdata' => $germplasm_raw_phenotypes, 'germ' => $germplasm_id)), array('type' => 'setting')); + // Construct field render array. $element[0] = array( @@ -140,27 +146,40 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { /** * Create select field. * - * @param $germplasm - * Stock id number. - * @param $trait - * Associative array, with keys id and name that represent - * the cvterm id and cvterm name of a trait respectively. - * @param $experiment - * An array containing all experiment the trait+germplasm - * was measured with location information besides the experiment. - * @param $user_experiment - * Array of experiments user has access to. This will be used to cross check - * if experiment should be enabled or disabled to a user. + * @param $trait_id + * Trait id number (cvterm id number) used as value for id attribute. */ - public function create_select($germplasm, $trait, $experiment, $user_experiment) { - // Array to hold select option for select field by location + experiment. + public function create_select($trait_id) { + $attributes = array( + 'id' => 'rawphenotypes-germplasm-field-filterby-' . $trait_id, + 'class' => array('rawphenotypes-germplasm-field-filterby') + ); + + $options = array( + 0 => '---' + ); + + return theme('select', array('element' => array('#attributes' => $attributes, '#options' => $options))); + } +} + + + + + + + + + + + + + +/* + // Array to hold select option for select field by location + experiment. $option_loc_exp = array(); // Array to hold select option for select field by experiment. $option_exp = array(); - // Array to hold sorted values for select field by experiment. - $experiments = array(); - // Store all experiments. - $cache_exp = array(); // Create select by location + experiment at the same time // prepare values for the other select (by experiment). @@ -170,68 +189,66 @@ class ncit__raw_data_formatter extends TripalFieldFormatter { $experiment_loc = $exp_loc['phenotype_customfield_terms:location']; $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; - - // Select by Experiment. - if (!in_array($experiment_id, $cache_exp)) { - $experiments[ $experiment_id ] = array( - 'disabled' => $disabled, - 'text' => $experiment_name, - 'value' => $trait['id'] . '#' . $experiment_id . '#' . $germplasm . '#' - ); - } - - $experiments[ $experiment_id ]['locations'][] = $experiment_loc; - $cache_exp[] = $experiment_id; - + // Select by Location + Experiment. $select_value = $trait['id'] . '#' . $experiment_id . '#' . $germplasm . '#' . $experiment_loc; - $option_loc_exp[] = ''; + $option_loc_exp[] = ''; } - $select_location_experiment = ''; + // Construct select by experiment. + unset($experiment_name, $experiment_id, $experiment_loc); + // All unique experiments this trait was measured. + $experiments = array_column($experiment, 'phenotype_customfield_terms:name', 'phenotype_customfield_terms:id'); + + foreach($experiments as $experiment_id => $experiment_name) { + $experiment_loc = array_column($experiment, 'phenotype_customfield_terms:location', $experiment_id); + $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; - // Construct select by experiment. - foreach($experiments as $experiment) { - $locations = implode('+', $experiment['locations']); - $select_value = $experiment['value'] . $locations; - - $option_exp[] = ''; + $select_value = $experiment_id . '#' . implode('+', array_values($experiment_loc)); + $option_exp[] = ''; } - $select_experiment = ''; - return sprintf($select_location_experiment, implode('', $option_loc_exp)) . sprintf($select_experiment, implode('', $option_exp)); - } -} -/** - -foreach($experiment as $exp_loc) { - $experiment_name = $exp_loc['phenotype_customfield_terms:name']; - $experiment_id = $exp_loc['phenotype_customfield_terms:id']; - $experiment_loc = $exp_loc['phenotype_customfield_terms:location']; - $select_value = $trait['id'] . '#' . $experiment_id . '#' . $experiment_loc . '#' . $germplasm; - $cache_exp[] = $experiment_id; - $disabled = (in_array($experiment_id, $user_experiment)) ? '' : 'disabled'; - $option_loc_exp[] = ''; - - - + + + + + // SELECT BY LOCATION + EXPERIMENT FIELD. + $select_location_experiment = ' + + '; - } - * - */ \ No newline at end of file + + + + + + + + + + + + + + + // SELECT BY EXPERIMENT FIELD. + $select_experiment = ' + + '; + + return sprintf($select_location_experiment, implode('', $option_loc_exp)) . sprintf($select_experiment, implode('', $option_exp)); + */ \ No newline at end of file diff --git a/theme/css/rawpheno.germplasmfield.style.css b/theme/css/rawpheno.germplasmfield.style.css index 59513d5..e75b291 100644 --- a/theme/css/rawpheno.germplasmfield.style.css +++ b/theme/css/rawpheno.germplasmfield.style.css @@ -222,4 +222,14 @@ .ui-autocomplete { font-family:Arial, Helvetica, sans-serif !important; font-size: 0.8em !important; +} + +#rawphenotypes-germplasm-controls-selectby { + float: right; +} + +#rawphenotypes-germplasm-controls-selectby span { + background-color: #314355; + color: #FFFFFF; + padding: 1px 3px; } \ No newline at end of file diff --git a/theme/js/rawpheno.germplasmfield.script.js b/theme/js/rawpheno.germplasmfield.script.js index de84ae6..48009eb 100644 --- a/theme/js/rawpheno.germplasmfield.script.js +++ b/theme/js/rawpheno.germplasmfield.script.js @@ -17,8 +17,9 @@ $('#rawphenotypes-define-raw-container').slideUp(200); }); } - - + + // Default select fields to Location + Experiment filter. + selectOptions('le'); var imgOpacity = 0.5; // Select box event - prepare link to download selection. @@ -32,12 +33,7 @@ // Reset other select to allow only one select field // at a time to export. - selects.each(function() { - if ($(this).attr('id') != selectId) { - $('#' + $(this).attr('id') + '-img').css('opacity', '0.5'); - this.selectedIndex = 0; - } - }); + selectReset(selectId); if (selectValue == '0') { // None selected - default to select an option. @@ -57,7 +53,6 @@ // Listen to images clicked to launch data download. $('#rawphenotypes-germplasm-field-table td:last-child img').click(function(e) { - var imgId = e.target.id; var imgOpacity = $(this).css('opacity'); if (imgOpacity == 1) { @@ -68,6 +63,19 @@ } }); + // Image to download ALL for a trait. + $('#rawphenotypes-germplasm-field-table td:first-child img').click(function(e) { + var imgId = e.target.id; + var i = imgId.split('-'); + // rawphenotypes-germplasm-field-filterby-%s-img + var downloadLink = 't=' + i[4] + '&p=All&l=All&g=' + Drupal.settings.rawpheno.germ; + + window.open( + Drupal.settings.rawpheno.exportLink + '?code=' + btoa(downloadLink), + '_blank' + ); + }); + // Listen to controls search, expand and select by. var tableWindow = $('#rawphenotypes-germplasm-export-table'); // Create border when scrolling. @@ -147,7 +155,107 @@ }); // Select by type. + $('#rawphenotypes-germplasm-controls-selectby a').click(function(e) { + e.preventDefault(); + + var selByStates = ['Experiment', 'Location + Experiment']; + + // If link is experiment - switch it to location + experiment + // and vice versa. + var selByText = ($(this).text() == 'Experiment') ? 1 : 0; + var selByCur = (selByText) ? 0 : 1; + + $(this).text(selByStates[ selByText ]); + $(this).parent().find('span').text(selByStates[ selByCur ]); + + // Match selet field. + selectReset(); + var sel = (selByCur) ? 'le' : 'e'; + selectOptions(sel); + }); + /** + * Reset select boxes and export link. + * + * @param selectId + * Trait id number used to reference a select field + * If this is specified, exclude this select from reset operation. + */ + function selectReset(selectId = -1) { + var selects = $('#rawphenotypes-germplasm-field-table select'); + selects.each(function() { + if ($(this).attr('id') != selectId) { + $('#' + $(this).attr('id') + '-img').css('opacity', '0.5'); + this.selectedIndex = 0; + } + }); + } + + /** + * Remove option element from a select field. + * @param select + * Object, reference to select element. + */ + function removeOptions(select) { + select.find('option').each(function(i, v) { + if (i > 0) $(this).remove(); + }); + } + + /** + * Create select options + * + * @param set + * String, indicate if options is for location+experiment or experiment. + * Default to le = location + experiment. + */ + function selectOptions(set = 'le') { + var dataset = Drupal.settings.rawpheno; + var germplasm = dataset.germ; + + // Prepare dataset. + if (set == 'le') { + // LOCATION + EXPERIMENT: + $.each(dataset.germRawdata, function(index, value){ + var trait = index.split('_'); + var element = $('#rawphenotypes-germplasm-field-filterby-' + trait[1]); + removeOptions(element); + $.each(value, function(i, v) { + var disabled = v['phenotype_customfield_terms:user_experiment'] == 1 ? '' : 'disabled'; + element.append($('