Repository navigation
Conversation
… read and write API paths
An archived tag still holds its value and external_id, so adding a tag with either one failed with the generic "already exists" message, even though no such tag is visible anywhere. Taxonomy.add_tag and the REST serializer's value check now say the value or external_id belongs to an archived tag and that a different one should be used. The message for an unarchived duplicate is unchanged. Part of openedx#814. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An import resolved an incoming row to an existing tag by value or external_id even when that tag was archived, so it updated, renamed, or nested under an archived tag and revived it. The import plan now reports each row whose value, id, previous_id or parent_id belongs to an archived tag, and builds no actions, so nothing is imported. A replace import no longer deletes an archived tag or anything under it: that subtree is out of the taxonomy and out of its export, so its absence from the file does not mean it should go. An archived tag whose parent is deleted is moved to the root, as any kept child is. Part of openedx#814. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export a taxonomy containing archived tags, then import the result into an empty taxonomy and, with replace, back into the same one. The export leaves out archived tags and their descendants, the import into an empty taxonomy succeeds, and the replace import leaves every archived subtree in place. Part of openedx#814. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import plan compared rows with archived tags using Python casefold, while the actions resolve rows through the database collation. On MySQL, which also ignores accents, a row id like "Cafe" passed the check and then renamed the archived tag "Café". The check now asks the database, so it agrees with the unique constraints and with the actions' own lookups. A tag under an archived tag is out of the taxonomy too: export leaves it out and a replace import keeps it. Import now refuses rows that target such a tag, and add_tag refuses a parent with an archived ancestor. The replace import uses the same definition of the archived subtree. Also pin the unchanged message for a taken external_id, test the archived external_id through REST, and correct docstrings and comments that overstated how hidden archived tags are or went stale. Part of openedx#814. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the pull request, @AShatsila! This repository is currently maintained by 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 approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo 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:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
|
Claude flagged this while I was looking into why #814 was inccorectly tagged as a frontend ticket. FYI @tbain for your review: The PR description looks like it was carried over from the #778 work and not updated for the #814 commits on top of it. Three places disagree with the code in this branch:
@AShatsila Could you update the description so it matches what the branch delivers? Either "Closes #778 and closes #814" with the #814 behavior described alongside the #778 behavior, or, if you'd rather keep the two tickets reviewable on their own, split the #814 commits into a second PR stacked on this one. I lean toward the single PR since the #814 import changes build directly on the |
Description
Closes #814.
An archived tag still holds its value and
external_id, because the per-taxonomy unique constraints include archived rows. Before this change, creating or importing a tag with such a value failed with a plain "already exists" message that pointed at a tag the administrator cannot see, and an import could update or revive the archived tag. This PR makes create and import refuse those collisions with a message that names the cause, and pins that export and re-import leave archived tags out.Stacked PR. This PR is stacked on #856 (#776, adds
Tag.archived) and tbain#1 (#778, hides archived records from read paths and export). Until those two merge, the diff also shows their seven commits. The #814 change is only the last four commits,2c8b34dthrough78e19ac: compare view. #778 keeps its own PR and is reviewed there.What changes
Create a tag (
Taxonomy.add_tag, and the REST serializer'svalidate_tag_value):Tag value '<value>' belongs to an archived tag in this taxonomy. Use a different value.The serializer returns the same text in its own quoting style.external_idheld by an archived tag is refused withTag external_id '<id>' belongs to an archived tag in this taxonomy. Use a different external_id.external_idheld by an unarchived tag keeps its existing message byte for byte.Cannot add a tag under the archived tag '<value>'.Import (
TagImportPlan.generate_actions):value,id,previous_idandparent_id. A row that points at an archived tag, or at a tag under one, produces anArchivedTagCollisionError, for exampleRow 2's value (Photosynthesis) belongs to an archived tag in this taxonomy. Use a different value.When any row fails, the plan builds no actions, so nothing is imported.utf8mb4_unicode_cialso ignores accents and trailing spaces, which Python'scasefold()does not. A parity test asserts that the plan's verdict equals the database's forStraße/STRASSEandCafé/Cafe.replace=True) never deletes an archived tag or any tag under it. The export leaves that subtree out, so its absence from the file is no reason to delete it. An archived tag whose unarchived parent is deleted is moved to the root, as any kept child already is.Export: no code change. Tests pin that JSON and CSV exports omit archived tags (#778's behavior), that an export imports cleanly into an empty taxonomy, and that a replace re-import of a taxonomy's own export keeps its archived subtrees.
Decisions beyond the ticket
The ticket's acceptance criteria cover archived tags themselves. Three rules were added during implementation and review. Please push back on any of them:
add_tagnow agree with them.parent_idorprevious_idthat points at an archived tag is a plan error.Open questions for reviewers
delete_tags()archive a tag instead of deleting it when something depends on it. A replace import deletes through its ownDeleteTagaction, which [BE] Branch tag and taxonomy deletes between archiving and hard delete #780 does not mention, so after [BE] Branch tag and taxonomy deletes between archiving and hard delete #780 a replace import could still hard-delete a depended-upon tag. Should [BE] Branch tag and taxonomy deletes between archiving and hard delete #780's scope cover import, or should this get its own issue?Known limits, not addressed here
Taxonomy.update_tagraises anIntegrityError. This predates the change and is outside [BE] Treat an archived tag as no longer part of its taxonomy on create, import, and export #814. The REST rename path is covered by the serializer.Testing instructions
Locally on SQLite: 705 passed.
mypy,pylint,pycodestyle,isortandlint-importsare clean, andmakemigrations --checkreports no changes. MySQL was not run locally, so the MySQL collation cases rely on CI.Manual check through the API, with a taxonomy that has an archived tag
Photosynthesis:POST /tagging/rest_api/v1/taxonomies/<id>/tags/with{"tag": "Photosynthesis"}returns 400 with the archived-tag message.Photosynthesisfails, the import log lists the archived-tag error, and the taxonomy is unchanged.Photosynthesis.AI usage
The implementation, tests, and this description were written with Claude Code (Claude Opus 5.5), and reviewed and directed by the author. The change was reviewed by separate AI review passes before opening.
🤖 Generated with Claude Code