instructure/canvas-lms · error · ImportError

Cannot move # # because it has # in it.

Error message

Cannot move #{type_display} #{sis_id} because it has #{item_type} in it.

What it means

check_context_movement guards against moving a group category / differentiation tag set to a different context while it still contains active groups (or tags). Such a move would orphan or mis-scope the child groups, so the importer raises ImportError instead of silently relocating them.

Solutions

  1. Delete the active groups/tags in the category first (or import their deletion), then move the category.
  2. Keep the category's context (account_id/course_id) unchanged in the CSV if it already exists with groups.
  3. Use a new sis_id for the category in the new context instead of moving the existing one.
  4. Manually move groups in the UI/API before changing the category's context in SIS.

Example fix

// before (category gc-1 has groups, moving from COURSE-42 to COURSE-7)
gc-1,Math Groups,,COURSE-7,active
// after (keep original context, or delete groups first)
gc-1,Math Groups,,COURSE-42,active
Defensive patterns

Strategy: validation

Validate before calling

if category && category.groups.active.exists?
  moving = category.context_id != context.id ||
           category.context_type != context.class.base_class.name
  raise ArgumentError, "cannot move #{category.sis_source_id}: it still has active groups" if moving
end

Try / catch

begin
  importer.add_group_category(sis_id, acct_id, course_id, name, status)
rescue SIS::GroupCategoryImporter::ImportError => e
  if e.message =~ /Cannot move/ then flag_for_manual_review(sis_id)
  else raise end
end

Prevention

When it happens

Trigger: add_group_category / add_differentiation_tag_set imports a sis_id that already exists (category.groups.active.exists?) but whose context_id/context_type differs from the resolved context of the current row — e.g., the row now names a different course or account.

Common situations: Changing a category's course_id or account_id in the CSV after groups were created inside it; re-parenting a tag set to another course while it still has tags; merged/re-imported CSVs where the same sis_id maps to two different courses.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/070993dca2f5c9a5. Report an issue: GitHub.

Appendix: source

Thrown at lib/sis/group_category_importer.rb:87

          context = @courses_cache[course_id]
          context ||= @root_account.all_courses.active.find_by(sis_source_id: course_id)
          raise ImportError, "Course with id \"#{course_id}\" didn't exist for #{type_display} #{sis_id}" unless context

          @courses_cache[context.sis_source_id] = context
        elsif course_id.nil? && type_display == "differentiation tag set"
          raise ImportError, "No course_id given for #{type_display} #{sis_id}"
        end

        context || @root_account
      end

      def check_context_movement(category, context, sis_id, type_display)
        if category && category.groups.active.exists? &&
           !(context.id == category.context_id && context.class.base_class.name == category.context_type)

          item_type = type_display.include?("tag") ? "tags" : "groups"

          raise ImportError, "Cannot move #{type_display} #{sis_id} because it has #{item_type} in it."
        end
      end

      def apply_status(category, status)
        case status
        when /active/i
          category.deleted_at = nil
        when /deleted/i
          category.deleted_at = Time.zone.now
        end
      end

      def save_category(category, sis_id, type_display)
        if category.save
          data = build_data(category)
          @roll_back_data << data if data
          @success_count += 1
          true

View on GitHub (pinned to 1c9f0bb801)