From 6f5a4bb9516a25b374f1ec919f063221066cdd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20=20MENAGER?= <herve.menager@pasteur.fr> Date: Tue, 31 Jul 2018 11:41:24 +0200 Subject: [PATCH] restructure the compounds list view * this view is now class based and a lot more structured * filters are handled supposedly in a clever manner Former-commit-id: 510e9b5282b06d28fb839ba20ff44d76ab41f584 --- ippisite/ippidb/templates/compound_list.html | 54 ++--- ippisite/ippidb/urls.py | 2 +- ippisite/ippidb/views.py | 233 ++++++++++++------- 3 files changed, 171 insertions(+), 118 deletions(-) diff --git a/ippisite/ippidb/templates/compound_list.html b/ippisite/ippidb/templates/compound_list.html index 8e1cd2cf..adc3a788 100644 --- a/ippisite/ippidb/templates/compound_list.html +++ b/ippisite/ippidb/templates/compound_list.html @@ -30,32 +30,32 @@ {% include "slider_button.html" with label="Publications" param_name="pubs" %} </div> <div class="m-2 d-flex "> - <span>{{ compounds.paginator.count }} compounds</span> - {% if selected_families or selected_ppis or selected_diseases or selected_taxonomies or selected_boundcomplexes or molecular_weight != molecular_weight_max or a_log_p != a_log_p_max or nb_donor_h != nb_donor_h_max or pubs != pubs_min %} + <span>{{ paginator.count }} compounds</span> + {% if selected_family or selected_ppi or selected_disease or selected_taxonomy or selected_boundcomplex or molecular_weight != molecular_weight_max or a_log_p != a_log_p_max or nb_donor_h != nb_donor_h_max or pubs != pubs_min %} <span> - filters: - {% if selected_families %} - {% for selected in selected_families %} + {% if selected_family %} + {% for selected in selected_family %} {% include "selected_badge.html" with param_name="family" selected=selected %} {% endfor %} {% endif %} - {% if selected_ppis %} - {% for selected in selected_ppis %} + {% if selected_ppi %} + {% for selected in selected_ppi %} {% include "selected_badge.html" with param_name="ppi" selected=selected %} {% endfor %} {% endif %} - {% if selected_diseases %} - {% for selected in selected_diseases %} + {% if selected_disease %} + {% for selected in selected_disease %} {% include "selected_badge.html" with param_name="disease" selected=selected %} {% endfor %} {% endif %} - {% if selected_taxonomies %} - {% for selected in selected_taxonomies %} + {% if selected_taxonomy %} + {% for selected in selected_taxonomy %} {% include "selected_badge.html" with param_name="taxonomy" selected=selected %} {% endfor %} {% endif %} - {% if selected_boundcomplexes %} - {% for selected in selected_boundcomplexes %} + {% if selected_boundcomplex %} + {% for selected in selected_boundcomplex %} {% include "selected_badge.html" with param_name="boundcomplex" selected=selected %} {% endfor %} {% endif %} @@ -96,20 +96,20 @@ </span> </div> </form> -{% if compounds %} +{% if page_obj %} <div class="container-fluid"> {% if display == 'v' %} <div class="d-flex flex-wrap m-2 justify-content-around"> - {% for compound in compounds %} + {% for compound in page_obj %} {% include "compound_v_item.html" with compound=compound %} {% endfor %} </div> {% elif display == 'l'%} - {% for compound in compounds %} + {% for compound in page_obj %} {% include "compound_l_item.html" with compound=compound %} {% endfor %} {% else %} - {% include "compound_t_list.html" with compounds=compounds %} + {% include "compound_t_list.html" with compounds=page_obj %} {% endif %} </div> {% else %} @@ -121,9 +121,9 @@ <nav class="d-flex"> <ul class="pagination pagination-sm ml-auto"> - {% if compounds.has_previous %} + {% if page_obj.has_previous %} <li class="page-item"> - <a class="page-link" href="?{% url_replace request 'page' compounds.previous_page_number %}" aria-label="Previous"> + <a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}" aria-label="Previous"> <span aria-hidden="true">«</span> <span class="sr-only">Previous</span> </a> @@ -131,12 +131,12 @@ {% endif %} <li class="page-item disabled"> - <span class="page-link">Page {{ compounds.number }} of {{ compounds.paginator.num_pages }}.</span> + <span class="page-link">Page {{ page_obj.number }} of {{ paginator.num_pages }}.</span> </li> - {% if compounds.has_next %} + {% if page_obj.has_next %} <li class="page-item"> - <a class="page-link" href="?{% url_replace request 'page' compounds.next_page_number %}" aria-label="Next"> + <a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}" aria-label="Next"> <span aria-hidden="true">»</span> <span class="sr-only">Next</span> </a> @@ -145,7 +145,7 @@ <li class="page-item ml-3"> <div class="input-group input-group-sm"> - <input id="pageNumber" type="number" min="1" max="{{compounds.paginator.num_pages}}" step="1" placeholder="page #" style="width: 4em;" aria-label="Page number"> + <input id="pageNumber" type="number" min="1" max="{{ paginator.num_pages }}" step="1" placeholder="page #" style="width: 4em;" aria-label="Page number"> <div class="input-group-append"> <button class="btn btn-outline-primary" type="button" onclick="modifyUrl('page',$('#pageNumber').val())">Go</button> </div> @@ -155,11 +155,11 @@ </ul> </nav> - {% include "multiselection_modal.html" with label="PPI Family" selected_list=selected_families unselected_list=families param_name="family" all_param_name="families_all" all_param_value=families_all %} - {% include "multiselection_modal.html" with label="PPI" selected_list=selected_ppis unselected_list=ppis param_name="ppi" all_param_name="ppis_all" all_param_value=ppis_all %} - {% include "multiselection_modal.html" with label="Disease" selected_list=selected_diseases unselected_list=diseases param_name="disease" all_param_name="diseases_all" all_param_value=diseases_all %} - {% include "multiselection_modal.html" with label="Organism" selected_list=selected_taxonomies unselected_list=taxonomies param_name="taxonomy" all_param_name="taxonomies_all" all_param_value=taxonomies_all %} - {% include "multiselection_modal.html" with label="Bound complex" selected_list=selected_boundcomplexes unselected_list=boundcomplexes param_name="boundcomplex" all_param_name="boundcomplexes_all" all_param_value=boundcomplexes_all %} + {% include "multiselection_modal.html" with label="PPI Family" selected_list=selected_family unselected_list=family param_name="family" all_param_name="family_all" all_param_value=family_all %} + {% include "multiselection_modal.html" with label="PPI" selected_list=selected_ppi unselected_list=ppi param_name="ppi" all_param_name="ppi_all" all_param_value=ppi_all %} + {% include "multiselection_modal.html" with label="Disease" selected_list=selected_disease unselected_list=disease param_name="disease" all_param_name="disease_all" all_param_value=disease_all %} + {% include "multiselection_modal.html" with label="Organism" selected_list=selected_taxonomy unselected_list=taxonomy param_name="taxonomy" all_param_name="taxonomy_all" all_param_value=taxonomy_all %} + {% include "multiselection_modal.html" with label="Bound complex" selected_list=selected_boundcomplex unselected_list=boundcomplex param_name="boundcomplex" all_param_name="boundcomplex_all" all_param_value=boundcomplex_all %} {% include "slider_modal.html" with label="Molecular Weight" param_name="molecular_weight" param_min=molecular_weight_min param_max=molecular_weight_max param_value=molecular_weight step='50' param_label='Select a cutoff value for the molecular weight of the compounds to be selected'%} {% include "slider_modal.html" with label="AlogP" param_name="a_log_p" param_min=a_log_p_min param_max=a_log_p_max param_value=a_log_p step='1' param_label='Select a cutoff value for the AlogP of the compounds to be selected'%} {% include "slider_modal.html" with label="H donors" param_name="nb_donor_h" param_min=nb_donor_h_min param_max=nb_donor_h_max param_value=nb_donor_h step='1' param_label='Select a cutoff value for the number of H donors in the compounds to be selected'%} diff --git a/ippisite/ippidb/urls.py b/ippisite/ippidb/urls.py index 82df36de..db1fb11d 100644 --- a/ippisite/ippidb/urls.py +++ b/ippisite/ippidb/urls.py @@ -11,7 +11,7 @@ urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^about$', views.about, name='about'), url(r'^about/general/$', views.general, name='general'), - url(r'^compounds/$', views.compound_list, name='compound_list'), + url(r'^compounds/$', views.CompoundListView.as_view(), name='compound_listbeta'), url(r'^compounds/(?P<compound_id>\w+)$', views.compound_card, name='compound_card'), url(r'^tutorials$', views.tutorials, name='tutorials'), url(r'^admin-session$', views.adminSession, name='admin-session'), diff --git a/ippisite/ippidb/views.py b/ippisite/ippidb/views.py index 6ae89210..62077cb8 100644 --- a/ippisite/ippidb/views.py +++ b/ippisite/ippidb/views.py @@ -7,6 +7,7 @@ from django.shortcuts import render from django.http import HttpResponseRedirect, Http404 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic.list import ListView from formtools.wizard.views import SessionWizardView, NamedUrlSessionWizardView import ippidb @@ -143,99 +144,151 @@ class IppiWizard(LoginRequiredMixin, NamedUrlSessionWizardView): 'form_data': [form.cleaned_data for form in form_list], }) +class CompoundListFilterHandler(object): + + def __init__(self, filter_class, relation_from_compound, relation_to_compound, parameter_name, filter_context, request_get): + self.filter_class = filter_class + self.relation_from_compound = relation_from_compound + self.parameter_name = parameter_name + self.filter_context = filter_context + self.relation_from_compound = relation_from_compound + self.relation_to_compound = relation_to_compound + self.value = request_get.getlist(parameter_name) + + def process(self, queryset): + """ to be called during queryset filtering """ + # if a value has been set for this filter + if self.value: + # filter queryset on the ID values specified + queryset = queryset.filter(**{self.relation_from_compound + '__id__in': self.value}) + # selected objects on the ID values specified + self.filter_context['selected_' + self.parameter_name] = self.filter_class.objects.filter(id__in = self.value) + # unselected objects on the ID values not specified + self.filter_context[self.parameter_name] = self.filter_class.objects.exclude(id__in = self.value) + return queryset + + def post_process(self, compound_ids, queryset): + """ to be called after queryset filtering """ + # if no value has been set for this filter + if not self.value: + # selected objects is an empty queryset + self.filter_context['selected_' + self.parameter_name] = self.filter_class.objects.none() + if compound_ids: + # unselected objects is the range related to the current queryset + self.filter_context[self.parameter_name] = self.filter_class.objects.filter(**{self.relation_to_compound + '__id__in':compound_ids}).distinct() + else: + # unselected objects are all objects if no filter is set + self.filter_context[self.parameter_name] = self.filter_class.objects.all() + + +class CompoundCutoffFilterHandler(object): + + def __init__(self, parameter_name, cutoff_dir, filter_context, request_get): + self.parameter_name = parameter_name + self.cutoff_dir = cutoff_dir + self.filter_context = filter_context + self.value = request_get.get(parameter_name) + + def process(self, queryset): + """ to be called during queryset filtering """ + # if a value has been set for this filter + if self.value: + # filter queryset on the ID values specified + self.filter_context[self.parameter_name] = self.value + filter_dict = {self.parameter_name + '__' + self.cutoff_dir + 'te': self.value} + queryset = queryset.filter(**filter_dict) + # max and min value are the max and min for + self.filter_context[self.parameter_name+'_max'] = str(int(math.ceil(float(Compound.objects.aggregate(Max(self.parameter_name))[self.parameter_name + '__max'] or 0)))) + self.filter_context[self.parameter_name+'_min'] = str(int(math.floor(float(Compound.objects.aggregate(Min(self.parameter_name))[self.parameter_name + '__min'] or 0)))) + return queryset + + def post_process(self, compound_ids, queryset): + """ to be called after queryset filtering """ + # if no value has been set for this filter + if not self.value: + self.filter_context[self.parameter_name+'_max'] = str(int(math.ceil(float(queryset.aggregate(Max(self.parameter_name))[self.parameter_name + '__max'] or 0)))) + self.filter_context[self.parameter_name+'_min'] = str(int(math.floor(float(queryset.aggregate(Min(self.parameter_name))[self.parameter_name + '__min'] or 0)))) + if self.cutoff_dir == 'l': + self.filter_context[self.parameter_name] = self.filter_context[self.parameter_name+'_max'] + else: + self.filter_context[self.parameter_name] = self.filter_context[self.parameter_name+'_min'] + + +class CompoundListView(ListView): + + model = Compound + template_name = "compound_list.html" + paginate_by = 15 + + table_view_default_fields = ['id', 'common_name', 'molecular_weight', 'a_log_p', 'compound_action_pdb_ids', 'pubs'] -def process_cutoff_value(name, context, request, cutoff_dir='l'): - c = context['compounds'] - context[name+'_max'] = str(int(math.ceil(float(c.aggregate(Max(name))[name + '__max'] or 0)))) - context[name+'_min'] = str(int(math.floor(float(c.aggregate(Min(name))[name + '__min'] or 0)))) - if request.GET.get(name): - context[name] = request.GET.get(name) - filter_dict = {name + '__' + cutoff_dir + 'te': context[name]} - context['compounds'] = context['compounds'].filter(**filter_dict) - elif cutoff_dir == 'l': - context[name] = context[name+'_max'] - else: - context[name] = context[name+'_min'] - return context - -def compound_list(request): - """ - Display the list of compounds - """ - context = {} - # pubs is the number of publications referring to the compound - compounds = Compound.objects.annotate(pubs=Count('refcompoundbiblio')) - context['compounds'] = compounds - if request.GET.get('family'): - context['compounds'] = context['compounds'].filter(compoundaction__ppi__family__id__in=request.GET.getlist('family')) - if request.GET.get('ppi'): - context['compounds'] = context['compounds'].filter(compoundaction__ppi__id__in=request.GET.getlist('ppi')) - if request.GET.get('disease'): - context['compounds'] = context['compounds'].filter(compoundaction__ppi__diseases__id__in=request.GET.getlist('disease')) - if request.GET.get('taxonomy'): - context['compounds'] = context['compounds'].filter(compoundaction__ppi_id__ppicomplex__complex__protein__organism__id__in=request.GET.getlist('taxonomy')) - if request.GET.get('boundcomplex'): - context['compounds'] = context['compounds'].filter(compoundaction__ppi_id__ppicomplex__complex__id__in=request.GET.getlist('boundcomplex')) - context = process_cutoff_value('molecular_weight', context, request) - context = process_cutoff_value('a_log_p', context, request) - context = process_cutoff_value('nb_donor_h', context, request) - context = process_cutoff_value('pubs', context, request, 'g') - selected_families = PpiFamily.objects.filter(id__in=request.GET.getlist('family')) - families = PpiFamily.objects.exclude(id__in=request.GET.getlist('family')) - selected_ppis = Ppi.objects.filter(id__in=request.GET.getlist('ppi')) - ppis = Ppi.objects.exclude(id__in=request.GET.getlist('ppi')) - selected_diseases = Disease.objects.filter(id__in=request.GET.getlist('disease')) - diseases = Disease.objects.exclude(id__in=request.GET.getlist('disease')) - selected_taxonomies = Taxonomy.objects.filter(id__in=request.GET.getlist('taxonomy')) - taxonomies = Taxonomy.objects.exclude(id__in=request.GET.getlist('taxonomy')) - selected_boundcomplexes = ProteinDomainBoundComplex.objects.filter(id__in=request.GET.getlist('boundcomplex')) - boundcomplexes = ProteinDomainBoundComplex.objects.exclude(id__in=request.GET.getlist('boundcomplex')) sort_by_option_ids = ['id', 'molecular_weight', 'a_log_p', 'nb_aromatic_sssr', 'nb_chiral_centers', 'pubs'] - sort_by = request.GET.get('sort_by', 'id') - context['compounds'] = context['compounds'].order_by(sort_by) - context['compounds'] = context['compounds'].distinct() - # handle pagination in compounds list - paginator = Paginator(context['compounds'], 15) - page = request.GET.get('page') - try: - context['compounds'] = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - context['compounds'] = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - context['compounds'] = paginator.page(paginator.num_pages) - display = request.GET.get('display') - if display not in ['l', 't']: - display = 'v' - context['display']=display - compound_fields = {f.name: f.verbose_name for f in Compound._meta.get_fields() if not(f.is_relation)} - sort_by_options = OrderedDict() - for sort_by_option_id in sort_by_option_ids: - if sort_by_option_id == 'pubs': - name = 'Number of publications' + + def get_ordering(self): + sort_by = self.request.GET.get('sort_by', 'id') + return sort_by + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # process display configuration + display = self.request.GET.get('display') + if display not in ['l', 't']: + display = 'v' + context['display']=display + # displayed fields in "table" mode + fields = self.request.GET.getlist('fields', self.table_view_default_fields) + if 'id' not in fields: + fields.append('id') + context['fields'] = fields + # filters + context.update(self.filter_context) + # sort by options + compound_fields = {f.name: f.verbose_name for f in Compound._meta.get_fields() if not(f.is_relation)} + sort_by_options = OrderedDict() + for sort_by_option_id in self.sort_by_option_ids: + if sort_by_option_id == 'pubs': + name = 'Number of publications' + else: + name = compound_fields.get(sort_by_option_id) + sort_by_options[sort_by_option_id] = {'name': name, 'order':'ascending', 'id': sort_by_option_id} + sort_by_options['-'+sort_by_option_id] = {'name': name, 'order':'descending', 'id': sort_by_option_id} + context['sort_by'] = self.get_ordering() + context['sort_by_options'] = sort_by_options + # return context + return context + + def get_queryset(self): + self.filter_context = {} + # get queryset + qs = super().get_queryset() + # annotate + qs = qs.annotate(pubs=Count('refcompoundbiblio')) + # add filters + cfhs = [\ + CompoundListFilterHandler(PpiFamily, 'compoundaction__ppi__family', 'ppi__compoundaction__compound', 'family', self.filter_context, self.request.GET), + CompoundListFilterHandler(Ppi, 'compoundaction__ppi', 'compoundaction__compound', 'ppi', self.filter_context, self.request.GET), + CompoundListFilterHandler(Disease, 'compoundaction__ppi__diseases', 'ppi__compoundaction__compound', 'disease', self.filter_context, self.request.GET), + CompoundListFilterHandler(Taxonomy, 'compoundaction__ppi_id__ppicomplex__complex__protein__organism', 'protein__proteindomaincomplex__ppicomplex__ppi__compoundaction__compound', 'taxonomy', self.filter_context, self.request.GET), + CompoundListFilterHandler(ProteinDomainBoundComplex, 'compoundaction__ppi_id__ppicomplex__complex', 'ppicomplex__ppi__compoundaction__compound', 'boundcomplex', self.filter_context, self.request.GET), + CompoundCutoffFilterHandler('molecular_weight', 'l', self.filter_context, self.request.GET), + CompoundCutoffFilterHandler('a_log_p', 'l', self.filter_context, self.request.GET), + CompoundCutoffFilterHandler('nb_donor_h', 'l', self.filter_context, self.request.GET), + CompoundCutoffFilterHandler('pubs', 'g', self.filter_context, self.request.GET), + ] + for cfh in cfhs: + qs = cfh.process(qs) + # sort results + # post filter filters + if self.filter_context: + # compound ids in the final selection if a selection has happened + cids = [c for c in qs.all().values_list('id', flat=True)] else: - name = compound_fields.get(sort_by_option_id) - sort_by_options[sort_by_option_id] = {'name': name, 'order':'ascending', 'id': sort_by_option_id} - sort_by_options['-'+sort_by_option_id] = {'name': name, 'order':'descending', 'id': sort_by_option_id} - - fields = request.GET.getlist('fields',['id', 'common_name', 'molecular_weight', 'a_log_p', 'compound_action_pdb_ids', 'pubs']) - if 'id' not in fields: - fields.append('id') - context['selected_ppis'] = selected_ppis - context['ppis'] = ppis - context['selected_families'] = selected_families - context['families'] = families - context['selected_diseases'] = selected_diseases - context['diseases'] = diseases - context['selected_taxonomies'] = selected_taxonomies - context['taxonomies'] = taxonomies - context['selected_boundcomplexes'] = selected_boundcomplexes - context['boundcomplexes'] = boundcomplexes - context['fields'] = fields - context['sort_by'] = sort_by - context['sort_by_options'] = sort_by_options - return render(request, 'compound_list.html', context) + cids = None + for cfh in cfhs: + cfh.post_process(cids, qs) + # return queryset + return qs + def compound_card(request, compound_id): try: -- GitLab