Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.2.17 on 2026-10-07 23:12

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('oel_tagging', '0022_tag_external_id_not_null'),
]

operations = [
migrations.AddField(
model_name='objecttag',
name='archived',
field=models.BooleanField(default=False, help_text='Hides this association from active use by default, without removing it.'),
),
migrations.AddField(
model_name='objecttag',
name='deletion_locked',
field=models.BooleanField(default=False, help_text='Set when something outside the tagging app depends on this association; a delete archives it instead of removing it.'),
),
migrations.AddField(
model_name='tag',
name='archived',
field=models.BooleanField(default=False, help_text='Hides this tag from active use by default, without removing it.'),
),
migrations.AddField(
model_name='taxonomy',
name='archived',
field=models.BooleanField(default=False, help_text="Hides this taxonomy from active use by default, without removing it. Unlike disabling it with 'enabled', archiving marks the taxonomy as retired."),
),
]
24 changes: 24 additions & 0 deletions src/openedx_tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ class Tag(models.Model):
" Set automatically by save(); do not set manually."
),
)
archived = models.BooleanField(
default=False,
help_text=_("Hides this tag from active use by default, without removing it."),
)

class Meta:
indexes = [
Expand Down Expand Up @@ -311,6 +315,13 @@ class Taxonomy(models.Model):
" taxonomy admins will not be permitted to add, edit, or delete its tags."
),
)
archived = models.BooleanField(
default=False,
help_text=_(
"Hides this taxonomy from active use by default, without removing it."
" Unlike disabling it with 'enabled', archiving marks the taxonomy as retired."
),
)

class Meta:
verbose_name_plural = "Taxonomies"
Expand Down Expand Up @@ -724,6 +735,17 @@ class ObjectTag(models.Model):
" If the tag field is set, then tag.value takes precedence over this field."
),
)
archived = models.BooleanField(
default=False,
help_text=_("Hides this association from active use by default, without removing it."),
)
deletion_locked = models.BooleanField(
default=False,
help_text=_(
"Set when something outside the tagging app depends on this association;"
" a delete archives it instead of removing it."
),
)

class Meta:
indexes = [
Expand Down Expand Up @@ -896,6 +918,8 @@ def copy(self, object_tag: ObjectTag) -> Self:
self.taxonomy = object_tag.taxonomy
self.object_id = object_tag.object_id
self.is_copied = object_tag.is_copied
self.archived = object_tag.archived
self.deletion_locked = object_tag.deletion_locked
self._value = object_tag._value # pylint: disable=protected-access
self._export_id = object_tag._export_id # pylint: disable=protected-access
return self
36 changes: 35 additions & 1 deletion tests/openedx_tagging/test_migrations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Tests for the 0022_tag_external_id_not_null migration's backfill logic.
Tests that run openedx_tagging migrations over rows created on the earlier schema.

Uses the `migrator` pytest fixture from django_test_migrations to run the actual
migration against historical (frozen) model states, the same way it will run against
Expand All @@ -12,6 +12,9 @@
MIGRATE_FROM = ("oel_tagging", "0021_remove_system_defined_add_read_only")
MIGRATE_TO = ("oel_tagging", "0022_tag_external_id_not_null")

MIGRATE_0023_FROM = ("oel_tagging", "0022_tag_external_id_not_null")
MIGRATE_0023_TO = ("oel_tagging", "0023_archived_and_deletion_locked")


@pytest.mark.django_db
def test_backfill_generates_missing_external_ids(migrator) -> None:
Expand Down Expand Up @@ -98,3 +101,34 @@ def test_reverse_migration_is_a_noop_that_keeps_data(migrator) -> None:

reverted_tag = RevertedTag.objects.get(pk=tag.pk)
assert reverted_tag.external_id == "Rollback Tag"


@pytest.mark.django_db
def test_archived_and_deletion_locked_default_to_false_for_existing_rows(migrator) -> None:
"""
Rows that exist before 0023 come out unarchived and unlocked.
"""
old_state = migrator.apply_initial_migration(MIGRATE_0023_FROM)
Taxonomy = old_state.apps.get_model("oel_tagging", "Taxonomy")
Tag = old_state.apps.get_model("oel_tagging", "Tag")
ObjectTag = old_state.apps.get_model("oel_tagging", "ObjectTag")

taxonomy = Taxonomy.objects.create(name="Flags Test", export_id="flags_test")
tag = Tag.objects.create(
taxonomy=taxonomy, value="Flags Tag", external_id="flags-tag", depth=0, lineage="Flags Tag\t",
)
object_tag = ObjectTag.objects.create(object_id="object:id:1", taxonomy=taxonomy, tag=tag)

new_state = migrator.apply_tested_migration(MIGRATE_0023_TO)
NewTaxonomy = new_state.apps.get_model("oel_tagging", "Taxonomy")
NewTag = new_state.apps.get_model("oel_tagging", "Tag")
NewObjectTag = new_state.apps.get_model("oel_tagging", "ObjectTag")

new_taxonomy = NewTaxonomy.objects.get(pk=taxonomy.pk)
new_tag = NewTag.objects.get(pk=tag.pk)
new_object_tag = NewObjectTag.objects.get(pk=object_tag.pk)

assert new_taxonomy.archived is False
assert new_tag.archived is False
assert new_object_tag.archived is False
assert new_object_tag.deletion_locked is False
7 changes: 7 additions & 0 deletions tests/openedx_tagging/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,13 @@ def test_cast(self):
== "<ObjectTagTestSubclass> object:id:1: Life on Earth=Bacteria"
)

def test_copying_a_tag_via_casting_copies_field_values(self) -> None:
self.object_tag.archived = True
self.object_tag.deletion_locked = True
copy_tag = ObjectTagTestSubclass.cast(self.object_tag)
assert copy_tag.archived is True
assert copy_tag.deletion_locked is True

def test_object_tag_export_id(self):
# ObjectTag's export_id defaults to its taxonomy's export_id
assert self.object_tag.export_id == self.taxonomy.export_id
Expand Down
Loading