getCourseOffering method

Future<CourseOfferingDetail?> getCourseOffering(
  1. String number
)

Reads number's offering detail (overview + schedule + teachers + classes), falling back to the course system when it is not cached.

Not a refreshX — a looked-up offering is not cached, so the detail is returned rather than written for a stream to re-emit, and only its semester row is persisted. Submitted syllabuses are fetched separately via watchSyllabuses. Returns null only when the course system reports that the number does not exist.

Course numbers are globally unique and identify at most one offering.

Implementation

Future<CourseOfferingDetail?> getCourseOffering(String number) async {
  final overviews = _database.courseOfferingOverviews;
  final cached =
      await (_database.select(overviews).join([
              innerJoin(
                _database.semesters,
                _database.semesters.id.equalsExp(overviews.semester),
              ),
            ])
            ..where(overviews.number.equals(number))
            ..orderBy([
              .desc(_database.semesters.year),
              .desc(_database.semesters.term),
            ])
            ..limit(1))
          .getSingleOrNull();

  if (cached?.readTable(overviews) case final overview?) {
    final offeringId = overview.id;
    final (schedule, teachers, classes) = await (
      _readOfferingSchedule(offeringId),
      _readOfferingTeachers(offeringId),
      _readOfferingClasses(offeringId),
    ).wait;

    return (
      overview: overview,
      schedule: schedule,
      teachers: teachers,
      classes: classes,
    );
  }

  final dto = await _authRepository.withAuth(
    () => _courseService.getCourseOffering(number),
    sso: [.courseService],
  );
  if (dto == null) return null;

  final semester = await _database.getOrCreateSemester(
    dto.semester.year!,
    dto.semester.term!,
  );

  final schedule = dto.schedule;
  return (
    // TODO: sentinel id because the offering itself is not persisted, so it
    // has no row to report. No caller reads `overview.id` — give
    // CourseOfferingDetail a DTO-backed variant if that stops being true.
    overview: CourseOfferingOverview(
      id: -1,
      courseCode: schedule.course?.id,
      semester: semester.id,
      number: schedule.number,
      nameZh: schedule.course?.nameZh,
      nameEn: schedule.course?.nameEn,
      credits: schedule.credits,
      hours: schedule.hours,
      phase: schedule.phase,
      courseType: dto.courseType,
      status: schedule.status,
      language: schedule.language,
      remarks: schedule.remarks,
      enrolled: dto.enrolled,
      withdrawn: dto.withdrawn,
      fetchedAt: DateTime.now(),
    ),
    schedule: [
      for (final slot
          in schedule.schedule ??
              const <
                ({
                  DayOfWeek day,
                  Period period,
                  ReferenceDto? classroom,
                })
              >[])
        (
          day: slot.day,
          period: slot.period,
          classroomNameZh: slot.classroom?.name,
          classroomNameEn: switch (slot.classroom?.name) {
            final name? => translateClassroomName(name),
            null => null,
          },
        ),
    ],
    teachers: [
      for (final teacher in schedule.teachers ?? const <LocalizedRefDto>[])
        if (teacher case LocalizedRefDto(:final id?, :final nameZh?))
          (code: id, nameZh: nameZh, nameEn: teacher.nameEn),
    ],
    classes: [
      for (final entity in schedule.classes ?? const <LocalizedRefDto>[])
        if (entity case LocalizedRefDto(:final id?, :final nameZh?))
          (code: id, nameZh: nameZh, nameEn: entity.nameEn),
    ],
  );
}