-
Notifications
You must be signed in to change notification settings - Fork 2
Meal groups #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Meal groups #68
Changes from all commits
11fad22
5721ac1
3949818
3d7a167
87677cb
60c6c51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "math/rand" | ||
| "net/http" | ||
|
|
||
| "github.com/go-chi/chi" | ||
|
|
@@ -29,6 +31,11 @@ type UpdateScanTypesPayload struct { | |
| ScanTypes []store.ScanType `json:"scan_types" validate:"required,dive"` | ||
| } | ||
|
|
||
| type CreateScanResponse struct { | ||
| *store.Scan | ||
| MealGroup *string `json:"meal_group"` | ||
| } | ||
|
|
||
| // getScanTypesHandler returns all configured scan types | ||
| // | ||
| // @Summary Get scan types (Admin) | ||
|
|
@@ -61,7 +68,7 @@ func (app *application) getScanTypesHandler(w http.ResponseWriter, r *http.Reque | |
| // @Accept json | ||
| // @Produce json | ||
| // @Param scan body CreateScanPayload true "Scan to create" | ||
| // @Success 201 {object} store.Scan | ||
| // @Success 201 {object} CreateScanResponse | ||
| // @Failure 400 {object} object{error=string} | ||
| // @Failure 401 {object} object{error=string} | ||
| // @Failure 403 {object} object{error=string} | ||
|
|
@@ -148,7 +155,23 @@ func (app *application) createScanHandler(w http.ResponseWriter, r *http.Request | |
| return | ||
| } | ||
|
|
||
| if err := app.jsonResponse(w, http.StatusCreated, scan); err != nil { | ||
| if found.Category == store.ScanCategoryCheckIn { | ||
| app.assignMealGroup(r.Context(), req.UserID) | ||
| } | ||
|
|
||
| // Fetch meal group for response (non-fatal) | ||
| mealGroup, err := app.store.Application.GetMealGroupByUserID(r.Context(), req.UserID) | ||
| if err != nil && !errors.Is(err, store.ErrNotFound) { | ||
| // We don't want to fail the scan if we can't get the meal group info | ||
| app.logger.Warnw("failed to fetch meal group for scan response", "user_id", req.UserID, "error", err) | ||
| } | ||
|
|
||
| response := CreateScanResponse{ | ||
| Scan: scan, | ||
| MealGroup: mealGroup, | ||
| } | ||
|
|
||
| if err := app.jsonResponse(w, http.StatusCreated, response); err != nil { | ||
|
NoelVarghese2006 marked this conversation as resolved.
|
||
| app.internalServerError(w, r, err) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a return here
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually its technically not needed, but include it for clarity |
||
| } | ||
| } | ||
|
|
@@ -185,6 +208,33 @@ func (app *application) getUserScansHandler(w http.ResponseWriter, r *http.Reque | |
| } | ||
| } | ||
|
|
||
| func (app *application) assignMealGroup(ctx context.Context, userID string) { | ||
| groups, err := app.store.Settings.GetMealGroups(ctx) | ||
| if err != nil { | ||
| app.logger.Warnw("failed to fetch meal groups for assignment", "error", err) | ||
| return | ||
| } | ||
|
|
||
| if len(groups) == 0 { | ||
| return | ||
| } | ||
|
|
||
| hackerApp, err := app.store.Application.GetByUserID(ctx, userID) | ||
| if err != nil { | ||
| app.logger.Warnw("failed to fetch application for meal group assignment", "user_id", userID, "error", err) | ||
| return | ||
| } | ||
|
|
||
| if hackerApp.MealGroup != nil { | ||
| return // Already assigned | ||
| } | ||
|
|
||
| selectedGroup := groups[rand.Intn(len(groups))] | ||
| if err := app.store.Application.SetMealGroup(ctx, hackerApp.ID, selectedGroup); err != nil { | ||
| app.logger.Warnw("failed to set meal group on application", "app_id", hackerApp.ID, "error", err) | ||
| } | ||
| } | ||
|
|
||
| // getScanStatsHandler returns aggregate scan counts grouped by scan type | ||
| // | ||
| // @Summary Get scan statistics (Admin) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -423,6 +423,113 @@ func (app *application) setHackathonDateRange(w http.ResponseWriter, r *http.Req | |
| } | ||
| } | ||
|
|
||
| type UpdateMealGroupsPayload struct { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update meal groups requires at least one string, but we have a specific check for len(groups) == 0 in assignMealGroup. Do we want a case where there are no meal groups? |
||
| Groups []string `json:"groups" validate:"required,min=1,dive,required,min=1,max=50"` | ||
| } | ||
|
|
||
| type MealGroupsResponse struct { | ||
| Groups []string `json:"groups"` | ||
| } | ||
|
|
||
| type MealGroupStatsResponse struct { | ||
| Stats map[string]int `json:"stats"` | ||
| } | ||
|
|
||
| // getMealGroups returns the configured meal group names | ||
| // | ||
| // @Summary Get meal groups (Super Admin) | ||
| // @Description Returns the configured list of meal group names | ||
| // @Tags superadmin/settings | ||
| // @Produce json | ||
| // @Success 200 {object} MealGroupsResponse | ||
| // @Failure 401 {object} object{error=string} | ||
| // @Failure 403 {object} object{error=string} | ||
| // @Failure 500 {object} object{error=string} | ||
| // @Security CookieAuth | ||
| // @Router /superadmin/settings/meal-groups [get] | ||
| func (app *application) getMealGroups(w http.ResponseWriter, r *http.Request) { | ||
| groups, err := app.store.Settings.GetMealGroups(r.Context()) | ||
| if err != nil { | ||
| app.internalServerError(w, r, err) | ||
| return | ||
| } | ||
|
|
||
| if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse{Groups: groups}); err != nil { | ||
| app.internalServerError(w, r, err) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add return for clarity after internalServerError |
||
| } | ||
| } | ||
|
|
||
| // updateMealGroups replaces all meal group names | ||
| // | ||
| // @Summary Update meal groups (Super Admin) | ||
| // @Description Replaces the available meal group names with the provided array | ||
| // @Tags superadmin/settings | ||
| // @Accept json | ||
| // @Produce json | ||
| // @Param groups body UpdateMealGroupsPayload true "Groups to set" | ||
| // @Success 200 {object} MealGroupsResponse | ||
| // @Failure 400 {object} object{error=string} | ||
| // @Failure 401 {object} object{error=string} | ||
| // @Failure 403 {object} object{error=string} | ||
| // @Failure 500 {object} object{error=string} | ||
| // @Security CookieAuth | ||
| // @Router /superadmin/settings/meal-groups [put] | ||
| func (app *application) updateMealGroups(w http.ResponseWriter, r *http.Request) { | ||
| var req UpdateMealGroupsPayload | ||
| if err := readJSON(w, r, &req); err != nil { | ||
| app.badRequestResponse(w, r, err) | ||
| return | ||
| } | ||
|
|
||
| if err := Validate.Struct(req); err != nil { | ||
| app.badRequestResponse(w, r, err) | ||
| return | ||
| } | ||
|
|
||
| // Validate unique names | ||
| nameMap := make(map[string]bool) | ||
| for _, name := range req.Groups { | ||
| if nameMap[name] { | ||
| app.badRequestResponse(w, r, errors.New("duplicate meal group name: "+name)) | ||
| return | ||
| } | ||
| nameMap[name] = true | ||
| } | ||
|
|
||
| if err := app.store.Settings.SetMealGroups(r.Context(), req.Groups); err != nil { | ||
| app.internalServerError(w, r, err) | ||
| return | ||
| } | ||
|
|
||
| if err := app.jsonResponse(w, http.StatusOK, MealGroupsResponse(req)); err != nil { | ||
| app.internalServerError(w, r, err) | ||
| } | ||
| } | ||
|
|
||
| // getMealGroupStats returns the number of hackers assigned to each meal group | ||
| // | ||
| // @Summary Get meal group stats (Super Admin) | ||
| // @Description Returns assignment counts for each configured meal group | ||
| // @Tags superadmin/settings | ||
| // @Produce json | ||
| // @Success 200 {object} MealGroupStatsResponse | ||
| // @Failure 401 {object} object{error=string} | ||
| // @Failure 403 {object} object{error=string} | ||
| // @Failure 500 {object} object{error=string} | ||
| // @Security CookieAuth | ||
| // @Router /superadmin/settings/meal-groups/stats [get] | ||
| func (app *application) getMealGroupStats(w http.ResponseWriter, r *http.Request) { | ||
| stats, err := app.store.Settings.GetMealGroupStats(r.Context()) | ||
| if err != nil { | ||
| app.internalServerError(w, r, err) | ||
| return | ||
| } | ||
|
|
||
| if err := app.jsonResponse(w, http.StatusOK, MealGroupStatsResponse{Stats: stats}); err != nil { | ||
| app.internalServerError(w, r, err) | ||
| } | ||
| } | ||
|
|
||
| // getApplicationsEnabled returns whether applications are currently open | ||
| // | ||
| // @Summary Get applications enabled status | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in the future we should separate meal scans and check in scans to different functions, might be helpful for implementing other things based on whether a person is checked in or not.