Added team list setup page.

This commit is contained in:
Patrick Fairbank
2014-06-05 23:09:03 -07:00
parent 9fdcac26e9
commit 719a4f02ed
8 changed files with 551 additions and 17 deletions

View File

@@ -7,13 +7,13 @@ package main
import (
"code.google.com/p/gofpdf"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"io"
"net/http"
"strconv"
"text/template"
"io"
"encoding/json"
)
// Generates a CSV-formatted report of the qualification rankings.

247
setup_teams.go Normal file
View File

@@ -0,0 +1,247 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web routes for configuring the team list.
package main
import (
"encoding/csv"
"fmt"
"github.com/gorilla/mux"
"html"
"html/template"
"io"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
var officialTeamInfoUrl = "https://my.usfirst.org/frc/scoring/index.lasso?page=teamlist"
var officialTeamInfo map[int][]string
// Shows the team list.
func TeamsGetHandler(w http.ResponseWriter, r *http.Request) {
renderTeams(w, r, false)
}
// Adds teams to the team list.
func TeamsPostHandler(w http.ResponseWriter, r *http.Request) {
if !canModifyTeamList() {
renderTeams(w, r, true)
return
}
err := r.ParseForm()
if err != nil {
handleWebErr(w, err)
return
}
var teamNumbers []int
for _, teamNumberString := range strings.Split(r.PostFormValue("teamNumbers"), "\r\n") {
teamNumber, err := strconv.Atoi(teamNumberString)
if err == nil {
teamNumbers = append(teamNumbers, teamNumber)
}
}
for _, teamNumber := range teamNumbers {
team, err := getOfficialTeamInfo(teamNumber)
if err != nil {
handleWebErr(w, err)
return
}
err = db.CreateTeam(team)
if err != nil {
handleWebErr(w, err)
return
}
}
renderTeams(w, r, false)
}
// Clears the team list.
func TeamsClearHandler(w http.ResponseWriter, r *http.Request) {
if !canModifyTeamList() {
renderTeams(w, r, true)
return
}
err := db.TruncateTeams()
if err != nil {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
}
// Shows the page to edit a team's fields.
func TeamEditGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
teamId, _ := strconv.Atoi(vars["id"])
team, err := db.GetTeamById(teamId)
if err != nil {
handleWebErr(w, err)
return
}
if team == nil {
http.Error(w, fmt.Sprintf("Error: No such team: %d", teamId), 400)
return
}
template, err := template.ParseFiles("templates/edit_team.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
err = template.ExecuteTemplate(w, "base", team)
if err != nil {
handleWebErr(w, err)
return
}
}
// Updates a team's fields.
func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
teamId, _ := strconv.Atoi(vars["id"])
team, err := db.GetTeamById(teamId)
if err != nil {
handleWebErr(w, err)
return
}
if team == nil {
http.Error(w, fmt.Sprintf("Error: No such team: %d", teamId), 400)
return
}
err = r.ParseForm()
if err != nil {
handleWebErr(w, err)
return
}
team.Name = r.PostFormValue("name")
team.Nickname = r.PostFormValue("nickname")
team.City = r.PostFormValue("city")
team.StateProv = r.PostFormValue("stateProv")
team.Country = r.PostFormValue("country")
rookieYear, _ := strconv.Atoi(r.PostFormValue("rookieYear"))
team.RookieYear = rookieYear
team.RobotName = r.PostFormValue("robotName")
err = db.SaveTeam(team)
if err != nil {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
}
// Removes a team from the team list.
func TeamDeletePostHandler(w http.ResponseWriter, r *http.Request) {
if !canModifyTeamList() {
renderTeams(w, r, true)
return
}
vars := mux.Vars(r)
teamId, _ := strconv.Atoi(vars["id"])
team, err := db.GetTeamById(teamId)
if err != nil {
handleWebErr(w, err)
return
}
if team == nil {
http.Error(w, fmt.Sprintf("Error: No such team: %d", teamId), 400)
return
}
err = db.DeleteTeam(team)
if err != nil {
handleWebErr(w, err)
return
}
renderTeams(w, r, false)
}
func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) {
teams, err := db.GetAllTeams()
if err != nil {
handleWebErr(w, err)
return
}
template, err := template.ParseFiles("templates/teams.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
Teams []Team
ShowErrorMessage bool
}{teams, showErrorMessage}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// Returns true if it is safe to change the team list (i.e. no matches/results exist yet).
func canModifyTeamList() bool {
matches, err := db.GetMatchesByType("qualification")
if err != nil || len(matches) > 0 {
return false
}
return true
}
// Returns the data for the given team number.
func getOfficialTeamInfo(teamId int) (*Team, error) {
if officialTeamInfo == nil {
// Download all team info from the FIRST website if it is not cached.
resp, err := http.Get(officialTeamInfoUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
re := regexp.MustCompile("(?s).*<PRE>(.*)</PRE>.*")
teamsCsv := re.FindStringSubmatch(string(body))[1]
reader := csv.NewReader(strings.NewReader(teamsCsv))
reader.Comma = '\t'
reader.FieldsPerRecord = -1
officialTeamInfo = make(map[int][]string)
reader.Read() // Ignore header line.
for {
fields, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
teamNumber, err := strconv.Atoi(fields[1])
if err != nil {
return nil, err
}
officialTeamInfo[teamNumber] = fields
}
}
teamData, ok := officialTeamInfo[teamId]
var team Team
if ok {
rookieYear, _ := strconv.Atoi(teamData[8])
team = Team{teamId, html.UnescapeString(teamData[2]), html.UnescapeString(teamData[7]),
html.UnescapeString(teamData[4]), html.UnescapeString(teamData[5]), html.UnescapeString(teamData[6]),
rookieYear, html.UnescapeString(teamData[9])}
} else {
// If no team data exists, just fill in the team number.
team = Team{Id: teamId}
}
return &team, nil
}

130
setup_teams_test.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSetupTeams(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
// Check that there are no teams to start.
recorder := getHttpResponse("/setup/teams")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "0 teams")
// Mock the URL to download team info from.
teamInfoBody := "<PRE>\nID_team\tteam_number\tteam_name\tteam_name_short\tteam_city\tteam_stateprov\t" +
"team_country\tteam_nickname team_rookieyear robot_name\n1\t254\tNASA\tChezy\tThe Cheesy Poofs\t" +
"San Jose\tCA\tUSA\t1999\tBarrage\n</PRE>"
teamInfoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, teamInfoBody)
}))
defer teamInfoServer.Close()
officialTeamInfoUrl = teamInfoServer.URL
// Add some teams.
recorder = postHttpResponse("/setup/teams", "teamNumbers=254\r\nnotateam\r\n1114\r\n")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "2 teams")
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
assert.Contains(t, recorder.Body.String(), "1114")
// Add another team.
recorder = postHttpResponse("/setup/teams", "teamNumbers=33")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "3 teams")
assert.Contains(t, recorder.Body.String(), "33")
// Edit a team.
recorder = getHttpResponse("/setup/teams/254/edit")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs")
// Delete a team.
recorder = postHttpResponse("/setup/teams/1114/delete", "")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "2 teams")
// Test clearing all teams.
recorder = postHttpResponse("/setup/teams/clear", "")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "0 teams")
}
func TestSetupTeamsDisallowModification(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
db.CreateTeam(&Team{Id: 254, Nickname: "The Cheesy Poofs"})
db.CreateMatch(&Match{Type: "qualification"})
// Disallow adding teams.
recorder := postHttpResponse("/setup/teams", "teamNumbers=33")
assert.Contains(t, recorder.Body.String(), "can't modify")
assert.Contains(t, recorder.Body.String(), "1 teams")
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
// Disallow deleting team.
recorder = postHttpResponse("/setup/teams/254/delete", "")
assert.Contains(t, recorder.Body.String(), "can't modify")
assert.Contains(t, recorder.Body.String(), "1 teams")
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
// Disallow clearing all teams.
recorder = postHttpResponse("/setup/teams/clear", "")
assert.Contains(t, recorder.Body.String(), "can't modify")
assert.Contains(t, recorder.Body.String(), "1 teams")
assert.Contains(t, recorder.Body.String(), "The Cheesy Poofs")
// Allow editing a team.
recorder = postHttpResponse("/setup/teams/254/edit", "nickname=Teh Chezy Pofs")
assert.NotContains(t, recorder.Body.String(), "can't modify")
assert.Contains(t, recorder.Body.String(), "1 teams")
assert.Contains(t, recorder.Body.String(), "Teh Chezy Pofs")
}
func TestSetupTeamsBadReqest(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
recorder := getHttpResponse("/setup/teams/254/edit")
assert.Equal(t, 400, recorder.Code)
assert.Contains(t, recorder.Body.String(), "No such team")
recorder = postHttpResponse("/setup/teams/254/edit", "")
assert.Equal(t, 400, recorder.Code)
assert.Contains(t, recorder.Body.String(), "No such team")
recorder = postHttpResponse("/setup/teams/254/delete", "")
assert.Equal(t, 400, recorder.Code)
assert.Contains(t, recorder.Body.String(), "No such team")
}
func postHttpResponse(path string, body string) *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
req, _ := http.NewRequest("POST", path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
newHandler().ServeHTTP(recorder, req)
return recorder
}

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<head>
<title>{{template "title" .}}</title>
<link rel="shortcut icon" href="/static/img/favicon32.png">
<link href="/static/lib/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-default navbar-static-top" role="navigation">
@@ -19,7 +19,7 @@
<ul class="dropdown-menu">
<li><a href="#">Event Wizard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Team List</a></li>
<li><a href="/setup/teams">Team List</a></li>
<li><a href="#">Match Scheduling</a></li>
<li><a href="#">Alliance Selection</a></li>
</ul>

62
templates/edit_team.html Normal file
View File

@@ -0,0 +1,62 @@
{{define "title"}}Cheesy Arena{{end}}
{{define "body"}}
<div class="row">
<div class="col-lg-6 col-lg-offset-3">
<div class="well">
<form class="form-horizontal" action="/setup/teams/{{.Id}}/edit" method="POST">
<fieldset>
<legend>Edit Team {{.Id}}</legend>
<div class="form-group">
<label for="textArea" class="col-lg-3 control-label">Name</label>
<div class="col-lg-9">
<textarea class="form-control" rows="5" name="name">{{.Name}}</textarea>
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Nickname</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="nickname" value="{{.Nickname}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">City</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="city" value="{{.City}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">State/Province</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="stateProv" value="{{.StateProv}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Country</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="country" value="{{.Country}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Rookie Year</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="rookieYear" value="{{.RookieYear}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Robot Name</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="robotName" value="{{.RobotName}}">
</div>
</div>
<div class="form-group">
<div class="col-lg-9 col-lg-offset-3">
<a href="/setup/teams"><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>
{{end}}

89
templates/teams.html Normal file
View File

@@ -0,0 +1,89 @@
{{define "title"}}Cheesy Arena{{end}}
{{define "body"}}
{{if .ShowErrorMessage}}
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
You can't modify the team list once the qualification schedule has been generated. If you need to change the
team list, clear all other data first on the Settings page.
</div>
{{end}}
<div class="row">
<div class="col-lg-2">
<form class="form-horizontal" action="/setup/teams" method="POST">
<fieldset>
<legend>Import Teams</legend>
<div class="form-group">
<textarea class="form-control" rows="10" name="teamNumbers"
placeholder="One team number per line"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-info">Add Teams</button>
</div>
<div class="form-group">
<button class="btn btn-primary" onclick="$('#confirmClearTeams').modal('show'); return false;">
Clear Team List
</button>
</div>
</fieldset>
</form>
</div>
<div class="col-lg-10">
<table class="table table-striped table-hover ">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Nickname</th>
<th>Location</th>
<th>Rookie Year</th>
<th>Robot Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{range $team := .Teams}}
<tr>
<td>{{$team.Id}}</td>
<td>{{$team.Name}}</td>
<td>{{$team.Nickname}}</td>
<td>{{$team.City}}, {{$team.StateProv}}, {{$team.Country}}</td>
<td>{{$team.RookieYear}}</td>
<td>{{$team.RobotName}}</td>
<td class="text-center" style="white-space: nowrap;">
<form action="/setup/teams/{{$team.Id}}/delete" method="POST">
<a href="/setup/teams/{{$team.Id}}/edit">
<button type="button" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-edit"></i>
</button>
</a>
<button type="submit" class="btn btn-primary btn-xs">
<i class="glyphicon glyphicon-trash"></i>
</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
<b>{{len .Teams}} teams</b>
</div>
<div id="confirmClearTeams" class="modal" style="top: 20%;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to clear the team list?</p>
</div>
<div class="modal-footer">
<form class="form-horizontal" action="/setup/teams/clear" method="POST">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Clear Team List</button>
</form>
</div>
</div>
</div>
</div>
{{end}}

30
web.go
View File

@@ -38,21 +38,21 @@ func ServeWebInterface() {
// Open in Default Web Browser
// Necessary to Authenticate
url := "http://localhost:"+strconv.Itoa(httpPort)
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")
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())
}
if err != nil {
println(err.Error())
}
// Start Server
http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil)
@@ -60,6 +60,12 @@ func ServeWebInterface() {
func newHandler() http.Handler {
router := mux.NewRouter()
router.HandleFunc("/setup/teams", TeamsGetHandler).Methods("GET")
router.HandleFunc("/setup/teams", TeamsPostHandler).Methods("POST")
router.HandleFunc("/setup/teams/clear", TeamsClearHandler).Methods("POST")
router.HandleFunc("/setup/teams/{id}/edit", TeamEditGetHandler).Methods("GET")
router.HandleFunc("/setup/teams/{id}/edit", TeamEditPostHandler).Methods("POST")
router.HandleFunc("/setup/teams/{id}/delete", TeamDeletePostHandler).Methods("POST")
router.HandleFunc("/reports/csv/rankings", RankingsCsvReportHandler)
router.HandleFunc("/reports/pdf/rankings", RankingsPdfReportHandler)
router.HandleFunc("/reports/json/rankings", RankingsJSONReportHandler)