diff --git a/Dockerfile b/Dockerfile index ac58114..ac29354 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN go build -o bin/vitty FROM alpine:3.15 AS runner WORKDIR /usr/src/app +RUN mkdir ./data COPY --from=builder /usr/src/app/bin/vitty ./bin/vitty diff --git a/vitty-backend-api/admin/pkg/handler.go b/vitty-backend-api/admin/pkg/handler.go index 2ecd2bd..217afdb 100644 --- a/vitty-backend-api/admin/pkg/handler.go +++ b/vitty-backend-api/admin/pkg/handler.go @@ -7,7 +7,11 @@ import ( "net/http" "reflect" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/cli/commands" + "github.com/gofiber/fiber/v2" "github.com/labstack/echo/v4" + "github.com/urfave/cli/v2" ) type TemplateRenderer struct { @@ -22,17 +26,20 @@ func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c return t.templates.ExecuteTemplate(w, name, data) } -func AdminHandler(app *echo.Echo) { - group := app.Group("") - group.Use(JWTMiddleware) - group.GET("", GetModelsView) - group.GET("/:model", GetModelView) - group.GET("/:model/create", CreateItemView) - group.POST("/:model/create", CreateItem) - group.GET("/:model/:id", GetItemView) - group.GET("/:model/:id/edit", UpdateItemView) - group.PUT("/:model/:id", UpdateItem) - group.DELETE("/:model/:id", DeleteItem) +func AdminHandler(app fiber.Router) { + group := app.Group("/admin") + group.Use(middleware.JWTAuthMiddleware) + group.Use(middleware.IsAdminMiddleware) + group.Post("/empty-classrooms/seed", seedEmptyClassrooms) + + // group.Get("", GetModelsView) + // group.Get("/:model", GetModelView) + // group.Get("/:model/create", CreateItemView) + // group.Post("/:model/create", CreateItem) + // group.Get("/:model/:id", GetItemView) + // group.Get("/:model/:id/edit", UpdateItemView) + // group.Put("/:model/:id", UpdateItem) + // group.Delete("/:model/:id", DeleteItem) } func GetModelsView(c echo.Context) error { @@ -212,4 +219,18 @@ func DeleteItem(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{ "message": "Item deleted", }) + +} + +func seedEmptyClassrooms(c *fiber.Ctx) error { + err := commands.GenerateEmptyRooms(&cli.Context{}) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "classrooms seed failed", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "classrooms has been initialized", + }) } diff --git a/vitty-backend-api/admin/pkg/service.go b/vitty-backend-api/admin/pkg/service.go index 738b7f1..f1de54f 100644 --- a/vitty-backend-api/admin/pkg/service.go +++ b/vitty-backend-api/admin/pkg/service.go @@ -42,7 +42,7 @@ func (s *AdminSvc) init() { "message": "Hello World", }) }) - AdminHandler(s.WebApp) + // AdminHandler(s.WebApp) } func (s *AdminSvc) Register(model ModelInterface) { diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index b2ebc28..bdcacac 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/admin/pkg" "github.com/gofiber/fiber/v2" ) @@ -13,4 +14,5 @@ func V2Handler(api fiber.Router) { circleHandler(group) reminderHandler(group) noteHandler(group) + pkg.AdminHandler(group) } diff --git a/vitty-backend-api/api/v2/userHandler.go b/vitty-backend-api/api/v2/userHandler.go index 52e2694..49303d8 100644 --- a/vitty-backend-api/api/v2/userHandler.go +++ b/vitty-backend-api/api/v2/userHandler.go @@ -1,6 +1,12 @@ package v2 import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" "errors" "fmt" "log" @@ -20,6 +26,7 @@ func userHandler(api fiber.Router) { group.Get("/", getUsers) group.Get("/search", searchUsers) group.Get("/suggested", getSuggestedUsers) + group.Get("/emptyClassRooms", getEmptyClassRooms) group.Get("/:username", getUser) group.Delete("/:username", deleteUser) } @@ -105,3 +112,42 @@ func deleteUser(c *fiber.Ctx) error { "detail": "User deleted successfully", }) } + +func getEmptyClassRooms(c *fiber.Ctx) error { + filterSlot := strings.ToUpper(c.Query("slot")) + + if filterSlot == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "please mention the slot", + }) + } + + file, err := os.Open("./data/freeClasses.json") + if err != nil { + log.Printf("Error opening file: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "please contact vitty support", + }) + } + defer file.Close() + + var freeClasses map[string]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) + } + + response := freeClasses[filterSlot] + + if response == nil { + response = "" + } + + return c.Status(fiber.StatusOK).JSON(map[string]interface{}{ + filterSlot: response, + }) +} diff --git a/vitty-backend-api/cli/commands/timetableCommands.go b/vitty-backend-api/cli/commands/timetableCommands.go index 99d2a41..a432bed 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: "Generates empty classrooms file", + Action: GenerateEmptyRooms, + }, { Name: "seed-course-table", @@ -44,7 +52,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 { @@ -81,6 +89,87 @@ func fixSlotTimes(c *cli.Context) error { return nil } +func GenerateEmptyRooms(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) + return err + } + + fmt.Println(green, "Complete", reset) + + return nil +} + func seedCourseTable(c *cli.Context) error { reset := "\033[0m" red := "\033[31m" 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 1ff54cd..3a2c164 100644 --- a/vitty-backend-api/internal/database/initialize.go +++ b/vitty-backend-api/internal/database/initialize.go @@ -20,6 +20,7 @@ func Connect(debug string, dbUrls string) { }) } else { DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), TranslateError: true, }) } 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"},