Skip to content

Commit

Permalink
feat: taxonomy view/management apis
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Jul 18, 2023
1 parent 0807949 commit a0fedd3
Show file tree
Hide file tree
Showing 19 changed files with 694 additions and 5 deletions.
15 changes: 15 additions & 0 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ def get_taxonomies(enabled=True) -> QuerySet:
return queryset.filter(enabled=enabled)


def update_taxonomy(id: int, **update_data) -> Union[Taxonomy, None]:
"""
Updates a Taxonomy which has the given ID.
"""
return Taxonomy.objects.filter(id=id).update(**update_data)


def delete_taxonomy(id: int):
"""
Deletes a Taxonomy which has the given ID.
"""
taxonomy = Taxonomy.objects.get(id=id)
taxonomy.delete()


def get_tags(taxonomy: Taxonomy) -> List[Tag]:
"""
Returns a list of predefined tags for the given taxonomy.
Expand Down
9 changes: 9 additions & 0 deletions openedx_tagging/core/tagging/rest_api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Taxonomies API URLs.
"""

from django.urls import path, include

from .v1 import urls as v1_urls

urlpatterns = [path("v1/", include(v1_urls))]
Empty file.
30 changes: 30 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Taxonomy permissions
"""

from rest_framework.permissions import DjangoObjectPermissions


class TaxonomyObjectPermissions(DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
# 'GET': [],
'OPTIONS': [],
# 'HEAD': ['%(app_label)s.view_%(model_name)s'],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}

def has_permission(self, request, view):
# Workaround to allow 'retrieve' view to pass through the
# method GET permission check. Actual object permission check
# is done in the get_object() method.
if view.action == 'retrieve':
return bool(request.user and request.user.is_authenticated)
else:
return super().has_permission(request, view)


72 changes: 72 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
API Serializers for taxonomies
"""

from django.utils.module_loading import import_string
from rest_framework import serializers

from openedx_tagging.core.tagging.models import Taxonomy

class TaxonomyListQueryParamsSerializer(serializers.Serializer):
"""
Serializer for the query params for the GET view
"""

enabled = serializers.BooleanField(required=False)

class ClassField(serializers.Field):
"""
Class field
"""
def to_representation(self, class_type):
if class_type:
# ref: https://stackoverflow.com/a/2020083
return ".".join(
[class_type.__module__, class_type.__qualname__]
)
return None

def to_internal_value(self, value):
if value:
try:
# Ensure that the class exists
return import_string(value)
except (ImportError, AttributeError):
raise serializers.ValidationError(
f"Invalid taxonomy_class: {value}."
)
return value

class TaxonomySerializer(serializers.ModelSerializer):
taxonomy_class = ClassField(required=False, allow_null=True)
class Meta:
model = Taxonomy
fields = [
"id",
"name",
"description",
"enabled",
"required",
"allow_multiple",
"allow_free_text",
"system_defined",
"visible_to_authors",
"taxonomy_class",
]

def save(self):
raise NotImplementedError(
"Cannot save a taxonomy through serializer. Use the create_taxonomy or"
"update_taxonomy function instead."
)

def update(self):
raise NotImplementedError(
"Cannot save a taxonomy through serializer. Use the create_taxonomy or"
"update_taxonomy function instead."
)
def create(self):
raise NotImplementedError(
"Cannot save a taxonomy through serializer. Use the create_taxonomy or"
"update_taxonomy function instead."
)
16 changes: 16 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Taxonomies API v1 URLs.
"""

from rest_framework.routers import DefaultRouter

from django.urls.conf import path, include

from . import views

router = DefaultRouter()
router.register("taxonomies", views.TaxonomyView, basename="taxonomy")

urlpatterns = [
path('', include(router.urls))
]
180 changes: 180 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""
Tagging API Views
"""
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from ...api import (
create_taxonomy,
delete_taxonomy,
get_taxonomy,
get_taxonomies,
update_taxonomy,
)
from .serializers import TaxonomyListQueryParamsSerializer, TaxonomySerializer
from .permissions import TaxonomyObjectPermissions


class TaxonomyView(ModelViewSet):
"""
View to list, create, retrieve, update, or delete Taxonomies.
NOTE: Only the GET request requires a request parameter, otherwise pass the uuid as part
of the post body
**List Query Parameters**
* enabled (optional) - Filter by enabled status (default: None). Valid values: true, false, 1, 0, "true", "false", "1"
**List Example Requests**
GET api/tagging/v1/taxonomy - Get all taxonomies
GET api/tagging/v1/taxonomy?enabled=true - Get all enabled taxonomies
GET api/tagging/v1/taxonomy?enabled=false - Get all disabled taxonomies
**List Query Returns**
* 200 - Success
* 400 - Invalid query parameter
* 403 - Permission denied
**Retrieve Parameters**
* id (required): - The id of the taxonomy to retrieve
**Retrieve Example Requests**
GET api/tagging/v1/taxonomy/:id - Get a specific taxonomy
**Retrieve Query Returns**
* 200 - Success
* 404 - Taxonomy not found or User does not have permission to access the taxonomy
**Create Parameters**
* name (required): User-facing label used when applying tags from this taxonomy to Open edX objects.
* description (optional): Provides extra information for the user when applying tags from this taxonomy to an object.
* enabled (optional): Only enabled taxonomies will be shown to authors (default: true).
* required (optional): Indicates that one or more tags from this taxonomy must be added to an object (default: False).
* allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to an object (default: False).
* allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors may enter their own tag values (default: False).
* taxonomy_class (optional): Taxonomy subclass used to instantiate this instance. Must be a fully-qualified
module and class name (default: None).
**Create Example Requests**
POST api/tagging/v1/taxonomy - Create a taxonomy
{
"name": "Taxonomy Name", - User-facing label used when applying tags from this taxonomy to Open edX objects."
"description": "This is a description",
"enabled": True,
"required": True,
"allow_multiple": True,
"allow_free_text": True,
"taxonomy_class": "class", #ToDo: add a example here
}
**Create Query Returns**
* 201 - Success
* 403 - Permission denied
**Update Parameters**
* id (required): - The id of the taxonomy to update
**Update Request Body**
* name (optional): User-facing label used when applying tags from this taxonomy to Open edX objects.
* description (optional): Provides extra information for the user when applying tags from this taxonomy to an object.
* enabled (optional): Only enabled taxonomies will be shown to authors.
* required (optional): Indicates that one or more tags from this taxonomy must be added to an object.
* allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to an object.
* allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors may enter their own tag values.
* taxonomy_class (optional): Taxonomy subclass used to instantiate this instance. Must be a fully-qualified
module and class name.
**Update Example Requests**
PUT api/tagging/v1/taxonomy/:id - Update a taxonomy
{
"name": "Taxonomy New Name",
"description": "This is a new description",
"enabled": False,
"required": False,
"allow_multiple": False,
"allow_free_text": True,
"taxonomy_class": "class", #ToDo: add a example here
}
PATCH api/tagging/v1/taxonomy/:id - Partially update a taxonomy
{
"name": "Taxonomy New Name",
}
**Update Query Returns**
* 200 - Success
* 403 - Permission denied
**Delete Parameters**
* id (required): - The id of the taxonomy to delete
**Delete Example Requests**
DELETE api/tagging/v1/taxonomy/:id - Delete a taxonomy
**Delete Query Returns**
* 200 - Success
* 404 - Taxonomy not found
* 403 - Permission denied
"""


serializer_class = TaxonomySerializer
lookup_field = "id"
permission_classes = [TaxonomyObjectPermissions]

def get_object(self):
"""
Return the requested taxonomy object, if the user has appropriate
permissions.
"""
id = self.kwargs.get("id")
taxonomy = get_taxonomy(id)
if not taxonomy:
raise Http404("Taxonomy not found")
self.check_object_permissions(self.request, taxonomy)

return taxonomy

def get_queryset(self):
"""
Return a list of taxonomies. If you want the disabled taxonomies, pass enabled=False. If you want all taxonomies (both enabled and disabled), pass enabled=None.
"""
query_params = TaxonomyListQueryParamsSerializer(
data=self.request.query_params.dict()
)
query_params.is_valid(raise_exception=True)
enabled = query_params.data.get("enabled", None)

return get_taxonomies(enabled)

def create(self, request):
"""
Create a new taxonomy.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
taxonomy = create_taxonomy(**serializer.validated_data)
return Response(
self.serializer_class(taxonomy).data, status=status.HTTP_201_CREATED
)

def update(self, request, *args, **kwargs):
"""
Update a taxonomy.
"""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
update_taxonomy(instance.id, **serializer.validated_data)

return Response(serializer.data)

def perform_destroy(self, instance):
"""
Delete a taxonomy.
"""
delete_taxonomy(instance.id)
2 changes: 1 addition & 1 deletion openedx_tagging/core/tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def can_change_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool:
Even taxonomy admins cannot change system taxonomies.
"""
return is_taxonomy_admin(user) and (
not taxonomy or not taxonomy or (taxonomy and not taxonomy.system_defined)
not taxonomy or (taxonomy and not taxonomy.system_defined)
)


Expand Down
10 changes: 10 additions & 0 deletions openedx_tagging/core/tagging/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Tagging API URLs.
"""

from django.urls import path, include

from .rest_api import urls

app_name = "oel_tagging"
urlpatterns = [path("", include(urls))]
1 change: 1 addition & 0 deletions projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
path("admin/", admin.site.urls),
path("media_server/", include("openedx_learning.contrib.media_server.urls")),
path("rest_api/", include("openedx_learning.rest_api.urls")),
path("tagging/rest_api/", include("openedx_tagging.core.tagging.urls", namespace="oel_tagging")),
path('__debug__/', include('debug_toolbar.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
4 changes: 3 additions & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ django==3.2.19
# djangorestframework
# edx-i18n-tools
django-debug-toolbar==4.1.0
# via -r requirements/dev.in
# via
# -r requirements/dev.in
# -r requirements/quality.txt
djangorestframework==3.14.0
# via -r requirements/quality.txt
docutils==0.20.1
Expand Down
4 changes: 4 additions & 0 deletions requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ django==3.2.19
# via
# -c requirements/constraints.txt
# -r requirements/test.txt
# django-debug-toolbar
# djangorestframework
# sphinxcontrib-django
django-debug-toolbar==4.1.0
# via -r requirements/test.txt
djangorestframework==3.14.0
# via -r requirements/test.txt
doc8==1.1.1
Expand Down Expand Up @@ -175,6 +178,7 @@ sqlparse==0.4.4
# via
# -r requirements/test.txt
# django
# django-debug-toolbar
stevedore==5.1.0
# via
# -r requirements/test.txt
Expand Down

0 comments on commit a0fedd3

Please sign in to comment.