Skip to content

Commit 3611b0e

Browse files
committed
WIP
1 parent 48214ab commit 3611b0e

8 files changed

+385
-6
lines changed

e.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
func eee(ctx context.Context) (res []student_ish, err error) {
10+
ni, err := queryNameID(ctx, "SELECT name, id FROM expected_students")
11+
if err != nil {
12+
return nil, wrapError(errUnexpectedDBError, err)
13+
}
14+
15+
rows, err := db.Query(
16+
ctx,
17+
"SELECT name, email, department, confirmed FROM users",
18+
)
19+
if err != nil {
20+
return nil, wrapError(errUnexpectedDBError, err)
21+
}
22+
for {
23+
if !rows.Next() {
24+
err := rows.Err()
25+
if err != nil {
26+
return nil, wrapError(errUnexpectedDBError, err)
27+
}
28+
break
29+
}
30+
var currentUserName, currentEmail, currentDepartment string
31+
var currentConfirmed bool
32+
err := rows.Scan(
33+
&currentUserName,
34+
&currentEmail,
35+
&currentDepartment,
36+
&currentConfirmed,
37+
)
38+
if err != nil {
39+
return nil, wrapError(errUnexpectedDBError, err)
40+
}
41+
unamepart, _, _ := strings.Cut(currentEmail, "@")
42+
unamepart = strings.TrimPrefix(strings.TrimPrefix(unamepart, "s"), "S")
43+
nii, _ := strconv.ParseInt(unamepart, 10, 64)
44+
delete(ni, nii)
45+
46+
if currentDepartment == staffDepartment {
47+
continue
48+
}
49+
50+
res = append(
51+
res,
52+
student_ish{
53+
Name: currentUserName,
54+
Email: currentEmail,
55+
Department: currentDepartment,
56+
Status: strconv.FormatBool(currentConfirmed),
57+
},
58+
)
59+
}
60+
61+
for k, v := range ni {
62+
/*
63+
res = append(
64+
res,
65+
[]string{
66+
v,
67+
"s" + strconv.FormatInt(k, 10) + "@ykpaoschool.cn",
68+
"Unknown",
69+
"never logged in",
70+
},
71+
)*/
72+
res = append(
73+
res,
74+
student_ish{
75+
Name: v,
76+
Email: "s" + strconv.FormatInt(k, 10) + "@ykpaoschool.cn",
77+
Department: "Unknown",
78+
Status: "never logged in",
79+
},
80+
)
81+
}
82+
83+
return
84+
}

endpoint_export_choices.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func handleExportChoices(
112112

113113
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
114114
w.Header().Set("Content-Disposition", "attachment;filename=cca_choices.csv")
115-
_, err = w.Write([]byte{0xEF, 0xBB, 0xBF})
115+
_, err = w.Write([]byte{0xEF, 0xBB, 0xBF}) // utf8 bom because excel
116116
if err != nil {
117117
return "", -1, fmt.Errorf("write http stream: %w", err)
118118
}

endpoint_export_students.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"net/http"
1414
"strconv"
15+
"strings"
1516
)
1617

1718
func handleExportStudents(
@@ -26,6 +27,11 @@ func handleExportStudents(
2627
return "", -1, errStaffOnly
2728
}
2829

30+
ni, err := queryNameID(req.Context(), "SELECT name, id FROM expected_students")
31+
if err != nil {
32+
return "", -1, wrapError(errUnexpectedDBError, err)
33+
}
34+
2935
rows, err := db.Query(
3036
req.Context(),
3137
"SELECT name, email, department, confirmed FROM users",
@@ -53,6 +59,10 @@ func handleExportStudents(
5359
if err != nil {
5460
return "", -1, wrapError(errUnexpectedDBError, err)
5561
}
62+
unamepart, _, _ := strings.Cut(currentEmail, "@")
63+
unamepart = strings.TrimPrefix(strings.TrimPrefix(unamepart, "s"), "S")
64+
nii, _ := strconv.ParseInt(unamepart, 10, 64)
65+
delete(ni, nii)
5666

5767
if currentDepartment == staffDepartment {
5868
continue
@@ -69,6 +79,18 @@ func handleExportStudents(
6979
)
7080
}
7181

82+
for k, v := range ni {
83+
output = append(
84+
output,
85+
[]string{
86+
v,
87+
"s" + strconv.FormatInt(k, 10) + "@ykpaoschool.cn",
88+
"Unknown",
89+
"never logged in",
90+
},
91+
)
92+
}
93+
7294
w.Header().Set(
7395
"Content-Type",
7496
"text/csv; charset=utf-8",
@@ -77,7 +99,7 @@ func handleExportStudents(
7799
"Content-Disposition",
78100
"attachment;filename=cca_students.csv",
79101
)
80-
_, err = w.Write([]byte{0xEF, 0xBB, 0xBF})
102+
_, err = w.Write([]byte{0xEF, 0xBB, 0xBF}) // utf8 bom for excel
81103
if err != nil {
82104
return "", -1, fmt.Errorf("write http stream: %w", err)
83105
}

endpoint_index.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ func handleIndex(w http.ResponseWriter, req *http.Request) (string, int, error)
104104
Sched: schedule_string,
105105
}
106106
}
107-
err := tmpl.ExecuteTemplate(
107+
108+
student_ish_es, err := eee(req.Context())
109+
if err != nil {
110+
return "", -1, err
111+
}
112+
113+
err = tmpl.ExecuteTemplate(
108114
w,
109115
"staff",
110116
struct {
@@ -115,6 +121,7 @@ func handleIndex(w http.ResponseWriter, req *http.Request) (string, int, error)
115121
}
116122
StatesOr uint32
117123
Groups *map[string]groupT
124+
Students []student_ish
118125
}{
119126
username,
120127
StatesDereferenced,
@@ -126,6 +133,7 @@ func handleIndex(w http.ResponseWriter, req *http.Request) (string, int, error)
126133
return ret
127134
}(),
128135
&_groups,
136+
student_ish_es,
129137
},
130138
)
131139
if err != nil {
@@ -194,3 +202,10 @@ func handleIndex(w http.ResponseWriter, req *http.Request) (string, int, error)
194202
}
195203
return "", -1, nil
196204
}
205+
206+
type student_ish struct {
207+
Name string
208+
Email string
209+
Department string
210+
Status string
211+
}

endpoint_newstudents.go

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Add students
3+
*
4+
* Copyright (C) 2024, 2025 Runxi Yu <https://runxiyu.org>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package main
9+
10+
import (
11+
"context"
12+
"encoding/csv"
13+
"errors"
14+
"fmt"
15+
"io"
16+
"net/http"
17+
"strconv"
18+
19+
"github.com/jackc/pgx/v5"
20+
)
21+
22+
func handleNewStudents(w http.ResponseWriter, req *http.Request) (string, int, error) {
23+
if req.Method != http.MethodPost {
24+
return "", http.StatusMethodNotAllowed, errPostOnly
25+
}
26+
27+
_, _, department, err := getUserInfoFromRequest(req)
28+
if err != nil {
29+
return "", -1, err
30+
}
31+
if department != staffDepartment {
32+
return "", http.StatusForbidden, errStaffOnly
33+
}
34+
35+
file, fileHeader, err := req.FormFile("newstudents")
36+
if err != nil {
37+
return "", http.StatusBadRequest, wrapError(errFormNoFile, err)
38+
}
39+
40+
if fileHeader.Header.Get("Content-Type") != "text/csv" {
41+
return "", http.StatusBadRequest, errNotACSV
42+
}
43+
44+
csvReader := csv.NewReader(file)
45+
titleLine, err := csvReader.Read()
46+
if err != nil {
47+
return "", http.StatusBadRequest, wrapError(errCannotReadCSV, err)
48+
}
49+
if titleLine == nil {
50+
return "", -1, errUnexpectedNilCSVLine
51+
}
52+
if len(titleLine) != 2 {
53+
return "", -1, wrapAny(
54+
errBadCSVFormat,
55+
"expecting 2 fields on the first line (Name, ID)",
56+
)
57+
}
58+
var nameIndex, idIndex int = -1, -1
59+
for i, v := range titleLine {
60+
switch v {
61+
case "Name":
62+
nameIndex = i
63+
case "ID":
64+
idIndex = i
65+
}
66+
}
67+
68+
if nameIndex == -1 {
69+
return "", http.StatusBadRequest, wrapAny(
70+
errMissingCSVColumn,
71+
"Name",
72+
)
73+
}
74+
if idIndex == -1 {
75+
return "", http.StatusBadRequest, wrapAny(
76+
errMissingCSVColumn,
77+
"ID",
78+
)
79+
}
80+
81+
lineNumber := 1
82+
ok, statusCode, err := func(ctx context.Context) (
83+
retBool bool,
84+
retStatus int,
85+
retErr error,
86+
) {
87+
tx, err := db.Begin(ctx)
88+
if err != nil {
89+
return false, -1, wrapError(errUnexpectedDBError, err)
90+
}
91+
defer func() {
92+
err := tx.Rollback(ctx)
93+
if err != nil && (!errors.Is(err, pgx.ErrTxClosed)) {
94+
retBool, retStatus, retErr = false, -1, wrapError(
95+
errUnexpectedDBError,
96+
err,
97+
)
98+
return
99+
}
100+
}()
101+
_, err = tx.Exec(
102+
ctx,
103+
"DELETE FROM expected_students",
104+
)
105+
if err != nil {
106+
return false, -1, wrapError(errUnexpectedDBError, err)
107+
}
108+
109+
for {
110+
lineNumber++
111+
line, err := csvReader.Read()
112+
if err != nil {
113+
if errors.Is(err, io.EOF) {
114+
break
115+
}
116+
return false, -1, wrapError(
117+
errCannotReadCSV,
118+
err,
119+
)
120+
}
121+
if line == nil {
122+
return false, -1, wrapError(
123+
errCannotReadCSV,
124+
errUnexpectedNilCSVLine,
125+
)
126+
}
127+
if len(line) != 2 {
128+
return false, -1, wrapAny(
129+
errInsufficientFields,
130+
fmt.Sprintf(
131+
"line %d has a wrong number of items",
132+
lineNumber,
133+
),
134+
)
135+
}
136+
137+
id, err := strconv.ParseInt(line[idIndex], 10, 64)
138+
if err != nil {
139+
return false, -1, wrapAny(
140+
errBadCSVFormat,
141+
fmt.Sprintf(
142+
"line %d, ID is not a number; make sure that you only submit clean numbers e.g. 12345 as the student ID, don't use s12345/S12345",
143+
lineNumber,
144+
),
145+
)
146+
}
147+
148+
name := line[nameIndex]
149+
150+
_, err = tx.Exec(
151+
ctx,
152+
"INSERT INTO expected_students(name, id) VALUES ($1, $2)",
153+
name, id,
154+
)
155+
if err != nil {
156+
return false, -1, wrapError(
157+
errUnexpectedDBError,
158+
err,
159+
)
160+
}
161+
}
162+
err = tx.Commit(ctx)
163+
if err != nil {
164+
return false, -1, wrapError(errUnexpectedDBError, err)
165+
}
166+
return true, -1, nil
167+
}(req.Context())
168+
if !ok {
169+
return "", statusCode, err
170+
}
171+
172+
http.Redirect(w, req, "/", http.StatusSeeOther)
173+
174+
return "", -1, nil
175+
}
176+
177+
func queryNameID(ctx context.Context, query string, args ...any) (result map[int64]string, err error) {
178+
result = make(map[int64]string)
179+
var rows pgx.Rows
180+
181+
if rows, err = db.Query(ctx, query, args...); err != nil {
182+
return nil, err
183+
}
184+
defer rows.Close()
185+
186+
for rows.Next() {
187+
var name string
188+
var id int64
189+
if err = rows.Scan(&name, &id); err != nil {
190+
return nil, err
191+
}
192+
result[id] = name
193+
}
194+
return result, rows.Err()
195+
}

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func main() {
111111
setHandler("/auth", handleAuth)
112112
setHandler("/state", handleState)
113113
setHandler("/newcourses", handleNewCourses)
114+
setHandler("/newstudents", handleNewStudents)
114115

115116
var l net.Listener
116117

0 commit comments

Comments
 (0)