diff options
-rw-r--r-- | auth.go | 2 | ||||
-rw-r--r-- | courses.go | 2 | ||||
-rw-r--r-- | export.go | 229 | ||||
-rw-r--r-- | index.go | 16 | ||||
-rw-r--r-- | main.go | 1 | ||||
-rw-r--r-- | tmpl/staff.html | 2 |
6 files changed, 250 insertions, 2 deletions
@@ -193,7 +193,7 @@ func handleAuth(w http.ResponseWriter, req *http.Request) { switch { case department == "SJ Co-Curricular Activities Office 松江课外项目办公室" || department == "High School Teaching & Learning 高中教学部门": - department = "Staff" + department = staffDepartment case department == "Y9" || department == "Y10" || department == "Y11" || department == "Y12": default: @@ -99,6 +99,8 @@ var courses sync.Map /* int, *courseT */ var numCourses uint32 /* atomic */ +const staffDepartment = "Staff" + /* * Read course information from the database. This should be called during * setup. diff --git a/export.go b/export.go new file mode 100644 index 0000000..69481d1 --- /dev/null +++ b/export.go @@ -0,0 +1,229 @@ +/* + * Staff page + * + * 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 ( + "encoding/csv" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/jackc/pgx/v5" +) + +func handleExport(w http.ResponseWriter, req *http.Request) { + sessionCookie, err := req.Cookie("session") + if errors.Is(err, http.ErrNoCookie) { + wstr( + w, + http.StatusUnauthorized, + "No session cookie, which is required for this endpoint", + ) + return + } else if err != nil { + wstr(w, http.StatusBadRequest, "Error: Unable to check cookie.") + return + } + + var userID, userName, userDepartment string + err = db.QueryRow( + req.Context(), + "SELECT id, name, department FROM users WHERE session = $1", + sessionCookie.Value, + ).Scan(&userID, &userName, &userDepartment) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + wstr( + w, + http.StatusForbidden, + "Invalid session cookie", + ) + return + } + wstr( + w, + http.StatusInternalServerError, + fmt.Sprintf( + "Error: Unexpected database error: %s", + err, + ), + ) + return + } + + if userDepartment != staffDepartment { + wstr( + w, + http.StatusForbidden, + "You are not authorized to view this page", + ) + return + } + + type userCacheT struct { + Name string + StudentID string + Department string + } + userCacheMap := make(map[string]userCacheT) + + rows, err := db.Query(req.Context(), "SELECT userid, courseid FROM choices") + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Unexpected database error", + ) + return + } + output := make([][]string, 0) + for { + if !rows.Next() { + err := rows.Err() + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Unexpected database error", + ) + return + } + break + } + var currentUserID, currentUserName, currentStudentID, currentDepartment string + var currentCourseID int + err := rows.Scan(¤tUserID, ¤tCourseID) + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Unexpected database error", + ) + return + } + currentUserCache, ok := userCacheMap[currentUserID] + if ok { + currentUserName = currentUserCache.Name + currentDepartment = currentUserCache.Department + currentStudentID = currentUserCache.StudentID + } else { + var currentUserEmail string + err := db.QueryRow( + req.Context(), + "SELECT name, email, department FROM users WHERE id = $1", + currentUserID, + ).Scan( + ¤tUserName, + ¤tUserEmail, + ¤tDepartment, + ) + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Unexpected database error", + ) + return + } + before, _, found := strings.Cut(currentUserEmail, "@") + if found { + currentStudentID = before + } else { + currentStudentID = currentUserEmail /* TODO */ + } + userCacheMap[currentUserID] = userCacheT{ + Name: currentUserName, + StudentID: currentStudentID, + Department: currentDepartment, + } + } + + _course, ok := courses.Load(currentCourseID) + if !ok { + wstr( + w, + http.StatusInternalServerError, + "Reference to non-existent course", + ) + return + } + course, ok := _course.(*courseT) + if !ok { + panic("courses map has non-\"*courseT\" items") + } + if course == nil { + wstr( + w, + http.StatusInternalServerError, + "Course is nil", + ) + return + } + output = append( + output, + []string{ + currentUserName, + currentStudentID, + currentDepartment, + course.Title, + string(course.Group), + }, + ) + } + + w.Header().Set("Content-Type", "text/csv; charset=utf-8") + w.Header().Set("Content-Disposition", "attachment;filename=cca.csv") + csvWriter := csv.NewWriter(w) + err = csvWriter.Write([]string{ + "Student Name", + "Student ID", + "Grade/Year", + "Group/Activity", + "Container", + }) + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Error writing output", + ) + return + } + err = csvWriter.WriteAll(output) + if err != nil { + wstr( + w, + http.StatusInternalServerError, + "Error writing output", + ) + return + } + csvWriter.Flush() + if csvWriter.Error() != nil { + wstr( + w, + http.StatusInternalServerError, + "Error occurred flushing output", + ) + return + } +} @@ -107,6 +107,22 @@ func handleIndex(w http.ResponseWriter, req *http.Request) { return } + if userDepartment == staffDepartment { + err := tmpl.ExecuteTemplate( + w, + "staff", + struct { + Name string + }{ + userName, + }, + ) + if err != nil { + log.Println(err) + } + return + } + /* TODO: The below should be completed on-update. */ type groupT struct { Handle courseGroupT @@ -116,6 +116,7 @@ func main() { log.Println("Registering handlers") http.HandleFunc("/{$}", handleIndex) + http.HandleFunc("/export", handleExport) http.HandleFunc("/auth", handleAuth) http.HandleFunc("/ws", handleWs) diff --git a/tmpl/staff.html b/tmpl/staff.html index 820f259..4b77b00 100644 --- a/tmpl/staff.html +++ b/tmpl/staff.html @@ -47,7 +47,7 @@ This site is still a work in progress and may contain bugs! Please contact <a href="mailto:s22537@stu.ykpaoschool.cn">Runxi Yu</a> for any issues. </p> </div> - <div> + <div class="reading-width"> <a href="./export" class="btn-primary btn">Export all choices as a spreadsheet</a> </div> </body> |