Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/configs/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,25 @@ func GetOptionLimit(query *bson.M, c *gin.Context) (*options.FindOptions, error)

return options.Find().SetSkip(offset).SetLimit(limit), err
}

// TODO: Is there a chance we can combine this with GetOptionLimit to reduce repretiveness ?
// Returns pairs of the offset and limit for pagination stage for aggregate endpoints pipeline
// returns (offset, limit, err)
func GetAggregateLimit(query *bson.M, c *gin.Context) (int64, int64, error) {
delete(*query, "offset") // remove offset field (if present) in the query

// parses offset if included in the query
var limit int64 = GetEnvLimit()
var offset int64
var err error

if c.Query("offset") == "" {
offset = 0 // default value
} else {
offset, err = strconv.ParseInt(c.Query("offset"), 10, 64)
if err != nil {
return offset, limit, err // default value
}
}
return offset, limit, err
}
121 changes: 121 additions & 0 deletions api/controllers/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,124 @@ func CourseAll(c *gin.Context) {
// return result
c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: courses})
}

// @Id courseSectionSearch
// @Router /course/sections [get]
// @Description "Returns all the sections of all the courses matching the query's string-typed key-value pairs"
// @Produce json
// @Param course_number query string false "The course's official number"
// @Param subject_prefix query string false "The course's subject prefix"
// @Param title query string false "The course's title"
// @Param description query string false "The course's description"
// @Param school query string false "The course's school"
// @Param credit_hours query string false "The number of credit hours awarded by successful completion of the course"
// @Param class_level query string false "The level of education that this course course corresponds to"
// @Param activity_type query string false "The type of class this course corresponds to"
// @Param grading query string false "The grading status of this course"
// @Param internal_course_number query string false "The internal (university) number used to reference this course"
// @Param lecture_contact_hours query string false "The weekly contact hours in lecture for a course"
// @Param offering_frequency query string false "The frequency of offering a course"
// @Success 200 {array} schema.Section "A list of sections"
func CourseSectionSearch() gin.HandlerFunc {
return func(c *gin.Context) {
courseSection("Search", c)
}
}

// @Id courseSectionById
// @Router /course/{id}/sections [get]
// @Description "Returns the all of the sections of the course with given ID"
// @Produce json
// @Param id path string true "ID of the course to get"
// @Success 200 {array} schema.Section "A list of sections"
func CourseSectionById() gin.HandlerFunc {
return func(c *gin.Context) {
courseSection("ById", c)
}
}

// get the sections of the courses, filters depending on the flag
func courseSection(flag string, c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var courseSections []schema.Section // the list of sections of the filtered courses
var courseQuery bson.M // query of the courses (or the single course)
var err error // error

// determine the course query
if flag == "Search" { // filter courses based on the query parameters
// build the key-value pair of query parameters
courseQuery, err = schema.FilterQuery[schema.Course](c)
if err != nil {
// return the validation error if there's anything wrong
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "schema validation error", Data: err.Error()})
return
}
} else if flag == "ById" { // filter the single course based on it's Id
// convert the id param with the ObjectID
courseId := c.Param("id")
courseObjId, convertIdErr := primitive.ObjectIDFromHex(courseId)
if convertIdErr != nil {
// return the id conversion error if there's error
log.WriteError(convertIdErr)
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "error with id", Data: convertIdErr.Error()})
return
}
courseQuery = bson.M{"_id": courseObjId}
} else {
// otherwise, something that messed up the server
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "internal error", Data: "broken endpoint"})
return
}

// determine the offset and limit for pagination stage
// and delete "offset" field in professorQuery
offset, limit, err := configs.GetAggregateLimit(&courseQuery, c)
if err != nil {
log.WriteErrorWithMsg(err, log.OffsetNotTypeInteger)
c.JSON(http.StatusConflict, responses.ErrorResponse{Status: http.StatusConflict, Message: "Error offset is not type integer", Data: err.Error()})
return
}

// pipeline to query the sections from the filtered courses
courseSectionPipeline := mongo.Pipeline{
// filter the courses
bson.D{{Key: "$match", Value: courseQuery}},

// paginate the courses before pulling the sections from thoses courses
bson.D{{Key: "$skip", Value: offset}}, // skip to the specified offset
bson.D{{Key: "$limit", Value: limit}}, // limit to the specified number of courses

// lookup the sections of the courses
bson.D{{Key: "$lookup", Value: bson.D{
{Key: "from", Value: "sections"},
{Key: "localField", Value: "sections"},
{Key: "foreignField", Value: "_id"},
{Key: "as", Value: "sections"},
}}},

// unwind the sections of the courses
bson.D{{Key: "$unwind", Value: bson.D{
{Key: "path", Value: "$sections"},
{Key: "preserveNullAndEmptyArrays", Value: false}, // avoid course documents that can't be replaced
}}},

// replace the courses with sections
bson.D{{Key: "$replaceWith", Value: "$sections"}},
}

// perform aggregation on the pipeline
cursor, err := courseCollection.Aggregate(ctx, courseSectionPipeline)
if err != nil {
// return error for any aggregation problem
log.WriteError(err)
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "aggregation error", Data: err.Error()})
return
}
// parse the array of sections of the course
if err = cursor.All(ctx, &courseSections); err != nil {
panic(err)
}
c.JSON(http.StatusOK, responses.MultiSectionResponse{Status: http.StatusOK, Message: "success", Data: courseSections})
}
143 changes: 143 additions & 0 deletions api/controllers/professor.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func ProfessorSearch(c *gin.Context) {
}

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

Expand Down Expand Up @@ -143,3 +144,145 @@ func ProfessorAll(c *gin.Context) {
// return result
c.JSON(http.StatusOK, responses.MultiProfessorResponse{Status: http.StatusOK, Message: "success", Data: professors})
}

// @Id professorCourseSearch
// @Router /professor/courses [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}/courses [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()

// determine the professor's query
if flag == "Search" { // if the flag is Search, filter professors based on query parameters
// build the key-value pairs of 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
}

// determine the offset and limit for pagination stage
// and delete "offset" field in professorQuery
offset, limit, err := configs.GetAggregateLimit(&professorQuery, c)
if err != nil {
log.WriteErrorWithMsg(err, log.OffsetNotTypeInteger)
c.JSON(http.StatusConflict, responses.ErrorResponse{Status: http.StatusConflict, Message: "Error offset is not type integer", Data: err.Error()})
return
}

// 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}},

// paginate the professors before pulling the courses from those professor
bson.D{{Key: "$skip", Value: offset}}, // skip to the specified offset
bson.D{{Key: "$limit", Value: limit}}, // limit to the specified number of professors

// 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: false}, // to avoid the professor documents that can't be replaced
}}},

// 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 {
log.WritePanic(err)
panic(err)
}
c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: professorCourses})
}
4 changes: 4 additions & 0 deletions api/routes/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ func CourseRoute(router *gin.Engine) {
courseGroup.GET("", controllers.CourseSearch)
courseGroup.GET(":id", controllers.CourseById)
courseGroup.GET("all", controllers.CourseAll)

// Endpoint to get the list of sections of the queried course, courses
courseGroup.GET("/sections", controllers.CourseSectionSearch())
courseGroup.GET("/:id/sections", controllers.CourseSectionById())
}
4 changes: 4 additions & 0 deletions api/routes/professor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("courses", controllers.ProfessorCourseSearch())
professorGroup.GET(":id/courses", controllers.ProfessorCourseById())
}