openedx-learning 0.14.0__py2.py3-none-any.whl → 0.16.0__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.
- openedx_learning/__init__.py +1 -1
- openedx_learning/apps/authoring/components/api.py +55 -1
- {openedx_learning-0.14.0.dist-info → openedx_learning-0.16.0.dist-info}/METADATA +3 -3
- {openedx_learning-0.14.0.dist-info → openedx_learning-0.16.0.dist-info}/RECORD +12 -11
- openedx_tagging/core/tagging/api.py +32 -0
- openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py +18 -0
- openedx_tagging/core/tagging/models/base.py +8 -0
- openedx_tagging/core/tagging/rest_api/v1/serializers.py +4 -1
- openedx_tagging/core/tagging/rest_api/v1/views.py +4 -0
- {openedx_learning-0.14.0.dist-info → openedx_learning-0.16.0.dist-info}/LICENSE.txt +0 -0
- {openedx_learning-0.14.0.dist-info → openedx_learning-0.16.0.dist-info}/WHEEL +0 -0
- {openedx_learning-0.14.0.dist-info → openedx_learning-0.16.0.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -12,16 +12,18 @@ are stored in this app.
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
from datetime import datetime
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
16
|
from enum import StrEnum, auto
|
|
17
17
|
from logging import getLogger
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from uuid import UUID
|
|
20
20
|
|
|
21
|
+
from django.core.exceptions import ValidationError
|
|
21
22
|
from django.db.models import Q, QuerySet
|
|
22
23
|
from django.db.transaction import atomic
|
|
23
24
|
from django.http.response import HttpResponse, HttpResponseNotFound
|
|
24
25
|
|
|
26
|
+
from ..collections.models import Collection, CollectionPublishableEntity
|
|
25
27
|
from ..contents import api as contents_api
|
|
26
28
|
from ..publishing import api as publishing_api
|
|
27
29
|
from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent
|
|
@@ -48,6 +50,7 @@ __all__ = [
|
|
|
48
50
|
"look_up_component_version_content",
|
|
49
51
|
"AssetError",
|
|
50
52
|
"get_redirect_response_for_component_asset",
|
|
53
|
+
"set_collections",
|
|
51
54
|
]
|
|
52
55
|
|
|
53
56
|
|
|
@@ -603,3 +606,54 @@ def get_redirect_response_for_component_asset(
|
|
|
603
606
|
)
|
|
604
607
|
|
|
605
608
|
return HttpResponse(headers={**info_headers, **redirect_headers})
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def set_collections(
|
|
612
|
+
learning_package_id: int,
|
|
613
|
+
component: Component,
|
|
614
|
+
collection_qset: QuerySet[Collection],
|
|
615
|
+
created_by: int | None = None,
|
|
616
|
+
) -> set[Collection]:
|
|
617
|
+
"""
|
|
618
|
+
Set collections for a given component.
|
|
619
|
+
|
|
620
|
+
These Collections must belong to the same LearningPackage as the Component, or a ValidationError will be raised.
|
|
621
|
+
|
|
622
|
+
Modified date of all collections related to component is updated.
|
|
623
|
+
|
|
624
|
+
Returns the updated collections.
|
|
625
|
+
"""
|
|
626
|
+
# Disallow adding entities outside the collection's learning package
|
|
627
|
+
invalid_collection = collection_qset.exclude(learning_package_id=learning_package_id).first()
|
|
628
|
+
if invalid_collection:
|
|
629
|
+
raise ValidationError(
|
|
630
|
+
f"Cannot add collection {invalid_collection.pk} in learning package "
|
|
631
|
+
f"{invalid_collection.learning_package_id} to component {component} in "
|
|
632
|
+
f"learning package {learning_package_id}."
|
|
633
|
+
)
|
|
634
|
+
current_relations = CollectionPublishableEntity.objects.filter(
|
|
635
|
+
entity=component.publishable_entity
|
|
636
|
+
).select_related('collection')
|
|
637
|
+
# Clear other collections for given component and add only new collections from collection_qset
|
|
638
|
+
removed_collections = set(
|
|
639
|
+
r.collection for r in current_relations.exclude(collection__in=collection_qset)
|
|
640
|
+
)
|
|
641
|
+
new_collections = set(collection_qset.exclude(
|
|
642
|
+
id__in=current_relations.values_list('collection', flat=True)
|
|
643
|
+
))
|
|
644
|
+
# Use `remove` instead of `CollectionPublishableEntity.delete()` to trigger m2m_changed signal which will handle
|
|
645
|
+
# updating component index.
|
|
646
|
+
component.publishable_entity.collections.remove(*removed_collections)
|
|
647
|
+
component.publishable_entity.collections.add(
|
|
648
|
+
*new_collections,
|
|
649
|
+
through_defaults={"created_by_id": created_by},
|
|
650
|
+
)
|
|
651
|
+
# Update modified date via update to avoid triggering post_save signal for collections
|
|
652
|
+
# The signal triggers index update for each collection synchronously which will be very slow in this case.
|
|
653
|
+
# Instead trigger the index update in the caller function asynchronously.
|
|
654
|
+
affected_collection = removed_collections | new_collections
|
|
655
|
+
Collection.objects.filter(
|
|
656
|
+
id__in=[collection.id for collection in affected_collection]
|
|
657
|
+
).update(modified=datetime.now(tz=timezone.utc))
|
|
658
|
+
|
|
659
|
+
return affected_collection
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Open edX Learning Core and Tagging.
|
|
5
5
|
Home-page: https://github.com/openedx/openedx-learning
|
|
6
6
|
Author: David Ormsbee
|
|
@@ -18,11 +18,11 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
License-File: LICENSE.txt
|
|
21
|
-
Requires-Dist: attrs
|
|
22
21
|
Requires-Dist: celery
|
|
23
22
|
Requires-Dist: djangorestframework<4.0
|
|
24
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: attrs
|
|
25
24
|
Requires-Dist: Django<5.0
|
|
25
|
+
Requires-Dist: rules<4.0
|
|
26
26
|
Requires-Dist: edx-drf-extensions
|
|
27
27
|
|
|
28
28
|
Open edX Learning Core (and Tagging)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
openedx_learning/__init__.py,sha256=
|
|
1
|
+
openedx_learning/__init__.py,sha256=GHiA1alNbJesOg1QdBMkyg2GfyNlz1kwiYuSV7HUxcY,68
|
|
2
2
|
openedx_learning/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
openedx_learning/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
openedx_learning/api/authoring.py,sha256=vbRpiQ2wOfN3oR2bbN0-bI2ra0QRtha9tVixKW1ENis,929
|
|
@@ -18,7 +18,7 @@ openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_opt
|
|
|
18
18
|
openedx_learning/apps/authoring/collections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
openedx_learning/apps/authoring/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
openedx_learning/apps/authoring/components/admin.py,sha256=3kFu_PR0BFb8U0zVv3WWhi27i__TDuJG0pFlwr3tKAw,4614
|
|
21
|
-
openedx_learning/apps/authoring/components/api.py,sha256=
|
|
21
|
+
openedx_learning/apps/authoring/components/api.py,sha256=TCLFCPb7ScoRWhe7YpQ7hBiJGjoa47OmazQWcN2hIwU,25759
|
|
22
22
|
openedx_learning/apps/authoring/components/apps.py,sha256=YoYPsI9gcleA3uEs8CiLIrjUncRMo2DKbYt4mDfzePg,770
|
|
23
23
|
openedx_learning/apps/authoring/components/models.py,sha256=T-wc7vxaWMlulQmMsVH7m6Pd857P3Eguo0vtflTLURI,13415
|
|
24
24
|
openedx_learning/apps/authoring/components/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -61,7 +61,7 @@ openedx_tagging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
61
61
|
openedx_tagging/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
62
|
openedx_tagging/core/tagging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
63
|
openedx_tagging/core/tagging/admin.py,sha256=Ngc2l9Mf6gkzmqu7aOwq-d0mgV8szx0GzSeuWFX7Kyg,1080
|
|
64
|
-
openedx_tagging/core/tagging/api.py,sha256=
|
|
64
|
+
openedx_tagging/core/tagging/api.py,sha256=HsdCmr1xv54fk6ggPuib3xKOwx5yJFbD3ZL4Lk5-d1o,19815
|
|
65
65
|
openedx_tagging/core/tagging/apps.py,sha256=-gp0VYqX4XQzwjjd-G68Ev2Op0INLh9Byz5UOqF5_7k,345
|
|
66
66
|
openedx_tagging/core/tagging/data.py,sha256=421EvmDzdM7H523dBVQk4J0W_UwTT4U5syqPRXUYK4g,1353
|
|
67
67
|
openedx_tagging/core/tagging/rules.py,sha256=Gzw2RCQxoAv2PpOwOWgpD17XoZfowlFnNgQqYn59q_g,6715
|
|
@@ -94,9 +94,10 @@ openedx_tagging/core/tagging/migrations/0014_minor_fixes.py,sha256=46_F-el1UylSR
|
|
|
94
94
|
openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py,sha256=jhS-T8o2mwu61E7hPbjjE_6MPLKRPQFAVu7pJHZNRz4,1454
|
|
95
95
|
openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py,sha256=X62sThGNv_0_wCBhWPcYRcs90EJwz9km9Nszr9yMVkM,2371
|
|
96
96
|
openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py,sha256=ljxONZOAGRW-tvPUIFmc0cNXN4Hoo3GJiyzGMncN6LI,567
|
|
97
|
+
openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py,sha256=zmr4b65T0vX6fYc8MpvSmQnYkAiNMpx3RKEd5tudsl8,517
|
|
97
98
|
openedx_tagging/core/tagging/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
99
|
openedx_tagging/core/tagging/models/__init__.py,sha256=yYdOnthuc7EUdfEULtZgqRwn5Y4bbYQmJCjVZqR5GTM,236
|
|
99
|
-
openedx_tagging/core/tagging/models/base.py,sha256=
|
|
100
|
+
openedx_tagging/core/tagging/models/base.py,sha256=RX6rZcG7njrgLvM4huh79lC1vXzFDLxqElJqOU50Od8,39606
|
|
100
101
|
openedx_tagging/core/tagging/models/import_export.py,sha256=Aj0pleh0nh2LNS6zmdB1P4bpdgUMmvmobTkqBerORAI,4570
|
|
101
102
|
openedx_tagging/core/tagging/models/system_defined.py,sha256=_6LfvUZGEltvQMtm2OXy6TOLh3C8GnVTqtZDSAZW6K4,9062
|
|
102
103
|
openedx_tagging/core/tagging/models/utils.py,sha256=-A3Dj24twmTf65UB7G4WLvb_9qEvduEPIwahZ-FJDlg,1926
|
|
@@ -106,12 +107,12 @@ openedx_tagging/core/tagging/rest_api/urls.py,sha256=egXaRQv1EAgF04ThgVZBQuvLK1L
|
|
|
106
107
|
openedx_tagging/core/tagging/rest_api/utils.py,sha256=XZXixZ44vpNlxiyFplW8Lktyh_m1EfR3Y-tnyvA7acc,3620
|
|
107
108
|
openedx_tagging/core/tagging/rest_api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
108
109
|
openedx_tagging/core/tagging/rest_api/v1/permissions.py,sha256=7HPE_NuKku_ISnkeE_HsFNXVYt0IbVkJN6M4wqwHGHU,2443
|
|
109
|
-
openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=
|
|
110
|
+
openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=D7brBbgmU7MnbU7Ln_xiLG-TtXno0aciE5AhjsXlClE,13903
|
|
110
111
|
openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
|
|
111
|
-
openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=
|
|
112
|
+
openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=LA0EF7-p91JgVrLZpZaG474elKD1dswODGvoPIw47Mg,35837
|
|
112
113
|
openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
|
|
113
|
-
openedx_learning-0.
|
|
114
|
-
openedx_learning-0.
|
|
115
|
-
openedx_learning-0.
|
|
116
|
-
openedx_learning-0.
|
|
117
|
-
openedx_learning-0.
|
|
114
|
+
openedx_learning-0.16.0.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
|
|
115
|
+
openedx_learning-0.16.0.dist-info/METADATA,sha256=KFY2MnwnFFqI4lWfz9rVFuvcSvhhLWr9L26T-RIJRtI,8777
|
|
116
|
+
openedx_learning-0.16.0.dist-info/WHEEL,sha256=AHX6tWk3qWuce7vKLrj7lnulVHEdWoltgauo8bgCXgU,109
|
|
117
|
+
openedx_learning-0.16.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
|
|
118
|
+
openedx_learning-0.16.0.dist-info/RECORD,,
|
|
@@ -484,3 +484,35 @@ def delete_tags_from_taxonomy(
|
|
|
484
484
|
"""
|
|
485
485
|
taxonomy = taxonomy.cast()
|
|
486
486
|
taxonomy.delete_tags(tags, with_subtags)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def copy_tags(source_object_id: str, dest_object_id: str):
|
|
490
|
+
"""
|
|
491
|
+
Copy all tags from one object to another.
|
|
492
|
+
|
|
493
|
+
This keeps all not-copied tags and delete all
|
|
494
|
+
previous copied tags of the dest object.
|
|
495
|
+
If there are not-copied tags that also are in 'source_object_id',
|
|
496
|
+
then they become copied.
|
|
497
|
+
"""
|
|
498
|
+
source_object_tags = get_object_tags(
|
|
499
|
+
source_object_id,
|
|
500
|
+
)
|
|
501
|
+
copied_tags = ObjectTag.objects.filter(
|
|
502
|
+
object_id=dest_object_id,
|
|
503
|
+
is_copied=True,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
with transaction.atomic():
|
|
507
|
+
# Delete all copied tags of destination
|
|
508
|
+
copied_tags.delete()
|
|
509
|
+
|
|
510
|
+
# Copy an create object_tags in destination
|
|
511
|
+
for object_tag in source_object_tags:
|
|
512
|
+
ObjectTag.objects.update_or_create(
|
|
513
|
+
object_id=dest_object_id,
|
|
514
|
+
taxonomy_id=object_tag.taxonomy_id,
|
|
515
|
+
tag_id=object_tag.tag_id,
|
|
516
|
+
defaults={"is_copied": True},
|
|
517
|
+
# Note: _value and _export_id are set automatically
|
|
518
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-10-04 19:21
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('oel_tagging', '0017_alter_tagimporttask_status'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='objecttag',
|
|
15
|
+
name='is_copied',
|
|
16
|
+
field=models.BooleanField(default=False, help_text="True if this object tag has been copied from one object to another using 'copy_tags' api function"),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -795,6 +795,13 @@ class ObjectTag(models.Model):
|
|
|
795
795
|
"Tag associated with this object tag. Provides the tag's 'value' if set."
|
|
796
796
|
),
|
|
797
797
|
)
|
|
798
|
+
is_copied = models.BooleanField(
|
|
799
|
+
default=False,
|
|
800
|
+
help_text=_(
|
|
801
|
+
"True if this object tag has been copied from one object to another"
|
|
802
|
+
" using 'copy_tags' api function"
|
|
803
|
+
),
|
|
804
|
+
)
|
|
798
805
|
_export_id = case_insensitive_char_field(
|
|
799
806
|
max_length=255,
|
|
800
807
|
help_text=_(
|
|
@@ -981,6 +988,7 @@ class ObjectTag(models.Model):
|
|
|
981
988
|
self.tag = object_tag.tag
|
|
982
989
|
self.taxonomy = object_tag.taxonomy
|
|
983
990
|
self.object_id = object_tag.object_id
|
|
991
|
+
self.is_copied = object_tag.is_copied
|
|
984
992
|
self._value = object_tag._value # pylint: disable=protected-access
|
|
985
993
|
self._export_id = object_tag._export_id # pylint: disable=protected-access
|
|
986
994
|
return self
|
|
@@ -176,6 +176,9 @@ class ObjectTagsByTaxonomySerializer(UserPermissionsSerializerMixin, serializers
|
|
|
176
176
|
"""
|
|
177
177
|
Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy
|
|
178
178
|
"""
|
|
179
|
+
# Allows consumers like edx-platform to override this
|
|
180
|
+
ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class
|
|
181
|
+
|
|
179
182
|
can_tag_object_perm = f"{self.app_label}.can_tag_object"
|
|
180
183
|
by_object: dict[str, dict[str, Any]] = {}
|
|
181
184
|
for obj_tag in instance:
|
|
@@ -194,7 +197,7 @@ class ObjectTagsByTaxonomySerializer(UserPermissionsSerializerMixin, serializers
|
|
|
194
197
|
"export_id": obj_tag.export_id,
|
|
195
198
|
}
|
|
196
199
|
taxonomies.append(tax_entry)
|
|
197
|
-
tax_entry["tags"].append(
|
|
200
|
+
tax_entry["tags"].append(ObjectTagViewMinimalSerializer(obj_tag, context=self.context).data)
|
|
198
201
|
return by_object
|
|
199
202
|
|
|
200
203
|
|
|
@@ -37,6 +37,7 @@ from ..utils import view_auth_classes
|
|
|
37
37
|
from .permissions import ObjectTagObjectPermissions, TaxonomyObjectPermissions, TaxonomyTagsObjectPermissions
|
|
38
38
|
from .serializers import (
|
|
39
39
|
ObjectTagListQueryParamsSerializer,
|
|
40
|
+
ObjectTagMinimalSerializer,
|
|
40
41
|
ObjectTagsByTaxonomySerializer,
|
|
41
42
|
ObjectTagSerializer,
|
|
42
43
|
ObjectTagUpdateBodySerializer,
|
|
@@ -443,7 +444,10 @@ class ObjectTagView(
|
|
|
443
444
|
* 405 - Method not allowed
|
|
444
445
|
"""
|
|
445
446
|
|
|
447
|
+
# Serializer used in `get_queryset` when getting tags per taxonomy
|
|
446
448
|
serializer_class = ObjectTagSerializer
|
|
449
|
+
# Serializer used in the result in `to_representation` in `ObjectTagsByTaxonomySerializer`
|
|
450
|
+
minimal_serializer_class = ObjectTagMinimalSerializer
|
|
447
451
|
permission_classes = [ObjectTagObjectPermissions]
|
|
448
452
|
lookup_field = "object_id"
|
|
449
453
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|