diff --git a/.gitignore b/.gitignore index ea7591535..828320592 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude *.py[cod] __pycache__ .pytest_cache diff --git a/src/openedx_content/applets/backup_restore/serializers.py b/src/openedx_content/applets/backup_restore/serializers.py index d8f7a5c15..549f3280f 100644 --- a/src/openedx_content/applets/backup_restore/serializers.py +++ b/src/openedx_content/applets/backup_restore/serializers.py @@ -156,7 +156,9 @@ class CollectionSerializer(serializers.Serializer): # pylint: disable=abstract- Serializer for collections. """ title = serializers.CharField(required=True) - key = serializers.CharField(required=True) + # The model field is now Collection.collection_code, but the archive format + # still uses "key". A future v2 format may align the name. + key = serializers.CharField(required=True, source="collection_code") description = serializers.CharField(required=True, allow_blank=True) entities = serializers.ListField( child=serializers.CharField(), diff --git a/src/openedx_content/applets/backup_restore/toml.py b/src/openedx_content/applets/backup_restore/toml.py index d39861803..520d6b877 100644 --- a/src/openedx_content/applets/backup_restore/toml.py +++ b/src/openedx_content/applets/backup_restore/toml.py @@ -220,7 +220,9 @@ def toml_collection(collection: Collection, entity_keys: list[str]) -> str: collection_table = tomlkit.table() collection_table.add("title", collection.title) - collection_table.add("key", collection.key) + # Note: the model field is now Collection.collection_code, but the archive + # format still uses "key". A future v2 format may align the name. + collection_table.add("key", collection.collection_code) collection_table.add("description", collection.description) collection_table.add("created", collection.created) collection_table.add("entities", entities_array) diff --git a/src/openedx_content/applets/backup_restore/zipper.py b/src/openedx_content/applets/backup_restore/zipper.py index 5f7b2457e..97629c596 100644 --- a/src/openedx_content/applets/backup_restore/zipper.py +++ b/src/openedx_content/applets/backup_restore/zipper.py @@ -401,7 +401,7 @@ def create_zip(self, path: str) -> None: collections = self.get_collections() for collection in collections: - collection_hash_slug = self.get_entity_toml_filename(collection.key) + collection_hash_slug = self.get_entity_toml_filename(collection.collection_code) collection_toml_file_path = collections_folder / f"{collection_hash_slug}.toml" entity_keys_related = collection.entities.order_by("key").values_list("key", flat=True) self.add_file_to_zip( @@ -779,7 +779,7 @@ def _save_collections(self, learning_package, collections): ) collection = collections_api.add_to_collection( learning_package_id=learning_package.id, - key=collection.key, + collection_code=collection.collection_code, entities_qset=publishing_api.get_publishable_entities(learning_package.id).filter(key__in=entities) ) diff --git a/src/openedx_content/applets/collections/admin.py b/src/openedx_content/applets/collections/admin.py index eb0685a27..41db9e082 100644 --- a/src/openedx_content/applets/collections/admin.py +++ b/src/openedx_content/applets/collections/admin.py @@ -13,14 +13,14 @@ class CollectionAdmin(admin.ModelAdmin): Allows users to easily disable/enable (aka soft delete and restore) or bulk delete Collections. """ - readonly_fields = ["key", "learning_package"] + readonly_fields = ["collection_code", "learning_package"] list_filter = ["enabled"] - list_display = ["key", "title", "enabled", "modified"] + list_display = ["collection_code", "title", "enabled", "modified"] fieldsets = [ ( "", { - "fields": ["key", "learning_package"], + "fields": ["collection_code", "learning_package"], } ), ( diff --git a/src/openedx_content/applets/collections/api.py b/src/openedx_content/applets/collections/api.py index b57b3bea8..ef9dae340 100644 --- a/src/openedx_content/applets/collections/api.py +++ b/src/openedx_content/applets/collections/api.py @@ -34,7 +34,7 @@ def create_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, *, title: str, created_by: int | None, @@ -44,35 +44,37 @@ def create_collection( """ Create a new Collection """ - collection = Collection.objects.create( + collection = Collection( learning_package_id=learning_package_id, - key=key, + collection_code=collection_code, title=title, created_by_id=created_by, description=description, enabled=enabled, ) + collection.full_clean() + collection.save() return collection -def get_collection(learning_package_id: LearningPackage.ID, collection_key: str) -> Collection: +def get_collection(learning_package_id: LearningPackage.ID, collection_code: str) -> Collection: """ Get a Collection by ID """ - return Collection.objects.get_by_key(learning_package_id, collection_key) + return Collection.objects.get_by_code(learning_package_id, collection_code) def update_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, *, title: str | None = None, description: str | None = None, ) -> Collection: """ - Update a Collection identified by the learning_package_id + key. + Update a Collection identified by the learning_package_id + collection_code. """ - collection = get_collection(learning_package_id, key) + collection = get_collection(learning_package_id, collection_code) # If no changes were requested, there's nothing to update, so just return # the Collection as-is @@ -90,17 +92,17 @@ def update_collection( def delete_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, *, hard_delete=False, ) -> Collection: """ - Disables or deletes a collection identified by the given learning_package + key. + Disables or deletes a collection identified by the given learning_package + collection_code. By default (hard_delete=False), the collection is "soft deleted", i.e disabled. Soft-deleted collections can be re-enabled using restore_collection. """ - collection = get_collection(learning_package_id, key) + collection = get_collection(learning_package_id, collection_code) if hard_delete: collection.delete() @@ -112,12 +114,12 @@ def delete_collection( def restore_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, ) -> Collection: """ Undo a "soft delete" by re-enabling a Collection. """ - collection = get_collection(learning_package_id, key) + collection = get_collection(learning_package_id, collection_code) collection.enabled = True collection.save() @@ -126,7 +128,7 @@ def restore_collection( def add_to_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, entities_qset: QuerySet[PublishableEntity], created_by: int | None = None, ) -> Collection: @@ -146,10 +148,10 @@ def add_to_collection( if invalid_entity: raise ValidationError( f"Cannot add entity {invalid_entity.id} in learning package {invalid_entity.learning_package_id} " - f"to collection {key} in learning package {learning_package_id}." + f"to collection {collection_code} in learning package {learning_package_id}." ) - collection = get_collection(learning_package_id, key) + collection = get_collection(learning_package_id, collection_code) collection.entities.add( *entities_qset.all(), through_defaults={"created_by_id": created_by}, @@ -162,7 +164,7 @@ def add_to_collection( def remove_from_collection( learning_package_id: LearningPackage.ID, - key: str, + collection_code: str, entities_qset: QuerySet[PublishableEntity], ) -> Collection: """ @@ -174,7 +176,7 @@ def remove_from_collection( Returns the updated Collection. """ - collection = get_collection(learning_package_id, key) + collection = get_collection(learning_package_id, collection_code) collection.entities.remove(*entities_qset.all()) collection.modified = datetime.now(tz=timezone.utc) @@ -198,7 +200,7 @@ def get_entity_collections(learning_package_id: LearningPackage.ID, entity_key: def get_collection_entities( learning_package_id: LearningPackage.ID, - collection_key: str, + collection_code: str, ) -> QuerySet[PublishableEntity]: """ Returns a QuerySet of PublishableEntities in a Collection. @@ -207,7 +209,7 @@ def get_collection_entities( """ return PublishableEntity.objects.filter( learning_package_id=learning_package_id, - collections__key=collection_key, + collections__collection_code=collection_code, ).order_by("pk") diff --git a/src/openedx_content/applets/collections/models.py b/src/openedx_content/applets/collections/models.py index 2f315b247..a1c0fa4a4 100644 --- a/src/openedx_content/applets/collections/models.py +++ b/src/openedx_content/applets/collections/models.py @@ -70,7 +70,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from openedx_django_lib.fields import MultiCollationTextField, case_insensitive_char_field, key_field +from openedx_django_lib.fields import MultiCollationTextField, case_insensitive_char_field, code_field, code_field_check from openedx_django_lib.validators import validate_utc_datetime from ..publishing.models import LearningPackage, PublishableEntity @@ -85,12 +85,12 @@ class CollectionManager(models.Manager): """ Custom manager for Collection class. """ - def get_by_key(self, learning_package_id: int, key: str): + def get_by_code(self, learning_package_id: int, collection_code: str): """ - Get the Collection for the given Learning Package + key. + Get the Collection for the given Learning Package + collection code. """ return self.select_related('learning_package') \ - .get(learning_package_id=learning_package_id, key=key) + .get(learning_package_id=learning_package_id, collection_code=collection_code) class Collection(models.Model): @@ -105,10 +105,11 @@ class Collection(models.Model): learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE) # Every collection is uniquely and permanently identified within its learning package - # by a 'key' that is set during creation. Both will appear in the + # by a 'code' that is set during creation. Both will appear in the # collection's opaque key: - # e.g. "lib-collection:lib:key" is the opaque key for a library collection. - key = key_field(db_column='_key') + # e.g. "lib-collection:{org_code}:{library_code}:{collection_code}" + # is the opaque key for a library collection. + collection_code = code_field() title = case_insensitive_char_field( null=False, @@ -170,14 +171,15 @@ class Collection(models.Model): class Meta: verbose_name_plural = "Collections" constraints = [ - # Keys are unique within a given LearningPackage. + # Collection codes are unique within a given LearningPackage. models.UniqueConstraint( fields=[ "learning_package", - "key", + "collection_code", ], name="oel_coll_uniq_lp_key", ), + code_field_check("collection_code", name="oel_coll_collection_code_regex"), ] indexes = [ models.Index( @@ -196,7 +198,7 @@ def __str__(self) -> str: """ User-facing string representation of a Collection. """ - return f"<{self.__class__.__name__}> (lp:{self.learning_package_id} {self.key}:{self.title})" + return f"<{self.__class__.__name__}> (lp:{self.learning_package_id} {self.collection_code}:{self.title})" class CollectionPublishableEntity(models.Model): diff --git a/src/openedx_content/applets/components/api.py b/src/openedx_content/applets/components/api.py index 6127cd3ec..1c3ed3e40 100644 --- a/src/openedx_content/applets/components/api.py +++ b/src/openedx_content/applets/components/api.py @@ -436,7 +436,7 @@ def get_components( # pylint: disable=too-many-positional-arguments def get_collection_components( learning_package_id: LearningPackage.ID, - collection_key: str, + collection_code: str, ) -> QuerySet[Component]: """ Returns a QuerySet of Components relating to the PublishableEntities in a Collection. @@ -445,7 +445,7 @@ def get_collection_components( """ return Component.objects.filter( learning_package_id=learning_package_id, - publishable_entity__collections__key=collection_key, + publishable_entity__collections__collection_code=collection_code, ).order_by('pk') diff --git a/src/openedx_content/applets/units/models.py b/src/openedx_content/applets/units/models.py index 913ea4890..109fb38b9 100644 --- a/src/openedx_content/applets/units/models.py +++ b/src/openedx_content/applets/units/models.py @@ -1,6 +1,7 @@ """ Models that implement units """ +from __future__ import annotations from typing import NewType, cast, override diff --git a/src/openedx_content/migrations/0008_rename_collection_key_to_collection_code.py b/src/openedx_content/migrations/0008_rename_collection_key_to_collection_code.py new file mode 100644 index 000000000..9bdab4915 --- /dev/null +++ b/src/openedx_content/migrations/0008_rename_collection_key_to_collection_code.py @@ -0,0 +1,73 @@ +""" +Rename Collection.key -> Collection.collection_code and change from key_field to code_field. +""" +import re + +import django.core.validators +import django.db.models.lookups +from django.conf import settings +from django.db import migrations, models + +import openedx_django_lib.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('openedx_content', '0007_publishlogrecord_direct'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + # Drop old constraint (references the old field name). + migrations.RemoveConstraint( + model_name='collection', + name='oel_coll_uniq_lp_key', + ), + # Rename the column. + migrations.RenameField( + model_name='collection', + old_name='key', + new_name='collection_code', + ), + # Change from key_field (max_length=500, no validator) to code_field + # (max_length=255, with regex validator). + migrations.AlterField( + model_name='collection', + name='collection_code', + field=openedx_django_lib.fields.MultiCollationCharField( + db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, + max_length=255, + validators=[ + django.core.validators.RegexValidator( + re.compile('^[a-zA-Z0-9_.-]+\\Z'), + 'Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.', + 'invalid', + ), + ], + ), + ), + # Re-add uniqueness constraint with the new field name. + migrations.AddConstraint( + model_name='collection', + constraint=models.UniqueConstraint( + fields=('learning_package', 'collection_code'), + name='oel_coll_uniq_lp_key', + ), + ), + # DB-level regex check constraint. + migrations.AddConstraint( + model_name='collection', + constraint=models.CheckConstraint( + condition=django.db.models.lookups.Regex( + models.F('collection_code'), + '^[a-zA-Z0-9_.-]+\\Z', + ), + name='oel_coll_collection_code_regex', + violation_error_message=( + 'Enter a valid "code name" consisting of letters, numbers,' + ' underscores, hyphens, or periods.' + ), + ), + ), + ] diff --git a/src/openedx_django_lib/fields.py b/src/openedx_django_lib/fields.py index 44bcb2a23..3374ac12b 100644 --- a/src/openedx_django_lib/fields.py +++ b/src/openedx_django_lib/fields.py @@ -12,10 +12,14 @@ from __future__ import annotations import hashlib +import re import uuid from typing import Any +from django.core.validators import RegexValidator from django.db import models +from django.db.models.lookups import Regex +from django.utils.translation import gettext_lazy as _ from .collations import MultiCollationMixin # Re-export these fields which are in a separate file so we can use .pyi type stubs: @@ -114,6 +118,60 @@ def immutable_uuid_field() -> models.UUIDField: ) +# Alphanumeric, hyphens, underscores, periods +CODE_REGEX = re.compile(r"^[a-zA-Z0-9_.-]+\Z") + + +_CODE_VIOLATION_MSG = _( + 'Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.' +) + + +def code_field(**kwargs) -> MultiCollationCharField: + """ + Field to hold a 'code', i.e. a slug-like local identifier. + + Use together with :func:`code_field_check` to enforce the same regex at + the database level via a ``CheckConstraint``. + """ + return case_sensitive_char_field( + max_length=255, + blank=False, + validators=[ + RegexValidator( + CODE_REGEX, + # Translators: "letters" means latin letters: a-z and A-Z. + _CODE_VIOLATION_MSG, + "invalid", + ), + ], + **kwargs, + ) + + +def code_field_check(field_name: str, *, name: str) -> models.CheckConstraint: + """ + Return a ``CheckConstraint`` that enforces :data:`CODE_REGEX` at the DB level. + + Django validators (used by :func:`code_field`) are not called on ``.save()`` + or ``.update()``. Adding this constraint ensures the regex is also enforced + by the database itself, and Django will additionally run it as a Python-level + validator automatically. + + Usage:: + + class Meta: + constraints = [ + code_field_check("my_code_field", name="myapp_mymodel_my_code_field_regex"), + ] + """ + return models.CheckConstraint( + condition=Regex(models.F(field_name), CODE_REGEX.pattern), + name=name, + violation_error_message=_CODE_VIOLATION_MSG, + ) + + def key_field(**kwargs) -> MultiCollationCharField: """ Externally created Identifier fields. diff --git a/src/openedx_tagging/models/base.py b/src/openedx_tagging/models/base.py index 000d4d338..7957a45df 100644 --- a/src/openedx_tagging/models/base.py +++ b/src/openedx_tagging/models/base.py @@ -421,7 +421,7 @@ def copy(self, taxonomy: Taxonomy) -> Taxonomy: return self - def get_filtered_tags( # pylint: disable=too-many-positional-arguments + def get_filtered_tags( self, depth: int | None = None, parent_tag_value: str | None = None, diff --git a/tests/openedx_content/applets/backup_restore/test_backup.py b/tests/openedx_content/applets/backup_restore/test_backup.py index efff635de..edafe3f66 100644 --- a/tests/openedx_content/applets/backup_restore/test_backup.py +++ b/tests/openedx_content/applets/backup_restore/test_backup.py @@ -146,7 +146,7 @@ def setUpTestData(cls): cls.collection = api.create_collection( cls.learning_package.id, - key="COL1", + collection_code="COL1", created_by=cls.user.id, title="Collection 1", description="Description of Collection 1", @@ -154,7 +154,7 @@ def setUpTestData(cls): api.add_to_collection( cls.learning_package.id, - cls.collection.key, + cls.collection.collection_code, components ) diff --git a/tests/openedx_content/applets/backup_restore/test_restore.py b/tests/openedx_content/applets/backup_restore/test_restore.py index 0116731c4..a905427e8 100644 --- a/tests/openedx_content/applets/backup_restore/test_restore.py +++ b/tests/openedx_content/applets/backup_restore/test_restore.py @@ -183,7 +183,7 @@ def verify_collections(self, lp): assert collections.count() == 1 collection = collections.first() assert collection.title == "Collection test1" - assert collection.key == "collection-test" + assert collection.collection_code == "collection-test" assert collection.description == "" assert collection.created_by is not None assert collection.created_by.username == "lp_user" diff --git a/tests/openedx_content/applets/collections/test_api.py b/tests/openedx_content/applets/collections/test_api.py index f53d9fbb7..42e4f1638 100644 --- a/tests/openedx_content/applets/collections/test_api.py +++ b/tests/openedx_content/applets/collections/test_api.py @@ -64,35 +64,35 @@ def setUpTestData(cls) -> None: super().setUpTestData() cls.collection1 = api.create_collection( cls.learning_package.id, - key="COL1", + collection_code="COL1", created_by=None, title="Collection 1", description="Description of Collection 1", ) cls.collection2 = api.create_collection( cls.learning_package.id, - key="COL2", + collection_code="COL2", created_by=None, title="Collection 2", description="Description of Collection 2", ) cls.collection3 = api.create_collection( cls.learning_package.id, - key="COL3", + collection_code="COL3", created_by=None, title="Collection 3", description="Description of Collection 3", ) cls.another_library_collection = api.create_collection( cls.learning_package_2.id, - key="another_library", + collection_code="another_library", created_by=None, title="Collection 4", description="Description of Collection 4", ) cls.disabled_collection = api.create_collection( cls.learning_package.id, - key="disabled_collection", + collection_code="disabled_collection", created_by=None, title="Disabled Collection", description="Description of Disabled Collection", @@ -123,7 +123,7 @@ def test_get_collection_wrong_learning_package(self): Test getting a collection that doesn't exist in the requested learning package. """ with self.assertRaises(ObjectDoesNotExist): - api.get_collection(self.learning_package.id, self.another_library_collection.key) + api.get_collection(self.learning_package.id, self.another_library_collection.collection_code) def test_get_collections(self): """ @@ -191,14 +191,14 @@ def test_create_collection(self): with freeze_time(created_time): collection = api.create_collection( self.learning_package.id, - key='MYCOL', + collection_code='MYCOL', title="My Collection", created_by=user.id, description="This is my collection", ) assert collection.title == "My Collection" - assert collection.key == "MYCOL" + assert collection.collection_code == "MYCOL" assert collection.description == "This is my collection" assert collection.enabled assert collection.created == created_time @@ -211,15 +211,35 @@ def test_create_collection_without_description(self): """ collection = api.create_collection( self.learning_package.id, - key='MYCOL', + collection_code='MYCOL', created_by=None, title="My Collection", ) assert collection.title == "My Collection" - assert collection.key == "MYCOL" + assert collection.collection_code == "MYCOL" assert collection.description == "" assert collection.enabled + def test_create_collection_invalid_code(self): + """ + collection_code must only contain alphanumerics, hyphens, underscores, and periods. + """ + invalid_codes = [ + "has space", + "has@symbol", + "has/slash", + "has#hash", + ] + for code in invalid_codes: + with self.subTest(code=code): + with self.assertRaises(ValidationError): + api.create_collection( + self.learning_package.id, + collection_code=code, + title="Test", + created_by=None, + ) + class CollectionEntitiesTestCase(CollectionsTestCase): """ @@ -283,16 +303,16 @@ def setUpTestData(cls) -> None: # Add some shared components to the collections cls.collection1 = api.add_to_collection( cls.learning_package.id, - key=cls.collection1.key, - entities_qset=PublishableEntity.objects.filter(pk__in=[ + collection_code=cls.collection1.collection_code, + entities_qset=PublishableEntity.objects.filter(id__in=[ cls.published_component.id, ]), created_by=cls.user.id, ) cls.collection2 = api.add_to_collection( cls.learning_package.id, - key=cls.collection2.key, - entities_qset=PublishableEntity.objects.filter(pk__in=[ + collection_code=cls.collection2.collection_code, + entities_qset=PublishableEntity.objects.filter(id__in=[ cls.published_component.id, cls.draft_component.id, cls.draft_unit.id, @@ -300,8 +320,8 @@ def setUpTestData(cls) -> None: ) cls.disabled_collection = api.add_to_collection( cls.learning_package.id, - key=cls.disabled_collection.key, - entities_qset=PublishableEntity.objects.filter(pk__in=[ + collection_code=cls.disabled_collection.collection_code, + entities_qset=PublishableEntity.objects.filter(id__in=[ cls.published_component.id, ]), ) @@ -335,8 +355,8 @@ def test_add_to_collection(self): with freeze_time(modified_time): self.collection1 = api.add_to_collection( self.learning_package.id, - self.collection1.key, - PublishableEntity.objects.filter(pk__in=[ + self.collection1.collection_code, + PublishableEntity.objects.filter(id__in=[ self.draft_component.id, self.draft_unit.id, ]), @@ -360,8 +380,8 @@ def test_add_to_collection_again(self): with freeze_time(modified_time): self.collection2 = api.add_to_collection( self.learning_package.id, - self.collection2.key, - PublishableEntity.objects.filter(pk__in=[ + self.collection2.collection_code, + PublishableEntity.objects.filter(id__in=[ self.published_component.id, ]), ) @@ -380,8 +400,8 @@ def test_add_to_collection_wrong_learning_package(self): with self.assertRaises(ValidationError): api.add_to_collection( self.learning_package_2.id, - self.another_library_collection.key, - PublishableEntity.objects.filter(pk__in=[ + self.another_library_collection.collection_code, + PublishableEntity.objects.filter(id__in=[ self.published_component.id, ]), ) @@ -396,8 +416,8 @@ def test_remove_from_collection(self): with freeze_time(modified_time): self.collection2 = api.remove_from_collection( self.learning_package.id, - self.collection2.key, - PublishableEntity.objects.filter(pk__in=[ + self.collection2.collection_code, + PublishableEntity.objects.filter(id__in=[ self.published_component.id, self.draft_unit.id, ]), @@ -424,46 +444,46 @@ def test_get_entity_collections(self): def test_get_collection_components(self): assert list(api.get_collection_components( self.learning_package.id, - self.collection1.key, + self.collection1.collection_code, )) == [self.published_component] assert list(api.get_collection_components( self.learning_package.id, - self.collection2.key, + self.collection2.collection_code, )) == [self.published_component, self.draft_component] assert not list(api.get_collection_components( self.learning_package.id, - self.collection3.key, + self.collection3.collection_code, )) assert not list(api.get_collection_components( self.learning_package.id, - self.another_library_collection.key, + self.another_library_collection.collection_code, )) def test_get_collection_containers(self): """ Test using `get_collection_entities()` to get containers """ - def get_collection_containers(learning_package_id: LearningPackage.ID, collection_key: str): + def get_collection_containers(learning_package_id: LearningPackage.ID, collection_code: str): return ( pe.container for pe in - api.get_collection_entities(learning_package_id, collection_key).exclude(container=None) + api.get_collection_entities(learning_package_id, collection_code).exclude(container=None) ) assert not list(get_collection_containers( self.learning_package.id, - self.collection1.key, + self.collection1.collection_code, )) assert list(get_collection_containers( self.learning_package.id, - self.collection2.key, + self.collection2.collection_code, )) == [self.draft_unit.container] assert not list(get_collection_containers( self.learning_package.id, - self.collection3.key, + self.collection3.collection_code, )) assert not list(get_collection_containers( self.learning_package.id, - self.another_library_collection.key, + self.another_library_collection.collection_code, )) @@ -481,7 +501,7 @@ def setUpTestData(cls) -> None: super().setUpTestData() cls.collection = api.create_collection( cls.learning_package.id, - key="MYCOL", + collection_code="MYCOL", title="Collection", created_by=None, description="Description of Collection", @@ -495,7 +515,7 @@ def test_update_collection(self): with freeze_time(modified_time): collection = api.update_collection( self.learning_package.id, - key=self.collection.key, + collection_code=self.collection.collection_code, title="New Title", description="", ) @@ -511,17 +531,19 @@ def test_update_collection_partial(self): """ collection = api.update_collection( self.learning_package.id, - key=self.collection.key, + collection_code=self.collection.collection_code, title="New Title", ) assert collection.title == "New Title" assert collection.description == self.collection.description # unchanged - assert f"{collection}" == f" (lp:{self.learning_package.id} {self.collection.key}:New Title)" + assert f"{collection}" == ( + f" (lp:{self.learning_package.id} {self.collection.collection_code}:New Title)" + ) collection = api.update_collection( self.learning_package.id, - key=self.collection.key, + collection_code=self.collection.collection_code, description="New description", ) @@ -536,7 +558,7 @@ def test_update_collection_empty(self): with freeze_time(modified_time): collection = api.update_collection( self.learning_package.id, - key=self.collection.key, + collection_code=self.collection.collection_code, ) assert collection.title == self.collection.title # unchanged @@ -550,7 +572,7 @@ def test_update_collection_not_found(self): with self.assertRaises(ObjectDoesNotExist): api.update_collection( self.learning_package.id, - key="12345", + collection_code="12345", title="New Title", ) @@ -568,13 +590,13 @@ def test_soft_delete(self): with freeze_time(modified_time): collection = api.delete_collection( self.learning_package.id, - key=self.collection2.key, + collection_code=self.collection2.collection_code, ) # Collection was disabled and still exists in the database assert not collection.enabled assert collection.modified == modified_time - assert collection == api.get_collection(self.learning_package.id, collection.key) + assert collection == api.get_collection(self.learning_package.id, collection.collection_code) # ...and the collection's entities remain intact. assert list(collection.entities.all()) == [ self.draft_unit.publishable_entity, @@ -590,7 +612,7 @@ def test_delete(self): with freeze_time(modified_time): collection = api.delete_collection( self.learning_package.id, - key=self.collection2.key, + collection_code=self.collection2.collection_code, hard_delete=True, ) @@ -598,7 +620,7 @@ def test_delete(self): assert collection.enabled assert not collection.id with self.assertRaises(ObjectDoesNotExist): - api.get_collection(self.learning_package.id, collection.key) + api.get_collection(self.learning_package.id, collection.collection_code) # ...and the entities have been removed from this collection assert list(api.get_entity_collections( self.learning_package.id, @@ -615,20 +637,20 @@ def test_restore(self): """ collection = api.delete_collection( self.learning_package.id, - key=self.collection2.key, + collection_code=self.collection2.collection_code, ) modified_time = datetime(2024, 8, 8, tzinfo=timezone.utc) with freeze_time(modified_time): collection = api.restore_collection( self.learning_package.id, - key=self.collection2.key, + collection_code=self.collection2.collection_code, ) # Collection was enabled and still exists in the database assert collection.enabled assert collection.modified == modified_time - assert collection == api.get_collection(self.learning_package.id, collection.key) + assert collection == api.get_collection(self.learning_package.id, collection.collection_code) # ...and the collection's entities remain intact. assert list(collection.entities.all()) == [ self.draft_unit.publishable_entity, @@ -719,7 +741,7 @@ def test_set_collection_wrong_learning_package(self): ) collection = api.create_collection( learning_package_3.id, - key="MYCOL", + collection_code="MYCOL", title="My Collection", created_by=None, description="Description of Collection", diff --git a/tests/openedx_content/applets/components/test_api.py b/tests/openedx_content/applets/components/test_api.py index cbaa889b2..74f482090 100644 --- a/tests/openedx_content/applets/components/test_api.py +++ b/tests/openedx_content/applets/components/test_api.py @@ -666,21 +666,21 @@ def setUpTestData(cls) -> None: ) cls.collection1 = collection_api.create_collection( cls.learning_package.id, - key="MYCOL1", + collection_code="MYCOL1", title="Collection1", created_by=None, description="Description of Collection 1", ) cls.collection2 = collection_api.create_collection( cls.learning_package.id, - key="MYCOL2", + collection_code="MYCOL2", title="Collection2", created_by=None, description="Description of Collection 2", ) cls.collection3 = collection_api.create_collection( cls.learning_package.id, - key="MYCOL3", + collection_code="MYCOL3", title="Collection3", created_by=None, description="Description of Collection 3",