{"record":{"id":"a94ff322bacec04d","repo":"instructure/canvas-lms","slug":"timetable-codes-can-t-be-blank","errorCode":null,"errorMessage":"timetable codes can't be blank","messagePattern":"timetable codes can't be blank","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/courses/timetable_event_builder.rb","lineNumber":79,"sourceCode":"              event_hash = { start_at: event_start_at, end_at: event_end_at }\n              event_hash[:location_name] = location_name if location_name\n              event_hashes << event_hash\n            end\n            current_date += 7 # move to next week\n          end\n        end\n      end\n      event_hashes\n    end\n\n    # expects an array of hashes\n    # with :start_at, :end_at required\n    # and optionally :location_name (other attributes could be added here if so desired)\n    # :code can be used to give it a unique identifier for syncing (otherwise will be generated based on the times)\n    # :title can be used to give a title to the event (otherwise a the name of the associated course will be used)\n    def create_or_update_events(event_hashes)\n      timetable_codes = event_hashes.pluck(:code)\n      raise \"timetable codes can't be blank\" if timetable_codes.any?(&:blank?)\n\n      # destroy unused events\n      event_context.calendar_events.active.for_timetable.where.not(timetable_code: timetable_codes)\n                   .update_all(workflow_state: \"deleted\", deleted_at: Time.now.utc)\n\n      existing_events = event_context.calendar_events.where(timetable_code: timetable_codes).to_a.index_by(&:timetable_code)\n      event_hashes.each do |event_hash|\n        CalendarEvent.unique_constraint_retry do |retry_count|\n          code = event_hash[:code]\n          event = event_context.calendar_events.where(timetable_code: code).first if retry_count > 0\n          event ||= existing_events[code] || create_new_event(event_hash)\n          sync_event(event, event_hash)\n        end\n      end\n    end\n\n    ALLOWED_TIMETABLE_KEYS = %i[weekdays course_start_at course_end_at start_time end_time location_name].freeze\n    def process_and_validate_timetables(timetable_hashes)","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/courses/timetable_event_builder.rb#L61-L97","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure every event hash includes a non-blank :code before calling create_or_update_events.","Generate/derive codes at the data-source level (e.g. from day+start+end) for rows missing them.","Filter out or repair rows with blank codes and report them to the source-data owner."],"exampleFix":"// before\nbuilder.create_or_update_events(rows.map { |r| {start_at: r.start, end_at: r.fin} })\n\n// after\nevents = rows.map { |r| {code: r.code.presence || \"#{r.day}-#{r.start}\", start_at: r.start, end_at: r.fin} }\nbuilder.create_or_update_events(events)","handlingStrategy":"validation","validationCode":"blank = event_hashes.select { |h| h[:code].blank? }\nraise 'blank codes: #{blank.size}' if blank.any?","typeGuard":"def all_coded?(event_hashes)\n  event_hashes.all? { |h| h[:code].present? }\nend","tryCatchPattern":null,"preventionTips":["Validate imported timetable rows before building event hashes","Default-generate codes from day/start/end when the source omits them","Log and quarantine rows with missing identifiers instead of passing them through"],"tags":["ruby-on-rails","timetables","calendar-events","validation"],"backgroundTag":"empty-required-field","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}