Hide keyboard shortcuts

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/>. 

21 

22 

23"""Filter queries for Subjects. 

24 

25There are many complicted filters for Subjects. 

26In order to avoid losing track these are collected in this file. 

27 

28All of the functions return instances of ``django.db.models.Q``, 

29which can be passed to ``Subjects.objects.filter()``. 

30""" 

31 

32import json 

33 

34from django.conf import settings 

35from django.contrib.gis.geos import GEOSGeometry 

36from django.db import models 

37from django.utils import timezone 

38 

39from castellum.contacts.models import Contact 

40from castellum.studies.models import Study 

41from castellum.subjects.models import Consent 

42from castellum.subjects.models import Subject 

43 

44from .filter_registry import custom_filters 

45from .models import AttributeDescription 

46from .models import ParticipationRequest 

47 

48 

49def study_exclusion(study): 

50 """Handle the different kinds of exclusiveness between studies.""" 

51 

52 # Excluded studies 

53 pr_q = models.Q( 

54 study__in=study.excluded_studies.all(), 

55 status=ParticipationRequest.INVITED, 

56 ) 

57 

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 ) 

64 

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 ) 

81 

82 prs = ParticipationRequest._base_manager.filter(pr_q) 

83 

84 return ~models.Q(id__in=prs.values('subject_id')) 

85 

86 

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()) 

90 

91 

92def subjectfilters(subjectfiltergroup, include_unknown=None): 

93 """Exclude subjects according to subjectfilters from a single subjectfiltergroup.""" 

94 

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 

97 

98 q = models.Q() 

99 for f in subjectfiltergroup.subjectfilter_set.all(): 

100 q &= f.to_q(include_unknown=include_unknown) 

101 

102 return q 

103 

104 

105def has_consent(include_waiting=False, exclude_deprecated=False): 

106 """Exclude subjects who do not want to be contacted for recruitment.""" 

107 

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) 

112 

113 q_document = models.Q(consent__document__is_valid=True) 

114 if exclude_deprecated: 

115 q_document &= models.Q(consent__document__is_deprecated=False) 

116 

117 return q_status & q_document 

118 

119 

120def to_be_deleted(): 

121 """Exclude subjects who want to be deleted.""" 

122 return models.Q(to_be_deleted__isnull=True) 

123 

124 

125def already_in_study(study): 

126 """Exclude subjects if they already have a participationrequest in this study.""" 

127 return ~models.Q(participationrequest__study=study) 

128 

129 

130def study_filters(study, include_unknown=None): 

131 """A wrapper around subjectfilters().""" 

132 

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) 

141 

142 return q 

143 

144 

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) 

156 

157 

158def geofilter(study): 

159 from castellum.geofilters.models import Geolocation 

160 

161 feature = json.load(study.geo_filter) 

162 study.geo_filter.seek(0) 

163 polygon = GEOSGeometry(json.dumps(feature['geometry'])) 

164 

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) 

176 

177 

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 

195 

196 

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 

205 

206 

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 )