Skip to content

Commit

Permalink
feat: add taxonomy view/management apis
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Jul 24, 2023
1 parent 0b75bda commit d532b0c
Show file tree
Hide file tree
Showing 18 changed files with 622 additions and 8 deletions.
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.
18 changes: 18 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,18 @@
"""
Taxonomy permissions
"""

from rest_framework.permissions import DjangoObjectPermissions


class TaxonomyObjectPermissions(DjangoObjectPermissions):
perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [],
"HEAD": ["%(app_label)s.view_%(model_name)s"],
"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"],
}

31 changes: 31 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,31 @@
"""
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 TaxonomySerializer(serializers.ModelSerializer):
class Meta:
model = Taxonomy
fields = [
"id",
"name",
"description",
"enabled",
"required",
"allow_multiple",
"allow_free_text",
"system_defined",
"visible_to_authors",
]

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))
]
147 changes: 147 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,147 @@
"""
Tagging API Views
"""
from django.http import Http404
from rest_framework.viewsets import ModelViewSet

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


class TaxonomyView(ModelViewSet):
"""
View to list, create, retrieve, update, or delete Taxonomies.
**List Query Parameters**
* enabled (optional) - Filter by enabled status. 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**
* pk (required): - The pk of the taxonomy to retrieve
**Retrieve Example Requests**
GET api/tagging/v1/taxonomy/:pk - 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).
**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,
}
**Create Query Returns**
* 201 - Success
* 403 - Permission denied
**Update Parameters**
* pk (required): - The pk 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.
**Update Example Requests**
PUT api/tagging/v1/taxonomy/:pk - Update a taxonomy
{
"name": "Taxonomy New Name",
"description": "This is a new description",
"enabled": False,
"required": False,
"allow_multiple": False,
"allow_free_text": True,
}
PATCH api/tagging/v1/taxonomy/:pk - Partially update a taxonomy
{
"name": "Taxonomy New Name",
}
**Update Query Returns**
* 200 - Success
* 403 - Permission denied
**Delete Parameters**
* pk (required): - The pk of the taxonomy to delete
**Delete Example Requests**
DELETE api/tagging/v1/taxonomy/:pk - Delete a taxonomy
**Delete Query Returns**
* 200 - Success
* 404 - Taxonomy not found
* 403 - Permission denied
"""


serializer_class = TaxonomySerializer
permission_classes = [TaxonomyObjectPermissions]

def get_object(self):
"""
Return the requested taxonomy object, if the user has appropriate
permissions.
"""
pk = self.kwargs.get("pk")
taxonomy = get_taxonomy(pk)
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.
Returns all taxonomies by default.
If you want the disabled taxonomies, pass enabled=False.
If you want the enabled taxonomies, pass enabled=True.
"""
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 perform_create(self, serializer):
"""
Create a new taxonomy.
"""
serializer.instance = create_taxonomy(**serializer.validated_data)
6 changes: 3 additions & 3 deletions openedx_tagging/core/tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
@rules.predicate
def can_view_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool:
"""
Anyone can view an enabled taxonomy,
Anyone can view an enabled taxonomy or list all taxonomies,
but only taxonomy admins can view a disabled taxonomy.
"""
return (taxonomy and taxonomy.enabled) or is_taxonomy_admin(user)
return not taxonomy or taxonomy.enabled or is_taxonomy_admin(user)


@rules.predicate
Expand All @@ -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")),
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
4 changes: 4 additions & 0 deletions requirements/quality.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ django==3.2.19
# via
# -c requirements/constraints.txt
# -r requirements/test.txt
# django-debug-toolbar
# djangorestframework
django-debug-toolbar==4.1.0
# via -r requirements/test.txt
djangorestframework==3.14.0
# via -r requirements/test.txt
docutils==0.20.1
Expand Down Expand Up @@ -198,6 +201,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
1 change: 1 addition & 0 deletions requirements/test.in
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ pytest-django # pytest extension for better Django support
code-annotations # provides commands used by the pii_check make target.
ddt # supports data driven tests
mock # supports overriding classes and methods in tests
django-debug-toolbar # provides a debug toolbar for Django
4 changes: 4 additions & 0 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ ddt==1.6.0
# via
# -c requirements/constraints.txt
# -r requirements/base.txt
# django-debug-toolbar
# djangorestframework
django-debug-toolbar==4.1.0
# via -r requirements/test.in
djangorestframework==3.14.0
# via -r requirements/base.txt
exceptiongroup==1.1.1
Expand Down Expand Up @@ -72,6 +75,7 @@ sqlparse==0.4.4
# via
# -r requirements/base.txt
# django
# django-debug-toolbar
stevedore==5.1.0
# via code-annotations
text-unidecode==1.3
Expand Down
6 changes: 4 additions & 2 deletions test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ def root(*args):
"django.contrib.sessions",
"django.contrib.staticfiles",
# Admin
# 'django.contrib.admin',
# 'django.contrib.admindocs',
'django.contrib.admin',
'django.contrib.admindocs',
# Debugging
"debug_toolbar",
# django-rules based authorization
'rules.apps.AutodiscoverRulesConfig',
# Our own apps
Expand Down
2 changes: 1 addition & 1 deletion tests/openedx_tagging/core/tagging/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_view_taxonomy_enabled(self, enabled):
assert self.superuser.has_perm("oel_tagging.view_taxonomy", self.taxonomy)
assert self.staff.has_perm("oel_tagging.view_taxonomy")
assert self.staff.has_perm("oel_tagging.view_taxonomy", self.taxonomy)
assert not self.learner.has_perm("oel_tagging.view_taxonomy")
assert self.learner.has_perm("oel_tagging.view_taxonomy")
assert (
self.learner.has_perm("oel_tagging.view_taxonomy", self.taxonomy) == enabled
)
Expand Down
Loading

0 comments on commit d532b0c

Please sign in to comment.