diff options
Diffstat (limited to '')
-rw-r--r-- | config.go | 95 | ||||
-rw-r--r-- | course_types.go | 36 | ||||
-rw-r--r-- | course_types_groups.go (renamed from course_groups.go) | 75 | ||||
-rw-r--r-- | docs/cca.scfg.example | 20 | ||||
-rw-r--r-- | endpoint_ws.go | 4 | ||||
-rw-r--r-- | errors.go | 1 | ||||
-rw-r--r-- | ws_connection.go | 25 | ||||
-rw-r--r-- | wsmsg_choose.go | 2 | ||||
-rw-r--r-- | wsmsg_confirm.go | 83 | ||||
-rw-r--r-- | wsmsg_unchoose.go | 2 |
10 files changed, 295 insertions, 48 deletions
@@ -69,6 +69,24 @@ var configWithPointers struct { UsemDelayShiftBits *int `scfg:"usem_delay_shift_bits"` PropagateImmediate *bool `scfg:"propagate_immediate"` } `scfg:"perf"` + Req struct { + Y9 struct { + Sport *int `scfg:"sport"` + NonSport *int `scfg:"non_sport"` + } `scfg:"y9"` + Y10 struct { + Sport *int `scfg:"sport"` + NonSport *int `scfg:"non_sport"` + } `scfg:"y10"` + Y11 struct { + Sport *int `scfg:"sport"` + NonSport *int `scfg:"non_sport"` + } `scfg:"y11"` + Y12 struct { + Sport *int `scfg:"sport"` + NonSport *int `scfg:"non_sport"` + } `scfg:"y12"` + } `scfg:"req"` } var config struct { @@ -103,7 +121,25 @@ var config struct { ReadHeaderTimeout int UsemDelayShiftBits int PropagateImmediate bool - } `scfg:"perf"` + } + Req struct { + Y9 struct { + Sport int + NonSport int + } + Y10 struct { + Sport int + NonSport int + } + Y11 struct { + Sport int + NonSport int + } + Y12 struct { + Sport int + NonSport int + } + } } func fetchConfig(path string) (retErr error) { @@ -259,5 +295,62 @@ func fetchConfig(path string) (retErr error) { } config.Perf.PropagateImmediate = *(configWithPointers.Perf.PropagateImmediate) + if configWithPointers.Req.Y9.Sport == nil { + return fmt.Errorf( + "%w: req.y9.sport", + errMissingConfigValue, + ) + } + config.Req.Y9.Sport = *(configWithPointers.Req.Y9.Sport) + if configWithPointers.Req.Y9.NonSport == nil { + return fmt.Errorf( + "%w: req.y9.non_sport", + errMissingConfigValue, + ) + } + config.Req.Y9.NonSport = *(configWithPointers.Req.Y9.NonSport) + if configWithPointers.Req.Y10.Sport == nil { + return fmt.Errorf( + "%w: req.y10.non_sport", + errMissingConfigValue, + ) + } + config.Req.Y10.Sport = *(configWithPointers.Req.Y10.Sport) + if configWithPointers.Req.Y10.NonSport == nil { + return fmt.Errorf( + "%w: req.y10.non_sport", + errMissingConfigValue, + ) + } + config.Req.Y10.NonSport = *(configWithPointers.Req.Y10.NonSport) + if configWithPointers.Req.Y11.Sport == nil { + return fmt.Errorf( + "%w: req.y11.sport", + errMissingConfigValue, + ) + } + config.Req.Y11.Sport = *(configWithPointers.Req.Y11.Sport) + if configWithPointers.Req.Y11.NonSport == nil { + return fmt.Errorf( + "%w: req.y11.non_sport", + errMissingConfigValue, + ) + } + config.Req.Y11.NonSport = *(configWithPointers.Req.Y11.NonSport) + if configWithPointers.Req.Y12.Sport == nil { + return fmt.Errorf( + "%w: req.y12.sport", + errMissingConfigValue, + ) + } + config.Req.Y12.Sport = *(configWithPointers.Req.Y12.Sport) + if configWithPointers.Req.Y12.NonSport == nil { + return fmt.Errorf( + "%w: req.y12.non_sport", + errMissingConfigValue, + ) + } + config.Req.Y12.NonSport = *(configWithPointers.Req.Y12.NonSport) + return nil } diff --git a/course_types.go b/course_types.go deleted file mode 100644 index 2087735..0000000 --- a/course_types.go +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Course types - * - * Copyright (C) 2024 Runxi Yu <https://runxiyu.org> - * SPDX-License-Identifier: AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package main - -const ( - sport string = "Sport" - nonSport string = "Non-sport" -) - -var courseTypes = map[string]struct{}{ - sport: {}, - nonSport: {}, -} - -func checkCourseType(ct string) bool { - _, ok := courseTypes[ct] - return ok -} diff --git a/course_groups.go b/course_types_groups.go index 598fb02..3c298ca 100644 --- a/course_groups.go +++ b/course_types_groups.go @@ -1,5 +1,5 @@ /* - * Course groups + * Course types and groups * * Copyright (C) 2024 Runxi Yu <https://runxiyu.org> * SPDX-License-Identifier: AGPL-3.0-or-later @@ -25,6 +25,70 @@ import ( "fmt" ) +/* Course types, e.g. Sport */ + +const ( + sport string = "Sport" + nonSport string = "Non-sport" +) + +var courseTypes = map[string]struct{}{ + sport: {}, + nonSport: {}, +} + +func checkCourseType(ct string) bool { + _, ok := courseTypes[ct] + return ok +} + +type userCourseTypesT map[string]int + +func getCourseTypeMinimumForYearGroup(yearGroup, courseType string) (int, error) { + switch yearGroup { + case "Y9": + switch courseType { + case sport: + return config.Req.Y9.Sport, nil + case nonSport: + return config.Req.Y9.NonSport, nil + default: + return 0, errInvalidCourseType + } + case "Y10": + switch courseType { + case sport: + return config.Req.Y10.Sport, nil + case nonSport: + return config.Req.Y10.NonSport, nil + default: + return 0, errInvalidCourseType + } + case "Y11": + switch courseType { + case sport: + return config.Req.Y11.Sport, nil + case nonSport: + return config.Req.Y11.NonSport, nil + default: + return 0, errInvalidCourseType + } + case "Y12": + switch courseType { + case sport: + return config.Req.Y12.Sport, nil + case nonSport: + return config.Req.Y12.NonSport, nil + default: + return 0, errInvalidCourseType + } + default: + return 0, errNoSuchYearGroup + } +} + +/* Course groups, e.g. MW1 */ + type userCourseGroupsT map[string]struct{} func checkCourseGroup(cg string) bool { @@ -50,8 +114,11 @@ var courseGroups = map[string]string{ tt3: "Tuesday/Thursday CCA3", } -func populateUserCourseGroups( +/* Populate both */ + +func populateUserCourseTypesAndGroups( ctx context.Context, + userCourseTypes *userCourseTypesT, userCourseGroups *userCourseGroupsT, userID string, ) error { @@ -85,7 +152,7 @@ func populateUserCourseGroups( err, ) } - var thisGroupName string + var thisGroupName, thisTypeName string _course, ok := courses.Load(thisCourseID) if !ok { return fmt.Errorf( @@ -99,6 +166,7 @@ func populateUserCourseGroups( panic("courses map has non-\"*courseT\" items") } thisGroupName = course.Group + thisTypeName = course.Type if _, ok := (*userCourseGroups)[thisGroupName]; ok { return fmt.Errorf( "%w: user %v, group %v", @@ -108,6 +176,7 @@ func populateUserCourseGroups( ) } (*userCourseGroups)[thisGroupName] = struct{}{} + (*userCourseTypes)[thisTypeName]++ } return nil } diff --git a/docs/cca.scfg.example b/docs/cca.scfg.example index 39c0b23..5ce05d3 100644 --- a/docs/cca.scfg.example +++ b/docs/cca.scfg.example @@ -100,3 +100,23 @@ perf { # propagated through a queue, rather than usems? senq 10 } + +# Minimum course requirements for each year group +req { + y9 { + sport 2 + non_sport 1 + } + y10 { + sport 2 + non_sport 1 + } + y11 { + sport 1 + non_sport 1 + } + y12 { + sport 1 + non_sport 1 + } +} diff --git a/endpoint_ws.go b/endpoint_ws.go index 45cee60..a46ea27 100644 --- a/endpoint_ws.go +++ b/endpoint_ws.go @@ -53,7 +53,7 @@ func handleWs(w http.ResponseWriter, req *http.Request) { _ = c.CloseNow() }() - userID, _, _, err := getUserInfoFromRequest(req) + userID, _, department, err := getUserInfoFromRequest(req) if err != nil { err := writeText(req.Context(), c, "U") if err != nil { @@ -62,7 +62,7 @@ func handleWs(w http.ResponseWriter, req *http.Request) { return } - err = handleConn(req.Context(), c, userID) + err = handleConn(req.Context(), c, userID, department) if err != nil { log.Println(err) return @@ -50,6 +50,7 @@ var ( errCannotCheckCookie = errors.New("error checking cookie") errNoCookie = errors.New("no cookie found") errNoSuchUser = errors.New("no such user") + errNoSuchYearGroup = errors.New("no such year group") ) func wrapError(a, b error) error { diff --git a/ws_connection.go b/ws_connection.go index e3cc6d2..7a0649b 100644 --- a/ws_connection.go +++ b/ws_connection.go @@ -51,6 +51,7 @@ func handleConn( ctx context.Context, c *websocket.Conn, userID string, + department string, ) (retErr error) { send := make(chan string, config.Perf.SendQ) chanPool.Store(userID, &send) @@ -142,16 +143,13 @@ func handleConn( }() } - /* - * userCourseGroups stores whether the user has already chosen a course - * in the courseGroup. - */ var userCourseGroups userCourseGroupsT = make(map[string]struct{}) - err := populateUserCourseGroups(newCtx, &userCourseGroups, userID) + var userCourseTypes userCourseTypesT = make(map[string]int) + err := populateUserCourseTypesAndGroups(newCtx, &userCourseTypes, &userCourseGroups, userID) if err != nil { return reportError( fmt.Sprintf( - "cannot populate user course groups: %v", + "cannot populate user course types/groups: %v", err, ), ) @@ -311,6 +309,7 @@ func handleConn( mar, userID, &userCourseGroups, + &userCourseTypes, ) if err != nil { return err @@ -323,6 +322,20 @@ func handleConn( mar, userID, &userCourseGroups, + &userCourseTypes, + ) + if err != nil { + return err + } + case "C": + err := messageConfirm( + newCtx, + c, + reportError, + mar, + userID, + department, + &userCourseTypes, ) if err != nil { return err diff --git a/wsmsg_choose.go b/wsmsg_choose.go index 9b8708b..79fee35 100644 --- a/wsmsg_choose.go +++ b/wsmsg_choose.go @@ -40,6 +40,7 @@ func messageChooseCourse( mar []string, userID string, userCourseGroups *userCourseGroupsT, + userCourseTypes *userCourseTypesT, ) error { if atomic.LoadUint32(&state) != 2 { err := writeText(ctx, c, "E :Course selections are not open") @@ -171,6 +172,7 @@ func messageChooseCourse( * concurrently for one connection. */ (*userCourseGroups)[course.Group] = struct{}{} + (*userCourseTypes)[course.Type]++ err = writeText(ctx, c, "Y "+mar[1]) if err != nil { diff --git a/wsmsg_confirm.go b/wsmsg_confirm.go new file mode 100644 index 0000000..f20aa2f --- /dev/null +++ b/wsmsg_confirm.go @@ -0,0 +1,83 @@ +/* + * Handle the "C" message + * + * Copyright (C) 2024 Runxi Yu <https://runxiyu.org> + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package main + +import ( + "context" + "fmt" + + "github.com/coder/websocket" +) + +func messageConfirm( + ctx context.Context, + c *websocket.Conn, + reportError reportErrorT, + mar []string, + userID string, + department string, + userCourseTypes *userCourseTypesT, +) error { + _ = mar + + select { + case <-ctx.Done(): + return wrapError( + errContextCancelled, + ctx.Err(), + ) + default: + } + + for courseType := range courseTypes { + minimum, err := getCourseTypeMinimumForYearGroup(department, courseType) + if err != nil { + return reportError("Invalid year group or course type, something is broken") + } + if (*userCourseTypes)[courseType] < minimum { + return writeText( + ctx, + c, + fmt.Sprintf( + "NC :You chose %d out of required %d of type %s", + (*userCourseTypes)[courseType], + minimum, + courseType, + ), + ) + } + } + + _, err := db.Exec( + ctx, + "UPDATE users SET confirmed = true WHERE id = $1", + userID, + ) + if err != nil { + return reportError("error updating database setting confirmation") + } + + return writeText( + ctx, + c, + "YC", + ) +} diff --git a/wsmsg_unchoose.go b/wsmsg_unchoose.go index a99e3f4..6e85bab 100644 --- a/wsmsg_unchoose.go +++ b/wsmsg_unchoose.go @@ -35,6 +35,7 @@ func messageUnchooseCourse( mar []string, userID string, userCourseGroups *userCourseGroupsT, + userCourseTypes *userCourseTypesT, ) error { if atomic.LoadUint32(&state) != 2 { err := writeText(ctx, c, "E :Course selections are not open") @@ -114,6 +115,7 @@ func messageUnchooseCourse( return reportError("inconsistent user course groups") } delete(*userCourseGroups, course.Group) + (*userCourseTypes)[course.Type]-- } err = writeText(ctx, c, "N "+mar[1]) |