Added basic structure of match control interface.

This commit is contained in:
Patrick Fairbank
2014-06-29 14:20:47 -07:00
parent 84af09c095
commit d5e0ae2bbf
5 changed files with 162 additions and 27 deletions

View File

@@ -19,6 +19,7 @@ type MatchPlayListItem struct {
Id int
DisplayName string
Time string
Status string
ColorClass string
}
@@ -27,6 +28,8 @@ type MatchPlayList []MatchPlayListItem
// Global var to hold the current active tournament so that its matches are displayed by default.
var currentMatchType string
var currentMatch *Match
// Shows the match play control interface.
func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
practiceMatches, err := buildMatchPlayList("practice")
@@ -55,11 +58,15 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
if currentMatchType == "" {
currentMatchType = "practice"
}
if currentMatch == nil {
currentMatch = new(Match)
}
data := struct {
*EventSettings
MatchesByType map[string]MatchPlayList
CurrentMatchType string
}{eventSettings, matchesByType, currentMatchType}
Match *Match
}{eventSettings, matchesByType, currentMatchType, currentMatch}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
@@ -67,6 +74,26 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
}
}
func MatchPlayQueueHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
match, err := db.GetMatchById(matchId)
if err != nil {
handleWebErr(w, err)
return
}
if match == nil {
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
return
}
// TODO(pat): Disallow if there is a match currently being played or there are uncommitted results.
currentMatch = match
currentMatchType = match.Type
http.Redirect(w, r, "/match_play", 302)
}
func MatchPlayFakeResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
@@ -149,7 +176,7 @@ func (list MatchPlayList) Len() int {
}
func (list MatchPlayList) Less(i, j int) bool {
return list[i].ColorClass == "" && list[j].ColorClass != ""
return list[i].Status != "complete" && list[j].Status == "complete"
}
func (list MatchPlayList) Swap(i, j int) {
@@ -173,6 +200,7 @@ func buildMatchPlayList(matchType string) (MatchPlayList, error) {
matchPlayList[i].Id = match.Id
matchPlayList[i].DisplayName = prefix + match.DisplayName
matchPlayList[i].Time = match.Time.Format("3:04 PM")
matchPlayList[i].Status = match.Status
switch match.Winner {
case "R":
matchPlayList[i].ColorClass = "danger"
@@ -183,6 +211,9 @@ func buildMatchPlayList(matchType string) (MatchPlayList, error) {
default:
matchPlayList[i].ColorClass = ""
}
if currentMatch != nil && matchPlayList[i].Id == currentMatch.Id {
matchPlayList[i].ColorClass = "success"
}
}
// Sort the list to put all completed matches at the bottom.

View File

@@ -4,6 +4,7 @@
package main
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -37,3 +38,52 @@ func TestMatchPlay(t *testing.T) {
assert.Contains(t, recorder.Body.String(), "SF1-1")
assert.Contains(t, recorder.Body.String(), "SF1-2")
}
func TestMatchPlayQueue(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_play")
assert.Equal(t, 200, recorder.Code)
assert.NotContains(t, recorder.Body.String(), "101")
assert.NotContains(t, recorder.Body.String(), "102")
assert.NotContains(t, recorder.Body.String(), "103")
assert.NotContains(t, recorder.Body.String(), "104")
assert.NotContains(t, recorder.Body.String(), "105")
assert.NotContains(t, recorder.Body.String(), "106")
// Queue the match and check for the team numbers again.
recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/queue", match.Id))
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/match_play")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "101")
assert.Contains(t, recorder.Body.String(), "102")
assert.Contains(t, recorder.Body.String(), "103")
assert.Contains(t, recorder.Body.String(), "104")
assert.Contains(t, recorder.Body.String(), "105")
assert.Contains(t, recorder.Body.String(), "106")
}
func TestMatchPlayErrors(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
// Queue an invalid match.
recorder := getHttpResponse("/match_play/1114/queue")
assert.Equal(t, 500, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid match")
}

View File

@@ -16,3 +16,22 @@
.well-darkblue {
background-color: #c4e3f3;
}
.col-no-padding {
padding-left: 0;
padding-right: 0;
}
.ds-status, .robot-status, .battery-status, .bypass-button {
background-color: #aaa;
color: #000;
border: 1px solid #999;
border-radius: 4px;
padding: 5px;
width: 40px;
height: 27px;
margin: 2px;
font-family: Arial;
margin: 0 auto;
}
.bypass-button {
background-color: #0e8;
}

View File

@@ -33,6 +33,9 @@
<a href="/match_play/{{$match.Id}}/generate_fake_result">
<b class="btn btn-info btn-xs">Generate Fake Result</b>
</a>
<a href="/match_play/{{$match.Id}}/queue">
<b class="btn btn-info btn-xs">Queue</b>
</a>
</td>
</tr>
{{end}}
@@ -42,13 +45,65 @@
{{end}}
</div>
</div>
<div class="col-lg-8">
<div class="jumbotron">
<h2>Placeholder for the match play interface.</h2>
<p>For now, use the "Generate Fake Result" buttons to simulate matches being played and scored.</p>
<div class="col-lg-8 text-center">
<div class="row">
<div class="col-lg-6 well well-darkblue">
<div class="row form-group">
<div class="col-lg-4">Blue Teams</div>
<div class="col-lg-2" data-toggle="tooltip" title="Driver Station">DS</div>
<div class="col-lg-2" data-toggle="tooltip" title="Robot">R</div>
<div class="col-lg-2" data-toggle="tooltip" title="Battery">B</div>
<div class="col-lg-2" data-toggle="tooltip" title="Bypass/Disable">Byp</div>
</div>
<div class="row form-group">
<div class="col-lg-1">1 </div>
{{template "matchPlayTeam" .Match.Blue1}}
</div>
<div class="row form-group">
<div class="col-lg-1">2 </div>
{{template "matchPlayTeam" .Match.Blue2}}
</div>
<div class="row form-group">
<div class="col-lg-1">3 </div>
{{template "matchPlayTeam" .Match.Blue3}}
</div>
</div>
<div class="col-lg-6 well well-darkred">
<div class="row form-group">
<div class="col-lg-4">Red Teams</div>
<div class="col-lg-2" data-toggle="tooltip" title="Driver Station">DS</div>
<div class="col-lg-2" data-toggle="tooltip" title="Robot">R</div>
<div class="col-lg-2" data-toggle="tooltip" title="Battery">B</div>
<div class="col-lg-2" data-toggle="tooltip" title="Bypass/Disable">Byp</div>
</div>
<div class="row form-group">
<div class="col-lg-1">3 </div>
{{template "matchPlayTeam" .Match.Red3}}
</div>
<div class="row form-group">
<div class="col-lg-1">2 </div>
{{template "matchPlayTeam" .Match.Red2}}
</div>
<div class="row form-group">
<div class="col-lg-1">1 </div>
{{template "matchPlayTeam" .Match.Red1}}
</div>
</div>
</div>
</div>
</div>
{{end}}
{{define "script"}}
<script>
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
</script>
{{end}}
{{define "matchPlayTeam"}}
<div class="col-lg-3">
<input type="text" class="form-control input-sm" value="{{.}}">
</div>
<div class="col-lg-2 col-no-padding"><div class="ds-status" id="team{{.}}Ds"></div></div>
<div class="col-lg-2 col-no-padding"><div class="robot-status" id="team{{.}}Robot"></div></div>
<div class="col-lg-2 col-no-padding"><div class="battery-status" id="team{{.}}Battery"></div></div>
<div class="col-lg-2 col-no-padding"><div class="bypass-button" id="team{{.}}Bypass"></div></div>
{{end}}

22
web.go
View File

@@ -11,9 +11,6 @@ import (
"html/template"
"log"
"net/http"
"os/exec"
"runtime"
"strconv"
)
const httpPort = 8080
@@ -39,24 +36,6 @@ func ServeWebInterface() {
http.Handle("/", newHandler())
log.Printf("Serving HTTP requests on port %d", httpPort)
// Open in Default Web Browser
// Necessary to Authenticate
url := "http://localhost:" + strconv.Itoa(httpPort)
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
case "windows":
err = exec.Command(`rundll32.exe`, "url.dll,FileProtocolHandler", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
println(err.Error())
}
// Start Server
http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil)
}
@@ -83,6 +62,7 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/alliance_selection/reset", AllianceSelectionResetHandler).Methods("POST")
router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST")
router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/queue", MatchPlayQueueHandler).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")