diff --git a/vitty-backend-api/api/v2/userHandler.go b/vitty-backend-api/api/v2/userHandler.go index a0a5a76..7997098 100644 --- a/vitty-backend-api/api/v2/userHandler.go +++ b/vitty-backend-api/api/v2/userHandler.go @@ -1,7 +1,10 @@ package v2 import ( + "encoding/json" "fmt" + "log" + "os" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" @@ -19,6 +22,7 @@ func userHandler(api fiber.Router) { group.Get("/suggested", getSuggestedUsers) group.Get("/:username", getUser) group.Delete("/:username", deleteUser) + group.Get("/emptyClassRooms", getEmptyClassRooms) } func searchUsers(c *fiber.Ctx) error { @@ -83,3 +87,24 @@ func deleteUser(c *fiber.Ctx) error { "detail": "User deleted successfully", }) } + +func getEmptyClassRooms(c *fiber.Ctx) error { + file, err := os.Open("./data/freeClasses.json") + if err != nil { + log.Printf("Error opening file: %v", err) + return c.Status(fiber.StatusInternalServerError).SendString("Please contact vitty support") + } + defer file.Close() + + var freeClasses interface{} + decoder := json.NewDecoder(file) + err = decoder.Decode(&freeClasses) + if err != nil { + log.Fatalf("Error decoding JSON: %v", err) + } + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) + } + + return c.Status(fiber.StatusOK).JSON(freeClasses) +} diff --git a/vitty-backend-api/cli/commands/timetableCommands.go b/vitty-backend-api/cli/commands/timetableCommands.go index daac625..1ec5265 100644 --- a/vitty-backend-api/cli/commands/timetableCommands.go +++ b/vitty-backend-api/cli/commands/timetableCommands.go @@ -1,7 +1,9 @@ package commands import ( + "encoding/json" "fmt" + "os" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" @@ -22,6 +24,12 @@ var TimetableCommands = []*cli.Command{ Usage: "Fix slot times", Action: fixSlotTimes, }, + { + Name: "empty-rooms", + Aliases: []string{"er"}, + Usage: "Shows empty classrooms", + Action: getEmptyRooms, + }, } func parseTimetable(c *cli.Context) error { @@ -37,7 +45,7 @@ func parseTimetable(c *cli.Context) error { fmt.Println("Parsed data: ") fmt.Println(timetableV1) - fmt.Println("\n\n") + fmt.Print("\n\n") var timetableSlots []models.Slot for _, slot := range timetableV1 { @@ -73,3 +81,83 @@ func fixSlotTimes(c *cli.Context) error { } return nil } + +func getEmptyRooms(c *cli.Context) error { + reset := "\033[0m" + red := "\033[31m" + green := "\033[32m" + cyan := "\033[36m " + + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Extracting Class details... ") + + err := database.DB.Exec(` + Drop table IF EXISTS joinData; + CREATE TABLE joinData ( + class text, + slots JSONB + ); + + INSERT INTO joinData (class, slots) + SELECT + elems.data->>'venue' AS venue, + jsonb_agg( DISTINCT elems.data->>'slot') AS slots + FROM + timetables, + jsonb_array_elements(timetables.slots::jsonb) AS elems(data) + GROUP BY + elems.data->>'venue'; + `).Error + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + fmt.Println(green, "Complete", reset) + + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Looking for empty classes... ") + + emptyClassRoomsJson := make(map[string]interface{}) + + for _, slot := range models.TimetableSlots { + freeClasses, err := findEmptyClassRooms(slot) + + if err != nil { + fmt.Println(red, "Failed") + fmt.Printf("Slot %s was not able to be processed\nError: %s %s", slot, err, reset) + } + + emptyClassRoomsJson[slot] = freeClasses + } + + fmt.Println(green, "Complete", reset) + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Saving result... ") + + jsonData, err := json.Marshal(emptyClassRoomsJson) + if err != nil { + fmt.Println("Error encoding JSON:", err) + } + + err = database.DB.Exec(` + Drop table joindata; + `).Error + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + err = os.WriteFile("./data/freeClasses.json", jsonData, 0644) + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + fmt.Println(green, "Complete", reset) + + return nil +} diff --git a/vitty-backend-api/cli/commands/utils.go b/vitty-backend-api/cli/commands/utils.go new file mode 100644 index 0000000..d0b1223 --- /dev/null +++ b/vitty-backend-api/cli/commands/utils.go @@ -0,0 +1,42 @@ +package commands + +import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" +) + +func findEmptyClassRooms(slot string) ([]string, error) { + var freeClasses []string + total := 0 + offset := 0 + limit := 1000 + + query := database.DB. + Table("joindata"). + Where("NOT (slots @> '[\"?\"]')", database.DB.Raw(slot)). + Where("slots::text !~ '\\[\"L.*\"\\]'") + + err := query. + Select("COUNT(class)"). + Scan(&total).Error + + if err != nil { + return freeClasses, err + } + + for total >= 0 { + err := query. + Select("class"). + Limit(limit). + Offset(offset). + Find(&freeClasses).Error + + if err != nil { + return freeClasses, err + } + + total -= limit + offset += limit + } + + return freeClasses, nil +} diff --git a/vitty-backend-api/internal/database/initialize.go b/vitty-backend-api/internal/database/initialize.go index 4a4d834..14bb4b0 100644 --- a/vitty-backend-api/internal/database/initialize.go +++ b/vitty-backend-api/internal/database/initialize.go @@ -18,7 +18,9 @@ func Connect(debug string, dbUrls string) { Logger: logger.Default.LogMode(logger.Info), }) } else { - DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{}) + DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) } if err != nil { diff --git a/vitty-backend-api/internal/models/slots.go b/vitty-backend-api/internal/models/slots.go index 72dbb9d..ffdfcdd 100644 --- a/vitty-backend-api/internal/models/slots.go +++ b/vitty-backend-api/internal/models/slots.go @@ -1,5 +1,9 @@ package models +var TimetableSlots = []string{"A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2", "E1", "E2", "F1", "F2", "G1", "G2", + "TA1", "TA2", "TAA1", "TAA2", "TB1", "TB2", "TBB2", "TC1", "TC2", "TCC1", "TCC2", "TD1", "TD2", "TDD2", "TE1", "TE2", "TF1", "TF2", + "TG1", "TG2", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "W21", "W22", "X11", "X12", "X21", "Y11", "Y12", "Y21", "Z21"} + var DailySlots = map[string]map[string][]string{ "Monday": { "Theory": {"A1", "F1", "D1", "TB1", "TG1", "A2", "F2", "D2", "TB2", "TG2", "V3"},