Added match review interface.

This commit is contained in:
Patrick Fairbank
2014-06-28 22:03:30 -07:00
parent 0200e50e4b
commit 84af09c095
10 changed files with 897 additions and 19 deletions

View File

@@ -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
View 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
View 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
}

View 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
View 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);
}

View File

@@ -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">

View 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}}

View 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}}

View File

@@ -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
View File

@@ -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)