Patches in the API can be filtered by labels using the 'labels' query parameter.
Signed-off-by: Franciszek Stachura <[email protected]> --- docs/api/schemas/latest/patchwork.yaml | 9 ++ docs/api/schemas/patchwork.j2 | 11 ++ docs/api/schemas/v1.4/patchwork.yaml | 9 ++ patchwork/api/filters.py | 20 ++++ patchwork/tests/unit/api/test_patch.py | 34 +++++++ patchwork/tests/unit/views/test_patch.py | 123 +++++++++++++++++++++++ patchwork/tests/utils.py | 8 ++ 7 files changed, 214 insertions(+) diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml index 1b85e952..196a2515 100644 --- a/docs/api/schemas/latest/patchwork.yaml +++ b/docs/api/schemas/latest/patchwork.yaml @@ -578,6 +578,15 @@ paths: schema: title: '' type: string + - in: query + name: labels + description: | + List of labels assigned to queried patches, separated by ','. + If a label name begins with a '-', patches with that label won't + be included. + schema: + title: '' + type: string responses: '200': description: 'List of patches' diff --git a/docs/api/schemas/patchwork.j2 b/docs/api/schemas/patchwork.j2 index cea28fc0..98e0845f 100644 --- a/docs/api/schemas/patchwork.j2 +++ b/docs/api/schemas/patchwork.j2 @@ -596,6 +596,17 @@ paths: schema: title: '' type: string +{% endif %} +{% if version >= (1, 4) %} + - in: query + name: labels + description: | + List of labels assigned to queried patches, separated by ','. + If a label name begins with a '-', patches with that label won't + be included. + schema: + title: '' + type: string {% endif %} responses: '200': diff --git a/docs/api/schemas/v1.4/patchwork.yaml b/docs/api/schemas/v1.4/patchwork.yaml index 359e8224..a63f18fb 100644 --- a/docs/api/schemas/v1.4/patchwork.yaml +++ b/docs/api/schemas/v1.4/patchwork.yaml @@ -578,6 +578,15 @@ paths: schema: title: '' type: string + - in: query + name: labels + description: | + List of labels assigned to queried patches, separated by ','. + If a label name begins with a '-', patches with that label won't + be included. + schema: + title: '' + type: string responses: '200': description: 'List of patches' diff --git a/patchwork/api/filters.py b/patchwork/api/filters.py index e332c531..9bd42254 100644 --- a/patchwork/api/filters.py +++ b/patchwork/api/filters.py @@ -26,6 +26,9 @@ from patchwork.models import Project from patchwork.models import Series from patchwork.models import State +from patchwork.models import exclude_submissions_by_labels +from patchwork.models import filter_submissions_by_labels + # custom backend @@ -175,6 +178,21 @@ def msgid_filter(queryset, name, value): return queryset.filter(**{name: '<' + value + '>'}) +def labels_filter(queryset, _, value): + label_names = value.split(',') + + labels_pos, labels_neg = [], [] + for label in label_names: + if not label.startswith('-'): + labels_pos.append(label) + else: + labels_neg.append(label[1:]) + + queryset = exclude_submissions_by_labels(queryset, labels_neg) + queryset = filter_submissions_by_labels(queryset, labels_pos) + return queryset + + class CoverFilterSet(TimestampMixin, BaseFilterSet): project = ProjectFilter(queryset=Project.objects.all(), distinct=False) # NOTE(stephenfin): We disable the select-based HTML widgets for these @@ -206,6 +224,7 @@ class PatchFilterSet(TimestampMixin, BaseFilterSet): state = StateFilter(queryset=State.objects.all(), distinct=False) hash = CharFilter(lookup_expr='iexact') msgid = CharFilter(method=msgid_filter) + labels = CharFilter(method=labels_filter) class Meta: model = Patch @@ -225,6 +244,7 @@ class PatchFilterSet(TimestampMixin, BaseFilterSet): ) versioned_fields = { '1.2': ('hash', 'msgid'), + '1.4': ('labels'), } diff --git a/patchwork/tests/unit/api/test_patch.py b/patchwork/tests/unit/api/test_patch.py index 19f2af8f..1facf311 100644 --- a/patchwork/tests/unit/api/test_patch.py +++ b/patchwork/tests/unit/api/test_patch.py @@ -228,6 +228,40 @@ class TestPatchAPI(utils.APITestCase): resp = self.client.get(self.api_url(), {'msgid': '[email protected]'}) self.assertEqual(0, len(resp.data)) + def test_list_filter_labels(self): + """Filter patches by labels.""" + label1 = create_label() + label2 = create_label() + label3 = create_label() + + person_obj = create_person(email='[email protected]') + project_obj = create_project(linkname='myproject') + state_obj = create_state(name='Under Review') + patch_kwargs = { + 'state': state_obj, + 'project': project_obj, + 'submitter': person_obj, + } + patch_no_labels = create_patch(**patch_kwargs) + patch1 = create_patch(**patch_kwargs, labels=[label1]) + patch2 = create_patch(**patch_kwargs, labels=[label1, label2]) + + resp = self.client.get(self.api_url(), {'labels': label1.name}) + self.assertEqual([patch1.id, patch2.id], [x['id'] for x in resp.data]) + + resp = self.client.get(self.api_url(), {'labels': label3.name}) + self.assertEqual(0, len(resp.data)) + + resp = self.client.get(self.api_url(), {'labels': '-' + label2.name}) + self.assertEqual( + [patch_no_labels.id, patch1.id], [x['id'] for x in resp.data] + ) + + resp = self.client.get( + self.api_url(), {'labels': label1.name + ',' + label2.name} + ) + self.assertEqual([patch2.id], [x['id'] for x in resp.data]) + @utils.store_samples('patch-list-1-0') def test_list_version_1_0(self): """List patches using API v1.0.""" diff --git a/patchwork/tests/unit/views/test_patch.py b/patchwork/tests/unit/views/test_patch.py index 133bae36..983cf85b 100644 --- a/patchwork/tests/unit/views/test_patch.py +++ b/patchwork/tests/unit/views/test_patch.py @@ -23,6 +23,7 @@ from patchwork.tests.utils import create_patch_comment from patchwork.tests.utils import create_patches from patchwork.tests.utils import create_person from patchwork.tests.utils import create_project +from patchwork.tests.utils import create_label from patchwork.tests.utils import create_state from patchwork.tests.utils import create_user from patchwork.tests.utils import read_patch @@ -203,6 +204,128 @@ class PatchListFilteringTest(TestCase): self.assertEqual(response.status_code, 200) +class PatchListLabelFilteringTest(TestCase): + def setUp(self): + self.project = create_project() + self.label1 = create_label(project=self.project) + self.label2 = create_label(project=self.project) + self.global_label = create_label(project=None) + + person = create_person(name='test', email='[email protected]') + + self.patches = [ + create_patch(submitter=person, project=self.project, labels=[]), + create_patch( + submitter=person, project=self.project, labels=[self.label1] + ), + create_patch( + submitter=person, + project=self.project, + labels=[self.label1, self.label2], + ), + create_patch( + submitter=person, + project=self.project, + labels=[self.label1, self.global_label], + ), + ] + + def _extract_patch_ids(self, response): + id_re = re.compile(r'<tr id="patch-row:(\d+)"') + ids = [ + int(m.group(1)) for m in id_re.finditer(response.content.decode()) + ] + + return ids + + def _extract_patches(self, response): + ids = self._extract_patch_ids(response) + if not ids: + return [] + return [Patch.objects.get(id=i) for i in ids] + + def test_no_labels(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get(url + '?labels=') + + patches = self._extract_patches(response) + self.assertEqual(len(patches), len(self.patches)) + + def test_one_label(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get(url + '?labels=' + self.label1.name) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 3) + self.assertIn(self.patches[1], patches) + self.assertIn(self.patches[2], patches) + self.assertIn(self.patches[3], patches) + + def test_two_labels(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get( + url + '?labels=' + self.label1.name + '+' + self.label2.name + ) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 1) + self.assertIn(self.patches[2], patches) + + def test_global_label(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get( + url + '?labels=' + self.label1.name + '+' + self.global_label.name + ) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 1) + self.assertIn(self.patches[3], patches) + + def test_negative_label(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get(url + '?labels=-' + self.label2.name) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 3) + self.assertIn(self.patches[0], patches) + self.assertIn(self.patches[1], patches) + self.assertIn(self.patches[3], patches) + + def test_positive_negative_label(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get( + url + '?labels=-' + self.label2.name + '+' + self.label1.name + ) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 2) + self.assertIn(self.patches[1], patches) + self.assertIn(self.patches[3], patches) + + def test_mutually_exclusive_labels(self): + url = reverse( + 'patch-list', kwargs={'project_id': self.project.linkname} + ) + response = self.client.get( + url + '?labels=-' + self.label1.name + '+' + self.label1.name + ) + + patches = self._extract_patches(response) + self.assertEqual(len(patches), 0) + + class PatchViewTest(TestCase): def test_redirect(self): patch = create_patch() diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py index d4d5029f..79d0ab3a 100644 --- a/patchwork/tests/utils.py +++ b/patchwork/tests/utils.py @@ -198,12 +198,20 @@ def create_patch(**kwargs): } values.update(kwargs) + labels = None + if 'labels' in values: + labels = values['labels'] + del values['labels'] + patch = Patch.objects.create(**values) if series: number = number or series.patches.count() + 1 series.add_patch(patch, number) + if labels is not None: + patch.labels.set(labels) + return patch -- 2.55.0 _______________________________________________ Patchwork mailing list [email protected] https://lists.ozlabs.org/listinfo/patchwork
