From d653a8e5e44c4119b7c6b86b2f200bce4d1cfc32 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 1 Oct 2024 19:36:03 -0500 Subject: [PATCH 1/9] feat: Copy tags function and read_only Object tags --- openedx_tagging/core/tagging/api.py | 30 +++++++ .../migrations/0018_objecttag_read_only.py | 18 ++++ openedx_tagging/core/tagging/models/base.py | 7 ++ .../openedx_tagging/core/tagging/test_api.py | 84 +++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 1ccc1992..1a7a5494 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -484,3 +484,33 @@ def delete_tags_from_taxonomy( """ taxonomy = taxonomy.cast() taxonomy.delete_tags(tags, with_subtags) + + +def copy_tags( + object_id_from: str, + object_id_to: str, + include_deleted: bool = True, + object_tag_class: type[ObjectTag] = ObjectTag +): + """ + Copy all tags from one object to another. + This deletes all previous object tags in destination. + """ + object_tags = get_object_tags( + object_id_from, + include_deleted=include_deleted, + object_tag_class=object_tag_class, + ) + ObjectTagClass = object_tag_class + with transaction.atomic(): + # Delete all object tags of destination + delete_object_tags(object_id_to) + + # Copy an create object_tags in destination + for object_tag in object_tags: + new_object_tag = ObjectTagClass() + new_object_tag.copy(object_tag) + new_object_tag.id = None # To create a new instance in DB + new_object_tag.object_id = object_id_to + new_object_tag.read_only = True + new_object_tag.save() diff --git a/openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py b/openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py new file mode 100644 index 00000000..ea49b97d --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-01 20:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0017_alter_tagimporttask_status'), + ] + + operations = [ + migrations.AddField( + model_name='objecttag', + name='read_only', + field=models.BooleanField(default=False, help_text='True if this object tag cannot be deleted by the user.'), + ), + ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 3a377587..5b2fce10 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -795,6 +795,12 @@ class ObjectTag(models.Model): "Tag associated with this object tag. Provides the tag's 'value' if set." ), ) + read_only = models.BooleanField( + default=False, + help_text=_( + "True if this object tag cannot be deleted by the user." + ), + ) _export_id = case_insensitive_char_field( max_length=255, help_text=_( @@ -981,6 +987,7 @@ def copy(self, object_tag: ObjectTag) -> Self: self.tag = object_tag.tag self.taxonomy = object_tag.taxonomy self.object_id = object_tag.object_id + self.read_only = object_tag.read_only self._value = object_tag._value # pylint: disable=protected-access self._export_id = object_tag._export_id # pylint: disable=protected-access return self diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 32142c77..508c8569 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -913,3 +913,87 @@ def test_get_object_tag_counts_deleted_disabled(self) -> None: assert tagging_api.get_object_tag_counts("object_*") == {obj1: 1, obj2: 2} tagging_api.add_tag_to_taxonomy(self.taxonomy, "DPANN", parent_tag_value="Archaea") assert tagging_api.get_object_tag_counts("object_*") == {obj1: 2, obj2: 2} + + def test_copy_tags(self) -> None: + obj1 = "object_id1" + obj2 = "object_id2" + + tags_list = [ + { + "value": "English", + "taxonomy": self.language_taxonomy, + }, + { + "value": "DPANN", + "taxonomy": self.taxonomy, + }, + ] + + for tag_object in tags_list: + tagging_api.tag_object(object_id=obj1, taxonomy=tag_object["taxonomy"], tags=[tag_object["value"]]) + + tagging_api.tag_object(object_id=obj2, taxonomy=self.taxonomy, tags=["Chordata"]) + tagging_api.tag_object(object_id=obj2, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) + + tagging_api.copy_tags(obj1, obj2) + + object_tags = tagging_api.get_object_tags(obj2) + + assert len(object_tags) == 2 + for index, object_tag in enumerate(object_tags): + object_tag.full_clean() + assert object_tag.value == tags_list[index]["value"] + assert not object_tag.is_deleted + assert object_tag.taxonomy == tags_list[index]["taxonomy"] + assert object_tag.object_id == obj2 + assert object_tag.read_only is True + + def test_copy_tags_with_deleted(self) -> None: + obj1 = "object_id1" + obj2 = "object_id2" + + tags_list = [ + { + "value": "English", + "taxonomy": self.language_taxonomy, + "deleted": False, + }, + { + "value": "DPANN", + "taxonomy": self.taxonomy, + "deleted": True, + }, + ] + + for tag_object in tags_list: + tagging_api.tag_object(object_id=obj1, taxonomy=tag_object["taxonomy"], tags=[tag_object["value"]]) + + tagging_api.tag_object(object_id=obj2, taxonomy=self.taxonomy, tags=["Chordata"]) + tagging_api.tag_object(object_id=obj2, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) + + tagging_api.delete_tags_from_taxonomy(self.taxonomy, ["DPANN"], with_subtags=True) + + # Copy tags, also with deleted tags + tagging_api.copy_tags(obj1, obj2) + + object_tags = tagging_api.get_object_tags(obj2, include_deleted=True) + + assert len(object_tags) == 2 + for index, object_tag in enumerate(object_tags): + assert object_tag.value == tags_list[index]["value"] + assert object_tag.is_deleted == tags_list[index]["deleted"] + assert object_tag.taxonomy == tags_list[index]["taxonomy"] + assert object_tag.object_id == obj2 + assert object_tag.read_only is True + + # Copy tags, without deleted tags + tagging_api.copy_tags(obj1, obj2, include_deleted=False) + + object_tags = tagging_api.get_object_tags(obj2, include_deleted=True) + + assert len(object_tags) == 1 + assert object_tags[0].value == tags_list[0]["value"] + assert not object_tags[0].is_deleted + assert object_tags[0].taxonomy == tags_list[0]["taxonomy"] + assert object_tags[0].object_id == obj2 + assert object_tags[0].read_only is True From 3e985d6a78388fb36746a1c6fc5eb0172b482e9b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 4 Oct 2024 15:21:05 -0500 Subject: [PATCH 2/9] refactor: copy_tags function to keep not-copied tags --- openedx_tagging/core/tagging/api.py | 53 ++++++-- ...ad_only.py => 0018_objecttag_is_copied.py} | 6 +- openedx_tagging/core/tagging/models/base.py | 7 +- .../openedx_tagging/core/tagging/test_api.py | 116 ++++++++++++++++-- 4 files changed, 153 insertions(+), 29 deletions(-) rename openedx_tagging/core/tagging/migrations/{0018_objecttag_read_only.py => 0018_objecttag_is_copied.py} (52%) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 1a7a5494..b88da3d4 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -487,30 +487,57 @@ def delete_tags_from_taxonomy( def copy_tags( - object_id_from: str, - object_id_to: str, + source_object_id: str, + dest_object_id: str, include_deleted: bool = True, object_tag_class: type[ObjectTag] = ObjectTag ): """ Copy all tags from one object to another. - This deletes all previous object tags in destination. + + This keeps all not-copied tags and delete all + previous copied tags of the dest object. + If there are not-copied tags that also are in 'source_object_id', + then they become copied. """ + ObjectTagClass = object_tag_class + + def object_has_tag(object_tag): + try: + result = ObjectTagClass.objects.get( + object_id=dest_object_id, + _export_id=object_tag.export_id, + _value=object_tag.value, + ) + return result + except ObjectTagClass.DoesNotExist: + return None + object_tags = get_object_tags( - object_id_from, + source_object_id, include_deleted=include_deleted, object_tag_class=object_tag_class, ) - ObjectTagClass = object_tag_class + copied_tags = ObjectTagClass.objects.filter( + object_id=dest_object_id, + is_copied=True, + ) + with transaction.atomic(): - # Delete all object tags of destination - delete_object_tags(object_id_to) + # Delete all copied tags of destination + copied_tags.delete() # Copy an create object_tags in destination for object_tag in object_tags: - new_object_tag = ObjectTagClass() - new_object_tag.copy(object_tag) - new_object_tag.id = None # To create a new instance in DB - new_object_tag.object_id = object_id_to - new_object_tag.read_only = True - new_object_tag.save() + existing_object_tag = object_has_tag(object_tag) + if existing_object_tag: + # Now this tag is copied + existing_object_tag.is_copied = True + existing_object_tag.save() + else: + new_object_tag = ObjectTagClass() + new_object_tag.copy(object_tag) + new_object_tag.id = None # To create a new instance in DB + new_object_tag.object_id = dest_object_id + new_object_tag.is_copied = True + new_object_tag.save() diff --git a/openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py b/openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py similarity index 52% rename from openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py rename to openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py index ea49b97d..add2b6e9 100644 --- a/openedx_tagging/core/tagging/migrations/0018_objecttag_read_only.py +++ b/openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-10-01 20:37 +# Generated by Django 4.2.16 on 2024-10-04 19:21 from django.db import migrations, models @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='objecttag', - name='read_only', - field=models.BooleanField(default=False, help_text='True if this object tag cannot be deleted by the user.'), + name='is_copied', + 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"), ), ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 5b2fce10..6b2cccf9 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -795,10 +795,11 @@ class ObjectTag(models.Model): "Tag associated with this object tag. Provides the tag's 'value' if set." ), ) - read_only = models.BooleanField( + is_copied = models.BooleanField( default=False, help_text=_( - "True if this object tag cannot be deleted by the user." + "True if this object tag has been copied from one object to another" + " using 'copy_tags' api function" ), ) _export_id = case_insensitive_char_field( @@ -987,7 +988,7 @@ def copy(self, object_tag: ObjectTag) -> Self: self.tag = object_tag.tag self.taxonomy = object_tag.taxonomy self.object_id = object_tag.object_id - self.read_only = object_tag.read_only + self.is_copied = object_tag.is_copied self._value = object_tag._value # pylint: disable=protected-access self._export_id = object_tag._export_id # pylint: disable=protected-access return self diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 508c8569..476065e1 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -932,9 +932,6 @@ def test_copy_tags(self) -> None: for tag_object in tags_list: tagging_api.tag_object(object_id=obj1, taxonomy=tag_object["taxonomy"], tags=[tag_object["value"]]) - tagging_api.tag_object(object_id=obj2, taxonomy=self.taxonomy, tags=["Chordata"]) - tagging_api.tag_object(object_id=obj2, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) - tagging_api.copy_tags(obj1, obj2) object_tags = tagging_api.get_object_tags(obj2) @@ -946,7 +943,7 @@ def test_copy_tags(self) -> None: assert not object_tag.is_deleted assert object_tag.taxonomy == tags_list[index]["taxonomy"] assert object_tag.object_id == obj2 - assert object_tag.read_only is True + assert object_tag.is_copied is True def test_copy_tags_with_deleted(self) -> None: obj1 = "object_id1" @@ -968,9 +965,6 @@ def test_copy_tags_with_deleted(self) -> None: for tag_object in tags_list: tagging_api.tag_object(object_id=obj1, taxonomy=tag_object["taxonomy"], tags=[tag_object["value"]]) - tagging_api.tag_object(object_id=obj2, taxonomy=self.taxonomy, tags=["Chordata"]) - tagging_api.tag_object(object_id=obj2, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) - tagging_api.delete_tags_from_taxonomy(self.taxonomy, ["DPANN"], with_subtags=True) # Copy tags, also with deleted tags @@ -984,16 +978,118 @@ def test_copy_tags_with_deleted(self) -> None: assert object_tag.is_deleted == tags_list[index]["deleted"] assert object_tag.taxonomy == tags_list[index]["taxonomy"] assert object_tag.object_id == obj2 - assert object_tag.read_only is True + assert object_tag.is_copied is True # Copy tags, without deleted tags tagging_api.copy_tags(obj1, obj2, include_deleted=False) - object_tags = tagging_api.get_object_tags(obj2, include_deleted=True) + object_tags = tagging_api.get_object_tags(obj2, include_deleted=False) assert len(object_tags) == 1 assert object_tags[0].value == tags_list[0]["value"] assert not object_tags[0].is_deleted assert object_tags[0].taxonomy == tags_list[0]["taxonomy"] assert object_tags[0].object_id == obj2 - assert object_tags[0].read_only is True + assert object_tags[0].is_copied is True + + def test_copy_tags_with_non_copied(self) -> None: + obj1 = "object_id1" + obj2 = "object_id2" + + tagging_api.tag_object(object_id=obj1, taxonomy=self.language_taxonomy, tags=["English"]) + + tagging_api.tag_object(object_id=obj2, taxonomy=self.taxonomy, tags=["Chordata"]) + tagging_api.tag_object(object_id=obj2, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) + + tagging_api.copy_tags(obj1, obj2) + object_tags = tagging_api.get_object_tags(obj2) + + # Tags must be the non-copied and the copied tag. + expected_tags = [ + { + "value": "has a notochord", + "taxonomy": self.free_text_taxonomy, + "copied": False, + }, + { + "value": "English", + "taxonomy": self.language_taxonomy, + "copied": True, + }, + { + "value": "Chordata", + "taxonomy": self.taxonomy, + "copied": False, + }, + ] + assert len(object_tags) == 3 + for index, object_tag in enumerate(object_tags): + assert object_tag.value == expected_tags[index]["value"] + assert not object_tag.is_deleted + assert object_tag.taxonomy == expected_tags[index]["taxonomy"] + assert object_tag.object_id == obj2 + assert object_tag.is_copied == expected_tags[index]["copied"] + + # Delete tags of 'obj1' and add other + tagging_api.delete_object_tags(obj1) + tagging_api.tag_object(object_id=obj1, taxonomy=self.taxonomy, tags=["DPANN"]) + tagging_api.copy_tags(obj1, obj2) + object_tags = tagging_api.get_object_tags(obj2) + + # Tags must be the non-copied and the new copied tag. + # The previous copied tags must be deleted. + expected_tags = [ + { + "value": "has a notochord", + "taxonomy": self.free_text_taxonomy, + "copied": False, + }, + { + "value": "DPANN", + "taxonomy": self.taxonomy, + "copied": True, + }, + { + "value": "Chordata", + "taxonomy": self.taxonomy, + "copied": False, + }, + ] + assert len(object_tags) == 3 + for index, object_tag in enumerate(object_tags): + assert object_tag.value == expected_tags[index]["value"] + assert not object_tag.is_deleted + assert object_tag.taxonomy == expected_tags[index]["taxonomy"] + assert object_tag.object_id == obj2 + assert object_tag.is_copied == expected_tags[index]["copied"] + + # Add a tag used by 'obj2' + tagging_api.tag_object(object_id=obj1, taxonomy=self.free_text_taxonomy, tags=["has a notochord"]) + tagging_api.copy_tags(obj1, obj2) + object_tags = tagging_api.get_object_tags(obj2) + + # The non-copied tag 'has a notochord' must be copied now. + expected_tags = [ + { + "value": "has a notochord", + "taxonomy": self.free_text_taxonomy, + "copied": True, + }, + { + "value": "DPANN", + "taxonomy": self.taxonomy, + "copied": True, + }, + { + "value": "Chordata", + "taxonomy": self.taxonomy, + "copied": False, + }, + ] + assert len(object_tags) == 3 + for index, object_tag in enumerate(object_tags): + assert object_tag.value == expected_tags[index]["value"] + assert not object_tag.is_deleted + assert object_tag.taxonomy == expected_tags[index]["taxonomy"] + assert object_tag.object_id == obj2 + assert object_tag.is_copied == expected_tags[index]["copied"] From fcd8437a113729a5879ae404842d78afba8bafc2 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 8 Oct 2024 16:05:08 -0500 Subject: [PATCH 3/9] refactor: Update `ObjectTagsByTaxonomySerializer` to use context to get the serializer for tags --- openedx_learning/__init__.py | 2 +- openedx_tagging/core/tagging/rest_api/v1/serializers.py | 3 ++- openedx_tagging/core/tagging/rest_api/v1/views.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 5bf29b11..7fde125a 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1,4 +1,4 @@ """ Open edX Learning ("Learning Core"). """ -__version__ = "0.13.1" +__version__ = "0.14.0" diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index 7e4d7167..eccf2b6a 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -176,6 +176,7 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: """ Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy """ + ObjectTagViewMinimalSerializer = self.context["view"].minimal_serilizer_class can_tag_object_perm = f"{self.app_label}.can_tag_object" by_object: dict[str, dict[str, Any]] = {} for obj_tag in instance: @@ -194,7 +195,7 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: "export_id": obj_tag.export_id, } taxonomies.append(tax_entry) - tax_entry["tags"].append(ObjectTagMinimalSerializer(obj_tag, context=self.context).data) + tax_entry["tags"].append(ObjectTagViewMinimalSerializer(obj_tag, context=self.context).data) return by_object diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 3562074f..60bdf836 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -37,6 +37,7 @@ from .permissions import ObjectTagObjectPermissions, TaxonomyObjectPermissions, TaxonomyTagsObjectPermissions from .serializers import ( ObjectTagListQueryParamsSerializer, + ObjectTagMinimalSerializer, ObjectTagsByTaxonomySerializer, ObjectTagSerializer, ObjectTagUpdateBodySerializer, @@ -444,6 +445,7 @@ class ObjectTagView( """ serializer_class = ObjectTagSerializer + minimal_serilizer_class = ObjectTagMinimalSerializer permission_classes = [ObjectTagObjectPermissions] lookup_field = "object_id" From 6b73ec4ff4c282a007eea3fe231b9db351faf9a7 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 9 Oct 2024 20:08:57 -0500 Subject: [PATCH 4/9] style: Update names of functions and variables --- openedx_tagging/core/tagging/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index b88da3d4..d7d7c969 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -502,7 +502,7 @@ def copy_tags( """ ObjectTagClass = object_tag_class - def object_has_tag(object_tag): + def dest_object_has_tag(object_tag): try: result = ObjectTagClass.objects.get( object_id=dest_object_id, @@ -513,7 +513,7 @@ def object_has_tag(object_tag): except ObjectTagClass.DoesNotExist: return None - object_tags = get_object_tags( + source_object_tags = get_object_tags( source_object_id, include_deleted=include_deleted, object_tag_class=object_tag_class, @@ -528,8 +528,8 @@ def object_has_tag(object_tag): copied_tags.delete() # Copy an create object_tags in destination - for object_tag in object_tags: - existing_object_tag = object_has_tag(object_tag) + for object_tag in source_object_tags: + existing_object_tag = dest_object_has_tag(object_tag) if existing_object_tag: # Now this tag is copied existing_object_tag.is_copied = True From 2bb68ee4876d2d9a650d7fa9365bca92cdeba306 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 11 Oct 2024 12:23:59 -0500 Subject: [PATCH 5/9] chore: Update version --- openedx_learning/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 7fde125a..07e30072 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1,4 +1,4 @@ """ Open edX Learning ("Learning Core"). """ -__version__ = "0.14.0" +__version__ = "0.15.0" From 744a539b9a1abfd79a698943e12468de029cbc9f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 14 Oct 2024 20:20:56 -0500 Subject: [PATCH 6/9] refactor: Updates on copy_tags and some nits --- openedx_tagging/core/tagging/api.py | 43 ++++------------- .../core/tagging/rest_api/v1/serializers.py | 2 +- .../core/tagging/rest_api/v1/views.py | 4 +- .../openedx_tagging/core/tagging/test_api.py | 47 ------------------- 4 files changed, 13 insertions(+), 83 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index d7d7c969..63ca064e 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -486,12 +486,7 @@ def delete_tags_from_taxonomy( taxonomy.delete_tags(tags, with_subtags) -def copy_tags( - source_object_id: str, - dest_object_id: str, - include_deleted: bool = True, - object_tag_class: type[ObjectTag] = ObjectTag -): +def copy_tags(source_object_id: str, dest_object_id: str): """ Copy all tags from one object to another. @@ -500,25 +495,10 @@ def copy_tags( If there are not-copied tags that also are in 'source_object_id', then they become copied. """ - ObjectTagClass = object_tag_class - - def dest_object_has_tag(object_tag): - try: - result = ObjectTagClass.objects.get( - object_id=dest_object_id, - _export_id=object_tag.export_id, - _value=object_tag.value, - ) - return result - except ObjectTagClass.DoesNotExist: - return None - source_object_tags = get_object_tags( source_object_id, - include_deleted=include_deleted, - object_tag_class=object_tag_class, ) - copied_tags = ObjectTagClass.objects.filter( + copied_tags = ObjectTag.objects.filter( object_id=dest_object_id, is_copied=True, ) @@ -529,15 +509,10 @@ def dest_object_has_tag(object_tag): # Copy an create object_tags in destination for object_tag in source_object_tags: - existing_object_tag = dest_object_has_tag(object_tag) - if existing_object_tag: - # Now this tag is copied - existing_object_tag.is_copied = True - existing_object_tag.save() - else: - new_object_tag = ObjectTagClass() - new_object_tag.copy(object_tag) - new_object_tag.id = None # To create a new instance in DB - new_object_tag.object_id = dest_object_id - new_object_tag.is_copied = True - new_object_tag.save() + ObjectTag.objects.update_or_create( + object_id=dest_object_id, + taxonomy_id=object_tag.taxonomy_id, + tag_id=object_tag.tag_id, + defaults={"is_copied": True}, + # Note: _value and _export_id should be set automatically + ) diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index eccf2b6a..183a0802 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -176,7 +176,7 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: """ Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy """ - ObjectTagViewMinimalSerializer = self.context["view"].minimal_serilizer_class + ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class can_tag_object_perm = f"{self.app_label}.can_tag_object" by_object: dict[str, dict[str, Any]] = {} for obj_tag in instance: diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 60bdf836..6786282b 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -444,8 +444,10 @@ class ObjectTagView( * 405 - Method not allowed """ + # Serializer used in `get_queryset` when getting tags per taxonomy serializer_class = ObjectTagSerializer - minimal_serilizer_class = ObjectTagMinimalSerializer + # Serializer used in the result in `to_representation` in `ObjectTagsByTaxonomySerializer` + minimal_serializer_class = ObjectTagMinimalSerializer permission_classes = [ObjectTagObjectPermissions] lookup_field = "object_id" diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 476065e1..ce32295f 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -945,53 +945,6 @@ def test_copy_tags(self) -> None: assert object_tag.object_id == obj2 assert object_tag.is_copied is True - def test_copy_tags_with_deleted(self) -> None: - obj1 = "object_id1" - obj2 = "object_id2" - - tags_list = [ - { - "value": "English", - "taxonomy": self.language_taxonomy, - "deleted": False, - }, - { - "value": "DPANN", - "taxonomy": self.taxonomy, - "deleted": True, - }, - ] - - for tag_object in tags_list: - tagging_api.tag_object(object_id=obj1, taxonomy=tag_object["taxonomy"], tags=[tag_object["value"]]) - - tagging_api.delete_tags_from_taxonomy(self.taxonomy, ["DPANN"], with_subtags=True) - - # Copy tags, also with deleted tags - tagging_api.copy_tags(obj1, obj2) - - object_tags = tagging_api.get_object_tags(obj2, include_deleted=True) - - assert len(object_tags) == 2 - for index, object_tag in enumerate(object_tags): - assert object_tag.value == tags_list[index]["value"] - assert object_tag.is_deleted == tags_list[index]["deleted"] - assert object_tag.taxonomy == tags_list[index]["taxonomy"] - assert object_tag.object_id == obj2 - assert object_tag.is_copied is True - - # Copy tags, without deleted tags - tagging_api.copy_tags(obj1, obj2, include_deleted=False) - - object_tags = tagging_api.get_object_tags(obj2, include_deleted=False) - - assert len(object_tags) == 1 - assert object_tags[0].value == tags_list[0]["value"] - assert not object_tags[0].is_deleted - assert object_tags[0].taxonomy == tags_list[0]["taxonomy"] - assert object_tags[0].object_id == obj2 - assert object_tags[0].is_copied is True - def test_copy_tags_with_non_copied(self) -> None: obj1 = "object_id1" obj2 = "object_id2" From adbf96749c6ea955dfe2c8ee232189e904fe8977 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 15 Oct 2024 10:40:34 -0500 Subject: [PATCH 7/9] chore: Bump version to 0.16.0 --- openedx_learning/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 07e30072..3aa9b8ce 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1,4 +1,4 @@ """ Open edX Learning ("Learning Core"). """ -__version__ = "0.15.0" +__version__ = "0.16.0" From 639501987f5618a574eeceff6327986869eee2b1 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 15 Oct 2024 19:44:35 -0500 Subject: [PATCH 8/9] style: Update comment --- openedx_tagging/core/tagging/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 63ca064e..c72ce6da 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -514,5 +514,5 @@ def copy_tags(source_object_id: str, dest_object_id: str): taxonomy_id=object_tag.taxonomy_id, tag_id=object_tag.tag_id, defaults={"is_copied": True}, - # Note: _value and _export_id should be set automatically + # Note: _value and _export_id are set automatically ) From 7289b2231ae0f7c0721327552d59501dbd8b2461 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 15 Oct 2024 19:53:27 -0500 Subject: [PATCH 9/9] style: Add comment --- openedx_tagging/core/tagging/rest_api/v1/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index 183a0802..9214453b 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -176,7 +176,9 @@ def to_representation(self, instance: list[ObjectTag]) -> dict: """ Convert this list of ObjectTags to the serialized dictionary, grouped by Taxonomy """ + # Allows consumers like edx-platform to override this ObjectTagViewMinimalSerializer = self.context["view"].minimal_serializer_class + can_tag_object_perm = f"{self.app_label}.can_tag_object" by_object: dict[str, dict[str, Any]] = {} for obj_tag in instance: