33"""
44from ..components .test_api import ComponentTestCase
55from openedx_learning .api import authoring as authoring_api
6+ from openedx_learning .api import authoring_models
67
78
89class UnitTestCase (ComponentTestCase ):
@@ -16,7 +17,7 @@ def setUp(self) -> None:
1617 created = self .now ,
1718 created_by = None ,
1819 )
19- self .component_2 , self .component_2_v2 = authoring_api .create_component_and_version (
20+ self .component_2 , self .component_2_v1 = authoring_api .create_component_and_version (
2021 self .learning_package .id ,
2122 component_type = self .problem_type ,
2223 local_key = "Query Counting (2)" ,
@@ -25,6 +26,49 @@ def setUp(self) -> None:
2526 created_by = None ,
2627 )
2728
29+ def create_unit_with_components (
30+ self ,
31+ components : list [authoring_models .Component | authoring_models .ComponentVersion ],
32+ * ,
33+ title = "Unit" ,
34+ key = "unit:key" ,
35+ ) -> authoring_models .Unit :
36+ """ Helper method to quickly create a unit with some components """
37+ unit , _unit_v1 = authoring_api .create_unit_and_version (
38+ learning_package_id = self .learning_package .id ,
39+ key = key ,
40+ title = title ,
41+ created = self .now ,
42+ created_by = None ,
43+ )
44+ _unit_v2 = authoring_api .create_next_unit_version (
45+ unit = unit ,
46+ title = title ,
47+ components = components ,
48+ created = self .now ,
49+ created_by = None ,
50+ )
51+ unit .refresh_from_db ()
52+ return unit
53+
54+ def modify_component (
55+ self ,
56+ component : authoring_models .Component ,
57+ * ,
58+ title = "Modified Component" ,
59+ timestamp = None ,
60+ ) -> authoring_models .ComponentVersion :
61+ """
62+ Helper method to modify a component for the purposes of testing units/drafts/pinning/publishing/etc.
63+ """
64+ return authoring_api .create_next_component_version (
65+ component .pk ,
66+ content_to_replace = {},
67+ title = title ,
68+ created = timestamp or self .now ,
69+ created_by = None ,
70+ )
71+
2872 def test_create_unit_with_content_instead_of_components (self ):
2973 """Test creating a unit with content instead of components.
3074
@@ -56,7 +100,7 @@ def test_create_empty_unit_and_version(self):
56100 assert unit .versioning .draft == unit_version
57101 assert unit .versioning .published is None
58102
59- def test_create_next_unit_version_with_two_components (self ):
103+ def test_create_next_unit_version_with_two_unpinned_components (self ):
60104 """Test creating a unit version with two unpinned components.
61105
62106 Expected results:
@@ -85,6 +129,33 @@ def test_create_next_unit_version_with_two_components(self):
85129 authoring_api .UnitListEntry (component_version = self .component_1 .versioning .draft , pinned = False ),
86130 authoring_api .UnitListEntry (component_version = self .component_2 .versioning .draft , pinned = False ),
87131 ]
132+ assert authoring_api .get_components_in_published_unit (unit ) is None
133+
134+ def test_create_next_unit_version_with_unpinned_and_pinned_components (self ):
135+ """
136+ Test creating a unit version with one unpinned and one pinned component.
137+ """
138+ unit , unit_version = authoring_api .create_unit_and_version (
139+ learning_package_id = self .learning_package .id ,
140+ key = f"unit:key" ,
141+ title = "Unit" ,
142+ created = self .now ,
143+ created_by = None ,
144+ )
145+ unit_version_v2 = authoring_api .create_next_unit_version (
146+ unit = unit ,
147+ title = "Unit" ,
148+ components = [self .component_1 , self .component_2_v1 ], # Note the "v1" pinning it to version 2
149+ created = self .now ,
150+ created_by = None ,
151+ )
152+ assert unit_version_v2 .version_num == 2
153+ assert unit_version_v2 in unit .versioning .versions .all ()
154+ assert authoring_api .get_components_in_draft_unit (unit ) == [
155+ authoring_api .UnitListEntry (component_version = self .component_1_v1 , pinned = False ),
156+ authoring_api .UnitListEntry (component_version = self .component_2_v1 , pinned = True ), # Pinned to v1
157+ ]
158+ assert authoring_api .get_components_in_published_unit (unit ) is None
88159
89160 def test_add_component_after_publish (self ):
90161 """
@@ -123,49 +194,31 @@ def test_add_component_after_publish(self):
123194 assert unit .versioning .draft == unit_version_v2
124195 assert unit .versioning .published == unit_version
125196
126- def test_modify_component_after_publish (self ):
197+ def test_modify_unpinned_component_after_publish (self ):
127198 """
128- Modifying a component in a published unit will NOT create a new version
129- nor show that the unit has unpublished changes (but it will "contain"
130- unpublished changes). The modifications will appear in the published
131- version of the unit only after the component is published.
199+ Modifying an unpinned component in a published unit will NOT create a
200+ new version nor show that the unit has unpublished changes (but it will
201+ "contain" unpublished changes). The modifications will appear in the
202+ published version of the unit only after the component is published.
132203 """
133- # Create a unit:
134- unit , unit_version = authoring_api .create_unit_and_version (
135- learning_package_id = self .learning_package .id ,
136- key = f"unit:key" ,
137- title = "Unit" ,
138- created = self .now ,
139- created_by = None ,
140- )
141- # Add a draft component (unpinned):
204+ # Create a unit with one unpinned draft component:
142205 assert self .component_1 .versioning .has_unpublished_changes == True
143- unit_version_v2 = authoring_api .create_next_unit_version (
144- unit = unit ,
145- title = unit_version .title ,
146- components = [self .component_1 ],
147- created = self .now ,
148- created_by = None ,
149- )
206+ unit = self .create_unit_with_components ([self .component_1 ])
207+ assert unit .versioning .has_unpublished_changes == True
208+
150209 # Publish the unit and the component:
151210 authoring_api .publish_all_drafts (self .learning_package .id )
152- unit .refresh_from_db () # Reloading the unit is necessary
211+ unit .refresh_from_db () # Reloading the unit is necessary if we accessed 'versioning' before publish
153212 self .component_1 .refresh_from_db ()
154213 assert unit .versioning .has_unpublished_changes == False # Shallow check
155214 assert authoring_api .contains_unpublished_changes (unit ) == False # Deeper check
156215 assert self .component_1 .versioning .has_unpublished_changes == False
157216
158217 # Now modify the component by changing its title (it remains a draft):
159- component_1_v2 = authoring_api .create_next_component_version (
160- self .component_1 .pk ,
161- content_to_replace = {},
162- title = "Modified Counting Problem with new title" ,
163- created = self .now ,
164- created_by = None ,
165- )
218+ component_1_v2 = self .modify_component (self .component_1 , title = "Modified Counting Problem with new title" )
166219
167220 # The component now has unpublished changes; the unit doesn't directly but does contain
168- unit .refresh_from_db () # Reloading the unit is necessary
221+ unit .refresh_from_db () # Reloading the unit is necessary, or 'unit.versioning' will be outdated
169222 self .component_1 .refresh_from_db ()
170223 assert unit .versioning .has_unpublished_changes == False # Shallow check should be false - unit is unchanged
171224 assert authoring_api .contains_unpublished_changes (unit ) == True # But unit DOES contain changes
@@ -189,19 +242,46 @@ def test_modify_component_after_publish(self):
189242 ]
190243 assert authoring_api .contains_unpublished_changes (unit ) == False # No longer contains unpublished changes
191244
245+ def test_modify_pinned_component (self ):
246+ """
247+ When a pinned 📌 component in unit is modified and/or published, it will
248+ have no effect on either the draft nor published version of the unit,
249+ which will continue to use the pinned version.
250+ """
251+ # Create a unit with one component (pinned 📌 to v1):
252+ unit = self .create_unit_with_components ([self .component_1_v1 ])
253+
254+ # Publish the unit and the component:
255+ authoring_api .publish_all_drafts (self .learning_package .id )
256+ expected_unit_contents = [
257+ authoring_api .UnitListEntry (component_version = self .component_1_v1 , pinned = True ), # pinned 📌 to v1
258+ ]
259+ assert authoring_api .get_components_in_published_unit (unit ) == expected_unit_contents
260+
261+ # Now modify the component by changing its title (it remains a draft):
262+ self .modify_component (self .component_1 , title = "Modified Counting Problem with new title" )
263+
264+ # The component now has unpublished changes; the unit is entirely unaffected
265+ unit .refresh_from_db () # Reloading the unit is necessary, or 'unit.versioning' will be outdated
266+ self .component_1 .refresh_from_db ()
267+ assert unit .versioning .has_unpublished_changes == False # Shallow check
268+ assert authoring_api .contains_unpublished_changes (unit ) == False # Deep check
269+ assert self .component_1 .versioning .has_unpublished_changes == True
270+
271+ # Neither the draft nor the published version of the unit is affected
272+ assert authoring_api .get_components_in_draft_unit (unit ) == expected_unit_contents
273+ assert authoring_api .get_components_in_published_unit (unit ) == expected_unit_contents
274+ # Even if we publish the component, the unit stays pinned to the specified version:
275+ self .publish_component (self .component_1 )
276+ assert authoring_api .get_components_in_draft_unit (unit ) == expected_unit_contents
277+ assert authoring_api .get_components_in_published_unit (unit ) == expected_unit_contents
278+
192279
193280 def test_query_count_of_contains_unpublished_changes (self ):
194281 """
195282 Checking for unpublished changes in a unit should require a fixed number
196283 of queries, not get more expensive as the unit gets larger.
197284 """
198- unit , unit_version = authoring_api .create_unit_and_version (
199- learning_package_id = self .learning_package .id ,
200- key = f"unit:key" ,
201- title = "Unit" ,
202- created = self .now ,
203- created_by = None ,
204- )
205285 # Add 100 components (unpinned)
206286 component_count = 100
207287 components = []
@@ -214,18 +294,17 @@ def test_query_count_of_contains_unpublished_changes(self):
214294 created = self .now ,
215295 )
216296 components .append (component )
217- authoring_api .create_next_unit_version (
218- unit = unit ,
219- title = unit_version .title ,
220- components = components ,
221- created = self .now ,
222- )
297+ unit = self .create_unit_with_components (components )
223298 authoring_api .publish_all_drafts (self .learning_package .id )
224299 unit .refresh_from_db ()
225300 with self .assertNumQueries (3 ):
226301 assert authoring_api .contains_unpublished_changes (unit ) == False
227302
228- # Test that pinned components with changes don't show up as "contains unpublished changes"
303+ # Modify the most recently created component:
304+ self .modify_component (component , title = "Modified Component" )
305+ with self .assertNumQueries (2 ):
306+ assert authoring_api .contains_unpublished_changes (unit ) == True
307+
229308 # Test that only components can be added to units
230309 # Test that components must be in the same learning package
231310 # Test that _version_pks=[] arguments must be related to publishable_entities_pks
0 commit comments