Skip to content

Commit 2eda20e

Browse files
committed
REST: Add filtering by labels to API
Patches in the API can be filtered by labels using the 'labels' query parameter.
1 parent 18867b3 commit 2eda20e

5 files changed

Lines changed: 134 additions & 0 deletions

File tree

‎docs/api/schemas/latest/patchwork.yaml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,15 @@ paths:
578578
schema:
579579
title: ''
580580
type: string
581+
- in: query
582+
name: labels
583+
description: |
584+
List of labels assigned to queried patches, separated by ','.
585+
If a label name begins with a '-', patches with that label won't
586+
be included.
587+
schema:
588+
title: ''
589+
type: string
581590
responses:
582591
'200':
583592
description: 'List of patches'

‎docs/api/schemas/patchwork.j2‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,17 @@ paths:
596596
schema:
597597
title: ''
598598
type: string
599+
{% endif %}
600+
{% if version >= (1, 4) %}
601+
- in: query
602+
name: labels
603+
description: |
604+
List of labels assigned to queried patches, separated by ','.
605+
If a label name begins with a '-', patches with that label won't
606+
be included.
607+
schema:
608+
title: ''
609+
type: string
599610
{% endif %}
600611
responses:
601612
'200':

‎docs/api/schemas/v1.4/patchwork.yaml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,15 @@ paths:
578578
schema:
579579
title: ''
580580
type: string
581+
- in: query
582+
name: labels
583+
description: |
584+
List of labels assigned to queried patches, separated by ','.
585+
If a label name begins with a '-', patches with that label won't
586+
be included.
587+
schema:
588+
title: ''
589+
type: string
581590
responses:
582591
'200':
583592
description: 'List of patches'

‎patchwork/api/filters.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ def msgid_filter(queryset, name, value):
175175
return queryset.filter(**{name: '<' + value + '>'})
176176

177177

178+
def labels_filter(queryset, _, value):
179+
label_names = value.split(',')
180+
181+
labels_pos, labels_neg = [], []
182+
for label in label_names:
183+
if not label.startswith('-'):
184+
labels_pos.append(label)
185+
else:
186+
labels_neg.append(label[1:])
187+
188+
queryset = queryset.exclude_labels(labels_neg)
189+
queryset = queryset.include_labels(labels_pos)
190+
return queryset
191+
192+
178193
class CoverFilterSet(TimestampMixin, BaseFilterSet):
179194
project = ProjectFilter(queryset=Project.objects.all(), distinct=False)
180195
# NOTE(stephenfin): We disable the select-based HTML widgets for these
@@ -206,6 +221,7 @@ class PatchFilterSet(TimestampMixin, BaseFilterSet):
206221
state = StateFilter(queryset=State.objects.all(), distinct=False)
207222
hash = CharFilter(lookup_expr='iexact')
208223
msgid = CharFilter(method=msgid_filter)
224+
labels = CharFilter(method=labels_filter)
209225

210226
class Meta:
211227
model = Patch
@@ -225,6 +241,7 @@ class Meta:
225241
)
226242
versioned_fields = {
227243
'1.2': ('hash', 'msgid'),
244+
'1.4': ('labels'),
228245
}
229246

230247

‎patchwork/tests/unit/api/test_patch.py‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,94 @@ def test_list_filter_msgid(self):
253253
resp = self.client.get(self.api_url(), {'msgid': 'fishfish@fish.fish'})
254254
self.assertEqual(0, len(resp.data))
255255

256+
def test_list_filter_labels(self):
257+
"""Filter patches by labels."""
258+
label1 = create_label()
259+
label2 = create_label()
260+
label3 = create_label()
261+
262+
person_obj = create_person(email='test@example.com')
263+
project_obj = create_project(linkname='myproject')
264+
state_obj = create_state(name='Under Review')
265+
patch_kwargs = {
266+
'state': state_obj,
267+
'project': project_obj,
268+
'submitter': person_obj,
269+
}
270+
patch_no_labels = create_patch(**patch_kwargs)
271+
patch1 = create_patch(**patch_kwargs, labels=[label1])
272+
patch2 = create_patch(**patch_kwargs, labels=[label1, label2])
273+
274+
resp = self.client.get(self.api_url(), {'labels': label1.name})
275+
self.assertEqual([patch1.id, patch2.id], [x['id'] for x in resp.data])
276+
277+
resp = self.client.get(self.api_url(), {'labels': label3.name})
278+
self.assertEqual(0, len(resp.data))
279+
280+
resp = self.client.get(self.api_url(), {'labels': '-' + label2.name})
281+
self.assertEqual(
282+
[patch_no_labels.id, patch1.id], [x['id'] for x in resp.data]
283+
)
284+
285+
resp = self.client.get(
286+
self.api_url(), {'labels': label1.name + ',' + label2.name}
287+
)
288+
self.assertEqual([patch2.id], [x['id'] for x in resp.data])
289+
290+
def test_list_filter_labels_local_global(self):
291+
"""Test filtering for patches with different labels, one local and
292+
one global (without a project) that have the same name.
293+
"""
294+
project_obj = create_project(linkname='myproject')
295+
project_obj2 = create_project(linkname='myproject2')
296+
297+
label_local = create_label(name='label', project=project_obj)
298+
label_local2 = create_label(name='label', project=project_obj2)
299+
label_global = create_label(name='label', project=None)
300+
301+
person_obj = create_person(email='test@example.com')
302+
state_obj = create_state(name='Under Review')
303+
patch_kwargs = {
304+
'state': state_obj,
305+
'project': project_obj,
306+
'submitter': person_obj,
307+
}
308+
patch_no_labels = create_patch(**patch_kwargs)
309+
patch1 = create_patch(**patch_kwargs, labels=[label_local])
310+
patch2 = create_patch(**patch_kwargs, labels=[label_global])
311+
patch3 = create_patch(
312+
**{'project': project_obj2, **patch_kwargs}, labels=[label_local2]
313+
)
314+
315+
resp = self.client.get(self.api_url(), {'labels': 'label'})
316+
self.assertEqual(
317+
[patch1.id, patch2.id, patch3.id], [x['id'] for x in resp.data]
318+
)
319+
320+
resp = self.client.get(self.api_url(), {'labels': '-label'})
321+
self.assertEqual([patch_no_labels.id], [x['id'] for x in resp.data])
322+
323+
def test_list_filter_labels_case_insensitive(self):
324+
"""Test if filtering by label is case-insensitive."""
325+
label = create_label(name='RFC')
326+
327+
person_obj = create_person(email='test@example.com')
328+
project_obj = create_project(linkname='myproject')
329+
state_obj = create_state(name='Under Review')
330+
patch_kwargs = {
331+
'state': state_obj,
332+
'project': project_obj,
333+
'submitter': person_obj,
334+
}
335+
create_patch(**patch_kwargs)
336+
patch1 = create_patch(**patch_kwargs, labels=[label])
337+
338+
resp = self.client.get(self.api_url(), {'labels': 'RFC'})
339+
self.assertEqual([patch1.id], [x['id'] for x in resp.data])
340+
341+
resp = self.client.get(self.api_url(), {'labels': 'rfc'})
342+
self.assertEqual([patch1.id], [x['id'] for x in resp.data])
343+
256344
@utils.store_samples('patch-list-1-0')
257345
def test_list_version_1_0(self):
258346
"""List patches using API v1.0."""

0 commit comments

Comments
 (0)