Skip to content

Commit

Permalink
Add pulp_label_filter for filtering by pulp_labels
Browse files Browse the repository at this point in the history
fixes #8067
  • Loading branch information
David Davis authored and daviddavis committed Feb 2, 2021
1 parent ebf025d commit 348c654
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGES/8067.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``pulp_label_select`` filter to allow users to filter by labels.
1 change: 1 addition & 0 deletions CHANGES/plugin_api/8067.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``LabelSelectFilter`` to filter resources by labels.
57 changes: 56 additions & 1 deletion pulpcore/app/viewsets/custom_filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
This module contains custom filters that might be used by more than one ViewSet.
"""
import re
from gettext import gettext as _
from urllib.parse import urlparse
from uuid import UUID
Expand All @@ -9,8 +10,9 @@
from django_filters import BaseInFilter, CharFilter, DateTimeFilter, Filter
from django_filters.fields import IsoDateTimeField
from rest_framework import serializers
from rest_framework.serializers import ValidationError as DRFValidationError

from pulpcore.app.models import ContentArtifact, RepositoryVersion
from pulpcore.app.models import ContentArtifact, Label, RepositoryVersion
from pulpcore.app.viewsets import NamedModelViewSet


Expand Down Expand Up @@ -274,3 +276,56 @@ def filter(self, qs, value):

class CharInFilter(BaseInFilter, CharFilter):
pass


class LabelSelectFilter(Filter):
"""Filter to get resources that match a label filter string."""

def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", _("Filter labels by search string"))
super().__init__(*args, **kwargs)

def filter(self, qs, value):
"""
Args:
qs (django.db.models.query.QuerySet): The Model queryset
value (string): label search querry
Returns:
Queryset of the Models filtered by label(s)
Raises:
rest_framework.exceptions.ValidationError: on invalid search string
"""
if value is None:
# user didn't supply a value
return qs

for term in value.split(","):
match = re.match(r"(!?[\w\s]+)(=|!=|~)?(.*)?", term)
if not match:
raise DRFValidationError(_("Invalid search term: '{}'.").format(term))
key, op, val = match.groups()

if key.startswith("!") and op:
raise DRFValidationError(_("Cannot use an operator with '{}'.").format(key))

if op == "=":
labels = Label.objects.filter(key=key, value=val)
qs = qs.filter(pulp_labels__in=labels)
elif op == "!=":
labels = Label.objects.filter(key=key, value=val)
qs = qs.exclude(pulp_labels__in=labels)
elif op == "~":
labels = Label.objects.filter(key=key, value__icontains=val)
qs = qs.filter(pulp_labels__in=labels)
else:
# 'foo', '!foo'
if key.startswith("!"):
labels = Label.objects.filter(key=key[1:])
qs = qs.exclude(pulp_labels__in=labels)
else:
labels = Label.objects.filter(key=key)
qs = qs.filter(pulp_labels__in=labels)

return qs
9 changes: 7 additions & 2 deletions pulpcore/app/viewsets/publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
BaseFilterSet,
NamedModelViewSet,
)
from pulpcore.app.viewsets.base import NAME_FILTER_OPTIONS, DATETIME_FILTER_OPTIONS
from pulpcore.app.viewsets.custom_filters import IsoDateTimeFilter, RepositoryVersionFilter
from pulpcore.app.viewsets.base import DATETIME_FILTER_OPTIONS, NAME_FILTER_OPTIONS
from pulpcore.app.viewsets.custom_filters import (
IsoDateTimeFilter,
LabelSelectFilter,
RepositoryVersionFilter,
)


class PublicationFilter(BaseFilterSet):
Expand Down Expand Up @@ -92,6 +96,7 @@ class DistributionFilter(BaseFilterSet):
# /?base_path__icontains=foo
name = filters.CharFilter()
base_path = filters.CharFilter()
pulp_label_select = LabelSelectFilter()

class Meta:
model = BaseDistribution
Expand Down
4 changes: 3 additions & 1 deletion pulpcore/app/viewsets/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@
NamedModelViewSet,
)
from pulpcore.app.viewsets.base import DATETIME_FILTER_OPTIONS, NAME_FILTER_OPTIONS
from pulpcore.app.viewsets.custom_filters import IsoDateTimeFilter
from pulpcore.app.viewsets.custom_filters import IsoDateTimeFilter, LabelSelectFilter
from pulpcore.tasking.tasks import enqueue_with_reservation


class RepositoryFilter(BaseFilterSet):
name = filters.CharFilter()
pulp_label_select = LabelSelectFilter()

class Meta:
model = Repository
Expand Down Expand Up @@ -260,6 +261,7 @@ class RemoteFilter(BaseFilterSet):
"""

name = filters.CharFilter()
pulp_label_select = LabelSelectFilter()
pulp_last_updated = IsoDateTimeFilter()

class Meta:
Expand Down
1 change: 1 addition & 0 deletions pulpcore/plugin/viewsets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from pulpcore.app.viewsets.custom_filters import ( # noqa
CharInFilter,
HyperlinkRelatedFilter,
LabelSelectFilter,
RepositoryVersionFilter,
)

Expand Down
98 changes: 96 additions & 2 deletions pulpcore/tests/functional/api/using_plugin/test_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,30 @@
from pulpcore.client.pulp_file.exceptions import ApiException


class CRUDRepoTestCase(unittest.TestCase):
"""CRUD repositories."""
class BaseLabelTestCase(unittest.TestCase):
"""Base class for label test classes."""

@classmethod
def setUpClass(cls):
"""Create class-wide variables."""
cls.cfg = config.get_config()
cls.repo = None

def setUp(self):
"""Create an API client."""
self.client = FileApiClient(self.cfg.get_bindings_config())
self.repo_api = RepositoriesFileApi(self.client)

def _create_repo(self, labels={}):
attrs = {"name": str(uuid4())}
if labels:
attrs["pulp_labels"] = labels
self.repo = self.repo_api.create(attrs)
self.addCleanup(self.repo_api.delete, self.repo.pulp_href)


class CRUDLabelTestCase(BaseLabelTestCase):
"""CRUD labels on repositories."""

@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -116,3 +138,75 @@ def test_invalid_labels(self):

self.assertEqual(400, ae.exception.status)
self.assertTrue("pulp_labels" in json.loads(ae.exception.body))


class FilterLabelTestCase(BaseLabelTestCase):
"""CRUD labels on repositories."""

@classmethod
def setUpClass(cls):
"""Create class-wide variables."""
cls.cfg = config.get_config()
cls.repo = None

def setUp(self):
"""Create an API client."""
self.client = FileApiClient(self.cfg.get_bindings_config())
self.repo_api = RepositoriesFileApi(self.client)

def _filter_labels(self, pulp_label_select):
resp = self.repo_api.list(pulp_label_select=pulp_label_select)
return resp.results

def test_label_select(self):
"""Test removing all labels."""
labels = {"environment": "production", "certified": "true"}
self._create_repo(labels)
labels = {"environment": "staging", "certified": "false"}
self._create_repo(labels)

repos = self._filter_labels("environment=production")
self.assertEqual(1, len(repos))

repos = self._filter_labels("environment!=production")
self.assertEqual(1, len(repos))

repos = self._filter_labels("environment")
self.assertEqual(2, len(repos))

repos = self._filter_labels("!environment")
self.assertEqual(0, len(repos))

repos = self._filter_labels("environment~prod")
self.assertEqual(1, len(repos))

repos = self._filter_labels("environment=production,certified=true")
self.assertEqual(1, len(repos))

repos = self._filter_labels("environment=production,certified!=false")
self.assertEqual(1, len(repos))

def test_empty_blank_filter(self):
"""Test filtering values with a blank string."""
labels = {"environment": ""}
self._create_repo(labels)

repos = self._filter_labels("environment=")
self.assertEqual(1, len(repos))

repos = self._filter_labels("environment~")
self.assertEqual(1, len(repos))

def test_invalid_label_select(self):
"""Test removing all labels."""
with self.assertRaises(ApiException) as ae:
self._filter_labels("")
self.assertEqual(400, ae.exception.status)

with self.assertRaises(ApiException) as ae:
self._filter_labels("!environment=production")
self.assertEqual(400, ae.exception.status)

with self.assertRaises(ApiException) as ae:
self._filter_labels("=bad filter")
self.assertEqual(400, ae.exception.status)

0 comments on commit 348c654

Please sign in to comment.