blob: 6f83bfe7f48b64fef095e7ad1ebfa14d313b305d [file] [log] [blame]
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08001package requests
2
3import (
Philipp Schraderfae8a7e2022-03-13 22:51:54 -07004 "encoding/base64"
Philipp Schraderd3fac192022-03-02 20:35:46 -08005 "errors"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08006 "fmt"
7 "io"
Philipp Schraderfae8a7e2022-03-13 22:51:54 -07008 "log"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08009 "net/http"
Emily Markovabf24c9e2023-02-08 20:31:11 -080010 "sort"
Philipp Schraderd3fac192022-03-02 20:35:46 -080011 "strconv"
12 "strings"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080013
Philipp Schrader8747f1b2022-02-23 23:56:22 -080014 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080015 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
Emily Markova290147d2023-03-03 22:40:06 -080016 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting"
17 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_2023_data_scouting_response"
Filip Kujawaf882e022022-12-14 13:14:08 -080018 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
19 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080020 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
21 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
Filip Kujawaf882e022022-12-14 13:14:08 -080022 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes"
23 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
Philipp Schraderacf96232022-03-01 22:03:30 -080024 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
25 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response"
Alex Perry81f96ba2022-03-13 18:26:19 -070026 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
27 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
Milo Lin1d59f0c2022-06-22 20:30:58 -070028 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
29 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
Sabina Leaver759090b2023-01-14 20:42:56 -080030 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
31 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080032 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
Philipp Schrader30005e42022-03-06 13:53:58 -080033 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
Filip Kujawa210a03b2022-11-24 14:41:11 -080034 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
35 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
Alex Perry81f96ba2022-03-13 18:26:19 -070036 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
37 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
Milo Lin1d59f0c2022-06-22 20:30:58 -070038 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
39 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080040 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
41 flatbuffers "github.com/google/flatbuffers/go"
42)
43
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080044type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
Philipp Schrader30005e42022-03-06 13:53:58 -080045type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080046type RequestAllMatches = request_all_matches.RequestAllMatches
47type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
Filip Kujawaf882e022022-12-14 13:14:08 -080048type RequestAllDriverRankings = request_all_driver_rankings.RequestAllDriverRankings
49type RequestAllDriverRankingsResponseT = request_all_driver_rankings_response.RequestAllDriverRankingsResponseT
50type RequestAllNotes = request_all_notes.RequestAllNotes
51type RequestAllNotesResponseT = request_all_notes_response.RequestAllNotesResponseT
Philipp Schraderacf96232022-03-01 22:03:30 -080052type RequestDataScouting = request_data_scouting.RequestDataScouting
53type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
Emily Markova290147d2023-03-03 22:40:06 -080054type Request2023DataScouting = request_2023_data_scouting.Request2023DataScouting
55type Request2023DataScoutingResponseT = request_2023_data_scouting_response.Request2023DataScoutingResponseT
Alex Perry81f96ba2022-03-13 18:26:19 -070056type SubmitNotes = submit_notes.SubmitNotes
57type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
58type RequestNotesForTeam = request_notes_for_team.RequestNotesForTeam
59type RequestNotesForTeamResponseT = request_notes_for_team_response.RequestNotesForTeamResponseT
Milo Lin1d59f0c2022-06-22 20:30:58 -070060type RequestShiftSchedule = request_shift_schedule.RequestShiftSchedule
61type RequestShiftScheduleResponseT = request_shift_schedule_response.RequestShiftScheduleResponseT
62type SubmitShiftSchedule = submit_shift_schedule.SubmitShiftSchedule
63type SubmitShiftScheduleResponseT = submit_shift_schedule_response.SubmitShiftScheduleResponseT
Filip Kujawa210a03b2022-11-24 14:41:11 -080064type SubmitDriverRanking = submit_driver_ranking.SubmitDriverRanking
65type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
Sabina Leaver759090b2023-01-14 20:42:56 -080066type SubmitActions = submit_actions.SubmitActions
67type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080068
Philipp Schrader8747f1b2022-02-23 23:56:22 -080069// The interface we expect the database abstraction to conform to.
70// We use an interface here because it makes unit testing easier.
71type Database interface {
Emily Markovabf24c9e2023-02-08 20:31:11 -080072 AddToMatch(db.TeamMatch) error
Milo Lin1d59f0c2022-06-22 20:30:58 -070073 AddToShift(db.Shift) error
Philipp Schrader8747f1b2022-02-23 23:56:22 -080074 AddToStats(db.Stats) error
Emily Markova290147d2023-03-03 22:40:06 -080075 AddToStats2023(db.Stats2023) error
Emily Markovabf24c9e2023-02-08 20:31:11 -080076 ReturnMatches() ([]db.TeamMatch, error)
Filip Kujawaf882e022022-12-14 13:14:08 -080077 ReturnAllNotes() ([]db.NotesData, error)
78 ReturnAllDriverRankings() ([]db.DriverRankingData, error)
Milo Lin1d59f0c2022-06-22 20:30:58 -070079 ReturnAllShifts() ([]db.Shift, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080080 ReturnStats() ([]db.Stats, error)
Emily Markova290147d2023-03-03 22:40:06 -080081 ReturnStats2023() ([]db.Stats2023, error)
Philipp Schrader0f7b6362023-03-11 14:02:48 -080082 ReturnStats2023ForTeam(teamNumber string, matchNumber int32, setNumber int32, compLevel string) ([]db.Stats2023, error)
Milo Lin1d59f0c2022-06-22 20:30:58 -070083 QueryAllShifts(int) ([]db.Shift, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080084 QueryStats(int) ([]db.Stats, error)
Philipp Schradereecb8962022-06-01 21:02:42 -070085 QueryNotes(int32) ([]string, error)
Filip Kujawaf947cb42022-11-21 10:00:30 -080086 AddNotes(db.NotesData) error
Filip Kujawa210a03b2022-11-24 14:41:11 -080087 AddDriverRanking(db.DriverRankingData) error
Philipp Schrader8747f1b2022-02-23 23:56:22 -080088}
89
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080090// Handles unknown requests. Just returns a 404.
91func unknown(w http.ResponseWriter, req *http.Request) {
92 w.WriteHeader(http.StatusNotFound)
93}
94
95func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
96 builder := flatbuffers.NewBuilder(1024)
97 builder.Finish((&error_response.ErrorResponseT{
98 ErrorMessage: errorMessage,
99 }).Pack(builder))
100 w.WriteHeader(statusCode)
101 w.Write(builder.FinishedBytes())
102}
103
104func respondNotImplemented(w http.ResponseWriter) {
105 respondWithError(w, http.StatusNotImplemented, "")
106}
107
Philipp Schraderb7e75932022-03-26 16:18:34 -0700108func parseRequest[T interface{}](w http.ResponseWriter, buf []byte, requestName string, parser func([]byte, flatbuffers.UOffsetT) *T) (*T, bool) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800109 success := true
110 defer func() {
111 if r := recover(); r != nil {
Philipp Schraderb7e75932022-03-26 16:18:34 -0700112 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse %s: %v", requestName, r))
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800113 success = false
114 }
115 }()
Philipp Schraderb7e75932022-03-26 16:18:34 -0700116 result := parser(buf, 0)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800117 return result, success
118}
119
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700120// Parses the authorization information that the browser inserts into the
121// headers. The authorization follows this format:
122//
Philipp Schrader35bb1532023-03-05 13:49:12 -0800123// req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700124func parseUsername(req *http.Request) string {
125 auth, ok := req.Header["Authorization"]
126 if !ok {
127 return "unknown"
128 }
129
130 parts := strings.Split(auth[0], " ")
131 if !(len(parts) == 2 && parts[0] == "Basic") {
132 return "unknown"
133 }
134
135 info, err := base64.StdEncoding.DecodeString(parts[1])
136 if err != nil {
137 log.Println("ERROR: Failed to parse Basic authentication.")
138 return "unknown"
139 }
140
141 loginParts := strings.Split(string(info), ":")
142 if len(loginParts) != 2 {
143 return "unknown"
144 }
145 return loginParts[0]
146}
147
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800148// Handles a SubmitDataScouting request.
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800149type submitDataScoutingHandler struct {
150 db Database
151}
152
153func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700154 // Get the username of the person submitting the data.
155 username := parseUsername(req)
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700156
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800157 requestBytes, err := io.ReadAll(req.Body)
158 if err != nil {
159 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
160 return
161 }
162
Philipp Schraderb7e75932022-03-26 16:18:34 -0700163 request, success := parseRequest[SubmitDataScouting](w, requestBytes, "SubmitDataScouting", submit_data_scouting.GetRootAsSubmitDataScouting)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800164 if !success {
165 return
166 }
167
Philipp Schraderd7b6eba2022-03-18 22:21:25 -0700168 log.Println("Got data scouting data for match", request.Match(), "team", request.Team(), "from", username)
169
Philipp Schrader30005e42022-03-06 13:53:58 -0800170 stats := db.Stats{
Philipp Schraderfee07e12022-03-17 22:19:47 -0700171 TeamNumber: request.Team(),
172 MatchNumber: request.Match(),
Philipp Schrader30b4a682022-04-16 14:36:17 -0700173 SetNumber: request.SetNumber(),
Philipp Schrader4535b7e2022-04-08 20:27:00 -0700174 CompLevel: string(request.CompLevel()),
Philipp Schraderfee07e12022-03-17 22:19:47 -0700175 StartingQuadrant: request.StartingQuadrant(),
176 AutoBallPickedUp: [5]bool{
177 request.AutoBall1(), request.AutoBall2(), request.AutoBall3(),
178 request.AutoBall4(), request.AutoBall5(),
179 },
Philipp Schraderfa45d742022-03-18 19:29:05 -0700180 ShotsMissedAuto: request.MissedShotsAuto(),
181 UpperGoalAuto: request.UpperGoalAuto(),
182 LowerGoalAuto: request.LowerGoalAuto(),
183 ShotsMissed: request.MissedShotsTele(),
184 UpperGoalShots: request.UpperGoalTele(),
185 LowerGoalShots: request.LowerGoalTele(),
186 PlayedDefense: request.DefenseRating(),
187 DefenseReceivedScore: request.DefenseReceivedRating(),
188 Climbing: int32(request.ClimbLevel()),
189 CollectedBy: username,
190 Comment: string(request.Comment()),
Philipp Schrader30005e42022-03-06 13:53:58 -0800191 }
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800192
Philipp Schraderfee07e12022-03-17 22:19:47 -0700193 // Do some error checking.
194 if stats.StartingQuadrant < 1 || stats.StartingQuadrant > 4 {
195 respondWithError(w, http.StatusBadRequest, fmt.Sprint(
196 "Invalid starting_quadrant field value of ", stats.StartingQuadrant))
197 return
198 }
199
Philipp Schrader30005e42022-03-06 13:53:58 -0800200 err = handler.db.AddToStats(stats)
201 if err != nil {
202 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
Philipp Schraderfee07e12022-03-17 22:19:47 -0700203 return
Philipp Schrader30005e42022-03-06 13:53:58 -0800204 }
205
206 builder := flatbuffers.NewBuilder(50 * 1024)
207 builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
208 w.Write(builder.FinishedBytes())
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800209}
210
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800211// Handles a RequestAllMaches request.
212type requestAllMatchesHandler struct {
213 db Database
214}
215
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800216// Change structure of match objects in the database(1 per team) to
217// the old match structure(1 per match) that the webserver uses.
218// We use the information in this struct to identify which match object
219// corresponds to which old match structure object.
220type MatchAssemblyKey struct {
221 MatchNumber int32
222 SetNumber int32
223 CompLevel string
224}
225
Emily Markovabf24c9e2023-02-08 20:31:11 -0800226func findIndexInList(list []string, comp_level string) (int, error) {
227 for index, value := range list {
228 if value == comp_level {
229 return index, nil
230 }
231 }
232 return -1, errors.New(fmt.Sprint("Failed to find comp level ", comp_level, " in list ", list))
233}
234
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800235func (handler requestAllMatchesHandler) teamHasBeenDataScouted(key MatchAssemblyKey, teamNumber int32) (bool, error) {
236 stats, err := handler.db.ReturnStats2023ForTeam(
237 strconv.Itoa(int(teamNumber)), key.MatchNumber, key.SetNumber, key.CompLevel)
238 if err != nil {
239 return false, err
240 }
241 return (len(stats) > 0), nil
242}
243
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800244func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
245 requestBytes, err := io.ReadAll(req.Body)
246 if err != nil {
247 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
248 return
249 }
250
Philipp Schraderb7e75932022-03-26 16:18:34 -0700251 _, success := parseRequest(w, requestBytes, "RequestAllMatches", request_all_matches.GetRootAsRequestAllMatches)
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800252 if !success {
253 return
254 }
255
256 matches, err := handler.db.ReturnMatches()
257 if err != nil {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700258 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800259 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800260 }
261
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800262 assembledMatches := map[MatchAssemblyKey]request_all_matches_response.MatchT{}
Emily Markovabf24c9e2023-02-08 20:31:11 -0800263
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800264 for _, match := range matches {
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800265 key := MatchAssemblyKey{match.MatchNumber, match.SetNumber, match.CompLevel}
266
267 // Retrieve the converted match structure we have assembled so
268 // far. If we haven't started assembling one yet, then start a
269 // new one.
Emily Markovabf24c9e2023-02-08 20:31:11 -0800270 entry, ok := assembledMatches[key]
271 if !ok {
272 entry = request_all_matches_response.MatchT{
273 MatchNumber: match.MatchNumber,
274 SetNumber: match.SetNumber,
275 CompLevel: match.CompLevel,
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800276 DataScouted: &request_all_matches_response.ScoutedLevelT{},
Emily Markovabf24c9e2023-02-08 20:31:11 -0800277 }
278 }
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800279
280 var team *int32
281 var dataScoutedTeam *bool
282
283 // Fill in the field for the match that we have in in the
284 // database. In the database, each match row only has 1 team
285 // number.
Emily Markovabf24c9e2023-02-08 20:31:11 -0800286 switch match.Alliance {
287 case "R":
288 switch match.AlliancePosition {
289 case 1:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800290 team = &entry.R1
291 dataScoutedTeam = &entry.DataScouted.R1
Emily Markovabf24c9e2023-02-08 20:31:11 -0800292 case 2:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800293 team = &entry.R2
294 dataScoutedTeam = &entry.DataScouted.R2
Emily Markovabf24c9e2023-02-08 20:31:11 -0800295 case 3:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800296 team = &entry.R3
297 dataScoutedTeam = &entry.DataScouted.R3
Emily Markovabf24c9e2023-02-08 20:31:11 -0800298 default:
299 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown red position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
300 return
301 }
302 case "B":
303 switch match.AlliancePosition {
304 case 1:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800305 team = &entry.B1
306 dataScoutedTeam = &entry.DataScouted.B1
Emily Markovabf24c9e2023-02-08 20:31:11 -0800307 case 2:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800308 team = &entry.B2
309 dataScoutedTeam = &entry.DataScouted.B2
Emily Markovabf24c9e2023-02-08 20:31:11 -0800310 case 3:
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800311 team = &entry.B3
312 dataScoutedTeam = &entry.DataScouted.B3
Emily Markovabf24c9e2023-02-08 20:31:11 -0800313 default:
314 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown blue position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
315 return
316 }
317 default:
318 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown alliance ", match.Alliance, " in match ", strconv.Itoa(int(match.AlliancePosition))))
319 return
320 }
Philipp Schrader0f7b6362023-03-11 14:02:48 -0800321
322 *team = match.TeamNumber
323
324 // Figure out if this team has been data scouted already.
325 *dataScoutedTeam, err = handler.teamHasBeenDataScouted(key, match.TeamNumber)
326 if err != nil {
327 respondWithError(w, http.StatusInternalServerError, fmt.Sprint(
328 "Failed to determine data scouting status for team ",
329 strconv.Itoa(int(match.AlliancePosition)),
330 " in match ",
331 strconv.Itoa(int(match.MatchNumber)),
332 err))
333 return
334 }
335
Emily Markovabf24c9e2023-02-08 20:31:11 -0800336 assembledMatches[key] = entry
337 }
338
339 var response RequestAllMatchesResponseT
340 for _, match := range assembledMatches {
341 copied_match := match
342 response.MatchList = append(response.MatchList, &copied_match)
343 }
344
345 var MATCH_TYPE_ORDERING = []string{"qm", "ef", "qf", "sf", "f"}
346
347 err = nil
348 sort.Slice(response.MatchList, func(i, j int) bool {
349 if err != nil {
350 return false
351 }
352 a := response.MatchList[i]
353 b := response.MatchList[j]
354
Emily Markovaabcac6e2023-02-18 17:50:03 -0800355 aMatchTypeIndex, err2 := findIndexInList(MATCH_TYPE_ORDERING, a.CompLevel)
356 if err2 != nil {
357 err = errors.New(fmt.Sprint("Comp level ", a.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err2))
Emily Markovabf24c9e2023-02-08 20:31:11 -0800358 return false
359 }
Emily Markovaabcac6e2023-02-18 17:50:03 -0800360 bMatchTypeIndex, err2 := findIndexInList(MATCH_TYPE_ORDERING, b.CompLevel)
361 if err2 != nil {
362 err = errors.New(fmt.Sprint("Comp level ", b.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err2))
Emily Markovabf24c9e2023-02-08 20:31:11 -0800363 return false
364 }
365
366 if aMatchTypeIndex < bMatchTypeIndex {
367 return true
368 }
369 if aMatchTypeIndex > bMatchTypeIndex {
370 return false
371 }
372
373 // Then sort by match number. E.g. in semi finals, all match 1 rounds
374 // are done first. Then come match 2 rounds. And then, if necessary,
375 // the match 3 rounds.
376 aMatchNumber := a.MatchNumber
377 bMatchNumber := b.MatchNumber
378 if aMatchNumber < bMatchNumber {
379 return true
380 }
381 if aMatchNumber > bMatchNumber {
382 return false
383 }
384 // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
385 // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
386 // Semi Final 2 Match 2.
387 aSetNumber := a.SetNumber
388 bSetNumber := b.SetNumber
389 if aSetNumber < bSetNumber {
390 return true
391 }
392 if aSetNumber > bSetNumber {
393 return false
394 }
395 return true
396 })
397
398 if err != nil {
399 // check if error happened during sorting and notify webpage if that
400 respondWithError(w, http.StatusInternalServerError, fmt.Sprint(err))
401 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800402 }
403
404 builder := flatbuffers.NewBuilder(50 * 1024)
405 builder.Finish((&response).Pack(builder))
406 w.Write(builder.FinishedBytes())
407}
408
Philipp Schraderacf96232022-03-01 22:03:30 -0800409// Handles a RequestDataScouting request.
410type requestDataScoutingHandler struct {
411 db Database
412}
413
414func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
415 requestBytes, err := io.ReadAll(req.Body)
416 if err != nil {
417 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
418 return
419 }
420
Philipp Schraderb7e75932022-03-26 16:18:34 -0700421 _, success := parseRequest(w, requestBytes, "RequestDataScouting", request_data_scouting.GetRootAsRequestDataScouting)
Philipp Schraderacf96232022-03-01 22:03:30 -0800422 if !success {
423 return
424 }
425
426 stats, err := handler.db.ReturnStats()
427 if err != nil {
Emily Markova290147d2023-03-03 22:40:06 -0800428 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800429 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800430 }
431
432 var response RequestDataScoutingResponseT
433 for _, stat := range stats {
434 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
Philipp Schraderfa45d742022-03-18 19:29:05 -0700435 Team: stat.TeamNumber,
436 Match: stat.MatchNumber,
Philipp Schrader30b4a682022-04-16 14:36:17 -0700437 SetNumber: stat.SetNumber,
Philipp Schrader4535b7e2022-04-08 20:27:00 -0700438 CompLevel: stat.CompLevel,
Philipp Schraderfa45d742022-03-18 19:29:05 -0700439 StartingQuadrant: stat.StartingQuadrant,
440 AutoBall1: stat.AutoBallPickedUp[0],
441 AutoBall2: stat.AutoBallPickedUp[1],
442 AutoBall3: stat.AutoBallPickedUp[2],
443 AutoBall4: stat.AutoBallPickedUp[3],
444 AutoBall5: stat.AutoBallPickedUp[4],
445 MissedShotsAuto: stat.ShotsMissedAuto,
446 UpperGoalAuto: stat.UpperGoalAuto,
447 LowerGoalAuto: stat.LowerGoalAuto,
448 MissedShotsTele: stat.ShotsMissed,
449 UpperGoalTele: stat.UpperGoalShots,
450 LowerGoalTele: stat.LowerGoalShots,
451 DefenseRating: stat.PlayedDefense,
452 DefenseReceivedRating: stat.DefenseReceivedScore,
453 ClimbLevel: request_data_scouting_response.ClimbLevel(stat.Climbing),
454 CollectedBy: stat.CollectedBy,
455 Comment: stat.Comment,
Philipp Schraderacf96232022-03-01 22:03:30 -0800456 })
457 }
458
459 builder := flatbuffers.NewBuilder(50 * 1024)
460 builder.Finish((&response).Pack(builder))
461 w.Write(builder.FinishedBytes())
462}
463
Alex Perry81f96ba2022-03-13 18:26:19 -0700464type submitNoteScoutingHandler struct {
465 db Database
466}
467
468func (handler submitNoteScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
469 requestBytes, err := io.ReadAll(req.Body)
470 if err != nil {
471 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
472 return
473 }
474
Philipp Schraderb7e75932022-03-26 16:18:34 -0700475 request, success := parseRequest(w, requestBytes, "SubmitNotes", submit_notes.GetRootAsSubmitNotes)
Alex Perry81f96ba2022-03-13 18:26:19 -0700476 if !success {
477 return
478 }
479
Filip Kujawaf947cb42022-11-21 10:00:30 -0800480 err = handler.db.AddNotes(db.NotesData{
Filip Kujawa7ddd5652023-03-07 19:56:15 -0800481 TeamNumber: request.Team(),
482 Notes: string(request.Notes()),
483 GoodDriving: bool(request.GoodDriving()),
484 BadDriving: bool(request.BadDriving()),
485 SketchyPickup: bool(request.SketchyPickup()),
486 SketchyPlacing: bool(request.SketchyPlacing()),
487 GoodDefense: bool(request.GoodDefense()),
488 BadDefense: bool(request.BadDefense()),
489 EasilyDefended: bool(request.EasilyDefended()),
Filip Kujawaf947cb42022-11-21 10:00:30 -0800490 })
Alex Perry81f96ba2022-03-13 18:26:19 -0700491 if err != nil {
492 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
493 return
494 }
495
496 var response SubmitNotesResponseT
497 builder := flatbuffers.NewBuilder(10)
498 builder.Finish((&response).Pack(builder))
499 w.Write(builder.FinishedBytes())
500}
501
Emily Markova1abe9782023-03-11 19:45:38 -0800502func ConvertActionsToStat(submitActions *submit_actions.SubmitActions) (db.Stats2023, error) {
503 overall_time := int64(0)
504 cycles := int64(0)
505 picked_up := false
506 lastPlacedTime := int64(0)
507 stat := db.Stats2023{TeamNumber: string(submitActions.TeamNumber()), MatchNumber: submitActions.MatchNumber(), SetNumber: submitActions.SetNumber(), CompLevel: string(submitActions.CompLevel()),
508 StartingQuadrant: 0, LowCubesAuto: 0, MiddleCubesAuto: 0, HighCubesAuto: 0, CubesDroppedAuto: 0,
509 LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0, ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 0, HighCubes: 0,
510 CubesDropped: 0, LowCones: 0, MiddleCones: 0, HighCones: 0, ConesDropped: 0, AvgCycle: 0, CollectedBy: string(submitActions.CollectedBy()),
511 }
512 // Loop over all actions.
513 for i := 0; i < submitActions.ActionsListLength(); i++ {
514 var action submit_actions.Action
515 if !submitActions.ActionsList(&action, i) {
516 return db.Stats2023{}, errors.New(fmt.Sprintf("Failed to parse submit_actions.Action"))
517 }
518 actionTable := new(flatbuffers.Table)
519 action_type := action.ActionTakenType()
520 if !action.ActionTaken(actionTable) {
521 return db.Stats2023{}, errors.New(fmt.Sprint("Failed to parse sub-action or sub-action was missing"))
522 }
523 if action_type == submit_actions.ActionTypeStartMatchAction {
524 var startMatchAction submit_actions.StartMatchAction
525 startMatchAction.Init(actionTable.Bytes, actionTable.Pos)
526 stat.StartingQuadrant = startMatchAction.Position()
527 } else if action_type == submit_actions.ActionTypePickupObjectAction {
528 var pick_up_action submit_actions.PickupObjectAction
529 pick_up_action.Init(actionTable.Bytes, actionTable.Pos)
530 if picked_up == true {
531 object := pick_up_action.ObjectType().String()
532 auto := pick_up_action.Auto()
533 if object == "kCube" && auto == false {
534 stat.CubesDropped += 1
535 } else if object == "kCube" && auto == true {
536 stat.CubesDroppedAuto += 1
537 } else if object == "kCone" && auto == false {
538 stat.ConesDropped += 1
539 } else if object == "kCube" && auto == true {
540 stat.ConesDroppedAuto += 1
541 }
542 } else {
543 picked_up = true
544 }
545 } else if action_type == submit_actions.ActionTypePlaceObjectAction {
546 var place_action submit_actions.PlaceObjectAction
547 place_action.Init(actionTable.Bytes, actionTable.Pos)
548 if !picked_up {
549 return db.Stats2023{}, errors.New(fmt.Sprintf("Got PlaceObjectAction without corresponding PickupObjectAction"))
550 }
551 object := place_action.ObjectType()
552 level := place_action.ScoreLevel()
553 auto := place_action.Auto()
554 if object == 0 && level == 0 && auto == true {
555 stat.LowCubesAuto += 1
556 } else if object == 0 && level == 0 && auto == false {
557 stat.LowCubes += 1
558 } else if object == 0 && level == 1 && auto == true {
559 stat.MiddleCubesAuto += 1
560 } else if object == 0 && level == 1 && auto == false {
561 stat.MiddleCubes += 1
562 } else if object == 0 && level == 2 && auto == true {
563 stat.HighCubesAuto += 1
564 } else if object == 0 && level == 2 && auto == false {
565 stat.HighCubes += 1
566 } else if object == 1 && level == 0 && auto == true {
567 stat.LowConesAuto += 1
568 } else if object == 1 && level == 0 && auto == false {
569 stat.LowCones += 1
570 } else if object == 1 && level == 1 && auto == true {
571 stat.MiddleConesAuto += 1
572 } else if object == 1 && level == 1 && auto == false {
573 stat.MiddleCones += 1
574 } else if object == 1 && level == 2 && auto == true {
575 stat.HighConesAuto += 1
576 } else if object == 1 && level == 2 && auto == false {
577 stat.HighCones += 1
578 } else {
579 return db.Stats2023{}, errors.New(fmt.Sprintf("Got unknown ObjectType/ScoreLevel/Auto combination"))
580 }
581 picked_up = false
582 if lastPlacedTime != int64(0) {
583 // If this is not the first time we place,
584 // start counting cycle time. We define cycle
585 // time as the time between placements.
586 overall_time += int64(action.Timestamp()) - lastPlacedTime
587 cycles += 1
588 }
589 lastPlacedTime = int64(action.Timestamp())
590 }
591 }
592 if cycles != 0 {
Philipp Schrader8c878a22023-03-20 22:36:38 -0700593 stat.AvgCycle = overall_time / cycles
Emily Markova1abe9782023-03-11 19:45:38 -0800594 } else {
595 stat.AvgCycle = 0
596 }
597 return stat, nil
598}
599
Emily Markova290147d2023-03-03 22:40:06 -0800600// Handles a Request2023DataScouting request.
601type request2023DataScoutingHandler struct {
602 db Database
603}
604
605func (handler request2023DataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
606 requestBytes, err := io.ReadAll(req.Body)
607 if err != nil {
608 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
609 return
610 }
611
612 _, success := parseRequest(w, requestBytes, "Request2023DataScouting", request_2023_data_scouting.GetRootAsRequest2023DataScouting)
613 if !success {
614 return
615 }
616
617 stats, err := handler.db.ReturnStats2023()
618 if err != nil {
619 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
620 return
621 }
622
623 var response Request2023DataScoutingResponseT
624 for _, stat := range stats {
625 response.StatsList = append(response.StatsList, &request_2023_data_scouting_response.Stats2023T{
626 TeamNumber: stat.TeamNumber,
627 MatchNumber: stat.MatchNumber,
628 SetNumber: stat.SetNumber,
629 CompLevel: stat.CompLevel,
630 StartingQuadrant: stat.StartingQuadrant,
631 LowCubesAuto: stat.LowCubesAuto,
632 MiddleCubesAuto: stat.MiddleCubesAuto,
633 HighCubesAuto: stat.HighCubesAuto,
634 CubesDroppedAuto: stat.CubesDroppedAuto,
635 LowConesAuto: stat.LowConesAuto,
636 MiddleConesAuto: stat.MiddleConesAuto,
637 HighConesAuto: stat.HighConesAuto,
638 ConesDroppedAuto: stat.ConesDroppedAuto,
639 LowCubes: stat.LowCubes,
640 MiddleCubes: stat.MiddleCubes,
641 HighCubes: stat.HighCubes,
642 CubesDropped: stat.CubesDropped,
643 LowCones: stat.LowCones,
644 MiddleCones: stat.MiddleCones,
645 HighCones: stat.HighCones,
646 ConesDropped: stat.ConesDropped,
647 AvgCycle: stat.AvgCycle,
648 CollectedBy: stat.CollectedBy,
649 })
650 }
651
652 builder := flatbuffers.NewBuilder(50 * 1024)
653 builder.Finish((&response).Pack(builder))
654 w.Write(builder.FinishedBytes())
655}
656
Alex Perry81f96ba2022-03-13 18:26:19 -0700657type requestNotesForTeamHandler struct {
658 db Database
659}
660
661func (handler requestNotesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
662 requestBytes, err := io.ReadAll(req.Body)
663 if err != nil {
664 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
665 return
666 }
667
Philipp Schraderb7e75932022-03-26 16:18:34 -0700668 request, success := parseRequest(w, requestBytes, "RequestNotesForTeam", request_notes_for_team.GetRootAsRequestNotesForTeam)
Alex Perry81f96ba2022-03-13 18:26:19 -0700669 if !success {
670 return
671 }
672
Philipp Schradereecb8962022-06-01 21:02:42 -0700673 notes, err := handler.db.QueryNotes(request.Team())
Alex Perry81f96ba2022-03-13 18:26:19 -0700674 if err != nil {
675 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query notes: %v", err))
676 return
677 }
678
679 var response RequestNotesForTeamResponseT
Philipp Schradereecb8962022-06-01 21:02:42 -0700680 for _, data := range notes {
Alex Perry81f96ba2022-03-13 18:26:19 -0700681 response.Notes = append(response.Notes, &request_notes_for_team_response.NoteT{data})
682 }
683
684 builder := flatbuffers.NewBuilder(1024)
685 builder.Finish((&response).Pack(builder))
686 w.Write(builder.FinishedBytes())
687}
688
Milo Lin1d59f0c2022-06-22 20:30:58 -0700689type requestShiftScheduleHandler struct {
690 db Database
691}
692
693func (handler requestShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
694 requestBytes, err := io.ReadAll(req.Body)
695 if err != nil {
696 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
697 return
698 }
699
700 _, success := parseRequest(w, requestBytes, "RequestShiftSchedule", request_shift_schedule.GetRootAsRequestShiftSchedule)
701 if !success {
702 return
703 }
704
705 shiftData, err := handler.db.ReturnAllShifts()
706 if err != nil {
707 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query shift schedule: %v", err))
708 return
709 }
710
711 var response RequestShiftScheduleResponseT
712 for _, shifts := range shiftData {
713 response.ShiftSchedule = append(response.ShiftSchedule, &request_shift_schedule_response.MatchAssignmentT{
714 MatchNumber: shifts.MatchNumber,
715 R1scouter: shifts.R1scouter,
716 R2scouter: shifts.R2scouter,
717 R3scouter: shifts.R3scouter,
718 B1scouter: shifts.B1scouter,
719 B2scouter: shifts.B2scouter,
720 B3scouter: shifts.B3scouter,
721 })
722 }
723
724 builder := flatbuffers.NewBuilder(1024)
725 builder.Finish((&response).Pack(builder))
726 w.Write(builder.FinishedBytes())
727}
728
729type submitShiftScheduleHandler struct {
730 db Database
731}
732
733func (handler submitShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
734 // Get the username of the person submitting the data.
735 username := parseUsername(req)
736
737 requestBytes, err := io.ReadAll(req.Body)
738 if err != nil {
739 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
740 return
741 }
742
743 request, success := parseRequest[SubmitShiftSchedule](w, requestBytes, "SubmitShiftSchedule", submit_shift_schedule.GetRootAsSubmitShiftSchedule)
744 if !success {
745 return
746 }
747
748 log.Println("Got shift schedule from", username)
749 shift_schedule_length := request.ShiftScheduleLength()
750 for i := 0; i < shift_schedule_length; i++ {
751 var match_assignment submit_shift_schedule.MatchAssignment
752 request.ShiftSchedule(&match_assignment, i)
753 current_shift := db.Shift{
754 MatchNumber: match_assignment.MatchNumber(),
755 R1scouter: string(match_assignment.R1scouter()),
756 R2scouter: string(match_assignment.R2scouter()),
757 R3scouter: string(match_assignment.R3scouter()),
758 B1scouter: string(match_assignment.B1scouter()),
759 B2scouter: string(match_assignment.B2scouter()),
760 B3scouter: string(match_assignment.B3scouter()),
761 }
762 err = handler.db.AddToShift(current_shift)
763 if err != nil {
764 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit shift schedule: ", err))
765 return
766 }
767 }
768
769 builder := flatbuffers.NewBuilder(50 * 1024)
770 builder.Finish((&SubmitShiftScheduleResponseT{}).Pack(builder))
771 w.Write(builder.FinishedBytes())
772}
773
Filip Kujawa210a03b2022-11-24 14:41:11 -0800774type SubmitDriverRankingHandler struct {
775 db Database
776}
777
778func (handler SubmitDriverRankingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
779 requestBytes, err := io.ReadAll(req.Body)
780 if err != nil {
781 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
782 return
783 }
784
785 request, success := parseRequest(w, requestBytes, "SubmitDriverRanking", submit_driver_ranking.GetRootAsSubmitDriverRanking)
786 if !success {
787 return
788 }
789
790 err = handler.db.AddDriverRanking(db.DriverRankingData{
791 MatchNumber: request.MatchNumber(),
792 Rank1: request.Rank1(),
793 Rank2: request.Rank2(),
794 Rank3: request.Rank3(),
795 })
796
797 if err != nil {
798 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert driver ranking: %v", err))
799 return
800 }
801
802 var response SubmitDriverRankingResponseT
803 builder := flatbuffers.NewBuilder(10)
804 builder.Finish((&response).Pack(builder))
805 w.Write(builder.FinishedBytes())
806}
807
Filip Kujawaf882e022022-12-14 13:14:08 -0800808type requestAllNotesHandler struct {
809 db Database
810}
811
812func (handler requestAllNotesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
813 requestBytes, err := io.ReadAll(req.Body)
814 if err != nil {
815 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
816 return
817 }
818
819 _, success := parseRequest(w, requestBytes, "RequestAllNotes", request_all_notes.GetRootAsRequestAllNotes)
820 if !success {
821 return
822 }
823
824 notes, err := handler.db.ReturnAllNotes()
825 if err != nil {
826 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
827 return
828 }
829
830 var response RequestAllNotesResponseT
831 for _, note := range notes {
832 response.NoteList = append(response.NoteList, &request_all_notes_response.NoteT{
Filip Kujawa7ddd5652023-03-07 19:56:15 -0800833 Team: note.TeamNumber,
834 Notes: note.Notes,
835 GoodDriving: note.GoodDriving,
836 BadDriving: note.BadDriving,
837 SketchyPickup: note.SketchyPickup,
838 SketchyPlacing: note.SketchyPlacing,
839 GoodDefense: note.GoodDefense,
840 BadDefense: note.BadDefense,
841 EasilyDefended: note.EasilyDefended,
Filip Kujawaf882e022022-12-14 13:14:08 -0800842 })
843 }
844
845 builder := flatbuffers.NewBuilder(50 * 1024)
846 builder.Finish((&response).Pack(builder))
847 w.Write(builder.FinishedBytes())
848}
849
850type requestAllDriverRankingsHandler struct {
851 db Database
852}
853
854func (handler requestAllDriverRankingsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
855 requestBytes, err := io.ReadAll(req.Body)
856 if err != nil {
857 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
858 return
859 }
860
861 _, success := parseRequest(w, requestBytes, "RequestAllDriverRankings", request_all_driver_rankings.GetRootAsRequestAllDriverRankings)
862 if !success {
863 return
864 }
865
866 rankings, err := handler.db.ReturnAllDriverRankings()
867 if err != nil {
868 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
869 return
870 }
871
872 var response RequestAllDriverRankingsResponseT
873 for _, ranking := range rankings {
874 response.DriverRankingList = append(response.DriverRankingList, &request_all_driver_rankings_response.RankingT{
875 MatchNumber: ranking.MatchNumber,
876 Rank1: ranking.Rank1,
877 Rank2: ranking.Rank2,
878 Rank3: ranking.Rank3,
879 })
880 }
881
882 builder := flatbuffers.NewBuilder(50 * 1024)
883 builder.Finish((&response).Pack(builder))
884 w.Write(builder.FinishedBytes())
885}
886
Philipp Schrader43c730b2023-02-26 20:27:44 -0800887func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800888 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800889 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800890 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Filip Kujawaf882e022022-12-14 13:14:08 -0800891 scoutingServer.Handle("/requests/request/all_notes", requestAllNotesHandler{db})
892 scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800893 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Emily Markova290147d2023-03-03 22:40:06 -0800894 scoutingServer.Handle("/requests/request/2023_data_scouting", request2023DataScoutingHandler{db})
Alex Perry81f96ba2022-03-13 18:26:19 -0700895 scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
896 scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
Milo Lin1d59f0c2022-06-22 20:30:58 -0700897 scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
898 scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
Filip Kujawa210a03b2022-11-24 14:41:11 -0800899 scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800900}