From b68d8ea1a69cec6d0a09a998afb4114c8baf67a8 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 18 Mar 2025 17:22:09 +0530 Subject: [PATCH 1/3] feat: add can_stand_alone flag to publishable entity It allows us to track components that were created independently and components that were created under a container like unit or subsection. --- .../apps/authoring/components/api.py | 17 +++++++++++++++-- .../apps/authoring/publishing/admin.py | 3 +++ .../apps/authoring/publishing/api.py | 11 ++++++++++- .../0004_publishableentity_can_stand_alone.py | 18 ++++++++++++++++++ .../publishing/models/publishable_entity.py | 1 + openedx_learning/apps/authoring/units/api.py | 17 +++++++++++++++-- .../apps/authoring/components/test_api.py | 11 +++++++++++ .../apps/authoring/units/test_api.py | 1 + 8 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py diff --git a/openedx_learning/apps/authoring/components/api.py b/openedx_learning/apps/authoring/components/api.py index 1c6d1693e..1316a1574 100644 --- a/openedx_learning/apps/authoring/components/api.py +++ b/openedx_learning/apps/authoring/components/api.py @@ -83,6 +83,8 @@ def create_component( local_key: str, created: datetime, created_by: int | None, + *, + can_stand_alone: bool = True, ) -> Component: """ Create a new Component (an entity like a Problem or Video) @@ -90,7 +92,11 @@ def create_component( key = f"{component_type.namespace}:{component_type.name}:{local_key}" with atomic(): publishable_entity = publishing_api.create_publishable_entity( - learning_package_id, key, created, created_by + learning_package_id, + key, + created, + created_by, + can_stand_alone=can_stand_alone ) component = Component.objects.create( publishable_entity=publishable_entity, @@ -239,13 +245,20 @@ def create_component_and_version( # pylint: disable=too-many-positional-argumen title: str, created: datetime, created_by: int | None = None, + *, + can_stand_alone: bool = True, ) -> tuple[Component, ComponentVersion]: """ Create a Component and associated ComponentVersion atomically """ with atomic(): component = create_component( - learning_package_id, component_type, local_key, created, created_by + learning_package_id, + component_type, + local_key, + created, + created_by, + can_stand_alone=can_stand_alone, ) component_version = create_component_version( component.pk, diff --git a/openedx_learning/apps/authoring/publishing/admin.py b/openedx_learning/apps/authoring/publishing/admin.py index a2b5dbde1..c36262b3e 100644 --- a/openedx_learning/apps/authoring/publishing/admin.py +++ b/openedx_learning/apps/authoring/publishing/admin.py @@ -85,6 +85,7 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin): "learning_package", "created", "created_by", + "can_stand_alone", ] list_filter = ["learning_package"] search_fields = ["key", "uuid"] @@ -98,6 +99,7 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin): "created", "created_by", "see_also", + "can_stand_alone", ] readonly_fields = [ "key", @@ -108,6 +110,7 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin): "created", "created_by", "see_also", + "can_stand_alone", ] def get_queryset(self, request): diff --git a/openedx_learning/apps/authoring/publishing/api.py b/openedx_learning/apps/authoring/publishing/api.py index 231e42c4c..12db8fe0e 100644 --- a/openedx_learning/apps/authoring/publishing/api.py +++ b/openedx_learning/apps/authoring/publishing/api.py @@ -173,6 +173,8 @@ def create_publishable_entity( created: datetime, # User ID who created this created_by: int | None, + *, + can_stand_alone: bool = True, ) -> PublishableEntity: """ Create a PublishableEntity. @@ -185,6 +187,7 @@ def create_publishable_entity( key=key, created=created, created_by_id=created_by, + can_stand_alone=can_stand_alone, ) @@ -583,6 +586,8 @@ def create_container( key: str, created: datetime, created_by: int | None, + *, + can_stand_alone: bool = True, # The types on the following line are correct, but mypy will complain - https://github.com/python/mypy/issues/3737 container_cls: type[ContainerModel] = Container, # type: ignore[assignment] ) -> ContainerModel: @@ -603,7 +608,11 @@ def create_container( assert issubclass(container_cls, Container) with atomic(): publishable_entity = create_publishable_entity( - learning_package_id, key, created, created_by + learning_package_id, + key, + created, + created_by, + can_stand_alone=can_stand_alone, ) container = container_cls.objects.create( publishable_entity=publishable_entity, diff --git a/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py b/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py new file mode 100644 index 000000000..6d8384ef7 --- /dev/null +++ b/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-03-17 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_publishing', '0003_containers'), + ] + + operations = [ + migrations.AddField( + model_name='publishableentity', + name='can_stand_alone', + field=models.BooleanField(default=True), + ), + ] diff --git a/openedx_learning/apps/authoring/publishing/models/publishable_entity.py b/openedx_learning/apps/authoring/publishing/models/publishable_entity.py index e05172bf7..ac733deba 100644 --- a/openedx_learning/apps/authoring/publishing/models/publishable_entity.py +++ b/openedx_learning/apps/authoring/publishing/models/publishable_entity.py @@ -126,6 +126,7 @@ class PublishableEntity(models.Model): null=True, blank=True, ) + can_stand_alone = models.BooleanField(default=True) class Meta: constraints = [ diff --git a/openedx_learning/apps/authoring/units/api.py b/openedx_learning/apps/authoring/units/api.py index 93058dd38..354f2568a 100644 --- a/openedx_learning/apps/authoring/units/api.py +++ b/openedx_learning/apps/authoring/units/api.py @@ -30,7 +30,12 @@ def create_unit( - learning_package_id: int, key: str, created: datetime, created_by: int | None + learning_package_id: int, + key: str, + created: datetime, + created_by: int | None, + *, + can_stand_alone: bool = True, ) -> Unit: """ [ 🛑 UNSTABLE ] Create a new unit. @@ -46,6 +51,7 @@ def create_unit( key, created, created_by, + can_stand_alone=can_stand_alone, container_cls=Unit, ) @@ -156,6 +162,7 @@ def create_unit_and_version( components: list[Component | ComponentVersion] | None = None, created: datetime, created_by: int | None = None, + can_stand_alone: bool = True, ) -> tuple[Unit, UnitVersion]: """ [ 🛑 UNSTABLE ] Create a new unit and its version. @@ -168,7 +175,13 @@ def create_unit_and_version( """ publishable_entities_pks, entity_version_pks = _pub_entities_for_components(components) with atomic(): - unit = create_unit(learning_package_id, key, created, created_by) + unit = create_unit( + learning_package_id, + key, + created, + created_by, + can_stand_alone=can_stand_alone, + ) unit_version = create_unit_version( unit, 1, diff --git a/tests/openedx_learning/apps/authoring/components/test_api.py b/tests/openedx_learning/apps/authoring/components/test_api.py index aba186c38..ac3902457 100644 --- a/tests/openedx_learning/apps/authoring/components/test_api.py +++ b/tests/openedx_learning/apps/authoring/components/test_api.py @@ -322,6 +322,7 @@ def setUpTestData(cls) -> None: local_key='my_component', created=cls.now, created_by=None, + can_stand_alone=False, ) def test_simple_get(self): @@ -333,6 +334,16 @@ def test_publishing_entity_key_convention(self): """Our mapping convention is {namespace}:{component_type}:{local_key}""" assert self.problem.key == "xblock.v1:problem:my_component" + def test_stand_alone_flag(self): + """Check if can_stand_alone flag is set""" + component = components_api.get_component_by_key( + self.learning_package.id, + namespace='xblock.v1', + type_name='html', + local_key='my_component', + ) + assert not component.publishable_entity.can_stand_alone + def test_get_by_key(self): assert self.html == components_api.get_component_by_key( self.learning_package.id, diff --git a/tests/openedx_learning/apps/authoring/units/test_api.py b/tests/openedx_learning/apps/authoring/units/test_api.py index 1556a34b8..103de1632 100644 --- a/tests/openedx_learning/apps/authoring/units/test_api.py +++ b/tests/openedx_learning/apps/authoring/units/test_api.py @@ -222,6 +222,7 @@ def test_create_empty_unit_and_version(self): assert unit.versioning.has_unpublished_changes assert unit.versioning.draft == unit_version assert unit.versioning.published is None + assert unit.publishable_entity.can_stand_alone def test_create_next_unit_version_with_two_unpinned_components(self): """Test creating a unit version with two unpinned components. From 408bc85cfb858488b05ee1d92ea6a8255b7d876a Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 19 Mar 2025 10:49:25 +0530 Subject: [PATCH 2/3] docs: update function docs --- openedx_learning/apps/authoring/publishing/api.py | 1 + .../apps/authoring/publishing/models/publishable_entity.py | 6 +++++- openedx_learning/apps/authoring/units/api.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openedx_learning/apps/authoring/publishing/api.py b/openedx_learning/apps/authoring/publishing/api.py index 12db8fe0e..30f9ec5cb 100644 --- a/openedx_learning/apps/authoring/publishing/api.py +++ b/openedx_learning/apps/authoring/publishing/api.py @@ -600,6 +600,7 @@ def create_container( key: The key of the container. created: The date and time the container was created. created_by: The ID of the user who created the container + can_stand_alone: Set to False when created as part of containers container_cls: The subclass of Container to use, if applicable Returns: diff --git a/openedx_learning/apps/authoring/publishing/models/publishable_entity.py b/openedx_learning/apps/authoring/publishing/models/publishable_entity.py index ac733deba..9a53f22cb 100644 --- a/openedx_learning/apps/authoring/publishing/models/publishable_entity.py +++ b/openedx_learning/apps/authoring/publishing/models/publishable_entity.py @@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.validators import MinValueValidator from django.db import models +from django.utils.translation import gettext as _ from openedx_learning.lib.fields import ( case_insensitive_char_field, @@ -126,7 +127,10 @@ class PublishableEntity(models.Model): null=True, blank=True, ) - can_stand_alone = models.BooleanField(default=True) + can_stand_alone = models.BooleanField( + default=True, + help_text=_("Set to True when created independently, False when created as part of a container."), + ) class Meta: constraints = [ diff --git a/openedx_learning/apps/authoring/units/api.py b/openedx_learning/apps/authoring/units/api.py index 354f2568a..8c6a37d92 100644 --- a/openedx_learning/apps/authoring/units/api.py +++ b/openedx_learning/apps/authoring/units/api.py @@ -45,6 +45,7 @@ def create_unit( key: The key. created: The creation date. created_by: The user who created the unit. + can_stand_alone: Set to False when created as part of containers """ return publishing_api.create_container( learning_package_id, @@ -172,6 +173,7 @@ def create_unit_and_version( key: The key. created: The creation date. created_by: The user who created the unit. + can_stand_alone: Set to False when created as part of containers """ publishable_entities_pks, entity_version_pks = _pub_entities_for_components(components) with atomic(): From 962944beee12400a2d0be2cda8e9e08f6f68fe21 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Mar 2025 05:58:06 +1030 Subject: [PATCH 3/3] fix: adds help text to migration --- .../migrations/0004_publishableentity_can_stand_alone.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py b/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py index 6d8384ef7..9949d159b 100644 --- a/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py +++ b/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py @@ -13,6 +13,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='publishableentity', name='can_stand_alone', - field=models.BooleanField(default=True), + field=models.BooleanField( + default=True, + help_text="Set to True when created independently, False when created as part of a container.", + ), ), ]