Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tags/roles and tags/collections endpoints #1931

1 change: 1 addition & 0 deletions CHANGES/2761.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add _ui/v1/tags/collections and _ui/v1/tags/roles endpoints. Add sorting by name and count, and enable filtering by name (exact, partial and startswith match).
4 changes: 4 additions & 0 deletions galaxy_ng/app/api/ui/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
router.register('distributions', viewsets.DistributionViewSet, basename='distributions')
router.register('my-distributions', viewsets.MyDistributionViewSet, basename='my-distributions')

router.register('tags/collections', viewsets.CollectionsTagsViewSet, basename='collections-tags')
router.register('tags/roles', viewsets.RolesTagsViewSet, basename='roles-tags')


auth_views = [
path("login/", views.LoginView.as_view(), name="auth-login"),
path("logout/", views.LogoutView.as_view(), name="auth-logout"),
Expand Down
8 changes: 7 additions & 1 deletion galaxy_ng/app/api/ui/viewsets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
)
from .my_namespace import MyNamespaceViewSet
from .my_synclist import MySyncListViewSet
from .tags import TagsViewSet
from .tags import (
TagsViewSet,
CollectionsTagsViewSet,
RolesTagsViewSet
)
from .user import UserViewSet, CurrentUserViewSet
from .synclist import SyncListViewSet
from .root import APIRootView
Expand All @@ -30,6 +34,8 @@
'CollectionImportViewSet',
'CollectionRemoteViewSet',
'TagsViewSet',
'CollectionsTagsViewSet',
'RolesTagsViewSet',
'CurrentUserViewSet',
'UserViewSet',
'SyncListViewSet',
Expand Down
102 changes: 100 additions & 2 deletions galaxy_ng/app/api/ui/viewsets/tags.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from django.db.models import Count
from rest_framework import mixins
from django_filters import filters
from django_filters.rest_framework import DjangoFilterBackend, filterset

from pulp_ansible.app.models import Tag
from pulp_ansible.app.serializers import TagSerializer

from galaxy_ng.app.api import base as api_base
from galaxy_ng.app.access_control import access_policy

from galaxy_ng.app.api.ui import versioning
from galaxy_ng.app.access_control import access_policy
from galaxy_ng.app.api.v1.models import LegacyRoleTag
from galaxy_ng.app.api.v1.serializers import LegacyRoleTagSerializer


class TagsViewSet(api_base.GenericViewSet):
Expand All @@ -21,3 +27,95 @@ def list(self, request, *args, **kwargs):
serializer = self.get_serializer(page, many=True)

return self.get_paginated_response(serializer.data)


class CollectionTagFilterOrdering(filters.OrderingFilter):
def filter(self, qs, value):
if value is not None and any(v in ["count", "-count"] for v in value):
order = "-" if "-count" in value else ""

return qs.filter(
ansible_collectionversion__ansible_crossrepositorycollectionversionindex__is_highest=True # noqa: E501
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it really need to use the crossrepo index? for community we have only one repo and it would be faster to just take the is_highest from the single repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but then this wouldn't work for PAH and CRC

).annotate(count=Count('ansible_collectionversion')).order_by(f"{order}count")

return super().filter(qs, value)


class CollectionTagFilter(filterset.FilterSet):
sort = CollectionTagFilterOrdering(
fields=(
("name", "name"),
('count', 'count')
),
)

class Meta:
model = Tag
fields = {
"name": ["exact", "icontains", "contains", "startswith"],
}


class CollectionsTagsViewSet(
api_base.GenericViewSet,
mixins.ListModelMixin
):
"""
ViewSet for collections' tags within the system.
"""
serializer_class = TagSerializer
permission_classes = [access_policy.TagsAccessPolicy]
versioning_class = versioning.UIVersioning
filter_backends = (DjangoFilterBackend,)
filterset_class = CollectionTagFilter

queryset = Tag.objects.all()

def get_queryset(self):
qs = super().get_queryset()
return qs.annotate(count=Count("ansible_collectionversion"))


class RoleTagFilterOrdering(filters.OrderingFilter):
def filter(self, qs, value):
if value is not None and any(v in ["count", "-count"] for v in value):
order = "-" if "-count" in value else ""

return qs.annotate(count=Count('legacyrole')).order_by(f"{order}count")

return super().filter(qs, value)


class RoleTagFilter(filterset.FilterSet):
sort = RoleTagFilterOrdering(
fields=(
("name", "name"),
('count', 'count')
),
)

class Meta:
model = LegacyRoleTag
fields = {
"name": ["exact", "icontains", "contains", "startswith"],
}


class RolesTagsViewSet(
api_base.GenericViewSet,
mixins.ListModelMixin
):
"""
ViewSet for roles' tags within the system.
Tags can be populated manually by running `django-admin populate-role-tags`.
"""
queryset = LegacyRoleTag.objects.all()
serializer_class = LegacyRoleTagSerializer
permission_classes = [access_policy.TagsAccessPolicy]
versioning_class = versioning.UIVersioning
filter_backends = (DjangoFilterBackend,)
filterset_class = RoleTagFilter

def get_queryset(self):
qs = super().get_queryset()
return qs.annotate(count=Count("legacyrole"))
12 changes: 12 additions & 0 deletions galaxy_ng/app/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ def __str__(self):
return self.name


class LegacyRoleTag(models.Model):
name = models.CharField(max_length=64, unique=True, editable=False)

def __repr__(self):
return f'<LegacyRoleTag: {self.name}>'

def __str__(self):
return self.name


class LegacyRole(models.Model):
"""
A legacy v1 role, which is just an index for github.
Expand Down Expand Up @@ -154,6 +164,8 @@ class LegacyRole(models.Model):
default=dict
)

tags = models.ManyToManyField(LegacyRoleTag, editable=False, related_name="legacyrole")

def __repr__(self):
return f'<LegacyRole: {self.namespace.name}.{self.name}>'

Expand Down
11 changes: 10 additions & 1 deletion galaxy_ng/app/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from galaxy_ng.app.models.namespace import Namespace
from galaxy_ng.app.utils.rbac import get_v3_namespace_owners
from galaxy_ng.app.api.v1.models import LegacyNamespace
from galaxy_ng.app.api.v1.models import LegacyRole
from galaxy_ng.app.api.v1.models import LegacyRole, LegacyRoleTag
from galaxy_ng.app.api.v1.models import LegacyRoleDownloadCount
from galaxy_ng.app.api.v1.utils import sort_versions

Expand Down Expand Up @@ -602,3 +602,12 @@ class LegacyTaskDetailSerializer(serializers.Serializer):
class Meta:
model = None
fields = ['results']


class LegacyRoleTagSerializer(serializers.ModelSerializer):

count = serializers.IntegerField(read_only=True)

class Meta:
model = LegacyRoleTag
fields = ['name', 'count']
36 changes: 36 additions & 0 deletions galaxy_ng/app/management/commands/populate-role-tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from gettext import gettext as _

import django_guid
from django.core.management.base import BaseCommand

# from galaxy_ng.app.api.v1.tasks import legacy_sync_from_upstream
from galaxy_ng.app.api.v1.models import LegacyRole, LegacyRoleTag


# Set logging_uid, this does not seem to get generated when task called via management command
django_guid.set_guid(django_guid.utils.generate_guid())


class Command(BaseCommand):
"""
Django management command for populating role tags ('_ui/v1/tags/roles/') within the system.
This command is run nightly on galaxy.ansible.com.
"""

help = _("Populate the 'LegacyRoleTag' model with tags from LegacyRole 'full_metadata__tags'.")

def handle(self, *args, **options):
created_tags = []
roles = LegacyRole.objects.all()
for role in roles:
for name in role.full_metadata["tags"]:
tag, created = LegacyRoleTag.objects.get_or_create(name=name)
tag.legacyrole.add(role)

if created:
created_tags.append(tag)

self.stdout.write(
"Successfully populated {} tags "
"from {} roles.".format(len(created_tags), len(roles))
)
31 changes: 31 additions & 0 deletions galaxy_ng/app/migrations/0043_legacyroletag_legacyrole_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.6 on 2023-10-25 21:26

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("galaxy", "0042_namespace_created_namespace_updated"),
]

operations = [
migrations.CreateModel(
name="LegacyRoleTag",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.CharField(editable=False, max_length=64, unique=True)),
],
),
migrations.AddField(
model_name="legacyrole",
name="tags",
field=models.ManyToManyField(
editable=False, related_name="legacyrole", to="galaxy.legacyroletag"
),
),
]
Loading
Loading