From b19fb11aea62a09e175e62dc606f77fe52012984 Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Thu, 14 Dec 2023 16:18:09 +0000 Subject: [PATCH 01/11] New Browse Scores page using pagination on the server side + updates to avoid too much code duplication between the client and sever side table paginations --- benchmark/views.py | 4 +- catalog/context_processors.py | 41 ++- catalog/static/catalog/pgs.js | 198 ++++++---- catalog/static/catalog/pgs.scss | 73 +++- catalog/tables.py | 151 +++++++- .../catalog/browse/pending_publications.html | 20 ++ .../catalog/browse/publications.html | 32 ++ catalog/templates/catalog/browse/scores.html | 46 +++ catalog/templates/catalog/browse/traits.html | 68 ++++ catalog/templates/catalog/browseby.html | 85 ----- catalog/templates/catalog/efo.html | 4 +- catalog/templates/catalog/gwas_gcst.html | 4 +- .../catalog/includes/ancestry_form.html | 61 ++++ catalog/templates/catalog/libs/js.html | 2 +- catalog/templates/catalog/pgp.html | 30 +- ..._catalog_django_tables2_browse_scores.html | 96 +++++ catalog/urls.py | 11 +- catalog/views.py | 337 ++++++++++-------- pgs_web/settings.py | 15 +- pgs_web/urls.py | 5 + search/search.py | 4 +- search/views.py | 27 +- 22 files changed, 947 insertions(+), 367 deletions(-) create mode 100644 catalog/templates/catalog/browse/pending_publications.html create mode 100644 catalog/templates/catalog/browse/publications.html create mode 100644 catalog/templates/catalog/browse/scores.html create mode 100644 catalog/templates/catalog/browse/traits.html delete mode 100644 catalog/templates/catalog/browseby.html create mode 100644 catalog/templates/catalog/includes/ancestry_form.html create mode 100644 catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html diff --git a/benchmark/views.py b/benchmark/views.py index 6da7f331..a13bd9f4 100644 --- a/benchmark/views.py +++ b/benchmark/views.py @@ -7,7 +7,7 @@ from .models import * from .tables import * from catalog.models import Score, EFOTrait -from catalog.views import ancestry_legend +from catalog.views import ancestry_filter bm_db = 'benchmark' @@ -195,7 +195,7 @@ def benchmark(request): 'pgs_data': pgs_data, 'table_scores': table_scores, 'cohorts': cohort_data, - 'ancestry_legend': ancestry_legend(), + 'ancestry_filter': ancestry_filter(), 'has_table': 1, 'has_chart': 1, 'is_benchmark': 1 diff --git a/catalog/context_processors.py b/catalog/context_processors.py index 786ef0f0..239dce37 100644 --- a/catalog/context_processors.py +++ b/catalog/context_processors.py @@ -90,4 +90,43 @@ def html_author(author): return { 'pgs_contributors': html - } \ No newline at end of file + } + + +def pgs_ancestry_legend(request) -> str: + ''' HTML code for the Ancestry legend. ''' + ancestry_labels = constants.ANCESTRY_LABELS + count = 0 + ancestry_keys = ancestry_labels.keys() + val = len(ancestry_keys) / 2 + entry_per_col = int((len(ancestry_keys) + 1) / 2); + + div_html_1 = '
' + div_html += div_content+'
' + legend_html += div_html + # New div + div_html = div_html_1 + div_content = '' + count = 0 + + label = ancestry_labels[key] + div_content += '
'+label+'
' + count += 1 + div_html += '">'+div_content+'' + legend_html += div_html + + return { + 'ancestry_legend': ''' +
+
Ancestry legend
+
{}
+
'''.format(legend_html) + } diff --git a/catalog/static/catalog/pgs.js b/catalog/static/catalog/pgs.js index 6a230209..d5046aa9 100644 --- a/catalog/static/catalog/pgs.js +++ b/catalog/static/catalog/pgs.js @@ -6,6 +6,8 @@ var anc_types = { }; var data_toggle_table = 'table[data-toggle="table"]'; +var data_big_table = '.pgs_big_table'; +var data_table_elements = [data_toggle_table,data_big_table]; $(document).ready(function() { @@ -113,54 +115,79 @@ $(document).ready(function() { }; - // Search autocompletion + /* + * Search autocompletion + */ + var autocomplete_url = "/autocomplete/"; - $("#q").autocomplete({ - minLength: 3, - source: function (request, response) { - $.ajax({ - url: autocomplete_url, - data: { 'q': request.term }, - success: function (data) { - response(data.results); - }, - error: function () { - response([]); - } - }); - }, - select: function(event, ui) { - $("#q").val(ui.item.id); - $("#search_form").submit(); - } - }) - .autocomplete( "instance" )._renderItem = function( ul, item ) { - return format_autocomplete(ul,item); - }; - // Search autocompletion - small screen - $("#q2").autocomplete({ - minLength: 3, - source: function (request, response) { - $.ajax({ - url: autocomplete_url, - data: { 'q': request.term }, - success: function (data) { - response(data.results); - }, - error: function () { - response([]); - } - }); - }, - select: function(event, ui) { - $("#q2").val(ui.item.id); - $("#search_form_2").submit(); - } - }) - .autocomplete( "instance" )._renderItem = function( ul, item ) { - return format_autocomplete(ul,item); - }; + // Search autocompletion - Main box ('q') + var main_search_id = 'q'; + if ($("#"+main_search_id).length) { + var main_search_form_id = 'search_form'; + $("#"+main_search_id).autocomplete({ + minLength: 3, + source: function (request, response) { + $.ajax({ + url: autocomplete_url, + data: { 'q': request.term }, // <= Keep 'q' + success: function (data) { + response(data.results); + }, + error: function () { + response([]); + } + }); + }, + select: function(event, ui) { + $("#"+main_search_id).val(ui.item.id); + $("#"+main_search_form_id).submit(); + } + }) + .autocomplete( "instance" )._renderItem = function( ul, item ) { + return format_autocomplete(ul,item); + }; + // Submit button control + $('#search_btn').click(function() { + if ($('#'+main_search_id).val() && $('#'+main_search_id).val() != ''){ + $('#'+main_search_form_id).submit(); + } + }) + } + + // Search autocompletion - small screen (q2) + var alt_search_id = 'q2'; + if ($("#"+alt_search_id).length) { + var alt_search_form_id = 'search_form_2'; + $("#"+alt_search_id).autocomplete({ + minLength: 3, + source: function (request, response) { + $.ajax({ + url: autocomplete_url, + data: { 'q': request.term }, // <= Keep 'q' + success: function (data) { + response(data.results); + }, + error: function () { + response([]); + } + }); + }, + select: function(event, ui) { + $("#"+alt_search_id).val(ui.item.id); + $("#"+alt_search_form_id).submit(); + } + }) + .autocomplete( "instance" )._renderItem = function( ul, item ) { + return format_autocomplete(ul,item); + }; + // Submit button control + $('#search_btn_2').click(function() { + if ($("#"+alt_search_id).val() && $("#"+alt_search_id).val() != ''){ + $("#"+alt_search_form_id).submit(); + } + }) + } // Button toggle $('.toggle_btn').click(function() { @@ -215,18 +242,6 @@ $(document).ready(function() { }); - // Control on search form(s) - $('#search_btn').click(function() { - if ($('#q').val() && $('#q').val() != ''){ - $('#search_form').submit(); - } - }) - $('#search_btn_2').click(function() { - if ($('#q2').val() && $('#q2').val() != ''){ - $('#search_form_2').submit(); - } - }) - // Buttons in the Search page results $('.search_facet').click(function(){ if ($(this).find('i.fa-circle')) { @@ -296,8 +311,61 @@ $(document).ready(function() { }); - function filter_score_table() { + // Ancestry filtering - Browse Scores + var anc_form_name = 'browse_ancestry_form'; + $('#browse_ancestry_type_list').on('change', function() { + document.forms[anc_form_name].submit(); + }); + $('#browse_ancestry_filter_ind').on('change', function() { + document.forms[anc_form_name].submit(); + }); + $("#browse_ancestry_filter_list").on("change", ".browse_ancestry_filter_cb",function() { + document.forms[anc_form_name].submit(); + }); + // Search box events for the Browse Scores page + $('#browse_scores_search_btn').on("click", function(e) { + document.forms[anc_form_name].submit(); + }); + var $browse_scores_search_input = $('#browse_scores_search'); + $browse_scores_search_input.on("keypress", function(e) { + if (e.keyCode === 13) { + document.forms[anc_form_name].submit(); + } + }); + // Functions to set timer on typing before submitting the form + var search_typing_timer; + //on keyup, start the countdown + $browse_scores_search_input.on('keyup', function () { + clearTimeout(search_typing_timer); + search_typing_timer = setTimeout(function() { + document.forms[anc_form_name].submit(); + }, 1000); + }); + //on keydown, clear the countdown + $browse_scores_search_input.on('keydown', function () { + clearTimeout(search_typing_timer); + }); + + // Send form with updated URL (sort) + $('.orderable > a').click(function(e) { + e.preventDefault(); + var sort_url = $(this).attr('href'); + var url = $('#'+anc_form_name).attr('action'); + $('#'+anc_form_name).attr('action', url+sort_url).submit(); + //document.forms[anc_form_name].submit(); + }); + // Send form with updated URL (pagination) + $('.pagination > li > a').click(function(e) { + e.preventDefault(); + var sort_url = $(this).attr('href'); + var url = $('#'+anc_form_name).attr('action'); + $('#'+anc_form_name).attr('action', url+sort_url).submit(); + //document.forms[anc_form_name].submit(); + }); + + + function filter_score_table() { /** Get data from Ancestry Filters form **/ // Traits // @@ -572,11 +640,13 @@ function alter_external_links(prefix) { // FTP Scoring File Link function scoring_file_link() { - $(data_toggle_table).on("click", '.file_link', function(){ - var ftp_url = $(this).parent().find('.only_export').html(); - ftp_url = ftp_url.substring(0, ftp_url.lastIndexOf('/'))+'/'; - window.open(ftp_url,'_blank'); - }); + for (const element of data_table_elements) { + $(element).on("click", '.file_link', function(){ + var ftp_url = $(this).parent().find('.only_export').html(); + ftp_url = ftp_url.substring(0, ftp_url.lastIndexOf('/'))+'/'; + window.open(ftp_url,'_blank'); + }); + } } diff --git a/catalog/static/catalog/pgs.scss b/catalog/static/catalog/pgs.scss index accdf7e8..d96ffc67 100644 --- a/catalog/static/catalog/pgs.scss +++ b/catalog/static/catalog/pgs.scss @@ -31,6 +31,9 @@ $pgs_colour_red: #E00; $pgs_colour_amber: #FFA500; $pgs_colour_green: #008000; +$bootstrap-table_border_colour: #dee2e6; +$bootstrap-table_colour: #495057; + $header_height_with_margin: 75px; $header_height_with_margin_mobile: $header_height_with_margin + 60px; $footer_height: 97px; @@ -102,6 +105,7 @@ a { color: $pgs_colour_1; text-decoration: none; border-bottom: 1px dotted $pgs_colour_2; + cursor: pointer !important; } } @@ -611,13 +615,16 @@ h2 { } } -th .th-inner { - padding: 10px 24px 10px 12px !important; +th { span { font-weight: normal !important; } } +th .th-inner { + padding: 10px 24px 10px 12px !important; +} + th .th-inner-not-sortable { overflow: hidden; text-overflow: ellipsis; @@ -916,6 +923,53 @@ td > ul { } } +.pgs_big_table { + border-bottom: 1px solid $bootstrap-table_border_colour; + th { + background-color: $pgs_light_grey; + color: $bootstrap-table_colour; + } + th, td { + border-left: 1px solid $bootstrap-table_border_colour; + } + th:last-child, td:last-child { + border-right: 1px solid $bootstrap-table_border_colour; + } + tr:hover { + background-color: rgba(0,0,0,.075); + } + th.orderable > a { + color: $bootstrap-table_colour; + border-bottom: none; + &:after { + font-family: "Font Awesome 6 Free"; + font-weight: 900; + content: "\f0dc"; + color: $pgs_darkC; + padding-left: 0.5rem; + } + } + th.orderable.asc > a { + &:after { + font-family: "Font Awesome 6 Free"; + font-weight: 900; + content: "\f0de"; + color: $pgs_colour_1; + padding-left: 0.5rem; + } + } + th.orderable.desc > a { + &:after { + font-family: "Font Awesome 6 Free"; + font-weight: 900; + content: "\f0dd"; + color: $pgs_colour_1; + padding-left: 0.5rem; + } + } +} + + /* Icons related CSS */ .external-link:after { font-family: "Font Awesome 6 Free"; @@ -2473,6 +2527,21 @@ footer { } } +.anc_col_subtitle { + font-size:12px; + white-space:nowrap; + text-align:center; + font-weight: normal !important; + div { + display:inline-block; + width:48px; + padding:4px 5px; + margin:0px; + } + div:not(:first-child) { + border-left: 1px solid $bootstrap-table_border_colour; + } +} .anc_chart_container { display:flex; > div { diff --git a/catalog/tables.py b/catalog/tables.py index cb4d5e06..2543fb87 100644 --- a/catalog/tables.py +++ b/catalog/tables.py @@ -13,6 +13,7 @@ page_size = "50" empty_cell_char = '—' is_pgs_on_curation_site = settings.PGS_ON_CURATION_SITE +anc_groups = '
GWAS
Dev
Eval
' def smaller_in_bracket(value): @@ -267,7 +268,149 @@ class Browse_ScoreTable(tables.Table): publication = tables.Column(accessor='publication', verbose_name=format_html('PGS Publication ID (PGP)'), orderable=True) trait_efo = tables.Column(accessor='trait_efo', verbose_name=format_html('Mapped Trait(s) (Ontology)'), orderable=False) ftp_link = tables.Column(accessor='link_filename', verbose_name=format_html('Scoring File (FTP Link)'), orderable=False) - ancestries = Column_format_html(accessor='ancestries', verbose_name='Ancestry distribution', orderable=False) + ancestries = Column_format_html(accessor='ancestries', verbose_name=format_html('Ancestry distribution'+anc_groups), orderable=False) + + class Meta: + model = Score + attrs = { + "id": "scores_table", + "data-sort-name" : "id" + } + fields = [ + 'id', + 'publication', + 'trait_reported', + 'trait_efo', + 'variants_number', + 'ancestries', + 'ftp_link' + ] + template_name = 'catalog/pgs_catalog_django_tables2_browse_scores.html' + + + def render_id(self, value, record): + return score_format(record) + + def render_publication(self, value): + return publication_format(value) + + def render_trait_efo(self,value): + traits_list = [ t.display_label for t in value.all() ] + return format_html(',
'.join(traits_list)) + + def render_ftp_link(self, value, record): + id = value.split('.')[0] + ftp_file_link = '{}/scores/{}/ScoringFiles/{}'.format(constants.USEFUL_URLS['PGS_FTP_HTTP_ROOT'], id, value) + margin_right = '' + license_icon = '' + if record.has_default_license == False: + margin_right = ' mr2' + license_icon = f' - Check Terms/Licenses' + return format_html(f' {ftp_file_link}{license_icon}') + + + def render_variants_number(self, value): + return '{:,}'.format(value) + + + def render_ancestries(self, value, record): + if not value: + return '-' + + anc_labels = constants.ANCESTRY_LABELS + stages = constants.PGS_STAGES + + ancestries_data = value + pgs_id = record.num + chart_id = f'ac_{pgs_id}' + data_stage = {} + data_title = {} + anc_list = {} + anc_all_list = { + 'dev_all': set(), + 'all': set() + } + + # Fetch the data for each stage + for stage in stages: + if stage in ancestries_data: + ancestries_data_stage = ancestries_data[stage] + anc_list[stage] = set() + # Details of the multi ancestry data + multi_title = {} + multi_anc = 'multi' + if multi_anc in ancestries_data_stage: + for mt in ancestries_data_stage[multi_anc]: + (ma,anc) = mt.split('_') + if ma not in multi_title: + multi_title[ma] = [] + multi_title[ma].append(f'
  • {anc_labels[anc]}
  • ') + + if anc == 'MAE': + continue + # Add to the unique list of ancestries + anc_list[stage].add(anc) + # Add to the unique list of ALL ancestries + anc_all_list['all'].add(anc) + # Add to the unique list of DEV ancestries + if stage != 'eval': + anc_all_list['dev_all'].add(anc) + + # Ancestry data for the stage: distribution, list of ancestries and content of the chart tootlip + data_stage[stage] = [] + data_title[stage] = [] + for key,val in sorted(ancestries_data_stage['dist'].items(), key=lambda item: float(item[1]), reverse=True): + label = anc_labels[key] + data_stage[stage].append(f'"{key}",{val}') + extra_title = '' + if key in multi_title: + extra_title += '' + data_title[stage].append(f'
    {val}%{extra_title}
    ') + + if key == 'MAE': + continue + # Add to the unique list of ancestries + anc_list[stage].add(key) + # Add to the unique list of ALL ancestries + anc_all_list['all'].add(key) + # Add to the unique list of DEV ancestries + if stage != 'eval': + anc_all_list['dev_all'].add(key) + + + # Skip if no expected data "stage" available + if data_stage.keys() == 0: + return None + + # Format the data for each stage: build the HTML + html_list = [] + for stage in stages: + if stage in data_stage: + id = chart_id+'_'+stage + title_count = '' + count = ancestries_data[stage]['count'] + if count != 0: + title_count = '{:,}'.format(count) + title = ''.join(data_title[stage])+title_count + html_chart = f'
    ' + html_list.append(html_chart) + else: + html_list.append('
    -
    ') + + # Wrap up the HTML + html = '
    ' + html += ''.join(html_list) + html += '
    ' + return format_html(html) + + +class ScoreTable(tables.Table): + '''Table to browse Scores (PGS) in the PGS Catalog''' + id = tables.Column(accessor='id', verbose_name='Polygenic Score ID & Name', orderable=True) + publication = tables.Column(accessor='publication', verbose_name=format_html('PGS Publication ID (PGP)'), orderable=True) + trait_efo = tables.Column(accessor='trait_efo', verbose_name=format_html('Mapped Trait(s) (Ontology)'), orderable=False) + ftp_link = tables.Column(accessor='link_filename', verbose_name=format_html('Scoring File (FTP Link)'), orderable=False) + ancestries = Column_format_html(accessor='ancestries', verbose_name=format_html(f'Ancestry distribution
    {anc_groups}
    '), orderable=False) class Meta: model = Score @@ -327,7 +470,6 @@ def render_ancestries(self, value, record): data_stage = {} data_title = {} anc_list = {} - multi_list = {} anc_all_list = { 'dev_all': set(), 'all': set() @@ -433,14 +575,15 @@ def render_ancestries(self, value, record): return format_html(html) -class Browse_ScoreTableEval(Browse_ScoreTable): +class ScoreTableEval(ScoreTable): class Meta: attrs = { "id": "scores_eval_table" } template_name = 'catalog/pgs_catalog_django_table.html' -class Browse_ScoreTableExample(Browse_ScoreTable): + +class ScoreTableExample(ScoreTable): class Meta: attrs = { "id": "scores_eg_table", diff --git a/catalog/templates/catalog/browse/pending_publications.html b/catalog/templates/catalog/browse/pending_publications.html new file mode 100644 index 00000000..67aafbcc --- /dev/null +++ b/catalog/templates/catalog/browse/pending_publications.html @@ -0,0 +1,20 @@ +{% extends 'catalog/base.html' %} +{% load render_table from django_tables2 %} + +{% block title %}All {{ view_name }}{% endblock %} + +{% block content %} + +
    +
    +

    {{ view_name }}

    + {% render_table table %} +
    +
    +{% endblock %} diff --git a/catalog/templates/catalog/browse/publications.html b/catalog/templates/catalog/browse/publications.html new file mode 100644 index 00000000..ff93ce85 --- /dev/null +++ b/catalog/templates/catalog/browse/publications.html @@ -0,0 +1,32 @@ +{% extends 'catalog/base.html' %} +{% load render_table from django_tables2 %} + +{% block title %}All {{ view_name }}{% endblock %} + +{% block desc %} + +{% endblock %} + +{% block content %} + +
    +
    +

    {{ view_name }}

    + {% render_table table %} +
    +
    +{% endblock %} diff --git a/catalog/templates/catalog/browse/scores.html b/catalog/templates/catalog/browse/scores.html new file mode 100644 index 00000000..8a345430 --- /dev/null +++ b/catalog/templates/catalog/browse/scores.html @@ -0,0 +1,46 @@ +{% extends 'catalog/base.html' %} +{% load render_table from django_tables2 %} + +{% block title %}All Scores {% endblock %} + +{% block desc %} + +{% endblock %} + +{% block content %} + +
    +
    +

    {{ view_name }}

    + +
    {% csrf_token %} + {% include "catalog/includes/ancestry_form.html" %} +
    +
    + +
    +
    + +
    +
    + {% render_table table %} +
    +
    +
    +{% endblock %} diff --git a/catalog/templates/catalog/browse/traits.html b/catalog/templates/catalog/browse/traits.html new file mode 100644 index 00000000..16dc1152 --- /dev/null +++ b/catalog/templates/catalog/browse/traits.html @@ -0,0 +1,68 @@ +{% extends 'catalog/base.html' %} +{% load render_table from django_tables2 %} + +{% block title %}All {{ view_name }}{% endblock %} + +{% block desc %} + +{% endblock %} + +{% block content %} + +
    +
    +

    {{ view_name }}

    + + +

    + Browse PGS by Trait Category + Reset view +

    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Traits

    + {% render_table table %} +
    +
    +{% endblock %} diff --git a/catalog/templates/catalog/browseby.html b/catalog/templates/catalog/browseby.html deleted file mode 100644 index dc27e447..00000000 --- a/catalog/templates/catalog/browseby.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends 'catalog/base.html' %} -{% load render_table from django_tables2 %} - -{% block title %}All {{ view_name }}{% endblock %} - -{% block desc %} - -{% endblock %} - -{% block content %} - -
    -
    -

    {{ view_name }}

    - {% if scores_list %} -
      - {% for score in scores_list %} -
    • {{ score }}
    • - {% endfor %} -
    - {% elif table %} - {% if data_chart %} - - -

    - Browse PGS by Trait Category - Reset view -

    - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -

    Traits

    - {% endif %} - - {% if ancestry_form %} - {{ ancestry_form|safe }} - {% endif %} - - {% render_table table %} - {% else %} -

    Not implemented yet.

    - {% endif %} -
    -
    -{% endblock %} diff --git a/catalog/templates/catalog/efo.html b/catalog/templates/catalog/efo.html index f30d4701..36316f73 100644 --- a/catalog/templates/catalog/efo.html +++ b/catalog/templates/catalog/efo.html @@ -133,9 +133,7 @@

    Trait: {{ trait.label }}

    Associated Polygenic Score(s) - {% if ancestry_form %} - {{ ancestry_form|safe }} - {% endif %} + {% include "catalog/includes/ancestry_form.html" %} {% if trait_scores_child_count and trait_scores_direct_count %}
    diff --git a/catalog/templates/catalog/gwas_gcst.html b/catalog/templates/catalog/gwas_gcst.html index c8048552..6d500f5d 100644 --- a/catalog/templates/catalog/gwas_gcst.html +++ b/catalog/templates/catalog/gwas_gcst.html @@ -52,9 +52,7 @@

    NHGRI-EBI GWAS Catalog Study: {{ gwas_i {% if table_scores %}

    PGS Developed Using Variant Associations from {{ gwas_id }}

    - {% if ancestry_form %} - {{ ancestry_form|safe }} - {% endif %} + {% include "catalog/includes/ancestry_form.html" %} {% render_table table_scores %}
    {% endif %} diff --git a/catalog/templates/catalog/includes/ancestry_form.html b/catalog/templates/catalog/includes/ancestry_form.html new file mode 100644 index 00000000..6e2cefaf --- /dev/null +++ b/catalog/templates/catalog/includes/ancestry_form.html @@ -0,0 +1,61 @@ +{% load static %} +
    + +
    +
    Filter PGS by Participant Ancestry
    + +
    + +
    +
    Individuals included in:
    + {% if is_browse_score %} + + {% else %} + + {% endif %} +
    +
    G - Source of Variant Associations (GWAS)
    +
    D - Score Development/Training
    +
    E - PGS Evaluation
    +
    +
    + +
    +
    List of ancestries includes:
    +
    + +
    +
    Display options:
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + {% if ancestry_legend %} + {{ ancestry_legend|safe }} + {% endif %} +
    diff --git a/catalog/templates/catalog/libs/js.html b/catalog/templates/catalog/libs/js.html index c9d1707b..7f7bdaa0 100644 --- a/catalog/templates/catalog/libs/js.html +++ b/catalog/templates/catalog/libs/js.html @@ -4,7 +4,7 @@ {% endif %} - + diff --git a/catalog/templates/catalog/pgp.html b/catalog/templates/catalog/pgp.html index aa1d3060..635a65fe 100644 --- a/catalog/templates/catalog/pgp.html +++ b/catalog/templates/catalog/pgp.html @@ -87,22 +87,20 @@

    PGS
    {% if table_scores or table_evaluated %} -

    Associated Polygenic Score(s)

    - {% if ancestry_form %} - {{ ancestry_form|safe }} - {% endif %} - {% if table_scores %} -

    - PGS Developed By This Publication -

    - {% render_table table_scores %} - {% endif %} - {% if table_evaluated %} -

    - External PGS Evaluated By This Publication -

    - {% render_table table_evaluated %} - {% endif %} +

    Associated Polygenic Score(s)

    + {% include "catalog/includes/ancestry_form.html" %} + {% if table_scores %} +

    + PGS Developed By This Publication +

    + {% render_table table_scores %} + {% endif %} + {% if table_evaluated %} +

    + External PGS Evaluated By This Publication +

    + {% render_table table_evaluated %} + {% endif %} {% endif %}
    diff --git a/catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html b/catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html new file mode 100644 index 00000000..71c9fac7 --- /dev/null +++ b/catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html @@ -0,0 +1,96 @@ +{% load django_tables2 %} +{% load i18n l10n %} +{% block table-wrapper %} +
    + {% block table %} + + {% block table.thead %} + {% if table.show_header %} + + + {% for column in table.columns %} + + {% endfor %} + + + {% endif %} + {% endblock table.thead %} + {% block table.tbody %} + + {% for row in table.paginated_rows %} + {% block table.tbody.row %} + + {% for column, cell in row.items %} + + {% endfor %} + + {% endblock table.tbody.row %} + {% empty %} + {% if table.empty_text %} + {% block table.tbody.empty_text %} + + {% endblock table.tbody.empty_text %} + {% endif %} + {% endfor %} + + {% endblock table.tbody %} + {% block table.tfoot %} + {% if table.has_footer %} + + + {% for column in table.columns %} + + {% endfor %} + + + {% endif %} + {% endblock table.tfoot %} +
    + {% if column.orderable %} + {{ column.header }} + {% else %} + {{ column.header }} + {% endif %} +
    {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
    {{ table.empty_text }}
    {{ column.footer }}
    + {% endblock table %} + + {% block pagination %} +
    + {% if table.rows|length > 0 %} + Showing {{ table.row_start }} to {{ table.row_end }} of {{ table.rows|length }} rows + {% endif %} + {% if table.page and table.paginator.num_pages > 1 %} + + {% endif %} +
    + {% endblock pagination %} +
    +{% endblock table-wrapper %} diff --git a/catalog/urls.py b/catalog/urls.py index 14273ca4..015a232e 100755 --- a/catalog/urls.py +++ b/catalog/urls.py @@ -28,8 +28,15 @@ # e.g.: /gwas/GCST001937/ path('gwas//', views.gwas_gcst, name='NHGRI-EBI GWAS Catalog Study'), - # e.g.: /browse/{scores, traits, studies, sample_set}/ - path('browse//', cache_page(cache_time)(views.browseby), name='Browse data'), + # Browse Catalog + # e.g.: /browse/scores/ + path('browse/scores/', views.browse_scores, name='Browse Scores'), + # e.g.: /browse/traits/ + path('browse/traits/', cache_page(cache_time)(views.browse_traits), name='Browse Traits'), + # e.g.: /browse/studies/ + path('browse/studies/', cache_page(cache_time)(views.browse_publications), name='Browse Publications'), + # e.g.: /browse/pending_studies/ + path('browse/pending_studies/', cache_page(cache_time)(views.browse_pending_publications), name='Browse Pending Publications'), # e.g.: /latest_release/ path('latest_release/', cache_page(cache_time)(views.latest_release), name='Latest Release'), diff --git a/catalog/views.py b/catalog/views.py index 8b04d792..4e53afbc 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -4,7 +4,7 @@ from django.views.generic import TemplateView from django.views.generic.base import RedirectView from django.conf import settings -from django.db.models import Prefetch +from django.db.models import Prefetch, Q from pgs_web import constants from .tables import * @@ -51,107 +51,21 @@ def score_disclaimer(publication_url): return constants.DISCLAIMERS['score'].format(publication_url) -def ancestry_legend(): - ''' HTML code for the Ancestry legend. ''' - ancestry_labels = constants.ANCESTRY_LABELS - count = 0; - ancestry_keys = ancestry_labels.keys() - val = len(ancestry_keys) / 2 - entry_per_col = int((len(ancestry_keys) + 1) / 2); - - div_html_1 = '
    ' - div_html += div_content+'
    ' - legend_html += div_html - # New div - div_html = div_html_1 - div_content = '' - count = 0 - - label = ancestry_labels[key] - div_content += '
    '+label+'
    ' - count += 1 - div_html += '">'+div_content+'

    ' - legend_html += div_html - - return ''' -
    -
    Ancestry legend
    -
    {}
    -
    '''.format(legend_html) - - -def ancestry_form(): - ''' HTML code for the Ancestry form. ''' - - option_html = '' - +def ancestry_filter(form_data:dict=None) -> str: + ''' HTML code for the Ancestry filter. ''' + # Ancestry form + ancestry_filter_ind_option_html = '' ancestry_labels = constants.ANCESTRY_LABELS for key in ancestry_labels.keys(): - label = ancestry_labels[key] - if key != 'MAO' and key != 'MAE': - opt = f'' - option_html += opt - - checkbox_title_eur = 'This button can be used to hide PGS with any European ancestry data to only show scores with data from other non-European ancestry group. The button is selected by default, displaying all PGS.' - checkbox_title_multi = 'Shows only PGS that include data from multiple ancestry groups at the selected study stage, hiding PGS with data from a single ancestry group.' - - return ''' -
    - -
    -
    Filter PGS by Participant Ancestry
    -
    - -
    -
    Individuals included in:
    - -
    -
    G - Source of Variant Associations (GWAS)
    -
    D - Score Development/Training
    -
    E - PGS Evaluation
    -
    -
    - -
    -
    List of ancestries includes:
    -
    - -
    -
    Display options:
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    - {ancestry_legend} -
    '''.format(ancestry_option=option_html, ancestry_legend=ancestry_legend(), title_eur=checkbox_title_eur, title_multi=checkbox_title_multi) + sel = '' + if form_data: + if 'browse_ancestry_filter_ind' in form_data and form_data['browse_ancestry_filter_ind'] == key: + sel =' selected' + opt = f'' + ancestry_filter_ind_option_html += opt + return ancestry_filter_ind_option_html def get_efo_traits_data(): @@ -238,63 +152,170 @@ def index(request): return render(request, 'catalog/index.html', context) -def browseby(request, view_selection): +def browse_scores(request): context = {} - if view_selection == 'traits': - efo_traits_data = get_efo_traits_data() - table = Browse_TraitTable(efo_traits_data[0]) - context = { - 'view_name': 'Traits', - 'table': table, - 'data_chart': efo_traits_data[1], - 'has_chart': 1 - } - elif view_selection == 'studies': - publication_defer = ['authors','curation_status','curation_notes','date_released'] - publication_prefetch_related = [pgs_prefetch['publication_score'], pgs_prefetch['publication_performance']] - publications = Publication.objects.defer(*publication_defer).all().prefetch_related(*publication_prefetch_related) - table = Browse_PublicationTable(publications, order_by="num") - context = { - 'view_name': 'Publications', - 'table': table - } - elif view_selection == 'pending_studies': - publication_defer = ['authors','curation_notes','date_released'] - publication_prefetch_related = [pgs_prefetch['publication_score'], pgs_prefetch['publication_performance']] - pending_publications = Publication.objects.defer(*publication_defer).filter(date_released__isnull=True).prefetch_related(*publication_prefetch_related) - table = Browse_PendingPublicationTable(pending_publications, order_by="num") - context = { - 'view_name': 'Pending Publications', - 'table': table - } - # elif view_selection == 'sample_set': - # context['view_name'] = 'Sample Sets' - # table = Browse_SampleSetTable(Sample.objects.defer(*pgs_defer['sample']).filter(sampleset__isnull=False).prefetch_related('sampleset', pgs_prefetch['cohorts']).order_by('sampleset__num')) - # context['table'] = table - elif view_selection == 'scores' : - score_only_attributes = ['id','name','trait_efo','trait_reported','variants_number','ancestries','license','publication__id','publication__date_publication','publication__journal','publication__firstauthor'] - table = Browse_ScoreTable(Score.objects.only(*score_only_attributes).select_related('publication').all().order_by('num').prefetch_related(pgs_prefetch['trait'])) - context = { - 'view_name': 'Polygenic Scores (PGS)', - 'table': table, - 'ancestry_form': ancestry_form(), - 'has_chart': 1 - } - elif view_selection == 'all': - return redirect('/browse/scores/', permanent=True) - else: - return redirect('/', permanent=True) + # Ancestry form + input_names = { + 'browse_ancestry_type_list': 'sel', + 'browse_ancestry_filter_ind': 'sel', + 'browse_anc_cb_EUR': 'cb', + 'browse_anc_cb_multi': 'cb', + 'browse_scores_search': 'in' + } + # Init form data + form_data = {} + for input_name in input_names.keys(): + form_data[input_name] = None + + if request.method == "POST": + for input_name in input_names.keys(): + type = input_names[input_name] + val = request.POST.get(input_name) + if type in ['sel','in']: + if val: + form_data[input_name] = val + elif type == 'cb': + if val: + form_data[input_name] = True + else: + form_data[input_name] = False + + score_only_attributes = ['id','name','trait_efo','trait_reported','variants_number','ancestries','license','publication__id','publication__date_publication','publication__journal','publication__firstauthor'] + queryset = Score.objects.only(*score_only_attributes).select_related('publication').all().prefetch_related(pgs_prefetch['trait']) + + ## Sorting ## + order_by = 'num' + sort_param = request.GET.get('sort') + if sort_param: + order_by = sort_param + queryset = queryset.order_by(order_by) + + ## Filter ancestry ## + gwas_step = 'gwas' + dev_step = 'dev' + eval_step = 'eval' + study_steps = [gwas_step,dev_step,eval_step] + # Ancestry Type + anc_step = form_data['browse_ancestry_type_list'] + anc_value = form_data['browse_ancestry_filter_ind'] + anc_include_eur = form_data['browse_anc_cb_EUR'] + anc_include_multi = form_data['browse_anc_cb_multi'] + browse_scores_search = form_data['browse_scores_search'] + # Study step and ancestry selections + if not anc_step or anc_step == 'all': + if anc_value: + filters = {} + for step in study_steps: + filters[step] = f'ancestries__{step}__dist__{anc_value}__isnull' + queryset = queryset.filter(Q(**{filters[gwas_step]:False}) | Q(**{filters[dev_step]:False}) | Q(**{filters[eval_step]:False})) + elif anc_step: + if anc_step in study_steps: + queryset = queryset.filter(ancestries__has_key=anc_step) + if anc_value: + anc_filter = f'ancestries__{anc_step}__dist__{anc_value}__isnull' + queryset = queryset.filter(**{anc_filter:False}) + elif anc_step == 'dev_all': + queryset = queryset.filter(ancestries__has_key=gwas_step).filter(ancestries__has_key=dev_step) + if anc_value: + filters = {} + for step in ['gwas','dev']: + filters[step] = f'ancestries__{step}__dist__{anc_value}__isnull' + queryset = queryset.filter(Q(**{filters[gwas_step]:False}) | Q(**{filters[dev_step]:False})) + + # Filter out European ancestry (including multi-ancestry with european) + if anc_include_eur == False: + for step in study_steps: + anc_filter = f'ancestries__{step}__dist__EUR__isnull' + queryset = queryset.filter(**{anc_filter:True}) + anc_filter = f'ancestries__{step}__dist__MAE__isnull' + queryset = queryset.filter(**{anc_filter:True}) + + # Filter to include multi-ancestry + if anc_include_multi == True: + multi_filters = {} + for step in study_steps: + anc_m_filter = f'ancestries__{step}__multi__isnull' + multi_filters[step] = anc_m_filter + queryset = queryset.filter(Q(**{multi_filters[gwas_step]:False}) | Q(**{multi_filters[dev_step]:False}) | Q(**{multi_filters[eval_step]:False})) + + # Filter term from the table search box + if browse_scores_search: + queryset = queryset.filter( + Q(id__icontains=browse_scores_search) | Q(name__icontains=browse_scores_search) | + Q(trait_reported__icontains=browse_scores_search) | Q(trait_efo__label__icontains=browse_scores_search) | + Q(publication__id__icontains=browse_scores_search) | Q(publication__title__icontains=browse_scores_search) | Q(publication__firstauthor__icontains=browse_scores_search) + ) + + # Data table + table = Browse_ScoreTable(queryset) + + # Pagination + rows_per_page = 50 + table.paginate(page=request.GET.get("page", 1), per_page=rows_per_page) + + page_rows_number = len(table.page) + page_number = table.page.number + table.row_start = rows_per_page * (page_number - 1) + 1 + table.row_end = table.row_start - 1 + page_rows_number; - context['has_table'] = 1 + context = { + 'view_name': 'Polygenic Scores (PGS)', + 'table': table, + 'form_data': form_data, + 'ancestry_filter': ancestry_filter(form_data), + 'has_chart': 1, + # 'has_table': 1, + 'is_browse_score': 1 + } + return render(request, 'catalog/browse/scores.html', context) - return render(request, 'catalog/browseby.html', context) + +def browse_traits(request): + efo_traits_data = get_efo_traits_data() + table = Browse_TraitTable(efo_traits_data[0]) + context = { + 'view_name': 'Traits', + 'table': table, + 'data_chart': efo_traits_data[1], + 'has_ebi_icons': 1, + 'has_chart': 1, + 'has_table': 1 + } + return render(request, 'catalog/browse/traits.html', context) + + +def browse_publications(request): + publication_defer = ['authors','curation_status','curation_notes','date_released'] + publication_prefetch_related = [pgs_prefetch['publication_score'], pgs_prefetch['publication_performance']] + publications = Publication.objects.defer(*publication_defer).all().prefetch_related(*publication_prefetch_related) + table = Browse_PublicationTable(publications, order_by="num") + context = { + 'view_name': 'Publications', + 'has_ebi_icons': 1, + 'table': table, + 'has_table': 1 + } + return render(request, 'catalog/browse/publications.html', context) + + +def browse_pending_publications(request): + publication_defer = ['authors','curation_notes','date_released'] + publication_prefetch_related = [pgs_prefetch['publication_score'], pgs_prefetch['publication_performance']] + pending_publications = Publication.objects.defer(*publication_defer).filter(date_released__isnull=True).prefetch_related(*publication_prefetch_related) + table = Browse_PendingPublicationTable(pending_publications, order_by="num") + context = { + 'view_name': 'Pending Publications', + 'table': table, + 'has_table': 1 + } + return render(request, 'catalog/browse/pending_publications.html', context) def latest_release(request): context = { - 'ancestry_form': ancestry_form(), + 'ancestry_filter': ancestry_filter(), 'release_date': 'NA', 'publications_count': 0, 'scores_count': 0, @@ -318,7 +339,7 @@ def latest_release(request): # Scores score_only_attributes = ['id','name','trait_efo','trait_reported','variants_number','ancestries','license','publication__id','publication__date_publication','publication__journal','publication__firstauthor'] - scores_table = Browse_ScoreTable(Score.objects.only(*score_only_attributes,'date_released').select_related('publication').filter(date_released=release_date).order_by('num').prefetch_related(pgs_prefetch['trait'])) + scores_table = ScoreTable(Score.objects.only(*score_only_attributes,'date_released').select_related('publication').filter(date_released=release_date).order_by('num').prefetch_related(pgs_prefetch['trait'])) context['scores_table'] = scores_table context['scores_count'] = latest_release['score_count'] @@ -422,13 +443,13 @@ def pgp(request, pub_id): 'performance_disclaimer': performance_disclaimer(), 'has_table': 1, 'has_chart': 1, - 'ancestry_form': ancestry_form() + 'ancestry_filter': ancestry_filter() } # Display scores that were developed by this publication related_scores = pub.publication_score.defer(*pgs_defer['generic']).select_related('publication').all().prefetch_related(pgs_prefetch['trait']) if related_scores.count() > 0: - table = Browse_ScoreTable(related_scores) + table = ScoreTable(related_scores) context['table_scores'] = table # Get PGS evaluated by the PGP @@ -476,7 +497,7 @@ def pgp(request, pub_id): # External Scores Evaluated By This Publication if len(external_scores) > 0: - table = Browse_ScoreTableEval(external_scores) + table = ScoreTableEval(external_scores) context['table_evaluated'] = table context['has_table'] = 1 @@ -558,11 +579,11 @@ def efo(request, efo_id): 'trait_scores_direct_count': len(related_direct_scores), 'trait_scores_child_count': len(related_child_scores), 'performance_disclaimer': performance_disclaimer(), - 'table_scores': Browse_ScoreTable(related_scores), + 'table_scores': ScoreTable(related_scores), 'include_children': False if exclude_children else True, 'has_table': 1, 'has_chart': 1, - 'ancestry_form': ancestry_form() + 'ancestry_filter': ancestry_filter() } # Find the evaluations of these scores @@ -598,11 +619,11 @@ def gwas_gcst(request, gcst_id): context = { 'gwas_id': gcst_id, 'performance_disclaimer': performance_disclaimer(), - 'table_scores' : Browse_ScoreTable(related_scores), + 'table_scores' : ScoreTable(related_scores), 'has_table': 1, 'use_gwas_api': 1, - 'ancestry_form': ancestry_form(), - 'has_chart': 1 + 'has_chart': 1, + 'ancestry_filter': ancestry_filter() } pquery = Performance.objects.defer(*pgs_defer['perf'],*pgs_defer['publication_sel']).select_related('score','publication').filter(score__in=related_scores).prefetch_related(*performance_prefetch) @@ -653,7 +674,7 @@ def pss(request, pss_id): if related_performance.count() > 0: # Scores related_scores = [x.score for x in related_performance] - table_scores = Browse_ScoreTable(related_scores) + table_scores = ScoreTable(related_scores) context['table_scores'] = table_scores # Display performance metrics associated with this sample set table_performance = PerformanceTable(related_performance) @@ -668,10 +689,10 @@ def ancestry_doc(request): pgs_id = "PGS000018" try: score = Score.objects.defer(*pgs_defer['generic']).select_related('publication').prefetch_related('trait_efo').get(id=pgs_id) - table_score = Browse_ScoreTableExample([score]) + table_score = ScoreTableExample([score]) context = { 'pgs_id_example': pgs_id, - 'ancestry_legend': ancestry_legend(), + 'ancestry_filter': ancestry_filter(), 'table_score': table_score, 'has_table': 1, 'has_chart': 1 diff --git a/pgs_web/settings.py b/pgs_web/settings.py index 02a7b6ee..4741b8fe 100644 --- a/pgs_web/settings.py +++ b/pgs_web/settings.py @@ -82,8 +82,9 @@ if PGS_ON_LIVE_SITE: INSTALLED_APPS.append('corsheaders') -# if DEBUG: -# INSTALLED_APPS.append('django_extensions') +# Debug helper +if DEBUG == True: + INSTALLED_APPS.append('debug_toolbar') # Debug SQL queries MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', @@ -97,6 +98,11 @@ # Live middleware if PGS_ON_LIVE_SITE: MIDDLEWARE.insert(2, 'corsheaders.middleware.CorsMiddleware') +# Debug toolbar +if DEBUG == True: + MIDDLEWARE.insert(5,'debug_toolbar.middleware.DebugToolbarMiddleware') # Debug SQL queries + # Debug SQL queries + INTERNAL_IPS = ['127.0.0.1'] ROOT_URLCONF = 'pgs_web.urls' @@ -109,7 +115,8 @@ 'catalog.context_processors.pgs_settings', 'catalog.context_processors.pgs_search_examples', 'catalog.context_processors.pgs_info', - 'catalog.context_processors.pgs_contributors' + 'catalog.context_processors.pgs_contributors', + 'catalog.context_processors.pgs_ancestry_legend' ] if PGS_ON_GAE == 1 and DEBUG == False: @@ -347,7 +354,7 @@ # Google Cloud Storage Settings # #---------------------------------# -if os.getenv('GAE_APPLICATION'): +if os.getenv('GAE_APPLICATION') and PGS_ON_CURATION_SITE: from google.oauth2 import service_account GS_CREDENTIALS = service_account.Credentials.from_service_account_file( os.path.join(BASE_DIR, os.environ['GS_SERVICE_ACCOUNT_SETTINGS']) diff --git a/pgs_web/urls.py b/pgs_web/urls.py index 8eca5e4f..3709dd12 100755 --- a/pgs_web/urls.py +++ b/pgs_web/urls.py @@ -28,3 +28,8 @@ from django.contrib import admin urlpatterns.append(path('admin/', admin.site.urls)) urlpatterns.append(path('', include('curation_tracker.urls'))) + +# Debug SQL queries +if settings.DEBUG: + import debug_toolbar + urlpatterns.append(path('__debug__/', include(debug_toolbar.urls))) \ No newline at end of file diff --git a/search/search.py b/search/search.py index 0c69ea7d..23b9ef6c 100644 --- a/search/search.py +++ b/search/search.py @@ -1,9 +1,7 @@ from elasticsearch_dsl import Q -from elasticsearch_dsl import Search from search.documents.efo_trait import EFOTraitDocument from search.documents.publication import PublicationDocument from search.documents.score import ScoreDocument -from elasticsearch import Elasticsearch class PGSSearch: @@ -211,7 +209,7 @@ def __init__(self, query): super().__init__(query) self.query_fields = [ "id^3", - "name", + "name" ] self.display_fields = [ 'id', diff --git a/search/views.py b/search/views.py index ca403411..a5301c58 100644 --- a/search/views.py +++ b/search/views.py @@ -1,13 +1,11 @@ from django.shortcuts import render -from elasticsearch_dsl import Q -from search.documents.efo_trait import EFOTraitDocument -from search.documents.publication import PublicationDocument -from search.documents.score import ScoreDocument from search.search import EFOTraitSearch, PublicationSearch, ScoreSearch from django.http import JsonResponse + all_results_scores = {} + def search(request): global all_results_scores @@ -63,13 +61,14 @@ def search(request): return render(request, 'search/search.html', context) -def autocomplete_base(request, query): +def autocomplete(request): """ Return suggestions for the autocomplete form. """ max_items = 15 results = [] - if query: + q = request.GET.get('q') + if q: # EFO Traits - efo_trait_search = EFOTraitSearch(query) + efo_trait_search = EFOTraitSearch(q) efo_trait_suggestions = efo_trait_search.suggest() results = [ result for result in efo_trait_suggestions[:max_items]] @@ -77,16 +76,9 @@ def autocomplete_base(request, query): return JsonResponse({ 'results': results }) -def autocomplete(request): - """ Return suggestions for the autocomplete form. """ - q = request.GET.get('q') - - return autocomplete_base(request,q) - - def format_score_results(request, data): """ Convert the Score results into HTML. """ - results = [] + for idx, d in enumerate(data): mapped_traits = [] @@ -112,7 +104,7 @@ def format_score_results(request, data): def format_efo_traits_results(request, data): """ Convert the EFO Trait results into HTML. """ - results = [] + for d in data: desc = d.description if desc: @@ -148,9 +140,6 @@ def format_efo_traits_results(request, data): def format_publications_results(request, data): """ Convert the Publication results into HTML. """ - results = [] - doi_url = 'https://doi.org/' - pubmed_url = 'https://www.ncbi.nlm.nih.gov/pubmed/' for idx, d in enumerate(data): id_suffix = d.id.replace('PGP','') From 8d253fa875d67616a6c4fa9c3a8a09439953260d Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Thu, 14 Dec 2023 16:32:05 +0000 Subject: [PATCH 02/11] Add legacy url for /browse/scores/ --- catalog/urls.py | 2 ++ catalog/views.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/catalog/urls.py b/catalog/urls.py index 015a232e..9bfb3a88 100755 --- a/catalog/urls.py +++ b/catalog/urls.py @@ -29,6 +29,8 @@ path('gwas//', views.gwas_gcst, name='NHGRI-EBI GWAS Catalog Study'), # Browse Catalog + # Legacy URL: /browse/all/ -> redirected to /browse/scores/ + path('browse/all/', views.browse_all, name='Browse All'), # e.g.: /browse/scores/ path('browse/scores/', views.browse_scores, name='Browse Scores'), # e.g.: /browse/traits/ diff --git a/catalog/views.py b/catalog/views.py index 4e53afbc..50773205 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -152,6 +152,10 @@ def index(request): return render(request, 'catalog/index.html', context) +def browse_all(request): + return redirect('/browse/scores/', permanent=True) + + def browse_scores(request): context = {} From 9f8d3a96f7f6e258a92c9451fa8458d9fd561a61 Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Fri, 15 Dec 2023 17:00:18 +0000 Subject: [PATCH 03/11] Change URL so the pending studies page is only available on Curation website --- catalog/urls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/catalog/urls.py b/catalog/urls.py index 9bfb3a88..1769b0f0 100755 --- a/catalog/urls.py +++ b/catalog/urls.py @@ -37,8 +37,6 @@ path('browse/traits/', cache_page(cache_time)(views.browse_traits), name='Browse Traits'), # e.g.: /browse/studies/ path('browse/studies/', cache_page(cache_time)(views.browse_publications), name='Browse Publications'), - # e.g.: /browse/pending_studies/ - path('browse/pending_studies/', cache_page(cache_time)(views.browse_pending_publications), name='Browse Pending Publications'), # e.g.: /latest_release/ path('latest_release/', cache_page(cache_time)(views.latest_release), name='Latest Release'), @@ -84,6 +82,9 @@ ] if settings.PGS_ON_CURATION_SITE: + # e.g.: /browse/pending_studies/ + urlpatterns.append(path('browse/pending_studies/', cache_page(cache_time)(views.browse_pending_publications), name='Browse Pending Publications')) + # e.g.: /stats/ urlpatterns.append(path('stats/', views.stats, name='Stats')) From 58ac108b47210fdfde143ccb1cd7f9e3a981925a Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Tue, 19 Dec 2023 10:48:28 +0000 Subject: [PATCH 04/11] Combine code for ancestry distribution + update settings --- catalog/tables.py | 358 +++++++++++++++----------------------------- pgs_web/settings.py | 25 ++-- 2 files changed, 134 insertions(+), 249 deletions(-) diff --git a/catalog/tables.py b/catalog/tables.py index 2543fb87..4eb59e56 100644 --- a/catalog/tables.py +++ b/catalog/tables.py @@ -46,6 +46,120 @@ def publication_format(value, is_external=False): return format_html(f'{value.id} {citation}{extra_html}') +def ancestries_format(value, record, include_filter=False): + if not value: + return '-' + + anc_labels = constants.ANCESTRY_LABELS + stages = constants.PGS_STAGES + + ancestries_data = value + pgs_id = record.num + chart_id = f'ac_{pgs_id}' + data_stage = {} + data_title = {} + anc_list = {} + anc_all_list = { 'dev_all': set(), 'all': set() } + multi_anc = 'multi' + + # Fetch and store the data for each stage + for stage in stages: + if stage in ancestries_data: + ancestries_data_stage = ancestries_data[stage] + anc_list[stage] = set() + data_stage[stage] = [] + data_title[stage] = [] + + # Details of the multi ancestry data + multi_title = {} + if multi_anc in ancestries_data_stage: + for mt in ancestries_data_stage[multi_anc]: + (ma,anc) = mt.split('_') # e.g. MAO_AFR + if ma not in multi_title: + multi_title[ma] = [] + multi_title[ma].append(f'
  • {anc_labels[anc]}
  • ') + + if anc == 'MAE': + continue + # Add to the unique list of ancestries + anc_list[stage].add(anc) + # Add to the unique list of ALL ancestries + anc_all_list['all'].add(anc) + # Add to the unique list of DEV ancestries + if stage != 'eval': + anc_all_list['dev_all'].add(anc) + + # Ancestry data for the stage: distribution, list of ancestries and content of the chart tootlip + for key,val in sorted(ancestries_data_stage['dist'].items(), key=lambda item: float(item[1]), reverse=True): + data_stage[stage].append(f'"{key}",{val}') + extra_title = '' + if key in multi_title: + extra_title += '
      '+''.join(multi_title[key])+'
    ' + data_title[stage].append(f'
    {val}%{extra_title}
    ') + + if key == 'MAE': + continue + # Add to the unique list of ancestries + anc_list[stage].add(key) + # Add to the unique list of ALL ancestries + anc_all_list['all'].add(key) + # Add to the unique list of DEV ancestries + if stage != 'eval': + anc_all_list['dev_all'].add(key) + + + # Skip if no expected data "stage" available + if data_stage.keys() == 0: + return None + + # Format the data for each stage: build the HTML + html_list = [] + html_filter = [] + for stage in stages: + if stage in data_stage: + id = chart_id+'_'+stage + + # Code use to add filters (except in the Browse Scores table) + if include_filter: + anc_list_stage = anc_list[stage] + html_filter = add_ancestries_filter(stage,anc_list_stage,html_filter) + + title_count = '' + count = ancestries_data[stage]['count'] + if count != 0: + title_count = '{:,}'.format(count) + title = ''.join(data_title[stage])+title_count + html_chart = f'
    ' + html_list.append(html_chart) + else: + html_list.append('
    -
    ') + + + # Code use to add filters for all dev and all data (except in the Browse Scores table) + if include_filter: + for all_stages in anc_all_list.keys(): + if len(anc_all_list[all_stages]): + anc_all_data = anc_all_list[all_stages] + html_filter = add_ancestries_filter(all_stages,anc_all_data,html_filter) + + # Wrap up the HTML + html = '
    ' + html += ''.join(html_list) + html += '
    ' + return format_html(html) + + +def add_ancestries_filter(stage:str,anc_data:list,html_filter:list) -> list: + ''' Add ancestry filter for a given study stage ''' + if len(anc_data) > 1: + anc_data.add('MAO') + if 'EUR' not in anc_data: + anc_data.add('non-EUR') + anc_data = [f'"{x}"' for x in list(anc_data)] + html_filter.append("data-anc-"+stage+"='["+','.join(anc_data)+"]'") + return html_filter + + class Column_joinlist(tables.Column): def render(self, value): values = smaller_in_bracket('
    '.join(value)) @@ -157,6 +271,9 @@ def render(self, value): return format_html(value) +################################################################################ + + class Browse_PublicationTable(tables.Table): '''Table to browse Publications in the PGS Catalog''' id = tables.Column(accessor='id', verbose_name=format_html('PGS Publication/Study ID (PGP)')) @@ -314,94 +431,7 @@ def render_variants_number(self, value): def render_ancestries(self, value, record): - if not value: - return '-' - - anc_labels = constants.ANCESTRY_LABELS - stages = constants.PGS_STAGES - - ancestries_data = value - pgs_id = record.num - chart_id = f'ac_{pgs_id}' - data_stage = {} - data_title = {} - anc_list = {} - anc_all_list = { - 'dev_all': set(), - 'all': set() - } - - # Fetch the data for each stage - for stage in stages: - if stage in ancestries_data: - ancestries_data_stage = ancestries_data[stage] - anc_list[stage] = set() - # Details of the multi ancestry data - multi_title = {} - multi_anc = 'multi' - if multi_anc in ancestries_data_stage: - for mt in ancestries_data_stage[multi_anc]: - (ma,anc) = mt.split('_') - if ma not in multi_title: - multi_title[ma] = [] - multi_title[ma].append(f'
  • {anc_labels[anc]}
  • ') - - if anc == 'MAE': - continue - # Add to the unique list of ancestries - anc_list[stage].add(anc) - # Add to the unique list of ALL ancestries - anc_all_list['all'].add(anc) - # Add to the unique list of DEV ancestries - if stage != 'eval': - anc_all_list['dev_all'].add(anc) - - # Ancestry data for the stage: distribution, list of ancestries and content of the chart tootlip - data_stage[stage] = [] - data_title[stage] = [] - for key,val in sorted(ancestries_data_stage['dist'].items(), key=lambda item: float(item[1]), reverse=True): - label = anc_labels[key] - data_stage[stage].append(f'"{key}",{val}') - extra_title = '' - if key in multi_title: - extra_title += '
      '+''.join(multi_title[key])+'
    ' - data_title[stage].append(f'
    {val}%{extra_title}
    ') - - if key == 'MAE': - continue - # Add to the unique list of ancestries - anc_list[stage].add(key) - # Add to the unique list of ALL ancestries - anc_all_list['all'].add(key) - # Add to the unique list of DEV ancestries - if stage != 'eval': - anc_all_list['dev_all'].add(key) - - - # Skip if no expected data "stage" available - if data_stage.keys() == 0: - return None - - # Format the data for each stage: build the HTML - html_list = [] - for stage in stages: - if stage in data_stage: - id = chart_id+'_'+stage - title_count = '' - count = ancestries_data[stage]['count'] - if count != 0: - title_count = '{:,}'.format(count) - title = ''.join(data_title[stage])+title_count - html_chart = f'
    ' - html_list.append(html_chart) - else: - html_list.append('
    -
    ') - - # Wrap up the HTML - html = '
    ' - html += ''.join(html_list) - html += '
    ' - return format_html(html) + return ancestries_format(value, record) class ScoreTable(tables.Table): @@ -458,121 +488,7 @@ def render_variants_number(self, value): def render_ancestries(self, value, record): - if not value: - return '-' - - anc_labels = constants.ANCESTRY_LABELS - stages = constants.PGS_STAGES - - ancestries_data = value - pgs_id = record.num - chart_id = f'ac_{pgs_id}' - data_stage = {} - data_title = {} - anc_list = {} - anc_all_list = { - 'dev_all': set(), - 'all': set() - } - - # Fetch the data for each stage - for stage in stages: - if stage in ancestries_data: - ancestries_data_stage = ancestries_data[stage] - anc_list[stage] = set() - # Details of the multi ancestry data - multi_title = {} - multi_anc = 'multi' - if multi_anc in ancestries_data_stage: - for mt in ancestries_data_stage[multi_anc]: - (ma,anc) = mt.split('_') - if ma not in multi_title: - multi_title[ma] = [] - multi_title[ma].append(f'
  • {anc_labels[anc]}
  • ') - - if anc == 'MAE': - continue - # Add to the unique list of ancestries - anc_list[stage].add(anc) - # Add to the unique list of ALL ancestries - anc_all_list['all'].add(anc) - # Add to the unique list of DEV ancestries - if stage != 'eval': - anc_all_list['dev_all'].add(anc) - - # Ancestry data for the stage: distribution, list of ancestries and content of the chart tootlip - data_stage[stage] = [] - data_title[stage] = [] - for key,val in sorted(ancestries_data_stage['dist'].items(), key=lambda item: float(item[1]), reverse=True): - label = anc_labels[key] - data_stage[stage].append(f'"{key}",{val}') - extra_title = '' - if key in multi_title: - extra_title += '
      '+''.join(multi_title[key])+'
    ' - data_title[stage].append(f'
    {val}%{extra_title}
    ') - - if key == 'MAE': - continue - # Add to the unique list of ancestries - anc_list[stage].add(key) - # Add to the unique list of ALL ancestries - anc_all_list['all'].add(key) - # Add to the unique list of DEV ancestries - if stage != 'eval': - anc_all_list['dev_all'].add(key) - - - # Skip if no expected data "stage" available - if data_stage.keys() == 0: - return None - - # Format the data for each stage: build the HTML - html_list = [] - html_filter = [] - for stage in stages: - if stage in data_stage: - id = chart_id+'_'+stage - anc_list_stage = anc_list[stage] - - if len(anc_list_stage) > 1: - anc_list_stage.add('MAO') - if 'EUR' not in anc_list_stage: - anc_list_stage.add('non-EUR') - - anc_list_stage = [f'"{x}"' for x in list(anc_list_stage)] - - html_filter.append("data-anc-"+stage+"='["+','.join(anc_list_stage)+"]'") - - title_count = '' - count = ancestries_data[stage]['count'] - if count != 0: - title_count = '{:,}'.format(count) - title = ''.join(data_title[stage])+title_count - html_chart = f'
    ' - html_list.append(html_chart) - else: - html_list.append('
    -
    ') - - - # All dev and all data - for all_stages in anc_all_list.keys(): - if len(anc_all_list[all_stages]): - anc_all_data = anc_all_list[all_stages] - - if len(anc_all_data) > 1: - anc_all_data.add('MAO') - if 'EUR' not in anc_all_data: - anc_all_data.add('non-EUR') - - anc_all_data = [f'"{x}"' for x in list(anc_all_data)] - - html_filter.append("data-anc-"+all_stages+"='["+','.join(anc_all_data)+"]'") - - # Wrap up the HTML - html = '
    ' - html += ''.join(html_list) - html += '
    ' - return format_html(html) + return ancestries_format(value, record, True) class ScoreTableEval(ScoreTable): @@ -596,42 +512,6 @@ class Meta: template_name = 'catalog/pgs_catalog_django_table.html' -# class Browse_SampleSetTable(tables.Table): -# '''Table to browse SampleSets (PSS; used in PGS evaluations) in the PGS Catalog''' -# sample_merged = Column_sample_merged(accessor='display_samples_for_table', verbose_name='Sample Numbers', orderable=False) -# sample_ancestry = Column_ancestry(accessor='display_ancestry', verbose_name='Sample Ancestry', orderable=False) -# sampleset = tables.Column(accessor='display_sampleset', verbose_name=format_html('PGS Sample Set ID
    (PSS)'), orderable=False) -# phenotyping_free = Column_shorten_text_content(accessor='phenotyping_free', verbose_name='Phenotype Definitions and Methods') -# cohorts = Column_cohorts(accessor='cohorts', verbose_name='Cohort(s)') -# -# class Meta: -# model = Sample -# attrs = { -# "id": "sampleset_table", -# "data-show-columns" : "true", -# "data-page-size" : page_size, -# "data-export-options" : '{"fileName": "pgs_samplesets_data"}' -# } -# fields = [ -# 'sampleset', -# 'phenotyping_free', -# 'sample_merged', -# 'sample_ancestry','ancestry_additional', -# 'cohorts', 'cohorts_additional', -# ] -# -# template_name = 'catalog/pgs_catalog_django_table.html' -# -# def render_sampleset(self, value): -# sampleset = f'{value.id}' -# if is_pgs_on_curation_site == 'True' and value.name: -# sampleset += f'
    ({value.name})
    ' -# return format_html(sampleset) -# -# def render_cohorts_additional(self, value): -# return format_html('{}', value) - - class SampleTable_variants(tables.Table): '''Table on PGS page - displays information about the GWAS samples used''' sample_merged = Column_sample_merged(accessor='display_samples_for_table', verbose_name='Sample Numbers', orderable=False) diff --git a/pgs_web/settings.py b/pgs_web/settings.py index 4741b8fe..5cffbf66 100644 --- a/pgs_web/settings.py +++ b/pgs_web/settings.py @@ -59,7 +59,6 @@ 'rest_api.apps.RestApiConfig', 'search.apps.SearchConfig', 'benchmark.apps.BenchmarkConfig', - 'curation_tracker.apps.CurationTrackerConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -81,6 +80,9 @@ # Live app installation if PGS_ON_LIVE_SITE: INSTALLED_APPS.append('corsheaders') +# Curation app installation +if PGS_ON_CURATION_SITE: + INSTALLED_APPS.append('curation_tracker.apps.CurationTrackerConfig') # Debug helper if DEBUG == True: @@ -175,8 +177,10 @@ 'PASSWORD': os.environ['DATABASE_PASSWORD_2'], 'HOST': os.environ['DATABASE_HOST_2'], 'PORT': os.environ['DATABASE_PORT_2'] - }, - 'curation_tracker': { + } + } + if PGS_ON_CURATION_SITE: + DATABASES['curation_tracker'] = { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': os.environ['DATABASE_NAME_TRACKER'], 'USER': os.environ['DATABASE_USER_TRACKER'], @@ -184,7 +188,6 @@ 'HOST': os.environ['DATABASE_HOST_TRACKER'], 'PORT': os.environ['DATABASE_PORT_TRACKER'] } - } else: # Running locally so connect to either a local PostgreSQL instance or connect # to Cloud SQL via the proxy. To start the proxy via command line: @@ -206,8 +209,10 @@ 'PASSWORD': os.environ['DATABASE_PASSWORD_2'], 'HOST': 'localhost', 'PORT': os.environ['DATABASE_PORT_LOCAL_2'] - }, - 'curation_tracker': { + } + } + if PGS_ON_CURATION_SITE: + DATABASES['curation_tracker'] = { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': os.environ['DATABASE_NAME_TRACKER'], 'USER': os.environ['DATABASE_USER_TRACKER'], @@ -215,11 +220,11 @@ 'HOST': 'localhost', 'PORT': os.environ['DATABASE_PORT_LOCAL_TRACKER'] } - } # [END db_setup] -if 'PGS_CURATION_SITE' in os.environ: +# Router +if PGS_ON_CURATION_SITE: DATABASE_ROUTERS = ['routers.db_routers.AuthRouter',] @@ -271,7 +276,7 @@ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder' ] -if not os.getenv('GAE_APPLICATION', None): +if PGS_ON_GAE == 0: STATICFILES_FINDERS.append('compressor.finders.CompressorFinder') @@ -354,7 +359,7 @@ # Google Cloud Storage Settings # #---------------------------------# -if os.getenv('GAE_APPLICATION') and PGS_ON_CURATION_SITE: +if PGS_ON_GAE == 1 and PGS_ON_CURATION_SITE: from google.oauth2 import service_account GS_CREDENTIALS = service_account.Credentials.from_service_account_file( os.path.join(BASE_DIR, os.environ['GS_SERVICE_ACCOUNT_SETTINGS']) From 7a04e0eac5e8e9fd2c4a9e286ef6cd75c92cfc3b Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Wed, 20 Dec 2023 14:16:02 +0000 Subject: [PATCH 05/11] Hide the European ancestry checkbox if the selected ancestry is European --- catalog/static/catalog/pgs.js | 50 +++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/catalog/static/catalog/pgs.js b/catalog/static/catalog/pgs.js index d5046aa9..14486bcd 100644 --- a/catalog/static/catalog/pgs.js +++ b/catalog/static/catalog/pgs.js @@ -8,6 +8,7 @@ var anc_types = { var data_toggle_table = 'table[data-toggle="table"]'; var data_big_table = '.pgs_big_table'; var data_table_elements = [data_toggle_table,data_big_table]; +var anc_eur = 'EUR'; $(document).ready(function() { @@ -311,26 +312,31 @@ $(document).ready(function() { }); + /* + * Browse Scores Form + */ + // Ancestry filtering - Browse Scores var anc_form_name = 'browse_ancestry_form'; $('#browse_ancestry_type_list').on('change', function() { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); }); $('#browse_ancestry_filter_ind').on('change', function() { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); }); $("#browse_ancestry_filter_list").on("change", ".browse_ancestry_filter_cb",function() { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); }); + show_hide_european_filter(true); - // Search box events for the Browse Scores page + // Search box events for - Browse Scores $('#browse_scores_search_btn').on("click", function(e) { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); }); var $browse_scores_search_input = $('#browse_scores_search'); $browse_scores_search_input.on("keypress", function(e) { if (e.keyCode === 13) { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); } }); // Functions to set timer on typing before submitting the form @@ -339,7 +345,7 @@ $(document).ready(function() { $browse_scores_search_input.on('keyup', function () { clearTimeout(search_typing_timer); search_typing_timer = setTimeout(function() { - document.forms[anc_form_name].submit(); + submit_browse_score_form(); }, 1000); }); //on keydown, clear the countdown @@ -347,24 +353,47 @@ $(document).ready(function() { clearTimeout(search_typing_timer); }); - // Send form with updated URL (sort) + // Send form with updated URL (sort) - Browse Scores $('.orderable > a').click(function(e) { e.preventDefault(); var sort_url = $(this).attr('href'); var url = $('#'+anc_form_name).attr('action'); + show_hide_european_filter(); $('#'+anc_form_name).attr('action', url+sort_url).submit(); - //document.forms[anc_form_name].submit(); }); // Send form with updated URL (pagination) $('.pagination > li > a').click(function(e) { e.preventDefault(); var sort_url = $(this).attr('href'); var url = $('#'+anc_form_name).attr('action'); + show_hide_european_filter(); $('#'+anc_form_name).attr('action', url+sort_url).submit(); - //document.forms[anc_form_name].submit(); }); + function submit_browse_score_form() { + show_hide_european_filter(); + document.forms[anc_form_name].submit(); + } + + + function show_hide_european_filter(show_hide_parent) { + // Function to show/hide the European filter checkbox - Browse Scores page + var filter_ind_anc = $("#browse_ancestry_filter_ind option:selected").val(); + var $cb_eur_id_elem = $('#browse_anc_cb_EUR'); + if (filter_ind_anc == anc_eur) { + var default_val = $cb_eur_id_elem.data('default'); + $cb_eur_id_elem.prop('checked', default_val); + if (show_hide_parent) { + $cb_eur_id_elem.parent().hide(); + } + } + else if (filter_ind_anc != anc_eur && show_hide_parent) { + $cb_eur_id_elem.parent().show(); + } + } + + function filter_score_table() { /** Get data from Ancestry Filters form **/ @@ -395,7 +424,6 @@ $(document).ready(function() { var stage = $("#ancestry_type_list option:selected").val(); var anc_eur_cb = 'anc_cb_EUR'; - var anc_eur = 'EUR'; // Single ancestry selection + show/hide European checkbox filter var ind_anc = $("#ancestry_filter_ind option:selected").val(); From 13233c6be7e2df05d49b945b7d198a713752842d Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Tue, 9 Jan 2024 10:48:53 +0000 Subject: [PATCH 06/11] Add even catcher when the 'X' is clicked in the search box (Chrome) --- catalog/static/catalog/pgs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/catalog/static/catalog/pgs.js b/catalog/static/catalog/pgs.js index 14486bcd..59d46947 100644 --- a/catalog/static/catalog/pgs.js +++ b/catalog/static/catalog/pgs.js @@ -339,6 +339,10 @@ $(document).ready(function() { submit_browse_score_form(); } }); + // Catch event when the "X" button is clicked in the search box. + $browse_scores_search_input.on('search', function () { + submit_browse_score_form(); + }); // Functions to set timer on typing before submitting the form var search_typing_timer; //on keyup, start the countdown From 92adc4a7847188ca30fc7d8d9207e5e7a49cc397 Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Fri, 26 Jan 2024 14:28:13 +0000 Subject: [PATCH 07/11] Make the browse code more generic so it can be used for other pages (e.g. Browse Publications), move styling code into SCSS and fix rare case of duplicated results when using the search box --- catalog/static/catalog/pgs.js | 44 ++++++++++--------- catalog/static/catalog/pgs.scss | 13 ++++++ catalog/tables.py | 2 +- catalog/templates/catalog/browse/scores.html | 12 ++--- ...=> pgs_catalog_django_tables2_browse.html} | 0 catalog/views.py | 15 +++---- 6 files changed, 50 insertions(+), 36 deletions(-) rename catalog/templates/catalog/{pgs_catalog_django_tables2_browse_scores.html => pgs_catalog_django_tables2_browse.html} (100%) diff --git a/catalog/static/catalog/pgs.js b/catalog/static/catalog/pgs.js index 59d46947..77f16af5 100644 --- a/catalog/static/catalog/pgs.js +++ b/catalog/static/catalog/pgs.js @@ -317,43 +317,43 @@ $(document).ready(function() { */ // Ancestry filtering - Browse Scores - var anc_form_name = 'browse_ancestry_form'; + var browse_form_name = 'browse_form'; $('#browse_ancestry_type_list').on('change', function() { - submit_browse_score_form(); + submit_browse_form(); }); $('#browse_ancestry_filter_ind').on('change', function() { - submit_browse_score_form(); + submit_browse_form(); }); $("#browse_ancestry_filter_list").on("change", ".browse_ancestry_filter_cb",function() { - submit_browse_score_form(); + submit_browse_form(); }); show_hide_european_filter(true); // Search box events for - Browse Scores - $('#browse_scores_search_btn').on("click", function(e) { - submit_browse_score_form(); + $('#browse_search_btn').on("click", function(e) { + submit_browse_form(); }); - var $browse_scores_search_input = $('#browse_scores_search'); - $browse_scores_search_input.on("keypress", function(e) { + var $browse_search_input = $('#browse_search'); + $browse_search_input.on("keypress", function(e) { if (e.keyCode === 13) { - submit_browse_score_form(); + submit_browse_form(); } }); // Catch event when the "X" button is clicked in the search box. - $browse_scores_search_input.on('search', function () { - submit_browse_score_form(); + $browse_search_input.on('search', function () { + submit_browse_form(); }); // Functions to set timer on typing before submitting the form var search_typing_timer; //on keyup, start the countdown - $browse_scores_search_input.on('keyup', function () { + $browse_search_input.on('keyup', function () { clearTimeout(search_typing_timer); search_typing_timer = setTimeout(function() { - submit_browse_score_form(); + submit_browse_form(); }, 1000); }); //on keydown, clear the countdown - $browse_scores_search_input.on('keydown', function () { + $browse_search_input.on('keydown', function () { clearTimeout(search_typing_timer); }); @@ -361,23 +361,25 @@ $(document).ready(function() { $('.orderable > a').click(function(e) { e.preventDefault(); var sort_url = $(this).attr('href'); - var url = $('#'+anc_form_name).attr('action'); + var url = $('#'+browse_form_name).attr('action'); show_hide_european_filter(); - $('#'+anc_form_name).attr('action', url+sort_url).submit(); + $('#'+browse_form_name).attr('action', url+sort_url).submit(); }); // Send form with updated URL (pagination) $('.pagination > li > a').click(function(e) { e.preventDefault(); var sort_url = $(this).attr('href'); - var url = $('#'+anc_form_name).attr('action'); + var url = $('#'+browse_form_name).attr('action'); show_hide_european_filter(); - $('#'+anc_form_name).attr('action', url+sort_url).submit(); + $('#'+browse_form_name).attr('action', url+sort_url).submit(); }); - function submit_browse_score_form() { - show_hide_european_filter(); - document.forms[anc_form_name].submit(); + function submit_browse_form() { + if ($('#browse_anc_cb_EUR').length) { + show_hide_european_filter(); + } + document.forms[browse_form_name].submit(); } diff --git a/catalog/static/catalog/pgs.scss b/catalog/static/catalog/pgs.scss index d96ffc67..ae0fcf75 100644 --- a/catalog/static/catalog/pgs.scss +++ b/catalog/static/catalog/pgs.scss @@ -969,6 +969,18 @@ td > ul { } } +.pgs_search_toolbar { + display:flex; + justify-content:flex-end; + > div:first-child { + width:200px; + margin: 8px 5px 8px 8px; + } + > div:last-child { + margin: 8px 0px; + } +} + /* Icons related CSS */ .external-link:after { @@ -2528,6 +2540,7 @@ footer { } .anc_col_subtitle { + display:flex; font-size:12px; white-space:nowrap; text-align:center; diff --git a/catalog/tables.py b/catalog/tables.py index 4eb59e56..d08be142 100644 --- a/catalog/tables.py +++ b/catalog/tables.py @@ -402,7 +402,7 @@ class Meta: 'ancestries', 'ftp_link' ] - template_name = 'catalog/pgs_catalog_django_tables2_browse_scores.html' + template_name = 'catalog/pgs_catalog_django_tables2_browse.html' def render_id(self, value, record): diff --git a/catalog/templates/catalog/browse/scores.html b/catalog/templates/catalog/browse/scores.html index 8a345430..044b817e 100644 --- a/catalog/templates/catalog/browse/scores.html +++ b/catalog/templates/catalog/browse/scores.html @@ -27,14 +27,14 @@

    {{ view_name }}

    -
    {% csrf_token %} + {% csrf_token %} {% include "catalog/includes/ancestry_form.html" %} -
    -
    - +
    +
    +
    -
    -
    diff --git a/catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html b/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html similarity index 100% rename from catalog/templates/catalog/pgs_catalog_django_tables2_browse_scores.html rename to catalog/templates/catalog/pgs_catalog_django_tables2_browse.html diff --git a/catalog/views.py b/catalog/views.py index 50773205..068e4345 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -165,7 +165,7 @@ def browse_scores(request): 'browse_ancestry_filter_ind': 'sel', 'browse_anc_cb_EUR': 'cb', 'browse_anc_cb_multi': 'cb', - 'browse_scores_search': 'in' + 'browse_search': 'in' } # Init form data form_data = {} @@ -186,7 +186,7 @@ def browse_scores(request): form_data[input_name] = False score_only_attributes = ['id','name','trait_efo','trait_reported','variants_number','ancestries','license','publication__id','publication__date_publication','publication__journal','publication__firstauthor'] - queryset = Score.objects.only(*score_only_attributes).select_related('publication').all().prefetch_related(pgs_prefetch['trait']) + queryset = Score.objects.only(*score_only_attributes).select_related('publication').all().prefetch_related(pgs_prefetch['trait']).distinct() ## Sorting ## order_by = 'num' @@ -205,7 +205,7 @@ def browse_scores(request): anc_value = form_data['browse_ancestry_filter_ind'] anc_include_eur = form_data['browse_anc_cb_EUR'] anc_include_multi = form_data['browse_anc_cb_multi'] - browse_scores_search = form_data['browse_scores_search'] + browse_search = form_data['browse_search'] # Study step and ancestry selections if not anc_step or anc_step == 'all': if anc_value: @@ -244,11 +244,11 @@ def browse_scores(request): queryset = queryset.filter(Q(**{multi_filters[gwas_step]:False}) | Q(**{multi_filters[dev_step]:False}) | Q(**{multi_filters[eval_step]:False})) # Filter term from the table search box - if browse_scores_search: + if browse_search: queryset = queryset.filter( - Q(id__icontains=browse_scores_search) | Q(name__icontains=browse_scores_search) | - Q(trait_reported__icontains=browse_scores_search) | Q(trait_efo__label__icontains=browse_scores_search) | - Q(publication__id__icontains=browse_scores_search) | Q(publication__title__icontains=browse_scores_search) | Q(publication__firstauthor__icontains=browse_scores_search) + Q(id__icontains=browse_search) | Q(name__icontains=browse_search) | + Q(trait_reported__icontains=browse_search) | Q(trait_efo__label__icontains=browse_search) | + Q(publication__id__icontains=browse_search) | Q(publication__title__icontains=browse_search) | Q(publication__firstauthor__icontains=browse_search) ) # Data table @@ -269,7 +269,6 @@ def browse_scores(request): 'form_data': form_data, 'ancestry_filter': ancestry_filter(form_data), 'has_chart': 1, - # 'has_table': 1, 'is_browse_score': 1 } return render(request, 'catalog/browse/scores.html', context) From 37565e3fa71ed4e1f75ff80790b833131da5c9e3 Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Mon, 29 Jan 2024 16:58:56 +0000 Subject: [PATCH 08/11] Make the new 'browse' tables responsive --- .../templates/catalog/pgs_catalog_django_tables2_browse.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html b/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html index 71c9fac7..abed7432 100644 --- a/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html +++ b/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html @@ -1,7 +1,7 @@ {% load django_tables2 %} {% load i18n l10n %} {% block table-wrapper %} -
    +
    {% block table %} {% block table.thead %} From ad4602aca7625329d569e3453499fa285d82f179 Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Wed, 31 Jan 2024 15:22:02 +0000 Subject: [PATCH 09/11] Fix new ancestry filtering and push further web display fixes on small screens --- catalog/context_processors.py | 2 +- catalog/static/catalog/pgs.scss | 7 +- .../catalog/includes/ancestry_form.html | 2 +- catalog/views.py | 98 ++++++++++++++----- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/catalog/context_processors.py b/catalog/context_processors.py index 239dce37..022f157b 100644 --- a/catalog/context_processors.py +++ b/catalog/context_processors.py @@ -125,7 +125,7 @@ def pgs_ancestry_legend(request) -> str: return { 'ancestry_legend': ''' -
    +
    Ancestry legend
    {}
    '''.format(legend_html) diff --git a/catalog/static/catalog/pgs.scss b/catalog/static/catalog/pgs.scss index ae0fcf75..90dbe7a0 100644 --- a/catalog/static/catalog/pgs.scss +++ b/catalog/static/catalog/pgs.scss @@ -2411,7 +2411,10 @@ footer { display: flex; justify-content: flex-start; > div:not(:last-child) { - margin-right: 1rem!important; + margin-right: 1rem; + } + .filter_container { + margin-bottom: 1rem; } } /* Mobile phone display */ @@ -2420,7 +2423,7 @@ footer { flex-direction:column; justify-content: center; > div { - margin-right:0px; + margin-right:0px !important; } } } diff --git a/catalog/templates/catalog/includes/ancestry_form.html b/catalog/templates/catalog/includes/ancestry_form.html index 6e2cefaf..e8350fee 100644 --- a/catalog/templates/catalog/includes/ancestry_form.html +++ b/catalog/templates/catalog/includes/ancestry_form.html @@ -1,7 +1,7 @@ {% load static %}
    -
    +
    Filter PGS by Participant Ancestry
    diff --git a/catalog/views.py b/catalog/views.py index 068e4345..0047976e 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -1,4 +1,5 @@ -import os +import operator +from functools import reduce from django.http import Http404 from django.shortcuts import render,redirect from django.views.generic import TemplateView @@ -188,25 +189,21 @@ def browse_scores(request): score_only_attributes = ['id','name','trait_efo','trait_reported','variants_number','ancestries','license','publication__id','publication__date_publication','publication__journal','publication__firstauthor'] queryset = Score.objects.only(*score_only_attributes).select_related('publication').all().prefetch_related(pgs_prefetch['trait']).distinct() - ## Sorting ## - order_by = 'num' - sort_param = request.GET.get('sort') - if sort_param: - order_by = sort_param - queryset = queryset.order_by(order_by) - ## Filter ancestry ## gwas_step = 'gwas' dev_step = 'dev' eval_step = 'eval' study_steps = [gwas_step,dev_step,eval_step] + dev_all_steps = [gwas_step,dev_step] # Ancestry Type anc_step = form_data['browse_ancestry_type_list'] anc_value = form_data['browse_ancestry_filter_ind'] anc_include_eur = form_data['browse_anc_cb_EUR'] anc_include_multi = form_data['browse_anc_cb_multi'] browse_search = form_data['browse_search'] - # Study step and ancestry selections + + # Study step (gwas,development,evaluation) and ancestry dropdown selection + # [G,D,E] if not anc_step or anc_step == 'all': if anc_value: filters = {} @@ -214,34 +211,78 @@ def browse_scores(request): filters[step] = f'ancestries__{step}__dist__{anc_value}__isnull' queryset = queryset.filter(Q(**{filters[gwas_step]:False}) | Q(**{filters[dev_step]:False}) | Q(**{filters[eval_step]:False})) elif anc_step: + # G | D | E if anc_step in study_steps: - queryset = queryset.filter(ancestries__has_key=anc_step) + has_anc_step = Q(ancestries__has_key=anc_step) if anc_value: - anc_filter = f'ancestries__{anc_step}__dist__{anc_value}__isnull' - queryset = queryset.filter(**{anc_filter:False}) + queryset = queryset.filter(has_anc_step & Q(**{f'ancestries__{anc_step}__dist__{anc_value}__isnull':False})) + else: + queryset = queryset.filter(has_anc_step) + # [G,D] elif anc_step == 'dev_all': - queryset = queryset.filter(ancestries__has_key=gwas_step).filter(ancestries__has_key=dev_step) - if anc_value: - filters = {} - for step in ['gwas','dev']: - filters[step] = f'ancestries__{step}__dist__{anc_value}__isnull' - queryset = queryset.filter(Q(**{filters[gwas_step]:False}) | Q(**{filters[dev_step]:False})) + has_step = {} + for step in dev_all_steps: + has_step[step] = Q(ancestries__has_key=step) + if anc_value: + filters = {} + for step in dev_all_steps: + filters[step] = f'ancestries__{step}__dist__{anc_value}__isnull' + queryset = queryset.filter((has_step[gwas_step] & Q(**{filters[gwas_step]:False})) | + (has_step[dev_step] & Q(**{filters[dev_step]:False}))) + else: + queryset = queryset.filter(has_step[gwas_step] | has_step[dev_step]) # Filter out European ancestry (including multi-ancestry with european) if anc_include_eur == False: + eur_filters = {} + eur_query_list = [] + eur_anc_labels = ['EUR','MAE'] + eur_filters_steps = [] for step in study_steps: - anc_filter = f'ancestries__{step}__dist__EUR__isnull' - queryset = queryset.filter(**{anc_filter:True}) - anc_filter = f'ancestries__{step}__dist__MAE__isnull' - queryset = queryset.filter(**{anc_filter:True}) + for anc_label in eur_anc_labels: + if not step in eur_filters.keys(): + eur_filters[step] = {} + eur_filters[step][anc_label] = Q(**{f'ancestries__{step}__dist__{anc_label}__isnull':True}) + # [G,D,E] + if not anc_step or anc_step == 'all': + eur_filters_steps = study_steps + elif anc_step: + # G | D | E + if anc_step in study_steps: + eur_filters_steps = [anc_step] + # [G,D] + elif anc_step == 'dev_all': + eur_filters_steps = dev_all_steps + # Build European filter + if eur_filters_steps: + for eur_step in eur_filters_steps: + for anc_label in eur_anc_labels: + eur_query_list.append(eur_filters[eur_step][anc_label]) + # Update query filter + queryset = queryset.filter(reduce(operator.and_,eur_query_list)) # Filter to include multi-ancestry if anc_include_multi == True: multi_filters = {} + multi_query_list = [] for step in study_steps: - anc_m_filter = f'ancestries__{step}__multi__isnull' - multi_filters[step] = anc_m_filter - queryset = queryset.filter(Q(**{multi_filters[gwas_step]:False}) | Q(**{multi_filters[dev_step]:False}) | Q(**{multi_filters[eval_step]:False})) + multi_filters[step] = Q(**{f'ancestries__{step}__has_any_keys':['multi','dist_count']}) + # [G,D,E] + if not anc_step or anc_step == 'all': + multi_query_list = [multi_filters[gwas_step],multi_filters[dev_step],multi_filters[eval_step]] + elif anc_step: + # G | D | E + if anc_step in study_steps: + multi_query_list = [multi_filters[anc_step]] + # [G,D] + elif anc_step == 'dev_all': + multi_query_list = [multi_filters[gwas_step],multi_filters[dev_step]] + # Update query filter + if multi_query_list: + if len(multi_query_list) > 1: + queryset = queryset.filter(reduce(operator.or_,multi_query_list)) + else: + queryset = queryset.filter(multi_query_list[0]) # Filter term from the table search box if browse_search: @@ -251,6 +292,13 @@ def browse_scores(request): Q(publication__id__icontains=browse_search) | Q(publication__title__icontains=browse_search) | Q(publication__firstauthor__icontains=browse_search) ) + ## Sorting ## + order_by = 'num' + sort_param = request.GET.get('sort') + if sort_param: + order_by = sort_param + queryset = queryset.order_by(order_by) + # Data table table = Browse_ScoreTable(queryset) From 8eff925fb0d10aa24221b2e4b19e4e209e94554d Mon Sep 17 00:00:00 2001 From: Laurent Gil Date: Wed, 31 Jan 2024 16:14:21 +0000 Subject: [PATCH 10/11] Update browse page titles (in tabs) --- catalog/templates/catalog/browse/pending_publications.html | 2 +- catalog/templates/catalog/browse/publications.html | 2 +- catalog/templates/catalog/browse/scores.html | 2 +- catalog/templates/catalog/browse/traits.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/catalog/templates/catalog/browse/pending_publications.html b/catalog/templates/catalog/browse/pending_publications.html index 67aafbcc..5ca525ae 100644 --- a/catalog/templates/catalog/browse/pending_publications.html +++ b/catalog/templates/catalog/browse/pending_publications.html @@ -1,7 +1,7 @@ {% extends 'catalog/base.html' %} {% load render_table from django_tables2 %} -{% block title %}All {{ view_name }}{% endblock %} +{% block title %}Browse {{ view_name }}{% endblock %} {% block content %}