Skip to content

Commit 85bdc3c

Browse files
authored
Add courses aggregate endpoint to professor endpoint (#234)
* Add courses aggregate endpoint to professor endpoint * fix the comment * Add the sections aggregate endpoints to the course endpoint * Add pagination to course and section aggregate endpoints
1 parent 3aceb23 commit 85bdc3c

File tree

5 files changed

+294
-0
lines changed

5 files changed

+294
-0
lines changed

api/configs/setup.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,25 @@ func GetOptionLimit(query *bson.M, c *gin.Context) (*options.FindOptions, error)
8080

8181
return options.Find().SetSkip(offset).SetLimit(limit), err
8282
}
83+
84+
// TODO: Is there a chance we can combine this with GetOptionLimit to reduce repretiveness ?
85+
// Returns pairs of the offset and limit for pagination stage for aggregate endpoints pipeline
86+
// returns (offset, limit, err)
87+
func GetAggregateLimit(query *bson.M, c *gin.Context) (int64, int64, error) {
88+
delete(*query, "offset") // remove offset field (if present) in the query
89+
90+
// parses offset if included in the query
91+
var limit int64 = GetEnvLimit()
92+
var offset int64
93+
var err error
94+
95+
if c.Query("offset") == "" {
96+
offset = 0 // default value
97+
} else {
98+
offset, err = strconv.ParseInt(c.Query("offset"), 10, 64)
99+
if err != nil {
100+
return offset, limit, err // default value
101+
}
102+
}
103+
return offset, limit, err
104+
}

api/controllers/course.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,124 @@ func CourseAll(c *gin.Context) {
133133
// return result
134134
c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: courses})
135135
}
136+
137+
// @Id courseSectionSearch
138+
// @Router /course/sections [get]
139+
// @Description "Returns all the sections of all the courses matching the query's string-typed key-value pairs"
140+
// @Produce json
141+
// @Param course_number query string false "The course's official number"
142+
// @Param subject_prefix query string false "The course's subject prefix"
143+
// @Param title query string false "The course's title"
144+
// @Param description query string false "The course's description"
145+
// @Param school query string false "The course's school"
146+
// @Param credit_hours query string false "The number of credit hours awarded by successful completion of the course"
147+
// @Param class_level query string false "The level of education that this course course corresponds to"
148+
// @Param activity_type query string false "The type of class this course corresponds to"
149+
// @Param grading query string false "The grading status of this course"
150+
// @Param internal_course_number query string false "The internal (university) number used to reference this course"
151+
// @Param lecture_contact_hours query string false "The weekly contact hours in lecture for a course"
152+
// @Param offering_frequency query string false "The frequency of offering a course"
153+
// @Success 200 {array} schema.Section "A list of sections"
154+
func CourseSectionSearch() gin.HandlerFunc {
155+
return func(c *gin.Context) {
156+
courseSection("Search", c)
157+
}
158+
}
159+
160+
// @Id courseSectionById
161+
// @Router /course/{id}/sections [get]
162+
// @Description "Returns the all of the sections of the course with given ID"
163+
// @Produce json
164+
// @Param id path string true "ID of the course to get"
165+
// @Success 200 {array} schema.Section "A list of sections"
166+
func CourseSectionById() gin.HandlerFunc {
167+
return func(c *gin.Context) {
168+
courseSection("ById", c)
169+
}
170+
}
171+
172+
// get the sections of the courses, filters depending on the flag
173+
func courseSection(flag string, c *gin.Context) {
174+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
175+
defer cancel()
176+
177+
var courseSections []schema.Section // the list of sections of the filtered courses
178+
var courseQuery bson.M // query of the courses (or the single course)
179+
var err error // error
180+
181+
// determine the course query
182+
if flag == "Search" { // filter courses based on the query parameters
183+
// build the key-value pair of query parameters
184+
courseQuery, err = schema.FilterQuery[schema.Course](c)
185+
if err != nil {
186+
// return the validation error if there's anything wrong
187+
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "schema validation error", Data: err.Error()})
188+
return
189+
}
190+
} else if flag == "ById" { // filter the single course based on it's Id
191+
// convert the id param with the ObjectID
192+
courseId := c.Param("id")
193+
courseObjId, convertIdErr := primitive.ObjectIDFromHex(courseId)
194+
if convertIdErr != nil {
195+
// return the id conversion error if there's error
196+
log.WriteError(convertIdErr)
197+
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "error with id", Data: convertIdErr.Error()})
198+
return
199+
}
200+
courseQuery = bson.M{"_id": courseObjId}
201+
} else {
202+
// otherwise, something that messed up the server
203+
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "internal error", Data: "broken endpoint"})
204+
return
205+
}
206+
207+
// determine the offset and limit for pagination stage
208+
// and delete "offset" field in professorQuery
209+
offset, limit, err := configs.GetAggregateLimit(&courseQuery, c)
210+
if err != nil {
211+
log.WriteErrorWithMsg(err, log.OffsetNotTypeInteger)
212+
c.JSON(http.StatusConflict, responses.ErrorResponse{Status: http.StatusConflict, Message: "Error offset is not type integer", Data: err.Error()})
213+
return
214+
}
215+
216+
// pipeline to query the sections from the filtered courses
217+
courseSectionPipeline := mongo.Pipeline{
218+
// filter the courses
219+
bson.D{{Key: "$match", Value: courseQuery}},
220+
221+
// paginate the courses before pulling the sections from thoses courses
222+
bson.D{{Key: "$skip", Value: offset}}, // skip to the specified offset
223+
bson.D{{Key: "$limit", Value: limit}}, // limit to the specified number of courses
224+
225+
// lookup the sections of the courses
226+
bson.D{{Key: "$lookup", Value: bson.D{
227+
{Key: "from", Value: "sections"},
228+
{Key: "localField", Value: "sections"},
229+
{Key: "foreignField", Value: "_id"},
230+
{Key: "as", Value: "sections"},
231+
}}},
232+
233+
// unwind the sections of the courses
234+
bson.D{{Key: "$unwind", Value: bson.D{
235+
{Key: "path", Value: "$sections"},
236+
{Key: "preserveNullAndEmptyArrays", Value: false}, // avoid course documents that can't be replaced
237+
}}},
238+
239+
// replace the courses with sections
240+
bson.D{{Key: "$replaceWith", Value: "$sections"}},
241+
}
242+
243+
// perform aggregation on the pipeline
244+
cursor, err := courseCollection.Aggregate(ctx, courseSectionPipeline)
245+
if err != nil {
246+
// return error for any aggregation problem
247+
log.WriteError(err)
248+
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "aggregation error", Data: err.Error()})
249+
return
250+
}
251+
// parse the array of sections of the course
252+
if err = cursor.All(ctx, &courseSections); err != nil {
253+
panic(err)
254+
}
255+
c.JSON(http.StatusOK, responses.MultiSectionResponse{Status: http.StatusOK, Message: "success", Data: courseSections})
256+
}

api/controllers/professor.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func ProfessorSearch(c *gin.Context) {
8383
}
8484

8585
// return result
86+
print(len(professors))
8687
c.JSON(http.StatusOK, responses.MultiProfessorResponse{Status: http.StatusOK, Message: "success", Data: professors})
8788
}
8889

@@ -143,3 +144,145 @@ func ProfessorAll(c *gin.Context) {
143144
// return result
144145
c.JSON(http.StatusOK, responses.MultiProfessorResponse{Status: http.StatusOK, Message: "success", Data: professors})
145146
}
147+
148+
// @Id professorCourseSearch
149+
// @Router /professor/courses [get]
150+
// @Description "Returns all of the courses of all the professors matching the query's string-typed key-value pairs"
151+
// @Produce json
152+
// @Param first_name query string false "The professor's first name"
153+
// @Param last_name query string false "The professor's last name"
154+
// @Param titles query string false "One of the professor's title"
155+
// @Param email query string false "The professor's email address"
156+
// @Param phone_number query string false "The professor's phone number"
157+
// @Param office.building query string false "The building of the location of the professor's office"
158+
// @Param office.room query string false "The room of the location of the professor's office"
159+
// @Param office.map_uri query string false "A hyperlink to the UTD room locator of the professor's office"
160+
// @Param profile_uri query string false "A hyperlink pointing to the professor's official university profile"
161+
// @Param image_uri query string false "A link to the image used for the professor on the professor's official university profile"
162+
// @Param office_hours.start_date query string false "The start date of one of the office hours meetings of the professor"
163+
// @Param office_hours.end_date query string false "The end date of one of the office hours meetings of the professor"
164+
// @Param office_hours.meeting_days query string false "One of the days that one of the office hours meetings of the professor"
165+
// @Param office_hours.start_time query string false "The time one of the office hours meetings of the professor starts"
166+
// @Param office_hours.end_time query string false "The time one of the office hours meetings of the professor ends"
167+
// @Param office_hours.modality query string false "The modality of one of the office hours meetings of the professor"
168+
// @Param office_hours.location.building query string false "The building of one of the office hours meetings of the professor"
169+
// @Param office_hours.location.room query string false "The room of one of the office hours meetings of the professor"
170+
// @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"
171+
// @Param sections query string false "The _id of one of the sections the professor teaches"
172+
// @Success 200 {array} schema.Course "A list of Courses"
173+
func ProfessorCourseSearch() gin.HandlerFunc {
174+
// Wrapper of professorCourse() with flag of Search
175+
return func(c *gin.Context) {
176+
professorCourse("Search", c)
177+
}
178+
}
179+
180+
// @Id professorCourseById
181+
// @Router /professor/{id}/courses [get]
182+
// @Description "Returns all the courses taught by the professor with given ID"
183+
// @Produce json
184+
// @Param id path string true "ID of the professor to get"
185+
// @Success 200 {array} schema.Course "A list of courses"
186+
func ProfessorCourseById() gin.HandlerFunc {
187+
// Essentially wrapper of professorCourse() with flag of ById
188+
return func(c *gin.Context) {
189+
professorCourse("ById", c)
190+
}
191+
}
192+
193+
// Get all of the courses of the professors depending on the type of flag
194+
func professorCourse(flag string, c *gin.Context) {
195+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
196+
197+
var professorCourses []schema.Course // array of courses of the professors (or single professor with Id)
198+
var professorQuery bson.M // query filter the professor
199+
var err error
200+
201+
defer cancel()
202+
203+
// determine the professor's query
204+
if flag == "Search" { // if the flag is Search, filter professors based on query parameters
205+
// build the key-value pairs of query parameters
206+
professorQuery, err = schema.FilterQuery[schema.Professor](c)
207+
if err != nil {
208+
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "schema validation error", Data: err.Error()})
209+
return
210+
}
211+
} else if flag == "ById" { // if the flag is ById, filter that single professor based on their _id
212+
// parse the ObjectId
213+
professorId := c.Param("id")
214+
professorObjId, err := primitive.ObjectIDFromHex(professorId)
215+
if err != nil {
216+
log.WriteError(err)
217+
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "error", Data: err.Error()})
218+
return
219+
}
220+
professorQuery = bson.M{"_id": professorObjId}
221+
} else {
222+
// something wrong that messed up the server
223+
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "error", Data: "Endpoint broken"})
224+
return
225+
}
226+
227+
// determine the offset and limit for pagination stage
228+
// and delete "offset" field in professorQuery
229+
offset, limit, err := configs.GetAggregateLimit(&professorQuery, c)
230+
if err != nil {
231+
log.WriteErrorWithMsg(err, log.OffsetNotTypeInteger)
232+
c.JSON(http.StatusConflict, responses.ErrorResponse{Status: http.StatusConflict, Message: "Error offset is not type integer", Data: err.Error()})
233+
return
234+
}
235+
236+
// Pipeline to query the courses from the filtered professors (or a single professor)
237+
professorCoursePipeline := mongo.Pipeline{
238+
// filter the professors
239+
bson.D{{Key: "$match", Value: professorQuery}},
240+
241+
// paginate the professors before pulling the courses from those professor
242+
bson.D{{Key: "$skip", Value: offset}}, // skip to the specified offset
243+
bson.D{{Key: "$limit", Value: limit}}, // limit to the specified number of professors
244+
245+
// lookup the array of sections from sections collection
246+
bson.D{{Key: "$lookup", Value: bson.D{
247+
{Key: "from", Value: "sections"},
248+
{Key: "localField", Value: "sections"},
249+
{Key: "foreignField", Value: "_id"},
250+
{Key: "as", Value: "sections"},
251+
}}},
252+
253+
// project the courses referenced by each section in the array
254+
bson.D{{Key: "$project", Value: bson.D{{Key: "courses", Value: "$sections.course_reference"}}}},
255+
256+
// lookup the array of courses from coures collection
257+
bson.D{{Key: "$lookup", Value: bson.D{
258+
{Key: "from", Value: "courses"},
259+
{Key: "localField", Value: "courses"},
260+
{Key: "foreignField", Value: "_id"},
261+
{Key: "as", Value: "courses"},
262+
}}},
263+
264+
// unwind the courses
265+
bson.D{{Key: "$unwind", Value: bson.D{
266+
{Key: "path", Value: "$courses"},
267+
{Key: "preserveNullAndEmptyArrays", Value: false}, // to avoid the professor documents that can't be replaced
268+
}}},
269+
270+
// replace the combination of ids and courses with the courses entirely
271+
bson.D{{Key: "$replaceWith", Value: "$courses"}},
272+
}
273+
274+
// Perform aggreration on the pipeline
275+
cursor, err := professorCollection.Aggregate(ctx, professorCoursePipeline)
276+
if err != nil {
277+
// return the error with there's something wrong with the aggregation
278+
log.WriteError(err)
279+
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "error", Data: err.Error()})
280+
return
281+
}
282+
// Parse the array of courses from these professors
283+
if err = cursor.All(ctx, &professorCourses); err != nil {
284+
log.WritePanic(err)
285+
panic(err)
286+
}
287+
c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: professorCourses})
288+
}

api/routes/course.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ func CourseRoute(router *gin.Engine) {
1414
courseGroup.GET("", controllers.CourseSearch)
1515
courseGroup.GET(":id", controllers.CourseById)
1616
courseGroup.GET("all", controllers.CourseAll)
17+
18+
// Endpoint to get the list of sections of the queried course, courses
19+
courseGroup.GET("/sections", controllers.CourseSectionSearch())
20+
courseGroup.GET("/:id/sections", controllers.CourseSectionById())
1721
}

api/routes/professor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ func ProfessorRoute(router *gin.Engine) {
1414
professorGroup.GET("", controllers.ProfessorSearch)
1515
professorGroup.GET(":id", controllers.ProfessorById)
1616
professorGroup.GET("all", controllers.ProfessorAll)
17+
18+
// Endpoints to get the courses of the professors
19+
professorGroup.GET("courses", controllers.ProfessorCourseSearch())
20+
professorGroup.GET(":id/courses", controllers.ProfessorCourseById())
1721
}

0 commit comments

Comments
 (0)