Tag import: reject a parent_id that names a tag the full replace import deletes
メンテナーはふだん 1 日以内に返信
@ufedaseyeuconsultant がすでに取り組んでいます。
2026年10月20日 から。
評価
この issue はまだ評価されていません。
説明
User Story
As a Platform Administrator re-importing a competency taxonomy through the Studio import wizard, I want the Plan step to reject any row whose parent_id names a competency that the same import deletes, in order to fix my file before I confirm, rather than have the confirmed import fail or finish with a hierarchy my file does not describe.
Acceptance Criteria
Every import below is a full replace import, as the Studio wizard always runs it: a tag without a row in the file is deleted. Every scenario holds whatever the order of the rows in the file.
Scenario: An existing tag moved under a tag the import deletes is rejected
Given a taxonomy with tag_1, tag_2 under tag_1, and tag_3
When the admin imports a file with rows for tag_1 and for tag_2 with parent_id "tag_3", and no row for tag_3
Then the Plan step rejects the import with an error that names tag_2's row and "tag_3", says that this import deletes tag_3 because the file has no row for it, and says to add a row for tag_3 or change the parent_id
And the admin cannot proceed
And an export afterward shows the taxonomy unchanged
Scenario: A row that keeps its current parent, which the import deletes, is rejected
Given a taxonomy with tag_3 and tag_4 under tag_3
When the admin imports a file with a row for tag_4 that still says parent_id "tag_3", and no row for tag_3
Then the Plan step rejects the import with the same error, naming tag_4's row
And an export afterward shows the taxonomy unchanged, with tag_4 still under tag_3
Scenario Outline: A new tag or a renamed tag under a tag the import deletes is rejected
Given a taxonomy with tag_1 and tag_3
When the file has no row for tag_3, and has <row>
Then the Plan step rejects the import with the same error, naming that row
Examples:
| row |
| a row that creates a new tag with parent_id "tag_3" |
| a row that renames tag_1 to "tag_50" with parent_id "tag_3" |
Scenario: Every row that names a deleted tag is reported in one pass
Given a taxonomy with tag_3 and tag_4 under tag_3
When the file has no row for tag_3, keeps tag_4 with parent_id "tag_3", and creates new_tag with parent_id "tag_3"
Then the Plan step shows one error for tag_4's row and one for new_tag's row, in the one error block
Scenario: A case variant of the deleted tag's id is treated like the exact spelling
Given a taxonomy with tag_3 and tag_4 under tag_3
When the file has no row for tag_3, and tag_4's row says parent_id "TAG_3"
Then the Plan step rejects the import with the same error, naming tag_4's row and "TAG_3"
Scenario: A kept child whose row has no parent_id still becomes a root
Given a taxonomy with tag_3 and tag_4 under tag_3, where tag_4 is applied to content
When the file has no row for tag_3, and tag_4's row has no parent_id
Then the import succeeds
And tag_4 is a root, is the same tag as before (not deleted and recreated), and keeps its content associations
Scenario: A kept child whose row names another kept tag moves under it
Given a taxonomy with tag_1, tag_3, and tag_4 under tag_3
When the file has rows for tag_1 and for tag_4 with parent_id "tag_1", and no row for tag_3
Then the import succeeds and tag_4 is under tag_1
Scenario: A child naming an id that another tag is renamed onto moves under the renamed tag
Given a taxonomy with tag_1, tag_3, and tag_4 under tag_3
When the file has no row for tag_3, renames tag_1 to "tag_3" (previous_id "tag_1", id "tag_3"), and tag_4's row says parent_id "tag_3" or "TAG_3"
Then the import succeeds
And tag_4 is under the tag that was tag_1 and now has the id "tag_3"
Description
Today, a full replace file in which a kept tag's row names its own parent, while the file has no row for that parent, imports successfully and moves the tag to the root, which is not what the row says. After this change that file is rejected at the Plan step. This is a deliberate change of outcome for Studio admins and for any other caller of the full replace import. Files that name a deleted tag in any other way already fail today, but only after the admin confirms the import, and the error names no row.
Technical Details
This section is background and a suggested approach, not the source of truth. The User Story and Acceptance Criteria are what must be true when the work is done.
In short
Where the check runs. The parent validation that each row's action runs cannot catch every case. A kept tag whose row names its current parent counts as "no change" for the parent-update action, so no action that validates its parent is built at all, and the delete phase quietly promotes the tag to the root. The check therefore runs once at the plan level, over the file's rows, and not inside the per-row parent validation. It looks only at each row's parent_id, so it treats a moved tag, a kept tag, a new tag and a renamed tag the same way.
What counts as naming a deleted tag. By the time the check runs, the plan has built the delete steps for the tags that have no row in the file, and it has computed the tag tree as it will be after the import. A row is rejected when its parent_id, compared without regard to case, names a tag the import deletes and no tag of the final tree holds that id. The second condition keeps the valid case working: when the file renames another tag onto the deleted tag's id, the renamed tag holds the id in the final tree, so a child that names the id is accepted and ends up under the renamed tag, as it does today. The error names the row and the parent_id as the row spells it. Every offending row gets its own error in the same pass.
What stays the same. A kept tag whose row has no parent_id is still promoted to the root by the delete phase, keeping its database row and its content associations. Running the import doesn't change, because a plan with errors never runs. The one changed outcome is the kept-current-parent file that the Description covers. Three existing tests import exactly such a file and change with it.
Implementation specifics
- New plan-level error
DeletedParentError(row, parent_id)insrc/openedx_tagging/import_export/exceptions.py: aTagImportErrorsubclass likeParentCycleError, using the "Row N:" form from #892. Suggested text:Row 2: Parent tag tag_3 is deleted by this import, because the file has no row for it. Add a row for tag_3 or change the parent_id.The final wording follows the Acceptance Criteria. - New method
TagImportPlan._validate_no_parent_is_deleted(tags, parent_of)insrc/openedx_tagging/import_export/import_plan.py, called ingenerate_actionsright after_validate_hierarchy. It buildsdeletedfromself.indexed_actions["delete"], casefolded the way_end_statebuilds it, and appends one error for each row whoseparent_id.casefold()is indeletedand is not a key ofparent_of. - Row numbers: a row's number is its 1-based position in
tags, as in_validate_no_duplicate_final_idsandImportAction.row. - Leave
_validate_parentandUpdateParentTag.applies_foralone: the row that keeps its current parent never reaches_validate_parent. Turning that row into a parent change only so it can be rejected would add a meaningless "update the parent from tag_3 to tag_3" step to the plan. - No database queries: the check reads only the delete steps and
parent_of, which the plan has already built. - Comments to update:
_validate_hierarchy's docstring says that a parent_id naming a deleted tag, with no row reclaiming the id, counts as a root because the delete phase promotes the children. Make it say the new check rejects such a parent_id.- The comment in
_build_detach_actionssays such a parent_id is "left to_validate_parent". Make it say the plan rejects it.
- Contract:
import_tags(..., replace=True)keeps its signature and return value, and nothing in it is marked UNSTABLE. The PR description and thefix:commit body must call out the changed outcome for the kept-current-parent file. Imports withreplace=Falsedelete nothing, so they are unaffected. No migration. - Three replace-mode data sets change (
tests/openedx_tagging/import_export/test_import_plan.py): the replace-mode data sets oftest_generate_actions,test_planandtest_executeeach import one row, tag_4 with parent_id tag_3, and no row for tag_3, which this ticket rejects. Drop parent_id from that row, so the data sets keep covering a replace import that deletes tags. - Their new expected outputs: with no parent_id, the row moves tag_4 to the root, because a row without a parent_id for a tag that has a parent is a parent change.
- The last expected action in
test_generate_actionsbecomesupdate_parentfor tag_4 instead ofwithout_changes. - The last line of
test_planbecomes#5: Update the parent of <Tag> (tag_4 / Tag 4) from parent <Tag> (tag_3 / Tag 3) to None. - In
test_execute, remove theif not replaceguard so the replace-mode case checks parents too; that guard is what hid this behavior.
- The last expected action in
- New unit tests in
test_import_plan.py: a moved tag under the deleted tag; a kept tag naming its current, deleted parent, spelledtag_3andTAG_3; a new tag and a renamed tag under the deleted tag; two offending rows reported in one pass. - New and extended acceptance tests in
tests/openedx_tagging/import_export/test_api.py:- Add the rejection scenarios to
TestImportAcceptanceScenarios, using_assert_rejectedacross_orders. - Extend
test_child_of_deleted_holder_moves_under_tag_renamed_onto_its_idwith a child that names the id in another case. - Add a content-association check to the case where a kept child becomes a root.
- Add the rejection scenarios to
- Out of scope:
- The double move to the root of a kept child whose row has no parent_id. The delete phase and the row each build one; this already happens today and is noted in the
generate_actionsTODO. - The error-text changes in #892.
- The double move to the root of a kept child whose row has no parent_id. The delete phase and the row each build one; this already happens today and is noted in the
Files to create and modify
Modified files
| File | Nature of modification |
|---|---|
| src/openedx_tagging/import_export/exceptions.py | add DeletedParentError |
| src/openedx_tagging/import_export/import_plan.py | add _validate_no_parent_is_deleted, call it in generate_actions; update two comments |
| tests/openedx_tagging/import_export/test_import_plan.py | update three replace-mode data sets and the test_execute guard; add rejection tests |
| tests/openedx_tagging/import_export/test_api.py | add acceptance scenarios; extend two existing acceptance tests |
Context
- Builds on the #858 pull request, whose final-tree computation (
TagImportPlan._end_state) this check reads; start after it merges. - Land this ticket after #892, whose "Row N:" form the new error follows.
- The Studio import endpoints in
src/openedx_tagging/rest_api/v1/views.pyalways callimport_tags(..., replace=True). - Prior art:
_validate_no_duplicate_final_idsand_end_stateinsrc/openedx_tagging/import_export/import_plan.py;ParentCycleErrorinsrc/openedx_tagging/import_export/exceptions.py. test_child_of_deleted_holder_moves_under_tag_renamed_onto_its_idintests/openedx_tagging/import_export/test_api.pyis the valid case that must keep passing.- #673 (previous_id); epic #720.
Open Questions
- [non-blocking] Is rejecting the kept-current-parent file a bug fix that only needs a callout in the PR description and the
fix:commit body, rather than a DEPR? Nothing inimport_tagsis marked UNSTABLE, and a file that imported successfully before is now rejected. Options: A) callout in the PR description and the commit body; B) open a DEPR ticket first. Recommended: A. Owner: maintainer.
- 主要言語
- Python
- スター
- 10
- フォーク
- 33
- 平均マージ
- 2日 23時間
- マージ済み PR(30日)
- 22
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
openedx/openedx-core のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
openedx/openedx-core#831 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
openedx/openedx-core#827 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 20/100
openedx/openedx-core#895 ·
メンテナーはふだん 1 日以内に返信
-
[Draft] Refactor: Consolidate taxonomy, tag and object-tag writes into one module対応中かも @jesperhodge が 1 日前に担当しました。 オープン
openedx/openedx-core#894 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Tag import: parent errors name their row and report every cycle as a cycle対応中かも @ufedaseyeuconsultant が 1 日前に担当しました。 オープン
openedx/openedx-core#892 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
openedx/openedx-core の issue をすべて見る
似ている issue
-
bug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
yunaremaia/proto-drift#22 ·
-
bug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
yunaremaia/memwatch#96 ·
-
bug good first issue
難易度 1/5 1時間未満 初心者へのやさしさ 91/100
yunaremaia/ci-gate-watch#34 ·
-
bug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
yunaremaia/agent-capability-attestation#64 ·
メンテナーはふだん 1 日以内に返信
-
bug good first issue
難易度 1/5 1時間未満 初心者へのやさしさ 86/100
yunaremaia/agent-call-graph#43 ·
メンテナーはふだん 1 日以内に返信