Skip to content

Commit 412e6a3

Browse files
committed
Merges #525 Closes #525 Fixes #520
2 parents bbf92ce + 131be07 commit 412e6a3

File tree

7 files changed

+281
-24
lines changed

7 files changed

+281
-24
lines changed

sortinghat/core/schema.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ class IdentityFilterType(graphene.InputObjectType):
263263
country = graphene.String(required=False)
264264
source = graphene.String(required=False)
265265
enrollment = graphene.String(required=False)
266+
enrollment_date = graphene.String(
267+
required=False,
268+
description='Filter with a comparison operator (>, >=, <, <=) and a date OR with a range operator (..) between\
269+
two dates, following ISO-8601 format. Examples:\n* `>=2020-10-12T09:35:06.13045+01:00` \
270+
\n * `2020-10-12T00:00:00..2020-11-22T00:00:00`.'
271+
)
266272
last_updated = graphene.String(
267273
required=False,
268274
description='Filter with a comparison operator (>, >=, <, <=) and a date OR with a range operator (..) between\
@@ -882,6 +888,39 @@ def resolve_individuals(self, info, filters=None,
882888
query = query.filter(identities__source=filters['source'])
883889
if filters and 'enrollment' in filters:
884890
query = query.filter(enrollments__organization__name__icontains=filters['enrollment'])
891+
if filters and 'enrollment_date' in filters:
892+
# Accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SS
893+
try:
894+
filter_data = parse_date_filter(filters['enrollment_date'])
895+
except ValueError as e:
896+
raise InvalidFilterError(filter_name='enrollment_date', msg=e)
897+
except InvalidDateError as e:
898+
raise InvalidFilterError(filter_name='enrollment_date', msg=e)
899+
date1 = filter_data['date1']
900+
date2 = filter_data['date2']
901+
if filter_data['operator']:
902+
operator = filter_data['operator']
903+
if operator == '<':
904+
query = query.filter(mk__in=Subquery(Enrollment.objects
905+
.filter(start__lt=date1)
906+
.values_list('individual__mk')))
907+
elif operator == '<=':
908+
query = query.filter(mk__in=Subquery(Enrollment.objects
909+
.filter(start__lte=date1)
910+
.values_list('individual__mk')))
911+
elif operator == '>':
912+
query = query.filter(mk__in=Subquery(Enrollment.objects
913+
.filter(end__gt=date1)
914+
.values_list('individual__mk')))
915+
elif operator == '>=':
916+
query = query.filter(mk__in=Subquery(Enrollment.objects
917+
.filter(end__gte=date1)
918+
.values_list('individual__mk')))
919+
elif operator == '..':
920+
query = query.filter(mk__in=Subquery(Enrollment.objects
921+
.filter(start__lte=date2,
922+
end__gte=date1)
923+
.values_list('individual__mk')))
885924
if filters and 'last_updated' in filters:
886925
# Accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SS
887926
try:

tests/test_schema.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,17 @@
408408
}
409409
}
410410
}"""
411+
SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER = """{
412+
individuals(filters: {enrollmentDate: "%s"}) {
413+
entities {
414+
mk
415+
enrollments {
416+
start
417+
end
418+
}
419+
}
420+
}
421+
}"""
411422
SH_INDIVIDUALS_LAST_UPDATED_FILTER = """{
412423
individuals(filters: {lastUpdated: "%s"}) {
413424
entities {
@@ -2225,6 +2236,129 @@ def test_filter_source_non_exist_registry(self):
22252236
individuals = executed['data']['individuals']['entities']
22262237
self.assertEqual(len(individuals), 0)
22272238

2239+
def test_filter_enrollment_date(self):
2240+
"""Check whether it returns the individual searched when using an enrollment date filter"""
2241+
2242+
# Individual is enrolled 1999-2000
2243+
org = Organization.objects.create(name='Bitergia')
2244+
indv = Individual.objects.create(mk='17ab00ed3825ec2f50483e33c88df223264182ba')
2245+
Enrollment.objects.create(individual=indv, organization=org,
2246+
start=datetime.datetime(1999, 1, 1, 0, 0, 0,
2247+
tzinfo=dateutil.tz.tzutc()),
2248+
end=datetime.datetime(2000, 1, 1, 0, 0, 0,
2249+
tzinfo=dateutil.tz.tzutc()))
2250+
client = graphene.test.Client(schema)
2251+
2252+
# Test enrollment before date '<'
2253+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<1998-01-01T00:00:00',
2254+
context_value=self.context_value)
2255+
individuals = executed['data']['individuals']['entities']
2256+
self.assertEqual(len(individuals), 0)
2257+
2258+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<1999-01-01T00:00:00',
2259+
context_value=self.context_value)
2260+
individuals = executed['data']['individuals']['entities']
2261+
self.assertEqual(len(individuals), 0)
2262+
2263+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<2000-01-01T00:00:00',
2264+
context_value=self.context_value)
2265+
individuals = executed['data']['individuals']['entities']
2266+
self.assertEqual(len(individuals), 1)
2267+
2268+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<2001-01-01T00:00:00',
2269+
context_value=self.context_value)
2270+
individuals = executed['data']['individuals']['entities']
2271+
self.assertEqual(len(individuals), 1)
2272+
2273+
# Test enrollment before or on date '<='
2274+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<=1998-01-01T00:00:00',
2275+
context_value=self.context_value)
2276+
individuals = executed['data']['individuals']['entities']
2277+
self.assertEqual(len(individuals), 0)
2278+
2279+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<=1999-01-01T00:00:00',
2280+
context_value=self.context_value)
2281+
individuals = executed['data']['individuals']['entities']
2282+
self.assertEqual(len(individuals), 1)
2283+
2284+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<=2000-01-01T00:00:00',
2285+
context_value=self.context_value)
2286+
individuals = executed['data']['individuals']['entities']
2287+
self.assertEqual(len(individuals), 1)
2288+
2289+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '<=2001-01-01T00:00:00',
2290+
context_value=self.context_value)
2291+
individuals = executed['data']['individuals']['entities']
2292+
self.assertEqual(len(individuals), 1)
2293+
2294+
# Test enrollment after date '>'
2295+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>1998-01-01T00:00:00',
2296+
context_value=self.context_value)
2297+
individuals = executed['data']['individuals']['entities']
2298+
self.assertEqual(len(individuals), 1)
2299+
2300+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>1999-01-01T00:00:00',
2301+
context_value=self.context_value)
2302+
individuals = executed['data']['individuals']['entities']
2303+
self.assertEqual(len(individuals), 1)
2304+
2305+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>2000-01-01T00:00:00',
2306+
context_value=self.context_value)
2307+
individuals = executed['data']['individuals']['entities']
2308+
self.assertEqual(len(individuals), 0)
2309+
2310+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>2001-01-01T00:00:00',
2311+
context_value=self.context_value)
2312+
individuals = executed['data']['individuals']['entities']
2313+
self.assertEqual(len(individuals), 0)
2314+
2315+
# Test enrollment after or on date '>='
2316+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>=1998-01-01T00:00:00',
2317+
context_value=self.context_value)
2318+
individuals = executed['data']['individuals']['entities']
2319+
self.assertEqual(len(individuals), 1)
2320+
2321+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>=1999-01-01T00:00:00',
2322+
context_value=self.context_value)
2323+
individuals = executed['data']['individuals']['entities']
2324+
self.assertEqual(len(individuals), 1)
2325+
2326+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>=2000-01-01T00:00:00',
2327+
context_value=self.context_value)
2328+
individuals = executed['data']['individuals']['entities']
2329+
self.assertEqual(len(individuals), 1)
2330+
2331+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '>=2001-01-01T00:00:00',
2332+
context_value=self.context_value)
2333+
individuals = executed['data']['individuals']['entities']
2334+
self.assertEqual(len(individuals), 0)
2335+
2336+
# Test enrollment range '..'
2337+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '1997-01-01T00:00:00..1998-01-01T00:00:00',
2338+
context_value=self.context_value)
2339+
individuals = executed['data']['individuals']['entities']
2340+
self.assertEqual(len(individuals), 0)
2341+
2342+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '1998-01-01T00:00:00..1999-01-01T00:00:00',
2343+
context_value=self.context_value)
2344+
individuals = executed['data']['individuals']['entities']
2345+
self.assertEqual(len(individuals), 1)
2346+
2347+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '1999-01-01T00:00:00..2000-01-01T00:00:00',
2348+
context_value=self.context_value)
2349+
individuals = executed['data']['individuals']['entities']
2350+
self.assertEqual(len(individuals), 1)
2351+
2352+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '2000-01-01T00:00:00..2001-01-01T00:00:00',
2353+
context_value=self.context_value)
2354+
individuals = executed['data']['individuals']['entities']
2355+
self.assertEqual(len(individuals), 1)
2356+
2357+
executed = client.execute(SH_INDIVIDUALS_ENROLLMENT_DATE_FILTER % '2001-01-01T00:00:00..2002-01-01T00:00:00',
2358+
context_value=self.context_value)
2359+
individuals = executed['data']['individuals']['entities']
2360+
self.assertEqual(len(individuals), 0)
2361+
22282362
def test_filter_last_updated(self):
22292363
"""Check whether it returns the uuids searched when using a date filter"""
22302364

ui/src/components/Search.vue

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export default {
135135
},
136136
{
137137
filter: "lastUpdated",
138-
type: "string"
138+
type: "date"
139139
},
140140
{
141141
filter: "source",
@@ -144,6 +144,10 @@ export default {
144144
{
145145
filter: "enrollment",
146146
type: "string"
147+
},
148+
{
149+
filter: "enrollmentDate",
150+
type: "date"
147151
}
148152
]
149153
},
@@ -194,8 +198,8 @@ export default {
194198
!this.validFilters.find(vfilter => vfilter.filter === filter)
195199
) {
196200
this.errorMessage = `Invalid filter "${filter}"`;
197-
} else if (filter === "lastUpdated") {
198-
this.parseLastUpdated(text);
201+
} else if (this.isDateFilter(filter)) {
202+
this.parseDateFilter(text, filter);
199203
} else if (this.isBooleanFilter(filter)) {
200204
this.parseBooleanFilter(filter, text);
201205
} else {
@@ -210,7 +214,7 @@ export default {
210214
this.filters.term = terms.join(" ").trim();
211215
}
212216
},
213-
parseLastUpdated(inputValue) {
217+
parseDateFilter(inputValue, filter) {
214218
const operator = ["<=", ">=", "<", ">", ".."].find(value =>
215219
inputValue.includes(value)
216220
);
@@ -222,7 +226,7 @@ export default {
222226
const values = inputValue.replace(operator, ` ${operator} `).split(" ");
223227
224228
try {
225-
this.filters.lastUpdated = values
229+
this.filters[filter] = values
226230
.map(value => {
227231
if (value) {
228232
return value === operator
@@ -252,7 +256,9 @@ export default {
252256
const filter = match[1];
253257
const value = match[2];
254258
if (this.validFilters.find(vfilter => vfilter.filter === filter)) {
255-
if (this.isBooleanFilter(filter)) {
259+
if (this.isDateFilter(filter)) {
260+
this.parseDateFilter(value, filter);
261+
} else if (this.isBooleanFilter(filter)) {
256262
this.parseBooleanFilter(filter, value);
257263
} else {
258264
this.filters[filter] = value;
@@ -278,13 +284,19 @@ export default {
278284
);
279285
return validFilter.type === "boolean";
280286
},
287+
isDateFilter(filter) {
288+
const validFilter = this.validFilters.find(
289+
vfilter => vfilter.filter === filter
290+
);
291+
return validFilter.type === "date";
292+
},
281293
setFilter(item) {
282294
this.inputValue = this.inputValue || "";
283-
if (item.filter === "lastUpdated") {
295+
if (this.isDateFilter(item.filter)) {
284296
const [month, day, year] = new Date()
285297
.toLocaleDateString("en-US")
286298
.split("/");
287-
this.inputValue += `lastUpdated:>=${year}-${month}-${day} `;
299+
this.inputValue += `${item.filter}:>=${year}-${month}-${day} `;
288300
} else if (this.isBooleanFilter(item.filter)) {
289301
this.inputValue += `${item.filter}:true `;
290302
} else {

ui/src/views/SearchHelp.vue

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,69 @@
139139
that include spaces should be wrapped between double quotes. For
140140
example: <code>enrollment:"Dumbledore's Army"</code>.
141141
</p>
142+
143+
<p class="subtitle-2">Filter by enrollment date</p>
144+
<p>
145+
You can filter individuals based on when they were affiliated to an
146+
organization, using the <code>enrollmentDate</code> filter.
147+
</p>
148+
<v-simple-table>
149+
<template v-slot:default>
150+
<thead>
151+
<tr>
152+
<th class="text-left" width="30%">
153+
Filter
154+
</th>
155+
<th class="text-left">
156+
Explanation
157+
</th>
158+
</tr>
159+
</thead>
160+
<tbody>
161+
<tr>
162+
<td width="30%"><code>enrollmentDate:>YYYY-MM-DD</code></td>
163+
<td>
164+
Matches individuals that were affiliated to an organization
165+
after the given date.
166+
</td>
167+
</tr>
168+
<tr>
169+
<td width="30%"><code>enrollmentDate:>=YYYY-MM-DD</code></td>
170+
<td>
171+
Matches individuals that were affiliated to an organization on
172+
or after the given date.
173+
</td>
174+
</tr>
175+
<tr>
176+
<td width="30%">
177+
<code>enrollmentDate:&lt;YYYY-MM-DD</code>
178+
</td>
179+
<td>
180+
Matches individuals that were affiliated to an organization
181+
before the given date.
182+
</td>
183+
</tr>
184+
<tr>
185+
<td width="30%">
186+
<code>enrollmentDate:&lt;=YYYY-MM-DD</code>
187+
</td>
188+
<td>
189+
Matches individuals that were affiliated to an organization on
190+
or before the given date.
191+
</td>
192+
</tr>
193+
<tr>
194+
<td width="30%">
195+
<code>enrollmentDate:YYYY-MM-DD..YYYY-MM-DD</code>
196+
</td>
197+
<td>
198+
Matches individuals that were affiliated to an organization
199+
between the given dates.
200+
</td>
201+
</tr>
202+
</tbody>
203+
</template>
204+
</v-simple-table>
142205
</v-card-text>
143206
</v-card>
144207
</v-main>

0 commit comments

Comments
 (0)