From 6f3df24eb34cbef2b1fbdac277c7b0fafd46db54 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Fri, 9 Feb 2024 15:54:37 -0600 Subject: [PATCH 01/50] Add launch config for `POPULATE_PHENOTYPES_DATABASE` --- .vscode/launch.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b51a6a1..c8398905 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -322,6 +322,28 @@ "SITE_BASE_URL": "https://localhost:8080" } }, + { + "name": "Run POPULATE_PHENOTYPES_DATASTORE", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/src/modules/db_operations/main.py", + "console": "integratedTerminal", + "python": "${workspaceFolder}/src/modules/db_operations/venv/bin/python", + "justMyCode": true, + "envFile": "${workspaceFolder}/src/modules/db_operations/.env", + "env": { + "DATABASE_OPERATION": "POPULATE_PHENOTYPES_DATASTORE", + "USE_MEMOIZATION": "1", + // "MODULE_DB_OPERATIONS_CONNECTION_TYPE": "localhost", + // "MODULE_DB_OPERATIONS_CONNECTION_TYPE": "memory", + "MODULE_DB_OPERATIONS_CONNECTION_TYPE": "file", + "MODULE_DB_OPERATIONS_CONNECTION_FILE": "${workspaceFolder}/temp/local.db", + "MODULE_DB_TIMEOUT": "300", + "CAENDR_USE_SSL": "1", + "SENTRY_URL": "", + "SITE_BASE_URL": "https://localhost:8080", + }, + }, { "name": "DB_OP TEST_ECHO", "type": "python", From 46e2e3208cedea72d3d18d993656036b1f4f2191 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Tue, 20 Feb 2024 15:24:56 -0600 Subject: [PATCH 02/50] Add trait display names to Google Sheet > GCP db op --- src/modules/db_operations/seed_trait_files.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/db_operations/seed_trait_files.py b/src/modules/db_operations/seed_trait_files.py index 4cc5d78c..d9933b6e 100644 --- a/src/modules/db_operations/seed_trait_files.py +++ b/src/modules/db_operations/seed_trait_files.py @@ -61,9 +61,12 @@ def populate_andersenlab_trait_files(): 'species': get_field_from_record(record, 'Species', nullable=False), # About trait - 'description_short': get_field_from_record(record, 'Short_Description'), - 'description_long': get_field_from_record(record, 'Long_Description'), - 'units': get_field_from_record(record, 'Units'), + 'trait_name_display_1': get_field_from_record(record, 'Trait_Name_Display1'), + 'trait_name_display_2': get_field_from_record(record, 'Trait_Name_Display2'), + 'trait_name_display_3': get_field_from_record(record, 'Trait_Name_Display3'), + 'description_short': get_field_from_record(record, 'Short_Description'), + 'description_long': get_field_from_record(record, 'Long_Description'), + 'units': get_field_from_record(record, 'Units'), # Tag list (categories) 'tags': list(filter( From f066a1b12493e44661acbf4b7b9413105815f112 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Tue, 20 Feb 2024 15:25:30 -0600 Subject: [PATCH 03/50] Add trait display names to datastore entity class --- src/pkg/caendr/caendr/models/datastore/trait_file.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pkg/caendr/caendr/models/datastore/trait_file.py b/src/pkg/caendr/caendr/models/datastore/trait_file.py index f5f42c25..681e5c94 100644 --- a/src/pkg/caendr/caendr/models/datastore/trait_file.py +++ b/src/pkg/caendr/caendr/models/datastore/trait_file.py @@ -33,6 +33,9 @@ def get_props_set(cls): 'dataset', # About trait + 'trait_name_display_1', + 'trait_name_display_2', + 'trait_name_display_3', 'description_short', 'description_long', 'units', From 5a00d1f64b39b58aa84e6f33844e5e7fcced061a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Tue, 20 Feb 2024 15:40:44 -0600 Subject: [PATCH 04/50] Better fallback behavior for display name --- .../_includes/phenotype-database-table.html | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index cefbe151..a75e34c4 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -312,7 +312,24 @@

tagsHTML += `` } } - + + // Render all provided components of the display name + let nameHTML; + if (arr[i].trait_name_display_1) { + nameHTML = `${arr[i].trait_name_display_1}`; + if (arr[i].trait_name_display_2) { + nameHTML += `

${arr[i].trait_name_display_2}

`; + } + if (arr[i].trait_name_display_3) { + if (!arr[i].trait_name_display_2) nameHTML += '
'; + nameHTML += `${arr[i].trait_name_display_3}`; + } + } + // If no display name, fall back to the CaeNDR trait name + else { + nameHTML = `${arr[i].trait_name_caendr}`; + } + let html = ` @@ -321,9 +338,7 @@

class="bi bi-chevron-right" aria-hidden="true">${speciesName} - ${arr[i].trait_name_display_1 || arr[i].trait_name_caendr} -

${arr[i].trait_name_display_1 ? arr[i].trait_name_display_2 : ''}

- ${arr[i].trait_name_display_1 ? arr[i].trait_name_display_3 : ''} + ${nameHTML} ${arr[i].description_short}. ${arr[i].source_lab} From 16630228deeb4bc19ff2a992b2f805a7d03c4a48 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Tue, 20 Feb 2024 15:40:58 -0600 Subject: [PATCH 05/50] Display tags column as `flex-row` Fixes rendering error if tags list is shorter than other columns (height-wise) --- .../site-v2/templates/_includes/phenotype-database-table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index a75e34c4..c00d0835 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -343,7 +343,7 @@

${arr[i].description_short}. ${arr[i].source_lab} ${createdOn} - ${tagsHTML} + ${tagsHTML} From 68e4ee649382256a342b8478f1ae893319f787ea Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Tue, 20 Feb 2024 16:25:11 -0600 Subject: [PATCH 06/50] Render empty table cell if no short description --- .../site-v2/templates/_includes/phenotype-database-table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index c00d0835..d06f0eba 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -340,7 +340,7 @@

${nameHTML} - ${arr[i].description_short}. + ${arr[i].description_short ? (arr[i].description_short + '.') : ''} ${arr[i].source_lab} ${createdOn} ${tagsHTML} From 2f3054060f31eb7cc6716b07a13f92ec4d9c44da Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 21 Feb 2024 16:59:02 -0600 Subject: [PATCH 07/50] Remove unused "label" field from trait report & link with "View Report" col --- .../base/views/tools/phenotype_database.py | 16 ++++++++-------- .../tools/phenotype_database/submit-traits.html | 1 - .../site-v2/templates/tools/report-list.html | 6 ++++-- .../caendr/models/datastore/phenotype_report.py | 1 - .../models/job_pipeline/phenotype_pipeline.py | 1 - 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/modules/site-v2/base/views/tools/phenotype_database.py b/src/modules/site-v2/base/views/tools/phenotype_database.py index cead747a..1cdf3056 100644 --- a/src/modules/site-v2/base/views/tools/phenotype_database.py +++ b/src/modules/site-v2/base/views/tools/phenotype_database.py @@ -44,13 +44,6 @@ def check_bp_enabled(): def results_columns(): return [ - { - 'title': 'Description', - 'class': 'label', - 'field': 'label', - 'width': 0.2, - 'link_to_data': True, - }, { 'title': 'Trait 1', 'class': 's1', @@ -63,6 +56,13 @@ def results_columns(): 'field': 'trait_2_name', 'width': 0.4, }, + { + 'title': 'View Report', + 'field': None, + 'value': 'View Report', + 'width': 0.2, + 'link_to_data': True, + }, ] @@ -251,7 +251,7 @@ def submit(): # Read & clean fields from JSON data data = { field: bleach.clean(request.json.get(field)) - for field in {'label', 'species', 'trait_1', 'trait_1_dataset'} + for field in {'species', 'trait_1', 'trait_1_dataset'} } # Read & clean values for trait 2, if given diff --git a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html index b2dc2b99..0abd8916 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html @@ -214,7 +214,6 @@ } data = { - 'label': '???', species, trait_1, trait_1_dataset, {%- if two_traits %} diff --git a/src/modules/site-v2/templates/tools/report-list.html b/src/modules/site-v2/templates/tools/report-list.html index bc13e57a..3618110b 100644 --- a/src/modules/site-v2/templates/tools/report-list.html +++ b/src/modules/site-v2/templates/tools/report-list.html @@ -84,11 +84,13 @@ {% if column.get('link_to_data', False) and (item.status == JobStatus.COMPLETE or item.status == JobStatus.ERROR) %} - {{ item[column.field] }} + {% if column.field %}{{ item[column.field] }}{% elif column.value %}{{ column.value }}{% endif %} {% else %} - {%- if item[column.field] %} + {%- if column.field and item[column.field] %} {{ item[column.field] }} + {%- elif column.value %} + {{ column.value }} {%- endif %} {% endif %} diff --git a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py index da24439b..14fb85fe 100644 --- a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py +++ b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py @@ -48,7 +48,6 @@ def get_props_set(cls): return { *super().get_props_set(), 'species', - 'label', 'trait_1', 'trait_1_name', 'trait_2', diff --git a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py index 30d5c3a7..08c5ca9c 100644 --- a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py +++ b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py @@ -47,7 +47,6 @@ def parse(cls, data, valid_file_extensions=None): # Start building props object props = { - 'label': data.get('label'), 'species': data.get('species'), } From 0e930363e4b0bde8986fb0ab8b157edebb1fd71e Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Fri, 23 Feb 2024 16:02:12 -0600 Subject: [PATCH 08/50] Move trait multiline HTML to new utils file --- .../_includes/phenotype-database-table.html | 22 +++---------------- .../site-v2/templates/_scripts/trait_utils.js | 13 +++++++++++ 2 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 src/modules/site-v2/templates/_scripts/trait_utils.js diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index d06f0eba..a5864d10 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -114,7 +114,8 @@

{% block script %} @@ -183,7 +185,8 @@

Description:

{%- if data.num_traits == 1 %} // Render chart(s) for one trait -const trait_label = {{ label_string_units(data.trait_names[0], report['trait_1']['units']) }}; +{%- set trait_display_name = report['trait_1'].display_name %} +const trait_label = {{ label_string_units( render_trait_name_string(trait_display_name[0], trait_display_name[1], trait_display_name[2]), report['trait_1']['units']) }}; try { render_histogram('#phenotype-chart-histogram', data, { @@ -209,8 +212,10 @@

Description:

{%- else %} // Render chart(s) for two traits -const trait_label_1 = {{ label_string_units(data.trait_names[0], report['trait_1']['units']) }}; -const trait_label_2 = {{ label_string_units(data.trait_names[1], report['trait_2']['units']) }}; +{%- set trait_1_display_name = report['trait_1'].display_name %} +{%- set trait_2_display_name = report['trait_2'].display_name %} +const trait_label_1 = {{ label_string_units( render_trait_name_string(trait_1_display_name[0], trait_1_display_name[1], trait_1_display_name[2]), report['trait_1']['units']) }}; +const trait_label_2 = {{ label_string_units( render_trait_name_string(trait_2_display_name[0], trait_2_display_name[1], trait_2_display_name[2]), report['trait_2']['units']) }}; try { render_scatterplot_histograms('#phenotype-chart', data, { diff --git a/src/pkg/caendr/caendr/models/datastore/trait_file.py b/src/pkg/caendr/caendr/models/datastore/trait_file.py index e13bad88..67fb9422 100644 --- a/src/pkg/caendr/caendr/models/datastore/trait_file.py +++ b/src/pkg/caendr/caendr/models/datastore/trait_file.py @@ -1,3 +1,5 @@ +from typing import Tuple, Optional + from caendr.utils.env import get_env_var from caendr.models.datastore import FileRecordEntity, PublishableEntity, SpeciesEntity, UserOwnedEntity @@ -95,3 +97,13 @@ def is_bulk_file(self): @is_bulk_file.setter def is_bulk_file(self, val): return self._set_raw_prop('is_bulk_file', bool(val)) + + + @property + def display_name(self) -> Tuple[str, Optional[str], Optional[str]]: + ''' + The trait display name as a tuple. The first element will always exist. + + Combines `trait_name_display_1`, `trait_name_display_2`, and `trait_name_display_3` into a single tuple. + ''' + return self['trait_name_display_1'], self['trait_name_display_2'], self['trait_name_display_3'] From f72fbf2fb84cb0c6bca5ba921bfabe95342fd32a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 28 Feb 2024 14:07:31 -0600 Subject: [PATCH 16/50] Move report list individual tool cols to Jinja Store as Jinja dict linked to tool bp name. Rendered with macros. --- .../base/views/tools/genetic_mapping.py | 19 ----- .../views/tools/heritability_calculator.py | 18 ---- .../base/views/tools/pairwise_indel_finder.py | 25 ------ .../base/views/tools/phenotype_database.py | 26 ------ .../templates/_includes/report_list_macros.j2 | 85 +++++++++++++++++++ .../site-v2/templates/tools/report-list.html | 21 ++--- 6 files changed, 91 insertions(+), 103 deletions(-) create mode 100644 src/modules/site-v2/templates/_includes/report_list_macros.j2 diff --git a/src/modules/site-v2/base/views/tools/genetic_mapping.py b/src/modules/site-v2/base/views/tools/genetic_mapping.py index db9c0d4b..5c8e3fc7 100755 --- a/src/modules/site-v2/base/views/tools/genetic_mapping.py +++ b/src/modules/site-v2/base/views/tools/genetic_mapping.py @@ -33,24 +33,6 @@ -def results_columns(): - return [ - { - 'title': 'Description', - 'class': 'label', - 'field': 'label', - 'width': 0.6, - 'link_to_data': True, - }, - { - 'title': 'Trait', - 'class': 'trait', - 'field': 'trait', - 'width': 0.4, - }, - ] - - @genetic_mapping_bp.route('', methods=['GET']) @jwt_required() def genetic_mapping(): @@ -162,7 +144,6 @@ def list_results(): # Table info 'species_list': Species.all(), 'items': list_reports(NemascanReport, None if show_all else user, filter_errs), - 'columns': results_columns(), 'JobStatus': JobStatus, }) diff --git a/src/modules/site-v2/base/views/tools/heritability_calculator.py b/src/modules/site-v2/base/views/tools/heritability_calculator.py index 3f961c1a..85ea559c 100644 --- a/src/modules/site-v2/base/views/tools/heritability_calculator.py +++ b/src/modules/site-v2/base/views/tools/heritability_calculator.py @@ -51,23 +51,6 @@ ) -def results_columns(): - return [ - { - 'title': 'Description', - 'class': 'label', - 'field': 'label', - 'width': 0.6, - 'link_to_data': True, - }, - { - 'title': 'Trait', - 'class': 'trait', - 'field': 'trait', - 'width': 0.4, - }, - ] - @heritability_calculator_bp.route('') @jwt_required() @@ -136,7 +119,6 @@ def list_results(): # Table info 'species_list': Species.all(), 'items': list_reports(HeritabilityReport, None if show_all else user, filter_errs), - 'columns': results_columns(), 'JobStatus': JobStatus, }) diff --git a/src/modules/site-v2/base/views/tools/pairwise_indel_finder.py b/src/modules/site-v2/base/views/tools/pairwise_indel_finder.py index 03176d54..5c7b09c4 100644 --- a/src/modules/site-v2/base/views/tools/pairwise_indel_finder.py +++ b/src/modules/site-v2/base/views/tools/pairwise_indel_finder.py @@ -32,30 +32,6 @@ -def results_columns(): - return [ - { - 'title': 'Site', - 'class': 'site', - 'field': 'site', - 'width': 0.5, - 'link_to_data': True, - 'data_order': lambda e: e['site'], - }, - { - 'title': 'Strain 1', - 'class': 's1', - 'field': 'strain_1', - 'width': 0.25, - }, - { - 'title': 'Strain 2', - 'class': 's2', - 'field': 'strain_2', - 'width': 0.25, - }, - ] - def try_get_sv_strains(species): try: return get_sv_strains(species) @@ -185,7 +161,6 @@ def list_results(): # Table info 'species_list': Species.all(), 'items': list_reports(IndelPrimerReport, None if show_all else user, filter_errs), - 'columns': results_columns(), 'JobStatus': JobStatus, }) diff --git a/src/modules/site-v2/base/views/tools/phenotype_database.py b/src/modules/site-v2/base/views/tools/phenotype_database.py index 1cdf3056..9b8b9c88 100644 --- a/src/modules/site-v2/base/views/tools/phenotype_database.py +++ b/src/modules/site-v2/base/views/tools/phenotype_database.py @@ -42,31 +42,6 @@ def check_bp_enabled(): -def results_columns(): - return [ - { - 'title': 'Trait 1', - 'class': 's1', - 'field': 'trait_1_name', - 'width': 0.4, - }, - { - 'title': 'Trait 2', - 'class': 's2', - 'field': 'trait_2_name', - 'width': 0.4, - }, - { - 'title': 'View Report', - 'field': None, - 'value': 'View Report', - 'width': 0.2, - 'link_to_data': True, - }, - ] - - - # # Main Endpoint # @@ -310,7 +285,6 @@ def list_results(): # Table info 'species_list': Species.all(), 'items': list_reports(PhenotypeReport, user = None if show_all else user, filter_errs=filter_errs), - 'columns': results_columns(), 'JobStatus': JobStatus, }) diff --git a/src/modules/site-v2/templates/_includes/report_list_macros.j2 b/src/modules/site-v2/templates/_includes/report_list_macros.j2 new file mode 100644 index 00000000..9b967025 --- /dev/null +++ b/src/modules/site-v2/templates/_includes/report_list_macros.j2 @@ -0,0 +1,85 @@ +{%- macro render_report_list_column(item, column) %} + {%- set create_link = column.get('linked', false) and (item.status in JobStatus.FINISHED) %} + {%- set render_value = item[column.field] if (column.field and item[column.field]) else column.value %} + + {%- if create_link %} + + {%- endif %} + {{ render_value }} + {%- if create_link %} + + {%- endif %} + +{%- endmacro %} + + +{%- macro render_report_list_columns(columns, item) %} + {%- for column in columns %} + {{ render_report_list_column( item, column ) }} + {%- endfor %} +{%- endmacro %} + + +{% set report_list_columns = { + 'pairwise_indel_finder': [ + { 'title': 'Site', + 'class': 'site', + 'field': 'site', + 'width': 0.5, + 'order': 'site', + 'linked': true, + }, + { 'title': 'Strain 1', + 'class': 's1', + 'field': 'strain_1', + 'width': 0.25, + }, + { 'title': 'Strain 2', + 'class': 's2', + 'field': 'strain_2', + 'width': 0.25, + }, + ], + 'genetic_mapping': [ + { 'title': 'Description', + 'class': 'label', + 'field': 'label', + 'width': 0.6, + 'linked': true, + }, + { 'title': 'Trait', + 'class': 'trait', + 'field': 'trait', + 'width': 0.4, + }, + ], + 'heritability_calculator': [ + { 'title': 'Description', + 'class': 'label', + 'field': 'label', + 'width': 0.6, + 'linked': true, + }, + { 'title': 'Trait', + 'class': 'trait', + 'field': 'trait', + 'width': 0.4, + }, + ], + 'phenotype_database': [ + { 'title': 'Trait 1', + 'field': 'trait_1_name', + 'width': 0.4, + }, + { 'title': 'Trait 2', + 'field': 'trait_2_name', + 'width': 0.4, + }, + { 'title': 'View Report', + 'value': 'View Report', + 'width': 0.2, + 'linked': true, + }, + ] + } +%} diff --git a/src/modules/site-v2/templates/tools/report-list.html b/src/modules/site-v2/templates/tools/report-list.html index 3618110b..8ac11dff 100644 --- a/src/modules/site-v2/templates/tools/report-list.html +++ b/src/modules/site-v2/templates/tools/report-list.html @@ -1,5 +1,8 @@ {% extends "_layouts/default.html" %} +{% from "_includes/report_list_macros.j2" import report_list_columns %} +{% set columns = report_list_columns[tool_name] %} + {% block custom_head %} @@ -28,6 +31,7 @@ {% block content %} {% from "_includes/macros.html" import render_dataTable_top_menu, render_dataTable_actions, render_dataTable_result_list_header %} +{% from "_includes/report_list_macros.j2" import render_report_list_columns with context %} {{ @@ -80,21 +84,8 @@ {% if item.species %}{{ species_list[item.species].short_name }}{% endif %} - {% for column in columns %} - - {% if column.get('link_to_data', False) and (item.status == JobStatus.COMPLETE or item.status == JobStatus.ERROR) %} - - {% if column.field %}{{ item[column.field] }}{% elif column.value %}{{ column.value }}{% endif %} - - {% else %} - {%- if column.field and item[column.field] %} - {{ item[column.field] }} - {%- elif column.value %} - {{ column.value }} - {%- endif %} - {% endif %} - - {% endfor %} + {# Render the columns specific to this report #} + {{ render_report_list_columns(columns, item) }} {% if item.status %} {{ item.status|capitalize }} {% else %} UNKNOWN {% endif %} From f1615cee2bea7d00df35d60ab1ffeb154b43610f Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 28 Feb 2024 14:10:07 -0600 Subject: [PATCH 17/50] Cache trait display names in report objects Adds new class method for `PhenotypeReport` to recompute all cached names. --- .../models/datastore/phenotype_report.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py index 14fb85fe..e705e504 100644 --- a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py +++ b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, List from caendr.models.datastore import ReportEntity, HashableEntity, TraitFile from caendr.models.trait import Trait @@ -48,10 +48,16 @@ def get_props_set(cls): return { *super().get_props_set(), 'species', + 'trait_1', 'trait_1_name', + 'trait_1_name_caendr', + 'trait_1_name_display', + 'trait_2', 'trait_2_name', + 'trait_2_name_caendr', + 'trait_2_name_display', } @@ -254,3 +260,37 @@ def trait_names(self) -> Tuple[str]: if self['trait_2'] is None: return self['trait_1_name'], return self['trait_1_name'], self['trait_2_name'] + + + @classmethod + def recompute_cached_names(cls, reports: List['PhenotypeReport'] = None): + ''' + Recomputes the cached CaeNDR and display names for trait reports, + based on the values stored in the referenced TraitFile entities. + + Arguments: + `reports` (optional): List of reports to recompute. If `None`, updates all reports. + ''' + if reports is None: + reports = PhenotypeReport.query_ds() + + for report in reports: + + # Set CaeNDR and display names for trait 1, as applicable + if report['trait_1']: + report['trait_1_name_caendr'] = report['trait_1']['trait_name_caendr'] + if report['trait_1'].display_name[0]: + report['trait_1_name_display'] = list(report['trait_1'].display_name) + else: + report['trait_1_name_display'] = [ report['trait_1_name'] ] + + # Set CaeNDR and display names for trait 2, as applicable + if report['trait_2']: + report['trait_2_name_caendr'] = report['trait_2']['trait_name_caendr'] + if report['trait_2'].display_name[0]: + report['trait_2_name_display'] = list(report['trait_2'].display_name) + else: + report['trait_2_name_display'] = [ report['trait_2_name'] ] + + # Save the report + report.save() From 6b49f96c3784ebdda26e9aab287d5cb79207e4d9 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 28 Feb 2024 14:11:20 -0600 Subject: [PATCH 18/50] Render trait display names in reports list w custom renderer --- .../templates/_includes/report_list_macros.j2 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/report_list_macros.j2 b/src/modules/site-v2/templates/_includes/report_list_macros.j2 index 9b967025..784b2f08 100644 --- a/src/modules/site-v2/templates/_includes/report_list_macros.j2 +++ b/src/modules/site-v2/templates/_includes/report_list_macros.j2 @@ -1,3 +1,6 @@ +{% from "_includes/select-trait.html" import render_trait_name_multiline %} + + {%- macro render_report_list_column(item, column) %} {%- set create_link = column.get('linked', false) and (item.status in JobStatus.FINISHED) %} {%- set render_value = item[column.field] if (column.field and item[column.field]) else column.value %} @@ -5,7 +8,7 @@ {%- if create_link %} {%- endif %} - {{ render_value }} + {%- if column.render %} {{ column.render(render_value) }} {% else %} {{ render_value }} {% endif %} {%- if create_link %} {%- endif %} @@ -20,6 +23,11 @@ {%- endmacro %} +{%- macro render_trait_name_safe(display_name) %} + {% if display_name %} {{ render_trait_name_multiline(display_name[0], display_name[1], display_name[2]) }} {% endif %} +{%- endmacro %} + + {% set report_list_columns = { 'pairwise_indel_finder': [ { 'title': 'Site', @@ -68,12 +76,14 @@ ], 'phenotype_database': [ { 'title': 'Trait 1', - 'field': 'trait_1_name', + 'field': 'trait_1_name_display', 'width': 0.4, + 'render': render_trait_name_safe, }, { 'title': 'Trait 2', - 'field': 'trait_2_name', + 'field': 'trait_2_name_display', 'width': 0.4, + 'render': render_trait_name_safe, }, { 'title': 'View Report', 'value': 'View Report', From 9538026f13c106af7e0a5584867b7fa9dae10f99 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 11:45:22 -0600 Subject: [PATCH 19/50] Fix initial trait none check (again) --- .../templates/tools/phenotype_database/submit-traits.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html index 1a7b8309..5615d544 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html @@ -106,7 +106,7 @@ // Set headers for AJAX requests {{ ajax_setup(form.csrf_token._value()) }} - {%- if initial_trait != null %} + {%- if initial_trait is not none %} // Set the initial trait {{ select_trait('trait-1', trait_obj=initial_trait) }} {%- endif %} From e46371d727b60e8709a67acbb79cb198a13f2d5a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:01:08 -0600 Subject: [PATCH 20/50] Sort out `PhenotypeReport` trait name fields (see desc) - Replace `trait_{n}_name` with `trait_{n}_name_caendr` - Compute display name(s) when creating new report & in the recompute function - Now use `trait_{n}_name_display` directly to render report page --- .../tools/phenotype_database/report.html | 4 +- .../models/datastore/phenotype_report.py | 56 ++++++++++++------- .../models/job_pipeline/phenotype_pipeline.py | 12 ++-- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 023bb42f..1aa099c1 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -79,7 +79,7 @@

Trait 1

Species:

{{ report['species']|replace('_', '. ')}}

Name:

-

{{ render_trait_name_multiline(report['trait_1']['trait_name_display_1'], report['trait_1']['trait_name_display_2'], report['trait_1']['trait_name_display_3']) }}

+

{{ render_trait_name_multiline(report['trait_1_name_display'][0], report['trait_1_name_display'][1], report['trait_1_name_display'][2]) }}

Description:

{{ report['trait_1']['description_short'] or report['trait_1']['description_long'] }}

@@ -96,7 +96,7 @@

Trait 2

Species:

{{ report['species']|replace('_', '. ')}}

Name:

-

{{ render_trait_name_multiline(report['trait_2']['trait_name_display_1'], report['trait_2']['trait_name_display_2'], report['trait_2']['trait_name_display_3']) }}

+

{{ render_trait_name_multiline(report['trait_2_name_display'][0], report['trait_2_name_display'][1], report['trait_2_name_display'][2]) }}

Description:

{{ report['trait_2']['description_short'] or report['trait_2']['description_long'] }}

diff --git a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py index e705e504..ebe1cd84 100644 --- a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py +++ b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py @@ -1,4 +1,4 @@ -from typing import Tuple, List +from typing import List, Optional, Tuple, Union from caendr.models.datastore import ReportEntity, HashableEntity, TraitFile from caendr.models.trait import Trait @@ -49,13 +49,13 @@ def get_props_set(cls): *super().get_props_set(), 'species', + # Identifiers for trait 1 (display name is cached) 'trait_1', - 'trait_1_name', 'trait_1_name_caendr', 'trait_1_name_display', + # Identifiers for trait 2 (display name is cached) 'trait_2', - 'trait_2_name', 'trait_2_name_caendr', 'trait_2_name_display', } @@ -258,12 +258,39 @@ def trait_files(self) -> Tuple[TraitFile]: @property def trait_names(self) -> Tuple[str]: if self['trait_2'] is None: - return self['trait_1_name'], - return self['trait_1_name'], self['trait_2_name'] + return self['trait_1_name_caendr'], + return self['trait_1_name_caendr'], self['trait_2_name_caendr'] + + + + # + # Computing & Caching Display Names + # + + + @classmethod + def compute_display_name(cls, trait: Union[Trait, TraitFile], trait_name: Optional[str] = None) -> List[str]: + ''' + Compute the cache-able display name for a given trait. + + If the `trait` argument is given as a `TraitFile` and references a bulk file, then the `trait_name` argument MUST be provided + to specify which trait in the file is intended. + ''' + + # Convert argument to a Trait object if it isn't one already + if isinstance(trait, TraitFile): + trait = Trait.from_datastore(trait, trait_name=trait_name) + + # Reject all other invalid data types + elif not isinstance(trait, Trait): + raise ValueError() + + # For bulk files, store the single trait name, otherwise convert the display_name fields to a list + return [trait.name] if trait.file['is_bulk_file'] else list(trait.file.display_name) @classmethod - def recompute_cached_names(cls, reports: List['PhenotypeReport'] = None): + def recompute_cached_display_names(cls, reports: List['PhenotypeReport'] = None): ''' Recomputes the cached CaeNDR and display names for trait reports, based on the values stored in the referenced TraitFile entities. @@ -272,25 +299,16 @@ def recompute_cached_names(cls, reports: List['PhenotypeReport'] = None): `reports` (optional): List of reports to recompute. If `None`, updates all reports. ''' if reports is None: - reports = PhenotypeReport.query_ds() + reports = cls.query_ds() + # Set display names for both traits, as applicable for report in reports: - # Set CaeNDR and display names for trait 1, as applicable if report['trait_1']: - report['trait_1_name_caendr'] = report['trait_1']['trait_name_caendr'] - if report['trait_1'].display_name[0]: - report['trait_1_name_display'] = list(report['trait_1'].display_name) - else: - report['trait_1_name_display'] = [ report['trait_1_name'] ] + report['trait_1_name_display'] = cls.compute_display_name(report['trait_1'], report['trait_1_name_caendr']) - # Set CaeNDR and display names for trait 2, as applicable if report['trait_2']: - report['trait_2_name_caendr'] = report['trait_2']['trait_name_caendr'] - if report['trait_2'].display_name[0]: - report['trait_2_name_display'] = list(report['trait_2'].display_name) - else: - report['trait_2_name_display'] = [ report['trait_2_name'] ] + report['trait_2_name_display'] = cls.compute_display_name(report['trait_2'], report['trait_2_name_caendr']) # Save the report report.save() diff --git a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py index 08c5ca9c..dffdbc7d 100644 --- a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py +++ b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py @@ -52,18 +52,20 @@ def parse(cls, data, valid_file_extensions=None): # Construct the first trait object and add relevant fields to props trait_1 = Trait(dataset=data['trait_1_dataset'], trait_name=data['trait_1']) - props['trait_1'] = trait_1.file - props['trait_1_name'] = trait_1.name + props['trait_1'] = trait_1.file + props['trait_1_name_caendr'] = trait_1.name + props['trait_1_name_display'] = PhenotypeReport.compute_display_name(trait_1) # If a second trait file is provided, construct trait object if (data.get('trait_2')): trait_2 = Trait(dataset=data['trait_2_dataset'], trait_name=data['trait_2']) - props['trait_2'] = trait_2.file - props['trait_2_name'] = trait_2.name + props['trait_2'] = trait_2.file + props['trait_2_name_caendr'] = trait_2.name + props['trait_2_name_display'] = PhenotypeReport.compute_display_name(trait_2) # Compute hash from unique trait names # Sort before combining, so either order will produce the same hash - hash_source = ' '.join(sorted([props.get('trait_1_name', trait_1.name), props.get('trait_2_name', trait_2.name)])) + hash_source = ' '.join(sorted([props.get('trait_1_name_caendr', trait_1.name), props.get('trait_2_name_caendr', trait_2.name)])) # Check that both traits have the same species # The front-end interface should prevent this, but if a job is somehow submitted with From 40a8dcf0895cec073824bd40670ee6137be5b1e0 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:10:15 -0600 Subject: [PATCH 21/50] Render report axis labels with styling (bold, italics, etc) --- src/modules/site-v2/base/static/js/plot.js | 4 ++-- .../site-v2/templates/_includes/macros.html | 5 ----- .../site-v2/templates/_includes/select-trait.html | 7 +++++++ .../tools/phenotype_database/report.html | 15 +++++++-------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/modules/site-v2/base/static/js/plot.js b/src/modules/site-v2/base/static/js/plot.js index 1ee6c8a7..5facb578 100644 --- a/src/modules/site-v2/base/static/js/plot.js +++ b/src/modules/site-v2/base/static/js/plot.js @@ -202,7 +202,7 @@ function render_scatterplot_histograms(container_selector, data, config={}) { .attr('x', margin.left + (width / 2)) .attr('y', height + margin.top + hist_height + 36) .attr('text-anchor', 'middle') - .text(config['x_label']) + .html(config['x_label']) } // Add label for y-axis, if one is provided @@ -214,7 +214,7 @@ function render_scatterplot_histograms(container_selector, data, config={}) { .attr('y', 0) .attr('dy', '.75em') .attr('text-anchor', 'middle') - .text(config['y_label']) + .html(config['y_label']) } diff --git a/src/modules/site-v2/templates/_includes/macros.html b/src/modules/site-v2/templates/_includes/macros.html index 08d65cc9..d7b3909a 100755 --- a/src/modules/site-v2/templates/_includes/macros.html +++ b/src/modules/site-v2/templates/_includes/macros.html @@ -167,8 +167,3 @@ Unable to retrieve files. {%- endif %} {%- endmacro %} - - -{%- macro label_string_units(name, units=null) %} -{%- if units -%} "{{ name }} ({{ units }})" {%- else -%} "{{ name }}" {%- endif -%} -{%- endmacro %} diff --git a/src/modules/site-v2/templates/_includes/select-trait.html b/src/modules/site-v2/templates/_includes/select-trait.html index 5be87b9d..816b4908 100644 --- a/src/modules/site-v2/templates/_includes/select-trait.html +++ b/src/modules/site-v2/templates/_includes/select-trait.html @@ -97,3 +97,10 @@

Short Description:

{%- macro render_trait_name_string(name_1, name_2, name_3) -%} {{ name_1 }}{% if name_2 %}, {{ name_2 }}{% endif %}{% if name_3 %}, {{ name_3 }}{% endif %} {%- endmacro %} + +{%- macro render_trait_name_label(name_1, name_2, name_3, units=none) -%} + {{ name_1 }} + {%- if name_2 %}, {{ name_2 }}{% endif -%} + {%- if name_3 %}, {{ name_3 }}{% endif -%} + {%- if units %} ({{ units }}){%- endif -%} +{%- endmacro %} diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 1aa099c1..a4115c56 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -164,8 +164,7 @@

Description:

{% block script %} -{% from "_includes/macros.html" import label_string_units %} -{% from "_includes/select-trait.html" import render_trait_name_string %} +{% from "_includes/select-trait.html" import render_trait_name_label %} @@ -185,8 +184,8 @@

Description:

{%- if data.num_traits == 1 %} // Render chart(s) for one trait -{%- set trait_display_name = report['trait_1'].display_name %} -const trait_label = {{ label_string_units( render_trait_name_string(trait_display_name[0], trait_display_name[1], trait_display_name[2]), report['trait_1']['units']) }}; +{%- set trait_display_name = report['trait_1_name_display'] %} +const trait_label = `{{ render_trait_name_label( trait_display_name[0], trait_display_name[1], trait_display_name[2], report['trait_1']['units'] ) }}`; try { render_histogram('#phenotype-chart-histogram', data, { @@ -212,10 +211,10 @@

Description:

{%- else %} // Render chart(s) for two traits -{%- set trait_1_display_name = report['trait_1'].display_name %} -{%- set trait_2_display_name = report['trait_2'].display_name %} -const trait_label_1 = {{ label_string_units( render_trait_name_string(trait_1_display_name[0], trait_1_display_name[1], trait_1_display_name[2]), report['trait_1']['units']) }}; -const trait_label_2 = {{ label_string_units( render_trait_name_string(trait_2_display_name[0], trait_2_display_name[1], trait_2_display_name[2]), report['trait_2']['units']) }}; +{%- set trait_1_display_name = report['trait_1_name_display'] %} +{%- set trait_2_display_name = report['trait_2_name_display'] %} +const trait_label_1 = `{{ render_trait_name_label( trait_1_display_name[0], trait_1_display_name[1], trait_1_display_name[2], report['trait_1']['units'] ) }}`; +const trait_label_2 = `{{ render_trait_name_label( trait_2_display_name[0], trait_2_display_name[1], trait_2_display_name[2], report['trait_2']['units'] ) }}`; try { render_scatterplot_histograms('#phenotype-chart', data, { From 3c7d1e36b44de6e69cedb28ff90bc0cba3910892 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:21:59 -0600 Subject: [PATCH 22/50] Add DbOp for recomputing cached phenotype report display names --- .vscode/launch.json | 18 ++++++++++++++++++ src/modules/db_operations/operations.py | 5 ++++- .../caendr/models/sql/database_operation.py | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c8398905..f0a5ea62 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -370,6 +370,24 @@ "SITE_BASE_URL": "https://localhost:8080", } }, + { + "name": "DbOp: Recompute Phenotype Report Cached Names", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/src/modules/db_operations/main.py", + "console": "integratedTerminal", + "python": "${workspaceFolder}/src/modules/db_operations/venv/bin/python", + "justMyCode": true, + "envFile": "${workspaceFolder}/src/modules/db_operations/.env", + "env": { + "DATABASE_OPERATION": "RECOMPUTE_PHENOTYPE_REPORT_CACHED_NAMES", + "RELOAD_FILES": "false", + "MODULE_DB_TIMEOUT": "300", + "CAENDR_USE_SSL": "1", + "SENTRY_URL": "", + "SITE_BASE_URL": "https://localhost:8080", + } + }, { "name": "Test Indel Finder", "type": "python", diff --git a/src/modules/db_operations/operations.py b/src/modules/db_operations/operations.py index cf1d4694..08be6307 100644 --- a/src/modules/db_operations/operations.py +++ b/src/modules/db_operations/operations.py @@ -2,7 +2,7 @@ from caendr.services.cloud.postgresql import health_database_status from caendr.services.logger import logger -from caendr.models.datastore import Species +from caendr.models.datastore import Species, PhenotypeReport from caendr.models.sql import DbOp, WormbaseGene, WormbaseGeneSummary, Strain, StrainAnnotatedVariant, PhenotypeDatabase, PhenotypeMetadata from caendr.services.sql.db import backup_external_db from caendr.services.sql.etl import ETLManager @@ -49,6 +49,9 @@ def execute_operation(app, db, db_op: DbOp, species=None, reload_files=True): logger.info("Using MOCK DATA") drop_and_populate_all_tables(app, db, species) + elif db_op == DbOp.RECOMPUTE_PHENOTYPE_REPORT_CACHED_NAMES: + PhenotypeReport.recompute_cached_display_names() + def drop_and_populate_strains(app, db, species, reload_files=True): diff --git a/src/pkg/caendr/caendr/models/sql/database_operation.py b/src/pkg/caendr/caendr/models/sql/database_operation.py index 3f36a579..12b3b70b 100644 --- a/src/pkg/caendr/caendr/models/sql/database_operation.py +++ b/src/pkg/caendr/caendr/models/sql/database_operation.py @@ -16,6 +16,7 @@ class DbOp(Enum): POPULATE_PHENOTYPES_DATASTORE = 'POPULATE_PHENOTYPES_DATASTORE' TEST_ECHO = 'TEST_ECHO' TEST_MOCK_DATA = 'TEST_MOCK_DATA' + RECOMPUTE_PHENOTYPE_REPORT_CACHED_NAMES = 'RECOMPUTE_PHENOTYPE_REPORT_CACHED_NAMES' def get_title(op): @@ -37,6 +38,7 @@ def get_title(op): DbOp.POPULATE_PHENOTYPES_DATASTORE: 'Create / update datastore trait file records from Google Sheet', DbOp.TEST_ECHO: 'Test ETL - Echo', DbOp.TEST_MOCK_DATA: 'Test ETL - Mock Data', + DbOp.RECOMPUTE_PHENOTYPE_REPORT_CACHED_NAMES: 'Recompute cached trait display names in Phenotype Report entities' } return titles.get(op, '???') From 6577a834a81625be0ec67e71b643231076a9f0db Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:22:46 -0600 Subject: [PATCH 23/50] Set full CaeNDR trait name as display name for Zhang traits --- src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py b/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py index 4a6e8628..a5c240be 100644 --- a/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py +++ b/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py @@ -40,7 +40,7 @@ def parse_phenotype_metadata(species: Species, **files: LocalDatastoreFile): yield { 'trait_name_caendr': trait_name, 'trait_name_user': md['trait_name_user'], - 'trait_name_display_1': '', + 'trait_name_display_1': trait_name, 'trait_name_display_2': '', 'trait_name_display_3': '', 'species_name': md.species.name, From bb48389eb9c56d996fd0dd7e1297c21a4031bfd5 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:24:49 -0600 Subject: [PATCH 24/50] Use trait display names in offcanvas header --- .../_includes/phenotype-database-table.html | 12 ++++++++++-- .../tools/phenotype_database/submit-traits.html | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index 0d500419..86453876 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -104,7 +104,13 @@
-

+
+

+ + + +

+
@@ -414,7 +420,9 @@

Full Description

} function fillOutModal(trait) { - $('#offcanvas1Label').text(trait.trait_name_caendr) + $('#offcanvas1-header-1').text(trait.trait_name_display_1) + $('#offcanvas1-header-2').text(trait.trait_name_display_2) + $('#offcanvas1-header-3').text(trait.trait_name_display_3) manageVal(trait.capture_date, '#captureDate', true) manageVal(trait.created_on, '#createdOn', true) manageVal(trait.modified_on, '#modifiedOn', true) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html index 5615d544..4711e930 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html @@ -46,7 +46,13 @@
-
example_trait_1234
+
+

+ + + +

+
From 315f0dbb2868bb3a079857e90722e8b24b3d707a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:28:08 -0600 Subject: [PATCH 25/50] Fix: render axis labels as HTML in single-trait report --- src/modules/site-v2/base/static/js/plot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/site-v2/base/static/js/plot.js b/src/modules/site-v2/base/static/js/plot.js index 5facb578..8daa4506 100644 --- a/src/modules/site-v2/base/static/js/plot.js +++ b/src/modules/site-v2/base/static/js/plot.js @@ -346,7 +346,7 @@ function render_histogram(container_selector, data, config={}) { .attr('x', margin.left + (width / 2)) .attr('y', height + margin.top + 36) .attr('text-anchor', 'middle') - .text(config['x_label']) + .html(config['x_label']) } // Add the histogram @@ -497,7 +497,7 @@ function render_ranked_barplot(container_selector, data, config={}) { .attr('y', 0) .attr('dy', '.75em') .attr('text-anchor', 'middle') - .text(config['y_label']) + .html(config['y_label']) } //Render tooltips setTooltips(); From 18e1538586dedc37442e13d5838588ffb6c0de0b Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 17:40:51 -0600 Subject: [PATCH 26/50] Fix: move display name computation to Trait & use to initialize Zhang trait in selector properly --- .../site-v2/templates/_includes/select-trait.html | 2 +- .../caendr/models/datastore/phenotype_report.py | 4 ++-- src/pkg/caendr/caendr/models/trait.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/select-trait.html b/src/modules/site-v2/templates/_includes/select-trait.html index 816b4908..a71c16df 100644 --- a/src/modules/site-v2/templates/_includes/select-trait.html +++ b/src/modules/site-v2/templates/_includes/select-trait.html @@ -64,7 +64,7 @@

Short Description:

document.getElementById("{{id}}-selector").dataset.trait = "{{trait_obj.name}}"; document.getElementById("{{id}}-selector").dataset.set = "{{trait_obj.dataset}}"; document.getElementById("{{id}}-species").innerText = "{{trait_obj.file.species.short_name}}" - document.getElementById("{{id}}-trait-name").innerHTML = `{{ render_trait_name_multiline(trait_obj.file['trait_name_display_1'], trait_obj.file['trait_name_display_2'], trait_obj.file['trait_name_display_3']) }}`; + document.getElementById("{{id}}-trait-name").innerHTML = `{{ render_trait_name_multiline(trait_obj.display_name[0], trait_obj.display_name[1], trait_obj.display_name[2]) }}`; document.getElementById("{{id}}-trait-desc").innerText = "{{trait_obj.file.description_short or trait_obj.file.description_long}}"; {%- else %} document.getElementById("{{id}}-selector").dataset.trait = {{trait_var}}.trait_name_caendr; diff --git a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py index ebe1cd84..f2cd1924 100644 --- a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py +++ b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py @@ -285,8 +285,8 @@ def compute_display_name(cls, trait: Union[Trait, TraitFile], trait_name: Option elif not isinstance(trait, Trait): raise ValueError() - # For bulk files, store the single trait name, otherwise convert the display_name fields to a list - return [trait.name] if trait.file['is_bulk_file'] else list(trait.file.display_name) + # Use the Trait object to compute the display name + return list(trait.display_name) @classmethod diff --git a/src/pkg/caendr/caendr/models/trait.py b/src/pkg/caendr/caendr/models/trait.py index f82ac764..4a351c63 100644 --- a/src/pkg/caendr/caendr/models/trait.py +++ b/src/pkg/caendr/caendr/models/trait.py @@ -114,3 +114,17 @@ def dataframe_to_dict(cls, df): mapping strain name to measured trait val. ''' return dataframe_cols_to_dict(df, 'strain_name', 'trait_value', drop_na=True) + + + # + # Computing Display Name + # + + @property + def display_name(self): + ''' + Compute the display name for this trait. + ''' + + # For bulk files, store the single trait name, otherwise convert the display_name fields to a list + return (self.name,) if self.file['is_bulk_file'] else self.file.display_name \ No newline at end of file From 6e1f4e1a2196708a5db6dc21db2df08e64739b8c Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 13 Mar 2024 10:07:57 -0500 Subject: [PATCH 27/50] Set expiration policy for subscription to never --- tf/caendr/modules/api/pipeline_task/pub-sub.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tf/caendr/modules/api/pipeline_task/pub-sub.tf b/tf/caendr/modules/api/pipeline_task/pub-sub.tf index 3d19ce30..c25bde69 100644 --- a/tf/caendr/modules/api/pipeline_task/pub-sub.tf +++ b/tf/caendr/modules/api/pipeline_task/pub-sub.tf @@ -24,7 +24,7 @@ resource "google_pubsub_subscription" "pipeline_task" { enable_message_ordering = "false" expiration_policy { - ttl = "2678400s" + ttl = "" } retry_policy { From c7f0734a19f1991c8c0780c0030ce867c002408b Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Mon, 19 Feb 2024 15:33:07 -0600 Subject: [PATCH 28/50] Move strain labels to first element of result tuples This matches the intended CSV/TSV format, where the strain name is the first column, and the phenotype trait measurements are subsequent columns --- src/modules/site-v2/base/static/js/plot.js | 39 ++++++++++++------- .../tools/phenotype_database/report.html | 16 ++++++++ .../models/job_pipeline/phenotype_pipeline.py | 4 +- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/modules/site-v2/base/static/js/plot.js b/src/modules/site-v2/base/static/js/plot.js index 8daa4506..a9c0ee97 100644 --- a/src/modules/site-v2/base/static/js/plot.js +++ b/src/modules/site-v2/base/static/js/plot.js @@ -40,6 +40,7 @@ function buffered_extent(data, buffer, f=null) { * - d (int): The dimension of the histogram. 0 for x-axis, 1 for y-axis * - axis: A d3 scaleLinear object mapping the domain of the given axis to its position in the graph * - data: The data to plot + * Expects an array with the label as the first element, and data value(s) as subsequent element(s). * - target: The SVG element to create the histogram in * - config: Optional keyword arguments. */ @@ -79,7 +80,7 @@ function add_histogram_along_axis(d, axis, data, target, config={}) { const bins = d3.histogram() .domain(axis.domain()) .thresholds(axis.ticks(num_bins)) - .value(n => n[d]) + .value(n => n[d + 1]) (data); // Compute the size of the bin with the max value @@ -129,6 +130,7 @@ function add_histogram_along_axis(d, axis, data, target, config={}) { * Arguments: * - container_selector: A selector for the element to create the graph in. * - data: The data to graph. + * Expects an array with the label as the first element, and data value(s) as subsequent element(s). * - config (dict): A set of optional parameters. Values include: * 'margin' * 'hist_height' @@ -183,8 +185,8 @@ function render_scatterplot_histograms(container_selector, data, config={}) { // The buffer has to be at least 0.1 bc of a quirk of the bin widths: // the target width is 0.2, but the axis will round its ticks to the nearest 0.5, // meaning there may be a gap of 0.1 on either end of either axis - const x = d3.scaleLinear().domain(buffered_extent(data, 0.1, n => n[0])).range([0, width]).nice(); - const y = d3.scaleLinear().domain(buffered_extent(data, 0.1, n => n[1])).range([height, 0]).nice(); + const x = d3.scaleLinear().domain(buffered_extent(data, 0.1, n => n[1])).range([0, width]).nice(); + const y = d3.scaleLinear().domain(buffered_extent(data, 0.1, n => n[2])).range([height, 0]).nice(); // Add the x-axis g.append("g") @@ -237,8 +239,8 @@ function render_scatterplot_histograms(container_selector, data, config={}) { .data(data) .enter() .append("circle") - .attr("cx", d => x(d[0]) ) - .attr("cy", d => y(d[1]) ) + .attr("cx", d => x(d[1]) ) + .attr("cy", d => y(d[2]) ) .attr("r", circle_radius) .attr("tabindex", "0") .style('fill', "#0719BC") @@ -293,6 +295,7 @@ function render_scatterplot_histograms(container_selector, data, config={}) { * Arguments: * - container_selector: A selector for the element to create the graph in. * - data: The data to graph. + * Expects an array with the label as the first element, and data value(s) as subsequent element(s). * - config (dict): A set of optional parameters. Values include: * 'margin' * 'width' @@ -333,7 +336,7 @@ function render_histogram(container_selector, data, config={}) { .attr("transform", `translate(${ margin.left }, ${ margin.top })`); // Create mapping functions from data set to coordinates in the scatterplot - const x = d3.scaleLinear().domain(d3.extent(data, n => n[0])).range([0, width]).nice(); + const x = d3.scaleLinear().domain(d3.extent(data, n => n[1])).range([0, width]).nice(); // Add the x-axis g.append("g") @@ -380,6 +383,7 @@ function render_histogram(container_selector, data, config={}) { * Arguments: * - container_selector: A selector for the element to create the graph in. * - data: The data to graph. + * Expects an array with the label as the first element, and data value(s) as subsequent element(s). * - config (dict): A set of optional parameters. Values include: * 'margin' * 'width' @@ -407,15 +411,21 @@ function render_ranked_barplot(container_selector, data, config={}) { // Read values from config, filling in default values when not supplied const fill_color = config['fill_color'] || 'black'; + // Create a template function for the tooltip + // By default, show label & value for each axis + const tooltip_id = config['tooltip_id'] || null; + const tooltip_template = config['tooltip_template'] || ((d) => ` +

${d[0]}: ${d[1]}

+ `); // Sort data data.sort(function(b, a) { - return b[0] - a[0]; + return b[1] - a[1]; }); // Compute the range of the data // If both values are positive or both negative, set end of range to 0 - const data_range = d3.extent(data, d => d[0]); + const data_range = d3.extent(data, d => d[1]); data_range[0] = Math.min(data_range[0], 0) data_range[1] = Math.max(data_range[1], 0) @@ -434,7 +444,7 @@ function render_ranked_barplot(container_selector, data, config={}) { // Create X axis (map strain name to x coordinate) const xScale = d3.scaleBand() - .domain(data.map( (d) => d[1] )) + .domain(data.map( (d) => d[0] )) .range([ margin.left, margin.left + width ]) .padding(0.05) const xAxis = d3.axisBottom(xScale) @@ -458,12 +468,11 @@ function render_ranked_barplot(container_selector, data, config={}) { .data(data) .enter() .append("rect") - .attr("x", (d) => xScale(d[1])) - .attr("y", (d) => yScale(Math.max(d[0], 0))) - .attr("width", xScale.bandwidth()) - .attr("height", (d) => bar_height(Math.abs(d[0]))) - .attr("fill", fill_color) - .attr("tabindex", "0") + .attr("x", (d) => xScale( d[0] ) ) + .attr("y", (d) => yScale( Math.max(d[1], 0) ) ) + .attr("width", xScale.bandwidth()) + .attr("height", (d) => bar_height( Math.abs(d[1]) )) + .attr("fill", fill_color) // Create tooltips for bars function setTooltips() { diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index a4115c56..45d2ae0a 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -193,6 +193,11 @@

Description:

fill_color: '#0719BC', bins_per_tick: 2, x_label: trait_label, + tooltip_id: 'tooltip-single-histogram', + tooltip_template: (d, labels) => ` +

Strain: ${d[0]}

+

${labels[0]}: ${d[1]}

+ `, }); } catch (err) { console.error(`Could not construct histogram. ${err}`) @@ -203,6 +208,11 @@

Description:

width: 1200, fill_color: '#0719BC', y_label: trait_label, + tooltip_id: 'tooltip-single-barplot', + tooltip_template: (d) => ` +

${d[0]}

+

Value = ${d[1].toFixed(4)}

+ `, }); } catch (err) { console.error(`Could not construct ranked bar plot. ${err}`) @@ -227,6 +237,12 @@

Description:

y_label: `${trait_label_2}`, opacity: 0.5, opacity_hover: 1, + tooltip_id: 'tooltip-double-scatterplot', + tooltip_template: (d, labels) => ` +

${d[0]}

+

x = ${d[1].toFixed(4)}

+

y = ${d[2].toFixed(4)}

+ `, }); } catch (err) { console.error(`Could not construct scatterplot. ${err}`) diff --git a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py index dffdbc7d..d0adbef0 100644 --- a/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py +++ b/src/pkg/caendr/caendr/models/job_pipeline/phenotype_pipeline.py @@ -137,8 +137,8 @@ def _parse_output(self, data): center_and_scale_data(d) for d in data_vals ) - # Zip the trait values together with the strain names, to get the full dataset array - data_tuples = list(zip( *data_vals, data_keys )) + # Zip the strain names together with the trait values, to get the full dataset array + data_tuples = list(zip( data_keys, *data_vals )) # Compute the Spearman Coefficient for the given data, if two traits are being compared if len(data_vals) == 2: From 0223e864fc29248cfefe5136d11d5617dc984175 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Mon, 19 Feb 2024 15:45:24 -0600 Subject: [PATCH 29/50] Add TSV download route for phenotype report --- .../base/views/tools/phenotype_database.py | 27 ++++++++++++++-- .../tools/phenotype_database/report.html | 32 +++++-------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/modules/site-v2/base/views/tools/phenotype_database.py b/src/modules/site-v2/base/views/tools/phenotype_database.py index 9b8b9c88..256be459 100644 --- a/src/modules/site-v2/base/views/tools/phenotype_database.py +++ b/src/modules/site-v2/base/views/tools/phenotype_database.py @@ -7,6 +7,7 @@ jsonify, flash, abort, + Response, Blueprint) from extensions import cache, compress from sqlalchemy import or_, func @@ -27,6 +28,7 @@ from caendr.models.status import JobStatus from caendr.models.sql import PhenotypeMetadata from caendr.models.trait import Trait +from caendr.utils.data import get_file_format, convert_data_to_download_file @@ -290,9 +292,18 @@ def list_results(): }) -@phenotype_database_bp.route("/report/", methods=['GET']) +@phenotype_database_bp.route("/report/", methods=['GET']) +@phenotype_database_bp.route("/report//download/", methods=['GET']) @jwt_required() -def report(id): +def report(id, file_ext=None): + + # Validate file extension, if provided + if file_ext: + file_format = get_file_format(file_ext, valid_formats={'tsv'}) + if file_format is None: + abort(404) + else: + file_format = None # Fetch requested phenotype report # Ensures the report exists and the user has permission to view it @@ -330,6 +341,18 @@ def report(id): logger.error(f'Error fetching Phenotype report {id}: Input data does not exist') return abort(404) + # If a file format was specified, return a downloadable file with the results + if file_format is not None: + columns = ['strain', *data['trait_names']] + values = sorted(result['trait_values'], key=lambda v: v[0]) + resp = Response(convert_data_to_download_file( values, columns, file_ext=file_ext ), mimetype=file_format['mimetype']) + try: + resp.headers['Content-Disposition'] = f'filename={job.report["species"]}_{"_".join(job.report.trait_names)}.{file_ext}' + except: + resp.headers['Content-Disposition'] = f'filename={job.report.id}.{file_ext}' + return resp + + # Otherwise, return view page return render_template('tools/phenotype_database/report.html', **{ 'title': 'Phenotype Analysis Report', 'tool_alt_parent_breadcrumb': {"title": "Tools", "url": url_for('tools.tools')}, diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 45d2ae0a..c4a2b944 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -90,30 +90,14 @@

Description:

Submit to Phenotype Database?
-->
- {%- if data.num_traits > 1 %} -
-

Trait 2

-

Species:

-

{{ report['species']|replace('_', '. ')}} -

Name:

-

{{ render_trait_name_multiline(report['trait_2_name_display'][0], report['trait_2_name_display'][1], report['trait_2_name_display'][2]) }}

-

Description:

-

{{ report['trait_2']['description_short'] or report['trait_2']['description_long'] }}

- - -
- {%- endif %} - - + +
From 02b438dd9e2662e763edc1be29ad8f7752971d9a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Mon, 19 Feb 2024 16:05:11 -0600 Subject: [PATCH 30/50] Add code to download SVGs --- .../site-v2/templates/_scripts/utils.js | 22 +++++++++++++++---- .../tools/phenotype_database/report.html | 19 +++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/modules/site-v2/templates/_scripts/utils.js b/src/modules/site-v2/templates/_scripts/utils.js index 5af2bbeb..7b8ad66d 100644 --- a/src/modules/site-v2/templates/_scripts/utils.js +++ b/src/modules/site-v2/templates/_scripts/utils.js @@ -92,10 +92,7 @@ function force_download(e) { // If everything succeeds, all future clicks will use the modified href // in the existing anchor element. If something fails, all clicks should // restart the blob download process, which is is acceptable. - const a = document.createElement('a'); - a.href = el.href; - a.download = el.download || 'download'; - a.click(); + download_from_href(el.href, el.download || 'download'); }) } @@ -108,6 +105,23 @@ function create_node(html) { } +function save_svg(selector, filename=null) { + const svg_el = document.querySelector(selector); + const data = (new XMLSerializer()).serializeToString(svg_el); + const svg_blob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); + const url = URL.createObjectURL(svg_blob); + download_from_href(url, filename || 'download.svg'); +} + + +function download_from_href(href, filename) { + const a = document.createElement('a'); + a.href = href; + a.download = filename || 'download'; + a.click(); +} + + /* Flash a message to the user without reloading the page. * This function is roundabout to prevent code injection. */ diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index c4a2b944..39e357f3 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -93,7 +93,7 @@

Description:

- - + --> + From a9fbb092c06aa89c42a33ee7afe207d4186fed4a Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 18:38:07 -0600 Subject: [PATCH 32/50] Connect SVG download buttons to download function --- .../tools/phenotype_database/report.html | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 0f168cd7..b90d9bc2 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -118,7 +118,9 @@

Description:

{%- if data.num_traits > 1 %}

Correlation: {{ result.correlation|round(4) }}

@@ -129,11 +131,15 @@

Description:

{%- if data.num_traits == 1 %}
{%- endif %}
@@ -164,16 +170,8 @@

Description:

From 595a4abab3856507920e3b68f77f96aa7e8e3d13 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 18:40:23 -0600 Subject: [PATCH 33/50] Fix indentation --- .../tools/phenotype_database/report.html | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index b90d9bc2..870f232b 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -110,11 +110,11 @@

Description:

-
-
- -
+
+
+ +
{%- if data.num_traits > 1 %}
@@ -131,20 +131,20 @@

Description:

{%- if data.num_traits == 1 %}
{%- endif %} +
-
From cbe9c19d9e854725bb7ff46e4333d820a29fab19 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Thu, 29 Feb 2024 19:31:02 -0600 Subject: [PATCH 34/50] Add download as PDF button --- .../tools/phenotype_database/report.html | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 870f232b..971ea5c3 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -5,6 +5,9 @@ + + + {% endblock %} @@ -68,7 +84,7 @@ {% from "_includes/select-trait.html" import render_trait_name_multiline %}
-
+

Trait Information

@@ -90,20 +106,39 @@

Description:

Submit to Phenotype Database?
-->
- -
-
- Download SVG - Download TSV + {%- if data.num_traits > 1 %} +
+

Trait 2

+

Species:

+

{{ report['species']|replace('_', '. ')}} +

Name:

+

{{ report['trait_2_name'] }}

+

Description:

+

{{ report['trait_2']['description_short'] or report['trait_2']['description_long'] }}

+ + +
+ {%- endif %} + +
-
+
-
+
@@ -117,7 +152,7 @@

Description:

{%- if data.num_traits > 1 %} -
+
@@ -130,13 +165,13 @@

Description:

{%- endif %} {%- if data.num_traits == 1 %}
-
+
-
+
@@ -173,6 +208,10 @@

Description:

function download_chart_as_svg(chart_id) { save_svg(`#${chart_id} svg`, `${chart_id}_{{ report.id }}`); } + +function download_report_as_pdf() { + $("#report-print-container").print() +} From 39cd76f1eaa77cb5ed88166870e074328400ad84 Mon Sep 17 00:00:00 2001 From: natalieroman Date: Fri, 1 Mar 2024 09:45:59 -0600 Subject: [PATCH 35/50] Changed PDF icon and changed copy --- .../site-v2/templates/tools/phenotype_database/report.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index 971ea5c3..b91716e1 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -130,7 +130,7 @@

Description:

Download TSV Download PDF + > Print Report
From 7eee7cce08500dc4ae0c47959992f0315a1da0c3 Mon Sep 17 00:00:00 2001 From: natalieroman Date: Fri, 1 Mar 2024 09:48:13 -0600 Subject: [PATCH 36/50] Removed custom no-print css classes and replaced with Bootstrap no print classes --- .../templates/tools/phenotype_database/report.html | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index b91716e1..c769b002 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -65,9 +65,6 @@ @media print { - .no-print, .no-print * { - display: none !important; - } {%- if data.num_traits == 1 %} #tool-column { @@ -125,7 +122,7 @@

Description:

{%- endif %} -
+
Download TSV @@ -138,7 +135,7 @@

Description:

-
+
@@ -165,13 +162,13 @@

Description:

{%- endif %} {%- if data.num_traits == 1 %}
-
+
-
+
From ce9a2bfeb05b0f27105fa5d1a3f6849a487225ae Mon Sep 17 00:00:00 2001 From: natalieroman Date: Fri, 1 Mar 2024 09:51:10 -0600 Subject: [PATCH 37/50] Fixed height of chart container to show ranked barplot "download svg" button --- .../site-v2/templates/tools/phenotype_database/report.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/report.html b/src/modules/site-v2/templates/tools/phenotype_database/report.html index c769b002..2369a21b 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/report.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/report.html @@ -54,7 +54,7 @@ @media screen and (min-width: 768px) { #chartContainer { - min-height:115vh; + min-height:125vh; } #phenotype-chart svg { From 15e155369d1b3750279ec5e3c93fa7a0487907d8 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 10 Apr 2024 13:56:39 -0500 Subject: [PATCH 38/50] Check Google Sheet columns before parsing rows --- src/modules/db_operations/seed_trait_files.py | 41 +++++++++++++++++-- src/pkg/caendr/caendr/models/error.py | 4 ++ .../caendr/caendr/services/cloud/sheets.py | 13 ++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/modules/db_operations/seed_trait_files.py b/src/modules/db_operations/seed_trait_files.py index d9933b6e..c1b32ed3 100644 --- a/src/modules/db_operations/seed_trait_files.py +++ b/src/modules/db_operations/seed_trait_files.py @@ -2,9 +2,9 @@ from caendr.services.cloud.secret import get_secret from caendr.models.datastore import TraitFile -from caendr.models.error import NotFoundError, NonUniqueEntity +from caendr.models.error import NotFoundError, NonUniqueEntity, GoogleSheetsParseError from caendr.models.status import PublishStatus -from caendr.services.cloud.sheets import get_field_from_record +from caendr.services.cloud.sheets import check_missing_columns, get_field_from_record from caendr.utils.data import unique_id from caendr.utils.local_files import LocalGoogleSheet @@ -13,6 +13,31 @@ ANDERSEN_LAB_TRAIT_SHEET = get_secret(f'ANDERSEN_LAB_TRAIT_SHEET') +# Set of column headers that must exist in the sheet +# NOTE: Not every entry needs to define all of these fields -- these are just the columns that should be available in the sheet. +REQUIRED_SHEET_HEADERS = frozenset({ + 'Trait_Name_CaeNDR', + 'Trait_Name_User', + 'Species', + 'Trait_Name_Display1', + 'Trait_Name_Display2', + 'Trait_Name_Display3', + 'Short_Description', + 'Long_Description', + 'Units', + 'Publication', + 'Protocol', + 'Institution', + 'Captured_By_UserID', + 'Capture_Date', + + # TODO: How should we handle category tags? + # 'Category1', + # 'Category2', + # 'Category3', +}) + + def populate_andersenlab_trait_files(): ''' @@ -27,7 +52,17 @@ def populate_andersenlab_trait_files(): # Fetch the Google Sheet and loop through all records trait_sheet = LocalGoogleSheet( 'TRAITS', ANDERSEN_LAB_TRAIT_SHEET ) - for record in trait_sheet.fetch_resource().get_all_records(): + resource = trait_sheet.fetch_resource() + + # Make sure the Google Sheet has all the required columns + missing_columns = check_missing_columns(resource, REQUIRED_SHEET_HEADERS) + if len(missing_columns): + msg = f'{trait_sheet} (ID: {trait_sheet._sheet_id}) is missing columns: [{", ".join(missing_columns)}]' + logger.error(msg) + raise GoogleSheetsParseError(msg) + + # Loop through rows in the sheet + for record in resource.get_all_records(): # Require that unique CaeNDR trait name is defined trait_unique_name = record.get('Trait_Name_CaeNDR') diff --git a/src/pkg/caendr/caendr/models/error.py b/src/pkg/caendr/caendr/models/error.py index 30b1ad8f..b6d65ffe 100755 --- a/src/pkg/caendr/caendr/models/error.py +++ b/src/pkg/caendr/caendr/models/error.py @@ -160,6 +160,10 @@ def __init__(self, missing_files: list): class GoogleSheetsParseError(InternalError): description = "Unable to parse Google Sheets document" + def __init__(self, description = None): + if description is not None: + self.description = f'{self.description}: {description}' + super().__init__() class ExternalMarkdownRenderError(InternalError): def __init__(self, url, src): diff --git a/src/pkg/caendr/caendr/services/cloud/sheets.py b/src/pkg/caendr/caendr/services/cloud/sheets.py index 984421e0..d5136d2e 100755 --- a/src/pkg/caendr/caendr/services/cloud/sheets.py +++ b/src/pkg/caendr/caendr/services/cloud/sheets.py @@ -4,6 +4,7 @@ import pandas as pd import requests import datetime +from typing import Iterable from io import StringIO from oauth2client.service_account import ServiceAccountCredentials @@ -103,3 +104,15 @@ def get_field_from_record(record, key, fallback=None, nullable=True, null_values elif val is None: raise ValueError() return val + + +def check_missing_columns(sheet: gspread.Worksheet, required_columns: Iterable[str]): + ''' + Given an iterable of column headers, return any headers that don't exist in the given sheet. + ''' + + # NOTE: row_values function is one-indexed! + header_row = sheet.row_values(1) + + # Compute the set of required columns that are not in the header row + return frozenset(filter(lambda col: col not in header_row, required_columns)) From 6ba326d2564e699053194ef4c56248de89b4bc32 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 10 Apr 2024 13:58:27 -0500 Subject: [PATCH 39/50] Fix: trait lookup request data --- src/modules/site-v2/templates/_scripts/trait_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/_scripts/trait_utils.js b/src/modules/site-v2/templates/_scripts/trait_utils.js index 8e51807f..9298c160 100644 --- a/src/modules/site-v2/templates/_scripts/trait_utils.js +++ b/src/modules/site-v2/templates/_scripts/trait_utils.js @@ -37,7 +37,7 @@ function queryTraitByName(trait_name, csrf_token=null) { return $.ajax({ type: "POST", url: "{{ url_for('phenotype_database.get_traits_json') }}", - data: JSON.stringify({trait_name}), + data: JSON.stringify(data), contentType: "application/json", dataType: "json", }); From 9080429a5d247e2235226b0676e93d9e059f1631 Mon Sep 17 00:00:00 2001 From: Vincent LaGrassa Date: Wed, 10 Apr 2024 14:05:16 -0500 Subject: [PATCH 40/50] Flash error message if trait metadata request fails --- .../tools/phenotype_database/submit-traits.html | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html index 4711e930..24205049 100644 --- a/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html +++ b/src/modules/site-v2/templates/tools/phenotype_database/submit-traits.html @@ -144,8 +144,18 @@ if (response !== null && typeof response === 'string') throw Error(`Invalid response for trait ${id}: "${response}"`); select_trait(id, response, dataset); }) - .fail((err) => { + .fail((error) => { + // If an error message was received, display it, otherwise use a fallback message console.error(error); + const resp = error.responseJSON + if (resp && resp.message && false) { + flash_message(resp.message, resp.full_msg_link, resp.full_msg_body); + } else{ + flash_message(`Failed to retrieve metadata for trait ${trait}`); + } + + // Close the modal so the user can see the error message + $('#phenotypeDBModal').modal('hide'); }) return } From daf59139a4500b1a46ccde07532b113e3ee0a0d8 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Thu, 9 May 2024 13:17:26 -0500 Subject: [PATCH 41/50] Fixing the POST request for trait details, adding off-canvas header ID --- .../_includes/phenotype-database-table.html | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index 19edecb2..db34cd16 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -105,14 +105,10 @@ aria-labelledby="offcanvas1Label">
-
+
-

- - - -

+

@@ -198,17 +194,10 @@

$('.offcanvas-header, .offcanvas-body').hide() $('#spinner').removeClass('d-none') const data = {csrf_token: $('#csrf_token').val(), trait_id: $(this).data('value')} - $.ajax({ - type: "POST", - url: "{{ url_for('phenotype_database.get_traits_json') }}", - data: JSON.stringify(data), - contentType: "application/json", - dataType: "json", - success: function(response) { - $('#spinner').addClass('d-none') - $('.offcanvas-header, .offcanvas-body').show() queryTraitByName($(this).data('value'), $('#csrf_token').val()) .then((response) => { + $('#spinner').addClass('d-none') + $('.offcanvas-header, .offcanvas-body').show() fillOutModal(response) }) .fail((error) => { @@ -222,7 +211,7 @@

flash_message(error.responseJSON.message) } }) - }) + }) @@ -442,6 +431,7 @@

Full Description

traitName = trait.trait_name_display_1 + ' ' + trait.trait_name_display_2 + ' ' + trait.trait_name_display_3 } $('#offcanvas1Label').text(traitName) + manageVal(trait.capture_date, '#captureDate', true) manageVal(trait.created_on, '#createdOn', true) manageVal(trait.modified_on, '#modifiedOn', true) From a01dde6728c1a11aecd2a1c9dcccaece7fa0490a Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Thu, 9 May 2024 13:18:23 -0500 Subject: [PATCH 42/50] Changing trait_name -> trait_id when fetching trait details --- src/modules/site-v2/templates/_scripts/trait_utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/site-v2/templates/_scripts/trait_utils.js b/src/modules/site-v2/templates/_scripts/trait_utils.js index 9298c160..82b01c6c 100644 --- a/src/modules/site-v2/templates/_scripts/trait_utils.js +++ b/src/modules/site-v2/templates/_scripts/trait_utils.js @@ -27,12 +27,12 @@ function traitNameString(name_1, name_2, name_3) { } -function queryTraitByName(trait_name, csrf_token=null) { +function queryTraitByName(trait_id, csrf_token=null) { // Construct the data object, using the optional CSRF token if provided - let data = {trait_name}; + let data = {trait_id}; if (csrf_token !== null) data['csrf_token'] = csrf_token; - + // Return the AJAX request as a Promise object return $.ajax({ type: "POST", From 874d05066b279cd5ae3409160b107126d0672118 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Thu, 9 May 2024 13:19:57 -0500 Subject: [PATCH 43/50] Updating Phenotype metadata parsing function - setting dataset correctly --- src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py b/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py index 79be5888..04377aaa 100644 --- a/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py +++ b/src/pkg/caendr/caendr/services/sql/etl/phenotype_metadata.py @@ -58,7 +58,7 @@ def parse_phenotype_metadata(species: Species, **files: LocalDatastoreFile): 'capture_date': md['capture_date'], 'created_on': md.created_on, 'modified_on': md.modified_on, - 'dataset': md['dataset'], + 'dataset': md['dataset'].value, 'is_bulk_file': md['is_bulk_file'], } else: @@ -83,6 +83,6 @@ def parse_phenotype_metadata(species: Species, **files: LocalDatastoreFile): 'capture_date': md['capture_date'], 'created_on': md.created_on, 'modified_on': md.modified_on, - 'dataset': md['dataset'], + 'dataset': md['dataset'].value, 'is_bulk_file': md['is_bulk_file'], } From c592c25c7442719515c047baa915114dc29fccf4 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Fri, 10 May 2024 12:35:32 -0500 Subject: [PATCH 44/50] Passing trait_id for zhang traits in analyze link() --- .../site-v2/templates/_includes/phenotype-database-table.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index db34cd16..d7a40113 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -171,8 +171,8 @@

{ "data": "Analyze", "render": function(data, type, row) { - const trait_name = row.trait_name_caendr; - const url = '{{ analyze_link | safe }}'.replace("TRAIT_NAME", trait_name).replace("TRAIT_SET", 'zhang'); + const trait_id = row.id; + const url = '{{ analyze_link | safe }}'.replace("TRAIT_NAME", trait_id).replace("TRAIT_SET", 'zhang'); return `
Analyze From 5b40e07bf41e5939dd9d5f0feb3d4c0512e64c81 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Fri, 10 May 2024 12:36:44 -0500 Subject: [PATCH 45/50] Updating display name for zhang files --- .../caendr/caendr/models/datastore/phenotype_report.py | 2 +- src/pkg/caendr/caendr/models/trait.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py index f2cd1924..870baa95 100644 --- a/src/pkg/caendr/caendr/models/datastore/phenotype_report.py +++ b/src/pkg/caendr/caendr/models/datastore/phenotype_report.py @@ -286,7 +286,7 @@ def compute_display_name(cls, trait: Union[Trait, TraitFile], trait_name: Option raise ValueError() # Use the Trait object to compute the display name - return list(trait.display_name) + return [trait.name,] if trait.dataset == 'zhang' else list(trait.display_name) @classmethod diff --git a/src/pkg/caendr/caendr/models/trait.py b/src/pkg/caendr/caendr/models/trait.py index 4a351c63..c2f2b2df 100644 --- a/src/pkg/caendr/caendr/models/trait.py +++ b/src/pkg/caendr/caendr/models/trait.py @@ -4,6 +4,7 @@ from caendr.models.datastore import TraitFile from caendr.models.sql import PhenotypeMetadata, PhenotypeDatabase from caendr.utils.data import dataframe_cols_to_dict +from caendr.api.phenotype import get_trait from caendr.services.cloud.postgresql import db @@ -57,9 +58,9 @@ def __init__(self, dataset = None, trait_name = None, trait_file_id = None, trai raise ValueError('Could not identify a unique trait from the given information') # Store the dataset value of the trait file - if dataset and self.file['dataset'] and dataset != self.file['dataset']: - raise ValueError('Mismatched dataset values') - self.dataset = self.file['dataset'] + # if dataset and self.file['dataset'].value and dataset != self.file['dataset'].value: + # raise ValueError('Mismatched dataset values') + self.dataset = self.file['dataset'].value # @@ -127,4 +128,4 @@ def display_name(self): ''' # For bulk files, store the single trait name, otherwise convert the display_name fields to a list - return (self.name,) if self.file['is_bulk_file'] else self.file.display_name \ No newline at end of file + return (Trait.from_sql(get_trait(self.name)).name,) if self.file['is_bulk_file'] else self.file.display_name \ No newline at end of file From 758d91d086395189c2997d408c0c0c3344e389c6 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Mon, 13 May 2024 11:19:54 -0500 Subject: [PATCH 46/50] Update module version of db_operations --- src/modules/db_operations/module.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/db_operations/module.env b/src/modules/db_operations/module.env index 777dbc07..24bb821e 100644 --- a/src/modules/db_operations/module.env +++ b/src/modules/db_operations/module.env @@ -1,2 +1,2 @@ MODULE_NAME=caendr-db-operations -MODULE_VERSION=v1.0.41 +MODULE_VERSION=v1.0.42 From 21c64416bebe50cc6b120ecce733490f4e95b03c Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Mon, 13 May 2024 11:36:10 -0500 Subject: [PATCH 47/50] Adding bleach module to db_operations --- src/modules/db_operations/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/db_operations/requirements.txt b/src/modules/db_operations/requirements.txt index a6d363ce..716b003c 100644 --- a/src/modules/db_operations/requirements.txt +++ b/src/modules/db_operations/requirements.txt @@ -1,4 +1,5 @@ backoff==2.2.1 +bleach==4.1.0 Flask==1.1.2 Flask_SQLAlchemy==2.5.1 SQLAlchemy==1.3.18 From 933b36bd07c3a5e63713fd799c03a4394268163b Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Mon, 20 May 2024 15:29:39 -0500 Subject: [PATCH 48/50] Updating analayze_link - changing trait_name to trait_id --- .../templates/_includes/phenotype-database-table.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/site-v2/templates/_includes/phenotype-database-table.html b/src/modules/site-v2/templates/_includes/phenotype-database-table.html index d7a40113..9822e049 100644 --- a/src/modules/site-v2/templates/_includes/phenotype-database-table.html +++ b/src/modules/site-v2/templates/_includes/phenotype-database-table.html @@ -306,8 +306,9 @@

function filterTable(arr) { for (let i = 0; i < arr.length; i++) { const speciesName = arr[i].species_name.charAt(0).toUpperCase() + arr[i].species_name.replace('_', '. ').slice(1) - const traitName = arr[i].trait_name_caendr - let url = '{{ analyze_link | safe }}'.replace("TRAIT_NAME", traitName).replace("TRAIT_SET", 'caendr') + const traitID = arr[i].id + const dataset = arr[i].dataset + let url = '{{ analyze_link | safe }}'.replace("TRAIT_NAME", traitID).replace("TRAIT_SET", dataset) const createdOn = formatDate(arr[i].created_on) let tagsHTML = '' if (arr[i].tags !== null && arr[i].tags.length) { From 43b96a06c128cc80e68a93773e3d4a7132135dbc Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Mon, 20 May 2024 15:31:44 -0500 Subject: [PATCH 49/50] Update module version --- src/modules/site-v2/module.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/site-v2/module.env b/src/modules/site-v2/module.env index 1ebc7449..0a5149f3 100755 --- a/src/modules/site-v2/module.env +++ b/src/modules/site-v2/module.env @@ -1,5 +1,5 @@ MODULE_NAME=caendr-site-v2 -MODULE_VERSION=v2.0.95 +MODULE_VERSION=v2.0.96 PORT=8080 FLASK_APP=main:app From deca6732c6516f0451e5bb2606bb58b45fb8d8e0 Mon Sep 17 00:00:00 2001 From: Orzu Tursunova Date: Mon, 20 May 2024 16:36:48 -0500 Subject: [PATCH 50/50] Update the way of fetching the trait --- .../base/views/tools/phenotype_database.py | 2 +- src/pkg/caendr/caendr/models/trait.py | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/modules/site-v2/base/views/tools/phenotype_database.py b/src/modules/site-v2/base/views/tools/phenotype_database.py index e7b523d1..e162dd68 100644 --- a/src/modules/site-v2/base/views/tools/phenotype_database.py +++ b/src/modules/site-v2/base/views/tools/phenotype_database.py @@ -197,7 +197,7 @@ def submit_traits(): # Try looking up the specified trait if initial_trait_name: try: - initial_trait = Trait(dataset=initial_trait_set, trait_name=initial_trait_name) + initial_trait = Trait.from_id(initial_trait_name) except NotFoundError: flash('That trait could not be found.', 'danger') initial_trait = None diff --git a/src/pkg/caendr/caendr/models/trait.py b/src/pkg/caendr/caendr/models/trait.py index c2f2b2df..17ad9e00 100644 --- a/src/pkg/caendr/caendr/models/trait.py +++ b/src/pkg/caendr/caendr/models/trait.py @@ -3,6 +3,7 @@ from caendr.models.datastore import TraitFile from caendr.models.sql import PhenotypeMetadata, PhenotypeDatabase +from caendr.models.error import NotFoundError from caendr.utils.data import dataframe_cols_to_dict from caendr.api.phenotype import get_trait @@ -62,6 +63,8 @@ def __init__(self, dataset = None, trait_name = None, trait_file_id = None, trai # raise ValueError('Mismatched dataset values') self.dataset = self.file['dataset'].value + self.sql_row = PhenotypeMetadata.query.filter_by(trait_name_caendr = self.name, dataset = self.dataset).one() + # # Constructors @@ -87,6 +90,24 @@ def from_sql(sql_row: PhenotypeMetadata) -> 'Trait': dataset = sql_row.dataset, trait_name = sql_row.trait_name_caendr, ) + + @classmethod + def from_id(cls, trait_id: str) -> 'Trait': + ''' + Instantiate a `Trait` object from a unique trait ID. + The given ID must exist in the PhenotypeMetadata SQL table, otherwise a `ValueError` will be raised. + ''' + + # Get the SQL row with the given trait ID + sql_row = PhenotypeMetadata.query.get(trait_id) + if sql_row is None: + raise NotFoundError(PhenotypeMetadata, {'id': trait_id}) + + # Construct a Trait object using the data in the SQL row + return cls( + trait_name = sql_row.trait_name_caendr, + dataset = sql_row.dataset, + ) # @@ -128,4 +149,4 @@ def display_name(self): ''' # For bulk files, store the single trait name, otherwise convert the display_name fields to a list - return (Trait.from_sql(get_trait(self.name)).name,) if self.file['is_bulk_file'] else self.file.display_name \ No newline at end of file + return (self.sql_row.trait_name_caendr,) if self.file['is_bulk_file'] else self.file.display_name \ No newline at end of file