mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Added match review interface.
This commit is contained in:
@@ -79,7 +79,8 @@ func MatchPlayFakeResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
|
||||
return
|
||||
}
|
||||
matchResult := MatchResult{MatchId: match.Id}
|
||||
matchResult := MatchResult{MatchId: match.Id, RedFouls: []Foul{}, BlueFouls: []Foul{Foul{17, "G22", 23.5, true}},
|
||||
Cards: Cards{[]int{17}, []int{8}}}
|
||||
matchResult.RedScore = randomScore()
|
||||
matchResult.BlueScore = randomScore()
|
||||
err = CommitMatchScore(match, &matchResult)
|
||||
@@ -93,21 +94,29 @@ func MatchPlayFakeResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func CommitMatchScore(match *Match, matchResult *MatchResult) error {
|
||||
// Determine the play number for this match.
|
||||
prevMatchResult, err := db.GetMatchResultForMatch(match.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prevMatchResult != nil {
|
||||
matchResult.PlayNumber = prevMatchResult.PlayNumber + 1
|
||||
} else {
|
||||
matchResult.PlayNumber = 1
|
||||
}
|
||||
if matchResult.PlayNumber == 0 {
|
||||
// Determine the play number for this new match result.
|
||||
prevMatchResult, err := db.GetMatchResultForMatch(match.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prevMatchResult != nil {
|
||||
matchResult.PlayNumber = prevMatchResult.PlayNumber + 1
|
||||
} else {
|
||||
matchResult.PlayNumber = 1
|
||||
}
|
||||
|
||||
// Save the match result record to the database.
|
||||
err = db.CreateMatchResult(matchResult)
|
||||
if err != nil {
|
||||
return err
|
||||
// Save the match result record to the database.
|
||||
err = db.CreateMatchResult(matchResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// We are updating a match result record that already exists.
|
||||
err := db.SaveMatchResult(matchResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update and save the match record to the database.
|
||||
@@ -121,7 +130,7 @@ func CommitMatchScore(match *Match, matchResult *MatchResult) error {
|
||||
} else {
|
||||
match.Winner = "T"
|
||||
}
|
||||
err = db.SaveMatch(match)
|
||||
err := db.SaveMatch(match)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
189
match_review.go
Normal file
189
match_review.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Web routes for editing match results.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type MatchReviewListItem struct {
|
||||
Id int
|
||||
DisplayName string
|
||||
Time string
|
||||
RedTeams []int
|
||||
BlueTeams []int
|
||||
RedScore int
|
||||
BlueScore int
|
||||
ColorClass string
|
||||
}
|
||||
|
||||
// Shows the match review interface.
|
||||
func MatchReviewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
practiceMatches, err := buildMatchReviewList("practice")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
qualificationMatches, err := buildMatchReviewList("qualification")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
eliminationMatches, err := buildMatchReviewList("elimination")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
template, err := template.ParseFiles("templates/match_review.html", "templates/base.html")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
matchesByType := map[string][]MatchReviewListItem{"practice": practiceMatches,
|
||||
"qualification": qualificationMatches, "elimination": eliminationMatches}
|
||||
if currentMatchType == "" {
|
||||
currentMatchType = "practice"
|
||||
}
|
||||
data := struct {
|
||||
*EventSettings
|
||||
MatchesByType map[string][]MatchReviewListItem
|
||||
CurrentMatchType string
|
||||
}{eventSettings, matchesByType, currentMatchType}
|
||||
err = template.ExecuteTemplate(w, "base", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Shows the page to edit the results for a match.
|
||||
func MatchReviewEditGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
match, matchResult, err := getMatchResultFromRequest(r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
template, err := template.ParseFiles("templates/edit_match_result.html", "templates/base.html")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
matchResultJson, err := matchResult.serialize()
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
data := struct {
|
||||
*EventSettings
|
||||
Match *Match
|
||||
MatchResultJson *MatchResultDb
|
||||
}{eventSettings, match, matchResultJson}
|
||||
err = template.ExecuteTemplate(w, "base", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the results for a match.
|
||||
func MatchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
match, matchResult, err := getMatchResultFromRequest(r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
matchResultJson := MatchResultDb{Id: matchResult.Id, MatchId: match.Id, PlayNumber: matchResult.PlayNumber,
|
||||
RedScoreJson: r.PostFormValue("redScoreJson"), BlueScoreJson: r.PostFormValue("blueScoreJson"),
|
||||
RedFoulsJson: r.PostFormValue("redFoulsJson"), BlueFoulsJson: r.PostFormValue("blueFoulsJson"),
|
||||
CardsJson: r.PostFormValue("cardsJson")}
|
||||
|
||||
// Deserialize the JSON using the same mechanism as to store scoring information in the database.
|
||||
matchResult, err = matchResultJson.deserialize()
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = CommitMatchScore(match, matchResult)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/match_review", 302)
|
||||
}
|
||||
|
||||
func getMatchResultFromRequest(r *http.Request) (*Match, *MatchResult, error) {
|
||||
vars := mux.Vars(r)
|
||||
matchId, _ := strconv.Atoi(vars["matchId"])
|
||||
match, err := db.GetMatchById(matchId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if match == nil {
|
||||
return nil, nil, fmt.Errorf("Error: No such match: %d", matchId)
|
||||
}
|
||||
matchResult, err := db.GetMatchResultForMatch(matchId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if matchResult == nil {
|
||||
// We're scoring a match that hasn't been played yet, but that's okay.
|
||||
matchResult = new(MatchResult)
|
||||
}
|
||||
|
||||
return match, matchResult, nil
|
||||
}
|
||||
|
||||
func buildMatchReviewList(matchType string) ([]MatchReviewListItem, error) {
|
||||
matches, err := db.GetMatchesByType(matchType)
|
||||
if err != nil {
|
||||
return []MatchReviewListItem{}, err
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if matchType == "practice" {
|
||||
prefix = "P"
|
||||
} else if matchType == "qualification" {
|
||||
prefix = "Q"
|
||||
}
|
||||
matchReviewList := make([]MatchReviewListItem, len(matches))
|
||||
for i, match := range matches {
|
||||
matchReviewList[i].Id = match.Id
|
||||
matchReviewList[i].DisplayName = prefix + match.DisplayName
|
||||
matchReviewList[i].Time = match.Time.Format("Mon 1/02 03:04 PM")
|
||||
matchReviewList[i].RedTeams = []int{match.Red1, match.Red2, match.Red3}
|
||||
matchReviewList[i].BlueTeams = []int{match.Blue1, match.Blue2, match.Blue3}
|
||||
matchResult, err := db.GetMatchResultForMatch(match.Id)
|
||||
if err != nil {
|
||||
return []MatchReviewListItem{}, err
|
||||
}
|
||||
if matchResult != nil {
|
||||
matchReviewList[i].RedScore = matchResult.RedScoreSummary().Score
|
||||
matchReviewList[i].BlueScore = matchResult.BlueScoreSummary().Score
|
||||
}
|
||||
switch match.Winner {
|
||||
case "R":
|
||||
matchReviewList[i].ColorClass = "danger"
|
||||
case "B":
|
||||
matchReviewList[i].ColorClass = "info"
|
||||
case "T":
|
||||
matchReviewList[i].ColorClass = "warning"
|
||||
default:
|
||||
matchReviewList[i].ColorClass = ""
|
||||
}
|
||||
}
|
||||
|
||||
return matchReviewList, nil
|
||||
}
|
||||
122
match_review_test.go
Normal file
122
match_review_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatchReview(t *testing.T) {
|
||||
clearDb()
|
||||
defer clearDb()
|
||||
var err error
|
||||
db, err = OpenDatabase(testDbPath)
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
eventSettings, _ = db.GetEventSettings()
|
||||
|
||||
match1 := Match{Type: "practice", DisplayName: "1", Status: "complete", Winner: "R"}
|
||||
match2 := Match{Type: "practice", DisplayName: "2"}
|
||||
match3 := Match{Type: "qualification", DisplayName: "1", Status: "complete", Winner: "B"}
|
||||
match4 := Match{Type: "elimination", DisplayName: "SF1-1", Status: "complete", Winner: "T"}
|
||||
match5 := Match{Type: "elimination", DisplayName: "SF1-2"}
|
||||
db.CreateMatch(&match1)
|
||||
db.CreateMatch(&match2)
|
||||
db.CreateMatch(&match3)
|
||||
db.CreateMatch(&match4)
|
||||
db.CreateMatch(&match5)
|
||||
|
||||
// Check that all matches are listed on the page.
|
||||
recorder := getHttpResponse("/match_review")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "P1")
|
||||
assert.Contains(t, recorder.Body.String(), "P2")
|
||||
assert.Contains(t, recorder.Body.String(), "Q1")
|
||||
assert.Contains(t, recorder.Body.String(), "SF1-1")
|
||||
assert.Contains(t, recorder.Body.String(), "SF1-2")
|
||||
}
|
||||
|
||||
func TestMatchReviewEditExistingResult(t *testing.T) {
|
||||
clearDb()
|
||||
defer clearDb()
|
||||
var err error
|
||||
db, err = OpenDatabase(testDbPath)
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
eventSettings, _ = db.GetEventSettings()
|
||||
|
||||
match := Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 101,
|
||||
Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
|
||||
db.CreateMatch(&match)
|
||||
matchResult := buildTestMatchResult(match.Id, 1)
|
||||
db.CreateMatchResult(&matchResult)
|
||||
|
||||
recorder := getHttpResponse("/match_review")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
assert.Contains(t, recorder.Body.String(), "312") // The red score
|
||||
assert.Contains(t, recorder.Body.String(), "593") // The blue score
|
||||
|
||||
// Check response for non-existent match.
|
||||
recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", 12345))
|
||||
assert.Equal(t, 500, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "No such match")
|
||||
|
||||
recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
|
||||
// Update the score to something else.
|
||||
postBody := "redScoreJson={\"AutoMobilityBonuses\":3}&blueScoreJson={\"Cycles\":[{\"ScoredHigh\":true}]}&" +
|
||||
"redFoulsJson=[{\"TeamId\":103,\"IsTechnical\":false}]&blueFoulsJson=[{\"TeamId\":104,\"IsTechnical\":" +
|
||||
"true}]&cardsJson={\"RedCardTeamIds\":[105]}"
|
||||
recorder = postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody)
|
||||
assert.Equal(t, 302, recorder.Code)
|
||||
|
||||
// Check for the updated scores back on the match list page.
|
||||
recorder = getHttpResponse("/match_review")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
assert.Contains(t, recorder.Body.String(), "65") // The red score
|
||||
assert.Contains(t, recorder.Body.String(), "30") // The blue score
|
||||
}
|
||||
|
||||
func TestMatchReviewCreateNewResult(t *testing.T) {
|
||||
clearDb()
|
||||
defer clearDb()
|
||||
var err error
|
||||
db, err = OpenDatabase(testDbPath)
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
eventSettings, _ = db.GetEventSettings()
|
||||
|
||||
match := Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 101,
|
||||
Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
|
||||
db.CreateMatch(&match)
|
||||
|
||||
recorder := getHttpResponse("/match_review")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
assert.NotContains(t, recorder.Body.String(), "312") // The red score
|
||||
assert.NotContains(t, recorder.Body.String(), "593") // The blue score
|
||||
|
||||
recorder = getHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
|
||||
// Update the score to something else.
|
||||
postBody := "redScoreJson={\"AutoHighHot\":4}&blueScoreJson={\"Cycles\":[{\"Assists\":3," +
|
||||
"\"ScoredLow\":true}]}&redFoulsJson=[]&blueFoulsJson=[]&cardsJson={}"
|
||||
recorder = postHttpResponse(fmt.Sprintf("/match_review/%d/edit", match.Id), postBody)
|
||||
assert.Equal(t, 302, recorder.Code)
|
||||
|
||||
// Check for the updated scores back on the match list page.
|
||||
recorder = getHttpResponse("/match_review")
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "QF4-3")
|
||||
assert.Contains(t, recorder.Body.String(), "80") // The red score
|
||||
assert.Contains(t, recorder.Body.String(), "31") // The blue score
|
||||
}
|
||||
18
static/css/cheesy-arena.css
Normal file
18
static/css/cheesy-arena.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.red-text {
|
||||
color: #f00;
|
||||
}
|
||||
.blue-text {
|
||||
color: #00f;
|
||||
}
|
||||
.well-red {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
.well-darkred {
|
||||
background-color: #ebcccc;
|
||||
}
|
||||
.well-blue {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
.well-darkblue {
|
||||
background-color: #c4e3f3;
|
||||
}
|
||||
186
static/js/match_review.js
Normal file
186
static/js/match_review.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Client-side methods for editing a match in the match review page.
|
||||
|
||||
var scoreTemplate = Handlebars.compile($("#scoreTemplate").html());
|
||||
var allianceResults = {};
|
||||
|
||||
// Hijack the form submission to inject the data in JSON form so that it's easier for the server to parse.
|
||||
$("form").submit(function() {
|
||||
updateResults("red");
|
||||
updateResults("blue");
|
||||
|
||||
var redScoreJson = JSON.stringify(allianceResults["red"].score);
|
||||
var blueScoreJson = JSON.stringify(allianceResults["blue"].score);
|
||||
var redFoulsJson = JSON.stringify(allianceResults["red"].fouls);
|
||||
var blueFoulsJson = JSON.stringify(allianceResults["blue"].fouls);
|
||||
|
||||
// Merge the red and blue cards data since that's what the database model expects.
|
||||
var mergedCards = {YellowCardTeamIds: allianceResults["red"].cards.YellowCardTeamIds.
|
||||
concat(allianceResults["blue"].cards.YellowCardTeamIds),
|
||||
RedCardTeamIds:allianceResults["red"].cards.RedCardTeamIds.
|
||||
concat(allianceResults["blue"].cards.RedCardTeamIds)};
|
||||
var cardsJson = JSON.stringify(mergedCards);
|
||||
|
||||
// Inject the JSON data into the form as hidden inputs.
|
||||
$("<input />").attr("type", "hidden").attr("name", "redScoreJson").attr("value", redScoreJson).appendTo("form");
|
||||
$("<input />").attr("type", "hidden").attr("name", "blueScoreJson").attr("value", blueScoreJson).appendTo("form");
|
||||
$("<input />").attr("type", "hidden").attr("name", "redFoulsJson").attr("value", redFoulsJson).appendTo("form");
|
||||
$("<input />").attr("type", "hidden").attr("name", "blueFoulsJson").attr("value", blueFoulsJson).appendTo("form");
|
||||
$("<input />").attr("type", "hidden").attr("name", "cardsJson").attr("value", cardsJson).appendTo("form");
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Draws the match-editing form for one alliance based on the cached result data.
|
||||
var renderResults = function(alliance) {
|
||||
var result = allianceResults[alliance];
|
||||
var scoreContent = scoreTemplate(result);
|
||||
$("#" + alliance + "Score").html(scoreContent);
|
||||
|
||||
// Set the values of the form fields from the JSON results data.
|
||||
$("select[name=" + alliance + "AutoMobilityBonuses]").val(result.score.AutoMobilityBonuses);
|
||||
$("input[name=" + alliance + "AutoHighHot]").val(result.score.AutoHighHot);
|
||||
$("input[name=" + alliance + "AutoHigh]").val(result.score.AutoHigh);
|
||||
$("input[name=" + alliance + "AutoLowHot]").val(result.score.AutoLowHot);
|
||||
$("input[name=" + alliance + "AutoLow]").val(result.score.AutoLow);
|
||||
$("input[name=" + alliance + "AutoClearHigh]").val(result.score.AutoClearHigh);
|
||||
$("input[name=" + alliance + "AutoClearLow]").val(result.score.AutoClearLow);
|
||||
|
||||
$.each(result.score.Cycles, function(k, v) {
|
||||
$("#" + alliance + "Cycle" + k + "Title").text("Cycle " + (k + 1));
|
||||
$("input[name=" + alliance + "Cycle" + k + "Assists][value=" + v.Assists + "]").prop("checked", true);
|
||||
|
||||
var trussCatch;
|
||||
if (v.Truss && v.Catch) {
|
||||
trussCatch = "TC";
|
||||
} else if (v.Truss) {
|
||||
trussCatch = "T";
|
||||
} else {
|
||||
trussCatch = "N";
|
||||
}
|
||||
$("input[name=" + alliance + "Cycle" + k + "TrussCatch][value=" + trussCatch + "]").prop("checked", true);
|
||||
|
||||
var cycleEnd;
|
||||
if (v.ScoredHigh) {
|
||||
cycleEnd = "SH";
|
||||
} else if (v.ScoredLow) {
|
||||
cycleEnd = "SL";
|
||||
} else if (v.DeadBall) {
|
||||
cycleEnd = "DB";
|
||||
} else {
|
||||
cycleEnd = "DE";
|
||||
}
|
||||
$("input[name=" + alliance + "Cycle" + k + "End][value=" + cycleEnd + "]").prop("checked", true);
|
||||
});
|
||||
|
||||
$.each(result.fouls, function(k, v) {
|
||||
$("input[name=" + alliance + "Foul" + k + "Team][value=" + v.TeamId + "]").prop("checked", true);
|
||||
$("input[name=" + alliance + "Foul" + k + "Tech][value=" + v.IsTechnical + "]").prop("checked", true);
|
||||
$("input[name=" + alliance + "Foul" + k + "Rule]").val(v.Rule);
|
||||
$("input[name=" + alliance + "Foul" + k + "Time]").val(v.TimeInMatchSec);
|
||||
});
|
||||
|
||||
$.each(result.cards.YellowCardTeamIds, function(k, v) {
|
||||
$("input[name=" + alliance + "Team" + v + "Card][value=Y]").prop("checked", true);
|
||||
});
|
||||
$.each(result.cards.RedCardTeamIds, function(k, v) {
|
||||
$("input[name=" + alliance + "Team" + v + "Card][value=R]").prop("checked", true);
|
||||
});
|
||||
}
|
||||
|
||||
// Converts the current form values back into JSON structures and caches them.
|
||||
var updateResults = function(alliance) {
|
||||
var result = allianceResults[alliance];
|
||||
var formData = {}
|
||||
$.each($("form").serializeArray(), function(k, v) {
|
||||
formData[v.name] = v.value;
|
||||
});
|
||||
|
||||
result.score.AutoMobilityBonuses = parseInt(formData[alliance + "AutoMobilityBonuses"]);
|
||||
result.score.AutoHighHot = parseInt(formData[alliance + "AutoHighHot"]);
|
||||
result.score.AutoHigh = parseInt(formData[alliance + "AutoHigh"]);
|
||||
result.score.AutoLowHot = parseInt(formData[alliance + "AutoLowHot"]);
|
||||
result.score.AutoLow = parseInt(formData[alliance + "AutoLow"]);
|
||||
result.score.AutoClearHigh = parseInt(formData[alliance + "AutoClearHigh"]);
|
||||
result.score.AutoClearLow = parseInt(formData[alliance + "AutoClearLow"]);
|
||||
|
||||
result.score.Cycles = [];
|
||||
for (var i = 0; formData[alliance + "Cycle" + i + "Assists"]; i++) {
|
||||
var prefix = alliance + "Cycle" + i;
|
||||
var cycle = {Assists: parseInt(formData[prefix + "Assists"]), Truss: false, Catch: false,
|
||||
ScoredHigh: false, ScoredLow: false, DeadBall: false}
|
||||
switch (formData[prefix + "TrussCatch"]) {
|
||||
case "TC":
|
||||
cycle.Catch = true;
|
||||
case "T":
|
||||
cycle.Truss = true;
|
||||
}
|
||||
switch (formData[prefix + "End"]) {
|
||||
case "SH":
|
||||
cycle.ScoredHigh = true;
|
||||
break;
|
||||
case "SL":
|
||||
cycle.ScoredLow = true;
|
||||
break;
|
||||
case "DB":
|
||||
cycle.DeadBall = true;
|
||||
}
|
||||
result.score.Cycles.push(cycle);
|
||||
}
|
||||
|
||||
result.fouls = [];
|
||||
for (var i = 0; formData[alliance + "Foul" + i + "Tech"]; i++) {
|
||||
var prefix = alliance + "Foul" + i;
|
||||
var foul = {TeamId: parseInt(formData[prefix + "Team"]), Rule: formData[prefix + "Rule"],
|
||||
TimeInMatchSec: parseFloat(formData[prefix + "Time"]),
|
||||
IsTechnical: (formData[prefix + "Tech"] == "true")};
|
||||
result.fouls.push(foul);
|
||||
}
|
||||
|
||||
result.cards.YellowCardTeamIds = []
|
||||
result.cards.RedCardTeamIds = []
|
||||
$.each([result.team1, result.team2, result.team3], function(i, team) {
|
||||
switch (formData[alliance + "Team" + team + "Card"]) {
|
||||
case "Y":
|
||||
result.cards.YellowCardTeamIds.push(team);
|
||||
break
|
||||
case "R":
|
||||
result.cards.RedCardTeamIds.push(team);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Appends a blank cycle to the end of the list.
|
||||
var addCycle = function(alliance) {
|
||||
updateResults(alliance);
|
||||
var result = allianceResults[alliance];
|
||||
result.score.Cycles.push({Assists: 1, Truss: false, Catch: false, ScoredHigh: false, ScoredLow: false,
|
||||
DeadBall: false})
|
||||
renderResults(alliance);
|
||||
}
|
||||
|
||||
// Removes the given cycle from the list.
|
||||
var deleteCycle = function(alliance, index) {
|
||||
updateResults(alliance);
|
||||
var result = allianceResults[alliance];
|
||||
result.score.Cycles.splice(index, 1);
|
||||
renderResults(alliance);
|
||||
}
|
||||
|
||||
// Appends a blank foul to the end of the list.
|
||||
var addFoul = function(alliance) {
|
||||
updateResults(alliance);
|
||||
var result = allianceResults[alliance];
|
||||
result.fouls.push({TeamId: 0, Rule: "", TimeInMatchSec: 0, IsTechnical: false})
|
||||
renderResults(alliance);
|
||||
}
|
||||
|
||||
// Removes the given foul from the list.
|
||||
var deleteFoul = function(alliance, index) {
|
||||
updateResults(alliance);
|
||||
var result = allianceResults[alliance];
|
||||
result.fouls.splice(index, 1);
|
||||
renderResults(alliance);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/css/bootstrap-colorpicker.min.css" rel="stylesheet">
|
||||
<link href="/static/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
|
||||
<link href="/static/css/cheesy-arena.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
@@ -37,7 +38,7 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Run</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/match_play">Match Play</a></li>
|
||||
<li class="disabled"><a href="#">Match Review</a></li>
|
||||
<li><a href="/match_review">Match Review</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
|
||||
294
templates/edit_match_result.html
Normal file
294
templates/edit_match_result.html
Normal file
@@ -0,0 +1,294 @@
|
||||
{{define "title"}}Edit Match Results{{end}}
|
||||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="well">
|
||||
<form class="form-horizontal" action="/match_review/{{.Match.Id}}/edit" method="POST">
|
||||
<fieldset>
|
||||
<legend>Edit Match {{.Match.DisplayName}} Results</legend>
|
||||
<div class="col-lg-6" id="redScore"></div>
|
||||
<div class="col-lg-6" id="blueScore"></div>
|
||||
<div class="row form-group">
|
||||
<div class="text-center col-lg-12">
|
||||
<a href="/match_review"><button type="button" class="btn btn-default">Cancel</button></a>
|
||||
<button type="submit" class="btn btn-info">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scoreTemplate" style="display: none;">
|
||||
<div class="well well-{{"{{alliance}}"}}">
|
||||
<legend>Autonomous</legend>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Moving Bonus</label>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-control input-sm" name="{{"{{alliance}}"}}AutoMobilityBonuses">
|
||||
<option value="0">0</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Scored High Hot</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoHighHot"></div>
|
||||
<label class="col-lg-4 control-label">Scored High Not Hot</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoHigh"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Scored Low Hot</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoLowHot"></div>
|
||||
<label class="col-lg-4 control-label">Scored Low Not Hot</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoLow"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Cleared High Teleop</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoClearHigh"></div>
|
||||
<label class="col-lg-4 control-label">Cleared Low Teleop</label>
|
||||
<div class="col-lg-2"><input type="text" class="form-control input-sm" name="{{"{{alliance}}"}}AutoClearLow"></div>
|
||||
</div>
|
||||
<legend>Teleoperated</legend>
|
||||
{{"{{#each score.Cycles}}"}}
|
||||
<div class="well well-sm well-dark{{"{{../alliance}}"}}">
|
||||
<b id="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}Title">Cycle</b>
|
||||
<button type="button" class="close" onclick="deleteCycle('{{"{{../alliance}}"}}', {{"{{@index}}"}});">×</button>
|
||||
<br />
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Number of Assists</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}Assists" value="1">1
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}Assists"value="2">2
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}Assists" value="3">3
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Truss/Catch</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}TrussCatch" value="N">
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}TrussCatch" value="T">
|
||||
Truss
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}TrussCatch" value="TC">
|
||||
Truss + Catch
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="col-lg-4 control-label">Cycle End</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}End" value="DE">
|
||||
Didn't End
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}End" value="SH">
|
||||
Scored High
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="col-lg-8 col-lg-offset-4">
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}End" value="SL">
|
||||
Scored Low
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Cycle{{"{{@index}}"}}End" value="DB">
|
||||
Dead Ball
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{"{{/each}}"}}
|
||||
<button type="button" class="btn btn-default btn-sm" onclick="addCycle('{{"{{alliance}}"}}');">
|
||||
Add Cycle
|
||||
</button>
|
||||
<br /><br />
|
||||
<legend>Fouls</legend>
|
||||
{{"{{#each fouls}}"}}
|
||||
<div class="well well-sm well-dark{{"{{../alliance}}"}}">
|
||||
<button type="button" class="close" onclick="deleteFoul('{{"{{../alliance}}"}}', {{"{{@index}}"}});">×</button>
|
||||
<br />
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Team</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Team" value="{{"{{../team1}}"}}">
|
||||
{{"{{../team1}}"}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Team" value="{{"{{../team2}}"}}">
|
||||
{{"{{../team2}}"}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Team" value="{{"{{../team3}}"}}">
|
||||
{{"{{../team3}}"}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Type</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Tech" value="false">
|
||||
Regular
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Tech" value="true">
|
||||
Technical
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Rule Violated</label>
|
||||
<div class="col-lg-3">
|
||||
<input type="text" class="form-control input-sm" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Rule">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Seconds Into Match</label>
|
||||
<div class="col-lg-3">
|
||||
<input type="text" class="form-control input-sm" name="{{"{{../alliance}}"}}Foul{{"{{@index}}"}}Time">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{"{{/each}}"}}
|
||||
<button type="button" class="btn btn-default btn-sm" onclick="addFoul('{{"{{alliance}}"}}');">
|
||||
Add Foul
|
||||
</button>
|
||||
<br /><br />
|
||||
<legend>Cards</legend>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Team {{"{{team1}}"}}</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team1}}"}}Card" value="N" checked>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team1}}"}}Card" value="Y">
|
||||
Yellow
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team1}}"}}Card" value="R">
|
||||
Red
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Team {{"{{team2}}"}}</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team2}}"}}Card" value="N" checked>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team2}}"}}Card" value="Y">
|
||||
Yellow
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team2}}"}}Card" value="R">
|
||||
Red
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Team {{"{{team3}}"}}</label>
|
||||
<div class="col-lg-8">
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team3}}"}}Card" value="N" checked>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-3">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team3}}"}}Card" value="Y">
|
||||
Yellow
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio col-lg-6">
|
||||
<label>
|
||||
<input type="radio" name="{{"{{alliance}}"}}Team{{"{{team3}}"}}Card" value="R">
|
||||
Red
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "script"}}
|
||||
<script src="/static/js/match_review.js"></script>
|
||||
<script>
|
||||
var matchId = {{.Match.Id}}
|
||||
var allianceResults = {};
|
||||
allianceResults["red"] = {alliance: "red", team1: {{.Match.Red1}}, team2: {{.Match.Red2}},
|
||||
team3: {{.Match.Red3}}, score: jQuery.parseJSON('{{.MatchResultJson.RedScoreJson}}'),
|
||||
fouls: jQuery.parseJSON('{{.MatchResultJson.RedFoulsJson}}'),
|
||||
cards: jQuery.parseJSON('{{.MatchResultJson.CardsJson}}')};
|
||||
allianceResults["blue"] = {alliance: "blue", team1: {{.Match.Blue1}}, team2: {{.Match.Blue2}},
|
||||
team3: {{.Match.Blue3}}, score: jQuery.parseJSON('{{.MatchResultJson.BlueScoreJson}}'),
|
||||
fouls: jQuery.parseJSON('{{.MatchResultJson.BlueFoulsJson}}'),
|
||||
cards: jQuery.parseJSON('{{.MatchResultJson.CardsJson}}')};
|
||||
renderResults("red");
|
||||
renderResults("blue");
|
||||
</script>
|
||||
{{end}}
|
||||
56
templates/match_review.html
Normal file
56
templates/match_review.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{{define "title"}}Match Review{{end}}
|
||||
{{define "body"}}
|
||||
<div class="row">
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 15px;">
|
||||
<li{{if eq .CurrentMatchType "practice" }} class="active"{{end}}>
|
||||
<a href="#practice" data-toggle="tab">Practice</a>
|
||||
</li>
|
||||
<li{{if eq .CurrentMatchType "qualification" }} class="active"{{end}}>
|
||||
<a href="#qualification" data-toggle="tab">Qualification</a>
|
||||
</li>
|
||||
<li{{if eq .CurrentMatchType "elimination" }} class="active"{{end}}>
|
||||
<a href="#elimination" data-toggle="tab">Elimination</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
{{range $type, $matches := .MatchesByType}}
|
||||
<div class="tab-pane {{if eq $.CurrentMatchType $type }} active{{end}}" id="{{$type}}">
|
||||
<table class="table table-striped table-hover ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Match</th>
|
||||
<th>Time</th>
|
||||
<th class="text-center">Red Alliance</th>
|
||||
<th class="text-center">Blue Alliance</th>
|
||||
<th class="text-center">Red Score</th>
|
||||
<th class="text-center">Blue Score</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $match := $matches}}
|
||||
<tr class="{{$match.ColorClass}}">
|
||||
<td>{{$match.DisplayName}}</td>
|
||||
<td>{{$match.Time}}</td>
|
||||
<td class="text-center red-text">
|
||||
{{index $match.RedTeams 0}}, {{index $match.RedTeams 1}}, {{index $match.RedTeams 2}}
|
||||
</td>
|
||||
<td class="text-center blue-text">
|
||||
{{index $match.BlueTeams 0}}, {{index $match.BlueTeams 1}}, {{index $match.BlueTeams 2}}
|
||||
</td>
|
||||
<td class="text-center red-text">{{$match.RedScore}}</td>
|
||||
<td class="text-center blue-text">{{$match.BlueScore}}</td>
|
||||
<td class="text-center" style="white-space: nowrap;">
|
||||
<a href="/match_review/{{$match.Id}}/edit"><b class="btn btn-info btn-xs">Edit</b></a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "script"}}
|
||||
{{end}}
|
||||
@@ -40,8 +40,8 @@
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-12">
|
||||
<button class="btn btn-default" onclick="addBlock(); return false;">Add Block</button>
|
||||
<button class="btn btn-info" onclick="generateSchedule(); return false;">
|
||||
<button type="button" class="btn btn-default" onclick="addBlock();">Add Block</button>
|
||||
<button type="button" class="btn btn-info" onclick="generateSchedule();">
|
||||
Generate Schedule
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Save Schedule</button>
|
||||
|
||||
3
web.go
3
web.go
@@ -84,6 +84,9 @@ func newHandler() http.Handler {
|
||||
router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST")
|
||||
router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET")
|
||||
router.HandleFunc("/match_play/{matchId}/generate_fake_result", MatchPlayFakeResultHandler).Methods("GET")
|
||||
router.HandleFunc("/match_review", MatchReviewHandler).Methods("GET")
|
||||
router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditGetHandler).Methods("GET")
|
||||
router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditPostHandler).Methods("POST")
|
||||
router.HandleFunc("/reports/csv/rankings", RankingsCsvReportHandler)
|
||||
router.HandleFunc("/reports/pdf/rankings", RankingsPdfReportHandler)
|
||||
router.HandleFunc("/reports/json/rankings", RankingsJSONReportHandler)
|
||||
|
||||
Reference in New Issue
Block a user