Skip to content
Draft
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
20 changes: 19 additions & 1 deletion src/openedx_tagging/import_export/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def _validate_parent(self, indexed_actions) -> ImportActionError | None:
action=self,
message=_(
"Unknown parent tag ({parent_id}). "
"You need to add parent before the child in your file."
"Add a row for the parent to your file."
).format(parent_id=self.tag.parent_id),
)
return None
Expand Down Expand Up @@ -343,12 +343,30 @@ def applies_for(cls, taxonomy: Taxonomy, tag: TagItem, indexed_actions=None) ->
"""
This action applies whenever there is a change on the parent.

A row whose parent_id equals the current parent's external_id is "no
change" and skips `_validate_parent`. TagImportPlan validates the
invalid cases of such rows itself: a parent renamed away, and parent
cycles. The exception is a parent that this import deletes while a
rename reclaims its id: the row applies, to move the child under the
renamed tag.

See ImportAction._resolve_update_target for when this doesn't apply
regardless of the parent change (a rename_external_id row).
"""
taxonomy_tag = cls._resolve_update_target(taxonomy, tag)
if taxonomy_tag is None:
return False
parent = taxonomy_tag.parent
# The parent is deleted by this import and a rename reclaims its id, so
# the child must move under the renamed tag instead of being promoted.
if (
parent is not None
and parent.external_id == tag.parent_id
and indexed_actions
and any(a.tag.id.casefold() == parent.external_id.casefold() for a in indexed_actions["delete"])
and any(a.tag.id.casefold() == parent.external_id.casefold() for a in indexed_actions["rename_external_id"])
):
return True
return (
taxonomy_tag.parent is not None
and taxonomy_tag.parent.external_id != tag.parent_id
Expand Down
49 changes: 49 additions & 0 deletions src/openedx_tagging/import_export/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,52 @@ class InvalidCSVField(TagParserError):
def __init__(self, tag: dict | None, field: str, row: int, **kargs):
super().__init__(tag, **kargs)
self.message = _("Invalid '{field}' field on the row {row}").format(field=field, row=row)


class ParentIdRenamedAwayError(TagImportError):
"""
Exception raised when a row's parent_id is its current parent's pre-import
id, and the same import renames that parent to a different id.

After the import that id names no tag or, after a swap of ids, a different
tag, so the message asks for the parent's new id instead. ``row`` is the
1-based position of the row among the file's data rows: row 1 is the first
row after a CSV header, or the first entry of a JSON file's ``tags``.
"""

def __init__(self, row: int, parent_id: str, new_id: str, **kargs):
super().__init__(**kargs)
self.message = _(
"Row {row}'s parent_id ({parent_id}) names an id this file renames away. "
"Use '{new_id}' as the parent_id."
).format(row=row, parent_id=parent_id, new_id=new_id)


class ParentCycleError(TagImportError):
"""
Exception raised when the parent_id of a row makes the tag its own
ancestor once the import is applied.

``row`` is numbered like in ``ParentIdRenamedAwayError``.
"""

def __init__(self, row: int, parent_id: str, **kargs):
super().__init__(**kargs)
self.message = _(
"Row {row}'s parent_id ({parent_id}) creates a parent cycle."
).format(row=row, parent_id=parent_id)


class InvalidParentHierarchyError(TagImportError):
"""
Exception raised when applying the import would build a tag tree that is too
deep or otherwise invalid, even when the importer first moves every tag whose
parent changes to the root.
"""

def __init__(self, max_levels: int, **kargs):
super().__init__(**kargs)
self.message = _(
"This import would leave tags deeper than {max_levels} levels, or build an invalid "
"parent hierarchy, so it cannot be applied."
).format(max_levels=max_levels)
Loading
Loading