instructure/canvas-lms · error

Course required

Error message

Course required

What it means

CourseSection#infer_defaults sets root_account_id and derives section names/sis defaults from the parent course; it requires the course association to be set and raises 'Course required' otherwise. Called from before_validation/before_save during section creation.

Solutions

  1. Assign section.course = the Course (or course_id) before saving so infer_defaults has its parent.
  2. Build sections via course.course_sections.create instead of standalone CourseSection.new.
  3. Reorder code so the course association is set before any save/validation runs.

Example fix

// before
section = CourseSection.new(name: 'Section A')
section.save!

// after
section = course.course_sections.new(name: 'Section A')
section.save!
Defensive patterns

Strategy: type-guard

Validate before calling

raise 'course missing' unless section.course || section.course_id

Type guard

def section_has_course?(section)
  section.course.present? || section.course_id.present?
end

Prevention

When it happens

Trigger: Creating or saving a CourseSection without assigning section.course = course first; bulk-importing sections without linking them to a course; SIS import code paths that build sections detached from their course.

Common situations: Hand-constructed CourseSection.new(...) in scripts/console missing the course; order-of-operations bug where course is assigned after validation triggers; importers creating sections per row but forgetting the FK on malformed rows.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at app/models/course_section.rb:244

    return true if !root_account_id_changed? && !integration_id_changed?

    scope = root_account.course_sections.where(integration_id:)
    scope = scope.where("id<>?", self) unless new_record?

    return true unless scope.exists?

    errors.add(:integration_id, t("integration_id_taken", "INTEGRATION ID \"%{integration_id}\" is already in use", integration_id:))
    throw :abort
  end

  alias_method :parent_event_context, :course

  def section_code
    name
  end

  def infer_defaults
    raise "Course required" unless course

    self.root_account_id = course.root_account_id || Account.default.id
    # This is messy, and I hate it.
    # The SIS import actually gives us three names for a section
    #   and I don't know which one is best, or which one to show.
    # Here's the current plan:
    # - otherwise, just use name
    # - use the method display_name to consolidate this logic
    self.name ||= course.name if default_section
    self.name ||= "#{course.name} #{Time.zone.today}"
  end

  def defined_by_sis?
    !!sis_source_id
  end

  # NOTE: Don't assume the section_name contains the course name
  # it might include it if the SIS specifies, but you shouldn't

View on GitHub (pinned to 1c9f0bb801)