Skip to content

Commit 92c5865

Browse files
fstachurastephenfin
authored andcommitted
REST: Add filtering by labels to API
Patches in the API can be filtered by labels using the 'labels' query parameter.
1 parent 6866f55 commit 92c5865

7 files changed

Lines changed: 214 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from patchwork.models import Series
2727
from patchwork.models import State
2828

29+
from patchwork.models import exclude_submissions_by_labels
30+
from patchwork.models import filter_submissions_by_labels
31+
2932

3033
# custom backend
3134

@@ -175,6 +178,21 @@ def msgid_filter(queryset, name, value):
175178
return queryset.filter(**{name: '<' + value + '>'})
176179

177180

181+
def labels_filter(queryset, _, value):
182+
label_names = value.split(',')
183+
184+
labels_pos, labels_neg = [], []
185+
for label in label_names:
186+
if not label.startswith('-'):
187+
labels_pos.append(label)
188+
else:
189+
labels_neg.append(label[1:])
190+
191+
queryset = exclude_submissions_by_labels(queryset, labels_neg)
192+
queryset = filter_submissions_by_labels(queryset, labels_pos)
193+
return queryset
194+
195+
178196
class CoverFilterSet(TimestampMixin, BaseFilterSet):
179197
project = ProjectFilter(queryset=Project.objects.all(), distinct=False)
180198
# NOTE(stephenfin): We disable the select-based HTML widgets for these
@@ -206,6 +224,7 @@ class PatchFilterSet(TimestampMixin, BaseFilterSet):
206224
state = StateFilter(queryset=State.objects.all(), distinct=False)
207225
hash = CharFilter(lookup_expr='iexact')
208226
msgid = CharFilter(method=msgid_filter)
227+
labels = CharFilter(method=labels_filter)
209228

210229
class Meta:
211230
model = Patch
@@ -225,6 +244,7 @@ class Meta:
225244
)
226245
versioned_fields = {
227246
'1.2': ('hash', 'msgid'),
247+
'1.4': ('labels'),
228248
}
229249

230250

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,40 @@ def test_list_filter_msgid(self):
228228
resp = self.client.get(self.api_url(), {'msgid': 'fishfish@fish.fish'})
229229
self.assertEqual(0, len(resp.data))
230230

231+
def test_list_filter_labels(self):
232+
"""Filter patches by labels."""
233+
label1 = create_label()
234+
label2 = create_label()
235+
label3 = create_label()
236+
237+
person_obj = create_person(email='test@example.com')
238+
project_obj = create_project(linkname='myproject')
239+
state_obj = create_state(name='Under Review')
240+
patch_kwargs = {
241+
'state': state_obj,
242+
'project': project_obj,
243+
'submitter': person_obj,
244+
}
245+
patch_no_labels = create_patch(**patch_kwargs)
246+
patch1 = create_patch(**patch_kwargs, labels=[label1])
247+
patch2 = create_patch(**patch_kwargs, labels=[label1, label2])
248+
249+
resp = self.client.get(self.api_url(), {'labels': label1.name})
250+
self.assertEqual([patch1.id, patch2.id], [x['id'] for x in resp.data])
251+
252+
resp = self.client.get(self.api_url(), {'labels': label3.name})
253+
self.assertEqual(0, len(resp.data))
254+
255+
resp = self.client.get(self.api_url(), {'labels': '-' + label2.name})
256+
self.assertEqual(
257+
[patch_no_labels.id, patch1.id], [x['id'] for x in resp.data]
258+
)
259+
260+
resp = self.client.get(
261+
self.api_url(), {'labels': label1.name + ',' + label2.name}
262+
)
263+
self.assertEqual([patch2.id], [x['id'] for x in resp.data])
264+
231265
@utils.store_samples('patch-list-1-0')
232266
def test_list_version_1_0(self):
233267
"""List patches using API v1.0."""

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from patchwork.tests.utils import create_patches
2424
from patchwork.tests.utils import create_person
2525
from patchwork.tests.utils import create_project
26+
from patchwork.tests.utils import create_label
2627
from patchwork.tests.utils import create_state
2728
from patchwork.tests.utils import create_user
2829
from patchwork.tests.utils import read_patch
@@ -203,6 +204,128 @@ def test_utf8_handling(self):
203204
self.assertEqual(response.status_code, 200)
204205

205206

207+
class PatchListLabelFilteringTest(TestCase):
208+
def setUp(self):
209+
self.project = create_project()
210+
self.label1 = create_label(project=self.project)
211+
self.label2 = create_label(project=self.project)
212+
self.global_label = create_label(project=None)
213+
214+
person = create_person(name='test', email='test@example.com')
215+
216+
self.patches = [
217+
create_patch(submitter=person, project=self.project, labels=[]),
218+
create_patch(
219+
submitter=person, project=self.project, labels=[self.label1]
220+
),
221+
create_patch(
222+
submitter=person,
223+
project=self.project,
224+
labels=[self.label1, self.label2],
225+
),
226+
create_patch(
227+
submitter=person,
228+
project=self.project,
229+
labels=[self.label1, self.global_label],
230+
),
231+
]
232+
233+
def _extract_patch_ids(self, response):
234+
id_re = re.compile(r'<tr id="patch-row:(\d+)"')
235+
ids = [
236+
int(m.group(1)) for m in id_re.finditer(response.content.decode())
237+
]
238+
239+
return ids
240+
241+
def _extract_patches(self, response):
242+
ids = self._extract_patch_ids(response)
243+
if not ids:
244+
return []
245+
return [Patch.objects.get(id=i) for i in ids]
246+
247+
def test_no_labels(self):
248+
url = reverse(
249+
'patch-list', kwargs={'project_id': self.project.linkname}
250+
)
251+
response = self.client.get(url + '?labels=')
252+
253+
patches = self._extract_patches(response)
254+
self.assertEqual(len(patches), len(self.patches))
255+
256+
def test_one_label(self):
257+
url = reverse(
258+
'patch-list', kwargs={'project_id': self.project.linkname}
259+
)
260+
response = self.client.get(url + '?labels=' + self.label1.name)
261+
262+
patches = self._extract_patches(response)
263+
self.assertEqual(len(patches), 3)
264+
self.assertIn(self.patches[1], patches)
265+
self.assertIn(self.patches[2], patches)
266+
self.assertIn(self.patches[3], patches)
267+
268+
def test_two_labels(self):
269+
url = reverse(
270+
'patch-list', kwargs={'project_id': self.project.linkname}
271+
)
272+
response = self.client.get(
273+
url + '?labels=' + self.label1.name + '+' + self.label2.name
274+
)
275+
276+
patches = self._extract_patches(response)
277+
self.assertEqual(len(patches), 1)
278+
self.assertIn(self.patches[2], patches)
279+
280+
def test_global_label(self):
281+
url = reverse(
282+
'patch-list', kwargs={'project_id': self.project.linkname}
283+
)
284+
response = self.client.get(
285+
url + '?labels=' + self.label1.name + '+' + self.global_label.name
286+
)
287+
288+
patches = self._extract_patches(response)
289+
self.assertEqual(len(patches), 1)
290+
self.assertIn(self.patches[3], patches)
291+
292+
def test_negative_label(self):
293+
url = reverse(
294+
'patch-list', kwargs={'project_id': self.project.linkname}
295+
)
296+
response = self.client.get(url + '?labels=-' + self.label2.name)
297+
298+
patches = self._extract_patches(response)
299+
self.assertEqual(len(patches), 3)
300+
self.assertIn(self.patches[0], patches)
301+
self.assertIn(self.patches[1], patches)
302+
self.assertIn(self.patches[3], patches)
303+
304+
def test_positive_negative_label(self):
305+
url = reverse(
306+
'patch-list', kwargs={'project_id': self.project.linkname}
307+
)
308+
response = self.client.get(
309+
url + '?labels=-' + self.label2.name + '+' + self.label1.name
310+
)
311+
312+
patches = self._extract_patches(response)
313+
self.assertEqual(len(patches), 2)
314+
self.assertIn(self.patches[1], patches)
315+
self.assertIn(self.patches[3], patches)
316+
317+
def test_mutually_exclusive_labels(self):
318+
url = reverse(
319+
'patch-list', kwargs={'project_id': self.project.linkname}
320+
)
321+
response = self.client.get(
322+
url + '?labels=-' + self.label1.name + '+' + self.label1.name
323+
)
324+
325+
patches = self._extract_patches(response)
326+
self.assertEqual(len(patches), 0)
327+
328+
206329
class PatchViewTest(TestCase):
207330
def test_redirect(self):
208331
patch = create_patch()

‎patchwork/tests/utils.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,20 @@ def create_patch(**kwargs):
192192
}
193193
values.update(kwargs)
194194

195+
labels = None
196+
if 'labels' in values:
197+
labels = values['labels']
198+
del values['labels']
199+
195200
patch = Patch.objects.create(**values)
196201

197202
if series:
198203
number = number or series.patches.count() + 1
199204
series.add_patch(patch, number)
200205

206+
if labels is not None:
207+
patch.labels.set(labels)
208+
201209
return patch
202210

203211

0 commit comments

Comments
 (0)