Skip to content

Commit e09a20e

Browse files
refactor: consolidate defined_list/initial_list/frozen_list into entity_list
1 parent e07a741 commit e09a20e

5 files changed

Lines changed: 22 additions & 255 deletions

File tree

openedx_learning/apps/authoring/containers/api.py

Lines changed: 13 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ def create_entity_list() -> EntityList:
7373
return EntityList.objects.create()
7474

7575

76-
def create_next_defined_list(
77-
previous_entity_list: EntityList | None, # pylint: disable=unused-argument
78-
new_entity_list: EntityList,
76+
def create_entity_list_with_rows(
7977
entity_pks: list[int],
8078
entity_version_pks: list[int | None],
8179
) -> EntityList:
@@ -84,52 +82,6 @@ def create_next_defined_list(
8482
Create new entity list rows for an entity list.
8583
8684
Args:
87-
previous_entity_list: The previous entity list that the new entity list is based on.
88-
new_entity_list: The entity list to create the rows for.
89-
entity_pks: The IDs of the publishable entities that the entity list rows reference.
90-
entity_version_pks: The IDs of the draft versions of the entities
91-
(PublishableEntityVersion) that the entity list rows reference, or
92-
Nones for "unpinned" (default).
93-
94-
Returns:
95-
The newly created entity list rows.
96-
"""
97-
order_nums = range(len(entity_pks))
98-
with atomic():
99-
# Case 1: create first container version (no previous rows created for container)
100-
# 1. Create new rows for the entity list
101-
# Case 2: create next container version (previous rows created for container)
102-
# 1. Get all the rows in the previous version entity list
103-
# 2. Only associate existent rows to the new entity list iff: the order is the same, the PublishableEntity is in
104-
# entity_pks and versions are not pinned
105-
# 3. If the order is different for a row with the PublishableEntity, create new row with the same
106-
# PublishableEntity for the new order and associate the new row to the new entity list
107-
new_rows = []
108-
for order_num, entity_pk, entity_version_pk in zip(
109-
order_nums, entity_pks, entity_version_pks
110-
):
111-
new_rows.append(
112-
EntityListRow(
113-
entity_list=new_entity_list,
114-
entity_id=entity_pk,
115-
order_num=order_num,
116-
entity_version_id=entity_version_pk,
117-
)
118-
)
119-
EntityListRow.objects.bulk_create(new_rows)
120-
return new_entity_list
121-
122-
123-
def create_defined_list_with_rows(
124-
entity_pks: list[int],
125-
entity_version_pks: list[int | None],
126-
) -> EntityList:
127-
"""
128-
[ 🛑 UNSTABLE ]
129-
Create new entity list rows for an entity list.
130-
131-
Args:
132-
entity_list: The entity list to create the rows for.
13385
entity_pks: The IDs of the publishable entities that the entity list rows reference.
13486
entity_version_pks: The IDs of the versions of the entities
13587
(PublishableEntityVersion) that the entity list rows reference, or
@@ -188,46 +140,6 @@ def get_entity_list_with_pinned_versions(
188140
return entity_list
189141

190142

191-
def check_unpinned_versions_in_defined_list(
192-
defined_list: EntityList,
193-
) -> bool:
194-
"""
195-
[ 🛑 UNSTABLE ]
196-
Check if there are any unpinned versions in the defined list.
197-
198-
Args:
199-
defined_list: The defined list to check for unpinned versions.
200-
201-
Returns:
202-
True if there are unpinned versions in the defined list, False otherwise.
203-
"""
204-
# Is there a way to short-circuit this?
205-
return any(
206-
row.entity_version is None
207-
for row in defined_list.entitylistrow_set.all()
208-
)
209-
210-
211-
def check_new_changes_in_defined_list(
212-
entity_list: EntityList, # pylint: disable=unused-argument
213-
publishable_entities_pks: list[int], # pylint: disable=unused-argument
214-
) -> bool:
215-
"""
216-
[ 🛑 UNSTABLE ]
217-
Check if there are any new changes in the defined list.
218-
219-
Args:
220-
entity_list: The entity list to check for new changes.
221-
publishable_entities: The publishable entities to check for new changes.
222-
223-
Returns:
224-
True if there are new changes in the defined list, False otherwise.
225-
"""
226-
# Is there a way to short-circuit this? Using queryset operations
227-
# For simplicity, return True
228-
return True
229-
230-
231143
def create_container_version(
232144
container_pk: int,
233145
version_num: int,
@@ -263,19 +175,14 @@ def create_container_version(
263175
created=created,
264176
created_by=created_by,
265177
)
266-
defined_list = create_defined_list_with_rows(
178+
entity_list = create_entity_list_with_rows(
267179
entity_pks=publishable_entities_pks,
268180
entity_version_pks=entity_version_pks,
269181
)
270182
container_version = ContainerEntityVersion.objects.create(
271183
publishable_entity_version=publishable_entity_version,
272184
container_id=container_pk,
273-
defined_list=defined_list,
274-
initial_list=defined_list,
275-
# TODO: Check for unpinned versions in defined_list to know whether to point this to the defined_list
276-
# point to None.
277-
# If this is the first version ever created for this ContainerEntity, then start as None.
278-
frozen_list=None,
185+
entity_list=entity_list,
279186
)
280187
return container_version
281188

@@ -330,50 +237,14 @@ def create_next_container_version(
330237
created=created,
331238
created_by=created_by,
332239
)
333-
# 1. Check if there are any changes in the container's members
334-
# 2. Pin versions in previous frozen list for last container version
335-
# 3. Create new defined list for author changes
336-
# 4. Pin versions in defined list to create initial list
337-
# 5. Check for unpinned references in defined_list to determine if frozen_list should be None
338-
# 6. Point frozen_list to None or defined_list
339-
if check_new_changes_in_defined_list(
340-
entity_list=last_version.defined_list,
341-
publishable_entities_pks=publishable_entities_pks,
342-
):
343-
# Only change if there are unpin versions in defined list, meaning last frozen list is None
344-
# When does this has to happen? Before?
345-
if not last_version.frozen_list:
346-
last_version.frozen_list = get_entity_list_with_pinned_versions(
347-
rows=last_version.defined_list.entitylistrow_set.all()
348-
)
349-
last_version.save()
350-
next_defined_list = create_next_defined_list(
351-
previous_entity_list=last_version.defined_list,
352-
new_entity_list=create_entity_list(),
353-
entity_pks=publishable_entities_pks,
354-
entity_version_pks=entity_version_pks,
355-
)
356-
next_initial_list = get_entity_list_with_pinned_versions(
357-
rows=next_defined_list.entitylistrow_set.all()
358-
)
359-
if check_unpinned_versions_in_defined_list(next_defined_list):
360-
next_frozen_list = None
361-
else:
362-
next_frozen_list = next_initial_list
363-
else:
364-
# Do I need to create new EntityList and copy rows?
365-
# I do think so because frozen can change when creating a new version
366-
# Does it need to change though?
367-
# What would happen if I only change the title?
368-
next_defined_list = last_version.defined_list
369-
next_initial_list = last_version.initial_list
370-
next_frozen_list = last_version.frozen_list
240+
next_entity_list = create_entity_list_with_rows(
241+
entity_pks=publishable_entities_pks,
242+
entity_version_pks=entity_version_pks,
243+
)
371244
next_container_version = ContainerEntityVersion.objects.create(
372245
publishable_entity_version=publishable_entity_version,
373246
container_id=container_pk,
374-
defined_list=next_defined_list,
375-
initial_list=next_initial_list,
376-
frozen_list=next_frozen_list,
247+
entity_list=next_entity_list,
377248
)
378249

379250
return next_container_version
@@ -461,8 +332,7 @@ def get_entities_in_draft_container(
461332
container = container.container_entity
462333
assert isinstance(container, ContainerEntity)
463334
entity_list = []
464-
defined_list = container.versioning.draft.defined_list
465-
for row in defined_list.entitylistrow_set.order_by("order_num"):
335+
for row in container.versioning.draft.entity_list.entitylistrow_set.order_by("order_num"):
466336
entity_version = row.entity_version or row.entity.draft.version
467337
if entity_version is not None: # As long as this hasn't been soft-deleted:
468338
entity_list.append(ContainerEntityListEntry(
@@ -490,13 +360,8 @@ def get_entities_in_published_container(
490360
if cev is None:
491361
return None # There is no published version of this container. Should this be an exception?
492362
assert isinstance(cev, ContainerEntityVersion)
493-
# TODO: do we ever need frozen_list? e.g. when accessing a historical version?
494-
# Doesn't make a lot of sense when the versions of the container don't capture many of the changes to the contents,
495-
# e.g. container version 1 had component version 1 through 50, and container version 2 had component versions 51
496-
# through 100, what is the point of tracking whether container version 1 "should" show v1 or v50 when they're wildly
497-
# different?
498363
entity_list = []
499-
for row in cev.defined_list.entitylistrow_set.order_by("order_num"):
364+
for row in cev.entity_list.entitylistrow_set.order_by("order_num"):
500365
entity_version = row.entity_version or row.entity.published.version
501366
if entity_version is not None: # As long as this hasn't been soft-deleted:
502367
entity_list.append(ContainerEntityListEntry(
@@ -524,7 +389,7 @@ def contains_unpublished_changes(
524389
"publishable_entity",
525390
"publishable_entity__draft",
526391
"publishable_entity__draft__version",
527-
"publishable_entity__draft__version__containerentityversion__defined_list",
392+
"publishable_entity__draft__version__containerentityversion__entity_list",
528393
).get(pk=container.container_entity_id)
529394
else:
530395
pass # TODO: select_related if we're given a raw ContainerEntity rather than a ContainerEntityMixin like Unit?
@@ -534,12 +399,12 @@ def contains_unpublished_changes(
534399
return True
535400

536401
# We only care about children that are un-pinned, since published changes to pinned children don't matter
537-
defined_list = container.versioning.draft.defined_list
402+
entity_list = container.versioning.draft.entity_list
538403

539404
# TODO: This is a naive inefficient implementation but hopefully correct.
540405
# Once we know it's correct and have a good test suite, then we can optimize.
541406
# We will likely change to a tracking-based approach rather than a "scan for changes" based approach.
542-
for row in defined_list.entitylistrow_set.filter(entity_version=None).select_related(
407+
for row in entity_list.entitylistrow_set.filter(entity_version=None).select_related(
543408
"entity__containerentity",
544409
"entity__draft__version",
545410
"entity__published__version",

openedx_learning/apps/authoring/containers/migrations/0001_initial.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ class Migration(migrations.Migration):
4343
fields=[
4444
('publishable_entity_version', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentityversion')),
4545
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='oel_containers.containerentity')),
46-
('defined_list', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='defined_list', to='oel_containers.entitylist')),
47-
('frozen_list', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='frozen_list', to='oel_containers.entitylist')),
48-
('initial_list', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='initial_list', to='oel_containers.entitylist')),
46+
('entity_list', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='entity_list', to='oel_containers.entitylist')),
4947
],
5048
options={
5149
'abstract': False,

openedx_learning/apps/authoring/containers/models.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class EntityList(models.Model):
2121
sometimes we'll want the same kind of data structure for things that we
2222
dynamically generate for individual students (e.g. Variants). EntityLists are
2323
anonymous in a sense–they're pointed to by ContainerEntityVersions and
24-
other models, rather than being looked up by their own identifers.
24+
other models, rather than being looked up by their own identifiers.
2525
"""
2626

2727

@@ -92,7 +92,7 @@ class ContainerEntityVersion(PublishableEntityVersionMixin):
9292
The last looks a bit odd, but it's because *how we've defined the Unit* has
9393
changed if we decide to explicitly pin a set of versions for the children,
9494
and then later change our minds and move to a different set. It also just
95-
makes things easier to reason about if we say that defined_list never
95+
makes things easier to reason about if we say that entity_list never
9696
changes for a given ContainerEntityVersion.
9797
"""
9898

@@ -102,46 +102,10 @@ class ContainerEntityVersion(PublishableEntityVersionMixin):
102102
related_name="versions",
103103
)
104104

105-
# This is the EntityList that the author defines. This should never change,
106-
# even if the things it references get soft-deleted (because we'll need to
107-
# maintain it for reverts).
108-
defined_list = models.ForeignKey(
105+
# The list of entities (frozen and/or unfrozen) in this container
106+
entity_list = models.ForeignKey(
109107
EntityList,
110108
on_delete=models.RESTRICT,
111109
null=False,
112-
related_name="defined_list",
113-
)
114-
115-
# inital_list is an EntityList where all the versions are pinned, to show
116-
# what the exact versions of the children were at the time that the
117-
# Container was created. We could technically derive this, but it would be
118-
# awkward to query.
119-
#
120-
# If the Container was defined so that all references were pinned, then this
121-
# can point to the exact same EntityList as defined_list.
122-
initial_list = models.ForeignKey(
123-
EntityList,
124-
on_delete=models.RESTRICT,
125-
null=False,
126-
related_name="initial_list",
127-
)
128-
129-
# This is the EntityList that's created when the next ContainerEntityVersion
130-
# is created. All references in this list should be pinned, and it serves as
131-
# "the last state the children were in for this version of the Container".
132-
# If defined_list has only pinned references, this should point to the same
133-
# EntityList as defined_list and initial_list.
134-
#
135-
# This value is mutable if and only if there are unpinned references in
136-
# defined_list. In that case, frozen_list should start as None, and be
137-
# updated to pin references when another version of this Container becomes
138-
# the Draft version. But if this version ever becomes the Draft *again*
139-
# (e.g. the user hits "discard changes" or some kind of revert happens),
140-
# then we need to clear this back to None.
141-
frozen_list = models.ForeignKey(
142-
EntityList,
143-
on_delete=models.RESTRICT,
144-
null=True,
145-
default=None,
146-
related_name="frozen_list",
110+
related_name="entity_list",
147111
)

openedx_learning/apps/authoring/units/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def get_components_in_published_unit_as_of(
283283
unit_version = unit_pub_entity_version.unitversion # type: ignore[attr-defined]
284284

285285
entity_list = []
286-
rows = unit_version.container_entity_version.defined_list.entitylistrow_set.order_by("order_num")
286+
rows = unit_version.container_entity_version.entity_list.entitylistrow_set.order_by("order_num")
287287
for row in rows:
288288
if row.entity_version is not None:
289289
component_version = row.entity_version.componentversion

0 commit comments

Comments
 (0)