instructure/canvas-lms · error

timetable codes can't be blank

Error message

timetable codes can't be blank

What it means

Courses::TimetableEventBuilder#create_or_update_events uses each event hash's :code as the unique timetable_code for syncing (creating/deleting calendar events). If any provided hash has a blank code, the sync keys are ambiguous, so it raises 'timetable codes can't be blank'.

Solutions

  1. Ensure every event hash includes a non-blank :code before calling create_or_update_events.
  2. Generate/derive codes at the data-source level (e.g. from day+start+end) for rows missing them.
  3. Filter out or repair rows with blank codes and report them to the source-data owner.

Example fix

// before
builder.create_or_update_events(rows.map { |r| {start_at: r.start, end_at: r.fin} })

// after
events = rows.map { |r| {code: r.code.presence || "#{r.day}-#{r.start}", start_at: r.start, end_at: r.fin} }
builder.create_or_update_events(events)
Defensive patterns

Strategy: validation

Validate before calling

blank = event_hashes.select { |h| h[:code].blank? }
raise 'blank codes: #{blank.size}' if blank.any?

Type guard

def all_coded?(event_hashes)
  event_hashes.all? { |h| h[:code].present? }
end

Prevention

When it happens

Trigger: Calling builder.create_or_update_events(event_hashes) where any hash omits :code or passes an empty string/nil code; rows from a source timetable missing the identifier column.

Common situations: CSV/timetable-import rows with a missing identifier cell; upstream data changes dropping the code field; callers assuming codes are auto-generated — generation only happens internally per event, not for blank inputs here.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at app/models/courses/timetable_event_builder.rb:79

              event_hash = { start_at: event_start_at, end_at: event_end_at }
              event_hash[:location_name] = location_name if location_name
              event_hashes << event_hash
            end
            current_date += 7 # move to next week
          end
        end
      end
      event_hashes
    end

    # expects an array of hashes
    # with :start_at, :end_at required
    # and optionally :location_name (other attributes could be added here if so desired)
    # :code can be used to give it a unique identifier for syncing (otherwise will be generated based on the times)
    # :title can be used to give a title to the event (otherwise a the name of the associated course will be used)
    def create_or_update_events(event_hashes)
      timetable_codes = event_hashes.pluck(:code)
      raise "timetable codes can't be blank" if timetable_codes.any?(&:blank?)

      # destroy unused events
      event_context.calendar_events.active.for_timetable.where.not(timetable_code: timetable_codes)
                   .update_all(workflow_state: "deleted", deleted_at: Time.now.utc)

      existing_events = event_context.calendar_events.where(timetable_code: timetable_codes).to_a.index_by(&:timetable_code)
      event_hashes.each do |event_hash|
        CalendarEvent.unique_constraint_retry do |retry_count|
          code = event_hash[:code]
          event = event_context.calendar_events.where(timetable_code: code).first if retry_count > 0
          event ||= existing_events[code] || create_new_event(event_hash)
          sync_event(event, event_hash)
        end
      end
    end

    ALLOWED_TIMETABLE_KEYS = %i[weekdays course_start_at course_end_at start_time end_time location_name].freeze
    def process_and_validate_timetables(timetable_hashes)

View on GitHub (pinned to 1c9f0bb801)