mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Added match scheduling page.
This commit is contained in:
14
schedule.go
14
schedule.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
167
setup_schedule.go
Normal 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
105
setup_schedule_test.go
Normal 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")
|
||||
}
|
||||
4
static/css/bootstrap-datetimepicker.min.css
vendored
Normal file
4
static/css/bootstrap-datetimepicker.min.css
vendored
Normal 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}}
|
||||
105
static/js/bootstrap-datetimepicker.min.js
vendored
Normal file
105
static/js/bootstrap-datetimepicker.min.js
vendored
Normal 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">‹</th><th colspan="5" class="switch"></th><th class="next">›</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
6
static/js/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
80
static/js/setup_schedule.js
Normal file
80
static/js/setup_schedule.js
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
141
templates/schedule.html
Normal 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}}
|
||||
@@ -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
3
web.go
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user