You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: Adding archived and deletion-locked flags to tagging models - #856
Adds archived = models.BooleanField(default=False) to Taxonomy, Tag, and ObjectTag, and deletion_locked = models.BooleanField(default=False) to ObjectTag only, in one schema-only
migration (oel_tagging0023_archived_and_deletion_locked).
Today these three models can only be hard-deleted, which would destroy the learner mastery history
that points at them. archived records that a row has been retired: hidden from active use, but
kept. deletion_locked records that something outside the tagging app depends on an association,
so a future delete must archive it instead of removing it. Tag and Taxonomy have no lock field
of their own; #777 derives whether one is locked from the associations beneath it.
ObjectTag.copy(), which ObjectTag.cast() uses, also copies both new fields, so saving a cast
instance does not reset them to False on the original row.
Apply the migration: python manage.py migrate oel_tagging
Confirm no migration is missing: python manage.py makemigrations --check --dry-run
Manual check in a Django shell: create a Taxonomy, a Tag, and an ObjectTag, and confirm archived and deletion_locked are False. Set deletion_locked=True on the ObjectTag,
save it, reload it, and confirm archived is still False.
Other information
PII: all three models are already listed as no_pii in .annotation_safe_list.yml, so the new
fields need no new annotations.
The migration is reversible: it contains only four AddField operations.
AI usage: this PR was drafted with Claude (Anthropic) in Cowork. The developer reviewed the
change and ran all tests and checks locally.
This repository is currently maintained by @axim-engineering.
Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.
🔘 Get product approval
If you haven't already, check this list to see if your contribution needs to go through the product review process.
If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
This process (including the steps you'll need to take) is documented here.
If it doesn't, simply proceed with the next step.
🔘 Provide context
To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
Dependencies
This PR must be merged before / after / at the same time as ...
The reason will be displayed to describe this comment to others. Learn more.
Claude and I collaborated on this code review. Claude wrote this comment:
Thanks for this, the model and migration changes look good. The ObjectTag.copy() fix is a good catch, and I confirmed that test_cast_copies_archived_and_deletion_locked fails when those two copy() lines are reverted, so that test is doing real work. I also ran the tagging model and migration tests, makemigrations --check, pylint, pycodestyle, isort, mypy, and lint-imports against the PR head, and everything passes.
My requests below are all about the tests. Most of the new tests check things that the schema already guarantees, so they can't fail.
1. Make the test_migrations.py module docstring clearer (line 2). "Run against existing data" is vague. What the tests actually do is create rows on the schema from before a migration, run the real migration, and then check those rows. Something like "Tests that run openedx_tagging migrations over rows created on the earlier schema." would say that directly.
2. In test_archived_and_deletion_locked_default_to_false_for_existing_rows, remove lines 140-147, and with them the line 121 comment and the explicit _export_id/_value arguments on line 123. Lines 140-147 check that the migration left every other field unchanged. Migration 0023 only adds four columns with a default, and adding a column can't change the values in other columns, so these assertions have no way to fail. Lines 146-147 in particular only check the test's own setup. I tried removing the _export_id/_value arguments from line 123: the only failure was assert '' == 'flags_test' on line 146, which comes from the test's setup, not from the migration. With lines 140-147, the comment, and those two arguments all removed, the test still passes. It still checks what matters, lines 135-138: pre-existing rows come out unarchived and unlocked.
3. Remove test_archived_defaults_to_false (test_models.py, line 158) and test_archived_and_deletion_locked_default_to_false (test_models.py, line 611). Both tests check that a newly created row has the default=False declared on the field, so they test the schema itself rather than any behavior. The defaults for pre-existing rows, which are the case #776 actually asks about, are already covered by the migration test.
4. Remove test_setting_one_flag_leaves_the_other_and_identity_unchanged (test_models.py, line 623).archived and deletion_locked are separate columns, so saving one can't change the other, and neither can change object_id, taxonomy_id, tag_id, or the cached fields. I know #776's acceptance criteria list "can be set independently ... with no other field affected", but not every acceptance criterion needs a unit test, and the schema already guarantees this one.
5. Nit, not required: the docstrings on the new tests in test_models.py could be dropped. Pylint doesn't require docstrings on tests here, and each one mostly restates the test name. Keeping them is fine too, since the file has both styles today. This applies to whichever of those tests remain after items 3 and 4, which would be only test_cast_copies_archived_and_deletion_locked.
Updated to address all the code review comments, handled with Claude 5.5 Opus. I also updated Step 3 of the testing instructions since we changed/removed some of the tests.
The reason will be displayed to describe this comment to others. Learn more.
Thank you! It seems like the CI tests are failing for reasons unrelated to this PR. I'm not sure how to re-trigger them other than possibly pushing a no-op commit unless someone else with access to re-trigger kicks it off.
The reason will be displayed to describe this comment to others. Learn more.
Hi @tbain , great work! There's only one nit suggestion (non blocking) for changing a test name.
I'd like to make sure that this PR is all set for community review. To this end, I went over the description and checked whether it is accurate and understandable.
I found some points that are not relevant to the PR reviewer, so I'd recommend cutting them so that the reviewer can focus on what's important and not lose time.
The "Choices that differ" section is a section that AI likes to add and that sometimes is very helpful, but in this case the points were either outdated/inaccurate like the "help_text_wording", or unnecessary / not real deviations from the ticket. That's pretty misleading. I would only keep the "copy()" mention and since that's not a deviation from the ticket but an edge case move it to a better section.
I came up with a cleaned up version of the PR description, which I recommend to use:
"
Description
Adds archived = models.BooleanField(default=False) to Taxonomy, Tag, and ObjectTag, and deletion_locked = models.BooleanField(default=False) to ObjectTag only, in one schema-only
migration (oel_tagging0023_archived_and_deletion_locked).
Today these three models can only be hard-deleted, which would destroy the learner mastery history
that points at them. archived records that a row has been retired: hidden from active use, but
kept. deletion_locked records that something outside the tagging app depends on an association,
so a future delete must archive it instead of removing it. Tag and Taxonomy have no lock field
of their own; #777 derives whether one is locked from the associations beneath it.
ObjectTag.copy(), which ObjectTag.cast() uses, also copies both new fields, so saving a cast
instance does not reset them to False on the original row.
Apply the migration: python manage.py migrate oel_tagging
Confirm no migration is missing: python manage.py makemigrations --check --dry-run
Manual check in a Django shell: create a Taxonomy, a Tag, and an ObjectTag, and confirm archived and deletion_locked are False. Set deletion_locked=True on the ObjectTag,
save it, reload it, and confirm archived is still False.
Other information
PII: all three models are already listed as no_pii in .annotation_safe_list.yml, so the new
fields need no new annotations.
The migration is reversible: it contains only four AddField operations.
AI usage: this PR was drafted with Claude (Anthropic) in Cowork. The developer reviewed the
change and ran all tests and checks locally.
No actionable comments were generated in the recent review.
Walkthrough
The change adds default-false archive fields to Tag, Taxonomy, and ObjectTag, plus a default-false deletion-lock field to ObjectTag. A migration adds the fields, and ObjectTag.copy() preserves the two new association values.
Changes: Tagging model fields
Layer / File(s)
Summary
Add tagging fields and migrate existing rows src/openedx_tagging/models/base.py, src/openedx_tagging/migrations/0023_archived_and_deletion_locked.py, tests/openedx_tagging/test_migrations.py
Tag, Taxonomy, and ObjectTag gain default-false archived fields. ObjectTag also gains a default-false deletion_locked field. The migration test checks that existing rows have these fields set to False.
Preserve fields when copying ObjectTag src/openedx_tagging/models/base.py, tests/openedx_tagging/test_models.py
ObjectTag.copy() copies archived and deletion_locked. A test checks that casting an ObjectTag preserves both values.
This change adds default-false archive and deletion-lock fields to the tagging models and preserves them when an association is copied. Existing rows get safe defaults and nothing reads the fields yet, so no behavior change is expected. No merge-blocking risk is evident.
Security architecture risk: Minimal · up to 2bab7
The change adds metadata without activating new deletion rules or weakening existing access controls. Copies preserve the new values. Archival and deletion protection are not enforced by these fields alone and remain follow-up work.
Retained concerns
No architecture-level concerns identified.
Security review details
Security Blast Radius
observed — The persistent change covers tagging records and object-tag associations. The migration adds metadata without changing primary keys, foreign keys, or existing association constraints, and copy preserves the same row identity.
Trust Boundaries and Controls
inferred — Taxonomy and Tag use admin registrations without explicit field restrictions, so their archived field becomes available through existing admin forms. ObjectTag's explicit admin field list excludes both new flags. This extends metadata editing within the existing administrative surface rather than adding a public flag-update endpoint.
Pre-merge checks: 4
Check name
Status
Explanation
Description Check
Check skipped - CodeRabbit’s high-level summary is enabled.
Title check
The title "feat: Adding archived and deletion-locked flags to tagging models" matches the main change. The PR adds archived fields to Taxonomy, Tag, and ObjectTag, adds deletion_locked to ObjectTag, a…
Linked Issues check
Directly linked issue #776 has coding requirements only. Verified at head: src/openedx_tagging/models/base.py declares archived = models.BooleanField(default=False) on Tag and Taxonomy, with h…
Out of Scope Changes check
The whole-PR changes (merge base 7e150e7 to head 2bab7c2) consist of the four new model fields and the ObjectTag.copy() update in models/base.py, the single schema migration 0023, and tests in `…
Full details: Title check
Explanation
The title "feat: Adding archived and deletion-locked flags to tagging models" matches the main change. The PR adds archived fields to Taxonomy, Tag, and ObjectTag, adds deletion_locked to ObjectTag, and includes the migration 0023 and the copy() update. The title is specific and concise, and it names the primary change for a reviewer scanning history.
Full details: Linked Issues check
Explanation
Directly linked issue #776 has coding requirements only. Verified at head: src/openedx_tagging/models/base.py declares archived = models.BooleanField(default=False) on Tag and Taxonomy, with help text. Migration 0023_archived_and_deletion_locked.py (depends on 0022_tag_external_id_not_null) contains exactly four AddField operations, all BooleanField(default=False): objecttag.archived, objecttag.deletion_locked, tag.archived, taxonomy.archived. No data migration, backfill, archived_at, or locked_at appears. deletion_locked is on ObjectTag only. The migration test in tests/openedx_tagging/test_migrations.py covers existing rows reading back as unarchived and unlocked. The model test in test_models.py covers copy()/cast() preserving both flags. Remaining items are not fully verified: the ObjectTag field declarations and copy() body in base.py were truncated at the 16000-byte read limit, and the base.py diff could not be read. The ObjectTag declarations are confirmed through the migration only. Help text match with #716 and make pii_check result could not be checked, because #716 code and the PII check output are not in the evidence. The index guidance in #776 is advisory ('likely warranted') and is not an acceptance criterion; no index was added, which is consistent with the criteria.
Full details: Out of Scope Changes check
Explanation
The whole-PR changes (merge base 7e150e7 to head 2bab7c2) consist of the four new model fields and the ObjectTag.copy() update in models/base.py, the single schema migration 0023, and tests in test_migrations.py and test_models.py. All of these implement #776 or support it. The copy() change keeps the new flags from resetting during cast(), which the PR description states. The tests validate the migration and the new defaults. No read path, write path, filter, delete branch, lock API, or REST change was identified in the reviewed evidence, which matches #776's exclusions. Unverified: the base.py diff and the ObjectTag/copy() source were not fully readable, so the absence of other edits in base.py rests on the PR summary.
Finishing Touches
Generate unit tests (beta)
Create a new PR
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: defaults
Review profile: CHILL
Plan: Advanced
Run ID: a5e81215-0b6f-4610-affd-dd7946ad34df
Commits
Reviewing files that changed from the base of the PR and between 7e150e7 and 2bab7c2.
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
archived = models.BooleanField(default=False)toTaxonomy,Tag, andObjectTag, anddeletion_locked = models.BooleanField(default=False)toObjectTagonly, in one schema-onlymigration (
oel_tagging0023_archived_and_deletion_locked).Today these three models can only be hard-deleted, which would destroy the learner mastery history
that points at them.
archivedrecords that a row has been retired: hidden from active use, butkept.
deletion_lockedrecords that something outside the tagging app depends on an association,so a future delete must archive it instead of removing it.
TagandTaxonomyhave no lock fieldof their own; #777 derives whether one is locked from the associations beneath it.
ObjectTag.copy(), whichObjectTag.cast()uses, also copies both new fields, so saving a castinstance does not reset them to
Falseon the original row.Impacted roles: Developer only.
Supporting information
docs/openedx_learning/decisions/0003-competency-criteria-versioning.rst, Decision 3archivedtoCompetencyCriteriaGroupandCompetencyCriterionTesting instructions
python manage.py migrate oel_taggingpython manage.py makemigrations --check --dry-runTaxonomy, aTag, and anObjectTag, and confirmarchivedanddeletion_lockedareFalse. Setdeletion_locked=Trueon theObjectTag,save it, reload it, and confirm
archivedis stillFalse.Other information
no_piiin.annotation_safe_list.yml, so the newfields need no new annotations.
AddFieldoperations.change and ran all tests and checks locally.
Generated with Claude Code
Summary by CodeRabbit