summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--config.go95
-rw-r--r--course_types.go36
-rw-r--r--course_types_groups.go (renamed from course_groups.go)75
-rw-r--r--docs/cca.scfg.example20
-rw-r--r--endpoint_ws.go4
-rw-r--r--errors.go1
-rw-r--r--ws_connection.go25
-rw-r--r--wsmsg_choose.go2
-rw-r--r--wsmsg_confirm.go83
-rw-r--r--wsmsg_unchoose.go2
10 files changed, 295 insertions, 48 deletions
diff --git a/config.go b/config.go
index 4fc098c..5f9ccd8 100644
--- a/config.go
+++ b/config.go
@@ -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
diff --git a/errors.go b/errors.go
index 6f1c130..5b39e4d 100644
--- a/errors.go
+++ b/errors.go
@@ -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])