diff --git a/api/configs/setup.go b/api/configs/setup.go index d6e990e1..2905c219 100644 --- a/api/configs/setup.go +++ b/api/configs/setup.go @@ -46,7 +46,6 @@ func ConnectDB() *mongo.Client { dbInstance = &DBSingleton{ client: client, } - }) return dbInstance.client @@ -81,24 +80,38 @@ 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 +// Returns the offsets and limit for pagination stage for aggregate endpoints pipeline +// (former offset, latter offset, limit, err) +func GetAggregateLimit(query *bson.M, c *gin.Context) (int64, int64, int64, error) { + // remove formerOffset and latterOffset field (if present) in the query + delete(*query, "former_offset") + delete(*query, "latter_offset") // parses offset if included in the query - var limit int64 = GetEnvLimit() - var offset int64 + var formerOffset, latterOffset int64 var err error - if c.Query("offset") == "" { - offset = 0 // default value + var limit int64 = GetEnvLimit() + + // get the offset on the "former" part of the endpoint + if c.Query("former_offset") == "" { + formerOffset = 0 } else { - offset, err = strconv.ParseInt(c.Query("offset"), 10, 64) + formerOffset, err = strconv.ParseInt(c.Query("former_offset"), 10, 64) if err != nil { - return offset, limit, err // default value + return 0, 0, limit, err } } - return offset, limit, err + + // get offset on the "latter" part of the endpoint + if c.Query("latter_offset") == "" { + latterOffset = 0 + } else { + latterOffset, err = strconv.ParseInt(c.Query("latter_offset"), 10, 64) + if err != nil { + return 0, 0, limit, err + } + } + + return formerOffset, latterOffset, limit, err } diff --git a/api/controllers/course.go b/api/controllers/course.go index be54090d..a122e1b4 100644 --- a/api/controllers/course.go +++ b/api/controllers/course.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "errors" "net/http" "time" @@ -75,6 +76,7 @@ func CourseSearch(c *gin.Context) { } // return result + log.Logger.Print(len(courses)) c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: courses}) } @@ -200,14 +202,16 @@ func courseSection(flag string, c *gin.Context) { } courseQuery = bson.M{"_id": courseObjId} } else { + err = errors.New("broken endpoint") // otherwise, something that messed up the server - c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "internal error", Data: "broken endpoint"}) + c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "internal error", Data: err.Error()}) return } // determine the offset and limit for pagination stage // and delete "offset" field in professorQuery - offset, limit, err := configs.GetAggregateLimit(&courseQuery, c) + formerOffset, latterOffset, 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()}) @@ -220,8 +224,8 @@ func courseSection(flag string, c *gin.Context) { 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 + bson.D{{Key: "$skip", Value: formerOffset}}, // 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{ @@ -239,6 +243,10 @@ func courseSection(flag string, c *gin.Context) { // replace the courses with sections bson.D{{Key: "$replaceWith", Value: "$sections"}}, + + // paginate the sections + bson.D{{Key: "$skip", Value: latterOffset}}, + bson.D{{Key: "$limit", Value: limit}}, } // perform aggregation on the pipeline @@ -253,5 +261,7 @@ func courseSection(flag string, c *gin.Context) { if err = cursor.All(ctx, &courseSections); err != nil { panic(err) } + + log.Logger.Print(len(courseSections)) c.JSON(http.StatusOK, responses.MultiSectionResponse{Status: http.StatusOK, Message: "success", Data: courseSections}) } diff --git a/api/controllers/grades.go b/api/controllers/grades.go index 1a57ffee..9594654f 100644 --- a/api/controllers/grades.go +++ b/api/controllers/grades.go @@ -50,7 +50,7 @@ import ( // ---- Prefix, Number, Professor // ---- Prefix, Number, Professor, SectionNumber -// 4 Functions +// 5 Functions // @Id gradeAggregationBySemester // @Router /grades/semester [get] @@ -68,6 +68,22 @@ func GradeAggregationSemester() gin.HandlerFunc { } } +// @Id gradeAggregationSectionType +// @Router /grades/semester/sectionType [get] +// @Description "Returns the grade distributions aggregated by semester and broken down into section type" +// @Produce json +// @Param prefix query string false "The course's subject prefix" +// @Param number query string false "The course's official number" +// @Param first_name query string false "The professor's first name" +// @Param last_name query string false "The professors's last name" +// @Param section_number query string false "The number of the section" +// @Success 200 {array} responses.SectionGradeResponse "An array of grade distributions for each section type for each semester included" +func GradesAggregationSectionType() gin.HandlerFunc { + return func(c *gin.Context) { + gradesAggregation("section_type", c) + } +} + // @Id gradeAggregationOverall // @Router /grades/overall [get] // @Description "Returns the overall grade distribution" @@ -89,6 +105,8 @@ func gradesAggregation(flag string, c *gin.Context) { var grades []map[string]interface{} var results []map[string]interface{} + var sectionTypeGrades []responses.GradeData // used to parse the response to section-type endpoints + var cursor *mongo.Cursor var collection *mongo.Collection var pipeline mongo.Pipeline @@ -100,7 +118,7 @@ func gradesAggregation(flag string, c *gin.Context) { var professorFind bson.D var sampleCourse schema.Course // the sample course with the given prefix and course number parameter - var sampleCourseQuery bson.D // the filter using prefix and course number to get sample course + var sampleCourseFind bson.D // the filter using prefix and course number to get sample course var err error @@ -117,6 +135,44 @@ func gradesAggregation(flag string, c *gin.Context) { professor := (first_name != "" || last_name != "") + // Find internal_course_number associated with subject_prefix and course_number, which will be used later on + sampleCourseFind = bson.D{ + {Key: "subject_prefix", Value: prefix}, + {Key: "course_number", Value: number}, + } + // Parse the queried document into the sample course + err = courseCollection.FindOne(ctx, sampleCourseFind).Decode(&sampleCourse) + // If the error is not that there is no matching documents, panic the error + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + panic(err) + } + internalCourseNumber := sampleCourse.Internal_course_number + + // arrays of regexes and section types + typeRegexes := [14]string{"0[0-9][0-9]", "0W[0-9]", "0H[0-9]", "0L[0-9]", "5H[0-9]", "1[0-9][0-9]", "2[0-9][0-9]", "3[0-9][0-9]", "5[0-9][0-9]", "6[0-9][0-9]", "7[0-9][0-9]", "HN[0-9]", "HON", "[0-9]U[0-9]"} + typeStrings := [14]string{"0xx", "0Wx", "0Hx", "0Lx", "5Hx", "1xx", "2xx", "3xx", "5xx", "6xx", "7xx", "HNx", "HON", "xUx"} + + var branches []bson.D // for without section pipeline + var withSectionBranches []bson.D // for with section pipeline + for i := 0; i < len(typeRegexes); i++ { + branches = append(branches, bson.D{ + {Key: "case", Value: bson.D{{Key: "$regexMatch", Value: bson.D{ + {Key: "input", Value: "$sections.section_number"}, + {Key: "regex", Value: typeRegexes[i]}, + }}}}, + {Key: "then", Value: typeStrings[i]}, + }) + + withSectionBranches = append(withSectionBranches, bson.D{ + {Key: "case", Value: bson.D{{Key: "$regexMatch", Value: bson.D{ + {Key: "input", Value: "$section_number"}, + {Key: "regex", Value: typeRegexes[i]}, + }}}}, + {Key: "then", Value: typeStrings[i]}, + }) + } + + // Stage to look up sections lookupSectionsStage := bson.D{ {Key: "$lookup", Value: bson.D{ {Key: "from", Value: "sections"}, @@ -126,22 +182,40 @@ func gradesAggregation(flag string, c *gin.Context) { }}, } + // Stage to unwind sections unwindSectionsStage := bson.D{{Key: "$unwind", Value: bson.D{{Key: "path", Value: "$sections"}}}} - projectGradeDistributionStage := bson.D{ - {Key: "$project", Value: bson.D{ - {Key: "_id", Value: "$sections.academic_session.name"}, - {Key: "grade_distribution", Value: "$sections.grade_distribution"}, - }}, + // Project grade distribution stage + project := bson.D{ + {Key: "_id", Value: "$sections.academic_session.name"}, + {Key: "grade_distribution", Value: "$sections.grade_distribution"}, + } + if flag == "section_type" { // add the section_type for each section + project = append(project, bson.E{Key: "section_type", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: branches}, + {Key: "default", Value: "OTHERS"}, // might be cases where section doesn't have type listed + }}, + }}) } + projectGradeDistributionStage := bson.D{{Key: "$project", Value: project}} - projectGradeDistributionWithSectionsStage := bson.D{ - {Key: "$project", Value: bson.D{ - {Key: "_id", Value: "$academic_session.name"}, - {Key: "grade_distribution", Value: "$grade_distribution"}, - }}, + // Stage to project grade distribution with section + project = bson.D{ + {Key: "_id", Value: "$academic_session.name"}, + {Key: "grade_distribution", Value: "$grade_distribution"}, } + if flag == "section_type" { // add the section_type for each section + project = append(project, bson.E{Key: "section_type", Value: bson.D{ + {Key: "$switch", Value: bson.D{ + {Key: "branches", Value: withSectionBranches}, + {Key: "default", Value: "OTHERS"}, + }}, + }}) + } + projectGradeDistributionWithSectionsStage := bson.D{{Key: "$project", Value: project}} + // Stage to unwind grade distribution unwindGradeDistributionStage := bson.D{ {Key: "$unwind", Value: bson.D{ {Key: "path", Value: "$grade_distribution"}, @@ -149,35 +223,79 @@ func gradesAggregation(flag string, c *gin.Context) { }}, } + // Stage to group grades + groupID := bson.D{ + {Key: "academic_session", Value: "$_id"}, + {Key: "ix", Value: "$ix"}, + } + // add section_type to _id so as to group grades by both academic_session and section_type + if flag == "section_type" { + groupID = append(groupID, bson.E{Key: "section_type", Value: "$section_type"}) + } groupGradesStage := bson.D{ {Key: "$group", Value: bson.D{ - {Key: "_id", Value: bson.D{ - {Key: "academic_session", Value: "$_id"}, - {Key: "ix", Value: "$ix"}, - }}, + {Key: "_id", Value: groupID}, {Key: "grades", Value: bson.D{{Key: "$push", Value: "$grade_distribution"}}}, }}, } - sortGradesStage := bson.D{ - {Key: "$sort", Value: bson.D{ + // Stage to sort grades + sort := bson.D{ + {Key: "_id.ix", Value: 1}, + {Key: "_id", Value: 1}, + } + if flag == "section_type" { + sort = bson.D{ // add section_type to id {Key: "_id.ix", Value: 1}, + {Key: "_id.section_type", Value: 1}, {Key: "_id", Value: 1}, - }}, + } } + sortGradesStage := bson.D{{Key: "$sort", Value: sort}} + // Stage to sum grades sumGradesStage := bson.D{{Key: "$addFields", Value: bson.D{{Key: "grades", Value: bson.D{{Key: "$sum", Value: "$grades"}}}}}} + // Stage to group grade distribution + groupDistributionID := bson.E{Key: "_id", Value: "$_id.academic_session"} + if flag == "section_type" { + groupDistributionID = bson.E{Key: "_id", Value: bson.D{ + {Key: "academic_section", Value: "$_id.academic_session"}, + {Key: "section_type", Value: "$_id.section_type"}, + }} + } groupGradeDistributionStage := bson.D{ {Key: "$group", Value: bson.D{ - {Key: "_id", Value: "$_id.academic_session"}, + groupDistributionID, {Key: "grade_distribution", Value: bson.D{{Key: "$push", Value: "$grades"}}}, }}, } + + // Additional stages for section-type pipeline + // Stage to sort the section-type-specific grade distributions before grouping + sortGradeDistributionsStage := bson.D{ + {Key: "$sort", Value: bson.D{ + {Key: "_id.section_type", Value: 1}, + {Key: "_id", Value: 1}, + }}, + } + + // Stage group section-type-specific grade distributions together based on semester + groupSemesterGradeDistributionsStage := bson.D{ + {Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$_id.academic_section"}, + {Key: "data", Value: bson.D{{Key: "$push", Value: bson.D{ + {Key: "type", Value: "$_id.section_type"}, + {Key: "grade_distribution", Value: "$grade_distribution"}, + }}}}, + }}, + } + switch { case prefix != "" && number == "" && section_number == "" && !professor: // Filter on Course collection = courseCollection + courseMatch = bson.D{{Key: "$match", Value: bson.M{"subject_prefix": prefix}}} pipeline = mongo.Pipeline{courseMatch, lookupSectionsStage, unwindSectionsStage, projectGradeDistributionStage, unwindGradeDistributionStage, groupGradesStage, sortGradesStage, sumGradesStage, groupGradeDistributionStage} @@ -185,22 +303,6 @@ func gradesAggregation(flag string, c *gin.Context) { // Filter on Course collection = courseCollection - // Find the internal_course_number associated with the subject_prefix and course_number - sampleCourseQuery = bson.D{ - {Key: "subject_prefix", Value: prefix}, - {Key: "course_number", Value: number}, - } - // parse the queried document into the sample course - err = collection.FindOne(ctx, sampleCourseQuery).Decode(&sampleCourse) - // If the error is not that there is no matching documents, panic the error - if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { - panic(err) - } - internalCourseNumber := sampleCourse.Internal_course_number - - // Old code that filter on combination of prefix and course number (in case we need it in the future) - // courseMatch := bson.D{{Key: "$match", Value: bson.M{"subject_prefix": prefix, "course_number": number}}} - // Query using internal_course_number of the documents courseMatch := bson.D{{Key: "$match", Value: bson.M{"internal_course_number": internalCourseNumber}}} pipeline = mongo.Pipeline{courseMatch, lookupSectionsStage, unwindSectionsStage, projectGradeDistributionStage, unwindGradeDistributionStage, groupGradesStage, sortGradesStage, sumGradesStage, groupGradeDistributionStage} @@ -209,26 +311,11 @@ func gradesAggregation(flag string, c *gin.Context) { // Filter on Course then Section collection = courseCollection - // Find the internal_course_number associated with the subject_prefix and course_number - sampleCourseQuery = bson.D{ - {Key: "subject_prefix", Value: prefix}, - {Key: "course_number", Value: number}, - } - // parse the queried document into the sample course - err = collection.FindOne(ctx, sampleCourseQuery).Decode(&sampleCourse) - // If the error is not that there is no matching documents, panic the error - if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { - panic(err) - } - internalCourseNumber := sampleCourse.Internal_course_number - - // Old code that filter on combination of prefix and course number (in case we need it in the future) - // courseMatch := bson.D{{Key: "$match", Value: bson.M{"subject_prefix": prefix, "course_number": number}}} - // Here we query all the courses with the given internal_couse_number, // and then filter on the section_number of those courses courseMatch := bson.D{{Key: "$match", Value: bson.M{"internal_course_number": internalCourseNumber}}} sectionMatch := bson.D{{Key: "$match", Value: bson.M{"sections.section_number": section_number}}} + pipeline = mongo.Pipeline{courseMatch, lookupSectionsStage, unwindSectionsStage, sectionMatch, projectGradeDistributionStage, unwindGradeDistributionStage, groupGradesStage, sortGradesStage, sumGradesStage, groupGradeDistributionStage} case prefix == "" && number == "" && section_number == "" && professor: @@ -244,7 +331,6 @@ func gradesAggregation(flag string, c *gin.Context) { professorMatch = bson.D{{Key: "$match", Value: bson.M{"first_name": first_name, "last_name": last_name}}} } - // Build grades pipeline pipeline = mongo.Pipeline{professorMatch, lookupSectionsStage, unwindSectionsStage, projectGradeDistributionStage, unwindGradeDistributionStage, groupGradesStage, sortGradesStage, sumGradesStage, groupGradeDistributionStage} case prefix != "" && professor: @@ -284,23 +370,10 @@ func gradesAggregation(flag string, c *gin.Context) { // Get valid course ids if number == "" { - // If only the prefix is provided, filter on the prefix + // if only the prefix is provided, filter only on the prefix courseFind = bson.D{{Key: "subject_prefix", Value: prefix}} } else { - // Old code that filter on combination of prefix and course number (in case we need it in the future) - // courseFind = bson.D{{Key: "subject_prefix", Value: prefix}, {Key: "course_number", Value: number}} - - // If both prefix and course_number are provided, find the associated internal_course_number to filter on - sampleCourseQuery = bson.D{ - {Key: "subject_prefix", Value: prefix}, - {Key: "course_number", Value: number}, - } - // parse the queried document into the sample course - err = courseCollection.FindOne(ctx, sampleCourseQuery).Decode(&sampleCourse) - if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { - panic(err) - } - internalCourseNumber := sampleCourse.Internal_course_number + // if both prefix and course_number are provided, filter on internal_course_number courseFind = bson.D{{Key: "internal_course_number", Value: internalCourseNumber}} } @@ -334,7 +407,6 @@ func gradesAggregation(flag string, c *gin.Context) { }}} } - // Build grades pipeline pipeline = mongo.Pipeline{sectionMatch, projectGradeDistributionWithSectionsStage, unwindGradeDistributionStage, groupGradesStage, sortGradesStage, sumGradesStage, groupGradeDistributionStage} default: @@ -342,6 +414,12 @@ func gradesAggregation(flag string, c *gin.Context) { return } + // if this is for section type, add the 2 additional stages to the pipeline + if flag == "section_type" { + pipeline = append(pipeline, sortGradeDistributionsStage) + pipeline = append(pipeline, groupSemesterGradeDistributionsStage) + } + // peform aggregation cursor, err = collection.Aggregate(ctx, pipeline) if err != nil { @@ -349,9 +427,15 @@ func gradesAggregation(flag string, c *gin.Context) { return } - // retrieve and parse all valid documents - if err = cursor.All(ctx, &grades); err != nil { - panic(err) + // retrieve and parse all valid documents to appropriate type + if flag != "section_type" { + if err = cursor.All(ctx, &grades); err != nil { + panic(err) + } + } else { + if err = cursor.All(ctx, §ionTypeGrades); err != nil { + panic(err) + } } if flag == "overall" { @@ -369,6 +453,8 @@ func gradesAggregation(flag string, c *gin.Context) { c.JSON(http.StatusOK, responses.GradeResponse{Status: http.StatusOK, Message: "success", Data: overallResponse}) } else if flag == "semester" { c.JSON(http.StatusOK, responses.GradeResponse{Status: http.StatusOK, Message: "success", Data: grades}) + } else if flag == "section_type" { + c.JSON(http.StatusOK, responses.SectionGradeResponse{Status: http.StatusOK, Message: "success", GradeData: sectionTypeGrades}) } else { c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "error", Data: "Endpoint broken"}) } diff --git a/api/controllers/professor.go b/api/controllers/professor.go index fb643865..d759cdff 100644 --- a/api/controllers/professor.go +++ b/api/controllers/professor.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "errors" "net/http" "time" @@ -84,7 +85,6 @@ func ProfessorSearch(c *gin.Context) { } // return result - print(len(professors)) c.JSON(http.StatusOK, responses.MultiProfessorResponse{Status: http.StatusOK, Message: "success", Data: professors}) } @@ -202,32 +202,13 @@ func professorCourse(flag string, c *gin.Context) { 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 + if professorQuery, err = getProfessorQuery(flag, c); err != nil { + return // if there's an error, the response will have already been thrown to the consumer, halt the funcion here } // determine the offset and limit for pagination stage // and delete "offset" field in professorQuery - offset, limit, err := configs.GetAggregateLimit(&professorQuery, c) + formerOffset, latterOffset, 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()}) @@ -240,8 +221,8 @@ func professorCourse(flag string, c *gin.Context) { 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 + bson.D{{Key: "$skip", Value: formerOffset}}, // 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{ @@ -270,6 +251,10 @@ func professorCourse(flag string, c *gin.Context) { // replace the combination of ids and courses with the courses entirely bson.D{{Key: "$replaceWith", Value: "$courses"}}, + + // paginate the courses + bson.D{{Key: "$skip", Value: latterOffset}}, + bson.D{{Key: "$limit", Value: limit}}, } // Perform aggreration on the pipeline @@ -287,3 +272,151 @@ func professorCourse(flag string, c *gin.Context) { } c.JSON(http.StatusOK, responses.MultiCourseResponse{Status: http.StatusOK, Message: "success", Data: professorCourses}) } + +// @Id professorSectionSearch +// @Router /professor/sections [get] +// @Description "Returns all of the sections 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.Section "A list of Sections" +func ProfessorSectionSearch() gin.HandlerFunc { + return func(c *gin.Context) { + professorSection("Search", c) + } +} + +// @Id professorSectionById +// @Router /professor/{id}/sections [get] +// @Description "Returns all the sections taught by the professor with given ID" +// @Produce json +// @Param id path string true "ID of the professor to get" +// @Success 200 {array} schema.Section "A list of sections" +func ProfessorSectionById() gin.HandlerFunc { + return func(c *gin.Context) { + professorSection("ById", c) + } +} + +// Get all of the sections of the professors depending on the type of flag +func professorSection(flag string, c *gin.Context) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + var professorSections []schema.Section // array of sections 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 professorQuery, err = getProfessorQuery(flag, c); err != nil { + return + } + + // determine the offset and limit for pagination stage + formerOffset, latterOffset, 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) + professorSectionPipeline := 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: formerOffset}}, // 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 sections + bson.D{{Key: "$project", Value: bson.D{{Key: "sections", Value: "$sections"}}}}, + + // unwind the sections + bson.D{{Key: "$unwind", Value: bson.D{ + {Key: "path", Value: "$sections"}, + {Key: "preserveNullAndEmptyArrays", Value: false}, // to avoid the professor documents that can't be replaced + }}}, + + // replace the combination of ids and sections with the sections entirely + bson.D{{Key: "$replaceWith", Value: "$sections"}}, + + // paginate the sections + bson.D{{Key: "$skip", Value: latterOffset}}, + bson.D{{Key: "$limit", Value: limit}}, + } + + // Perform aggreration on the pipeline + cursor, err := professorCollection.Aggregate(ctx, professorSectionPipeline) + 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 sections from these professors + if err = cursor.All(ctx, &professorSections); err != nil { + log.WritePanic(err) + panic(err) + } + c.JSON(http.StatusOK, responses.MultiSectionResponse{Status: http.StatusOK, Message: "success", Data: professorSections}) +} + +// function to determine the query of the professor based on the parameters passed from context avoid redundancy in the code +// if there's an error, throw an error response back to the API consumer and return only the error +func getProfessorQuery(flag string, c *gin.Context) (bson.M, error) { + var professorQuery bson.M + var err error + + 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 nil, err // return only the error + } + } 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, convertIdErr := primitive.ObjectIDFromHex(professorId) + if convertIdErr != nil { + log.WriteError(convertIdErr) + c.JSON(http.StatusBadRequest, responses.ErrorResponse{Status: http.StatusBadRequest, Message: "id conversion error", Data: convertIdErr.Error()}) + return nil, convertIdErr + } + professorQuery = bson.M{"_id": professorObjId} + } else { + // something wrong that messed up the server + err = errors.New("broken endpoint") + c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Status: http.StatusInternalServerError, Message: "endpoint error", Data: err.Error()}) + return nil, err + } + return professorQuery, err +} diff --git a/api/docs/docs.go b/api/docs/docs.go index 7b2a3980..b901c76c 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -129,6 +129,100 @@ const docTemplate = `{ } } }, + "/course/sections": { + "get": { + "description": "\"Returns all the sections of all the courses matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "courseSectionSearch", + "parameters": [ + { + "type": "string", + "description": "The course's official number", + "name": "course_number", + "in": "query" + }, + { + "type": "string", + "description": "The course's subject prefix", + "name": "subject_prefix", + "in": "query" + }, + { + "type": "string", + "description": "The course's title", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "The course's description", + "name": "description", + "in": "query" + }, + { + "type": "string", + "description": "The course's school", + "name": "school", + "in": "query" + }, + { + "type": "string", + "description": "The number of credit hours awarded by successful completion of the course", + "name": "credit_hours", + "in": "query" + }, + { + "type": "string", + "description": "The level of education that this course course corresponds to", + "name": "class_level", + "in": "query" + }, + { + "type": "string", + "description": "The type of class this course corresponds to", + "name": "activity_type", + "in": "query" + }, + { + "type": "string", + "description": "The grading status of this course", + "name": "grading", + "in": "query" + }, + { + "type": "string", + "description": "The internal (university) number used to reference this course", + "name": "internal_course_number", + "in": "query" + }, + { + "type": "string", + "description": "The weekly contact hours in lecture for a course", + "name": "lecture_contact_hours", + "in": "query" + }, + { + "type": "string", + "description": "The frequency of offering a course", + "name": "offering_frequency", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/course/{id}": { "get": { "description": "\"Returns the course with given ID\"", @@ -155,6 +249,35 @@ const docTemplate = `{ } } }, + "/course/{id}/sections": { + "get": { + "description": "\"Returns the all of the sections of the course with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "courseSectionById", + "parameters": [ + { + "type": "string", + "description": "ID of the course to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/grades/overall": { "get": { "description": "\"Returns the overall grade distribution\"", @@ -401,6 +524,290 @@ const docTemplate = `{ } } }, + "/professor/courses": { + "get": { + "description": "\"Returns all of the courses of all the professors matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "professorCourseSearch", + "parameters": [ + { + "type": "string", + "description": "The professor's first name", + "name": "first_name", + "in": "query" + }, + { + "type": "string", + "description": "The professor's last name", + "name": "last_name", + "in": "query" + }, + { + "type": "string", + "description": "One of the professor's title", + "name": "titles", + "in": "query" + }, + { + "type": "string", + "description": "The professor's email address", + "name": "email", + "in": "query" + }, + { + "type": "string", + "description": "The professor's phone number", + "name": "phone_number", + "in": "query" + }, + { + "type": "string", + "description": "The building of the location of the professor's office", + "name": "office.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of the location of the professor's office", + "name": "office.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of the professor's office", + "name": "office.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink pointing to the professor's official university profile", + "name": "profile_uri", + "in": "query" + }, + { + "type": "string", + "description": "A link to the image used for the professor on the professor's official university profile", + "name": "image_uri", + "in": "query" + }, + { + "type": "string", + "description": "The start date of one of the office hours meetings of the professor", + "name": "office_hours.start_date", + "in": "query" + }, + { + "type": "string", + "description": "The end date of one of the office hours meetings of the professor", + "name": "office_hours.end_date", + "in": "query" + }, + { + "type": "string", + "description": "One of the days that one of the office hours meetings of the professor", + "name": "office_hours.meeting_days", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor starts", + "name": "office_hours.start_time", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor ends", + "name": "office_hours.end_time", + "in": "query" + }, + { + "type": "string", + "description": "The modality of one of the office hours meetings of the professor", + "name": "office_hours.modality", + "in": "query" + }, + { + "type": "string", + "description": "The building of one of the office hours meetings of the professor", + "name": "office_hours.location.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of one of the office hours meetings of the professor", + "name": "office_hours.location.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of one of the office hours meetings of the professor", + "name": "office_hours.location.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "The _id of one of the sections the professor teaches", + "name": "sections", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of Courses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Course" + } + } + } + } + } + }, + "/professor/sections": { + "get": { + "description": "\"Returns all of the sections of all the professors matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "professorSectionSearch", + "parameters": [ + { + "type": "string", + "description": "The professor's first name", + "name": "first_name", + "in": "query" + }, + { + "type": "string", + "description": "The professor's last name", + "name": "last_name", + "in": "query" + }, + { + "type": "string", + "description": "One of the professor's title", + "name": "titles", + "in": "query" + }, + { + "type": "string", + "description": "The professor's email address", + "name": "email", + "in": "query" + }, + { + "type": "string", + "description": "The professor's phone number", + "name": "phone_number", + "in": "query" + }, + { + "type": "string", + "description": "The building of the location of the professor's office", + "name": "office.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of the location of the professor's office", + "name": "office.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of the professor's office", + "name": "office.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink pointing to the professor's official university profile", + "name": "profile_uri", + "in": "query" + }, + { + "type": "string", + "description": "A link to the image used for the professor on the professor's official university profile", + "name": "image_uri", + "in": "query" + }, + { + "type": "string", + "description": "The start date of one of the office hours meetings of the professor", + "name": "office_hours.start_date", + "in": "query" + }, + { + "type": "string", + "description": "The end date of one of the office hours meetings of the professor", + "name": "office_hours.end_date", + "in": "query" + }, + { + "type": "string", + "description": "One of the days that one of the office hours meetings of the professor", + "name": "office_hours.meeting_days", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor starts", + "name": "office_hours.start_time", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor ends", + "name": "office_hours.end_time", + "in": "query" + }, + { + "type": "string", + "description": "The modality of one of the office hours meetings of the professor", + "name": "office_hours.modality", + "in": "query" + }, + { + "type": "string", + "description": "The building of one of the office hours meetings of the professor", + "name": "office_hours.location.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of one of the office hours meetings of the professor", + "name": "office_hours.location.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of one of the office hours meetings of the professor", + "name": "office_hours.location.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "The _id of one of the sections the professor teaches", + "name": "sections", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of Sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/professor/{id}": { "get": { "description": "\"Returns the professor with given ID\"", @@ -427,6 +834,64 @@ const docTemplate = `{ } } }, + "/professor/{id}/courses": { + "get": { + "description": "\"Returns all the courses taught by the professor with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "professorCourseById", + "parameters": [ + { + "type": "string", + "description": "ID of the professor to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of courses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Course" + } + } + } + } + } + }, + "/professor/{id}/sections": { + "get": { + "description": "\"Returns all the sections taught by the professor with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "professorSectionById", + "parameters": [ + { + "type": "string", + "description": "ID of the professor to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/section": { "get": { "description": "\"Returns all courses matching the query's string-typed key-value pairs\"", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 8d5f08ec..70c6c06e 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -125,6 +125,100 @@ } } }, + "/course/sections": { + "get": { + "description": "\"Returns all the sections of all the courses matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "courseSectionSearch", + "parameters": [ + { + "type": "string", + "description": "The course's official number", + "name": "course_number", + "in": "query" + }, + { + "type": "string", + "description": "The course's subject prefix", + "name": "subject_prefix", + "in": "query" + }, + { + "type": "string", + "description": "The course's title", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "The course's description", + "name": "description", + "in": "query" + }, + { + "type": "string", + "description": "The course's school", + "name": "school", + "in": "query" + }, + { + "type": "string", + "description": "The number of credit hours awarded by successful completion of the course", + "name": "credit_hours", + "in": "query" + }, + { + "type": "string", + "description": "The level of education that this course course corresponds to", + "name": "class_level", + "in": "query" + }, + { + "type": "string", + "description": "The type of class this course corresponds to", + "name": "activity_type", + "in": "query" + }, + { + "type": "string", + "description": "The grading status of this course", + "name": "grading", + "in": "query" + }, + { + "type": "string", + "description": "The internal (university) number used to reference this course", + "name": "internal_course_number", + "in": "query" + }, + { + "type": "string", + "description": "The weekly contact hours in lecture for a course", + "name": "lecture_contact_hours", + "in": "query" + }, + { + "type": "string", + "description": "The frequency of offering a course", + "name": "offering_frequency", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/course/{id}": { "get": { "description": "\"Returns the course with given ID\"", @@ -151,6 +245,35 @@ } } }, + "/course/{id}/sections": { + "get": { + "description": "\"Returns the all of the sections of the course with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "courseSectionById", + "parameters": [ + { + "type": "string", + "description": "ID of the course to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/grades/overall": { "get": { "description": "\"Returns the overall grade distribution\"", @@ -397,6 +520,290 @@ } } }, + "/professor/courses": { + "get": { + "description": "\"Returns all of the courses of all the professors matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "professorCourseSearch", + "parameters": [ + { + "type": "string", + "description": "The professor's first name", + "name": "first_name", + "in": "query" + }, + { + "type": "string", + "description": "The professor's last name", + "name": "last_name", + "in": "query" + }, + { + "type": "string", + "description": "One of the professor's title", + "name": "titles", + "in": "query" + }, + { + "type": "string", + "description": "The professor's email address", + "name": "email", + "in": "query" + }, + { + "type": "string", + "description": "The professor's phone number", + "name": "phone_number", + "in": "query" + }, + { + "type": "string", + "description": "The building of the location of the professor's office", + "name": "office.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of the location of the professor's office", + "name": "office.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of the professor's office", + "name": "office.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink pointing to the professor's official university profile", + "name": "profile_uri", + "in": "query" + }, + { + "type": "string", + "description": "A link to the image used for the professor on the professor's official university profile", + "name": "image_uri", + "in": "query" + }, + { + "type": "string", + "description": "The start date of one of the office hours meetings of the professor", + "name": "office_hours.start_date", + "in": "query" + }, + { + "type": "string", + "description": "The end date of one of the office hours meetings of the professor", + "name": "office_hours.end_date", + "in": "query" + }, + { + "type": "string", + "description": "One of the days that one of the office hours meetings of the professor", + "name": "office_hours.meeting_days", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor starts", + "name": "office_hours.start_time", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor ends", + "name": "office_hours.end_time", + "in": "query" + }, + { + "type": "string", + "description": "The modality of one of the office hours meetings of the professor", + "name": "office_hours.modality", + "in": "query" + }, + { + "type": "string", + "description": "The building of one of the office hours meetings of the professor", + "name": "office_hours.location.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of one of the office hours meetings of the professor", + "name": "office_hours.location.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of one of the office hours meetings of the professor", + "name": "office_hours.location.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "The _id of one of the sections the professor teaches", + "name": "sections", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of Courses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Course" + } + } + } + } + } + }, + "/professor/sections": { + "get": { + "description": "\"Returns all of the sections of all the professors matching the query's string-typed key-value pairs\"", + "produces": [ + "application/json" + ], + "operationId": "professorSectionSearch", + "parameters": [ + { + "type": "string", + "description": "The professor's first name", + "name": "first_name", + "in": "query" + }, + { + "type": "string", + "description": "The professor's last name", + "name": "last_name", + "in": "query" + }, + { + "type": "string", + "description": "One of the professor's title", + "name": "titles", + "in": "query" + }, + { + "type": "string", + "description": "The professor's email address", + "name": "email", + "in": "query" + }, + { + "type": "string", + "description": "The professor's phone number", + "name": "phone_number", + "in": "query" + }, + { + "type": "string", + "description": "The building of the location of the professor's office", + "name": "office.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of the location of the professor's office", + "name": "office.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of the professor's office", + "name": "office.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink pointing to the professor's official university profile", + "name": "profile_uri", + "in": "query" + }, + { + "type": "string", + "description": "A link to the image used for the professor on the professor's official university profile", + "name": "image_uri", + "in": "query" + }, + { + "type": "string", + "description": "The start date of one of the office hours meetings of the professor", + "name": "office_hours.start_date", + "in": "query" + }, + { + "type": "string", + "description": "The end date of one of the office hours meetings of the professor", + "name": "office_hours.end_date", + "in": "query" + }, + { + "type": "string", + "description": "One of the days that one of the office hours meetings of the professor", + "name": "office_hours.meeting_days", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor starts", + "name": "office_hours.start_time", + "in": "query" + }, + { + "type": "string", + "description": "The time one of the office hours meetings of the professor ends", + "name": "office_hours.end_time", + "in": "query" + }, + { + "type": "string", + "description": "The modality of one of the office hours meetings of the professor", + "name": "office_hours.modality", + "in": "query" + }, + { + "type": "string", + "description": "The building of one of the office hours meetings of the professor", + "name": "office_hours.location.building", + "in": "query" + }, + { + "type": "string", + "description": "The room of one of the office hours meetings of the professor", + "name": "office_hours.location.room", + "in": "query" + }, + { + "type": "string", + "description": "A hyperlink to the UTD room locator of one of the office hours meetings of the professor", + "name": "office_hours.location.map_uri", + "in": "query" + }, + { + "type": "string", + "description": "The _id of one of the sections the professor teaches", + "name": "sections", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A list of Sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/professor/{id}": { "get": { "description": "\"Returns the professor with given ID\"", @@ -423,6 +830,64 @@ } } }, + "/professor/{id}/courses": { + "get": { + "description": "\"Returns all the courses taught by the professor with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "professorCourseById", + "parameters": [ + { + "type": "string", + "description": "ID of the professor to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of courses", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Course" + } + } + } + } + } + }, + "/professor/{id}/sections": { + "get": { + "description": "\"Returns all the sections taught by the professor with given ID\"", + "produces": [ + "application/json" + ], + "operationId": "professorSectionById", + "parameters": [ + { + "type": "string", + "description": "ID of the professor to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "A list of sections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Section" + } + } + } + } + } + }, "/section": { "get": { "description": "\"Returns all courses matching the query's string-typed key-value pairs\"", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index a9cf01fb..49630e59 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -334,6 +334,89 @@ paths: description: A course schema: $ref: '#/definitions/schema.Course' + /course/{id}/sections: + get: + description: '"Returns the all of the sections of the course with given ID"' + operationId: courseSectionById + parameters: + - description: ID of the course to get + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: A list of sections + schema: + items: + $ref: '#/definitions/schema.Section' + type: array + /course/sections: + get: + description: '"Returns all the sections of all the courses matching the query''s + string-typed key-value pairs"' + operationId: courseSectionSearch + parameters: + - description: The course's official number + in: query + name: course_number + type: string + - description: The course's subject prefix + in: query + name: subject_prefix + type: string + - description: The course's title + in: query + name: title + type: string + - description: The course's description + in: query + name: description + type: string + - description: The course's school + in: query + name: school + type: string + - description: The number of credit hours awarded by successful completion of + the course + in: query + name: credit_hours + type: string + - description: The level of education that this course course corresponds to + in: query + name: class_level + type: string + - description: The type of class this course corresponds to + in: query + name: activity_type + type: string + - description: The grading status of this course + in: query + name: grading + type: string + - description: The internal (university) number used to reference this course + in: query + name: internal_course_number + type: string + - description: The weekly contact hours in lecture for a course + in: query + name: lecture_contact_hours + type: string + - description: The frequency of offering a course + in: query + name: offering_frequency + type: string + produces: + - application/json + responses: + "200": + description: A list of sections + schema: + items: + $ref: '#/definitions/schema.Section' + type: array /grades/overall: get: description: '"Returns the overall grade distribution"' @@ -517,6 +600,240 @@ paths: description: A professor schema: $ref: '#/definitions/schema.Professor' + /professor/{id}/courses: + get: + description: '"Returns all the courses taught by the professor with given ID"' + operationId: professorCourseById + parameters: + - description: ID of the professor to get + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: A list of courses + schema: + items: + $ref: '#/definitions/schema.Course' + type: array + /professor/{id}/sections: + get: + description: '"Returns all the sections taught by the professor with given ID"' + operationId: professorSectionById + parameters: + - description: ID of the professor to get + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: A list of sections + schema: + items: + $ref: '#/definitions/schema.Section' + type: array + /professor/courses: + get: + description: '"Returns all of the courses of all the professors matching the + query''s string-typed key-value pairs"' + operationId: professorCourseSearch + parameters: + - description: The professor's first name + in: query + name: first_name + type: string + - description: The professor's last name + in: query + name: last_name + type: string + - description: One of the professor's title + in: query + name: titles + type: string + - description: The professor's email address + in: query + name: email + type: string + - description: The professor's phone number + in: query + name: phone_number + type: string + - description: The building of the location of the professor's office + in: query + name: office.building + type: string + - description: The room of the location of the professor's office + in: query + name: office.room + type: string + - description: A hyperlink to the UTD room locator of the professor's office + in: query + name: office.map_uri + type: string + - description: A hyperlink pointing to the professor's official university profile + in: query + name: profile_uri + type: string + - description: A link to the image used for the professor on the professor's + official university profile + in: query + name: image_uri + type: string + - description: The start date of one of the office hours meetings of the professor + in: query + name: office_hours.start_date + type: string + - description: The end date of one of the office hours meetings of the professor + in: query + name: office_hours.end_date + type: string + - description: One of the days that one of the office hours meetings of the + professor + in: query + name: office_hours.meeting_days + type: string + - description: The time one of the office hours meetings of the professor starts + in: query + name: office_hours.start_time + type: string + - description: The time one of the office hours meetings of the professor ends + in: query + name: office_hours.end_time + type: string + - description: The modality of one of the office hours meetings of the professor + in: query + name: office_hours.modality + type: string + - description: The building of one of the office hours meetings of the professor + in: query + name: office_hours.location.building + type: string + - description: The room of one of the office hours meetings of the professor + in: query + name: office_hours.location.room + type: string + - description: A hyperlink to the UTD room locator of one of the office hours + meetings of the professor + in: query + name: office_hours.location.map_uri + type: string + - description: The _id of one of the sections the professor teaches + in: query + name: sections + type: string + produces: + - application/json + responses: + "200": + description: A list of Courses + schema: + items: + $ref: '#/definitions/schema.Course' + type: array + /professor/sections: + get: + description: '"Returns all of the sections of all the professors matching the + query''s string-typed key-value pairs"' + operationId: professorSectionSearch + parameters: + - description: The professor's first name + in: query + name: first_name + type: string + - description: The professor's last name + in: query + name: last_name + type: string + - description: One of the professor's title + in: query + name: titles + type: string + - description: The professor's email address + in: query + name: email + type: string + - description: The professor's phone number + in: query + name: phone_number + type: string + - description: The building of the location of the professor's office + in: query + name: office.building + type: string + - description: The room of the location of the professor's office + in: query + name: office.room + type: string + - description: A hyperlink to the UTD room locator of the professor's office + in: query + name: office.map_uri + type: string + - description: A hyperlink pointing to the professor's official university profile + in: query + name: profile_uri + type: string + - description: A link to the image used for the professor on the professor's + official university profile + in: query + name: image_uri + type: string + - description: The start date of one of the office hours meetings of the professor + in: query + name: office_hours.start_date + type: string + - description: The end date of one of the office hours meetings of the professor + in: query + name: office_hours.end_date + type: string + - description: One of the days that one of the office hours meetings of the + professor + in: query + name: office_hours.meeting_days + type: string + - description: The time one of the office hours meetings of the professor starts + in: query + name: office_hours.start_time + type: string + - description: The time one of the office hours meetings of the professor ends + in: query + name: office_hours.end_time + type: string + - description: The modality of one of the office hours meetings of the professor + in: query + name: office_hours.modality + type: string + - description: The building of one of the office hours meetings of the professor + in: query + name: office_hours.location.building + type: string + - description: The room of one of the office hours meetings of the professor + in: query + name: office_hours.location.room + type: string + - description: A hyperlink to the UTD room locator of one of the office hours + meetings of the professor + in: query + name: office_hours.location.map_uri + type: string + - description: The _id of one of the sections the professor teaches + in: query + name: sections + type: string + produces: + - application/json + responses: + "200": + description: A list of Sections + schema: + items: + $ref: '#/definitions/schema.Section' + type: array /section: get: description: '"Returns all courses matching the query''s string-typed key-value diff --git a/api/responses/grades_response.go b/api/responses/grades_response.go index 06ed3aaf..43cbab96 100644 --- a/api/responses/grades_response.go +++ b/api/responses/grades_response.go @@ -5,3 +5,17 @@ type GradeResponse struct { Message string `json:"message"` Data interface{} `json:"data"` } + +type SectionGradeResponse struct { + Status int `json:"status"` + Message string `json:"message"` + GradeData []GradeData `json:"grade_data"` +} + +type GradeData struct { + Id string `bson:"_id" json:"_id"` + Data []struct { + Type string `bson:"type" json:"type"` + GradeDistribution interface{} `bson:"grade_distribution" json:"grade_distribution"` + } `bson:"data" json:"data"` +} diff --git a/api/routes/grades.go b/api/routes/grades.go index 734ee58f..58b3b39d 100644 --- a/api/routes/grades.go +++ b/api/routes/grades.go @@ -17,5 +17,6 @@ func GradesRoute(router *gin.Engine) { // ---- gradesGroup.OPTIONS("overall", controllers.Preflight) gradesGroup.GET("semester", controllers.GradeAggregationSemester()) + gradesGroup.GET("semester/section_type", controllers.GradesAggregationSectionType()) gradesGroup.GET("overall", controllers.GradesAggregationOverall()) } diff --git a/api/routes/professor.go b/api/routes/professor.go index 375ab37e..0ce099d4 100644 --- a/api/routes/professor.go +++ b/api/routes/professor.go @@ -18,4 +18,8 @@ func ProfessorRoute(router *gin.Engine) { // Endpoints to get the courses of the professors professorGroup.GET("courses", controllers.ProfessorCourseSearch()) professorGroup.GET(":id/courses", controllers.ProfessorCourseById()) + + // Endpoints to get the sections of the professors + professorGroup.GET("sections", controllers.ProfessorSectionSearch()) + professorGroup.GET(":id/sections", controllers.ProfessorSectionById()) }