diff --git a/entity/course.go b/entity/course.go index 130d1bb..1c8c063 100644 --- a/entity/course.go +++ b/entity/course.go @@ -1,5 +1,7 @@ package entity +import "gorm.io/datatypes" + type CriteriaGrade struct { A float64 `json:"criteriaGradeA" gorm:"column:criteria_grade_a" validate:"required"` BP float64 `json:"criteriaGradeBP" gorm:"column:criteria_grade_bp" validate:"required"` @@ -70,6 +72,7 @@ type Course struct { Description string `json:"description"` ExpectedPassingCloPercentage float64 `json:"expectedPassingCloPercentage"` IsPortfolioCompleted *bool `json:"isPortfolioCompleted" gorm:"default:false"` + PortfolioData datatypes.JSON SemesterId string `json:"semesterId"` UserId string `json:"userId"` diff --git a/entity/course_portfolio.go b/entity/course_portfolio.go index 2ad0f78..3993765 100644 --- a/entity/course_portfolio.go +++ b/entity/course_portfolio.go @@ -1,5 +1,7 @@ package entity +import "gorm.io/datatypes" + // [1] Info type CourseInfo struct { Name string `json:"courseName"` @@ -9,8 +11,8 @@ type CourseInfo struct { // [2] Summary type CourseSummary struct { - TeachingMethods []string `json:"teachingMethod"` - OnlineTool string `json:"onlineTool"` + TeachingMethods []string `json:"teachingMethods"` + OnlineTools string `json:"onlineTools"` Objectives []string `json:"objectives"` } @@ -84,6 +86,7 @@ type CoursePortfolio struct { CourseSummary CourseSummary `json:"summary"` CourseResult CourseResult `json:"result"` CourseDevelopment CourseDevelopment `json:"development"` + Raw datatypes.JSON `json:"raw"` } type AssignmentPercentage struct { @@ -187,7 +190,7 @@ type PloCoursesGorm struct { } type PoCourses struct { - ProgramOutcomeId string `json:"programLearningOutcomeId"` + ProgramOutcomeId string `json:"programOutcomeId"` Courses []CourseData `json:"courses"` } @@ -201,6 +204,86 @@ type PoCoursesGorm struct { SemesterSequence string } +type StudentCourseData struct { + Id string `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Pass bool `json:"pass"` + Year int `json:"year"` + SemesterSequence string `json:"semesterSequence"` +} + +type StudentPloData struct { + ProgramLearningOutcomeId string `json:"programLearningOutcomeId"` + Code string `json:"code"` + DescriptionThai string `json:"descriptionThai"` + Courses []StudentCourseData `json:"courses"` +} + +type StudentPoData struct { + ProgramOutcomeId string `json:"programOutcomeId"` + Code string `json:"code"` + Name string `json:"name"` + Courses []StudentCourseData `json:"courses"` +} + +type StudentOutcomes struct { + StudentId string `json:"studentId"` + ProgramLearningOutcomes []StudentPloData `json:"programLearningOutcomes"` + ProgramOutcomes []StudentPoData `json:"programOutcomes"` +} + +type StudentPlosGorm struct { + StudentId string + ProgramLearningOutcomeId string `gorm:"column:plo_id"` + ProgramLearningOutcomeCode string `gorm:"column:plo_code"` + DescriptionThai string + CourseId string + CourseCode string + CourseName string + Pass bool + Year int + SemesterSequence string +} + +type StudentPosGorm struct { + StudentId string + ProgramOutcomeId string `gorm:"column:p_id"` + ProgramOutcomeCode string `gorm:"column:po_code"` + ProgramOutcomeName string `gorm:"column:po_name"` + CourseId string + CourseCode string + CourseName string + Pass bool + Year int + SemesterSequence string +} + +// /// + +// type NameObject struct { +// Name string `json:"name"` +// } + +// type CourseSummmaryForm struct { +// TeachingMethods []NameObject `json:"teachingMethod"` +// OnlineTool string `json:"onlineTool"` +// Objectives []NameObject `json:"objectives"` +// } + +// type CourseDevelopmentForm struct { +// Plans []NameObject `json:"plans"` +// DoAndChecks []NameObject `json:"doAndChecks"` +// Acts []NameObject `json:"acts"` +// SubjectComments SubjectComments `json:"subjectComments"` +// OtherComment string `json:"otherComment"` +// } + +type PortfolioData struct { + Summary CourseSummary `json:"summary"` + Development CourseDevelopment `json:"development"` +} + type CoursePortfolioRepository interface { EvaluatePassingAssignmentPercentage(courseId string) ([]AssignmentPercentage, error) EvaluatePassingPoPercentage(courseId string) ([]PoPercentage, error) @@ -210,6 +293,10 @@ type CoursePortfolioRepository interface { EvaluatePassingPoStudents(courseId string) ([]PoPassingStudentGorm, error) EvaluateAllPloCourses() ([]PloCoursesGorm, error) EvaluateAllPoCourses() ([]PoCoursesGorm, error) + EvaluateProgramLearningOutcomesByStudentId(studentId string) ([]StudentPlosGorm, error) + EvaluateProgramOutcomesByStudentId(studentId string) ([]StudentPosGorm, error) + + UpdateCoursePortfolio(courseId string, data datatypes.JSON) error } type CoursePortfolioUseCase interface { @@ -220,4 +307,7 @@ type CoursePortfolioUseCase interface { GetStudentOutcomesStatusByCourseId(courseId string) ([]StudentOutcomeStatus, error) GetAllProgramLearningOutcomeCourses() ([]PloCourses, error) GetAllProgramOutcomeCourses() ([]PoCourses, error) + GetOutcomesByStudentId(studentId string) ([]StudentOutcomes, error) + + UpdateCoursePortfolio(courseId string, summary CourseSummary, development CourseDevelopment) error } diff --git a/go.mod b/go.mod index cb6763c..2414370 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/spf13/viper v1.16.0 go.uber.org/zap v1.25.0 golang.org/x/crypto v0.20.0 + gorm.io/datatypes v1.2.0 gorm.io/driver/mysql v1.5.1 gorm.io/gorm v1.25.9 ) diff --git a/go.sum b/go.sum index 4644584..4c0ba85 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,10 @@ github.com/gofiber/contrib/fiberzap/v2 v2.0.0 h1:pgFhveNyFlNzr8AQlRG7o1/EKMi+vOm github.com/gofiber/contrib/fiberzap/v2 v2.0.0/go.mod h1:NUXSG7SUq6b5NQfAyWCEdaaq3ZKsWz6atunN+NJ4gYY= github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -147,6 +151,12 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= +github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -175,6 +185,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= @@ -537,11 +551,17 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= +gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= -gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/infrastructure/fiber/controller/course_portfolio.go b/infrastructure/fiber/controller/course_portfolio.go index 639dbcd..5cf013b 100644 --- a/infrastructure/fiber/controller/course_portfolio.go +++ b/infrastructure/fiber/controller/course_portfolio.go @@ -1,8 +1,11 @@ package controller import ( + "fmt" + "github.com/gofiber/fiber/v2" "github.com/team-inu/inu-backyard/entity" + "github.com/team-inu/inu-backyard/infrastructure/fiber/request" "github.com/team-inu/inu-backyard/infrastructure/fiber/response" "github.com/team-inu/inu-backyard/internal/validator" ) @@ -64,3 +67,30 @@ func (c coursePortfolioController) GetAllProgramOutcomeCourses(ctx *fiber.Ctx) e } return response.NewSuccessResponse(ctx, fiber.StatusOK, records) } + +func (c coursePortfolioController) Update(ctx *fiber.Ctx) error { + var payload request.SaveCoursePortfolioPayload + if ok, err := c.Validator.Validate(&payload, ctx); !ok { + return err + } + + courseId := ctx.Params("courseId") + + fmt.Println(payload) + err := c.coursePortfolioUseCase.UpdateCoursePortfolio(courseId, payload.CourseSummary, payload.CourseDevelopment) + if err != nil { + return err + } + + return nil +} + +func (c coursePortfolioController) GetOutcomesByStudentId(ctx *fiber.Ctx) error { + studentId := ctx.Params("studentId") + + records, err := c.coursePortfolioUseCase.GetOutcomesByStudentId(studentId) + if err != nil { + return err + } + return response.NewSuccessResponse(ctx, fiber.StatusOK, records) +} diff --git a/infrastructure/fiber/request/course_portfolio.go b/infrastructure/fiber/request/course_portfolio.go new file mode 100644 index 0000000..6d6db66 --- /dev/null +++ b/infrastructure/fiber/request/course_portfolio.go @@ -0,0 +1,8 @@ +package request + +import "github.com/team-inu/inu-backyard/entity" + +type SaveCoursePortfolioPayload struct { + CourseSummary entity.CourseSummary `json:"summary" validate:"required"` + CourseDevelopment entity.CourseDevelopment `json:"development" validate:"required"` +} diff --git a/infrastructure/fiber/server.go b/infrastructure/fiber/server.go index de2d5e8..ab3f7b4 100644 --- a/infrastructure/fiber/server.go +++ b/infrastructure/fiber/server.go @@ -127,7 +127,7 @@ func (f *fiberServer) initUseCase() { assignmentUseCase := usecase.NewAssignmentUseCase(f.assignmentRepository, courseLearningOutcomeUseCase, courseUseCase) scoreUseCase := usecase.NewScoreUseCase(f.scoreRepository, enrollmentUseCase, assignmentUseCase, courseUseCase, userUseCase, studentUseCase) courseStreamUseCase := usecase.NewCourseStreamUseCase(f.courseStreamRepository, courseUseCase) - coursePortfolioUseCase := usecase.NewCoursePortfolioUseCase(f.coursePortfolioRepository, courseUseCase, userUseCase, enrollmentUseCase, assignmentUseCase, scoreUseCase, courseLearningOutcomeUseCase, courseStreamUseCase) + coursePortfolioUseCase := usecase.NewCoursePortfolioUseCase(f.coursePortfolioRepository, courseUseCase, userUseCase, enrollmentUseCase, assignmentUseCase, scoreUseCase, studentUseCase, courseLearningOutcomeUseCase, courseStreamUseCase) predictionUseCase := usecase.NewPredictionUseCase(f.predictionRepository, f.config) f.assignmentUseCase = assignmentUseCase @@ -196,6 +196,7 @@ func (f *fiberServer) initController() error { student.Post("/", studentController.Create) student.Post("/bulk", studentController.CreateMany) student.Get("/:studentId", studentController.GetById) + student.Get("/:studentId/outcomes", coursePortfolioController.GetOutcomesByStudentId) student.Patch("/:studentId", studentController.Update) student.Delete("/:studentId", studentController.Delete) @@ -213,6 +214,7 @@ func (f *fiberServer) initController() error { course.Get("/:courseId/students/outcomes", coursePortfolioController.GetStudentOutcomeStatusByCourseId) course.Get("/:courseId/enrollments", enrollmentController.GetByCourseId) course.Get("/:courseId/portfolio", coursePortfolioController.Generate) + course.Patch("/:courseId/portfolio", coursePortfolioController.Update) course.Get("/:courseId/assignments", assignmentController.GetByCourseId) course.Get("/:courseId/assignment-groups", assignmentController.GetGroupByCourseId) diff --git a/repository/course_portfolio.go b/repository/course_portfolio.go index cef8466..f3adea0 100644 --- a/repository/course_portfolio.go +++ b/repository/course_portfolio.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/team-inu/inu-backyard/entity" + "gorm.io/datatypes" "gorm.io/gorm" ) @@ -642,7 +643,7 @@ func (r coursePortfolioRepositoryGorm) evaluateOutcomesAllCourses(selector Tabee FROM clo_count_by_plo JOIN student_clo_passing_count_by_plo ON clo_count_by_plo.plo_id = student_clo_passing_count_by_plo.plo_id - LEFT JOIN courses ON courses.id = clo_count_by_plo.course_id AND courses.id = student_clo_passing_count_by_plo.course_id + LEFT JOIN courses ON courses.id = clo_count_by_plo.course_id AND clo_count_by_plo.course_id = student_clo_passing_count_by_plo.course_id ), total_plo_pass AS ( SELECT @@ -717,3 +718,364 @@ func (r coursePortfolioRepositoryGorm) evaluateOutcomesAllCourses(selector Tabee return nil } + +func (r coursePortfolioRepositoryGorm) UpdateCoursePortfolio(courseId string, data datatypes.JSON) error { + completed := true + + err := r.gorm.Model(&entity.Course{}).Where("id = ?", courseId).Updates(&entity.Course{ + PortfolioData: data, + IsPortfolioCompleted: &completed, + }).Error + if err != nil { + return fmt.Errorf("cannot update course: %w", err) + } + + return nil +} + +func (r coursePortfolioRepositoryGorm) EvaluateProgramLearningOutcomesByStudentId(studentId string) ([]entity.StudentPlosGorm, error) { + var res = []entity.StudentPlosGorm{} + + err := r.evaluateOutcomesByStudentId(studentId, TabeeSelectorPloPassingStudents, &res) + if err != nil { + return nil, fmt.Errorf("cannot query to evaluate student program learning outcomes: %w", err) + } + + return res, nil +} + +func (r coursePortfolioRepositoryGorm) EvaluateProgramOutcomesByStudentId(studentId string) ([]entity.StudentPosGorm, error) { + var res = []entity.StudentPosGorm{} + + err := r.evaluateOutcomesByStudentId(studentId, TabeeSelectorPoPassingStudents, &res) + if err != nil { + return nil, fmt.Errorf("cannot query to evaluate student program outcomes: %w", err) + } + + return res, nil +} + +func (r coursePortfolioRepositoryGorm) evaluateOutcomesByStudentId(studentId string, selector TabeeSelector, x interface{}) error { + template := ` + WITH + courses AS ( + SELECT expected_passing_clo_percentage, id + FROM course + ), + clos AS ( + SELECT + courses.id AS course_id, + course_learning_outcome.id, + expected_passing_assignment_percentage, + program_outcome_id + FROM + course_learning_outcome + JOIN courses ON courses.id = course_learning_outcome.course_id + ), + assignments AS ( + SELECT + assignment.name, + assignment.max_score, + assignment.expected_score_percentage, + clos.expected_passing_assignment_percentage, + clos.id AS c_id, + assignment.id AS a_id, + course_id + FROM clos + JOIN clo_assignment AS ca ON ca.course_learning_outcome_id = clos.id + JOIN assignment ON ca.assignment_id = assignment.id + WHERE assignment.is_included_in_clo IS True + ), + scores AS ( + SELECT * + FROM assignments + JOIN score ON score.assignment_id = a_id + WHERE student_id = ? + ), + student_passing_assignment AS ( + SELECT + scores.score >= scores.expected_score_percentage / 100 * scores.max_score AS pass, + scores.student_id, + scores.a_id AS a_id, + scores.c_id AS c_id, + course_id + FROM + scores + ), + total_assignment_pass AS ( + SELECT + SUM(pass) AS pass_count, + a_id, + c_id, + course_id + FROM + student_passing_assignment + GROUP BY + a_id, c_id + ), + student_count_by_assignment AS ( + SELECT + COUNT(*) AS count, + a_id, + c_id, + course_id + FROM + student_passing_assignment + GROUP BY + a_id, c_id + ), + student_passing_assignment_percentage AS ( + SELECT + total_assignment_pass.pass_count / student_count_by_assignment.count * 100 AS passing_percentage, + total_assignment_pass.a_id, + total_assignment_pass.c_id, + total_assignment_pass.course_id + FROM + total_assignment_pass + JOIN student_count_by_assignment ON total_assignment_pass.a_id = student_count_by_assignment.a_id + AND total_assignment_pass.c_id = student_count_by_assignment.c_id + ), + student_assignment_pass_count AS ( + SELECT + SUM(pass) AS pass_count, + c_id, + student_id, + course_id + FROM + student_passing_assignment + GROUP BY + c_id, student_id + ), + student_count AS ( + SELECT COUNT(*) AS count, c_id, course_id FROM student_assignment_pass_count GROUP BY c_id + ), + assignments_count AS ( + SELECT COUNT(*) AS count , c_id, course_id FROM assignments GROUP BY c_id + ), + student_passing_clo AS ( + SELECT + student_assignment_pass_count.pass_count >= (clos.expected_passing_assignment_percentage / 100 * assignments_count.count) + AS pass, + clos.program_outcome_id, + clos.id AS clo_id, + student_assignment_pass_count.student_id, + clos.course_id + FROM + clos + JOIN assignments_count ON clos.id = assignments_count.c_id + JOIN student_assignment_pass_count ON clos.id = student_assignment_pass_count.c_id + ), + total_clo_pass AS ( + SELECT SUM(pass) AS count, clo_id, course_id FROM student_passing_clo GROUP BY clo_id + ), + student_passing_clo_percentage AS ( + SELECT + total_clo_pass.count / student_count.count * 100 AS passing_percentage, total_clo_pass.clo_id, total_clo_pass.course_id + FROM + total_clo_pass + JOIN student_count ON total_clo_pass.clo_id = student_count.c_id + ), + student_clo_passing_count_by_po AS ( + SELECT + SUM(pass) AS pass_count, + student_id, + program_outcome_id, + course_id + FROM + student_passing_clo + GROUP BY + course_id, program_outcome_id, student_id + ), + clo_count_by_po AS ( + SELECT + COUNT(*) AS clo_count, + program_outcome_id AS p_id, + course_id + FROM + clos + GROUP BY + course_id, program_outcome_id + ), + student_passing_po AS ( + SELECT + (pass_count >= courses.expected_passing_clo_percentage / 100 * clo_count_by_po.clo_count) AS pass, + clo_count_by_po.p_id, + student_clo_passing_count_by_po.student_id, + clo_count_by_po.course_id + FROM + clo_count_by_po + JOIN student_clo_passing_count_by_po ON clo_count_by_po.p_id = student_clo_passing_count_by_po.program_outcome_id + JOIN courses ON courses.id = clo_count_by_po.course_id AND courses.id = student_clo_passing_count_by_po.course_id + ), + total_po_pass AS ( + SELECT + SUM(pass) AS count, + p_id, + course_id + FROM + student_passing_po + GROUP BY + p_id, course_id + ), + student_count_by_po AS ( + SELECT + COUNT(*) AS count, + program_outcome_id, + course_id + FROM + student_clo_passing_count_by_po + GROUP BY + course_id, program_outcome_id + ), + student_passing_po_percentage AS ( + SELECT + total_po_pass.count / student_count_by_po.count * 100 AS passing_percentage, + total_po_pass.p_id, + total_po_pass.course_id + FROM + total_po_pass + JOIN student_count_by_po ON student_count_by_po.program_outcome_id = total_po_pass.p_id + AND student_count_by_po.course_id = total_po_pass.course_id + ), + plos AS ( + SELECT + clos.id AS c_id, + sub_program_learning_outcome.id AS splo_id, + sub_program_learning_outcome.program_learning_outcome_id AS plo_id, + course_id + FROM + clos + JOIN clo_subplo ON clos.id = clo_subplo.course_learning_outcome_id + JOIN sub_program_learning_outcome ON clo_subplo.sub_program_learning_outcome_id = sub_program_learning_outcome.id + ), + distinct_plos AS ( + SELECT + DISTINCT + c_id, + plo_id, + course_id + FROM + plos + ), + student_passing_clo_with_plo AS ( + SELECT + pass, + c_id, + student_id, + plo_id, + student_passing_clo.course_id + FROM + student_passing_clo + JOIN distinct_plos ON student_passing_clo.clo_id = distinct_plos.c_id + ), + student_clo_passing_count_by_plo AS ( + SELECT + SUM(pass) AS pass_count, + plo_id, + student_id, + course_id + FROM + student_passing_clo_with_plo + GROUP BY + course_id, plo_id, student_id + ), + clo_count_by_plo AS ( + SELECT + COUNT(*) AS clo_count, + plo_id, + course_id + FROM + distinct_plos + GROUP BY + course_id, plo_id + ), + student_passing_plo AS ( + SELECT + (pass_count >= courses.expected_passing_clo_percentage / 100 * clo_count_by_plo.clo_count) AS pass, + clo_count_by_plo.plo_id, + student_clo_passing_count_by_plo.student_id, + clo_count_by_plo.course_id + FROM + clo_count_by_plo + JOIN student_clo_passing_count_by_plo ON clo_count_by_plo.plo_id = student_clo_passing_count_by_plo.plo_id + JOIN courses ON courses.id = clo_count_by_plo.course_id AND clo_count_by_plo.course_id = student_clo_passing_count_by_plo.course_id + WHERE + student_id IS NOT NULL + ), + total_plo_pass AS ( + SELECT + SUM(pass) AS count, + plo_id, + course_id + FROM + student_passing_plo + GROUP BY + course_id, plo_id + ), + student_count_by_plo AS ( + SELECT + COUNT(*) AS count, + plo_id, + course_id + FROM + student_clo_passing_count_by_plo + GROUP BY + course_id, plo_id + ), + student_passing_plo_percentage AS ( + SELECT + total_plo_pass.count / student_count_by_plo.count * 100 AS passing_percentage, + total_plo_pass.plo_id, + total_plo_pass.course_id + FROM + total_plo_pass + JOIN student_count_by_plo ON student_count_by_plo.plo_id = total_plo_pass.plo_id + AND total_plo_pass.course_id = student_count_by_plo.course_id + ), student_passing_plo_with_information AS ( + SELECT + program_learning_outcome.code AS plo_code, + program_learning_outcome.description_thai, + program_learning_outcome.program_year, + student_passing_plo.pass, + student_passing_plo.plo_id, + student_passing_plo.student_id, + course_id, + course.name AS course_name, + course.code AS course_code, + semester.year, + semester.semester_sequence + FROM student_passing_plo + JOIN program_learning_outcome ON student_passing_plo.plo_id = program_learning_outcome.id + JOIN course ON course.id = student_passing_plo.course_id + JOIN semester ON semester.id = course.semester_id + ), + student_passing_po_with_information AS ( + SELECT + program_outcome.code AS po_code, + program_outcome.name AS po_name, + student_passing_po.pass, + student_passing_po.p_id, + student_passing_po.student_id, + course_id, + course.name AS course_name, + course.code AS course_code, + semester.year, + semester.semester_sequence + FROM student_passing_po + JOIN program_outcome ON student_passing_po.p_id = program_outcome.id + JOIN course ON course.id = student_passing_po.course_id + JOIN semester ON semester.id = course.semester_id + ) + SELECT * + FROM %s; + ` + + query := fmt.Sprintf(template, selector) + + err := r.gorm.Raw(query, studentId).Scan(x).Error + if err != nil { + return fmt.Errorf("cannot query to evaluate student outcomes: %w", err) + } + + return nil +} diff --git a/usecase/course.go b/usecase/course.go index a31cb61..d1f83ce 100644 --- a/usecase/course.go +++ b/usecase/course.go @@ -1,6 +1,8 @@ package usecase import ( + "encoding/json" + "github.com/oklog/ulid/v2" "github.com/team-inu/inu-backyard/entity" errs "github.com/team-inu/inu-backyard/entity/error" @@ -73,6 +75,7 @@ func (u courseUseCase) Create(user entity.User, semesterId string, userId string return errs.New(errs.ErrCreateCourse, "invalid criteria grade") } + emptyJson, _ := json.Marshal(map[string]string{}) course := entity.Course{ Id: ulid.Make().String(), SemesterId: semesterId, @@ -83,6 +86,7 @@ func (u courseUseCase) Create(user entity.User, semesterId string, userId string Description: description, ExpectedPassingCloPercentage: expectedPassingCloPercentage, CriteriaGrade: criteriaGrade, + PortfolioData: emptyJson, } err = u.courseRepo.Create(&course) diff --git a/usecase/course_portfolio.go b/usecase/course_portfolio.go index b1f1cb5..4cf425b 100644 --- a/usecase/course_portfolio.go +++ b/usecase/course_portfolio.go @@ -1,6 +1,7 @@ package usecase import ( + "encoding/json" "fmt" "math" @@ -16,6 +17,7 @@ type coursePortfolioUseCase struct { EnrollmentUseCase entity.EnrollmentUseCase AssignmentUseCase entity.AssignmentUseCase ScoreUseCase entity.ScoreUseCase + StudentUseCase entity.StudentUseCase CourseLearningOutcomeUseCase entity.CourseLearningOutcomeUseCase CourseStreamUseCase entity.CourseStreamsUseCase } @@ -27,6 +29,7 @@ func NewCoursePortfolioUseCase( enrollmentUseCase entity.EnrollmentUseCase, assignmentUseCase entity.AssignmentUseCase, scoreUseCase entity.ScoreUseCase, + studentUscase entity.StudentUseCase, courseLearningOutcomeUseCase entity.CourseLearningOutcomeUseCase, courseStreamUseCase entity.CourseStreamsUseCase, ) entity.CoursePortfolioUseCase { @@ -37,6 +40,7 @@ func NewCoursePortfolioUseCase( EnrollmentUseCase: enrollmentUseCase, AssignmentUseCase: assignmentUseCase, ScoreUseCase: scoreUseCase, + StudentUseCase: studentUscase, CourseLearningOutcomeUseCase: courseLearningOutcomeUseCase, CourseStreamUseCase: courseStreamUseCase, } @@ -104,19 +108,29 @@ func (u coursePortfolioUseCase) Generate(courseId string) (*entity.CoursePortfol } } + portfolioData := entity.PortfolioData{} + + err = json.Unmarshal(course.PortfolioData, &portfolioData) + if err != nil { + return nil, errs.New(0, "cannot unmarshal data from db") + } + courseDevelopment := entity.CourseDevelopment{ - Plans: make([]string, 0), - DoAndChecks: make([]string, 0), - Acts: make([]string, 0), + Plans: portfolioData.Development.Plans, + DoAndChecks: portfolioData.Development.DoAndChecks, + Acts: portfolioData.Development.Acts, SubjectComments: entity.SubjectComments{ UpstreamSubjects: upstreamSubject, DownstreamSubjects: downStreamSubject, + Other: portfolioData.Development.SubjectComments.Other, }, + OtherComment: portfolioData.Development.OtherComment, } courseSummary := entity.CourseSummary{ - TeachingMethods: make([]string, 0), - Objectives: make([]string, 0), + TeachingMethods: portfolioData.Summary.TeachingMethods, + Objectives: portfolioData.Summary.Objectives, + OnlineTools: portfolioData.Summary.OnlineTools, } coursePortfolio := &entity.CoursePortfolio{ @@ -124,6 +138,7 @@ func (u coursePortfolioUseCase) Generate(courseId string) (*entity.CoursePortfol CourseResult: courseResult, CourseSummary: courseSummary, CourseDevelopment: courseDevelopment, + Raw: course.PortfolioData, } return coursePortfolio, nil @@ -321,6 +336,34 @@ func (u coursePortfolioUseCase) EvaluateTabeeOutcomes(courseId string) ([]entity tabeeOutcomesByPoId := make(map[string][]entity.TabeeOutcome, 0) for _, clo := range clos { + checkIsSameOutcomeName := func(foundOutcome []entity.TabeeOutcome, clo entity.CourseLearningOutcomeWithPO) bool { + isNameSame := false + + for _, tabeeOutcome := range foundOutcome { + if tabeeOutcome.Name == clo.ProgramOutcomeName { + isNameSame = true + break + } + } + + return isNameSame + } + + foundOutcome, found := tabeeOutcomesByPoId[clo.ProgramOutcomeId] + if !found { + tabeeOutcomesByPoId[clo.ProgramOutcomeId] = append(tabeeOutcomesByPoId[clo.ProgramOutcomeId], entity.TabeeOutcome{ + Name: clo.ProgramOutcomeName, + CourseOutcomes: courseOutcomeByPoId[clo.ProgramOutcomeId], + MinimumPercentage: passingPoPercentageByPoId[clo.ProgramOutcomeId], + }) + continue + } + + isNameSame := checkIsSameOutcomeName(foundOutcome, clo) + if isNameSame { + continue + } + tabeeOutcomesByPoId[clo.ProgramOutcomeId] = append(tabeeOutcomesByPoId[clo.ProgramOutcomeId], entity.TabeeOutcome{ Name: clo.ProgramOutcomeName, CourseOutcomes: courseOutcomeByPoId[clo.ProgramOutcomeId], @@ -501,3 +544,183 @@ func (u coursePortfolioUseCase) GetAllProgramOutcomeCourses() ([]entity.PoCourse return pos, nil } + +func (u coursePortfolioUseCase) UpdateCoursePortfolio(courseId string, summary entity.CourseSummary, development entity.CourseDevelopment) error { + portfolioData := &entity.PortfolioData{ + Summary: summary, + Development: development, + } + + JsonByte, err := json.Marshal(*portfolioData) + if err != nil { + return errs.New(errs.SameCode, "cannot marshal course summary %s", err) + } + + err = u.CoursePortfolioRepository.UpdateCoursePortfolio(courseId, JsonByte) + if err != nil { + return errs.New(errs.SameCode, "cannot update course portfolio %s", err) + } + + return nil +} + +func (u coursePortfolioUseCase) GetOutcomesByStudentId(studentId string) ([]entity.StudentOutcomes, error) { + student, err := u.StudentUseCase.GetById(studentId) + if err != nil { + return nil, errs.New(errs.SameCode, "cannot get student id %s while getting student outcomes", student, err) + } else if student == nil { + return nil, errs.New(errs.ErrStudentNotFound, "student id %s not found while getting student outcomes", studentId, err) + } + + ploRecords, err := u.CoursePortfolioRepository.EvaluateProgramLearningOutcomesByStudentId(studentId) + if err != nil { + return nil, errs.New(errs.SameCode, "cannot evaluate student plos by student id %s", studentId, err) + } + + poRecords, err := u.CoursePortfolioRepository.EvaluateProgramOutcomesByStudentId(studentId) + if err != nil { + return nil, errs.New(errs.SameCode, "cannot evaluate student pos by student id %s", studentId, err) + } + + studentPloMap := make(map[string][]entity.StudentPloData) + studentPoMap := make(map[string][]entity.StudentPoData) + + PloCourseMap := make(map[string][]entity.StudentCourseData) + PoCourseMap := make(map[string][]entity.StudentCourseData) + + for _, record := range ploRecords { + studentPloData, ok := studentPloMap[record.StudentId] + if !ok { + + studentPloMap[record.StudentId] = append(studentPloMap[record.StudentId], entity.StudentPloData{ + ProgramLearningOutcomeId: record.ProgramLearningOutcomeId, + Code: record.ProgramLearningOutcomeCode, + DescriptionThai: record.DescriptionThai, + }) + } else { + isExist := false + for i := range studentPloData { + if studentPloData[i].ProgramLearningOutcomeId == record.ProgramLearningOutcomeId { + isExist = true + break + } + } + if !isExist { + studentPloMap[record.StudentId] = append(studentPloMap[record.StudentId], entity.StudentPloData{ + ProgramLearningOutcomeId: record.ProgramLearningOutcomeId, + Code: record.ProgramLearningOutcomeCode, + DescriptionThai: record.DescriptionThai, + }) + } + } + + ploData, ok := PloCourseMap[record.ProgramLearningOutcomeId] + + if !ok { + + PloCourseMap[record.ProgramLearningOutcomeId] = append(PloCourseMap[record.ProgramLearningOutcomeId], entity.StudentCourseData{ + Id: record.CourseId, + Code: record.CourseCode, + Name: record.CourseName, + Pass: record.Pass, + Year: record.Year, + SemesterSequence: record.SemesterSequence, + }) + } else { + isExist := false + for i := range ploData { + if ploData[i].Id == record.CourseId { + isExist = true + break + } + } + if !isExist { + PloCourseMap[record.ProgramLearningOutcomeId] = append(PloCourseMap[record.ProgramLearningOutcomeId], entity.StudentCourseData{ + Id: record.CourseId, + Code: record.CourseCode, + Name: record.CourseName, + Pass: record.Pass, + Year: record.Year, + SemesterSequence: record.SemesterSequence, + }) + } + } + } + + for _, record := range poRecords { + studentData, found := studentPoMap[record.StudentId] + if !found { + studentPoMap[record.StudentId] = append(studentPoMap[record.StudentId], entity.StudentPoData{ + ProgramOutcomeId: record.ProgramOutcomeId, + Code: record.ProgramOutcomeCode, + Name: record.ProgramOutcomeName, + }) + + } else { + isExist := false + for i := range studentData { + if studentData[i].ProgramOutcomeId == record.ProgramOutcomeId { + isExist = true + break + } + } + if !isExist { + studentPoMap[record.StudentId] = append(studentPoMap[record.StudentId], entity.StudentPoData{ + ProgramOutcomeId: record.ProgramOutcomeId, + Code: record.ProgramOutcomeCode, + Name: record.ProgramOutcomeName, + }) + } + } + + poData, found := PoCourseMap[record.ProgramOutcomeId] + + if !found { + PoCourseMap[record.ProgramOutcomeId] = append(PoCourseMap[record.ProgramOutcomeId], entity.StudentCourseData{ + Id: record.CourseId, + Code: record.CourseCode, + Name: record.CourseName, + Pass: record.Pass, + Year: record.Year, + SemesterSequence: record.SemesterSequence, + }) + } else { + isExist := false + for i := range poData { + if poData[i].Id == record.CourseId { + isExist = true + break + } + } + if !isExist { + PoCourseMap[record.ProgramOutcomeId] = append(PoCourseMap[record.ProgramOutcomeId], entity.StudentCourseData{ + Id: record.CourseId, + Code: record.CourseCode, + Name: record.CourseName, + Pass: record.Pass, + Year: record.Year, + SemesterSequence: record.SemesterSequence, + }) + } + } + } + + students := make([]entity.StudentOutcomes, 0) + + for studentId := range studentPloMap { + for ploIndex := range studentPloMap[studentId] { + studentPloMap[studentId][ploIndex].Courses = PloCourseMap[studentPloMap[studentId][ploIndex].ProgramLearningOutcomeId] + } + for poIndex := range studentPoMap[studentId] { + studentPoMap[studentId][poIndex].Courses = PoCourseMap[studentPoMap[studentId][poIndex].ProgramOutcomeId] + } + + students = append(students, entity.StudentOutcomes{ + StudentId: studentId, + ProgramLearningOutcomes: studentPloMap[studentId], + ProgramOutcomes: studentPoMap[studentId], + }) + } + + return students, nil +}