Added match scheduling page.

This commit is contained in:
Patrick Fairbank
2014-06-08 21:47:31 -07:00
parent 247b4f05eb
commit beb00e674d
13 changed files with 638 additions and 23 deletions

View File

@@ -18,9 +18,9 @@ const schedulesDir = "schedules"
const teamsPerMatch = 6
type ScheduleBlock struct {
startTime time.Time
numMatches int
matchSpacingSec int
StartTime time.Time
NumMatches int
MatchSpacingSec int
}
// Creates a random schedule for the given parameters and returns it as a list of matches.
@@ -31,7 +31,7 @@ func BuildRandomSchedule(teams []Team, scheduleBlocks []ScheduleBlock, matchType
matchesPerTeam := int(float32(numMatches*teamsPerMatch) / float32(numTeams))
file, err := os.Open(fmt.Sprintf("%s/%d_%d.csv", schedulesDir, numTeams, matchesPerTeam))
if err != nil {
return nil, fmt.Errorf("No schedule exists for %d teams and %d matches", numTeams, matchesPerTeam)
return nil, fmt.Errorf("No schedule template exists for %d teams and %d matches", numTeams, matchesPerTeam)
}
defer file.Close()
reader := csv.NewReader(file)
@@ -77,8 +77,8 @@ func BuildRandomSchedule(teams []Team, scheduleBlocks []ScheduleBlock, matchType
// Fill in the match times.
matchIndex := 0
for _, block := range scheduleBlocks {
for i := 0; i < block.numMatches; i++ {
matches[matchIndex].Time = block.startTime.Add(time.Duration(i*block.matchSpacingSec) * time.Second)
for i := 0; i < block.NumMatches; i++ {
matches[matchIndex].Time = block.StartTime.Add(time.Duration(i*block.MatchSpacingSec) * time.Second)
matchIndex++
}
}
@@ -89,7 +89,7 @@ func BuildRandomSchedule(teams []Team, scheduleBlocks []ScheduleBlock, matchType
func countMatches(scheduleBlocks []ScheduleBlock) int {
numMatches := 0
for _, block := range scheduleBlocks {
numMatches += block.numMatches
numMatches += block.NumMatches
}
return numMatches
}

View File

@@ -15,7 +15,7 @@ func TestNonExistentSchedule(t *testing.T) {
teams := make([]Team, 6)
scheduleBlocks := []ScheduleBlock{{time.Unix(0, 0).UTC(), 2, 60}}
_, err := BuildRandomSchedule(teams, scheduleBlocks, "test")
expectedErr := "No schedule exists for 6 teams and 2 matches"
expectedErr := "No schedule template exists for 6 teams and 2 matches"
if assert.NotNil(t, err) {
assert.Equal(t, expectedErr, err.Error())
}

167
setup_schedule.go Normal file
View File

@@ -0,0 +1,167 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web routes for generating practice and qualification schedules.
package main
import (
"fmt"
"html/template"
"net/http"
"strconv"
"time"
)
// Global vars to hold schedules that are in the process of being generated.
var cachedMatchType string
var cachedScheduleBlocks []ScheduleBlock
var cachedMatches []Match
var cachedTeamFirstMatches map[int]string
// Shows the schedule editing page.
func ScheduleGetHandler(w http.ResponseWriter, r *http.Request) {
if len(cachedScheduleBlocks) == 0 {
tomorrow := time.Now().AddDate(0, 0, 1)
location, _ := time.LoadLocation("Local")
startTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 9, 0, 0, 0, location)
cachedScheduleBlocks = append(cachedScheduleBlocks, ScheduleBlock{startTime, 10, 360})
cachedMatchType = "practice"
}
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches, "")
}
// Generates the schedule and presents it for review without saving it to the database.
func ScheduleGeneratePostHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
scheduleBlocks, err := getScheduleBlocks(r)
if err != nil {
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches,
"Incomplete or invalid schedule block parameters specified.")
return
}
// Build the schedule.
teams, err := db.GetAllTeams()
if err != nil {
handleWebErr(w, err)
return
}
if len(teams) == 0 {
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches,
"No team list is configured. Set up the list of teams at the event before generating the schedule.")
return
}
if len(teams) < 18 {
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches,
fmt.Sprintf("There are only %d teams. There must be at least 18 teams to generate a schedule.", len(teams)))
return
}
matches, err := BuildRandomSchedule(teams, scheduleBlocks, r.PostFormValue("matchType"))
if err != nil {
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches,
fmt.Sprintf("Error generating schedule: %s.", err.Error()))
return
}
// Determine each team's first match.
teamFirstMatches := make(map[int]string)
for _, match := range matches {
checkTeam := func(team int) {
_, ok := teamFirstMatches[team]
if !ok {
teamFirstMatches[team] = match.DisplayName
}
}
checkTeam(match.Red1)
checkTeam(match.Red2)
checkTeam(match.Red3)
checkTeam(match.Blue1)
checkTeam(match.Blue2)
checkTeam(match.Blue3)
}
cachedMatchType = r.PostFormValue("matchType")
cachedScheduleBlocks = scheduleBlocks
cachedMatches = matches
cachedTeamFirstMatches = teamFirstMatches
http.Redirect(w, r, "/setup/schedule", 302)
}
// Saves the generated schedule to the database.
func ScheduleSavePostHandler(w http.ResponseWriter, r *http.Request) {
existingMatches, err := db.GetMatchesByType(cachedMatchType)
if err != nil {
handleWebErr(w, err)
return
}
if len(existingMatches) > 0 {
renderSchedule(w, r, cachedMatchType, cachedScheduleBlocks, cachedMatches, cachedTeamFirstMatches,
fmt.Sprintf("Can't save schedule because a schedule of %d %s matches already exists. Clear it first "+
" on the Settings page.", len(existingMatches), cachedMatchType))
return
}
for _, match := range cachedMatches {
err = db.CreateMatch(&match)
if err != nil {
handleWebErr(w, err)
return
}
}
http.Redirect(w, r, "/setup/schedule", 302)
}
func renderSchedule(w http.ResponseWriter, r *http.Request, matchType string, scheduleBlocks []ScheduleBlock,
matches []Match, teamFirstMatches map[int]string, errorMessage string) {
teams, err := db.GetAllTeams()
if err != nil {
handleWebErr(w, err)
return
}
template, err := template.ParseFiles("templates/schedule.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
MatchType string
ScheduleBlocks []ScheduleBlock
NumTeams int
Matches []Match
TeamFirstMatches map[int]string
ErrorMessage string
}{eventSettings, matchType, scheduleBlocks, len(teams), matches, teamFirstMatches, errorMessage}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// Converts the post form variables into a slice of schedule blocks.
func getScheduleBlocks(r *http.Request) ([]ScheduleBlock, error) {
numScheduleBlocks, err := strconv.Atoi(r.PostFormValue("numScheduleBlocks"))
if err != nil {
return []ScheduleBlock{}, err
}
scheduleBlocks := make([]ScheduleBlock, numScheduleBlocks)
location, _ := time.LoadLocation("Local")
for i := 0; i < numScheduleBlocks; i++ {
scheduleBlocks[i].StartTime, err = time.ParseInLocation("2006-01-02 03:04:05 PM",
r.PostFormValue(fmt.Sprintf("startTime%d", i)), location)
if err != nil {
return []ScheduleBlock{}, err
}
scheduleBlocks[i].NumMatches, err = strconv.Atoi(r.PostFormValue(fmt.Sprintf("numMatches%d", i)))
if err != nil {
return []ScheduleBlock{}, err
}
scheduleBlocks[i].MatchSpacingSec, err = strconv.Atoi(r.PostFormValue(fmt.Sprintf("matchSpacingSec%d", i)))
if err != nil {
return []ScheduleBlock{}, err
}
}
return scheduleBlocks, nil
}

105
setup_schedule_test.go Normal file
View File

@@ -0,0 +1,105 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSetupSchedule(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
for i := 0; i < 38; i++ {
db.CreateTeam(&Team{Id: i + 101})
}
db.CreateMatch(&Match{Type: "practice", DisplayName: "1"})
// Check the default setting values.
recorder := getHttpResponse("/setup/schedule")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "360") // The default match spacing.
// Submit a schedule for generation.
postData := "numScheduleBlocks=3&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" +
"startTime1=2014-01-02 09:56:00 AM&numMatches1=17&matchSpacingSec1=420&startTime2=2014-01-03 01:00:00 PM&" +
"numMatches2=40&matchSpacingSec2=360&matchType=qualification"
recorder = postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/schedule")
assert.Contains(t, recorder.Body.String(), "2014-01-01 09:48:00") // Last match of first block.
assert.Contains(t, recorder.Body.String(), "2014-01-02 11:48:00") // Last match of second block.
assert.Contains(t, recorder.Body.String(), "2014-01-03 16:54:00") // Last match of third block.
// Save schedule.
recorder = postHttpResponse("/setup/schedule/save", "")
matches, err := db.GetMatchesByType("qualification")
assert.Nil(t, err)
assert.Equal(t, 64, len(matches))
assert.Equal(t, 1388595600, matches[0].Time.Unix())
assert.Equal(t, 1388685360, matches[7].Time.Unix())
assert.Equal(t, 1388782800, matches[24].Time.Unix())
}
func TestSetupScheduleErrors(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
// No teams.
postData := "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" +
"matchType=practice"
recorder := postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "No team list is configured.")
// Insufficient number of teams.
for i := 0; i < 17; i++ {
db.CreateTeam(&Team{Id: i + 101})
}
postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=7&matchSpacingSec0=480&" +
"matchType=practice"
recorder = postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "There must be at least 18 teams to generate a schedule.")
// More matches per team than schedules exist for.
db.CreateTeam(&Team{Id: 118})
postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=700&matchSpacingSec0=480&" +
"matchType=practice"
recorder = postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "No schedule template exists for 18 teams and 233 matches")
// Incomplete scheduling data received.
postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=&matchSpacingSec0=480&" +
"matchType=practice"
recorder = postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Incomplete or invalid schedule block parameters specified.")
// Previous schedule already exists.
for i := 18; i < 38; i++ {
db.CreateTeam(&Team{Id: i + 101})
}
db.CreateMatch(&Match{Type: "practice", DisplayName: "1"})
db.CreateMatch(&Match{Type: "practice", DisplayName: "2"})
postData = "numScheduleBlocks=1&startTime0=2014-01-01 09:00:00 AM&numMatches0=64&matchSpacingSec0=480&" +
"matchType=practice"
recorder = postHttpResponse("/setup/schedule/generate", postData)
assert.Equal(t, 302, recorder.Code)
recorder = postHttpResponse("/setup/schedule/save", postData)
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "schedule of 2 practice matches already exists")
}

View File

@@ -0,0 +1,4 @@
/*!
* Datetimepicker for Bootstrap v3
* https://github.com/Eonasdan/bootstrap-datetimepicker/
*/.bootstrap-datetimepicker-widget{top:0;left:0;width:250px;padding:4px;margin-top:1px;z-index:99999 !important;border-radius:4px}.bootstrap-datetimepicker-widget.timepicker-sbs{width:600px}.bootstrap-datetimepicker-widget.bottom:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);position:absolute;top:-7px;left:7px}.bootstrap-datetimepicker-widget.bottom:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:8px}.bootstrap-datetimepicker-widget.top:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.top:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #fff;position:absolute;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget .dow{width:14.2857%}.bootstrap-datetimepicker-widget.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget>ul{list-style-type:none;margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#999}.bootstrap-datetimepicker-widget td.today{position:relative}.bootstrap-datetimepicker-widget td.today:before{content:'';display:inline-block;border-left:7px solid transparent;border-bottom:7px solid #428bca;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{background-color:#428bca;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{background:none;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget td span{display:block;width:54px;height:54px;line-height:54px;float:left;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{background-color:#428bca;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td span.old{color:#999}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{background:none;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget th.switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{background:none;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-group.date .input-group-addon span{display:block;cursor:pointer;width:16px;height:16px}.bootstrap-datetimepicker-widget.left-oriented:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.left-oriented:after{left:auto;right:7px}.bootstrap-datetimepicker-widget ul.list-unstyled li div.timepicker div.timepicker-picker table.table-condensed tbody>tr>td{padding:0 !important}@media screen and (max-width:767px){.bootstrap-datetimepicker-widget.timepicker-sbs{width:283px}}

View File

@@ -0,0 +1,105 @@
/*
Version 3.0.0
=========================================================
bootstrap-datetimepicker.js
https://github.com/Eonasdan/bootstrap-datetimepicker
=========================================================
The MIT License (MIT)
Copyright (c) 2014 Jonathan Peterson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
!function(e){if("function"==typeof define&&define.amd)define(["jquery","moment"],e)
else{if(!jQuery)throw"bootstrap-datetimepicker requires jQuery to be loaded first"
if(!moment)throw"bootstrap-datetimepicker requires moment.js to be loaded first"
e(jQuery,moment)}}(function(e,t){if(void 0===t)throw alert("momentjs is requried"),Error("momentjs is required")
var a=0,o=t,n=function(t,n){var s={pickDate:!0,pickTime:!0,useMinutes:!0,useSeconds:!1,useCurrent:!0,minuteStepping:1,minDate:new o({y:1900}),maxDate:(new o).add(100,"y"),showToday:!0,collapse:!0,language:"en",defaultDate:"",disabledDates:!1,enabledDates:!1,icons:{},useStrict:!1,direction:"auto",sideBySide:!1,daysOfWeekDisabled:!1},d={time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down"},r=this,c=function(){var i,c=!1
if(r.options=e.extend({},s,n),r.options.icons=e.extend({},d,r.options.icons),r.element=e(t),l(),!r.options.pickTime&&!r.options.pickDate)throw Error("Must choose at least one picker")
if(r.id=a++,o.lang(r.options.language),r.date=o(),r.unset=!1,r.isInput=r.element.is("input"),r.component=!1,r.element.hasClass("input-group")&&(r.component=r.element.find(0==r.element.find(".datepickerbutton").size()?"[class^='input-group-']":".datepickerbutton")),r.format=r.options.format,i=o()._lang._longDateFormat,r.format||(r.format=r.options.pickDate?i.L:"",r.options.pickDate&&r.options.pickTime&&(r.format+=" "),r.format+=r.options.pickTime?i.LT:"",r.options.useSeconds&&(~i.LT.indexOf(" A")?r.format=r.format.split(" A")[0]+":ss A":r.format+=":ss")),r.use24hours=r.format.toLowerCase().indexOf("a")<1,r.component&&(c=r.component.find("span")),r.options.pickTime&&c&&c.addClass(r.options.icons.time),r.options.pickDate&&c&&(c.removeClass(r.options.icons.time),c.addClass(r.options.icons.date)),r.widget=e(W()).appendTo("body"),r.options.useSeconds&&!r.use24hours&&r.widget.width(300),r.minViewMode=r.options.minViewMode||0,"string"==typeof r.minViewMode)switch(r.minViewMode){case"months":r.minViewMode=1
break
case"years":r.minViewMode=2
break
default:r.minViewMode=0}if(r.viewMode=r.options.viewMode||0,"string"==typeof r.viewMode)switch(r.viewMode){case"months":r.viewMode=1
break
case"years":r.viewMode=2
break
default:r.viewMode=0}if(r.options.disabledDates=j(r.options.disabledDates),r.options.enabledDates=j(r.options.enabledDates),r.startViewMode=r.viewMode,r.setMinDate(r.options.minDate),r.setMaxDate(r.options.maxDate),g(),w(),k(),b(),y(),h(),P(),V(),""!==r.options.defaultDate&&""==p().val()&&r.setValue(r.options.defaultDate),1!==r.options.minuteStepping){var m=r.options.minuteStepping
r.date.minutes(Math.round(r.date.minutes()/m)*m%60).seconds(0)}},p=function(){return r.isInput?r.element:dateStr=r.element.find("input")},l=function(){var e
e=(r.element.is("input"),r.element.data()),void 0!==e.dateFormat&&(r.options.format=e.dateFormat),void 0!==e.datePickdate&&(r.options.pickDate=e.datePickdate),void 0!==e.datePicktime&&(r.options.pickTime=e.datePicktime),void 0!==e.dateUseminutes&&(r.options.useMinutes=e.dateUseminutes),void 0!==e.dateUseseconds&&(r.options.useSeconds=e.dateUseseconds),void 0!==e.dateUsecurrent&&(r.options.useCurrent=e.dateUsecurrent),void 0!==e.dateMinutestepping&&(r.options.minuteStepping=e.dateMinutestepping),void 0!==e.dateMindate&&(r.options.minDate=e.dateMindate),void 0!==e.dateMaxdate&&(r.options.maxDate=e.dateMaxdate),void 0!==e.dateShowtoday&&(r.options.showToday=e.dateShowtoday),void 0!==e.dateCollapse&&(r.options.collapse=e.dateCollapse),void 0!==e.dateLanguage&&(r.options.language=e.dateLanguage),void 0!==e.dateDefaultdate&&(r.options.defaultDate=e.dateDefaultdate),void 0!==e.dateDisableddates&&(r.options.disabledDates=e.dateDisableddates),void 0!==e.dateEnableddates&&(r.options.enabledDates=e.dateEnableddates),void 0!==e.dateIcons&&(r.options.icons=e.dateIcons),void 0!==e.dateUsestrict&&(r.options.useStrict=e.dateUsestrict),void 0!==e.dateDirection&&(r.options.direction=e.dateDirection),void 0!==e.dateSidebyside&&(r.options.sideBySide=e.dateSidebyside)},m=function(){var t="absolute",i=r.component?r.component.offset():r.element.offset(),a=e(window)
r.width=r.component?r.component.outerWidth():r.element.outerWidth(),i.top=i.top+r.element.outerHeight()
var o
"up"===r.options.direction?o="top":"bottom"===r.options.direction?o="bottom":"auto"===r.options.direction&&(o=i.top+r.widget.height()>a.height()+a.scrollTop()&&r.widget.height()+r.element.outerHeight()<i.top?"top":"bottom"),"top"===o?(i.top-=r.widget.height()+r.element.outerHeight()+15,r.widget.addClass("top").removeClass("bottom")):(i.top+=1,r.widget.addClass("bottom").removeClass("top")),void 0!==r.options.width&&r.widget.width(r.options.width),"left"===r.options.orientation&&(r.widget.addClass("left-oriented"),i.left=i.left-r.widget.width()+20),Y()&&(t="fixed",i.top-=a.scrollTop(),i.left-=a.scrollLeft()),a.width()<i.left+r.widget.outerWidth()?(i.right=a.width()-i.left-r.width,i.left="auto",r.widget.addClass("pull-right")):(i.right="auto",r.widget.removeClass("pull-right")),r.widget.css({position:t,top:i.top,left:i.left,right:i.right})},u=function(e,t){o(r.date).isSame(o(e))||(r.element.trigger({type:"dp.change",date:o(r.date),oldDate:o(e)}),"change"!==t&&r.element.change())},f=function(e){r.element.trigger({type:"dp.error",date:o(e)})},h=function(e){o.lang(r.options.language)
var t=e
t||(t=p().val(),t&&(r.date=o(t,r.format,r.options.useStrict)),r.date||(r.date=o())),r.viewDate=o(r.date).startOf("month"),v(),D()},g=function(){o.lang(r.options.language)
var t,i=e("<tr>"),a=o.weekdaysMin()
if(0==o()._lang._week.dow)for(t=0;7>t;t++)i.append('<th class="dow">'+a[t]+"</th>")
else for(t=1;8>t;t++)i.append(7==t?'<th class="dow">'+a[0]+"</th>":'<th class="dow">'+a[t]+"</th>")
r.widget.find(".datepicker-days thead").append(i)},w=function(){o.lang(r.options.language)
for(var e="",t=0,i=o.monthsShort();12>t;)e+='<span class="month">'+i[t++]+"</span>"
r.widget.find(".datepicker-months td").append(e)},v=function(){o.lang(r.options.language)
var t,i,a,n,s,d,c,p,l=r.viewDate.year(),m=r.viewDate.month(),u=r.options.minDate.year(),f=r.options.minDate.month(),h=r.options.maxDate.year(),g=r.options.maxDate.month(),w=[],v=o.months()
for(r.widget.find(".datepicker-days").find(".disabled").removeClass("disabled"),r.widget.find(".datepicker-months").find(".disabled").removeClass("disabled"),r.widget.find(".datepicker-years").find(".disabled").removeClass("disabled"),r.widget.find(".datepicker-days th:eq(1)").text(v[m]+" "+l),t=o(r.viewDate).subtract("months",1),d=t.daysInMonth(),t.date(d).startOf("week"),(l==u&&f>=m||u>l)&&r.widget.find(".datepicker-days th:eq(0)").addClass("disabled"),(l==h&&m>=g||l>h)&&r.widget.find(".datepicker-days th:eq(2)").addClass("disabled"),i=o(t).add(42,"d");t.isBefore(i);){if(t.weekday()===o().startOf("week").weekday()&&(a=e("<tr>"),w.push(a)),n="",t.year()<l||t.year()==l&&t.month()<m?n+=" old":(t.year()>l||t.year()==l&&t.month()>m)&&(n+=" new"),t.isSame(o({y:r.date.year(),M:r.date.month(),d:r.date.date()}))&&(n+=" active"),(N(t)||!U(t))&&(n+=" disabled"),r.options.showToday===!0&&t.isSame(o(),"day")&&(n+=" today"),r.options.daysOfWeekDisabled)for(s in r.options.daysOfWeekDisabled)if(t.day()==r.options.daysOfWeekDisabled[s]){n+=" disabled"
break}a.append('<td class="day'+n+'">'+t.date()+"</td>"),t.add(1,"d")}for(r.widget.find(".datepicker-days tbody").empty().append(w),p=r.date.year(),v=r.widget.find(".datepicker-months").find("th:eq(1)").text(l).end().find("span").removeClass("active"),p===l&&v.eq(r.date.month()).addClass("active"),u>p-1&&r.widget.find(".datepicker-months th:eq(0)").addClass("disabled"),p+1>h&&r.widget.find(".datepicker-months th:eq(2)").addClass("disabled"),s=0;12>s;s++)l==u&&f>s||u>l?e(v[s]).addClass("disabled"):(l==h&&s>g||l>h)&&e(v[s]).addClass("disabled")
for(w="",l=10*parseInt(l/10,10),c=r.widget.find(".datepicker-years").find("th:eq(1)").text(l+"-"+(l+9)).end().find("td"),r.widget.find(".datepicker-years").find("th").removeClass("disabled"),u>l&&r.widget.find(".datepicker-years").find("th:eq(0)").addClass("disabled"),l+9>h&&r.widget.find(".datepicker-years").find("th:eq(2)").addClass("disabled"),l-=1,s=-1;11>s;s++)w+='<span class="year'+(-1===s||10===s?" old":"")+(p===l?" active":"")+(u>l||l>h?" disabled":"")+'">'+l+"</span>",l+=1
c.html(w)},k=function(){o.lang(r.options.language)
var e,t,i,a=r.widget.find(".timepicker .timepicker-hours table"),n=""
if(a.parent().hide(),r.use24hours)for(e=0,t=0;6>t;t+=1){for(n+="<tr>",i=0;4>i;i+=1)n+='<td class="hour">'+F(""+e)+"</td>",e++
n+="</tr>"}else for(e=1,t=0;3>t;t+=1){for(n+="<tr>",i=0;4>i;i+=1)n+='<td class="hour">'+F(""+e)+"</td>",e++
n+="</tr>"}a.html(n)},b=function(){var e,t,i=r.widget.find(".timepicker .timepicker-minutes table"),a="",o=0,n=r.options.minuteStepping
for(i.parent().hide(),1==n&&(n=5),e=0;e<Math.ceil(60/n/4);e++){for(a+="<tr>",t=0;4>t;t+=1)60>o?(a+='<td class="minute">'+F(""+o)+"</td>",o+=n):a+="<td></td>"
a+="</tr>"}i.html(a)},y=function(){var e,t,i=r.widget.find(".timepicker .timepicker-seconds table"),a="",o=0
for(i.parent().hide(),e=0;3>e;e++){for(a+="<tr>",t=0;4>t;t+=1)a+='<td class="second">'+F(""+o)+"</td>",o+=5
a+="</tr>"}i.html(a)},D=function(){if(r.date){var e=r.widget.find(".timepicker span[data-time-component]"),t=r.date.hours(),i="AM"
r.use24hours||(t>=12&&(i="PM"),0===t?t=12:12!=t&&(t%=12),r.widget.find(".timepicker [data-action=togglePeriod]").text(i)),e.filter("[data-time-component=hours]").text(F(t)),e.filter("[data-time-component=minutes]").text(F(r.date.minutes())),e.filter("[data-time-component=seconds]").text(F(r.date.second()))}},M=function(t){t.stopPropagation(),t.preventDefault(),r.unset=!1
var i,a,n,s,d=e(t.target).closest("span, td, th"),c=o(r.date)
if(1===d.length&&!d.is(".disabled"))switch(d[0].nodeName.toLowerCase()){case"th":switch(d[0].className){case"switch":P(1)
break
case"prev":case"next":n=B.modes[r.viewMode].navStep,"prev"===d[0].className&&(n=-1*n),r.viewDate.add(n,B.modes[r.viewMode].navFnc),v()}break
case"span":d.is(".month")?(i=d.parent().find("span").index(d),r.viewDate.month(i)):(a=parseInt(d.text(),10)||0,r.viewDate.year(a)),r.viewMode===r.minViewMode&&(r.date=o({y:r.viewDate.year(),M:r.viewDate.month(),d:r.viewDate.date(),h:r.date.hours(),m:r.date.minutes(),s:r.date.seconds()}),u(c,t.type),O()),P(-1),v()
break
case"td":d.is(".day")&&(s=parseInt(d.text(),10)||1,i=r.viewDate.month(),a=r.viewDate.year(),d.is(".old")?0===i?(i=11,a-=1):i-=1:d.is(".new")&&(11==i?(i=0,a+=1):i+=1),r.date=o({y:a,M:i,d:s,h:r.date.hours(),m:r.date.minutes(),s:r.date.seconds()}),r.viewDate=o({y:a,M:i,d:Math.min(28,s)}),v(),O(),u(c,t.type))}},x={incrementHours:function(){L("add","hours",1)},incrementMinutes:function(){L("add","minutes",r.options.minuteStepping)},incrementSeconds:function(){L("add","seconds",1)},decrementHours:function(){L("subtract","hours",1)},decrementMinutes:function(){L("subtract","minutes",r.options.minuteStepping)},decrementSeconds:function(){L("subtract","seconds",1)},togglePeriod:function(){var e=r.date.hours()
e>=12?e-=12:e+=12,r.date.hours(e)},showPicker:function(){r.widget.find(".timepicker > div:not(.timepicker-picker)").hide(),r.widget.find(".timepicker .timepicker-picker").show()},showHours:function(){r.widget.find(".timepicker .timepicker-picker").hide(),r.widget.find(".timepicker .timepicker-hours").show()},showMinutes:function(){r.widget.find(".timepicker .timepicker-picker").hide(),r.widget.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){r.widget.find(".timepicker .timepicker-picker").hide(),r.widget.find(".timepicker .timepicker-seconds").show()},selectHour:function(t){var i=r.widget.find(".timepicker [data-action=togglePeriod]").text(),a=parseInt(e(t.target).text(),10)
"PM"==i&&(a+=12),r.date.hours(a),x.showPicker.call(r)},selectMinute:function(t){r.date.minutes(parseInt(e(t.target).text(),10)),x.showPicker.call(r)},selectSecond:function(t){r.date.seconds(parseInt(e(t.target).text(),10)),x.showPicker.call(r)}},S=function(t){var i=o(r.date),a=e(t.currentTarget).data("action"),n=x[a].apply(r,arguments)
return T(t),r.date||(r.date=o({y:1970})),O(),D(),u(i,t.type),n},T=function(e){e.stopPropagation(),e.preventDefault()},C=function(t){o.lang(r.options.language)
var i=e(t.target),a=o(r.date),n=o(i.val(),r.format,r.options.useStrict)
n.isValid()&&!N(n)&&U(n)?(h(),r.setValue(n),u(a,t.type),O()):(r.viewDate=a,u(a,t.type),f(n),r.unset=!0)},P=function(e){e&&(r.viewMode=Math.max(r.minViewMode,Math.min(2,r.viewMode+e)))
B.modes[r.viewMode].clsName
r.widget.find(".datepicker > div").hide().filter(".datepicker-"+B.modes[r.viewMode].clsName).show()},V=function(){var t,i,a,o,n
r.widget.on("click",".datepicker *",e.proxy(M,this)),r.widget.on("click","[data-action]",e.proxy(S,this)),r.widget.on("mousedown",e.proxy(T,this)),r.options.pickDate&&r.options.pickTime&&r.widget.on("click.togglePicker",".accordion-toggle",function(s){if(s.stopPropagation(),t=e(this),i=t.closest("ul"),a=i.find(".in"),o=i.find(".collapse:not(.in)"),a&&a.length){if(n=a.data("collapse"),n&&n.date-transitioning)return
a.collapse("hide"),o.collapse("show"),t.find("span").toggleClass(r.options.icons.time+" "+r.options.icons.date),r.element.find(".input-group-addon span").toggleClass(r.options.icons.time+" "+r.options.icons.date)}}),r.isInput?r.element.on({focus:e.proxy(r.show,this),change:e.proxy(C,this),blur:e.proxy(r.hide,this)}):(r.element.on({change:e.proxy(C,this)},"input"),r.component?r.component.on("click",e.proxy(r.show,this)):r.element.on("click",e.proxy(r.show,this)))},q=function(){e(window).on("resize.datetimepicker"+r.id,e.proxy(m,this)),r.isInput||e(document).on("mousedown.datetimepicker"+r.id,e.proxy(r.hide,this))},I=function(){r.widget.off("click",".datepicker *",r.click),r.widget.off("click","[data-action]"),r.widget.off("mousedown",r.stopEvent),r.options.pickDate&&r.options.pickTime&&r.widget.off("click.togglePicker"),r.isInput?r.element.off({focus:r.show,change:r.change}):(r.element.off({change:r.change},"input"),r.component?r.component.off("click",r.show):r.element.off("click",r.show))},H=function(){e(window).off("resize.datetimepicker"+r.id),r.isInput||e(document).off("mousedown.datetimepicker"+r.id)},Y=function(){if(r.element){var t,i=r.element.parents(),a=!1
for(t=0;t<i.length;t++)if("fixed"==e(i[t]).css("position")){a=!0
break}return a}return!1},O=function(){o.lang(r.options.language)
var e=""
r.unset||(e=o(r.date).format(r.format)),p().val(e),r.element.data("date",e),r.options.pickTime||r.hide()},L=function(e,t,i){o.lang(r.options.language)
var a
return"add"==e?(a=o(r.date),23==a.hours()&&a.add(i,t),a.add(i,t)):a=o(r.date).subtract(i,t),N(o(a.subtract(i,t)))||N(a)?void f(a.format(r.format)):("add"==e?r.date.add(i,t):r.date.subtract(i,t),void(r.unset=!1))},N=function(e){return o.lang(r.options.language),e.isAfter(r.options.maxDate)||e.isBefore(r.options.minDate)?!0:r.options.disabledDates===!1?!1:r.options.disabledDates[o(e).format("YYYY-MM-DD")]===!0},U=function(e){return o.lang(r.options.language),r.options.enabledDates===!1?!0:r.options.enabledDates[o(e).format("YYYY-MM-DD")]===!0},j=function(e){var t={},a=0
for(i=0;i<e.length;i++)dDate=o(e[i]),dDate.isValid()&&(t[dDate.format("YYYY-MM-DD")]=!0,a++)
return a>0?t:!1},F=function(e){return e=""+e,e.length>=2?e:"0"+e},W=function(){if(r.options.pickDate&&r.options.pickTime){var e=""
return e='<div class="bootstrap-datetimepicker-widget'+(r.options.sideBySide?" timepicker-sbs":"")+' dropdown-menu" style="z-index:9999 !important;">',e+=r.options.sideBySide?'<div class="row"><div class="col-sm-6 datepicker">'+B.template+'</div><div class="col-sm-6 timepicker">'+E.getTemplate()+"</div></div>":'<ul class="list-unstyled"><li'+(r.options.collapse?' class="collapse in"':"")+'><div class="datepicker">'+B.template+'</div></li><li class="picker-switch accordion-toggle"><a class="btn" style="width:100%"><span class="'+r.options.icons.time+'"></span></a></li><li'+(r.options.collapse?' class="collapse"':"")+'><div class="timepicker">'+E.getTemplate()+"</div></li></ul>",e+="</div>"}return r.options.pickTime?'<div class="bootstrap-datetimepicker-widget dropdown-menu"><div class="timepicker">'+E.getTemplate()+"</div></div>":'<div class="bootstrap-datetimepicker-widget dropdown-menu"><div class="datepicker">'+B.template+"</div></div>"},B={modes:[{clsName:"days",navFnc:"month",navStep:1},{clsName:"months",navFnc:"year",navStep:1},{clsName:"years",navFnc:"year",navStep:10}],headTemplate:'<thead><tr><th class="prev">&lsaquo;</th><th colspan="5" class="switch"></th><th class="next">&rsaquo;</th></tr></thead>',contTemplate:'<tbody><tr><td colspan="7"></td></tr></tbody>'},E={hourTemplate:'<span data-action="showHours" data-time-component="hours" class="timepicker-hour"></span>',minuteTemplate:'<span data-action="showMinutes" data-time-component="minutes" class="timepicker-minute"></span>',secondTemplate:'<span data-action="showSeconds" data-time-component="seconds" class="timepicker-second"></span>'}
B.template='<div class="datepicker-days"><table class="table-condensed">'+B.headTemplate+'<tbody></tbody></table></div><div class="datepicker-months"><table class="table-condensed">'+B.headTemplate+B.contTemplate+'</table></div><div class="datepicker-years"><table class="table-condensed">'+B.headTemplate+B.contTemplate+"</table></div>",E.getTemplate=function(){return'<div class="timepicker-picker"><table class="table-condensed"><tr><td><a href="#" class="btn" data-action="incrementHours"><span class="'+r.options.icons.up+'"></span></a></td><td class="separator"></td><td>'+(r.options.useMinutes?'<a href="#" class="btn" data-action="incrementMinutes"><span class="'+r.options.icons.up+'"></span></a>':"")+"</td>"+(r.options.useSeconds?'<td class="separator"></td><td><a href="#" class="btn" data-action="incrementSeconds"><span class="'+r.options.icons.up+'"></span></a></td>':"")+(r.use24hours?"":'<td class="separator"></td>')+"</tr><tr><td>"+E.hourTemplate+'</td> <td class="separator">:</td><td>'+(r.options.useMinutes?E.minuteTemplate:'<span class="timepicker-minute">00</span>')+"</td> "+(r.options.useSeconds?'<td class="separator">:</td><td>'+E.secondTemplate+"</td>":"")+(r.use24hours?"":'<td class="separator"></td><td><button type="button" class="btn btn-primary" data-action="togglePeriod"></button></td>')+'</tr><tr><td><a href="#" class="btn" data-action="decrementHours"><span class="'+r.options.icons.down+'"></span></a></td><td class="separator"></td><td>'+(r.options.useMinutes?'<a href="#" class="btn" data-action="decrementMinutes"><span class="'+r.options.icons.down+'"></span></a>':"")+"</td>"+(r.options.useSeconds?'<td class="separator"></td><td><a href="#" class="btn" data-action="decrementSeconds"><span class="'+r.options.icons.down+'"></span></a></td>':"")+(r.use24hours?"":'<td class="separator"></td>')+'</tr></table></div><div class="timepicker-hours" data-action="selectHour"><table class="table-condensed"></table></div><div class="timepicker-minutes" data-action="selectMinute"><table class="table-condensed"></table></div>'+(r.options.useSeconds?'<div class="timepicker-seconds" data-action="selectSecond"><table class="table-condensed"></table></div>':"")},r.destroy=function(){I(),H(),r.widget.remove(),r.element.removeData("DateTimePicker"),r.component&&r.component.removeData("DateTimePicker")},r.show=function(e){if(r.options.useCurrent&&""==p().val())if(1!==r.options.minuteStepping){var t=o(),i=r.options.minuteStepping
t.minutes(Math.round(t.minutes()/i)*i%60).seconds(0),r.setValue(t.format(r.format))}else r.setValue(o().format(r.format))
r.widget.show(),r.height=r.component?r.component.outerHeight():r.element.outerHeight(),m(),r.element.trigger({type:"dp.show",date:o(r.date)}),q(),e&&T(e)},r.disable=function(){var e=r.element.find("input")
e.prop("disabled")||(e.prop("disabled",!0),I())},r.enable=function(){var e=r.element.find("input")
e.prop("disabled")&&(e.prop("disabled",!1),V())},r.hide=function(t){if(!t||!e(t.target).is(r.element.attr("id"))){var i,a,n=r.widget.find(".collapse")
for(i=0;i<n.length;i++)if(a=n.eq(i).data("collapse"),a&&a.date-transitioning)return
r.widget.hide(),r.viewMode=r.startViewMode,P(),r.element.trigger({type:"dp.hide",date:o(r.date)}),H()}},r.setValue=function(e){o.lang(r.options.language),e?r.unset=!1:(r.unset=!0,O()),o.isMoment(e)||(e=o(e,r.format)),e.isValid()?(r.date=e,O(),r.viewDate=o({y:r.date.year(),M:r.date.month()}),v(),D()):f(e)},r.getDate=function(){return r.unset?null:r.date},r.setDate=function(e){var t=o(r.date)
r.setValue(e?e:null),u(t,"function")},r.setDisabledDates=function(e){r.options.disabledDates=j(e),r.viewDate&&h()},r.setEnabledDates=function(e){r.options.enabledDates=j(e),r.viewDate&&h()},r.setMaxDate=function(e){void 0!=e&&(r.options.maxDate=o(e),r.viewDate&&h())},r.setMinDate=function(e){void 0!=e&&(r.options.minDate=o(e),r.viewDate&&h())},c()}
e.fn.datetimepicker=function(t){return this.each(function(){var i=e(this),a=i.data("DateTimePicker")
a||i.data("DateTimePicker",new n(this,t))})}})

6
static/js/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,80 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side methods for the schedule generation page.
var blockTemplate = Handlebars.compile($("#blockTemplate").html());
var lastBlockNumber = 0;
var blockMatches = {};
// Adds a new scheduling block to the page.
var addBlock = function(startTime, numMatches, matchSpacingSec) {
var endTime = moment(startTime + numMatches * matchSpacingSec * 1000);
lastBlockNumber += 1;
var block = blockTemplate({blockNumber: lastBlockNumber, matchSpacingSec: matchSpacingSec});
$("#blockContainer").append(block);
$("#startTimePicker" + lastBlockNumber).datetimepicker({useSeconds: true}).
data("DateTimePicker").setDate(startTime);
$("#endTimePicker" + lastBlockNumber).datetimepicker({useSeconds: true}).
data("DateTimePicker").setDate(endTime);
updateBlock(lastBlockNumber);
}
// Updates the per-block and global schedule statistics.
var updateBlock = function(blockNumber) {
var startTime = moment(Date.parse($("#startTime" + blockNumber).val()));
var endTime = moment(Date.parse($("#endTime" + blockNumber).val()));
var matchSpacingSec = parseInt($("#matchSpacingSec" + blockNumber).val());
var numMatches = Math.floor((endTime - startTime) / matchSpacingSec / 1000);
var actualEndTime = moment(startTime + numMatches * matchSpacingSec * 1000).format("hh:mm:ss A");
blockMatches[blockNumber] = numMatches;
if (matchSpacingSec == "" || isNaN(numMatches) || numMatches <= 0) {
numMatches = "";
actualEndTime = "";
blockMatches[blockNumber] = 0;
}
$("#numMatches" + blockNumber).text(numMatches);
$("#actualEndTime" + blockNumber).text(actualEndTime);
// Update total number of matches.
var totalNumMatches = 0;
$.each(blockMatches, function(k, v) {
totalNumMatches += v;
});
var matchesPerTeam = Math.floor(totalNumMatches * 6 / numTeams);
var numExcessMatches = totalNumMatches - Math.ceil(matchesPerTeam * numTeams / 6);
var nextLevelMatches = Math.ceil((matchesPerTeam + 1) * numTeams / 6) - totalNumMatches;
$("#totalNumMatches").text(totalNumMatches);
$("#matchesPerTeam").text(matchesPerTeam);
$("#numExcessMatches").text(numExcessMatches);
$("#nextLevelMatches").text(nextLevelMatches);
}
var deleteBlock = function(blockNumber) {
delete blockMatches[blockNumber];
$("#block" + blockNumber).remove();
updateBlock(blockNumber);
}
// Dynamically generates and posts a form containing the schedule blocks to the server for population.
var generateSchedule = function() {
var form = $("#scheduleForm");
form.attr("method", "POST");
form.attr("action", "/setup/schedule/generate");
var addField = function(name, value) {
var field = $(document.createElement("input"));
field.attr("type", "hidden");
field.attr("name", name);
field.attr("value", value);
form.append(field);
}
var i = 0;
$.each(blockMatches, function(k, v) {
addField("startTime" + i, $("#startTime" + k).val())
addField("numMatches" + i, $("#numMatches" + k).text())
addField("matchSpacingSec" + i, $("#matchSpacingSec" + k).val())
i++;
});
addField("numScheduleBlocks", i);
form.submit();
}

View File

@@ -6,6 +6,7 @@
<link rel="shortcut icon" href="/static/img/favicon32.png">
<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">
</head>
<body>
<div class="navbar navbar-default navbar-static-top" role="navigation">
@@ -21,7 +22,7 @@
<li><a href="#">Event Wizard</a></li>
<li><a href="/setup/settings">Settings</a></li>
<li><a href="/setup/teams">Team List</a></li>
<li><a href="#">Match Scheduling</a></li>
<li><a href="/setup/schedule">Match Scheduling</a></li>
<li><a href="#">Alliance Selection</a></li>
</ul>
</li>
@@ -81,6 +82,9 @@
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/bootstrap.min.js"></script>
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/moment.min.js"></script>
<script src="/static/js/bootstrap-datetimepicker.min.js"></script>
<script src="/static/lib/handlebars-1.3.0.js"></script>
{{template "script" .}}
</body>
</html>

View File

@@ -7,43 +7,43 @@
<fieldset>
<legend>Edit Team {{.Team.Id}}</legend>
<div class="form-group">
<label for="textArea" class="col-lg-3 control-label">Name</label>
<label class="col-lg-3 control-label">Name</label>
<div class="col-lg-9">
<textarea class="form-control" rows="5" name="name">{{.Team.Name}}</textarea>
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Nickname</label>
<label class="col-lg-3 control-label">Nickname</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="nickname" value="{{.Team.Nickname}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">City</label>
<label class="col-lg-3 control-label">City</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="city" value="{{.Team.City}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">State/Province</label>
<label class="col-lg-3 control-label">State/Province</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="stateProv" value="{{.Team.StateProv}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Country</label>
<label class="col-lg-3 control-label">Country</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="country" value="{{.Team.Country}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Rookie Year</label>
<label class="col-lg-3 control-label">Rookie Year</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="rookieYear" value="{{.Team.RookieYear}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-3 control-label">Robot Name</label>
<label class="col-lg-3 control-label">Robot Name</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="robotName" value="{{.Team.RobotName}}">
</div>

141
templates/schedule.html Normal file
View File

@@ -0,0 +1,141 @@
{{define "title"}}Match Scheduling{{end}}
{{define "body"}}
<div class="row">
{{if .ErrorMessage}}
<div class="alert alert-dismissable alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
{{.ErrorMessage}}
</div>
{{end}}
<div class="col-lg-5">
<div class="well">
<form id="scheduleForm" class="form-horizontal" action="/setup/schedule/save" method="POST">
<fieldset>
<legend>Schedule Parameters</legend>
<div class="form-group">
<label class="col-lg-5 control-label">Match Type</label>
<div class="col-lg-7">
<div class="radio">
<label>
<input type="radio" name="matchType" value="practice"
{{if eq .MatchType "practice"}}checked{{end}}>
Practice
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="matchType" value="qualification"
{{if eq .MatchType "qualification"}}checked{{end}}>
Qualification
</label>
</div>
</div>
</div>
<div id="blockContainer"></div>
<p>
<b>Total match count: <span id="totalNumMatches">0</span></b><br />
<b>Matches per team: <span id="matchesPerTeam">0</span></b><br />
<b>Excess matches: <span id="numExcessMatches">0</span></b><br />
<b>Matches needed for +1 per team: <span id="nextLevelMatches">0</span></b>
</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;">
Generate Schedule
</button>
<button type="submit" class="btn btn-primary">Save Schedule</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="col-lg-5">
<table class="table table-striped table-hover ">
<thead>
<tr>
<th>Match</th>
<th>Type</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{{range $match := .Matches}}
<tr>
<td>{{$match.DisplayName}}</td>
<td>{{$match.Type}}</td>
<td>{{$match.Time}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="col-lg-2">
<table class="table table-striped table-hover ">
<thead>
<tr>
<th>Team</th>
<th>First Match</th>
</tr>
</thead>
<tbody>
{{range $team, $firstMatch := .TeamFirstMatches}}
<tr>
<td>{{$team}}</td>
<td>{{$firstMatch}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div id="blockTemplate" style="display: none;">
<div class="well well-sm" id="block{{"{{blockNumber}}"}}">
<b>Block {{"{{blockNumber}}"}}</b>
<button type="button" class="close" onclick="deleteBlock({{"{{blockNumber}}"}});">×</button><br /><br />
<div class="form-group">
<label class="col-lg-4 control-label">Start Time</label>
<div class="col-lg-8">
<div class="input-group date" id="startTimePicker{{"{{blockNumber}}"}}"
data-date-format="YYYY-MM-DD hh:mm:ss A" onchange="updateBlock({{"{{blockNumber}}"}});">
<input type="text" class="form-control" id="startTime{{"{{blockNumber}}"}}"
onchange="updateBlock({{"{{blockNumber}}"}});">
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-4 control-label">End Time</label>
<div class="col-lg-8">
<div class="input-group date" id="endTimePicker{{"{{blockNumber}}"}}"
data-date-format="YYYY-MM-DD hh:mm:ss A" onchange="updateBlock({{"{{blockNumber}}"}});">
<input type="text" class="form-control" id="endTime{{"{{blockNumber}}"}}"
onchange="updateBlock({{"{{blockNumber}}"}});">
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-4 control-label">Cycle Time (sec)</label>
<div class="col-lg-8">
<input type="text" class="form-control" id="matchSpacingSec{{"{{blockNumber}}"}}"
value="{{"{{matchSpacingSec}}"}}" placeholder="360" onchange="updateBlock({{"{{blockNumber}}"}});">
</div>
</div>
<div class="form-group">
<div class="col-lg-5">Match count: <span id="numMatches{{"{{blockNumber}}"}}"></span></div>
<div class="col-lg-7">Actual end time: <span id="actualEndTime{{"{{blockNumber}}"}}"></span></div>
</div>
</div>
</div>
{{end}}
{{define "script"}}
<script>var numTeams = {{.NumTeams}};</script>
<script src="/static/js/setup_schedule.js"></script>
<script>
{{range $block := .ScheduleBlocks}}
addBlock(moment(Date.parse({{$block.StartTime}})), {{$block.NumMatches}}, {{$block.MatchSpacingSec}});
{{end}}
</script>
{{end}}

View File

@@ -13,19 +13,19 @@
<fieldset>
<legend>Event Settings</legend>
<div class="form-group">
<label for="textArea" class="col-lg-5 control-label">Name</label>
<label class="col-lg-5 control-label">Name</label>
<div class="col-lg-7">
<input type="text" class="form-control" name="name" value="{{.Name}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Code</label>
<label class="col-lg-5 control-label">Code</label>
<div class="col-lg-7">
<input type="text" class="form-control" name="code" value="{{.Code}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Display Background Color</label>
<label class="col-lg-5 control-label">Display Background Color</label>
<div class="col-lg-7">
<div class="input-group" id="displayBackgroundColor">
<input type="text" class="form-control" name="displayBackgroundColor"
@@ -35,13 +35,13 @@
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Number of Alliances</label>
<label class="col-lg-5 control-label">Number of Alliances</label>
<div class="col-lg-7">
<input type="text" class="form-control" name="numElimAlliances" value="{{.NumElimAlliances}}">
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Round 1 Selection Order</label>
<label class="col-lg-5 control-label">Round 1 Selection Order</label>
<div class="col-lg-7">
<div class="radio">
<label>
@@ -60,7 +60,7 @@
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Round 2 Selection Order</label>
<label class="col-lg-5 control-label">Round 2 Selection Order</label>
<div class="col-lg-7">
<div class="radio">
<label>
@@ -79,7 +79,7 @@
</div>
</div>
<div class="form-group">
<label for="inputEmail" class="col-lg-5 control-label">Round 3 Selection Order</label>
<label class="col-lg-5 control-label">Round 3 Selection Order</label>
<div class="col-lg-7">
<div class="radio">
<label>

3
web.go
View File

@@ -74,6 +74,9 @@ func newHandler() http.Handler {
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("/setup/schedule", ScheduleGetHandler).Methods("GET")
router.HandleFunc("/setup/schedule/generate", ScheduleGeneratePostHandler).Methods("POST")
router.HandleFunc("/setup/schedule/save", ScheduleSavePostHandler).Methods("POST")
router.HandleFunc("/reports/csv/rankings", RankingsCsvReportHandler)
router.HandleFunc("/reports/pdf/rankings", RankingsPdfReportHandler)
router.HandleFunc("/reports/json/rankings", RankingsJSONReportHandler)