openedx-learning 0.3.3__py2.py3-none-any.whl → 0.3.5__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.
@@ -1,4 +1,4 @@
1
1
  """
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
- __version__ = "0.3.3"
4
+ __version__ = "0.3.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openedx-learning
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: An experiment.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -17,12 +17,12 @@ Classifier: Natural Language :: English
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3.8
19
19
  Requires-Python: >=3.8
20
- Requires-Dist: Django (<5.0)
21
- Requires-Dist: celery
22
- Requires-Dist: attrs
23
- Requires-Dist: edx-drf-extensions
24
20
  Requires-Dist: djangorestframework (<4.0)
21
+ Requires-Dist: celery
25
22
  Requires-Dist: rules (<4.0)
23
+ Requires-Dist: Django (<5.0)
24
+ Requires-Dist: edx-drf-extensions
25
+ Requires-Dist: attrs
26
26
 
27
27
  openedx-learning
28
28
  =============================
@@ -210,9 +210,10 @@ Change Log
210
210
  Unreleased
211
211
  ~~~~~~~~~~
212
212
 
213
+ * Removed usage of ``tox-battery`` and added support for ``tox 4.0``
213
214
  * Switch from ``edx-sphinx-theme`` to ``sphinx-book-theme`` since the former is
214
215
  deprecated. See https://github.com/openedx/edx-sphinx-theme/issues/184 for
215
- more details.
216
+ more details.
216
217
 
217
218
  [0.1.0] - 2021-08-08
218
219
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1,4 +1,4 @@
1
- openedx_learning/__init__.py,sha256=uCM-EDfQ6j2lvGvX4fNePRahmlupHBm1GFrksXxa37k,67
1
+ openedx_learning/__init__.py,sha256=Ho0NTFgVeG0pxXs5miy44Bt4snnD-dyzeE4yKvRL1bE,67
2
2
  openedx_learning/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  openedx_learning/contrib/media_server/__init__.py,sha256=iYijWFCl5RNR9omSu22kMl49EfponoqXBqXr0HMp4QI,56
4
4
  openedx_learning/contrib/media_server/apps.py,sha256=FPT0rsUFtPyhFpWKjSI1e_s58wU0IbDyaAW_66V6sY4,816
@@ -43,7 +43,7 @@ openedx_tagging/__init__.py,sha256=V9N8M7f9LYlAbA_DdPUsHzTnWjYRXKGa5qHw9P1JnNI,3
43
43
  openedx_tagging/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  openedx_tagging/core/tagging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  openedx_tagging/core/tagging/admin.py,sha256=5mSTxlftMq5MVGzdE8xl3AcxgtfpnrKpXPNdvw7hAno,1075
46
- openedx_tagging/core/tagging/api.py,sha256=P0NBPTsJvrQqRzuLrFgM6x0SumKsxQ2c-pRZGWC2OKs,12063
46
+ openedx_tagging/core/tagging/api.py,sha256=KFXA3x06ahaUeYRVZ9ZaELCJogDedm3lqFj558FjCK8,13840
47
47
  openedx_tagging/core/tagging/apps.py,sha256=-gp0VYqX4XQzwjjd-G68Ev2Op0INLh9Byz5UOqF5_7k,345
48
48
  openedx_tagging/core/tagging/data.py,sha256=C5uRsR3she-iB3n1OyPdQm6PsIc1Lg-XOI7HT0yNLPI,1327
49
49
  openedx_tagging/core/tagging/rules.py,sha256=vhBaVpRBAjsx7B2tbeFqorU3H-LZgWERlbpFKFaEeM4,6113
@@ -87,10 +87,10 @@ openedx_tagging/core/tagging/rest_api/v1/permissions.py,sha256=FeSulmsFD7wAAuYpx
87
87
  openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=HMDWJ--VoRx1KfmCIRdjHsz8yNUHDYT3BoxsU_kZbiY,8558
88
88
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
89
89
  openedx_tagging/core/tagging/rest_api/v1/utils.py,sha256=PR8ERgXaL4_r9kiOLpOSgGdtK7oA6jIP74ej2gDWCUs,822
90
- openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=-wcFEHEvVTCbjRYNaGp7N-1w_AXTZqGvEh50kfZH0Zw,28989
90
+ openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=MixTy8JSnJ6-F6Km-udNDh-SY6IKcijGILP7Q3aWCvc,28636
91
91
  openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
92
- openedx_learning-0.3.3.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
93
- openedx_learning-0.3.3.dist-info/METADATA,sha256=cOczYR2FkAwPNSDqfF6fn-KvEEqqe036iJGj6TvcIXM,8687
94
- openedx_learning-0.3.3.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110
95
- openedx_learning-0.3.3.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
96
- openedx_learning-0.3.3.dist-info/RECORD,,
92
+ openedx_learning-0.3.5.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
93
+ openedx_learning-0.3.5.dist-info/METADATA,sha256=Fs4z5Sc1ZxmTxQSamHeACYCb1JjoQx9V8ecsI2RjXPo,8758
94
+ openedx_learning-0.3.5.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110
95
+ openedx_learning-0.3.5.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
96
+ openedx_learning-0.3.5.dist-info/RECORD,,
@@ -12,6 +12,8 @@ are stored in this app.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ from typing import Any
16
+
15
17
  from django.db import models, transaction
16
18
  from django.db.models import F, QuerySet, Value
17
19
  from django.db.models.functions import Coalesce, Concat, Lower
@@ -157,6 +159,7 @@ def resync_object_tags(object_tags: QuerySet | None = None) -> int:
157
159
  def get_object_tags(
158
160
  object_id: str,
159
161
  taxonomy_id: int | None = None,
162
+ include_deleted: bool = False,
160
163
  object_tag_class: type[ObjectTag] = ObjectTag
161
164
  ) -> QuerySet[ObjectTag]:
162
165
  """
@@ -165,8 +168,16 @@ def get_object_tags(
165
168
  Pass taxonomy_id to limit the returned object_tags to a specific taxonomy.
166
169
  """
167
170
  filters = {"taxonomy_id": taxonomy_id} if taxonomy_id else {}
171
+ base_qs = (
172
+ object_tag_class.objects
173
+ .filter(object_id=object_id, **filters)
174
+ .exclude(taxonomy__enabled=False) # Exclude if the whole taxonomy is disabled
175
+ )
176
+ if not include_deleted:
177
+ base_qs = base_qs.exclude(taxonomy_id=None) # Exclude if the whole taxonomy was deleted
178
+ base_qs = base_qs.exclude(tag_id=None, taxonomy__allow_free_text=False) # Exclude if just the tag is deleted
168
179
  tags = (
169
- object_tag_class.objects.filter(object_id=object_id, **filters)
180
+ base_qs
170
181
  # Preload related objects, including data for the "get_lineage" method on ObjectTag/Tag:
171
182
  .select_related("taxonomy", "tag", "tag__parent", "tag__parent__parent")
172
183
  # Sort the tags within each taxonomy in "tree order". See Taxonomy._get_filtered_tags_deep for details on this:
@@ -185,6 +196,31 @@ def get_object_tags(
185
196
  return tags
186
197
 
187
198
 
199
+ def get_object_tag_counts(object_id_pattern: str) -> dict[str, int]:
200
+ """
201
+ Given an object ID, a "starts with" glob pattern like
202
+ "course-v1:foo+bar+baz@*", or a list of "comma,separated,IDs", return a
203
+ dict of matching object IDs and how many tags each object has.
204
+
205
+ Deleted tags and disabled taxonomies are excluded from the counts, even if
206
+ ObjectTag data about them is present.
207
+ """
208
+ # Note: in the future we may add an option to exclude system taxonomies from the count.
209
+ qs: Any = ObjectTag.objects
210
+ if object_id_pattern.endswith("*"):
211
+ qs = qs.filter(object_id__startswith=object_id_pattern[0:len(object_id_pattern) - 1])
212
+ elif "*" in object_id_pattern:
213
+ raise ValueError("Wildcard matches are only supported if the * is at the end.")
214
+ else:
215
+ qs = qs.filter(object_id__in=object_id_pattern.split(","))
216
+ # Don't include deleted tags or disabled taxonomies:
217
+ qs = qs.exclude(taxonomy_id=None) # The whole taxonomy was deleted
218
+ qs = qs.exclude(taxonomy__enabled=False) # The whole taxonomy is disabled
219
+ qs = qs.exclude(tag_id=None, taxonomy__allow_free_text=False) # The taxonomy exists but the tag is deleted
220
+ qs = qs.values("object_id").annotate(num_tags=models.Count("id")).order_by("object_id")
221
+ return {row["object_id"]: row["num_tags"] for row in qs}
222
+
223
+
188
224
  def delete_object_tags(object_id: str):
189
225
  """
190
226
  Delete all ObjectTag entries for a given object.
@@ -3,8 +3,6 @@ Tagging API Views
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- from typing import Any
7
-
8
6
  from django.db import models
9
7
  from django.http import Http404, HttpResponse
10
8
  from rest_framework import mixins, status
@@ -20,6 +18,7 @@ from ...api import (
20
18
  add_tag_to_taxonomy,
21
19
  create_taxonomy,
22
20
  delete_tags_from_taxonomy,
21
+ get_object_tag_counts,
23
22
  get_object_tags,
24
23
  get_taxonomies,
25
24
  get_taxonomy,
@@ -29,7 +28,7 @@ from ...api import (
29
28
  from ...data import TagDataQuerySet
30
29
  from ...import_export.api import export_tags, get_last_import_log, import_tags
31
30
  from ...import_export.parsers import ParserFormat
32
- from ...models import ObjectTag, Taxonomy
31
+ from ...models import Taxonomy
33
32
  from ...rules import ObjectTagPermissionItem
34
33
  from ..paginators import TAGS_THRESHOLD, DisabledTagsPagination, TagsPagination
35
34
  from .permissions import ObjectTagObjectPermissions, TagObjectPermissions, TaxonomyObjectPermissions
@@ -295,7 +294,8 @@ class TaxonomyView(ModelViewSet):
295
294
  @action(detail=True, url_path="tags/import", methods=["put"])
296
295
  def update_import(self, request: Request, **_kwargs) -> Response:
297
296
  """
298
- Imports tags from the uploaded file to an already created taxonomy.
297
+ Imports tags from the uploaded file to an already created taxonomy,
298
+ overwriting any existing tags.
299
299
  """
300
300
  body = TaxonomyImportBodySerializer(data=request.data)
301
301
  body.is_valid(raise_exception=True)
@@ -305,7 +305,7 @@ class TaxonomyView(ModelViewSet):
305
305
 
306
306
  taxonomy = self.get_object()
307
307
  try:
308
- import_success = import_tags(taxonomy, file, parser_format)
308
+ import_success = import_tags(taxonomy, file, parser_format, replace=True)
309
309
 
310
310
  if import_success:
311
311
  serializer = self.get_serializer(taxonomy)
@@ -517,16 +517,10 @@ class ObjectTagCountsView(
517
517
  """
518
518
  # This API does NOT bother doing any permission checks as the # of tags is not considered sensitive information.
519
519
  object_id_pattern = self.kwargs["object_id_pattern"]
520
- qs: Any = ObjectTag.objects
521
- if object_id_pattern.endswith("*"):
522
- qs = qs.filter(object_id__startswith=object_id_pattern[0:len(object_id_pattern) - 1])
523
- elif "*" in object_id_pattern:
524
- raise ValidationError("Wildcard matches are only supported if the * is at the end.")
525
- else:
526
- qs = qs.filter(object_id__in=object_id_pattern.split(","))
527
-
528
- qs = qs.values("object_id").annotate(num_tags=models.Count("id")).order_by("object_id")
529
- return Response({row["object_id"]: row["num_tags"] for row in qs})
520
+ try:
521
+ return Response(get_object_tag_counts(object_id_pattern))
522
+ except ValueError as err:
523
+ raise ValidationError(err.args[0]) from err
530
524
 
531
525
 
532
526
  @view_auth_classes