7676 "get_containers" ,
7777 "ChildrenEntitiesAction" ,
7878 "ContainerEntityListEntry" ,
79+ "ContainerEntityRow" ,
7980 "get_entities_in_container" ,
8081 "contains_unpublished_changes" ,
8182 "get_containers_with_entity" ,
@@ -639,8 +640,7 @@ def create_entity_list() -> EntityList:
639640
640641
641642def create_entity_list_with_rows (
642- entity_pks : list [int ],
643- entity_version_pks : list [int | None ],
643+ entity_rows : list [ContainerEntityRow ],
644644 * ,
645645 learning_package_id : int | None ,
646646) -> EntityList :
@@ -649,10 +649,7 @@ def create_entity_list_with_rows(
649649 Create new entity list rows for an entity list.
650650
651651 Args:
652- entity_pks: The IDs of the publishable entities that the entity list rows reference.
653- entity_version_pks: The IDs of the versions of the entities
654- (PublishableEntityVersion) that the entity list rows reference, or
655- Nones for "unpinned" (default).
652+ entity_rows: List of ContainerEntityRows specifying the publishable entity ID and version ID (if pinned).
656653 learning_package_id: Optional. Verify that all the entities are from
657654 the specified learning package.
658655
@@ -662,27 +659,37 @@ def create_entity_list_with_rows(
662659 # Do a quick check that the given entities are in the right learning package:
663660 if learning_package_id :
664661 if PublishableEntity .objects .filter (
665- pk__in = entity_pks ,
662+ pk__in = [ entity . entity_pk for entity in entity_rows ] ,
666663 ).exclude (
667664 learning_package_id = learning_package_id ,
668665 ).exists ():
669666 raise ValidationError ("Container entities must be from the same learning package." )
670667
671- order_nums = range (len (entity_pks ))
668+ # Ensure that any pinned entity versions are linked to the correct entity
669+ pinned_entities = {
670+ entity .version_pk : entity .entity_pk
671+ for entity in entity_rows if entity .pinned
672+ }
673+ if pinned_entities :
674+ entity_versions = PublishableEntityVersion .objects .filter (
675+ pk__in = pinned_entities .keys (),
676+ ).only ('pk' , 'entity_id' )
677+ for entity_version in entity_versions :
678+ if pinned_entities [entity_version .pk ] != entity_version .entity_id :
679+ raise ValidationError ("Container entity versions must belong to the specified entity." )
680+
672681 with atomic (savepoint = False ):
673682
674683 entity_list = create_entity_list ()
675684 EntityListRow .objects .bulk_create (
676685 [
677686 EntityListRow (
678687 entity_list = entity_list ,
679- entity_id = entity_pk ,
688+ entity_id = entity . entity_pk ,
680689 order_num = order_num ,
681- entity_version_id = entity_version_pk ,
682- )
683- for order_num , entity_pk , entity_version_pk in zip (
684- order_nums , entity_pks , entity_version_pks
690+ entity_version_id = entity .version_pk ,
685691 )
692+ for order_num , entity in enumerate (entity_rows )
686693 ]
687694 )
688695 return entity_list
@@ -725,8 +732,7 @@ def create_container_version(
725732 version_num : int ,
726733 * ,
727734 title : str ,
728- publishable_entities_pks : list [int ],
729- entity_version_pks : list [int | None ] | None ,
735+ entity_rows : list [ContainerEntityRow ],
730736 created : datetime ,
731737 created_by : int | None ,
732738 container_version_cls : type [ContainerVersionModel ] = ContainerVersion , # type: ignore[assignment]
@@ -739,8 +745,7 @@ def create_container_version(
739745 container_id: The ID of the container that the version belongs to.
740746 version_num: The version number of the container.
741747 title: The title of the container.
742- publishable_entities_pks: The IDs of the members of the container.
743- entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
748+ entity_rows: List of ContainerEntityRows specifying the publishable entity ID and version ID (if pinned).
744749 created: The date and time the container version was created.
745750 created_by: The ID of the user who created the container version.
746751 container_version_cls: The subclass of ContainerVersion to use, if applicable.
@@ -749,16 +754,13 @@ def create_container_version(
749754 The newly created container version.
750755 """
751756 assert title is not None
752- assert publishable_entities_pks is not None
757+ assert entity_rows is not None
753758
754759 with atomic (savepoint = False ):
755760 container = Container .objects .select_related ("publishable_entity" ).get (pk = container_id )
756761 entity = container .publishable_entity
757- if entity_version_pks is None :
758- entity_version_pks = [None ] * len (publishable_entities_pks )
759762 entity_list = create_entity_list_with_rows (
760- entity_pks = publishable_entities_pks ,
761- entity_version_pks = entity_version_pks ,
763+ entity_rows = entity_rows ,
762764 learning_package_id = entity .learning_package_id ,
763765 )
764766 container_version = _create_container_version (
@@ -785,8 +787,7 @@ class ChildrenEntitiesAction(Enum):
785787def create_next_entity_list (
786788 learning_package_id : int ,
787789 last_version : ContainerVersion ,
788- publishable_entities_pks : list [int ],
789- entity_version_pks : list [int | None ] | None ,
790+ entity_rows : list [ContainerEntityRow ],
790791 entities_action : ChildrenEntitiesAction = ChildrenEntitiesAction .REPLACE ,
791792) -> EntityList :
792793 """
@@ -795,55 +796,53 @@ def create_next_entity_list(
795796 Args:
796797 learning_package_id: Learning package ID
797798 last_version: Last version of container.
798- publishable_entities_pks: The IDs of the members current members of the container.
799- entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
799+ entity_rows: List of ContainerEntityRows specifying the publishable entity ID and version ID (if pinned).
800800 entities_action: APPEND, REMOVE or REPLACE given entities from/to the container
801801
802802 Returns:
803803 The newly created entity list.
804804 """
805- if entity_version_pks is None :
806- entity_version_pks : list [int | None ] = [None ] * len (publishable_entities_pks ) # type: ignore[no-redef]
807805 if entities_action == ChildrenEntitiesAction .APPEND :
808806 # get previous entity list rows
809807 last_entities = last_version .entity_list .entitylistrow_set .only (
810808 "entity_id" ,
811809 "entity_version_id"
812810 ).order_by ("order_num" )
813- # append given publishable_entities_pks and entity_version_pks
814- publishable_entities_pks = [entity .entity_id for entity in last_entities ] + publishable_entities_pks
815- entity_version_pks = [ # type: ignore[operator, assignment]
816- entity .entity_version_id
811+ # append given entity_rows to the existing children
812+ entity_rows = [
813+ ContainerEntityRow (
814+ entity_pk = entity .entity_id ,
815+ version_pk = entity .entity_version_id ,
816+ )
817817 for entity in last_entities
818- ] + entity_version_pks
818+ ] + entity_rows
819819 elif entities_action == ChildrenEntitiesAction .REMOVE :
820- # get previous entity list rows
820+ # get previous entity list, excluding the entities in entity_rows
821821 last_entities = last_version .entity_list .entitylistrow_set .only (
822822 "entity_id" ,
823823 "entity_version_id"
824+ ).exclude (
825+ entity_id__in = [entity .entity_pk for entity in entity_rows ]
824826 ).order_by ("order_num" )
825- # Remove entities that are in publishable_entities_pks
826- new_entities = [
827- entity
828- for entity in last_entities
829- if entity .entity_id not in publishable_entities_pks
827+ entity_rows = [
828+ ContainerEntityRow (
829+ entity_pk = entity .entity_id ,
830+ version_pk = entity .entity_version_id ,
831+ )
832+ for entity in last_entities .all ()
830833 ]
831- publishable_entities_pks = [entity .entity_id for entity in new_entities ]
832- entity_version_pks = [entity .entity_version_id for entity in new_entities ]
833- next_entity_list = create_entity_list_with_rows (
834- entity_pks = publishable_entities_pks ,
835- entity_version_pks = entity_version_pks , # type: ignore[arg-type]
834+
835+ return create_entity_list_with_rows (
836+ entity_rows = entity_rows ,
836837 learning_package_id = learning_package_id ,
837838 )
838- return next_entity_list
839839
840840
841841def create_next_container_version (
842842 container_pk : int ,
843843 * ,
844844 title : str | None ,
845- publishable_entities_pks : list [int ] | None ,
846- entity_version_pks : list [int | None ] | None ,
845+ entity_rows : list [ContainerEntityRow ] | None ,
847846 created : datetime ,
848847 created_by : int | None ,
849848 container_version_cls : type [ContainerVersionModel ] = ContainerVersion , # type: ignore[assignment]
@@ -863,8 +862,8 @@ def create_next_container_version(
863862 Args:
864863 container_pk: The ID of the container to create the next version of.
865864 title: The title of the container. None to keep the current title.
866- publishable_entities_pks: The IDs of the members current members of the container. Or None for no change .
867- entity_version_pks: The IDs of the versions to pin to, if pinning is desired .
865+ entity_rows: List of ContainerEntityRows specifying the publishable entity ID and version ID (if pinned) .
866+ Or None for no change .
868867 created: The date and time the container version was created.
869868 created_by: The ID of the user who created the container version.
870869 container_version_cls: The subclass of ContainerVersion to use, if applicable.
@@ -879,15 +878,15 @@ def create_next_container_version(
879878 last_version = container .versioning .latest
880879 assert last_version is not None
881880 next_version_num = last_version .version_num + 1
882- if publishable_entities_pks is None :
881+
882+ if entity_rows is None :
883883 # We're only changing metadata. Keep the same entity list.
884884 next_entity_list = last_version .entity_list
885885 else :
886886 next_entity_list = create_next_entity_list (
887887 entity .learning_package_id ,
888888 last_version ,
889- publishable_entities_pks ,
890- entity_version_pks ,
889+ entity_rows ,
891890 entities_action
892891 )
893892
@@ -969,6 +968,23 @@ def entity(self):
969968 return self .entity_version .entity
970969
971970
971+ @dataclass (frozen = True , kw_only = True , slots = True )
972+ class ContainerEntityRow :
973+ """
974+ [ 🛑 UNSTABLE ]
975+ Used to specify the primary key of PublishableEntity and optional PublishableEntityVersion.
976+
977+ If version_pk is None (default), then the entity is considered "unpinned",
978+ meaning that the latest version of the entity will be used.
979+ """
980+ entity_pk : int
981+ version_pk : int | None = None
982+
983+ @property
984+ def pinned (self ):
985+ return self .entity_pk and self .version_pk is not None
986+
987+
972988def get_entities_in_container (
973989 container : Container ,
974990 * ,
0 commit comments