diff --git a/match.go b/match.go index 53b8894..f9e5faf 100644 --- a/match.go +++ b/match.go @@ -45,6 +45,8 @@ type Match struct { BlueDefense5 string } +var placeableDefenses = []string{"CDF", "M", "R", "RW", "RT"} + func (database *Database) CreateMatch(match *Match) error { return database.matchMap.Insert(match) } diff --git a/reports.go b/reports.go index cd70a40..f6360fd 100644 --- a/reports.go +++ b/reports.go @@ -235,6 +235,68 @@ func SchedulePdfReportHandler(w http.ResponseWriter, r *http.Request) { } } +// Generates a PDF-formatted report of the defenses schedule. +func DefensesPdfReportHandler(w http.ResponseWriter, r *http.Request) { + if !UserIsReader(w, r) { + return + } + + vars := mux.Vars(r) + matches, err := db.GetMatchesByType(vars["type"]) + if err != nil { + handleWebErr(w, err) + return + } + + // The widths of the table columns in mm, stored here so that they can be referenced for each row. + colWidths := map[string]float64{"Type": 25, "Match": 15, "Defense": 15.5} + rowHeight := 6.5 + + pdf := gofpdf.New("P", "mm", "Letter", "font") + pdf.AddPage() + + // Render table header row. + pdf.SetFont("Arial", "B", 10) + pdf.SetFillColor(220, 220, 220) + pdf.CellFormat(195, rowHeight, "Defenses Schedule - "+eventSettings.Name, "", 1, "C", false, 0, "") + pdf.CellFormat(colWidths["Type"], rowHeight, "Type", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Match"], rowHeight, "Match", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Red 1", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Red 2", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Red 3", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Red 4", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Red 5", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Blue 1", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Blue 2", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Blue 3", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Blue 4", "1", 0, "C", true, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, "Blue 5", "1", 1, "C", true, 0, "") + pdf.SetFont("Arial", "", 10) + for _, match := range matches { + // Render match defenses row. + pdf.CellFormat(colWidths["Type"], rowHeight, match.CapitalizedType(), "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Match"], rowHeight, match.DisplayName, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.RedDefense1, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.RedDefense2, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.RedDefense3, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.RedDefense4, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.RedDefense5, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.BlueDefense1, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.BlueDefense2, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.BlueDefense3, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.BlueDefense4, "1", 0, "C", false, 0, "") + pdf.CellFormat(colWidths["Defense"], rowHeight, match.BlueDefense5, "1", 1, "C", false, 0, "") + } + + // Write out the PDF file as the HTTP response. + w.Header().Set("Content-Type", "application/pdf") + err = pdf.Output(w) + if err != nil { + handleWebErr(w, err) + return + } +} + // Generates a CSV-formatted report of the team list. func TeamsCsvReportHandler(w http.ResponseWriter, r *http.Request) { if !UserIsReader(w, r) { diff --git a/reports_test.go b/reports_test.go index 8d6edb9..6905fcf 100644 --- a/reports_test.go +++ b/reports_test.go @@ -60,9 +60,11 @@ func TestScheduleCsvReport(t *testing.T) { assert.Equal(t, 200, recorder.Code) assert.Equal(t, "text/plain", recorder.HeaderMap["Content-Type"][0]) expectedBody := "Match,Type,Time,Red1,Red1IsSurrogate,Red2,Red2IsSurrogate,Red3,Red3IsSurrogate,Blue1," + - "Blue1IsSurrogate,Blue2,Blue2IsSurrogate,Blue3,Blue3IsSurrogate\n1,qualification," + - "1969-12-31 16:00:00 -0800 PST,1,false,2,false,3,false,4,true,5,true,6,true\n" + - "2,qualification,1969-12-31 16:10:00 -0800 PST,7,true,8,true,9,true,10,false,11,false,12,false\n\n" + "Blue1IsSurrogate,Blue2,Blue2IsSurrogate,Blue3,Blue3IsSurrogate,RedDefense1,RedDefense2," + + "RedDefense3,RedDefense4,RedDefense5,BlueDefense1,BlueDefense2,BlueDefense3,BlueDefense4," + + "BlueDefense5\n1,qualification,1969-12-31 16:00:00 -0800 PST,1,false,2,false,3,false,4,true,5,true," + + "6,true,,,,,,,,,,\n2,qualification,1969-12-31 16:10:00 -0800 PST,7,true,8,true,9,true,10,false,11,false,12," + + "false,,,,,,,,,,\n\n" assert.Equal(t, expectedBody, recorder.Body.String()) } diff --git a/schedule.go b/schedule.go index 75180fa..6fa2b4e 100644 --- a/schedule.go +++ b/schedule.go @@ -88,6 +88,8 @@ func BuildRandomSchedule(teams []Team, scheduleBlocks []ScheduleBlock, matchType } } + randomizeDefenses(matches, numTeams) + return matches, nil } @@ -99,3 +101,28 @@ func countMatches(scheduleBlocks []ScheduleBlock) int { } return numMatches } + +// Fills in a random set of defenses per round of all teams playing. +func randomizeDefenses(matches []Match, numTeams int) { + // Take the floor, to err on the side of a team missing a set of defenses instead of seeing it twice. + matchesPerRound := numTeams / 6 + + var defenseShuffle []int + for i := 0; i < len(matches); i++ { + if i%matchesPerRound == 0 { + // Pick a new set of defenses. + defenseShuffle = rand.Perm(len(placeableDefenses)) + } + + matches[i].RedDefense1 = "LB" + matches[i].RedDefense2 = placeableDefenses[defenseShuffle[0]] + matches[i].RedDefense3 = placeableDefenses[defenseShuffle[1]] + matches[i].RedDefense4 = placeableDefenses[defenseShuffle[2]] + matches[i].RedDefense5 = placeableDefenses[defenseShuffle[3]] + matches[i].BlueDefense1 = "LB" + matches[i].BlueDefense2 = placeableDefenses[defenseShuffle[0]] + matches[i].BlueDefense3 = placeableDefenses[defenseShuffle[1]] + matches[i].BlueDefense4 = placeableDefenses[defenseShuffle[2]] + matches[i].BlueDefense5 = placeableDefenses[defenseShuffle[3]] + } +} diff --git a/schedule_test.go b/schedule_test.go index ac178b4..46ad52a 100644 --- a/schedule_test.go +++ b/schedule_test.go @@ -56,17 +56,29 @@ func TestScheduleTeams(t *testing.T) { matches, err := BuildRandomSchedule(teams, scheduleBlocks, "test") assert.Nil(t, err) assert.Equal(t, Match{Type: "test", DisplayName: "1", Time: time.Unix(0, 0).UTC(), Red1: 115, Red2: 111, - Red3: 108, Blue1: 109, Blue2: 116, Blue3: 117}, matches[0]) + Red3: 108, Blue1: 109, Blue2: 116, Blue3: 117, RedDefense1: "LB", RedDefense2: "RW", RedDefense3: "RT", + RedDefense4: "R", RedDefense5: "M", BlueDefense1: "LB", BlueDefense2: "RW", BlueDefense3: "RT", + BlueDefense4: "R", BlueDefense5: "M"}, matches[0]) assert.Equal(t, Match{Type: "test", DisplayName: "2", Time: time.Unix(60, 0).UTC(), Red1: 114, Red2: 112, - Red3: 103, Blue1: 101, Blue2: 104, Blue3: 118}, matches[1]) + Red3: 103, Blue1: 101, Blue2: 104, Blue3: 118, RedDefense1: "LB", RedDefense2: "RW", RedDefense3: "RT", + RedDefense4: "R", RedDefense5: "M", BlueDefense1: "LB", BlueDefense2: "RW", BlueDefense3: "RT", + BlueDefense4: "R", BlueDefense5: "M"}, matches[1]) assert.Equal(t, Match{Type: "test", DisplayName: "3", Time: time.Unix(120, 0).UTC(), Red1: 110, Red2: 107, - Red3: 105, Blue1: 106, Blue2: 113, Blue3: 102}, matches[2]) + Red3: 105, Blue1: 106, Blue2: 113, Blue3: 102, RedDefense1: "LB", RedDefense2: "RW", RedDefense3: "RT", + RedDefense4: "R", RedDefense5: "M", BlueDefense1: "LB", BlueDefense2: "RW", BlueDefense3: "RT", + BlueDefense4: "R", BlueDefense5: "M"}, matches[2]) assert.Equal(t, Match{Type: "test", DisplayName: "4", Time: time.Unix(180, 0).UTC(), Red1: 112, Red2: 108, - Red3: 109, Blue1: 101, Blue2: 111, Blue3: 103}, matches[3]) + Red3: 109, Blue1: 101, Blue2: 111, Blue3: 103, RedDefense1: "LB", RedDefense2: "RT", RedDefense3: "M", + RedDefense4: "CDF", RedDefense5: "R", BlueDefense1: "LB", BlueDefense2: "RT", BlueDefense3: "M", + BlueDefense4: "CDF", BlueDefense5: "R"}, matches[3]) assert.Equal(t, Match{Type: "test", DisplayName: "5", Time: time.Unix(240, 0).UTC(), Red1: 113, Red2: 117, - Red3: 115, Blue1: 110, Blue2: 114, Blue3: 102}, matches[4]) + Red3: 115, Blue1: 110, Blue2: 114, Blue3: 102, RedDefense1: "LB", RedDefense2: "RT", RedDefense3: "M", + RedDefense4: "CDF", RedDefense5: "R", BlueDefense1: "LB", BlueDefense2: "RT", BlueDefense3: "M", + BlueDefense4: "CDF", BlueDefense5: "R"}, matches[4]) assert.Equal(t, Match{Type: "test", DisplayName: "6", Time: time.Unix(300, 0).UTC(), Red1: 118, Red2: 105, - Red3: 106, Blue1: 107, Blue2: 104, Blue3: 116}, matches[5]) + Red3: 106, Blue1: 107, Blue2: 104, Blue3: 116, RedDefense1: "LB", RedDefense2: "RT", RedDefense3: "M", + RedDefense4: "CDF", RedDefense5: "R", BlueDefense1: "LB", BlueDefense2: "RT", BlueDefense3: "M", + BlueDefense4: "CDF", BlueDefense5: "R"}, matches[5]) // Check with excess room for matches in the schedule. scheduleBlocks = []ScheduleBlock{{time.Unix(0, 0).UTC(), 7, 60}} diff --git a/templates/base.html b/templates/base.html index 20f292f..57200e4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -60,8 +60,11 @@
  • Team List
  • Practice Schedule
  • +
  • Practice Defenses
  • Qualification Schedule
  • +
  • Qualification Defenses
  • Playoff Schedule
  • +
  • Playoff Defenses
  • Standings
  • diff --git a/templates/schedule.csv b/templates/schedule.csv index e9dc85f..f13d359 100644 --- a/templates/schedule.csv +++ b/templates/schedule.csv @@ -1,3 +1,3 @@ -Match,Type,Time,Red1,Red1IsSurrogate,Red2,Red2IsSurrogate,Red3,Red3IsSurrogate,Blue1,Blue1IsSurrogate,Blue2,Blue2IsSurrogate,Blue3,Blue3IsSurrogate -{{range $match := .}}{{$match.DisplayName}},{{$match.Type}},{{$match.Time.Local}},{{$match.Red1}},{{$match.Red1IsSurrogate}},{{$match.Red2}},{{$match.Red2IsSurrogate}},{{$match.Red3}},{{$match.Red3IsSurrogate}},{{$match.Blue1}},{{$match.Blue1IsSurrogate}},{{$match.Blue2}},{{$match.Blue2IsSurrogate}},{{$match.Blue3}},{{$match.Blue3IsSurrogate}} +Match,Type,Time,Red1,Red1IsSurrogate,Red2,Red2IsSurrogate,Red3,Red3IsSurrogate,Blue1,Blue1IsSurrogate,Blue2,Blue2IsSurrogate,Blue3,Blue3IsSurrogate,RedDefense1,RedDefense2,RedDefense3,RedDefense4,RedDefense5,BlueDefense1,BlueDefense2,BlueDefense3,BlueDefense4,BlueDefense5 +{{range $match := .}}{{$match.DisplayName}},{{$match.Type}},{{$match.Time.Local}},{{$match.Red1}},{{$match.Red1IsSurrogate}},{{$match.Red2}},{{$match.Red2IsSurrogate}},{{$match.Red3}},{{$match.Red3IsSurrogate}},{{$match.Blue1}},{{$match.Blue1IsSurrogate}},{{$match.Blue2}},{{$match.Blue2IsSurrogate}},{{$match.Blue3}},{{$match.Blue3IsSurrogate}},{{$match.RedDefense1}},{{$match.RedDefense2}},{{$match.RedDefense3}},{{$match.RedDefense4}},{{$match.RedDefense5}},{{$match.BlueDefense1}},{{$match.BlueDefense2}},{{$match.BlueDefense3}},{{$match.BlueDefense4}},{{$match.BlueDefense5}} {{end}} diff --git a/web.go b/web.go index 76a7937..5e787a2 100644 --- a/web.go +++ b/web.go @@ -193,6 +193,7 @@ func newHandler() http.Handler { router.HandleFunc("/reports/pdf/rankings", RankingsPdfReportHandler).Methods("GET") router.HandleFunc("/reports/csv/schedule/{type}", ScheduleCsvReportHandler).Methods("GET") router.HandleFunc("/reports/pdf/schedule/{type}", SchedulePdfReportHandler).Methods("GET") + router.HandleFunc("/reports/pdf/defenses/{type}", DefensesPdfReportHandler).Methods("GET") router.HandleFunc("/reports/csv/teams", TeamsCsvReportHandler).Methods("GET") router.HandleFunc("/reports/pdf/teams", TeamsPdfReportHandler).Methods("GET") router.HandleFunc("/reports/csv/wpa_keys", WpaKeysCsvReportHandler).Methods("GET")