Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) 2018-2020
2# MPIB <https://www.mpib-berlin.mpg.de/>,
3# MPI-CBS <https://www.cbs.mpg.de/>,
4# MPIP <http://www.psych.mpg.de/>
5#
6# This file is part of Castellum.
7#
8# Castellum is free software; you can redistribute it and/or modify it
9# under the terms of the GNU Affero General Public License as published
10# by the Free Software Foundation; either version 3 of the License, or
11# (at your option) any later version.
12#
13# Castellum is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# Affero General Public License for more details.
17#
18# You should have received a copy of the GNU Affero General Public
19# License along with Castellum. If not, see
20# <http://www.gnu.org/licenses/>.
23"""Filter queries for Subjects.
25There are many complicted filters for Subjects.
26In order to avoid losing track these are collected in this file.
28All of the functions return instances of ``django.db.models.Q``,
29which can be passed to ``Subjects.objects.filter()``.
30"""
32import json
34from django.conf import settings
35from django.contrib.gis.geos import GEOSGeometry
36from django.db import models
37from django.utils import timezone
39from castellum.contacts.models import Contact
40from castellum.studies.models import Study
41from castellum.subjects.models import Consent
42from castellum.subjects.models import Subject
44from .filter_registry import custom_filters
45from .models import AttributeDescription
46from .models import ParticipationRequest
49def study_exclusion(study):
50 """Handle the different kinds of exclusiveness between studies."""
52 # Excluded studies
53 pr_q = models.Q(
54 study__in=study.excluded_studies.all(),
55 status=ParticipationRequest.INVITED,
56 )
58 # Reverse excluded studies:
59 # Exclude studies which exclude this study.
60 pr_q |= models.Q(
61 study__excluded_studies=study,
62 status=ParticipationRequest.INVITED,
63 )
65 if study.is_exclusive:
66 # Exclusive studies:
67 # Exclude all other studies (including exclusive studies).
68 pr_q |= models.Q(
69 ~models.Q(study__status=Study.FINISHED),
70 ~models.Q(status=ParticipationRequest.UNSUITABLE),
71 ~models.Q(study__pk=study.pk),
72 )
73 else:
74 # Reverse exclusive studies:
75 # All other studies still exclude exclusive studies.
76 pr_q |= models.Q(
77 ~models.Q(study__status=Study.FINISHED),
78 ~models.Q(status=ParticipationRequest.UNSUITABLE),
79 study__is_exclusive=True,
80 )
82 prs = ParticipationRequest._base_manager.filter(pr_q)
84 return ~models.Q(id__in=prs.values('subject_id'))
87def study_disinterest(study):
88 """Exclude subjects who are disinterested in this study's study type."""
89 return ~models.Q(attributeset__study_type_disinterest__in=study.study_type.all())
92def subjectfilters(subjectfiltergroup, include_unknown=None):
93 """Exclude subjects according to subjectfilters from a single subjectfiltergroup."""
95 if include_unknown is None: 95 ↛ 98line 95 didn't jump to line 98, because the condition on line 95 was never false
96 include_unknown = not subjectfiltergroup.study.complete_matches_only
98 q = models.Q()
99 for f in subjectfiltergroup.subjectfilter_set.all():
100 q &= f.to_q(include_unknown=include_unknown)
102 return q
105def has_consent(include_waiting=False, exclude_deprecated=False):
106 """Exclude subjects who do not want to be contacted for recruitment."""
108 q_status = models.Q(consent__status=Consent.CONFIRMED)
109 if include_waiting:
110 acceptable_datetime = timezone.now() - settings.CASTELLUM_CONSENT_REVIEW_PERIOD
111 q_status |= models.Q(consent__updated_at__gte=acceptable_datetime)
113 q_document = models.Q(consent__document__is_valid=True)
114 if exclude_deprecated:
115 q_document &= models.Q(consent__document__is_deprecated=False)
117 return q_status & q_document
120def to_be_deleted():
121 """Exclude subjects who want to be deleted."""
122 return models.Q(to_be_deleted__isnull=True)
125def already_in_study(study):
126 """Exclude subjects if they already have a participationrequest in this study."""
127 return ~models.Q(participationrequest__study=study)
130def study_filters(study, include_unknown=None):
131 """A wrapper around subjectfilters()."""
133 if study.custom_filter:
134 q = custom_filters[study.custom_filter](include_unknown=include_unknown)
135 else:
136 # Filter according to all subjectfilters from all
137 # subjectfiltergroups.
138 q = models.Q()
139 for group in study.subjectfiltergroup_set.all():
140 q |= subjectfilters(group, include_unknown=include_unknown)
142 return q
145def pk_filter(qs, key='subject_id'):
146 # This is a mirco-optimization to avoid large IN queries in postgres
147 # See https://dba.stackexchange.com/questions/91247
148 pks = list(qs.values_list(key, flat=True).order_by())
149 total = Subject.objects.count()
150 if len(pks) > total / 2:
151 all_pks = Subject.objects.values_list('pk', flat=True)
152 pks = set(all_pks).difference(pks)
153 return ~models.Q(pk__in=pks)
154 else:
155 return models.Q(pk__in=pks)
158def geofilter(study):
159 from castellum.geofilters.models import Geolocation
161 feature = json.load(study.geo_filter)
162 study.geo_filter.seek(0)
163 polygon = GEOSGeometry(json.dumps(feature['geometry']))
165 outside = Geolocation.objects.exclude(point__within=polygon)
166 wards = (
167 Contact.objects
168 .exclude(guardians__geolocation=None)
169 .exclude(guardians__geolocation__in=outside)
170 )
171 contacts = Contact.objects.filter(
172 models.Q(geolocation__point__within=polygon)
173 | models.Q(id__in=wards.values('id'))
174 )
175 return pk_filter(contacts)
178def completeness_expr():
179 """Expression that can be used to annotate a queryset."""
180 # Needs to match ``AttributeSet.get_completeness()``.
181 total = 0
182 completeness = models.Value(0, output_field=models.IntegerField())
183 for description in AttributeDescription.objects.all():
184 total += 1
185 field_name = 'attributeset__data__' + description.json_key
186 completeness += models.Case(
187 models.When((
188 models.Q(**{field_name: ''}) |
189 models.Q(**{field_name: None}) |
190 models.Q(**{field_name + '__isnull': True})
191 ), models.Value(0)),
192 default=models.Value(1),
193 )
194 return completeness, total
197def study_except_filters(study, include_unknown=None):
198 q = models.Q()
199 q &= study_exclusion(study)
200 q &= study_disinterest(study)
201 q &= to_be_deleted()
202 if 'castellum.geofilters' in settings.INSTALLED_APPS and study.geo_filter: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true
203 q &= geofilter(study)
204 return q
207def study(study, include_unknown=None):
208 """Combine all filters related to ``study``."""
209 return (
210 study_filters(study, include_unknown=include_unknown)
211 & study_except_filters(study, include_unknown=include_unknown)
212 )