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..022f157b 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..9165b206 100644 --- a/catalog/static/catalog/pgs.js +++ b/catalog/static/catalog/pgs.js @@ -6,6 +6,9 @@ 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() { @@ -113,54 +116,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 +243,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 +312,95 @@ $(document).ready(function() { }); - function filter_score_table() { + /* + * Browse Scores Form + */ + + // Ancestry filtering - Browse Scores + var browse_form_name = 'browse_form'; + $('#browse_ancestry_type_list').on('change', function() { + submit_browse_form(); + }); + $('#browse_ancestry_filter_ind').on('change', function() { + submit_browse_form(); + }); + $("#browse_ancestry_filter_list").on("change", ".browse_ancestry_filter_cb",function() { + submit_browse_form(); + }); + show_hide_european_filter(true); + + // Search box events for - Browse Scores + $('#browse_search_btn').on("click", function(e) { + submit_browse_form(); + }); + var $browse_search_input = $('#browse_search'); + $browse_search_input.on("keypress", function(e) { + if (e.keyCode === 13) { + submit_browse_form(); + } + }); + // Catch event when the "X" button is clicked in the search box. + $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_search_input.on('keyup', function () { + clearTimeout(search_typing_timer); + search_typing_timer = setTimeout(function() { + submit_browse_form(); + }, 1000); + }); + //on keydown, clear the countdown + $browse_search_input.on('keydown', function () { + clearTimeout(search_typing_timer); + }); + // Send form with updated URL (sort) - Browse Scores + $('.orderable > a').click(function(e) { + e.preventDefault(); + var sort_url = $(this).attr('href'); + var url = $('#'+browse_form_name).attr('action'); + show_hide_european_filter(); + $('#'+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 = $('#'+browse_form_name).attr('action'); + show_hide_european_filter(); + $('#'+browse_form_name).attr('action', url+sort_url).submit(); + }); + + + function submit_browse_form() { + if ($('#browse_anc_cb_EUR').length) { + show_hide_european_filter(); + } + document.forms[browse_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 **/ // Traits // @@ -326,29 +429,29 @@ $(document).ready(function() { $(sample_table_id).bootstrapTable('filterBy', {}); var stage = $("#ancestry_type_list option:selected").val(); - var anc_eur_cb = 'anc_cb_EUR'; - var anc_eur = 'EUR'; + var anc_eur_cb_id = 'anc_cb_EUR'; + var $anc_eur_cb = $('#'+anc_eur_cb_id); // Single ancestry selection + show/hide European checkbox filter var ind_anc = $("#ancestry_filter_ind option:selected").val(); if (ind_anc != '') { // Hide European checkbox if European selected in the dropdown if (ind_anc == anc_eur) { - var default_val = $('#'+anc_eur_cb).data('default'); - $('#'+anc_eur_cb).prop('checked', default_val); - $('#'+anc_eur_cb).parent().hide(); + var default_val = $anc_eur_cb.data('default'); + $anc_eur_cb.prop('checked', default_val); + $anc_eur_cb.parent().hide(); } anc_filter.push(ind_anc); } // Show European checkbox if European is not selected in the dropdown if (ind_anc != anc_eur) { - $('#'+anc_eur_cb).parent().show(); + $anc_eur_cb.parent().show(); } // Fetch checkboxes selection $(".ancestry_filter_cb").each(function () { // Add filter when "European" checkbox is NOT checked - if ($(this).attr('id') == anc_eur_cb) { + if ($(this).attr('id') == anc_eur_cb_id) { if (!$(this).prop('checked')) { anc_filter.push('non-'+anc_eur); } @@ -362,8 +465,11 @@ $(document).ready(function() { /** Filter the PGS Scores table **/ + if (anc_filter_length != 0 || stage || trait_filter != '') { - if ((anc_filter_length != 0 && stage) || trait_filter != '') { + if (stage) { + anc_filter_length += 1; + } if (trait_filter != '') { anc_filter_length += 1; @@ -383,23 +489,49 @@ $(document).ready(function() { } pgs_ids_list_link[scores_table_id] = []; - var data = $(scores_table_id).bootstrapTable('getData'); + // Filter each row $.each(data,function(i, row) { var pass_filter = 0; // Ancestry - if (anc_filter.length != 0 && stage) { + if (anc_filter.length != 0 || stage) { var ancestry_html = $(row[ancestry_col]); - var anc_list = ancestry_html.attr('data-anc-'+stage); - if (!anc_list) { - return; + var stages_list = [stage]; + if (stage == 'all') { + stages_list = ['gwas','dev','eval']; } - anc_list = JSON.parse(anc_list); - - for (var f in anc_filter) { - if (anc_list.includes(anc_filter[f])) { - pass_filter += 1; + var pass_anc_stage = 0; + var pass_anc_filter = 0; + for (var i=0; i 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; + } + } +} + +.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 { font-family: "Font Awesome 6 Free"; @@ -2350,7 +2416,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 */ @@ -2359,7 +2428,7 @@ footer { flex-direction:column; justify-content: center; > div { - margin-right:0px; + margin-right:0px !important; } } } @@ -2478,6 +2547,22 @@ footer { } } +.anc_col_subtitle { + display:flex; + 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..12e60d06 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): @@ -45,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(), 'any': 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['any'].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 += '' + 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['any'].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)) @@ -156,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)')) @@ -167,10 +285,8 @@ class Browse_PublicationTable(tables.Table): class Meta: model = Publication attrs = { - "data-show-columns" : "true", - "data-sort-name" : "id", - "data-page-size" : page_size, - "data-export-options" : '{"fileName": "pgs_publications_data"}' + "id": "publications_table", + "data-sort-name" : "id" } fields = [ 'id', @@ -183,7 +299,7 @@ class Meta: 'doi', 'PMID' ] - template_name = 'catalog/pgs_catalog_django_table.html' + template_name = 'catalog/pgs_catalog_django_tables2_browse.html' def render_id(self, value): return format_html('{}', value, value) @@ -267,16 +383,13 @@ 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-show-columns" : "true", - "data-sort-name" : "id", - "data-page-size" : page_size, - "data-export-options" : '{"fileName": "pgs_scores_data"}' + "data-sort-name" : "id" } fields = [ 'id', @@ -287,7 +400,8 @@ class Meta: 'ancestries', 'ftp_link' ] - template_name = 'catalog/pgs_catalog_django_table.html' + template_name = 'catalog/pgs_catalog_django_tables2_browse.html' + def render_id(self, value, record): return score_format(record) @@ -315,132 +429,75 @@ 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 = {} - multi_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) - + return ancestries_format(value, record) - # 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') +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) - anc_list_stage = [f'"{x}"' for x in list(anc_list_stage)] + class Meta: + model = Score + attrs = { + "id": "scores_table", + "data-show-columns" : "true", + "data-sort-name" : "id", + "data-page-size" : page_size, + "data-export-options" : '{"fileName": "pgs_scores_data"}' + } + fields = [ + 'id', + 'publication', + 'trait_reported', + 'trait_efo', + 'variants_number', + 'ancestries', + 'ftp_link' + ] + template_name = 'catalog/pgs_catalog_django_table.html' - html_filter.append("data-anc-"+stage+"='["+','.join(anc_list_stage)+"]'") + def render_id(self, value, record): + return score_format(record) - 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('
    -
    ') + 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)) - # 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] + 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}') - 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)] + def render_variants_number(self, value): + return '{:,}'.format(value) - 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) + def render_ancestries(self, value, record): + return ancestries_format(value, record, True) -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", @@ -453,42 +510,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/catalog/templates/catalog/browse/pending_publications.html b/catalog/templates/catalog/browse/pending_publications.html new file mode 100644 index 00000000..5ca525ae --- /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 %}Browse {{ 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..c6ba8598 --- /dev/null +++ b/catalog/templates/catalog/browse/publications.html @@ -0,0 +1,45 @@ +{% extends 'catalog/base.html' %} +{% load render_table from django_tables2 %} + +{% block title %}Browse {{ view_name }}{% endblock %} + +{% block desc %} + +{% endblock %} + +{% block content %} + +
    +
    +

    {{ view_name }}

    + +
    {% csrf_token %} +
    +
    + +
    +
    + +
    +
    + {% 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..5ac2ed2d --- /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 %}Browse Polygenic Scores (PGS){% 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..abd871d4 --- /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 %}Browse {{ 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..9b2ead57 --- /dev/null +++ b/catalog/templates/catalog/includes/ancestry_form.html @@ -0,0 +1,63 @@ +{% 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 de7dac25..54675961 100644 --- a/catalog/templates/catalog/pgp.html +++ b/catalog/templates/catalog/pgp.html @@ -90,22 +90,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.html b/catalog/templates/catalog/pgs_catalog_django_tables2_browse.html new file mode 100644 index 00000000..abed7432 --- /dev/null +++ b/catalog/templates/catalog/pgs_catalog_django_tables2_browse.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 896d1c9a..8d4b8a98 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 + # 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/ + 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.: /latest_release/ path('latest_release/', cache_page(cache_time)(views.latest_release), name='Latest Release'), @@ -78,6 +85,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')) diff --git a/catalog/views.py b/catalog/views.py index 12c67a00..ad1460d0 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -1,10 +1,11 @@ -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 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 +52,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 +153,313 @@ def index(request): return render(request, 'catalog/index.html', context) -def browseby(request, view_selection): +def browse_all(request): + return redirect('/browse/scores/', permanent=True) + + +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_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']).distinct() + + ## 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] + # Filters + g_d_e = [] + g_e = [] + # 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 (gwas,development,evaluation) and ancestry dropdown selection + # [G | D | E] + if not anc_step or anc_step == 'any': + 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: + # [G + D + E] - Build "All" filter + if anc_step == 'all': + for step in study_steps: + g_d_e.append(Q(ancestries__has_key=step)) + if step == 'dev': + g_e.append(~Q(ancestries__has_key=step)) + else: + g_e.append(Q(ancestries__has_key=step)) + + if anc_value: + g_d_e.append(Q(**{f'ancestries__{step}__dist__{anc_value}__isnull':False})) + if step != 'dev': + g_e.append(Q(**{f'ancestries__{step}__dist__{anc_value}__isnull':False})) + # G | D | E + elif anc_step in study_steps: + query_list = [] + query_list.append(Q(ancestries__has_key=anc_step)) + if anc_value: + query_list.append(Q(**{f'ancestries__{anc_step}__dist__{anc_value}__isnull':False})) + queryset = queryset.filter(reduce(operator.and_,query_list)) + # [G,D] + elif anc_step == 'dev_all': + has_step = {} + for step in dev_all_steps: + has_step[step] = Q(ancestries__has_key=step) + for step in dev_all_steps: + if anc_value: + filters = {} + for step2 in dev_all_steps: + filters[step2] = f'ancestries__{step2}__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: + 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 == 'any': + eur_filters_steps = study_steps + # [G + D + E] - Build "All" filter + elif anc_step == 'all': + for step in study_steps: + for anc_label in eur_anc_labels: + g_d_e.append(eur_filters[step][anc_label]) + if step != 'dev': + g_e.append(eur_filters[step][anc_label]) + 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: + multi_filters[step] = Q(**{f'ancestries__{step}__has_any_keys':['multi','dist_count']}) + # [G | D | E] + if not anc_step or anc_step == 'any': + for step in study_steps: + multi_query_list.append(multi_filters[step]) + # [G + D + E] - Build "All" filter + elif anc_step == 'all': + for step in study_steps: + g_d_e.append(multi_filters[step]) + if step != 'dev': + g_e.append(multi_filters[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 and anc_step != 'all': + if len(multi_query_list) > 1: + queryset = queryset.filter(reduce(operator.or_,multi_query_list)) + else: + queryset = queryset.filter(multi_query_list[0]) + + # Finalise the "All" filter + if anc_step == 'all': + g_d_e_filter = reduce(operator.and_,g_d_e) + g_e_filter = reduce(operator.and_,g_e) + queryset = queryset.filter(g_d_e_filter | g_e_filter) + + # Filter term from the table search box + if browse_search: + queryset = queryset.filter( + 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) + ) + + ## Sorting ## + order_by = 'num' + sort_param = request.GET.get('sort') + if sort_param: + order_by = sort_param + queryset = queryset.order_by(order_by) - context['has_table'] = 1 + # Data table + table = Browse_ScoreTable(queryset) - return render(request, 'catalog/browseby.html', context) + # 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 = { + 'view_name': 'Polygenic Scores (PGS)', + 'table': table, + 'form_data': form_data, + 'ancestry_filter': ancestry_filter(form_data), + 'has_chart': 1, + 'is_browse_score': 1 + } + return render(request, 'catalog/browse/scores.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): + context = {} + + # Ancestry form + input_names = { + 'browse_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 + + publication_defer = ['authors','curation_status','curation_notes','date_released'] + publication_prefetch_related = [pgs_prefetch['publication_score'], pgs_prefetch['publication_performance']] + queryset = Publication.objects.defer(*publication_defer).all().prefetch_related(*publication_prefetch_related) + + browse_search = form_data['browse_search'] + + ## Filter by search ## + if browse_search: + queryset = queryset.filter( + Q(id__icontains=browse_search) | Q(title__icontains=browse_search) | + Q(firstauthor__icontains=browse_search) | Q(PMID__icontains=browse_search) | + Q(journal__icontains=browse_search) | Q(doi__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_PublicationTable(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 = { + 'view_name': 'Publications', + 'has_ebi_icons': 1, + 'form_data': form_data, + 'table': table + } + 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 +483,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 +587,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 +641,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 +723,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 +763,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 +818,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 +833,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 5c5470d3..3782389c 100644 --- a/pgs_web/settings.py +++ b/pgs_web/settings.py @@ -83,8 +83,10 @@ # Curation app installation if PGS_ON_CURATION_SITE: INSTALLED_APPS.append('curation_tracker.apps.CurationTrackerConfig') -# 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', @@ -98,6 +100,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' @@ -110,7 +117,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: 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','')