Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions core/collections/migrations/0072_expansion_built_with.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.17 on 2026-11-10 05:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('collections', '0071_retire_public_edit_access'),
]

operations = [
migrations.AddField(
model_name='expansion',
name='built_with',
field=models.CharField(blank=True, choices=[('lock', 'lock'), ('latest', 'latest')], max_length=10, null=True),
),
]
13 changes: 9 additions & 4 deletions core/collections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ def seed_from_head_expansion_in_snapshot(self, is_outermost, lock=True):
parameters[ExpansionParameters.INCLUDE_SYSTEM] = system_versions
expansion = Expansion(
mnemonic=mnemonic, collection_version=self, parameters=parameters,
built_with=VERSION_EXPANSION_LOCK if lock else VERSION_EXPANSION_LATEST,
created_by_id=self.created_by_id, updated_by_id=self.created_by_id)
expansion.clean()
expansion.full_clean()
Expand Down Expand Up @@ -1540,6 +1541,10 @@ class Meta:
evaluated_source_versions = models.ManyToManyField(
'sources.Source', related_name='expansions_evaluated_source_versions_set')
unresolved_repo_versions = ArrayField(models.JSONField(), default=list, null=True, blank=True)
# lock or latest when version create built it with that choice, so a rebuild repeats how it was built
built_with = models.CharField(
max_length=10, null=True, blank=True,
choices=[(choice, choice) for choice in (VERSION_EXPANSION_LOCK, VERSION_EXPANSION_LATEST)])

@property
def is_head(self):
Expand Down Expand Up @@ -1863,7 +1868,8 @@ def add_references( # pylint: disable=too-many-locals,too-many-statements,too-m
should_reevaluate = force_reevaluate or (attempt_reevaluate and not self.is_auto_generated)

resolve_by_resource_version = CollectionReference.is_resource_version_repo_resolution_on()
use_any_lock = CollectionReference.is_expansion_locked_repo_version_on()
# one built with lock or latest keeps evaluating as it was built, whatever the toggle is now
use_any_lock = CollectionReference.is_expansion_locked_repo_version_on() or bool(self.built_with)
lock_filters = {} if use_any_lock else {'released': True, 'retired': False}
should_write_back = use_any_lock and self.is_default
resolved_ref_ids = []
Expand Down Expand Up @@ -2128,10 +2134,9 @@ def replace_head_rows(rows, owner, field, klass):

def seed_children(self, index=True, force_reevaluate=False):
version = self.collection_version
# the expansion version create built is rebuilt as it was built, whichever expansion is now the default:
# one built with lock or latest is rebuilt as it was built, whatever the toggle or default expansion is now:
# HEAD rows swapped for resource versions and cascades without a cap
swap_head = force_reevaluate and not version.is_head and self.is_auto_generated and \
CollectionReference.is_expansion_locked_repo_version_on()
swap_head = force_reevaluate and not version.is_head and bool(self.built_with)
previous_concept_ids = set(self.concepts.values_list('id', flat=True)) if swap_head and index else set()
previous_mapping_ids = set(self.mappings.values_list('id', flat=True)) if swap_head and index else set()
references = version.references
Expand Down
58 changes: 57 additions & 1 deletion core/collections/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,61 @@ def test_rebuild_keeps_members_past_the_cascade_cap_as_creation_does(self, _):
self.assertGreater(expansion.concepts.count(), 1000)
self.assertTrue(expansion.concepts.filter(versioned_object_id=grandchild.versioned_object_id).exists())

@patch('core.common.tasks.export_collection')
def test_rebuild_keeps_valueset_only_members_after_the_toggle_is_turned_off(self, _):
valueset = OrganizationCollectionFactory()
_, concept = self.valueset_release(valueset, 'v1')
head = OrganizationCollectionFactory()
expansion = ExpansionFactory(collection_version=head, mnemonic='autoexpand-HEAD')
head.expansion_uri = expansion.uri
head.save()
reference = CollectionReference(
expression=valueset.uri, collection=head, valueset=[valueset.uri], reference_type='concepts')
reference.clean()
reference.save()
version = OrganizationCollectionFactory(organization=head.organization, version='v1', mnemonic=head.mnemonic)
seed_children_to_new_version('collection', version.id, False, expansion='lock') # pylint: disable=no-value-for-parameter
version.refresh_from_db()
expansion = version.expansion
self.assertEqual(list(expansion.concepts.all()), [concept])

with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=False):
expansion.seed_children(index=False, force_reevaluate=True)

self.assertEqual(list(expansion.concepts.all()), [concept])

@patch('core.common.tasks.export_collection')
def test_rebuild_keeps_members_past_the_cascade_cap_after_the_toggle_is_turned_off(self, _):
grandchild, version = self.head_cascading_past_the_cap()
seed_children_to_new_version('collection', version.id, False, expansion='lock') # pylint: disable=no-value-for-parameter
version.refresh_from_db()
expansion = version.expansion
member_ids = set(expansion.concepts.values_list('id', flat=True))

with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=False):
expansion.seed_children(index=False, force_reevaluate=True)

self.assertEqual(set(expansion.concepts.values_list('id', flat=True)), member_ids)
self.assertTrue(expansion.concepts.filter(versioned_object_id=grandchild.versioned_object_id).exists())

@patch('core.common.tasks.export_collection')
def test_rebuild_of_an_expansion_built_with_the_toggle_off_stays_capped_once_it_is_on(self, _):
grandchild, version = self.head_cascading_past_the_cap()
with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=False):
seed_children_to_new_version('collection', version.id, False) # pylint: disable=no-value-for-parameter
version.refresh_from_db()
expansion = version.expansion
self.assertIsNone(expansion.built_with)
member_ids = set(expansion.concepts.values_list('id', flat=True))
self.assertFalse(expansion.concepts.filter(versioned_object_id=grandchild.versioned_object_id).exists())
other = ExpansionFactory(collection_version=version, mnemonic='other')
Collection.objects.filter(id=version.id).update(expansion_uri=other.uri)
expansion = Expansion.objects.get(id=expansion.id)

expansion.seed_children(index=False, force_reevaluate=True)

self.assertEqual(set(expansion.concepts.values_list('id', flat=True)), member_ids)

@patch('core.common.tasks.export_collection')
def test_rebuild_keeps_members_past_the_cascade_cap_when_another_expansion_is_the_default(self, _):
grandchild, version = self.head_cascading_past_the_cap()
Expand Down Expand Up @@ -2417,12 +2472,13 @@ def test_rebuild_keeps_the_resource_versions_a_head_resolved_version_was_created

expansion.seed_children(index=False, force_reevaluate=True)

self.assertEqual(expansion.built_with, choice)
self.assertEqual(list(expansion.concepts.all()), [latest])
self.assertEqual(list(collection_v1.references.first().concepts.all()), [latest])

with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=False):
expansion.seed_children(index=False, force_reevaluate=True)
self.assertEqual(list(expansion.concepts.all()), [concept])
self.assertEqual(list(expansion.concepts.all()), [latest])

@patch('core.common.tasks.export_collection')
def test_seed_children_task_evaluates_references_while_head_expansion_is_processing(self, _):
Expand Down
Loading