Repository navigation
[Follow-up BE] Hide archived tagging records in edx-platform's content_tagging app #903
Description
User Story
As a course author, I want archived taxonomies, tags, and tag associations to stay out of Studio's tag counts, tag exports, and copied or imported content, in order to not see or bring back something that is no longer in use.
Description
This is a follow-up ticket to a gap uncovered while implementing and code reviewing #778. #778 hides archived records on openedx-core's read paths, but edx-platform's content_tagging app counts tags, reads tag associations, and imports course tags with its own queries. We need those places to respect the archived state the same way openedx-core does.
Context
- Depends on [BE] Exclude archived tagging records from read paths and block edits to them #778, which adds the
unarchived()querysets and documents the opt-out (default managers return every row). - [BE] Branch tag association deletes between archiving and hard delete #779 and [BE] Branch tag and taxonomy deletes between archiving and hard delete #780 are what set
archived=True; until they land, nothing in the product produces an archived row. - [FE] Treat an archived tag as no longer part of its taxonomy on create, import, and export #814 covers importing over an archived tag's value inside openedx-core.
- Raised in the review of [BE] Exclude archived tagging records from read paths and block edits to them #778's PR (feat: add logic to exclude archived/retired tags and tag objects from API read/write tbain/openedx-core#1), finding 5.
Acceptance Criteria
Scenario Outline: A taxonomy's tag count leaves out archived tags
Given a taxonomy with three tags, one of them archived
When taxonomies are listed in Studio <filtered>
Then that taxonomy shows a tag count of two
Examples:
| filtered |
| with no filter |
| for the organization that owns it |
| as unassigned taxonomies |
Scenario: A taxonomy whose tags are all archived shows a count of zero
Given an unarchived taxonomy whose only tags are archived
When taxonomies are listed in Studio
Then the taxonomy appears with a tag count of zero
Scenario: An archived association is left out of a tag export
Given a course with two tagged blocks, one tagged through an archived association and one through an unarchived association
When the course's tags are exported
Then only the unarchived association appears in the export
Scenario: Tags under an archived tag or taxonomy are left out of a tag export
Given one block tagged with an archived tag and another tagged from an archived taxonomy, with neither association itself archived
When the course's tags are exported
Then neither tag appears in the export
And the archived taxonomy is not mentioned anywhere in the export
Scenario: A course whose only tags are archived exports with no tags
Given a course whose every tag association is archived
When the course's tags are exported from Studio's tag export
Then every block is listed with no tags
Scenario: Exporting and importing a course does not bring back archived associations
Given a course block with one archived and one unarchived association
When the course is exported and imported as a new course
Then the new course's block carries only the unarchived tag
Scenario: Duplicating a block does not copy archived associations
Given a block with one archived and one unarchived association
When the block is duplicated in Studio
Then the duplicate carries only the unarchived tag
And the original block's archived association is unchanged
Scenario: Copying and pasting a block does not carry archived associations
Given a course block with one archived and one unarchived association
When the block is copied and pasted into another unit
Then the pasted block carries only the unarchived tag
Scenario: Copying a library container does not carry archived associations
Given a library unit with an archived association, and a child component that also has an archived association
When the unit is copied and pasted into a course
Then neither the pasted unit nor its child carries an archived tag
Scenario: Unarchived tags still travel exactly as they do today
Given a block with tags from two unarchived taxonomies, one of them disabled
When the block is exported, duplicated, or copied and pasted
Then every tag, including the disabled taxonomy's, is carried as it is today
Scenario: Importing a course leaves out tags from a taxonomy archived on this instance
Given a course export, made before the taxonomy was archived or on another instance, whose tags come from one taxonomy archived on this instance and one live taxonomy
When the course is imported as a new course
Then the import completes
And each block carries its tags from the live taxonomy
And no block carries a tag from the archived taxonomy
Scenario: Left-out tags cannot reappear after a later taxonomy import
Given a course imported as a new course, whose import left out tags from an archived taxonomy
When an administrator later imports a tag file into any taxonomy
Then those blocks have no association with the archived taxonomy, visible or hidden
Scenario: Tags from a taxonomy that does not exist on this instance are still kept for later
Given a course export whose tags come from a taxonomy that does not exist on this instance
When the course is imported
And a matching taxonomy is later created from a tag file containing those values
Then the blocks carry those tags, as they do today
Scenario Outline: Re-importing a course into itself keeps the associations an author cannot see
Given a course block with <hidden association> and one association the author can see
When the course is exported and re-imported into the same course
Then the hidden association still exists, exactly once, and stays hidden
And the block's visible tags are replaced from the file as they are today
Examples:
| hidden association |
| an archived association |
| an association to an archived tag in a live taxonomy |
| an association under a taxonomy archived on this instance |
Scenario Outline: Saving a block's tags in Studio keeps the associations an author cannot see
Given a course block with <hidden association> and one association the author can see
When the author saves the block's tags in Studio, changing only the tags they can see
Then the hidden association still exists, exactly once, and stays hidden
And the block's visible tags match what the author saved
Examples:
| hidden association |
| an archived association |
| an association to an archived tag in a live taxonomy |
Out of scope: an incoming value, on paste or import, that belongs to an archived tag in a live taxonomy. This ticket accepts that value as-is, and the read paths already hide the association it creates.
Technical Details
This is a suggested approach, not the source of truth. The User Story, Description, and Acceptance Criteria define what must be true when the work is done.
Summary
Two repositories change. In openedx-core, tag_object stops deleting the associations that unarchived() hides, so a full-list save or import leaves them alone. In openedx-platform, the Studio taxonomy list counts only unarchived tags, and get_all_object_tags, the one read behind every export, duplicate, and copy path, reads through ObjectTag.objects.unarchived(). Course tag import clears only the associations that unarchived() returns, and it uses an unfiltered lookup to tell an archived taxonomy apart from a missing one: it skips and logs tags from an archived taxonomy, and creates placeholders only for a missing one.
Approach
-
Keep hidden associations on a full-list save (openedx-core,
src/openedx_tagging/api.py).tag_objectreplaces an object's associations for one taxonomy:_get_current_tagsreads them through the unfiltered manager, each tag in the new list claims its matching row, and the rows left unclaimed are removed. The removal step skips every row thatunarchived()hides. A hidden row therefore survives a save that lists other tags of its taxonomy, and it is still matched when its own tag is re-applied, so [BE] Branch tag association deletes between archiving and hard delete #779's un-archive on re-apply keeps working. This change lands after [BE] Branch tag association deletes between archiving and hard delete #779 and applies to the removal set that [BE] Branch tag association deletes between archiving and hard delete #779 builds, which it then splits into locked rows to archive and the rest to delete. Studio's tag drawer andset_all_object_tagson paste build their new list from the filteredget_object_tags, so without this step every save would delete the hidden rows the author cannot see._check_new_tag_countstill counts hidden rows towardOBJECT_MAX_TAGS(100); that is accepted. -
Tag count (openedx-platform). In
openedx/core/djangoapps/content_tagging/rest_api/v1/views.py,TaxonomyOrgView.get_querysetannotatestags_count=Count("tag", filter=Q(tag__archived=False), distinct=True)on all three branches (org, unassigned, and neither). This is the same expression that openedx-core'sTaxonomyViewuses. -
Association reads (openedx-platform). In
openedx/core/djangoapps/content_tagging/api.py,get_all_object_tagsstarts fromObjectTag.objects.unarchived(). This one query feeds:- the CSV export (
generate_csv_rows, and the coursetags.csvthroughexport_tags_in_csv_file); - duplication (
copy_object_tags); - clipboard copy (
openedx/core/lib/xblock_serializer/block_serializer.py); - library containers (
openedx/core/djangoapps/content_libraries/api/serializers.py).
The CSV header is built from the returned rows, so an archived taxonomy drops out of the header too.
xmodule/modulestore/xml_exporter.pywritestags.csvonly when openedx-core'sget_object_tag_countsfinds a tag. [BE] Exclude archived tagging records from read paths and block edits to them #778 already filters that count, so a course whose only tags are archived writes notags.csv. That count also skips disabled taxonomies, so a test that exports a disabled taxonomy's tags needs a live tag in the same course. - the CSV export (
-
Course tag import (openedx-platform), in
set_exported_object_tags:- Clearing. Clear with
ObjectTag.objects.filter(object_id=content_key_str).unarchived().delete()in place ofoel_tagging.delete_object_tags. A hidden association is exactly a row thatunarchived()excludes: an archived association, or one whose tag, tag's taxonomy, or own taxonomy is archived. Rows in disabled taxonomies, rows whose tag was deleted, and placeholders (taxonomy=None) are still cleared and re-created from the file as today. The clear stays a querysetdelete()so that [BE] Branch tag association deletes between archiving and hard delete #779'sObjectTagQuerySet.delete()override covers it: the override archives a locked visible row, andtag_objectun-archives that row if the file lists it again. - Resolving taxonomies. Resolve each export id with one unfiltered
Taxonomy.objects.filter(export_id=...).first()in place ofget_taxonomy_by_export_id, which returnsNonefor an archived and a missing taxonomy alike. Then branch on the result:- a live taxonomy: call
tag_objectas today, which keeps the block's hidden rows in that taxonomy because of step 1; - an archived taxonomy: skip it, with no placeholder and no events, and
log.infothe content key, export id, and skipped values. A placeholder is wrong here becauseObjectTag.resync()looks taxonomies up through plainTaxonomy.objects, so it would re-link the placeholder to the archived taxonomy; - a missing taxonomy: create placeholders as today.
- a live taxonomy: call
- Hand-built files. A hand-built file that lists a hidden association's own tag re-applies that association, and after [BE] Branch tag association deletes between archiving and hard delete #779 the re-application un-archives it. That behavior is accepted. A file exported from this instance never lists a hidden association's tag.
- Clearing. Clear with
-
Paste needs no change beyond step 1.
set_all_object_tagsresolves taxonomies through the filteredget_taxonomy, and step 3 keeps hidden rows out of the copied OLX.
Out of scope:
- Deleting content, where
handlers.pycallsdelete_object_tags([BE] Branch tag association deletes between archiving and hard delete #779). - Upstream-linked paste and library sync, which use openedx-core's
copy_tags([BE] Exclude archived tagging records from read paths and block edits to them #778, [BE] Branch tag association deletes between archiving and hard delete #779).
Files to create and modify
| File | Change |
|---|---|
| openedx-core: src/openedx_tagging/api.py | modified: tag_object removal step skips hidden rows |
| openedx-core: tests/openedx_tagging/test_api.py | modified: hidden rows survive a full-list save; re-apply still un-archives |
| openedx-platform: openedx/core/djangoapps/content_tagging/rest_api/v1/views.py | modified: TaxonomyOrgView.get_queryset |
| openedx-platform: openedx/core/djangoapps/content_tagging/api.py | modified: get_all_object_tags, set_exported_object_tags, module logger |
| openedx-platform: requirements/edx/*.txt | modified: openedx-core pin, if the regular upgrade has not already bumped it |
Additional context
- openedx-platform pins
openedx-core==1.4.0. The openedx-platform part needs an openedx-core release that contains [BE] Exclude archived tagging records from read paths and block edits to them #778, [BE] Branch tag association deletes between archiving and hard delete #779, and step 1, so the openedx-core part of this ticket ships first.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
Activity
[-][BE] Hide archived tagging records in edx-platform's content_tagging app[/-][+][Follow-up BE] Hide archived tagging records in edx-platform's content_tagging app[/+]