Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"],
}

30 changes: 30 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,30 @@
"""
API Serializers for taxonomies
"""

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