diff --git a/handler/api/rest/v3/registered_course.go b/handler/api/rest/v3/registered_course.go index e80b7bf..24c9954 100644 --- a/handler/api/rest/v3/registered_course.go +++ b/handler/api/rest/v3/registered_course.go @@ -2,7 +2,6 @@ package restv3 import ( "context" - "fmt" "github.com/labstack/echo/v4" "github.com/samber/lo" @@ -14,7 +13,7 @@ import ( timetabledomain "github.com/twin-te/twinte-back/module/timetable/domain" ) -func toApiRegisteredCourse(registeredCourse *timetabledomain.RegisteredCourse, idToCourse map[idtype.CourseID]*timetabledomain.Course) (ret openapi.RegisteredCourse, err error) { +func toApiRegisteredCourse(registeredCourse *timetabledomain.RegisteredCourse) (ret openapi.RegisteredCourse, err error) { ret = openapi.RegisteredCourse{ Absence: int(registeredCourse.Absence), Attendance: int(registeredCourse.Attendance), @@ -31,8 +30,8 @@ func toApiRegisteredCourse(registeredCourse *timetabledomain.RegisteredCourse, i Year: registeredCourse.Year.Int(), } - if registeredCourse.CourseID != nil { - course, err := toApiCourse(idToCourse[*registeredCourse.CourseID]) + if registeredCourse.HasBasedCourse() { + course, err := toApiCourse(registeredCourse.CourseAssociation.MustGet()) if err != nil { return openapi.RegisteredCourse{}, err } @@ -80,7 +79,7 @@ func (h *impl) GetRegisteredCourses(ctx context.Context, request openapi.GetRegi return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, registeredCourses) + apiRegisteredCourses, err := base.MapWithErr(registeredCourses, toApiRegisteredCourse) if err != nil { return } @@ -106,7 +105,7 @@ func (h *impl) postRegisteredCourses0(ctx context.Context, reqBody openapi.PostR return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, registeredCourses) + apiRegisteredCourses, err := base.MapWithErr(registeredCourses, toApiRegisteredCourse) if err != nil { return } @@ -142,7 +141,7 @@ func (h *impl) postRegisteredCourses1(ctx context.Context, reqBody openapi.PostR registeredCourses = append(registeredCourses, rcs...) } - return h.getApiRegisteredCourses(ctx, registeredCourses) + return base.MapWithErr(registeredCourses, toApiRegisteredCourse) } func (h *impl) postRegisteredCourses2(ctx context.Context, reqBody openapi.PostRegisteredCoursesJSONBody2) (apiRegisteredCourse openapi.RegisteredCourse, err error) { @@ -180,7 +179,7 @@ func (h *impl) postRegisteredCourses2(ctx context.Context, reqBody openapi.PostR return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, []*timetabledomain.RegisteredCourse{registeredCourse}) + apiRegisteredCourses, err := base.MapWithErr([]*timetabledomain.RegisteredCourse{registeredCourse}, toApiRegisteredCourse) if err != nil { return } @@ -249,7 +248,7 @@ func (h *impl) GetRegisteredCoursesId(ctx context.Context, request openapi.GetRe return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, []*timetabledomain.RegisteredCourse{registeredCourse}) + apiRegisteredCourses, err := base.MapWithErr([]*timetabledomain.RegisteredCourse{registeredCourse}, toApiRegisteredCourse) if err != nil { return } @@ -330,7 +329,7 @@ func (h *impl) PutRegisteredCoursesId(ctx context.Context, request openapi.PutRe return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, []*timetabledomain.RegisteredCourse{registeredCourse}) + apiRegisteredCourses, err := base.MapWithErr([]*timetabledomain.RegisteredCourse{registeredCourse}, toApiRegisteredCourse) if err != nil { return } @@ -339,26 +338,3 @@ func (h *impl) PutRegisteredCoursesId(ctx context.Context, request openapi.PutRe return } - -func (h *impl) getApiRegisteredCourses(ctx context.Context, registeredCourses []*timetabledomain.RegisteredCourse) ([]openapi.RegisteredCourse, error) { - courseIDs := make([]idtype.CourseID, 0, len(registeredCourses)) - for _, registeredCourse := range registeredCourses { - if registeredCourse.CourseID != nil { - courseIDs = append(courseIDs, *registeredCourse.CourseID) - } - } - - courses, err := h.timetableUseCase.GetCoursesByIDs(ctx, courseIDs) - if err != nil { - return nil, err - } - if len(courseIDs) != len(courses) { - return nil, fmt.Errorf("not found courses in getApiRegisteredCourses %+v", courseIDs) - } - - idToCourse := lo.SliceToMap(courses, func(course *timetabledomain.Course) (idtype.CourseID, *timetabledomain.Course) { - return course.ID, course - }) - - return base.MapWithArgAndErr(registeredCourses, idToCourse, toApiRegisteredCourse) -} diff --git a/handler/api/rest/v3/timetable.go b/handler/api/rest/v3/timetable.go index 54493fd..4ac8f56 100644 --- a/handler/api/rest/v3/timetable.go +++ b/handler/api/rest/v3/timetable.go @@ -16,7 +16,7 @@ import ( func (h *impl) GetTimetableDate(ctx context.Context, request openapi.GetTimetableDateRequestObject) (res openapi.GetTimetableDateResponseObject, err error) { date := civil.DateOf(request.Date.Time) - year, err := shareddomain.NewAcademicYear(date.Year, date.Month) + year, err := shareddomain.NewAcademicYearFromDate(date) if err != nil { return } @@ -54,7 +54,7 @@ func (h *impl) GetTimetableDate(ctx context.Context, request openapi.GetTimetabl return } - apiRegisteredCourses, err := h.getApiRegisteredCourses(ctx, registeredCourses) + apiRegisteredCourses, err := base.MapWithErr(registeredCourses, toApiRegisteredCourse) if err != nil { return } diff --git a/module/announcement/domain/announcement.go b/module/announcement/domain/announcement.go index d4d5046..eb66bbc 100644 --- a/module/announcement/domain/announcement.go +++ b/module/announcement/domain/announcement.go @@ -28,7 +28,7 @@ func ParseAnnouncementTag(s string) (AnnouncementTag, error) { if ok { return ret, nil } - return AnnouncementTag(0), fmt.Errorf("failed to parse AnnouncementTag %#v", s) + return 0, fmt.Errorf("failed to parse AnnouncementTag %#v", s) } var ( diff --git a/module/shared/domain/association.go b/module/shared/domain/association.go new file mode 100644 index 0000000..5a71c09 --- /dev/null +++ b/module/shared/domain/association.go @@ -0,0 +1,27 @@ +package shareddomain + +import "github.com/samber/mo" + +type Association[T any] struct { + v mo.Option[T] +} + +func (a *Association[T]) IsPresent() bool { + return a.v.IsPresent() +} + +func (a *Association[T]) IsAbsent() bool { + return a.v.IsAbsent() +} + +func (a *Association[T]) Get() (T, bool) { + return a.v.Get() +} + +func (a *Association[T]) MustGet() T { + return a.v.MustGet() +} + +func (a *Association[T]) Set(v T) { + a.v = mo.Some(v) +} diff --git a/module/timetable/domain/registered_course.go b/module/timetable/domain/registered_course.go index dc1fdc5..aa0eef5 100644 --- a/module/timetable/domain/registered_course.go +++ b/module/timetable/domain/registered_course.go @@ -17,9 +17,28 @@ var ( // RegisteredCourse is identified by one of the following fields. // - ID -// - UserID and CourseID ( if CourseID is not nil ) +// - UserID and CourseID ( if it has based course ) // -// If CourseID is nil, Name, Instructors, Credit, Methods, Schedules are required. +// There are two types of RegisteredCourse. +// - RegisteredCourse created manually +// - RegisteredCourse that has the based course +// +// If RegisteredCourse has the based course, the following fields are always present. +// - CourseID +// +// And the following fields are present only if overwritten. +// - Name +// - Instructors +// - Credit +// - Methods +// - Schedules +// +// If RegisteredCourse is created manually, the following fields are always present. +// - Name +// - Instructors +// - Credit +// - Methods +// - Schedules type RegisteredCourse struct { ID idtype.RegisteredCourseID UserID idtype.UserID @@ -37,6 +56,47 @@ type RegisteredCourse struct { TagIDs []idtype.TagID EntityBeforeUpdated *RegisteredCourse + + CourseAssociation shareddomain.Association[*Course] +} + +func (rc *RegisteredCourse) HasBasedCourse() bool { + return rc.CourseID != nil +} + +func (rc *RegisteredCourse) GetName() shareddomain.RequiredString { + if rc.HasBasedCourse() { + return lo.FromPtrOr(rc.Name, rc.CourseAssociation.MustGet().Name) + } + return *rc.Name +} + +func (rc *RegisteredCourse) GetInstructors() string { + if rc.HasBasedCourse() { + return lo.FromPtrOr(rc.Instructors, rc.CourseAssociation.MustGet().Instructors) + } + return *rc.Instructors +} + +func (rc *RegisteredCourse) GetCredit() Credit { + if rc.HasBasedCourse() { + return lo.FromPtrOr(rc.Credit, rc.CourseAssociation.MustGet().Credit) + } + return *rc.Credit +} + +func (rc *RegisteredCourse) GetMethods() []CourseMethod { + if rc.HasBasedCourse() { + return lo.FromPtrOr(rc.Methods, rc.CourseAssociation.MustGet().Methods) + } + return *rc.Methods +} + +func (rc *RegisteredCourse) GetSchedules() []Schedule { + if rc.HasBasedCourse() { + return lo.FromPtrOr(rc.Schedules, rc.CourseAssociation.MustGet().Schedules) + } + return *rc.Schedules } func (rc *RegisteredCourse) Clone() *RegisteredCourse { @@ -88,25 +148,60 @@ type RegisteredCourseDataToUpdate struct { TagIDs *[]idtype.TagID } +func (rc *RegisteredCourse) updateName(name shareddomain.RequiredString) { + if rc.HasBasedCourse() && rc.Name == nil && rc.CourseAssociation.MustGet().Name == name { + return + } + rc.Name = &name +} + +func (rc *RegisteredCourse) updateInstructors(instructors string) { + if rc.HasBasedCourse() && rc.Instructors == nil && rc.CourseAssociation.MustGet().Instructors == instructors { + return + } + rc.Instructors = &instructors +} + +func (rc *RegisteredCourse) updateCredit(credit Credit) { + if rc.HasBasedCourse() && rc.Credit == nil && rc.CourseAssociation.MustGet().Credit == credit { + return + } + rc.Credit = &credit +} + +func (rc *RegisteredCourse) updateMethods(methods []CourseMethod) { + // if rc.HasBasedCourse() && rc.Methods == nil && rc.CourseAssociation.MustGet().Methods == methods { + // return + // } + rc.Methods = &methods +} + +func (rc *RegisteredCourse) updateSchedules(schedules []Schedule) { + // if rc.HasBasedCourse() && rc.Schedules == nil && rc.CourseAssociation.MustGet().Schedules == schedules { + // return + // } + rc.Schedules = &schedules +} + func (rc *RegisteredCourse) Update(data RegisteredCourseDataToUpdate) error { if data.Name != nil { - rc.Name = data.Name + rc.updateName(*data.Name) } if data.Instructors != nil { - rc.Instructors = data.Instructors + rc.updateInstructors(*data.Instructors) } if data.Credit != nil { - rc.Credit = data.Credit + rc.updateCredit(*data.Credit) } if data.Methods != nil { - rc.Methods = data.Methods + rc.updateMethods(*data.Methods) } if data.Schedules != nil { - rc.Schedules = data.Schedules + rc.updateSchedules(*data.Schedules) } if data.Memo != nil { @@ -139,7 +234,7 @@ func ConstructRegisteredCourse(fn func(rc *RegisteredCourse) (err error)) (*Regi } if rc.CourseID == nil && (rc.Name == nil || rc.Instructors == nil || rc.Credit == nil || rc.Methods == nil || rc.Schedules == nil) { - return nil, fmt.Errorf("the registered course, which does not have course id, must have name, instructors, credit, methods, and schedules. %+v", rc) + return nil, fmt.Errorf("the registered course, which does not have the based course, must have name, instructors, credit, methods, and schedules. %+v", rc) } if rc.ID.IsZero() || rc.UserID.IsZero() || rc.Year.IsZero() { diff --git a/module/timetable/factory/impl.go b/module/timetable/factory/impl.go index e2a4125..23a41d4 100644 --- a/module/timetable/factory/impl.go +++ b/module/timetable/factory/impl.go @@ -42,6 +42,7 @@ func (f *impl) NewRegisteredCourseFromCourse(userID idtype.UserID, course *timet rc.UserID = userID rc.CourseID = &course.ID rc.Year = course.Year + rc.CourseAssociation.Set(course) return nil }) } diff --git a/module/timetable/module.go b/module/timetable/module.go index e3a3b92..d1f7c78 100644 --- a/module/timetable/module.go +++ b/module/timetable/module.go @@ -30,6 +30,7 @@ type UseCase interface { SearchCourses(ctx context.Context, in SearchCoursesIn) ([]*timetabledomain.Course, error) // CreateRegisteredCoursesByCodes creates new registered courses by the given year and codes. + // And it returns the registered courses, each of which has the course association loaded if it has the based course. // // [Authentication] required // @@ -39,21 +40,25 @@ type UseCase interface { CreateRegisteredCoursesByCodes(ctx context.Context, year shareddomain.AcademicYear, codes []timetabledomain.Code) ([]*timetabledomain.RegisteredCourse, error) // CreateRegisteredCourseManually creates a new registered course mannually. + // And it returns the registered course, which has the course association loaded if it has the based course. // // [Authentication] required CreateRegisteredCourseManually(ctx context.Context, in CreateRegisteredCourseManuallyIn) (*timetabledomain.RegisteredCourse, error) // GetRegisteredCourseByID returns the registered course specified by the given id. + // And it returns the registered course, which has the course association loaded if it has the based course. // // [Authentication] required GetRegisteredCourseByID(ctx context.Context, id idtype.RegisteredCourseID) (*timetabledomain.RegisteredCourse, error) // GetRegisteredCourses returns the registered courses. + // And it returns the registered courses, each of which has the course association loaded if it has the based course. // // [Authentication] required GetRegisteredCourses(ctx context.Context, year *shareddomain.AcademicYear) ([]*timetabledomain.RegisteredCourse, error) // UpdateRegisteredCourse updates registered course specified by the given id. + // And it returns the registered course, which has the course association loaded if it has the based course. // // [Authentication] required // diff --git a/module/timetable/port/repository.go b/module/timetable/port/repository.go index 8b46f84..391178d 100644 --- a/module/timetable/port/repository.go +++ b/module/timetable/port/repository.go @@ -29,6 +29,8 @@ type Repository interface { UpdateRegisteredCourse(ctx context.Context, registeredCourse *timetabledomain.RegisteredCourse) error DeleteRegisteredCourses(ctx context.Context, conds DeleteRegisteredCoursesConds) (rowsAffected int, err error) + LoadCourseToRegisteredCourse(ctx context.Context, registeredCourses []*timetabledomain.RegisteredCourse, lock sharedport.Lock) error + // Tag FindTag(ctx context.Context, conds FindTagConds, lock sharedport.Lock) (*timetabledomain.Tag, error) diff --git a/module/timetable/repository/registered_course.go b/module/timetable/repository/registered_course.go index f8c1241..99060db 100644 --- a/module/timetable/repository/registered_course.go +++ b/module/timetable/repository/registered_course.go @@ -166,6 +166,34 @@ func (r *impl) DeleteRegisteredCourses(ctx context.Context, conds timetableport. return int(db.Delete(&model.RegisteredCourse{}).RowsAffected), db.Error } +func (r *impl) LoadCourseToRegisteredCourse(ctx context.Context, registeredCourses []*timetabledomain.RegisteredCourse, lock sharedport.Lock) error { + courseIDToRegisteredCourse := make(map[idtype.CourseID]*timetabledomain.RegisteredCourse, len(registeredCourses)) + for _, registeredCourse := range registeredCourses { + if registeredCourse.HasBasedCourse() && registeredCourse.CourseAssociation.IsAbsent() { + courseIDToRegisteredCourse[*registeredCourse.CourseID] = registeredCourse + } + } + + courses, err := r.ListCourses(ctx, timetableport.ListCoursesConds{ + IDs: lo.ToPtr(lo.Keys(courseIDToRegisteredCourse)), + }, lock) + if err != nil { + return err + } + + for _, course := range courses { + courseIDToRegisteredCourse[course.ID].CourseAssociation.Set(course) + } + + for courseID, registeredCourse := range courseIDToRegisteredCourse { + if registeredCourse.CourseAssociation.IsAbsent() { + return fmt.Errorf("can't load course (%s) to registered course (%s)", courseID, registeredCourse.ID) + } + } + + return nil +} + func fromDBRegisteredCourse(dbRegisteredCourse *model.RegisteredCourse) (*timetabledomain.RegisteredCourse, error) { return timetabledomain.ConstructRegisteredCourse(func(registeredCourse *timetabledomain.RegisteredCourse) (err error) { registeredCourse.ID, err = idtype.ParseRegisteredCourseID(dbRegisteredCourse.ID) diff --git a/module/timetable/usecase/registered_course.go b/module/timetable/usecase/registered_course.go index 43022f5..bbecc41 100644 --- a/module/timetable/usecase/registered_course.go +++ b/module/timetable/usecase/registered_course.go @@ -59,14 +59,14 @@ func (uc *impl) CreateRegisteredCoursesByCodes(ctx context.Context, year sharedd return nil, apperr.New(sharederr.CodeAlreadyExists, msg) } - codeToCourseMap := lo.SliceToMap(courses, func(course *timetabledomain.Course) (timetabledomain.Code, *timetabledomain.Course) { + codeToCourse := lo.SliceToMap(courses, func(course *timetabledomain.Course) (timetabledomain.Code, *timetabledomain.Course) { return course.Code, course }) registeredCourses := make([]*timetabledomain.RegisteredCourse, 0, len(codes)) for _, code := range codes { - course, ok := codeToCourseMap[code] + course, ok := codeToCourse[code] if !ok { return nil, apperr.New(sharederr.CodeNotFound, fmt.Sprintf("not found course with code %s", code)) } @@ -115,10 +115,14 @@ func (uc impl) GetRegisteredCourseByID(ctx context.Context, id idtype.Registered ID: id, UserID: &userID, }, sharedport.LockNone) - if errors.Is(err, sharedport.ErrNotFound) { - return nil, apperr.New(sharederr.CodeNotFound, "") + if err != nil { + if errors.Is(err, sharedport.ErrNotFound) { + return nil, apperr.New(sharederr.CodeNotFound, fmt.Sprintf("not found registered course whose id is %s", registeredCourse.ID)) + } + return nil, err } - return registeredCourse, err + + return registeredCourse, uc.r.LoadCourseToRegisteredCourse(ctx, []*timetabledomain.RegisteredCourse{registeredCourse}, sharedport.LockNone) } func (uc impl) GetRegisteredCourses(ctx context.Context, year *shareddomain.AcademicYear) ([]*timetabledomain.RegisteredCourse, error) { @@ -127,10 +131,15 @@ func (uc impl) GetRegisteredCourses(ctx context.Context, year *shareddomain.Acad return nil, err } - return uc.r.ListRegisteredCourses(ctx, timetableport.ListRegisteredCoursesConds{ + registeredCourses, err := uc.r.ListRegisteredCourses(ctx, timetableport.ListRegisteredCoursesConds{ UserID: &userID, Year: year, }, sharedport.LockNone) + if err != nil { + return nil, err + } + + return registeredCourses, uc.r.LoadCourseToRegisteredCourse(ctx, registeredCourses, sharedport.LockNone) } func (uc impl) UpdateRegisteredCourse(ctx context.Context, in timetablemodule.UpdateRegisteredCourseIn) (registeredCourse *timetabledomain.RegisteredCourse, err error) { @@ -151,6 +160,10 @@ func (uc impl) UpdateRegisteredCourse(ctx context.Context, in timetablemodule.Up return err } + if err := uc.r.LoadCourseToRegisteredCourse(ctx, []*timetabledomain.RegisteredCourse{registeredCourse}, sharedport.LockNone); err != nil { + return err + } + if in.TagIDs != nil { tags, err := rtx.ListTags(ctx, timetableport.ListTagsConds{ UserID: &userID,