Skip to content

Commit

Permalink
PROJ-1953-Cr-er-une-option-pour-chercher-des-tags-dans-tous-les-refs-…
Browse files Browse the repository at this point in the history
…activ-s-par-tags-et-skills (#328)

* add reserved slugs in tag classifications for specific search

* add test
  • Loading branch information
samonaisi authored Dec 2, 2024
1 parent 8e46d09 commit 4aa3160
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 5 deletions.
2 changes: 1 addition & 1 deletion apps/commons/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def people_group_router_register(
def organization_user_router_register(
router: DefaultRouter, path: str, viewset: View, basename: str = None
):
prefix = r"organization/(?P<organization_code>[^/]+)/user(?P<user_id>[^/]+)"
prefix = r"organization/(?P<organization_code>[^/]+)/user/(?P<user_id>[^/]+)"
if path:
prefix += "/" + path
router.register(prefix, viewset, basename)
Expand Down
11 changes: 10 additions & 1 deletion apps/skills/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ class TagClassification(models.Model, HasMultipleIDs, OrganizationRelated):
Users are allowed to create their own tags and classifications.
"""

class ReservedSlugs(models.TextChoices):
"""Reserved slugs for tag classifications."""

ENABLED_FOR_PROJECTS = "enabled-for-projects"
ENABLED_FOR_SKILLS = "enabled-for-skills"

class TagClassificationType(models.TextChoices):
"""Main type of a tag."""

Expand Down Expand Up @@ -141,7 +147,10 @@ def get_slug(self) -> str:
pass
slug = raw_slug
same_slug_count = 0
while TagClassification.objects.filter(slug=slug).exists():
while (
TagClassification.objects.filter(slug=slug).exists()
or slug in self.ReservedSlugs.values
):
same_slug_count += 1
slug = f"{raw_slug}-{same_slug_count}"
return slug
Expand Down
136 changes: 136 additions & 0 deletions apps/skills/tests/views/test_classification_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,142 @@ def test_retrieve_tag(self, role):
self.assertEqual(content["description"], tag.description_en)


class EnabledClassificationTagTestCase(WikipediaTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.organization = OrganizationFactory()
cls.query = faker.word()
cls.enabled_tags_1 = [
TagFactory(organization=cls.organization, title_en=f"{cls.query} {i}")
for i in range(5)
]
cls.enabled_tags_2 = [
TagFactory(organization=cls.organization, title_en=f"{cls.query} {i}")
for i in range(5, 10)
]
cls.wikipedia_tags = [
TagFactory(
type=Tag.TagType.WIKIPEDIA,
organization=cls.organization,
title_en=f"{cls.query} {i}",
)
for i in range(10, 15)
]
cls.disabled_tags = [
TagFactory(organization=cls.organization, title_en=f"{cls.query} {i}")
for i in range(15, 20)
]
cls.enabled_classification_1 = TagClassificationFactory(
organization=cls.organization, tags=cls.enabled_tags_1
)
cls.enabled_classification_2 = TagClassificationFactory(
organization=cls.organization, tags=cls.enabled_tags_2
)
cls.wikipedia_classification = (
TagClassification.get_or_create_default_classification(
classification_type=TagClassification.TagClassificationType.WIKIPEDIA
)
)
cls.wikipedia_classification.tags.add(*cls.wikipedia_tags)
cls.disabled_classification = TagClassificationFactory(
organization=cls.organization, tags=cls.disabled_tags
)
cls.organization.enabled_projects_tag_classifications.add(
cls.enabled_classification_1,
cls.enabled_classification_2,
cls.wikipedia_classification,
)
cls.organization.enabled_skills_tag_classifications.add(
cls.enabled_classification_1,
cls.enabled_classification_2,
cls.wikipedia_classification,
)

@parameterized.expand(
[
(TagClassification.ReservedSlugs.ENABLED_FOR_PROJECTS,),
(TagClassification.ReservedSlugs.ENABLED_FOR_SKILLS,),
]
)
@patch("apps.skills.views.TagViewSet.wikipedia_search")
def test_list_enabled_tag_classifications(self, enabled_for, mocked_search):
mocked_search.side_effect = lambda _: None
response = self.client.get(
reverse(
"ClassificationTag-list",
args=(self.organization.code, enabled_for),
)
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
mocked_search.assert_has_calls([])
self.assertSetEqual(
{tag["id"] for tag in content},
{
tag.id
for tag in self.enabled_tags_1
+ self.enabled_tags_2
+ self.wikipedia_tags
},
)

@parameterized.expand(
[
(TagClassification.ReservedSlugs.ENABLED_FOR_PROJECTS,),
(TagClassification.ReservedSlugs.ENABLED_FOR_SKILLS,),
]
)
@patch("apps.skills.views.TagViewSet.wikipedia_search")
def test_search_enabled_tag_classifications(self, enabled_for, mocked_search):
mocked_search.side_effect = lambda _: None
response = self.client.get(
reverse(
"ClassificationTag-list",
args=(self.organization.code, enabled_for),
)
+ f"?search={self.query}"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
mocked_search.assert_called_once()
self.assertSetEqual(
{tag["id"] for tag in content},
{
tag.id
for tag in self.enabled_tags_1
+ self.enabled_tags_2
+ self.wikipedia_tags
},
)

@parameterized.expand(
[
(TagClassification.ReservedSlugs.ENABLED_FOR_PROJECTS,),
(TagClassification.ReservedSlugs.ENABLED_FOR_SKILLS,),
]
)
def test_autocomplete_enabled_tag_classifications(self, enabled_for):
response = self.client.get(
reverse(
"ClassificationTag-autocomplete",
args=(self.organization.code, enabled_for),
)
+ "?limit=100"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()
self.assertSetEqual(
set(content),
{
tag.title_en
for tag in self.enabled_tags_1
+ self.enabled_tags_2
+ self.wikipedia_tags
},
)


class SearchClassificationTagTestCase(WikipediaTestCase):
@classmethod
def setUpTestData(cls):
Expand Down
4 changes: 4 additions & 0 deletions apps/skills/tests/views/test_tag_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@ def test_get_slug(self):
tag_classification.slug.startswith("tag-classification-"),
tag_classification.slug,
)
tag_classification = TagClassificationFactory(title="enabled for projects")
self.assertEqual(tag_classification.slug, "enabled-for-projects-1")
tag_classification = TagClassificationFactory(title="enabled for skills")
self.assertEqual(tag_classification.slug, "enabled-for-skills-1")

def test_multiple_lookups(self):
tag_classification = TagClassificationFactory(organization=self.organization)
Expand Down
90 changes: 87 additions & 3 deletions apps/skills/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Union

from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Count, Q
from django.db.models import Count, Q, QuerySet
from django.db.utils import IntegrityError
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
Expand All @@ -9,6 +11,7 @@
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.request import Request
from rest_framework.response import Response

from apps.accounts.models import PrivacySettings, ProjectUser
Expand Down Expand Up @@ -189,6 +192,18 @@ class TagViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet):
(TagClassification, "tag_classification_id"),
]

def get_tag_classification_id_from_lookup_value(
self, tag_classification_id: str
) -> Union[int, str]:
"""
Override the default method to handle multiple lookup values to allow fetching
all tags from the organization that are enabled for projects or skills by using
the slugs `enabled-for-projects` and `enabled-for-skills`.
"""
if tag_classification_id in TagClassification.ReservedSlugs.values:
return tag_classification_id
return TagClassification.get_main_id(tag_classification_id)

def get_permissions(self):
codename = map_action_to_permission(self.action, "tag")
if codename:
Expand All @@ -200,12 +215,58 @@ def get_permissions(self):
]
return super().get_permissions()

def get_enabled_tags(
self, organization_code: str, enabled_for: str
) -> QuerySet[Tag]:
"""
Get all tags from an organization that are enabled for projects or skills.
If one of the enabled classifications is Wikipedia, the method will also
perform a search in the Wikipedia database before returning the results.
This will only happen if the `search` parameter is provided in the request,
it allows to add new tags from Wikipedia to the organization when searching
multiple classifications at once.
"""
if enabled_for == TagClassification.ReservedSlugs.ENABLED_FOR_PROJECTS:
classifications = TagClassification.objects.filter(
enabled_organizations_projects__code=organization_code
)
elif enabled_for == TagClassification.ReservedSlugs.ENABLED_FOR_SKILLS:
classifications = TagClassification.objects.filter(
enabled_organizations_skills__code=organization_code
)
else:
return Tag.objects.none()
wikipedia = TagClassification.get_or_create_default_classification(
classification_type=TagClassification.TagClassificationType.WIKIPEDIA
)
if wikipedia in classifications and self.request.query_params.get(
"search", None
):
self.wikipedia_search(self.request)
return Tag.objects.filter(tag_classifications__in=classifications)

def get_queryset(self):
"""
This viewset can be used in three ways:
- To get all custom tags from an organization
When accessed with the `organization_code` parameter in the URL
- To get all tags from a specific classification
When accessed with the `organization_code` and `tag_classification_id`
parameters in the URL
- To get all tags from an organization that are enabled for projects or skills
When accessed with the `organization_code` and `tag_classification_id`
parameters in the URL and the `tag_classification_id` parameter is set
to `enabled-for-projects` or `enabled-for-skills`
"""
organization_code = self.kwargs.get("organization_code", None)
tag_classification_id = self.kwargs.get("tag_classification_id", None)
if organization_code and not tag_classification_id:
return Tag.objects.filter(organization__code=organization_code)
if organization_code and tag_classification_id:
if tag_classification_id in TagClassification.ReservedSlugs.values:
return self.get_enabled_tags(organization_code, tag_classification_id)
return Tag.objects.filter(tag_classifications__id=tag_classification_id)
return Tag.objects.all()

Expand Down Expand Up @@ -246,20 +307,31 @@ def perform_create(self, serializer: TagSerializer):
)
def list(self, request, *args, **kwargs):
"""
List all pages returned from wikipedia with a text query
List all custom tags of an organization (if only `organization_code` is provided
in the url), or all tags from a specific classification (if `organization_code`
and `tag_classification_id` are provided in the url).
Additionally, when using this endpoint with the `tag_classification_id`
parameter, you can use the following values instead of slugs to retrieve
specific tags classifications:
- `enabled-for-projects`: Tags that are enabled for projects in the organization
- `enabled-for-skills`: Tags that are enabled for skills in the organization
"""
wikipedia = TagClassification.get_or_create_default_classification(
classification_type=TagClassification.TagClassificationType.WIKIPEDIA
)
if (
self.request.query_params.get("search", None)
and self.kwargs.get("tag_classification_id", None)
and self.kwargs["tag_classification_id"]
not in TagClassification.ReservedSlugs.values
and int(self.kwargs["tag_classification_id"]) == int(wikipedia.id)
):
return self.wikipedia_search(request)
return super().list(request, *args, **kwargs)

def wikipedia_search(self, request):
def wikipedia_search(self, request: Request) -> Response:
params = {
"query": str(self.request.query_params.get("search", "")),
"language": str(self.request.query_params.get("language", "en")),
Expand Down Expand Up @@ -304,6 +376,18 @@ def wikipedia_search(self, request):
)
@action(detail=False, methods=["GET"])
def autocomplete(self, request, *args, **kwargs):
"""
Autocomplete custom tags of an organization (if only `organization_code` is
provided in the url), or all tags from a specific classification (if
`organization_code` and `tag_classification_id` are provided in the url).
Additionally, when using this endpoint with the `tag_classification_id`
parameter, you can use the following values instead of slugs to look through
specific tags classifications:
- `enabled-for-projects`: Tags that are enabled for projects in the organization
- `enabled-for-skills`: Tags that are enabled for skills in the organization
"""
language = self.request.query_params.get("language", "en")
limit = int(self.request.query_params.get("limit", 5))
search = self.request.query_params.get("search", "")
Expand Down

0 comments on commit 4aa3160

Please sign in to comment.