openedx-learning 0.1.0__py2.py3-none-any.whl → 0.1.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. openedx_learning/__init__.py +1 -1
  2. {openedx_learning-0.1.0.dist-info → openedx_learning-0.1.2.dist-info}/METADATA +4 -3
  3. {openedx_learning-0.1.0.dist-info → openedx_learning-0.1.2.dist-info}/RECORD +25 -9
  4. openedx_tagging/core/tagging/api.py +45 -1
  5. openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml +1298 -0
  6. openedx_tagging/core/tagging/management/commands/__init__.py +0 -0
  7. openedx_tagging/core/tagging/management/commands/build_language_fixture.py +48 -0
  8. openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +76 -0
  9. openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +34 -0
  10. openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +29 -0
  11. openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +16 -0
  12. openedx_tagging/core/tagging/models/__init__.py +11 -0
  13. openedx_tagging/core/tagging/{models.py → models/base.py} +112 -24
  14. openedx_tagging/core/tagging/models/system_defined.py +269 -0
  15. openedx_tagging/core/tagging/rest_api/urls.py +9 -0
  16. openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
  17. openedx_tagging/core/tagging/rest_api/v1/permissions.py +17 -0
  18. openedx_tagging/core/tagging/rest_api/v1/serializers.py +31 -0
  19. openedx_tagging/core/tagging/rest_api/v1/urls.py +14 -0
  20. openedx_tagging/core/tagging/rest_api/v1/views.py +147 -0
  21. openedx_tagging/core/tagging/rules.py +14 -15
  22. openedx_tagging/core/tagging/urls.py +10 -0
  23. {openedx_learning-0.1.0.dist-info → openedx_learning-0.1.2.dist-info}/LICENSE.txt +0 -0
  24. {openedx_learning-0.1.0.dist-info → openedx_learning-0.1.2.dist-info}/WHEEL +0 -0
  25. {openedx_learning-0.1.0.dist-info → openedx_learning-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,269 @@
1
+ """ Tagging app system-defined taxonomies data models """
2
+ import logging
3
+ from typing import Any, List, Type, Union
4
+
5
+ from django.conf import settings
6
+ from django.contrib.auth import get_user_model
7
+ from django.db import models
8
+
9
+ from openedx_tagging.core.tagging.models.base import ObjectTag
10
+
11
+ from .base import Tag, Taxonomy, ObjectTag
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ class SystemDefinedTaxonomy(Taxonomy):
17
+ """
18
+ Simple subclass of Taxonomy which requires the system_defined flag to be set.
19
+ """
20
+
21
+ class Meta:
22
+ proxy = True
23
+
24
+ @property
25
+ def system_defined(self) -> bool:
26
+ """
27
+ Indicates that tags and metadata for this taxonomy are maintained by the system;
28
+ taxonomy admins will not be permitted to modify them.
29
+ """
30
+ return True
31
+
32
+
33
+ class ModelObjectTag(ObjectTag):
34
+ """
35
+ Model-based ObjectTag, abstract class.
36
+
37
+ Used by ModelSystemDefinedTaxonomy to maintain dynamic Tags which are associated with a configured Model instance.
38
+ """
39
+
40
+ class Meta:
41
+ proxy = True
42
+
43
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
44
+ """
45
+ Checks if the `tag_class_model` is correct
46
+ """
47
+ assert issubclass(self.tag_class_model, models.Model)
48
+ super().__init__(*args, **kwargs)
49
+
50
+ @property
51
+ def tag_class_model(self) -> Type:
52
+ """
53
+ Subclasses must implement this method to return the Django.model
54
+ class referenced by these object tags.
55
+ """
56
+ raise NotImplementedError
57
+
58
+ @property
59
+ def tag_class_value(self) -> str:
60
+ """
61
+ Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy.
62
+
63
+ Subclasses may override this method to use different fields.
64
+ """
65
+ return "pk"
66
+
67
+ def get_instance(self) -> Union[models.Model, None]:
68
+ """
69
+ Returns the instance of tag_class_model associated with this object tag, or None if not found.
70
+ """
71
+ instance_id = self.tag.external_id if self.tag else None
72
+ if instance_id:
73
+ try:
74
+ return self.tag_class_model.objects.get(pk=instance_id)
75
+ except ValueError as e:
76
+ log.exception(f"{self}: {str(e)}")
77
+ except self.tag_class_model.DoesNotExist:
78
+ log.exception(
79
+ f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist."
80
+ )
81
+
82
+ return None
83
+
84
+ def _resync_tag(self) -> bool:
85
+ """
86
+ Resync our tag's value with the value from the instance.
87
+
88
+ If the instance associated with the tag no longer exists, we unset our tag, because it's no longer valid.
89
+
90
+ Returns True if the given tag was changed, False otherwise.
91
+ """
92
+ instance = self.get_instance()
93
+ if instance:
94
+ value = getattr(instance, self.tag_class_value)
95
+ self.value = value
96
+ if self.tag and self.tag.value != value:
97
+ self.tag.value = value
98
+ self.tag.save()
99
+ return True
100
+ else:
101
+ self.tag = None
102
+
103
+ return False
104
+
105
+ @property
106
+ def tag_ref(self) -> str:
107
+ return (self.tag.external_id or self.tag.id) if self.tag_id else self._value
108
+
109
+ @tag_ref.setter
110
+ def tag_ref(self, tag_ref: str):
111
+ """
112
+ Sets the ObjectTag's Tag and/or value, depending on whether a valid Tag is found, or can be created.
113
+
114
+ Creates a Tag for the given tag_ref value, if one containing that external_id not already exist.
115
+ """
116
+ self.value = tag_ref
117
+
118
+ if self.taxonomy_id:
119
+ try:
120
+ self.tag = self.taxonomy.tag_set.get(
121
+ external_id=tag_ref,
122
+ )
123
+ except (ValueError, Tag.DoesNotExist):
124
+ # Creates a new Tag for this instance
125
+ self.tag = Tag(
126
+ taxonomy=self.taxonomy,
127
+ external_id=tag_ref,
128
+ )
129
+
130
+ self._resync_tag()
131
+
132
+
133
+ class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy):
134
+ """
135
+ Model based system taxonomy abstract class.
136
+
137
+ This type of taxonomy has an associated Django model in ModelObjectTag.tag_class_model().
138
+ They are designed to create Tags when required for new ObjectTags, to maintain
139
+ their status as "closed" taxonomies.
140
+ The Tags are representations of the instances of the associated model.
141
+
142
+ Tag.external_id stores an identifier from the instance (`pk` as default)
143
+ and Tag.value stores a human readable representation of the instance
144
+ (e.g. `username`).
145
+ The subclasses can override this behavior, to choose the right field.
146
+
147
+ When an ObjectTag is created with an existing Tag,
148
+ the Tag is re-synchronized with its instance.
149
+ """
150
+
151
+ class Meta:
152
+ proxy = True
153
+
154
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
155
+ """
156
+ Checks if the `object_tag_class` is a subclass of ModelObjectTag.
157
+ """
158
+ assert issubclass(self.object_tag_class, ModelObjectTag)
159
+ super().__init__(*args, **kwargs)
160
+
161
+ @property
162
+ def object_tag_class(self) -> Type:
163
+ """
164
+ Returns the ObjectTag subclass associated with this taxonomy.
165
+
166
+ Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass.
167
+ """
168
+ raise NotImplementedError
169
+
170
+ def _check_instance(self, object_tag: ObjectTag) -> bool:
171
+ """
172
+ Returns True if the instance exists
173
+
174
+ Subclasses can override this method to perform their own instance validation checks.
175
+ """
176
+ object_tag = self.object_tag_class.cast(object_tag)
177
+ return bool(object_tag.get_instance())
178
+
179
+ def _check_tag(self, object_tag: ObjectTag) -> bool:
180
+ """
181
+ Returns True if the instance is valid
182
+ """
183
+ return super()._check_tag(object_tag) and self._check_instance(object_tag)
184
+
185
+
186
+ class UserModelObjectTag(ModelObjectTag):
187
+ """
188
+ ObjectTags for the UserSystemDefinedTaxonomy.
189
+ """
190
+
191
+ class Meta:
192
+ proxy = True
193
+
194
+ @property
195
+ def tag_class_model(self) -> Type:
196
+ """
197
+ Associate the user model
198
+ """
199
+ return get_user_model()
200
+
201
+ @property
202
+ def tag_class_value(self) -> str:
203
+ """
204
+ Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy.
205
+
206
+ Subclasses may override this method to use different fields.
207
+ """
208
+ return "username"
209
+
210
+
211
+ class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy):
212
+ """
213
+ User based system taxonomy class.
214
+ """
215
+
216
+ class Meta:
217
+ proxy = True
218
+
219
+ @property
220
+ def object_tag_class(self) -> Type:
221
+ """
222
+ Returns the ObjectTag subclass associated with this taxonomy, which is ModelObjectTag by default.
223
+
224
+ Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass.
225
+ """
226
+ return UserModelObjectTag
227
+
228
+
229
+ class LanguageTaxonomy(SystemDefinedTaxonomy):
230
+ """
231
+ Language System-defined taxonomy
232
+
233
+ The tags are filtered and validated taking into account the
234
+ languages available in Django LANGUAGES settings var
235
+ """
236
+
237
+ class Meta:
238
+ proxy = True
239
+
240
+ def get_tags(self, tag_set: models.QuerySet = None) -> List[Tag]:
241
+ """
242
+ Returns a list of all the available Language Tags, annotated with ``depth`` = 0.
243
+ """
244
+ available_langs = self._get_available_languages()
245
+ tag_set = self.tag_set.filter(external_id__in=available_langs)
246
+ return super().get_tags(tag_set=tag_set)
247
+
248
+ def _get_available_languages(cls) -> List[str]:
249
+ """
250
+ Get available languages from Django LANGUAGE.
251
+ """
252
+ langs = set()
253
+ for django_lang in settings.LANGUAGES:
254
+ # Split to get the language part
255
+ langs.add(django_lang[0].split("-")[0])
256
+ return langs
257
+
258
+ def _check_valid_language(self, object_tag: ObjectTag) -> bool:
259
+ """
260
+ Returns True if the tag is on the available languages
261
+ """
262
+ available_langs = self._get_available_languages()
263
+ return object_tag.tag.external_id in available_langs
264
+
265
+ def _check_tag(self, object_tag: ObjectTag) -> bool:
266
+ """
267
+ Returns True if the tag is on the available languages
268
+ """
269
+ return super()._check_tag(object_tag) and self._check_valid_language(object_tag)
@@ -0,0 +1,9 @@
1
+ """
2
+ Taxonomies API URLs.
3
+ """
4
+
5
+ from django.urls import path, include
6
+
7
+ from .v1 import urls as v1_urls
8
+
9
+ urlpatterns = [path("v1/", include(v1_urls))]
File without changes
@@ -0,0 +1,17 @@
1
+ """
2
+ Taxonomy permissions
3
+ """
4
+
5
+ from rest_framework.permissions import DjangoObjectPermissions
6
+
7
+
8
+ class TaxonomyObjectPermissions(DjangoObjectPermissions):
9
+ perms_map = {
10
+ "GET": ["%(app_label)s.view_%(model_name)s"],
11
+ "OPTIONS": [],
12
+ "HEAD": ["%(app_label)s.view_%(model_name)s"],
13
+ "POST": ["%(app_label)s.add_%(model_name)s"],
14
+ "PUT": ["%(app_label)s.change_%(model_name)s"],
15
+ "PATCH": ["%(app_label)s.change_%(model_name)s"],
16
+ "DELETE": ["%(app_label)s.delete_%(model_name)s"],
17
+ }
@@ -0,0 +1,31 @@
1
+ """
2
+ API Serializers for taxonomies
3
+ """
4
+
5
+ from rest_framework import serializers
6
+
7
+ from openedx_tagging.core.tagging.models import Taxonomy
8
+
9
+
10
+ class TaxonomyListQueryParamsSerializer(serializers.Serializer):
11
+ """
12
+ Serializer for the query params for the GET view
13
+ """
14
+
15
+ enabled = serializers.BooleanField(required=False)
16
+
17
+
18
+ class TaxonomySerializer(serializers.ModelSerializer):
19
+ class Meta:
20
+ model = Taxonomy
21
+ fields = [
22
+ "id",
23
+ "name",
24
+ "description",
25
+ "enabled",
26
+ "required",
27
+ "allow_multiple",
28
+ "allow_free_text",
29
+ "system_defined",
30
+ "visible_to_authors",
31
+ ]
@@ -0,0 +1,14 @@
1
+ """
2
+ Taxonomies API v1 URLs.
3
+ """
4
+
5
+ from rest_framework.routers import DefaultRouter
6
+
7
+ from django.urls.conf import path, include
8
+
9
+ from . import views
10
+
11
+ router = DefaultRouter()
12
+ router.register("taxonomies", views.TaxonomyView, basename="taxonomy")
13
+
14
+ urlpatterns = [path("", include(router.urls))]
@@ -0,0 +1,147 @@
1
+ """
2
+ Tagging API Views
3
+ """
4
+ from django.http import Http404
5
+ from rest_framework.viewsets import ModelViewSet
6
+
7
+ from ...api import (
8
+ create_taxonomy,
9
+ get_taxonomy,
10
+ get_taxonomies,
11
+ )
12
+ from .permissions import TaxonomyObjectPermissions
13
+ from .serializers import TaxonomyListQueryParamsSerializer, TaxonomySerializer
14
+
15
+
16
+ class TaxonomyView(ModelViewSet):
17
+ """
18
+ View to list, create, retrieve, update, or delete Taxonomies.
19
+
20
+ **List Query Parameters**
21
+ * enabled (optional) - Filter by enabled status. Valid values: true, false, 1, 0, "true", "false", "1"
22
+ * page (optional) - Page number (default: 1)
23
+ * page_size (optional) - Number of items per page (default: 10)
24
+
25
+ **List Example Requests**
26
+ GET api/tagging/v1/taxonomy - Get all taxonomies
27
+ GET api/tagging/v1/taxonomy?enabled=true - Get all enabled taxonomies
28
+ GET api/tagging/v1/taxonomy?enabled=false - Get all disabled taxonomies
29
+
30
+ **List Query Returns**
31
+ * 200 - Success
32
+ * 400 - Invalid query parameter
33
+ * 403 - Permission denied
34
+
35
+ **Retrieve Parameters**
36
+ * pk (required): - The pk of the taxonomy to retrieve
37
+
38
+ **Retrieve Example Requests**
39
+ GET api/tagging/v1/taxonomy/:pk - Get a specific taxonomy
40
+
41
+ **Retrieve Query Returns**
42
+ * 200 - Success
43
+ * 404 - Taxonomy not found or User does not have permission to access the taxonomy
44
+
45
+ **Create Parameters**
46
+ * name (required): User-facing label used when applying tags from this taxonomy to Open edX objects.
47
+ * description (optional): Provides extra information for the user when applying tags from this taxonomy to an object.
48
+ * enabled (optional): Only enabled taxonomies will be shown to authors (default: true).
49
+ * required (optional): Indicates that one or more tags from this taxonomy must be added to an object (default: False).
50
+ * allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to an object (default: False).
51
+ * allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors may enter their own tag values (default: False).
52
+
53
+ **Create Example Requests**
54
+ POST api/tagging/v1/taxonomy - Create a taxonomy
55
+ {
56
+ "name": "Taxonomy Name", - User-facing label used when applying tags from this taxonomy to Open edX objects."
57
+ "description": "This is a description",
58
+ "enabled": True,
59
+ "required": True,
60
+ "allow_multiple": True,
61
+ "allow_free_text": True,
62
+ }
63
+
64
+ **Create Query Returns**
65
+ * 201 - Success
66
+ * 403 - Permission denied
67
+
68
+ **Update Parameters**
69
+ * pk (required): - The pk of the taxonomy to update
70
+
71
+ **Update Request Body**
72
+ * name (optional): User-facing label used when applying tags from this taxonomy to Open edX objects.
73
+ * description (optional): Provides extra information for the user when applying tags from this taxonomy to an object.
74
+ * enabled (optional): Only enabled taxonomies will be shown to authors.
75
+ * required (optional): Indicates that one or more tags from this taxonomy must be added to an object.
76
+ * allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to an object.
77
+ * allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors may enter their own tag values.
78
+
79
+ **Update Example Requests**
80
+ PUT api/tagging/v1/taxonomy/:pk - Update a taxonomy
81
+ {
82
+ "name": "Taxonomy New Name",
83
+ "description": "This is a new description",
84
+ "enabled": False,
85
+ "required": False,
86
+ "allow_multiple": False,
87
+ "allow_free_text": True,
88
+ }
89
+ PATCH api/tagging/v1/taxonomy/:pk - Partially update a taxonomy
90
+ {
91
+ "name": "Taxonomy New Name",
92
+ }
93
+
94
+ **Update Query Returns**
95
+ * 200 - Success
96
+ * 403 - Permission denied
97
+
98
+ **Delete Parameters**
99
+ * pk (required): - The pk of the taxonomy to delete
100
+
101
+ **Delete Example Requests**
102
+ DELETE api/tagging/v1/taxonomy/:pk - Delete a taxonomy
103
+
104
+ **Delete Query Returns**
105
+ * 200 - Success
106
+ * 404 - Taxonomy not found
107
+ * 403 - Permission denied
108
+
109
+ """
110
+
111
+ serializer_class = TaxonomySerializer
112
+ permission_classes = [TaxonomyObjectPermissions]
113
+
114
+ def get_object(self):
115
+ """
116
+ Return the requested taxonomy object, if the user has appropriate
117
+ permissions.
118
+ """
119
+ pk = self.kwargs.get("pk")
120
+ taxonomy = get_taxonomy(pk)
121
+ if not taxonomy:
122
+ raise Http404("Taxonomy not found")
123
+ self.check_object_permissions(self.request, taxonomy)
124
+
125
+ return taxonomy
126
+
127
+ def get_queryset(self):
128
+ """
129
+ Return a list of taxonomies.
130
+
131
+ Returns all taxonomies by default.
132
+ If you want the disabled taxonomies, pass enabled=False.
133
+ If you want the enabled taxonomies, pass enabled=True.
134
+ """
135
+ query_params = TaxonomyListQueryParamsSerializer(
136
+ data=self.request.query_params.dict()
137
+ )
138
+ query_params.is_valid(raise_exception=True)
139
+ enabled = query_params.data.get("enabled", None)
140
+
141
+ return get_taxonomies(enabled)
142
+
143
+ def perform_create(self, serializer):
144
+ """
145
+ Create a new taxonomy.
146
+ """
147
+ serializer.instance = create_taxonomy(**serializer.validated_data)
@@ -16,10 +16,10 @@ is_taxonomy_admin = rules.is_staff
16
16
  @rules.predicate
17
17
  def can_view_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool:
18
18
  """
19
- Anyone can view an enabled taxonomy,
19
+ Anyone can view an enabled taxonomy or list all taxonomies,
20
20
  but only taxonomy admins can view a disabled taxonomy.
21
21
  """
22
- return (taxonomy and taxonomy.enabled) or is_taxonomy_admin(user)
22
+ return not taxonomy or taxonomy.cast().enabled or is_taxonomy_admin(user)
23
23
 
24
24
 
25
25
  @rules.predicate
@@ -28,24 +28,21 @@ def can_change_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool:
28
28
  Even taxonomy admins cannot change system taxonomies.
29
29
  """
30
30
  return is_taxonomy_admin(user) and (
31
- not taxonomy or not taxonomy or (taxonomy and not taxonomy.system_defined)
31
+ not taxonomy or (taxonomy and not taxonomy.cast().system_defined)
32
32
  )
33
33
 
34
34
 
35
35
  @rules.predicate
36
- def can_change_taxonomy_tag(user: User, tag: Tag = None) -> bool:
36
+ def can_change_tag(user: User, tag: Tag = None) -> bool:
37
37
  """
38
38
  Even taxonomy admins cannot add tags to system taxonomies (their tags are system-defined), or free-text taxonomies
39
39
  (these don't have predefined tags).
40
40
  """
41
+ taxonomy = tag.taxonomy.cast() if (tag and tag.taxonomy_id) else None
41
42
  return is_taxonomy_admin(user) and (
42
43
  not tag
43
- or not tag.taxonomy
44
- or (
45
- tag.taxonomy
46
- and not tag.taxonomy.allow_free_text
47
- and not tag.taxonomy.system_defined
48
- )
44
+ or not taxonomy
45
+ or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined)
49
46
  )
50
47
 
51
48
 
@@ -54,10 +51,12 @@ def can_change_object_tag(user: User, object_tag: ObjectTag = None) -> bool:
54
51
  """
55
52
  Taxonomy admins can create or modify object tags on enabled taxonomies.
56
53
  """
54
+ taxonomy = (
55
+ object_tag.taxonomy.cast() if (object_tag and object_tag.taxonomy_id) else None
56
+ )
57
+ object_tag = taxonomy.object_tag_class.cast(object_tag) if taxonomy else object_tag
57
58
  return is_taxonomy_admin(user) and (
58
- not object_tag
59
- or not object_tag.taxonomy
60
- or (object_tag.taxonomy and object_tag.taxonomy.enabled)
59
+ not object_tag or not taxonomy or (taxonomy and taxonomy.enabled)
61
60
  )
62
61
 
63
62
 
@@ -68,8 +67,8 @@ rules.add_perm("oel_tagging.delete_taxonomy", can_change_taxonomy)
68
67
  rules.add_perm("oel_tagging.view_taxonomy", can_view_taxonomy)
69
68
 
70
69
  # Tag
71
- rules.add_perm("oel_tagging.add_tag", can_change_taxonomy_tag)
72
- rules.add_perm("oel_tagging.change_tag", can_change_taxonomy_tag)
70
+ rules.add_perm("oel_tagging.add_tag", can_change_tag)
71
+ rules.add_perm("oel_tagging.change_tag", can_change_tag)
73
72
  rules.add_perm("oel_tagging.delete_tag", is_taxonomy_admin)
74
73
  rules.add_perm("oel_tagging.view_tag", rules.always_allow)
75
74
 
@@ -0,0 +1,10 @@
1
+ """
2
+ Tagging API URLs.
3
+ """
4
+
5
+ from django.urls import path, include
6
+
7
+ from .rest_api import urls
8
+
9
+ app_name = "oel_tagging"
10
+ urlpatterns = [path("", include(urls))]