From b21fa852ad8eec0496a72a9877f6a048b871cc94 Mon Sep 17 00:00:00 2001 From: mikehquan19 Date: Thu, 31 Oct 2024 02:52:21 -0500 Subject: [PATCH] Add courses aggregate endpoint to professor endpoint --- api/controllers/professor.go | 130 +++++++++++++++++++++++++++++++++++ api/routes/professor.go | 4 ++ 2 files changed, 134 insertions(+) diff --git a/api/controllers/professor.go b/api/controllers/professor.go index 705a075..47d4cfb 100644 --- a/api/controllers/professor.go +++ b/api/controllers/professor.go @@ -143,3 +143,133 @@ func ProfessorAll(c *gin.Context) { // return result c.JSON(http.StatusOK, responses.MultiProfessorResponse{Status: http.StatusOK, Message: "success", Data: professors}) } + +// @Id professorCourseSearch +// @Router /professor [get] +// @Description "Returns all of the courses of all the professors matching the query's string-typed key-value pairs" +// @Produce json +// @Param first_name query string false "The professor's first name" +// @Param last_name query string false "The professor's last name" +// @Param titles query string false "One of the professor's title" +// @Param email query string false "The professor's email address" +// @Param phone_number query string false "The professor's phone number" +// @Param office.building query string false "The building of the location of the professor's office" +// @Param office.room query string false "The room of the location of the professor's office" +// @Param office.map_uri query string false "A hyperlink to the UTD room locator of the professor's office" +// @Param profile_uri query string false "A hyperlink pointing to the professor's official university profile" +// @Param image_uri query string false "A link to the image used for the professor on the professor's official university profile" +// @Param office_hours.start_date query string false "The start date of one of the office hours meetings of the professor" +// @Param office_hours.end_date query string false "The end date of one of the office hours meetings of the professor" +// @Param office_hours.meeting_days query string false "One of the days that one of the office hours meetings of the professor" +// @Param office_hours.start_time query string false "The time one of the office hours meetings of the professor starts" +// @Param office_hours.end_time query string false "The time one of the office hours meetings of the professor ends" +// @Param office_hours.modality query string false "The modality of one of the office hours meetings of the professor" +// @Param office_hours.location.building query string false "The building of one of the office hours meetings of the professor" +// @Param office_hours.location.room query string false "The room of one of the office hours meetings of the professor" +// @Param office_hours.location.map_uri query string false "A hyperlink to the UTD room locator of one of the office hours meetings of the professor" +// @Param sections query string false "The _id of one of the sections the professor teaches" +// @Success 200 {array} schema.Course "A list of Courses" +func ProfessorCourseSearch() gin.HandlerFunc { + // Wrapper of professorCourse() with flag of Search + return func(c *gin.Context) { + professorCourse("Search", c) + } +} + +// @Id professorCourseById +// @Router /professor/{id} [get] +// @Description "Returns all the courses taught by the professor with given ID" +// @Produce json +// @Param id path string true "ID of the professor to get" +// @Success 200 {array} schema.Course "A list of courses" +func ProfessorCourseById() gin.HandlerFunc { + // Essentially wrapper of professorCourse() with flag of ById + return func(c *gin.Context) { + professorCourse("ById", c) + } +} + +// Get all of the courses of the professors depending on the type of flag +func professorCourse(flag string, c *gin.Context) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + var professorCourses []schema.Course // array of courses of the professors (or single professor with Id) + var professorQuery bson.M // query filter the professor + var err error + + defer cancel() + + if flag == "Search" { + // if the flag is Search, filter professors based on query parameters + professorQuery, err = schema.FilterQuery[schema.Professor](c) + if err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "schema validation error", Data: err.Error()}) + return + } + } else if flag == "ById" { + // if the flag is ById, filter that single professor based on their _id + // parse the ObjectId + professorId := c.Param("id") + professorObjId, err := primitive.ObjectIDFromHex(professorId) + if err != nil { + log.WriteError(err) + c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "error", Data: err.Error()}) + return + } + professorQuery = bson.M{"_id": professorObjId} + } else { + // something wrong that messed up the server + c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "error", Data: "Endpoint broken"}) + return + } + + // TODO: ASK QUESTION ABOUT THE OPTION LIMIT + + // Pipeline to query the courses from the filtered professors (or a single professor) + professorCoursePipeline := mongo.Pipeline{ + // filter the professors + bson.D{{Key: "$match", Value: professorQuery}}, + + // lookup the array of sections from sections collection + bson.D{{Key: "$lookup", Value: bson.D{ + {Key: "from", Value: "sections"}, + {Key: "localField", Value: "sections"}, + {Key: "foreignField", Value: "_id"}, + {Key: "as", Value: "sections"}, + }}}, + + // project the courses referenced by each section in the array + bson.D{{Key: "$project", Value: bson.D{{Key: "courses", Value: "$sections.course_reference"}}}}, + + // lookup the array of courses from coures collection + bson.D{{Key: "$lookup", Value: bson.D{ + {Key: "from", Value: "courses"}, + {Key: "localField", Value: "courses"}, + {Key: "foreignField", Value: "_id"}, + {Key: "as", Value: "courses"}, + }}}, + + // unwind the courses + bson.D{{Key: "$unwind", Value: bson.D{ + {Key: "path", Value: "$courses"}, + {Key: "preserveNullAndEmptyArrays", Value: true}, + }}}, + + // replace the combination of ids and courses with the courses entirely + bson.D{{Key: "$replaceWith", Value: "$courses"}}, + } + + // Perform aggreration on the pipeline + cursor, err := professorCollection.Aggregate(ctx, professorCoursePipeline) + if err != nil { + // return the error with there's something wrong with the aggregation + log.WriteError(err) + c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "error", Data: err.Error()}) + return + } + // Parse the array of courses from these professors + if err = cursor.All(ctx, &professorCourses); err != nil { + panic(err) + } + c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: professorCourses}) +} diff --git a/api/routes/professor.go b/api/routes/professor.go index d006e70..8bdc9fb 100644 --- a/api/routes/professor.go +++ b/api/routes/professor.go @@ -14,4 +14,8 @@ func ProfessorRoute(router *gin.Engine) { professorGroup.GET("", controllers.ProfessorSearch) professorGroup.GET(":id", controllers.ProfessorById) professorGroup.GET("all", controllers.ProfessorAll) + + // Endpoints to get the courses of the professors + professorGroup.GET("course", controllers.ProfessorCourseSearch()) + professorGroup.GET(":id/course", controllers.ProfessorCourseById()) }