-
Notifications
You must be signed in to change notification settings - Fork 23
feat!: Add Container.container_code field #545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ed3aadc
9c6a894
0aa26e2
4593e05
f42b544
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,8 +11,9 @@ | |
| from django.db import models | ||
| from typing_extensions import deprecated | ||
|
|
||
| from openedx_django_lib.fields import case_sensitive_char_field | ||
| from openedx_django_lib.fields import case_sensitive_char_field, code_field, code_field_check | ||
|
|
||
| from ..publishing.models.learning_package import LearningPackage | ||
| from ..publishing.models.publishable_entity import ( | ||
| PublishableEntity, | ||
| PublishableEntityMixin, | ||
|
|
@@ -171,6 +172,12 @@ class Container(PublishableEntityMixin): | |
| olx_tag_name: str = "" | ||
| _type_instance: ContainerType # Cache used by get_container_type() | ||
|
|
||
| # This foreign key is technically redundant because we're already locked to | ||
| # a single LearningPackage through our publishable_entity relation. However, | ||
| # having this foreign key directly allows us to make indexes that efficiently | ||
| # query by other Container fields within a given LearningPackage. | ||
| learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE) | ||
|
bradenmacdonald marked this conversation as resolved.
|
||
|
|
||
| # The type of the container. Cannot be changed once the container is created. | ||
| container_type = models.ForeignKey( | ||
| ContainerType, | ||
|
|
@@ -179,6 +186,11 @@ class Container(PublishableEntityMixin): | |
| editable=False, | ||
| ) | ||
|
|
||
| # container_code is an identifier that is local to the learning_package. | ||
| # Unlike component_code, it is unique across all container types within | ||
| # the same LearningPackage. | ||
|
Comment on lines
+190
to
+191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will requiring this code to be unique across all containers in a [course] bite us later when we try to migrate old courses to Learning Core, since they may have e.g. a section and unit with the same
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hard to say. On one hand, we'll likely have multiple course runs per learning package. So, we'll need to handle collisions between container block url_names anyway. I'm imagining that there'll have to be a table which maps CourseRunUsages to Entities, rather than a simple mapping function between On the other hand, it's weird to me that container_code and component_code have different uniqueness constraints. I have a feeling that the inconsistency will make consuming code more complicated rather than having any benefit to us. We're riding the line now, but maybe we can sneak a fix to this in before the cutoff.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I'd prefer to make container_code work like component_code if we can. |
||
| container_code = code_field() | ||
|
|
||
| @property | ||
| def id(self) -> ID: | ||
| return cast(Container.ID, self.publishable_entity_id) | ||
|
|
@@ -194,6 +206,15 @@ def pk(self): | |
| # override this with a deprecated marker, so it shows a warning in developer's IDEs like VS Code. | ||
| return self.id | ||
|
|
||
| class Meta: | ||
| constraints = [ | ||
| models.UniqueConstraint( | ||
| fields=["learning_package", "container_code"], | ||
| name="oel_container_uniq_lp_cc", | ||
| ), | ||
|
kdmccormick marked this conversation as resolved.
|
||
| code_field_check("container_code", name="oel_container_code_regex"), | ||
| ] | ||
|
|
||
| @classmethod | ||
| def validate_entity(cls, entity: PublishableEntity) -> None: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import re | ||
|
|
||
| import django.core.validators | ||
| import django.db.models.deletion | ||
| from django.db import migrations, models | ||
|
|
||
| import openedx_django_lib.fields | ||
|
|
||
|
|
||
| def backfill_container_code(apps, schema_editor): | ||
| """ | ||
| Backfill container_code and learning_package from publishable_entity. | ||
|
|
||
| For existing containers, container_code is set to the entity key (the | ||
| only identifier available at this point). Future containers will have | ||
| container_code set by the caller. | ||
| """ | ||
| Container = apps.get_model("openedx_content", "Container") | ||
| for container in Container.objects.select_related("publishable_entity__learning_package").all(): | ||
| container.learning_package = container.publishable_entity.learning_package | ||
| container.container_code = container.publishable_entity.key | ||
| container.save(update_fields=["learning_package", "container_code"]) | ||
|
Comment on lines
+18
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Comment, no action needed] It's really unfortunate that we can't use |
||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("openedx_content", "0009_rename_component_local_key_to_component_code"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| # 1. Add learning_package FK (nullable initially for backfill) | ||
| migrations.AddField( | ||
| model_name="container", | ||
| name="learning_package", | ||
| field=models.ForeignKey( | ||
| null=True, | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| to="openedx_content.learningpackage", | ||
| ), | ||
| ), | ||
| # 2. Add container_code (nullable initially for backfill) | ||
| migrations.AddField( | ||
| model_name="container", | ||
| name="container_code", | ||
| field=openedx_django_lib.fields.MultiCollationCharField( | ||
| db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"}, | ||
| max_length=255, | ||
| null=True, | ||
| ), | ||
| ), | ||
| # 3. Backfill both fields from publishable_entity | ||
| migrations.RunPython(backfill_container_code, migrations.RunPython.noop), | ||
| # 4. Make both fields non-nullable and add regex validation to container_code | ||
| migrations.AlterField( | ||
| model_name="container", | ||
| name="learning_package", | ||
| field=models.ForeignKey( | ||
| null=False, | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| to="openedx_content.learningpackage", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="container", | ||
| name="container_code", | ||
| field=openedx_django_lib.fields.MultiCollationCharField( | ||
| db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"}, | ||
| max_length=255, | ||
|
kdmccormick marked this conversation as resolved.
|
||
| validators=[ | ||
| django.core.validators.RegexValidator( | ||
| re.compile(r"^[a-zA-Z0-9_.-]+\Z"), | ||
| "Enter a valid \"code name\" consisting of letters, numbers, " | ||
| "underscores, hyphens, or periods.", | ||
| "invalid", | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| # 5. Add uniqueness constraint | ||
| migrations.AddConstraint( | ||
| model_name="container", | ||
| constraint=models.UniqueConstraint( | ||
| fields=["learning_package", "container_code"], | ||
| name="oel_container_uniq_lp_cc", | ||
| ), | ||
| ), | ||
| # 6. Add db-level regex validation | ||
| migrations.AddConstraint( | ||
| model_name='container', | ||
| constraint=models.CheckConstraint( | ||
| condition=django.db.models.lookups.Regex(models.F('container_code'), '^[a-zA-Z0-9_.-]+\\Z'), | ||
| name='oel_container_code_regex', | ||
| violation_error_message='Enter a valid "code name" consisting of letters, numbers, underscores, hyphens, or periods.', | ||
| ), | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,4 @@ | |
| """ | ||
|
|
||
| # The version for the entire repository | ||
| __version__ = "0.41.0" | ||
| __version__ = "0.42.0" | ||
Uh oh!
There was an error while loading. Please reload this page.