import django_filters from django import forms from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field from collections import OrderedDict from .models import ( Case, CaseCollection, Condition, Finding, PathologicalProcess, Presentation, Procedure, QuestionSchema, Series, Structure, Subspecialty, NormalCase, Resource, Site, ) from django.contrib.auth.models import User from django.db.models import Q from django.http import QueryDict def get_authors(request): if request is None: return User.objects.none() if not request.user.groups.filter(name="atlas_editor").exists(): queryset = User.objects.filter(id=request.user.id) else: queryset = User.objects.all() return queryset def get_cases(request): if request is None: return Case.objects.none() if not request.user.groups.filter(name="atlas_editor").exists(): queryset = Case.objects.filter(author__id=request.user.id) else: queryset = Case.objects.all() return queryset def get_series(request): if request is None: return Series.objects.none() if not request.user.groups.filter(name="atlas_editor").exists(): queryset = Series.objects.filter(author__id=request.user.id) else: queryset = Series.objects.all() queryset = queryset.prefetch_related("examination", "plane") return queryset class CaseCollectionFilter(django_filters.FilterSet): author = django_filters.ModelMultipleChoiceFilter( queryset=get_authors, null_label="No author" ) cases = django_filters.ModelMultipleChoiceFilter( queryset=get_cases, null_label="No cases" ) class Meta: model = CaseCollection fields = ( "archive", "author", ) # "site", def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): # Default the archive filter to False when not provided so collections # are not archived by default in listings. if data is None: data = QueryDict('', mutable=True) data['archive'] = 'false' else: # QueryDict instances need to be copied to be mutable try: if 'archive' not in data: data = data.copy() data['archive'] = 'false' except Exception: # Fallback for non-QueryDict mappings if isinstance(data, dict) and 'archive' not in data: data = dict(data) data['archive'] = 'false' if not request.user.groups.filter(name="atlas_editor").exists(): queryset = queryset.prefetch_related("author").filter( author__id=request.user.id ) super(CaseCollectionFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass class CaseFilter(django_filters.FilterSet): author = django_filters.ModelMultipleChoiceFilter( queryset=get_authors, null_label="No author" ) # Show a distinct list of `Series.examination` values instead of raw Series # entries so examinations that differ only by plane/description appear once. series = django_filters.ModelMultipleChoiceFilter( field_name='series__examination', queryset=lambda request: __import__('generic.models', fromlist=['Examination']).Examination.objects.filter(atlas_series_examination__isnull=False).distinct(), label='Examination', ) # Free-text search to help find the correct examination when there are # many similarly-named Series/Examination entries. examination_text = django_filters.CharFilter(method='filter_examination_text', label='Examination (text)') # Full list of conditions and a text search to find conditions condition = django_filters.ModelMultipleChoiceFilter( queryset=Condition.objects.all(), label='Condition', ) condition_text = django_filters.CharFilter(method='filter_condition_text', label='Condition (text)') differential_case = django_filters.ModelMultipleChoiceFilter( label="Differential", field_name="differentialcase__condition", # to_field_name="differential", queryset=Condition.objects.all(), ) class Meta: model = Case fields = ( "created_date", "open_access", "author", "differential_case", ) # "site", def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): if not request.user.groups.filter(name="atlas_editor").exists(): queryset = queryset.prefetch_related("author").filter( (Q(author__id=request.user.id) | Q(open_access=True)) ) super(CaseFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass def filter_examination_text(self, queryset, name, value): """Filter Cases by a text search on related Examination records. Matches against the Examination string representation and any common identifying fields (name/code). Uses a case-insensitive contains lookup and filters Cases which have a Series linked to a matching Examination. """ if not value: return queryset from generic.models import Examination # Only attempt to match PK when the provided value looks like an integer try: pk_val = int(value) except Exception: pk_val = None if pk_val is not None: matches = Examination.objects.filter( Q(examination__icontains=value) | Q(pk=pk_val) ).distinct() else: matches = Examination.objects.filter( Q(examination__icontains=value) ).distinct() return queryset.filter(series__examination__in=matches).distinct() def filter_condition_text(self, queryset, name, value): """Filter Cases by a text search on related Condition records. Matches both direct `condition` M2M and `differential` entries. """ if not value: return queryset # Only try PK match when numeric try: pk_val = int(value) except Exception: pk_val = None if pk_val is not None: matches = Condition.objects.filter(Q(name__icontains=value) | Q(pk=pk_val)).distinct() else: matches = Condition.objects.filter(Q(name__icontains=value)).distinct() # Match either direct condition M2M or differential (through Differential) return queryset.filter(Q(condition__in=matches) | Q(differential__in=matches)).distinct() class SeriesFilter(django_filters.FilterSet): case = django_filters.ModelMultipleChoiceFilter( queryset=get_cases, null_label="No case" ) author = django_filters.ModelMultipleChoiceFilter( queryset=get_authors, null_label="No author" ) class Meta: model = Series fields = ("modality", "case", "examination", "author") def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): if not request.user.groups.filter(name="atlas_editor").exists(): # queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id) queryset = queryset.filter(author__id=request.user.id) super(SeriesFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass class ConditionFilter(django_filters.FilterSet): # Replace legacy `primary` filter with `is_canonical` and provide a # `synonym` filter which will filter by the canonical group of the # selected condition. is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical") synonym = django_filters.ModelChoiceFilter(queryset=Condition.objects.all(), method="filter_synonym", label="Synonym group") class Meta: model = Condition fields = { "name": ["icontains"], "subspecialty": ["exact"], "rcr_curriculum": ["exact"], } def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(ConditionFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass def filter_is_canonical(self, queryset, name, value): if value in (True, "True", "true", 1, "1"): return queryset.filter(canonical__isnull=True) if value in (False, "False", "false", 0, "0"): return queryset.filter(canonical__isnull=False) return queryset def filter_synonym(self, queryset, name, value): # value is a Condition instance selected in the filter. Return all # Conditions that belong to the same canonical group as `value`. if not value: return queryset master = value.canonical if value.canonical else value return queryset.filter(Q(canonical=master) | Q(pk=master.pk)) class FindingFilter(django_filters.FilterSet): # Replace legacy `primary`/`synonym` with canonical-based filters is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical") synonym = django_filters.ModelChoiceFilter(queryset=Finding.objects.all(), method="filter_synonym", label="Synonym group") class Meta: model = Finding fields = { "name": ["icontains"], } def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(FindingFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass def filter_is_canonical(self, queryset, name, value): if value in (True, "True", "true", 1, "1"): return queryset.filter(canonical__isnull=True) if value in (False, "False", "false", 0, "0"): return queryset.filter(canonical__isnull=False) return queryset def filter_synonym(self, queryset, name, value): # value is a Finding instance selected in the filter. Return all # Findings that belong to the same canonical group as `value`. if not value: return queryset master = value.canonical if value.canonical else value return queryset.filter(Q(canonical=master) | Q(pk=master.pk)) class StructureFilter(django_filters.FilterSet): is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical") synonym = django_filters.ModelChoiceFilter(queryset=Structure.objects.all(), method="filter_synonym", label="Synonym group") class Meta: model = Structure fields = {"name": ["icontains"]} def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(StructureFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass def filter_is_canonical(self, queryset, name, value): if value in (True, "True", "true", 1, "1"): return queryset.filter(canonical__isnull=True) if value in (False, "False", "false", 0, "0"): return queryset.filter(canonical__isnull=False) return queryset def filter_synonym(self, queryset, name, value): if not value: return queryset master = value.canonical if value.canonical else value return queryset.filter(Q(canonical=master) | Q(pk=master.pk)) class PresentationFilter(django_filters.FilterSet): class Meta: model = Presentation fields = ("name", "subspecialty") def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(PresentationFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass class PathologicalProcessFilter(django_filters.FilterSet): class Meta: model = PathologicalProcess fields = ("name",) def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(PathologicalProcessFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass class SubspecialtyFilter(django_filters.FilterSet): class Meta: model = Subspecialty fields = ("name",) def __init__( self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None, ): super(SubspecialtyFilter, self).__init__( data=data, queryset=queryset, prefix=prefix, request=request ) pass class NormalCaseFilter(django_filters.FilterSet): """FilterSet for the NormalCase model allowing filtering by examination, modality and nearest-year age ranges. """ examination = django_filters.ModelChoiceFilter(queryset=Series.objects.none()) modality = django_filters.ModelChoiceFilter(queryset=Series.objects.none()) procedures = django_filters.ModelMultipleChoiceFilter(field_name="case__procedures", queryset=Procedure.objects.all(), label="Procedure") # Accept min/max as years in the UI but filter against the canonical # `age_days` field by converting years -> days. # Numeric min/max values entered by the user (interpreted via `age_unit`) min_value = django_filters.NumberFilter(label="Min", method="filter_min_value") max_value = django_filters.NumberFilter(label="Max", method="filter_max_value") AGE_UNIT_CHOICES = ( ("days", "Days"), ("weeks", "Weeks"), ("months", "Months"), ("years", "Years"), ) age_unit = django_filters.ChoiceFilter(label="Unit", choices=AGE_UNIT_CHOICES, method="filter_age_unit") class Meta: model = NormalCase fields = ("examination", "modality", "procedures", "age_days") def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None): # Use the global Examination and Modality querysets from the Series model from generic.models import Examination, Modality # We'll set the actual querysets for the filters after applying # any NormalCase queryset restrictions for the current user so # that the choices only include examinations/modalities that # actually have NormalCase records available. # If not an atlas editor restrict to cases authored by the user or open access cases if request is not None and not request.user.groups.filter(name="atlas_editor").exists(): from django.db.models import Q if queryset is None: queryset = NormalCase.objects.select_related("case") queryset = queryset.filter((Q(case__author__id=request.user.id) | Q(case__open_access=True))) # Determine the NormalCase queryset we'll use to build choices nc_qs = queryset if queryset is not None else NormalCase.objects.all() # Limit examination and modality choices to those present on the # available NormalCase records (respecting user restrictions). try: exam_ids = nc_qs.values_list("examination_id", flat=True) mod_ids = nc_qs.values_list("modality_id", flat=True) proc_ids = nc_qs.values_list("case__procedures", flat=True) self.base_filters.get("examination").queryset = Examination.objects.filter(id__in=exam_ids).distinct() self.base_filters.get("modality").queryset = Modality.objects.filter(id__in=mod_ids).distinct() self.base_filters.get("procedures").queryset = Procedure.objects.filter(id__in=proc_ids).distinct() except Exception: # Fallback to all if something goes wrong self.base_filters.get("examination").queryset = Examination.objects.all() self.base_filters.get("modality").queryset = Modality.objects.all() super(NormalCaseFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request) # Attach a Crispy FormHelper to render the filter form as a # horizontal Bootstrap grid. We set `form_tag=False` because the # template already provides the