diff --git a/.gitignore b/.gitignore index 2eacb04..a51d231 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ # Ignore the binaries -gjvote -gjvote.darwin64 -gjvote.linux386 -gjvote.linux64 -gjvote.linuxarm -gjvote.win386.exe -gjvote.win64.exe +ictgj-voting +ictgj-voting.darwin64 +ictgj-voting.linux386 +ictgj-voting.linux64 +ictgj-voting.linuxarm +ictgj-voting.win386.exe +ictgj-voting.win64.exe # Ignore the DBs *.db diff --git a/admin_archive.go b/admin_archive.go index 0a49456..e127b30 100644 --- a/admin_archive.go +++ b/admin_archive.go @@ -15,6 +15,6 @@ func handleAdminArchive(w http.ResponseWriter, req *http.Request, page *pageData type archivePageData struct { Gamejams []Gamejam } - apd := new(archivePageData) + //apd := new(archivePageData) } } diff --git a/admin_clients.go b/admin_clients.go index 7e2c2c0..641fa78 100644 --- a/admin_clients.go +++ b/admin_clients.go @@ -11,29 +11,31 @@ func handleAdminClients(w http.ResponseWriter, req *http.Request, page *pageData vars := mux.Vars(req) page.SubTitle = "Clients" clientId := vars["id"] - client := db.getClient(clientId) + client, err := m.GetClient(clientId) + if err != nil { + client = NewClient(clientId) + } clientIp, _, _ := net.SplitHostPort(req.RemoteAddr) if clientId == "" { type clientsPageData struct { Clients []Client } - page.TemplateData = clientsPageData{Clients: db.getAllClients()} + page.TemplateData = clientsPageData{Clients: m.clients} page.SubTitle = "Clients" page.show("admin-clients.html", w) } else { switch vars["function"] { case "add": page.SubTitle = "Authenticate Client" - cli := db.getClient(clientId) - if cli.IP == "" { - cli.IP = clientIp + if client.IP == "" { + client.IP = clientIp } type actClientPageData struct { Id string Ip string Name string } - page.TemplateData = actClientPageData{Id: cli.UUID, Ip: cli.IP, Name: cli.Name} + page.TemplateData = actClientPageData{Id: client.UUID, Ip: client.IP, Name: client.Name} page.show("admin-activateclient.html", w) case "auth": email := req.FormValue("email") @@ -44,16 +46,13 @@ func handleAdminClients(w http.ResponseWriter, req *http.Request, page *pageData client.Name = clientName } client.IP = clientIp - client.save() + m.UpdateClient(client) if page.LoggedIn || doLogin(email, password) == nil { // Received a valid login // Authenticate the client client.Auth = true - if client.save() == nil { - page.session.setFlashMessage("Client Authenticated", "success") - } else { - page.session.setFlashMessage("Client Authentication Failed", "error") - } + m.UpdateClient(client) + page.session.setFlashMessage("Client Authenticated", "success") if page.LoggedIn { redirect("/admin/clients", w, req) } @@ -61,11 +60,8 @@ func handleAdminClients(w http.ResponseWriter, req *http.Request, page *pageData redirect("/", w, req) case "deauth": client.Auth = false - if client.save() == nil { - page.session.setFlashMessage("Client De-Authenticated", "success") - } else { - page.session.setFlashMessage("Client De-Authentication Failed", "success") - } + m.UpdateClient(client) + page.session.setFlashMessage("Client De-Authenticated", "success") redirect("/admin/clients", w, req) } } diff --git a/admin_endpoints.go b/admin_endpoints.go index 52251c6..dd26b18 100644 --- a/admin_endpoints.go +++ b/admin_endpoints.go @@ -62,8 +62,8 @@ func handleAdminSetMode(w http.ResponseWriter, req *http.Request, page *pageData if err != nil { page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error") } - if dbSetPublicSiteMode(newMode) != nil { - page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error") + if err = m.site.SetPublicMode(newMode); err != nil { + page.session.setFlashMessage(err.Error(), "error") } redirect("/admin", w, req) } @@ -74,8 +74,8 @@ func handleAdminSetAuthMode(w http.ResponseWriter, req *http.Request, page *page if err != nil { page.session.setFlashMessage("Invalid Authentication Mode: "+vars["id"], "error") } - if db.setAuthMode(newMode) != nil { - page.session.setFlashMessage("Invalid Authentication Mode: "+vars["id"], "error") + if err = m.site.SetAuthMode(newMode); err != nil { + page.session.setFlashMessage(err.Error(), "error") } redirect("/admin", w, req) } diff --git a/admin_games.go b/admin_games.go index 8fc755f..2485333 100644 --- a/admin_games.go +++ b/admin_games.go @@ -20,40 +20,72 @@ func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) teamId := vars["id"] if teamId == "" { // Games List - type gamesPageData struct { - Teams []Team - } - gpd := new(gamesPageData) - gpd.Teams = db.getAllTeams() - page.TemplateData = gpd + page.TemplateData = m.jam page.SubTitle = "Games" page.show("admin-games.html", w) } else { - tm := db.getTeam(teamId) + tm, _ := m.jam.GetTeamById(teamId) if tm != nil { switch vars["function"] { case "save": - gm := newGame(tm.UUID) + var err error + var gm *Game + if gm, err = NewGame(tm.UUID); err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + } gm.Name = req.FormValue("gamename") gm.Link = req.FormValue("gamelink") gm.Description = req.FormValue("gamedesc") - if err := gm.save(); err != nil { + if err := m.jam.UpdateGame(tm.UUID, gm); err != nil { page.session.setFlashMessage("Error updating game: "+err.Error(), "error") } else { page.session.setFlashMessage("Team game updated", "success") } redirect("/admin/teams/"+tm.UUID+"#game", w, req) + case "screenshotupload": - if err := saveScreenshots(tm, req); err != nil { + var ss *Screenshot + tm, err := m.jam.GetTeamById(tm.UUID) + if err != nil { page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + redirect("/admin/teams/"+tm.UUID+"#game", w, req) + } + ss, err = ssFromRequest(tm, req) + if err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + redirect("/admin/teams/"+tm.UUID+"#game", w, req) + } + gm := tm.Game + gm.Screenshots = append(gm.Screenshots, *ss) + if err = m.jam.UpdateGame(tm.UUID, gm); err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + } else { + page.session.setFlashMessage("Screenshot Uploaded", "success") } redirect("/admin/teams/"+tm.UUID+"#game", w, req) + case "screenshotdelete": + var err error ssid := vars["subid"] - if err := tm.deleteScreenshot(ssid); err != nil { - page.session.setFlashMessage("Error deleting screenshot: "+err.Error(), "error") + tm, err := m.jam.GetTeamById(tm.UUID) + if err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + redirect("/admin/teams/"+tm.UUID+"#game", w, req) + break + } + gm := tm.Game + if err = gm.RemoveScreenshot(ssid); err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + redirect("/admin/teams/"+tm.UUID+"#game", w, req) + break + } + if err = m.jam.UpdateGame(tm.UUID, gm); err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + } else { + page.session.setFlashMessage("Screenshot Removed", "success") } redirect("/admin/teams/"+tm.UUID+"#game", w, req) + } } else { page.session.setFlashMessage("Not a valid team id", "error") @@ -62,40 +94,47 @@ func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) } } -func saveScreenshots(tm *Team, req *http.Request) error { +func ssFromRequest(tm *Team, req *http.Request) (*Screenshot, error) { var err error + var ss *Screenshot + file, hdr, err := req.FormFile("newssfile") if err != nil { - return err + return nil, err } extIdx := strings.LastIndex(hdr.Filename, ".") fltp := "png" if len(hdr.Filename) > extIdx { fltp = hdr.Filename[extIdx+1:] } - m, _, err := image.Decode(file) + mI, _, err := image.Decode(file) buf := new(bytes.Buffer) // We convert everything to jpg - if err = jpeg.Encode(buf, m, nil); err != nil { - return errors.New("Unable to encode image") + if err = jpeg.Encode(buf, mI, nil); err != nil { + return nil, errors.New("Unable to encode image") } - thm := resize.Resize(200, 0, m, resize.Lanczos3) + thm := resize.Resize(200, 0, mI, resize.Lanczos3) thmBuf := new(bytes.Buffer) var thmString string if fltp == "gif" { if err = gif.Encode(thmBuf, thm, nil); err != nil { - return errors.New("Unable to encode image") + return nil, errors.New("Unable to encode image") } } else { if err = jpeg.Encode(thmBuf, thm, nil); err != nil { - return errors.New("Unable to encode image") + return nil, errors.New("Unable to encode image") } } thmString = base64.StdEncoding.EncodeToString(thmBuf.Bytes()) - return tm.saveScreenshot(&Screenshot{ - Image: base64.StdEncoding.EncodeToString(buf.Bytes()), - Thumbnail: thmString, - Filetype: fltp, - }) + if ss, err = NewScreenshot(tm.UUID, ""); err != nil { + return nil, err + } + + ss.Image = base64.StdEncoding.EncodeToString(buf.Bytes()) + ss.Thumbnail = thmString + ss.Filetype = fltp + + return ss, nil + //return m.jam.SaveScreenshot(ss) } diff --git a/admin_teams.go b/admin_teams.go index d2000f0..795a0f6 100644 --- a/admin_teams.go +++ b/admin_teams.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" _ "image/gif" @@ -10,10 +11,6 @@ import ( "github.com/gorilla/mux" ) -func refreshTeamsInMemory() { - site.Teams = db.getAllTeams() -} - func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) { vars := mux.Vars(req) page.SubTitle = "Teams" @@ -23,17 +20,11 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) switch vars["function"] { case "save": name := req.FormValue("teamname") - if db.getTeamByName(name) != nil { - // A team with that name already exists - page.session.setFlashMessage("A team with the name "+name+" already exists!", "error") - } else { - if err := db.newTeam(name); err != nil { - page.session.setFlashMessage(err.Error(), "error") - } else { - page.session.setFlashMessage("Team "+name+" created!", "success") - } + tm := NewTeam("") + tm.Name = name + if err := m.jam.AddTeam(tm); err != nil { + page.session.setFlashMessage("Error adding team: "+err.Error(), "error") } - refreshTeamsInMemory() redirect("/admin/teams", w, req) default: page.SubTitle = "Add New Team" @@ -41,57 +32,58 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) } } else if teamId != "" { // Functions for existing team - tm := db.getTeam(teamId) + tm, _ := m.jam.GetTeamById(teamId) if tm != nil { switch vars["function"] { case "save": - tm.UUID = teamId tm.Name = req.FormValue("teamname") - if err := tm.save(); err != nil { - page.session.setFlashMessage("Error updating team: "+err.Error(), "error") - } else { - page.session.setFlashMessage("Team Updated!", "success") - } - refreshTeamsInMemory() + page.session.setFlashMessage("Team Updated!", "success") redirect("/admin/teams", w, req) case "delete": var err error - if err = tm.delete(); err != nil { - page.session.setFlashMessage("Error deleting team: "+err.Error(), "error") + if err = m.jam.RemoveTeamById(teamId); err != nil { + page.session.setFlashMessage("Error removing team: "+err.Error(), "error") } else { - page.session.setFlashMessage("Team "+tm.Name+" Deleted", "success") + page.session.setFlashMessage("Team "+tm.Name+" Removed", "success") } - refreshTeamsInMemory() redirect("/admin/teams", w, req) case "savemember": mbrName := req.FormValue("newmembername") - mbr := newTeamMember(mbrName) - mbr.SlackId = req.FormValue("newmemberslackid") - mbr.Twitter = req.FormValue("newmembertwitter") - mbr.Email = req.FormValue("newmemberemail") - if err := tm.updateTeamMember(mbr); err != nil { + mbr, err := NewTeamMember(tm.UUID, "") + if err == nil { + mbr.Name = mbrName + mbr.SlackId = req.FormValue("newmemberslackid") + mbr.Twitter = req.FormValue("newmembertwitter") + mbr.Email = req.FormValue("newmemberemail") + } + if err := tm.AddTeamMember(mbr); err != nil { page.session.setFlashMessage("Error adding team member: "+err.Error(), "error") } else { page.session.setFlashMessage(mbrName+" added to team!", "success") } - refreshTeamsInMemory() redirect("/admin/teams/"+teamId+"#members", w, req) case "deletemember": - m := tm.getTeamMember(req.FormValue("memberid")) - if m != nil { - if err := tm.deleteTeamMember(m); err != nil { - page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error") - } else { - page.session.setFlashMessage(m.Name+" deleted from team", "success") - } - refreshTeamsInMemory() + var err error + var mbr *TeamMember + if mbr, err = tm.GetTeamMemberById(req.FormValue("memberid")); err != nil { + fmt.Println("Error removing team member: " + err.Error()) + page.session.setFlashMessage("Error deleting team member", "error") + redirect("/admin/teams/"+teamId+"#members", w, req) + } + if err = tm.RemoveTeamMemberById(mbr.UUID); err != nil { + fmt.Println("Error removing team member: " + err.Error()) + page.session.setFlashMessage("Error deleting team member", "error") } else { - page.session.setFlashMessage("Couldn't find team member to delete", "error") + page.session.setFlashMessage(mbr.Name+" deleted from team", "success") } redirect("/admin/teams/"+teamId+"#members", w, req) default: page.SubTitle = "Edit Team" - t := db.getTeam(teamId) + t, err := m.jam.GetTeamById(teamId) + if err != nil { + page.session.setFlashMessage("Error loading team: "+err.Error(), "error") + redirect("/admin/teams", w, req) + } page.TemplateData = t page.show("admin-editteam.html", w) } @@ -104,7 +96,7 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) type teamsPageData struct { Teams []Team } - page.TemplateData = teamsPageData{Teams: db.getAllTeams()} + page.TemplateData = m.jam page.SubTitle = "Teams" page.show("admin-teams.html", w) } diff --git a/admin_users.go b/admin_users.go index f4bdcea..1768f62 100644 --- a/admin_users.go +++ b/admin_users.go @@ -29,7 +29,7 @@ func handleAdminDoLogin(w http.ResponseWriter, req *http.Request) { // If it can't, it returns an error func doLogin(email, password string) error { if strings.TrimSpace(email) != "" && strings.TrimSpace(password) != "" { - return db.checkCredentials(email, password) + return m.checkCredentials(email, password) } return errors.New("Invalid Credentials") } @@ -53,12 +53,12 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) switch vars["function"] { case "save": email = req.FormValue("email") - if db.isValidUserEmail(email) { + if m.isValidUserEmail(email) { // User already exists page.session.setFlashMessage("A user with email address "+email+" already exists!", "error") } else { password := req.FormValue("password") - if err := db.updateUserPassword(email, string(password)); err != nil { + if err := m.updateUserPassword(email, string(password)); err != nil { page.session.setFlashMessage(err.Error(), "error") } else { page.session.setFlashMessage("User "+email+" created!", "success") @@ -73,10 +73,10 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) switch vars["function"] { case "save": var err error - if db.isValidUserEmail(email) { + if m.isValidUserEmail(email) { password := req.FormValue("password") if password != "" { - if err = db.updateUserPassword(email, password); err != nil { + if err = m.updateUserPassword(email, password); err != nil { page.session.setFlashMessage(err.Error(), "error") } else { page.session.setFlashMessage("User "+email+" created!", "success") @@ -86,8 +86,8 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) } case "delete": var err error - if db.isValidUserEmail(email) { - if err = db.deleteUser(email); err != nil { + if m.isValidUserEmail(email) { + if err = m.deleteUser(email); err != nil { page.session.setFlashMessage(err.Error(), "error") } else { page.session.setFlashMessage("User "+email+" deleted!", "success") @@ -96,7 +96,7 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) redirect("/admin/users", w, req) default: page.SubTitle = "Edit Admin User" - if !db.isValidUserEmail(email) { + if !m.isValidUserEmail(email) { page.session.setFlashMessage("Couldn't find the requested user, please try again.", "error") redirect("/admin/users", w, req) } @@ -107,7 +107,7 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) type usersPageData struct { Users []string } - page.TemplateData = usersPageData{Users: db.getAllUsers()} + page.TemplateData = usersPageData{Users: m.getAllUsers()} page.SubTitle = "Admin Users" page.show("admin-users.html", w) diff --git a/admin_votes.go b/admin_votes.go index 63ef05f..582ba50 100644 --- a/admin_votes.go +++ b/admin_votes.go @@ -23,22 +23,22 @@ func getCondorcetResult() []Ranking { } var allPairs []teamPair var ret []Ranking - for i := 0; i < len(site.Teams); i++ { - for j := i + 1; j < len(site.Teams); j++ { + for i := 0; i < len(m.jam.Teams); i++ { + for j := i + 1; j < len(m.jam.Teams); j++ { // For each pairing find a winner - winner, pct, _ := findWinnerBetweenTeams(&site.Teams[i], &site.Teams[j]) + winner, pct, _ := findWinnerBetweenTeams(&m.jam.Teams[i], &m.jam.Teams[j]) newPair := new(teamPair) if winner != nil { newPair.winner = winner - if winner.UUID == site.Teams[i].UUID { - newPair.loser = &site.Teams[j] + if winner.UUID == m.jam.Teams[i].UUID { + newPair.loser = &m.jam.Teams[j] } else { - newPair.loser = &site.Teams[i] + newPair.loser = &m.jam.Teams[i] } newPair.majority = pct } else { - newPair.winner = &site.Teams[i] - newPair.loser = &site.Teams[j] + newPair.winner = &m.jam.Teams[i] + newPair.loser = &m.jam.Teams[j] newPair.majority = 50 } allPairs = append(allPairs, *newPair) @@ -46,8 +46,8 @@ func getCondorcetResult() []Ranking { } // initialize map of team wins teamWins := make(map[string]int) - for i := range site.Teams { - teamWins[site.Teams[i].UUID] = 0 + for i := range m.jam.Teams { + teamWins[m.jam.Teams[i].UUID] = 0 } // Figure out how many wins each team has for i := range allPairs { @@ -72,7 +72,10 @@ func getCondorcetResult() []Ranking { nR := new(Ranking) nR.Rank = currRank for i := range rankedWins[topWins] { - nR.Teams = append(nR.Teams, *site.getTeamByUUID(rankedWins[topWins][i])) + tm, _ := m.jam.GetTeamById(rankedWins[topWins][i]) + if tm != nil { + nR.Teams = append(nR.Teams, *tm) + } } ret = append(ret, *nR) delete(rankedWins, topWins) @@ -99,7 +102,7 @@ func uuidIsInRankingSlice(uuid string, sl []Ranking) bool { func findWinnerBetweenTeams(tm1, tm2 *Team) (*Team, float32, error) { // tally gets incremented for a tm1 win, decremented for a tm2 win var tm1votes, tm2votes float32 - for _, v := range site.Votes { + for _, v := range m.jam.Votes { for _, chc := range v.Choices { if chc.Team == tm1.UUID { tm1votes++ @@ -138,12 +141,12 @@ func handleAdminVotes(w http.ResponseWriter, req *http.Request, page *pageData) Results []Ranking } vpd := new(votePageData) - for i := range site.Votes { + for i := range m.jam.Votes { v := new(vpdVote) - v.Timestamp = site.Votes[i].Timestamp.Format(time.RFC3339) - v.ClientId = site.Votes[i].ClientId - for _, choice := range site.Votes[i].Choices { - for _, fndTm := range site.Teams { + v.Timestamp = m.jam.Votes[i].Timestamp.Format(time.RFC3339) + v.ClientId = m.jam.Votes[i].ClientId + for _, choice := range m.jam.Votes[i].Choices { + for _, fndTm := range m.jam.Teams { if fndTm.UUID == choice.Team { v.Choices = append(v.Choices, fndTm) break diff --git a/assets.go b/assets.go index 4fb2c8c..703575c 100644 --- a/assets.go +++ b/assets.go @@ -12113,16 +12113,16 @@ XzHfrrS2uzLN6JsOkTvo3Iny7fxf2KYCxs3I30osvf4OAAD//yR+jxHtAwAA "/templates/admin-votes.html": { local: "templates/admin-votes.html", - size: 837, - modtime: 1506689947, + size: 848, + modtime: 1508506407, compressed: ` -H4sIAAAJbogA/3xSTW/cIBC9768YWXusbam9RRip2lxy6SFd9Y7NJIuKwYJZS1WU/94Bm5Av5cTM4w3z -5g1CmxUmq2IcmoDxaim2k3ekjMPQyIO4fJenawjoCO63e9Ezdnh6gqDcI8LRfIPjCjcDdGecF6sIbxWp -bmfD8/MBoLIp0SnzjytXqHmnZBJD98r9ZeQmp7R2v9SMnIsxQC+3p9DpVFMj0fMYLJbUaBGMHprVE8Y2 -502ZL/qwEZZrwPZ92I4+aAyoYeJh8/AAgi6otMzyBIUtyLA8mxkjqXkRPWevLk7WJLfubt9fpMmMe4wV -5yi/mZC9j6DR63+yGPK1xz+t/ZMGLQ6+kajlZuiL0ORiT/ojY1N8pz8h7CEn3takCpuSsqms83TxZqpy -9kprcp+p7rJn6PVj+xpfKvrarAoqZr2tYLQYJujBe/pkWbr8AIsPxN/B27goNzQ/miTMooPutyHsipdw -9qQs5PRjfz63PhykjyMP/wMAAP//x7l0qEUDAAA= +H4sIAAAJbogA/3xSPW/cMAzd/SsI48b6DLRbIAsoLkuWDsGhu2wxOaGyZEg8A0WQ/15KtqPcRzOJfHoU +Hx8ltJlhsCrGrg4Yz5ZiM3hHyjgMtazE6bs8nENAR/C83IuWsertDYJyrwg78w12Mzx0sD/iOFlF+KhI +7Vc2vL9XAIVNiU6Zv5u5Qo0rJZMYelbuDyMPOaV5/0uNyLnoA7RyeQqdTjUlEi2PwWJJ9RbB6K6ePWFs +cl5v80UfFsJ0Dthch03vg8aAGgYeNg8PIOiESsssT1BYggzLoxkxkhon0XL26eJgTXLr6fH6Ik1m3Gss +OEf5zYSsfQT1Xv+VmyFfe/zT2t9p0M3BC4laLoZ+CE0utqRvGYviJ32HsIaceFuSImxIyoZtnYeTN0OR +s1Zak/sMZZctQ58fW9f4UdGWZkXQZtZlBaObYYJevKc7y9LbD7D4QvwdvI2Tcl39o07CLLr/2wpHT8pC +Tm+l8Lm05CD9IVn9CwAA//8Mc47HUAMAAA== `, }, diff --git a/main.go b/main.go index 29dcd08..3cc889d 100644 --- a/main.go +++ b/main.go @@ -9,8 +9,11 @@ import ( "log" "net/http" "os" + "os/signal" "strconv" "strings" + "syscall" + "time" "golang.org/x/crypto/ssh/terminal" @@ -58,25 +61,37 @@ type menuItem struct { var sessionSecret = "JCOP5e8ohkTcOzcSMe74" var sessionStore = sessions.NewCookieStore([]byte(sessionSecret)) -var site *siteData var r *mux.Router +var m *model func main() { - db = new(gjDatabase) + var err error + if m, err = NewModel(); err != nil { + errorExit("Unable to initialize Model: " + err.Error()) + } + loadConfig() - site.save() + if err = m.site.SaveToDB(); err != nil { + errorExit("Unable to save site config to DB: " + err.Error()) + } + + // Save changes to the DB every 5 minutes + go func() { + for { + m.saveChanges() + time.Sleep(5 * time.Minute) + } + }() + initialize() r = mux.NewRouter() r.StrictSlash(true) - if site.DevMode { + if m.site.DevMode { fmt.Println("Operating in Development Mode") } - //s := http.StripPrefix("/assets/", http.FileServer(FS(site.DevMode))) - //http.Dir(site.ServerDir+"assets/"))) - //r.PathPrefix("/assets/").Handler(s) - r.PathPrefix("/assets/").Handler(http.FileServer(FS(site.DevMode))) + r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.site.DevMode))) // Public Subrouter pub := r.PathPrefix("/").Subrouter() @@ -105,13 +120,22 @@ func main() { chain := alice.New(loggingHandler).Then(r) - fmt.Printf("Listening on port %d\n", site.Port) - log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(site.Port), chain)) + // Set up a channel to intercept Ctrl+C for graceful shutdowns + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + // Save the changes when the app quits + fmt.Println("\nFinishing up...") + m.saveChanges() + os.Exit(0) + }() + + fmt.Printf("Listening on port %d\n", m.site.Port) + log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.site.Port), chain)) } func loadConfig() { - site = db.getSiteConfig() - if len(os.Args) > 1 { for _, v := range os.Args { key := v @@ -124,27 +148,27 @@ func loadConfig() { } switch key { case "-title": - site.Title = val - fmt.Print("Set site title: ", site.Title, "\n") + m.site.Title = val + fmt.Print("Set site title: ", m.site.Title, "\n") case "-port": var tryPort int var err error if tryPort, err = strconv.Atoi(val); err != nil { fmt.Print("Invalid port given: ", val, " (Must be an integer)\n") - tryPort = site.Port + tryPort = m.site.Port } // TODO: Make sure a valid port number is given - site.Port = tryPort + m.site.Port = tryPort case "-session-name": - site.SessionName = val + m.site.SessionName = val case "-server-dir": // TODO: Probably check if the given directory is valid - site.ServerDir = val + m.site.ServerDir = val case "-help", "-h", "-?": printHelp() done() case "-dev": - site.DevMode = true + m.site.DevMode = true case "-reset-defaults": resetToDefaults() done() @@ -154,10 +178,9 @@ func loadConfig() { } func initialize() { - // Check if the database has been created - assertError(db.initialize()) - - if !db.hasUser() { + // Test if we have an admin user first + if !m.hasUser() { + // Nope, create one reader := bufio.NewReader(os.Stdin) fmt.Println("Create new Admin user") fmt.Print("Email: ") @@ -175,29 +198,25 @@ func initialize() { fmt.Println("Entered Passwords don't match!") } } - assertError(db.updateUserPassword(email, string(pw1))) + assertError(m.updateUserPassword(email, string(pw1))) } - if !db.hasCurrentJam() { + + // Now test if the 'current jam' is named + if m.jam.Name == "" { reader := bufio.NewReader(os.Stdin) fmt.Println("Create New Game Jam") fmt.Print("GameJam Name: ") gjName, _ := reader.ReadString('\n') gjName = strings.TrimSpace(gjName) - if db.setCurrentJam(gjName) != nil { - fmt.Println("Error saving Current Jam") - } + m.jam.Name = gjName + assertError(m.jam.SaveToDB()) } - jmNm, err := db.getCurrentJam() - if err == nil { - fmt.Println("Current Jam Name: " + jmNm) + if m.jam.Name != "" { + fmt.Println("Current Jam Name: " + m.jam.Name) } else { - fmt.Println(err.Error()) + fmt.Println("No Jam Name Specified") } - - // Load all votes into memory - site.Votes = db.getAllVotes() - site.Teams = db.getAllTeams() } func loggingHandler(h http.Handler) http.Handler { @@ -205,14 +224,14 @@ func loggingHandler(h http.Handler) http.Handler { } func InitPageData(w http.ResponseWriter, req *http.Request) *pageData { - if site.DevMode { + if m.site.DevMode { w.Header().Set("Cache-Control", "no-cache") } p := new(pageData) // Get session var err error var s *sessions.Session - if s, err = sessionStore.Get(req, site.SessionName); err != nil { + if s, err = sessionStore.Get(req, m.site.SessionName); err != nil { http.Error(w, err.Error(), 500) return p } @@ -224,9 +243,9 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData { // First check if we're logged in userEmail, _ := p.session.getStringValue("email") // With a valid account - p.LoggedIn = db.isValidUserEmail(userEmail) + p.LoggedIn = m.isValidUserEmail(userEmail) - p.Site = site + p.Site = m.site p.SubTitle = "GameJam Voting" p.Stylesheets = make([]string, 0, 0) p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css") @@ -261,20 +280,19 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData { } p.HideAdminMenu = true - if p.CurrentJam, err = db.getCurrentJam(); err != nil { - p.FlashMessage = "Error Loading Current GameJam: " + err.Error() - p.FlashClass = "error" - } - p.ClientId = p.session.getClientId() - cl := db.getClient(p.ClientId) + var cl *Client + if cl, err = m.GetClient(p.ClientId); err != nil { + // A new client + cl = NewClient(p.ClientId) + } p.ClientIsAuth = cl.Auth p.ClientIsServer = clientIsServer(req) // Public Mode - p.PublicMode = db.getPublicSiteMode() + p.PublicMode = m.site.GetPublicMode() // Authentication Mode - p.AuthMode = db.getAuthMode() + p.AuthMode = m.site.GetAuthMode() return p } @@ -299,8 +317,8 @@ func (p *pageData) show(tmplName string, w http.ResponseWriter) error { // Spit out a template func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error { n := "/templates/" + tmplName - l := template.Must(template.New("layout").Parse(FSMustString(site.DevMode, n))) - t := template.Must(l.Parse(FSMustString(site.DevMode, n))) + l := template.Must(template.New("layout").Parse(FSMustString(m.site.DevMode, n))) + t := template.Must(l.Parse(FSMustString(m.site.DevMode, n))) return t.Execute(w, tmplData) } @@ -310,19 +328,19 @@ func redirect(url string, w http.ResponseWriter, req *http.Request) { } func resetToDefaults() { - def := NewSiteData() + def := NewSiteData(m) fmt.Println("Reset settings to defaults?") - fmt.Print(site.Title, " -> ", def.Title, "\n") - fmt.Print(site.Port, " -> ", def.Port, "\n") - fmt.Print(site.SessionName, " -> ", def.SessionName, "\n") - fmt.Print(site.ServerDir, " -> ", def.ServerDir, "\n") + fmt.Print(m.site.Title, " -> ", def.Title, "\n") + fmt.Print(m.site.Port, " -> ", def.Port, "\n") + fmt.Print(m.site.SessionName, " -> ", def.SessionName, "\n") + fmt.Print(m.site.ServerDir, " -> ", def.ServerDir, "\n") fmt.Println("Are you sure? (y/N): ") reader := bufio.NewReader(os.Stdin) conf, _ := reader.ReadString('\n') conf = strings.ToUpper(strings.TrimSpace(conf)) if strings.HasPrefix(conf, "Y") { - if def.save() != nil { - errorExit("Error resetting to defaults") + if err := def.SaveToDB(); err != nil { + errorExit("Error resetting to defaults: " + err.Error()) } fmt.Println("Reset to defaults") } diff --git a/model.go b/model.go index 488f262..88606d8 100644 --- a/model.go +++ b/model.go @@ -2,45 +2,69 @@ package main import ( "errors" - "strings" + "fmt" "github.com/br0xen/boltease" ) -// TODO: I don't think we need a global for this... -var db *currJamDb +// model stores the current jam in memory, and has the ability to access archived dbs +type model struct { + bolt *boltease.DB + dbOpened int + dbFileName string -// gjdb is the interface that works for the current jam database as well as all archive databases -type gjdb interface { - getDB() *boltease.DB - open() error - close() error + site *siteData // Configuration data for the site + jam *Gamejam // The currently active gamejam + clients []Client // Web clients that have connected to the server - getJamName() string - setJamName(nm string) + clientsUpdated bool } +// Update Flags: Which parts of the model need to be updated const ( - AuthModeAuthentication = iota - AuthModeAll - AuthModeError + UpdateSiteData = iota + UpdateJamData ) -// currJamDb also contains site configuration information -type currJamDb struct { - bolt *boltease.DB - dbOpened int +func NewModel() (*model, error) { + var err error + m := new(model) + + m.dbFileName = DbName + if err = m.openDB(); err != nil { + return nil, errors.New("Unable to open DB: " + err.Error()) + } + defer m.closeDB() + + // Initialize the DB + if err = m.initDB(); err != nil { + return nil, errors.New("Unable to initialize DB: " + err.Error()) + } + + // Load the site data + m.site = NewSiteData(m) + if err = m.site.LoadFromDB(); err != nil { + // Error loading from the DB, set to defaults + def := NewSiteData(m) + m.site = def + } + + // Load the jam data + if m.jam, err = m.LoadCurrentJam(); err != nil { + return nil, errors.New("Unable to load current jam: " + err.Error()) + } + + // Load web clients + m.clients = m.LoadAllClients() + + return m, nil } -func (db *currJamDb) getDB() *boltease.DB { - return db.bolt -} - -func (db *currJamDb) open() error { - db.dbOpened += 1 - if db.dbOpened == 1 { +func (m *model) openDB() error { + m.dbOpened += 1 + if m.dbOpened == 1 { var err error - db.bolt, err = boltease.Create(DbName, 0600, nil) + m.bolt, err = boltease.Create(m.dbFileName, 0600, nil) if err != nil { return err } @@ -48,112 +72,68 @@ func (db *currJamDb) open() error { return nil } -func (db *currJamDb) close() error { - db.dbOpened -= 1 - if db.dbOpened == 0 { - return db.bolt.CloseDB() +func (m *model) closeDB() error { + m.dbOpened -= 1 + if m.dbOpened == 0 { + return m.bolt.CloseDB() } return nil } -// initialize the 'current jam' database -func (db *currJamDb) initialize() error { +func (m *model) initDB() error { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return err } - defer db.close() + defer m.closeDB() // Create the path to the bucket to store admin users - if err := db.bolt.MkBucketPath([]string{"users"}); err != nil { + if err = m.bolt.MkBucketPath([]string{"users"}); err != nil { return err } - // Create the path to the bucket to store jam informations - if err := db.bolt.MkBucketPath([]string{"jam"}); err != nil { + // Create the path to the bucket to store the web clients + if err = m.bolt.MkBucketPath([]string{"clients"}); err != nil { + return err + } + // Create the path to the bucket to store the current jam & teams + if err = m.bolt.MkBucketPath([]string{"jam", "teams"}); err != nil { + return err + } + // Create the path to the bucket to store the list of archived jams + if err = m.bolt.MkBucketPath([]string{"archive"}); err != nil { return err } // Create the path to the bucket to store site config data - return db.bolt.MkBucketPath([]string{"site"}) + return m.bolt.MkBucketPath([]string{"site"}) } -func (db *currJamDb) getSiteConfig() *siteData { - var ret *siteData - def := NewSiteData() +// saveChanges saves any parts of the model that have been flagged as changed to the database +func (m *model) saveChanges() error { var err error - if err = db.open(); err != nil { - return def - } - defer db.close() - - ret = new(siteData) - siteConf := []string{"site"} - if ret.Title, err = db.bolt.GetValue(siteConf, "title"); err != nil { - ret.Title = def.Title - } - if ret.Port, err = db.bolt.GetInt(siteConf, "port"); err != nil { - ret.Port = def.Port - } - if ret.SessionName, err = db.bolt.GetValue(siteConf, "session-name"); err != nil { - ret.SessionName = def.SessionName - } - if ret.ServerDir, err = db.bolt.GetValue(siteConf, "server-dir"); err != nil { - ret.ServerDir = def.ServerDir - } - return ret -} - -func (db *currJamDb) setJamName(name string) error { - var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return err } - defer db.close() + defer m.closeDB() - return db.bolt.SetValue([]string{"site"}, "current-jam", name) -} - -func (db *currJamDb) getJamName() string { - var ret string - var err error - if err = db.open(); err != nil { - return "", err + //if m.site.NeedsSave() { + fmt.Println("Saving Site data to DB") + if err = m.site.SaveToDB(); err != nil { + return err } - defer db.close() - - ret, err = db.bolt.GetValue([]string{"site"}, "current-jam") - - if err == nil && strings.TrimSpace(ret) == "" { - return ret, errors.New("No Jam Name Specified") + //} + //if m.jam.IsChanged { + fmt.Println("Saving Jam data to DB") + if err = m.jam.SaveToDB(); err != nil { + return err } - return ret, err -} - -func (db *currJamDb) getAuthMode() int { - if ret, err := db.bolt.GetInt([]string{"site"}, "auth-mode"); err != nil { - return AuthModeAuthentication - } else { - return ret + m.jam.IsChanged = false + //} + //if m.clientsUpdated { + fmt.Println("Saving Client data to DB") + if err = m.SaveAllClients(); err != nil { + return err } -} - -func (db *currJamDb) setAuthMode(mode int) error { - if mode < 0 || mode >= AuthModeError { - return errors.New("Invalid site mode") - } - return db.bolt.SetInt([]string{"site"}, "auth-mode", mode) -} - -func (db *currJamDb) getPublicSiteMode() int { - if ret, err := db.bolt.GetInt([]string{"site"}, "public-mode"); err != nil { - return SiteModeWaiting - } else { - return ret - } -} - -func (db *currJamDb) setPublicSiteMode(mode int) error { - if mode < 0 || mode >= SiteModeError { - return errors.New("Invalid site mode") - } - return db.bolt.SetInt([]string{"site"}, "public-mode", mode) + m.clientsUpdated = false + //} + return nil } diff --git a/model_clients.go b/model_clients.go index 4a0790a..9191f0e 100644 --- a/model_clients.go +++ b/model_clients.go @@ -1,154 +1,164 @@ package main import ( - "fmt" - "strconv" - "strings" - "time" + "errors" + + "github.com/pborman/uuid" ) +/** + * Client + * A client is a system that is connecting to the web server + */ type Client struct { UUID string Auth bool Name string IP string + + mPath []string // The path in the DB to this client } -func (db *currJamDb) getAllClients() []Client { - var ret []Client +func NewClient(id string) *Client { + if id == "" { + id = uuid.New() + } + return &Client{ + UUID: id, + mPath: []string{"clients", id}, + } +} + +func (m *model) AddClient(cl *Client) error { + for i := range m.clients { + if m.clients[i].UUID == cl.UUID { + return errors.New("A client with that ID already exists") + } + if m.clients[i].IP == cl.IP { + return errors.New("A client with that IP already exists") + } + if m.clients[i].Name == cl.Name { + return errors.New("A client with that Name already exists") + } + } + m.clients = append(m.clients, *cl) + m.clientsUpdated = true + return nil +} + +/** + * DB Functions + * These are generally just called when the app starts up, or when the periodic 'save' runs + */ + +// Load all clients from the DB +func (m *model) LoadAllClients() []Client { var err error - if err = db.open(); err != nil { + var ret []Client + if err = m.openDB(); err != nil { return ret } - defer db.close() + defer m.closeDB() var clientUids []string - if clientUids, err = db.bolt.GetBucketList([]string{"clients"}); err != nil { + cliPath := []string{"clients"} + if clientUids, err = m.bolt.GetBucketList(cliPath); err != nil { return ret } for _, v := range clientUids { - if cl := db.getClient(v); cl != nil { + if cl := m.LoadClient(v); cl != nil { ret = append(ret, *cl) } } return ret } -func (db *currJamDb) getClient(id string) *Client { +// Load a client from the DB and return it +func (m *model) LoadClient(clId string) *Client { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return nil } - defer db.close() + defer m.closeDB() - cl := new(Client) - cl.UUID = id - cl.Auth, _ = db.bolt.GetBool([]string{"clients", id}, "auth") - cl.Name, _ = db.bolt.GetValue([]string{"clients", id}, "name") - cl.IP, _ = db.bolt.GetValue([]string{"clients", id}, "ip") + cl := NewClient(clId) + cl.Auth, _ = m.bolt.GetBool(cl.mPath, "auth") + cl.Name, _ = m.bolt.GetValue(cl.mPath, "name") + cl.IP, _ = m.bolt.GetValue(cl.mPath, "ip") return cl } -func (db *currJamDb) getClientByIp(ip string) *Client { +// SaveAllClients saves all clients to the DB +func (m *model) SaveAllClients() error { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return nil } - defer db.close() + defer m.closeDB() - allClients := db.getAllClients() - for i := range allClients { - if allClients[i].IP == ip { - return &allClients[i] + for _, v := range m.clients { + if err = m.SaveClient(&v); err != nil { + return err } } return nil } -func (c *Client) save() error { +// SaveClient saves a client to the DB +func (m *model) SaveClient(cl *Client) error { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return nil } - defer db.close() + defer m.closeDB() - if err = db.bolt.SetBool([]string{"clients", c.UUID}, "auth", c.Auth); err != nil { + if err = m.bolt.SetBool(cl.mPath, "auth", cl.Auth); err != nil { return err } - if err = db.bolt.SetValue([]string{"clients", c.UUID}, "name", c.Name); err != nil { + if err = m.bolt.SetValue(cl.mPath, "name", cl.Name); err != nil { return err } - return db.bolt.SetValue([]string{"clients", c.UUID}, "ip", c.IP) + return m.bolt.SetValue(cl.mPath, "ip", cl.IP) } -func (c *Client) getVotes() []Vote { - var ret []Vote - var err error - if err = db.open(); err != nil { - return ret - } - defer db.close() +/** + * In Memory functions + * This is generally how the app accesses client data + */ - var times []string - votesBkt := []string{"votes", c.UUID} - if times, err = db.bolt.GetBucketList(votesBkt); err != nil { - return ret - } - for _, t := range times { - var tm time.Time - if tm, err = time.Parse(time.RFC3339, t); err == nil { - var vt *Vote - if vt, err = c.getVote(tm); err == nil { - ret = append(ret, *vt) - } else { - fmt.Println(err) - } +// Return a client by it's UUID +func (m *model) GetClient(id string) (*Client, error) { + for i := range m.clients { + if m.clients[i].UUID == id { + return &m.clients[i], nil } } - return ret + return nil, errors.New("Invalid Id") } -func (c *Client) getVote(timestamp time.Time) (*Vote, error) { - var err error - if err = db.open(); err != nil { - return nil, err - } - defer db.close() - - vt := new(Vote) - vt.Timestamp = timestamp - vt.ClientId = c.UUID - votesBkt := []string{"votes", c.UUID, timestamp.Format(time.RFC3339)} - var choices []string - if choices, err = db.bolt.GetKeyList(votesBkt); err != nil { - // Couldn't find the vote... - return nil, err - } - for _, v := range choices { - ch := new(GameChoice) - var rank int - - if rank, err = strconv.Atoi(v); err == nil { - ch.Rank = rank - ch.Team, err = db.bolt.GetValue(votesBkt, v) - vt.Choices = append(vt.Choices, *ch) +// Return a client by it's IP address +func (m *model) GetClientByIp(ip string) (*Client, error) { + for i := range m.clients { + if m.clients[i].IP == ip { + return &m.clients[i], nil } } - return vt, nil + return nil, errors.New("Invalid Ip") } -func (c *Client) saveVote(timestamp time.Time, votes []string) error { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - // Make sure we don't clobber a duplicate vote - votesBkt := []string{"votes", c.UUID, timestamp.Format(time.RFC3339)} - for i := range votes { - if strings.TrimSpace(votes[i]) != "" { - db.bolt.SetValue(votesBkt, strconv.Itoa(i), votes[i]) +// Add/Update a client in the data model +func (m *model) UpdateClient(cl *Client) { + var found bool + for i := range m.clients { + if m.clients[i].UUID == cl.UUID { + found = true + m.clients[i].Auth = cl.Auth + m.clients[i].Name = cl.Name + m.clients[i].IP = cl.IP } } - return err + if !found { + m.clients = append(m.clients, *cl) + } + m.clientsUpdated = true } diff --git a/model_gamejam.go b/model_gamejam.go index 1c343af..a8a028f 100644 --- a/model_gamejam.go +++ b/model_gamejam.go @@ -1,12 +1,15 @@ package main import ( + "errors" + "strings" "time" - - "github.com/br0xen/boltease" ) -// Gamejam is specifically for an archived game jam +/** + * Gamejam + * Gamejam is the struct for any gamejam (current or archived) + */ type Gamejam struct { UUID string Name string @@ -14,74 +17,73 @@ type Gamejam struct { Teams []Team Votes []Vote - db *boltease.DB - dbOpened int + m *model // The model that holds this gamejam's data + mPath []string // The path in the db to this gamejam + + IsChanged bool // Flag to tell if we need to update the db } -// Archived Gamejam data is stored in it's own file to keep things nice and organized -func (gj *Gamejam) openDB() error { - gj.dbOpened += 1 - if gj.dbOpened == 1 { - var err error - gj.db, err = boltease.Create(gj.UUID+".db", 0600, nil) - if err != nil { - return err - } +func NewGamejam(m *model) *Gamejam { + gj := new(Gamejam) + gj.m = m + gj.mPath = []string{"jam"} + return gj +} + +/** + * DB Functions + * These are generally just called when the app starts up, or when the periodic 'save' runs + */ + +func (m *model) LoadCurrentJam() (*Gamejam, error) { + if err := m.openDB(); err != nil { + return nil, err } - return nil + defer m.closeDB() + + gj := NewGamejam(m) + gj.Name, _ = m.bolt.GetValue(gj.mPath, "name") + + // Load all teams + gj.Teams = gj.LoadAllTeams() + + // Load all votes + gj.Votes = gj.LoadAllVotes() + + return gj, nil } -func (gj *Gamejam) closeDB() error { - gj.dbOpened -= 1 - if gj.dbOpened == 0 { - return gj.db.CloseDB() - } - return nil -} - -// archiveGameJam creates a separate gamejam file and populates it with the -// given name, teams, and votes -func archiveGamejam(nm string, teams []Team, votes []Vote) error { - // TODO - return nil -} - -// dbGetGamejam returns a gamejam with the given uuid -// or nil if it couldn't be found -func dbGetGamejam(id string) *Gamejam { - var err error - if err = openDatabase(); err != nil { - return nil - } - defer closeDatabase() - - ret := Gamejam{UUID: id} - // TODO: Load gamejam teams, other details - return ret -} - -// dbGetGamejamByName looks for a gamejam with the given name -// and returns it, or it returns nil if it couldn't find it -func dbGetGamejamByName(nm string) *Gamejam { - var err error - if err = openDatabase(); err != nil { +// Save everything to the DB whether it's flagged as changed or not +func (gj *Gamejam) SaveToDB() error { + if err := gj.m.openDB(); err != nil { return err } - defer closeDatabase() + defer gj.m.closeDB() - var gjid string - if gjs, err = db.GetBucketList([]string{"gamejams"}); err == nil { - for _, v := range gjUids { - tstNm, _ := db.GetValue([]string{"gamejams", v}, "name") - if tstNm == nm { - // We've got it - gjid = v - break - } + var errs []error + if err := gj.m.bolt.SetValue(gj.mPath, "name", gj.Name); err != nil { + errs = append(errs, err) + } + // Save all Teams + for _, tm := range gj.Teams { + if err := gj.SaveTeam(&tm); err != nil { + errs = append(errs, err) } } - if gjid == "" { - return nil + + // Save all Votes + for _, vt := range gj.Votes { + if err := gj.SaveVote(&vt); err != nil { + errs = append(errs, err) + } } - return dbGetGamejam(gjid) + if len(errs) > 0 { + var errTxt string + for i := range errs { + errTxt = errTxt + errs[i].Error() + "\n" + } + errTxt = strings.TrimSpace(errTxt) + return errors.New("Error(s) saving to DB: " + errTxt) + } + return nil } diff --git a/model_games.go b/model_games.go index d0fddbf..888b5de 100644 --- a/model_games.go +++ b/model_games.go @@ -1,13 +1,59 @@ package main -import "errors" +import ( + "errors" + "fmt" + "github.com/pborman/uuid" +) + +/** + * Game + * A team's game, including links, description, and screenshots + */ type Game struct { Name string TeamId string Link string Description string Screenshots []Screenshot + + mPath []string // The path in the DB to this game +} + +// Create a new game object +func NewGame(tmId string) (*Game, error) { + if tmId == "" { + return nil, errors.New("Team ID is required") + } + return &Game{ + TeamId: tmId, + mPath: []string{"jam", "teams", tmId, "game"}, + }, nil +} + +func (gm *Game) GetScreenshot(ssId string) (*Screenshot, error) { + for _, ss := range gm.Screenshots { + if ss.UUID == ssId { + return &ss, nil + } + } + return nil, errors.New("Invalid Id") +} + +func (gm *Game) RemoveScreenshot(ssId string) error { + idx := -1 + for i, ss := range gm.Screenshots { + if ss.UUID == ssId { + idx = i + } + } + if idx < 0 { + return errors.New("Invalid Id") + } + fmt.Print("Removing Screenshot (", ssId, ") (IDX:", idx, ")\n") + gm.Screenshots = append(gm.Screenshots[:idx], gm.Screenshots[idx+1:]...) + return nil } type Screenshot struct { @@ -16,63 +62,231 @@ type Screenshot struct { Image string Thumbnail string Filetype string + + mPath []string // The path in the DB to this screenshot } -// Create a new game object, must have a valid team id -func newGame(tmId string) *Game { +// Create a Screenshot Object +func NewScreenshot(tmId, ssId string) (*Screenshot, error) { + if tmId == "" { + return nil, errors.New("Team ID is required") + } + if ssId == "" { + // Generate a new UUID + ssId = uuid.New() + } + return &Screenshot{ + UUID: ssId, + mPath: []string{"jam", "teams", tmId, "game", "screenshots", ssId}, + }, nil +} + +/** + * DB Functions + * These are generally just called when the app starts up, or when the periodic 'save' runs + */ + +// Load a team's game from the DB and return it +func (gj *Gamejam) LoadTeamGame(tmId string) (*Game, error) { var err error - if err = db.open(); err != nil { + if err = gj.m.openDB(); err != nil { + return nil, err + } + defer gj.m.closeDB() + + gm, err := NewGame(tmId) + if err != nil { + return nil, err + } + if gm.Name, err = gj.m.bolt.GetValue(gm.mPath, "name"); err != nil { + gm.Name = "" + } + if gm.Description, err = gj.m.bolt.GetValue(gm.mPath, "description"); err != nil { + gm.Description = "" + } + if gm.Link, err = gj.m.bolt.GetValue(gm.mPath, "link"); err != nil { + gm.Link = "" + } + + // Now get the game screenshots + gm.Screenshots = gj.LoadTeamGameScreenshots(tmId) + + return gm, nil +} + +// Load a games screenshots from the DB +func (gj *Gamejam) LoadTeamGameScreenshots(tmId string) []Screenshot { + var err error + if err = gj.m.openDB(); err != nil { return nil } - defer db.close() + defer gj.m.closeDB() - tm := db.getTeam(tmId) - if tm == nil { - return nil + var ret []Screenshot + gm, err := NewGame(tmId) + if err != nil { + return ret } - return &Game{TeamId: tmId} -} - -func (gm *Game) save() error { - var err error - if err = db.open(); err != nil { - return err - } - defer db.close() - - tm := db.getTeam(gm.TeamId) - if tm == nil { - return errors.New("Invalid Team: " + gm.TeamId) - } - gamePath := []string{"teams", gm.TeamId, "game"} - if err := db.bolt.MkBucketPath(gamePath); err != nil { - return err - } - - if gm.Name == "" { - gm.Name = tm.Name + "'s Game" - } - if err := db.bolt.SetValue(gamePath, "name", gm.Name); err != nil { - return err - } - if err := db.bolt.SetValue(gamePath, "link", gm.Link); err != nil { - return err - } - if err := db.bolt.SetValue(gamePath, "description", gm.Description); err != nil { - return err - } - if err := db.bolt.MkBucketPath(append(gamePath, "screenshots")); err != nil { - return err - } - - return err -} - -func (db *gjDatabase) getAllGames() []Game { - var ret []Game - tms := db.getAllTeams() - for i := range tms { - ret = append(ret, *tms[i].getGame()) + ssBktPath := append(gm.mPath, "screenshots") + var ssIds []string + ssIds, _ = gj.m.bolt.GetBucketList(ssBktPath) + for _, v := range ssIds { + ssLd, _ := gj.LoadTeamGameScreenshot(tmId, v) + if ssLd != nil { + ret = append(ret, *ssLd) + } } return ret } + +// Load a screenshot from the DB +func (gj *Gamejam) LoadTeamGameScreenshot(tmId, ssId string) (*Screenshot, error) { + var err error + if err = gj.m.openDB(); err != nil { + return nil, err + } + defer gj.m.closeDB() + + ret, err := NewScreenshot(tmId, ssId) + if err != nil { + return nil, err + } + if ret.Description, err = gj.m.bolt.GetValue(ret.mPath, "description"); err != nil { + return nil, err + } + if ret.Image, err = gj.m.bolt.GetValue(ret.mPath, "image"); err != nil { + return nil, err + } + if ret.Thumbnail, err = gj.m.bolt.GetValue(ret.mPath, "thumbnail"); err != nil { + return nil, err + } + if ret.Thumbnail == "" { + ret.Thumbnail = ret.Image + } + if ret.Filetype, err = gj.m.bolt.GetValue(ret.mPath, "filetype"); err != nil { + return nil, err + } + return ret, err +} + +// Save a game to the DB +func (gj *Gamejam) SaveGame(gm *Game) error { + var err error + if err = gj.m.openDB(); err != nil { + return err + } + defer gj.m.closeDB() + + if err := gj.m.bolt.MkBucketPath(gm.mPath); err != nil { + return err + } + + var tm *Team + if tm, err = gj.GetTeamById(gm.TeamId); err != nil { + return err + } + if gm.Name == "" { + gm.Name = tm.Name + "'s Game" + } + if err := gj.m.bolt.SetValue(gm.mPath, "name", gm.Name); err != nil { + return err + } + if err := gj.m.bolt.SetValue(gm.mPath, "link", gm.Link); err != nil { + return err + } + if err := gj.m.bolt.SetValue(gm.mPath, "description", gm.Description); err != nil { + return err + } + if err := gj.m.bolt.MkBucketPath(append(gm.mPath, "screenshots")); err != nil { + return err + } + return gj.SaveScreenshots(gm) +} + +// Save all of the game's screenshots to the DB +// Remove screenshots from the DB that aren't in the game object +func (gj *Gamejam) SaveScreenshots(gm *Game) error { + var err error + if err = gj.m.openDB(); err != nil { + return err + } + defer gj.m.closeDB() + + for _, ss := range gm.Screenshots { + if err = gj.SaveScreenshot(&ss); err != nil { + return err + } + } + // Now remove unused screenshots + ssPath := append(gm.mPath, "screenshots") + var ssIds []string + if ssIds, err = gj.m.bolt.GetBucketList(ssPath); err != nil { + return err + } + for i := range ssIds { + ss, _ := gm.GetScreenshot(ssIds[i]) + if ss != nil { + // A valid screenshot, next + continue + } + if ss, err = NewScreenshot(gm.TeamId, ssIds[i]); err != nil { + // Error building screenshot to delete... + continue + } + if err = gj.DeleteScreenshot(ss); err != nil { + return err + } + } + return nil +} + +// Save a screenshot +func (gj *Gamejam) SaveScreenshot(ss *Screenshot) error { + var err error + if err = gj.m.openDB(); err != nil { + return err + } + defer gj.m.closeDB() + + if err = gj.m.bolt.MkBucketPath(ss.mPath); err != nil { + return err + } + if err = gj.m.bolt.SetValue(ss.mPath, "description", ss.Description); err != nil { + return err + } + if err = gj.m.bolt.SetValue(ss.mPath, "image", ss.Image); err != nil { + return err + } + if err = gj.m.bolt.SetValue(ss.mPath, "filetype", ss.Filetype); err != nil { + return err + } + return nil +} + +// Delete a screenshot +func (gj *Gamejam) DeleteScreenshot(ss *Screenshot) error { + var err error + if err = gj.m.openDB(); err != nil { + return nil + } + defer gj.m.closeDB() + + ssPath := ss.mPath[:len(ss.mPath)-1] + return gj.m.bolt.DeleteBucket(ssPath, ss.UUID) +} + +/** + * In Memory functions + * This is generally how the app accesses client data + */ + +// Set the given team's game to gm +func (gj *Gamejam) UpdateGame(tmId string, gm *Game) error { + tm, err := gj.GetTeamById(tmId) + if err != nil { + return errors.New("Error getting team: " + err.Error()) + } + tm.Game = gm + gj.IsChanged = true + return nil +} diff --git a/model_site.go b/model_site.go deleted file mode 100644 index 2a0125f..0000000 --- a/model_site.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -const ( - SiteModeWaiting = iota - SiteModeVoting - SiteModeError -) - -// SiteData is stuff that stays the same -type siteData struct { - Title string - Port int - SessionName string - ServerDir string - DevMode bool - Mode int - - CurrentJam string - - Teams []Team - Votes []Vote -} - -// NewSiteData returns a siteData object with the default values -func NewSiteData() *siteData { - ret := new(siteData) - ret.Title = "ICT GameJam" - ret.Port = 8080 - ret.SessionName = "ict-gamejam" - ret.ServerDir = "./" - return ret -} - -func (s *siteData) getTeamByUUID(uuid string) *Team { - for i := range s.Teams { - if s.Teams[i].UUID == uuid { - return &s.Teams[i] - } - } - return nil -} - -// save 's' to the database -func (s *siteData) save() error { - var err error - if err = db.open(); err != nil { - return err - } - defer db.close() - - siteConf := []string{"site"} - if err = db.bolt.SetValue(siteConf, "title", s.Title); err != nil { - return err - } - if err = db.bolt.SetInt(siteConf, "port", s.Port); err != nil { - return err - } - if err = db.bolt.SetValue(siteConf, "session-name", s.SessionName); err != nil { - return err - } - return db.bolt.SetValue(siteConf, "server-dir", s.ServerDir) -} diff --git a/model_sitedata.go b/model_sitedata.go new file mode 100644 index 0000000..4d7467c --- /dev/null +++ b/model_sitedata.go @@ -0,0 +1,140 @@ +package main + +import ( + "errors" + "strconv" + "strings" +) + +/** + * SiteData + * Contains configuration for the website + */ +type siteData struct { + Title string + Port int + SessionName string + ServerDir string + authMode int + publicMode int + + DevMode bool + Mode int + + m *model + mPath []string // The path in the db to this site data + changed bool +} + +// NewSiteData returns a siteData object with the default values +func NewSiteData(m *model) *siteData { + ret := new(siteData) + ret.Title = "ICT GameJam" + ret.Port = 8080 + ret.SessionName = "ict-gamejam" + ret.ServerDir = "./" + ret.mPath = []string{"site"} + ret.m = m + return ret +} + +// Authentication Modes: Flags for which clients are able to vote +const ( + AuthModeAuthentication = iota + AuthModeAll + AuthModeError +) + +// Mode flags for how the site is currently running +const ( + SiteModeWaiting = iota + SiteModeVoting + SiteModeError +) + +// load the site data out of the database +// If fields don't exist in the DB, don't clobber what is already in s +func (s *siteData) LoadFromDB() error { + if err := s.m.openDB(); err != nil { + return err + } + defer s.m.closeDB() + + if title, _ := s.m.bolt.GetValue(s.mPath, "title"); strings.TrimSpace(title) != "" { + s.Title = title + } + if port, err := s.m.bolt.GetInt(s.mPath, "port"); err == nil { + s.Port = port + } + if sessionName, _ := s.m.bolt.GetValue(s.mPath, "session-name"); strings.TrimSpace(sessionName) != "" { + s.SessionName = sessionName + } + if serverDir, _ := s.m.bolt.GetValue(s.mPath, "server-dir"); strings.TrimSpace(serverDir) != "" { + s.ServerDir = serverDir + } + s.changed = false + return nil +} + +// Return if the site data in memory has changed +func (s *siteData) NeedsSave() bool { + return s.changed +} + +// Save the site data into the DB +func (s *siteData) SaveToDB() error { + var err error + if err = s.m.openDB(); err != nil { + return err + } + defer s.m.closeDB() + + if err = s.m.bolt.SetValue(s.mPath, "title", s.Title); err != nil { + return err + } + if err = s.m.bolt.SetInt(s.mPath, "port", s.Port); err != nil { + return err + } + if err = s.m.bolt.SetValue(s.mPath, "session-name", s.SessionName); err != nil { + return err + } + if err = s.m.bolt.SetValue(s.mPath, "server-dir", s.ServerDir); err != nil { + return err + } + s.changed = false + return nil +} + +// Return the Auth Mode +func (s *siteData) GetAuthMode() int { + return s.authMode +} + +// Set the auth mode +func (s *siteData) SetAuthMode(mode int) error { + if mode < AuthModeAuthentication || mode >= AuthModeError { + return errors.New("Invalid Authentication Mode: " + strconv.Itoa(mode)) + } + if mode != s.authMode { + s.authMode = mode + s.changed = true + } + return nil +} + +// Return the public site mode +func (s *siteData) GetPublicMode() int { + return s.publicMode +} + +// Set the public site mode +func (s *siteData) SetPublicMode(mode int) error { + if mode < SiteModeWaiting || mode >= SiteModeError { + return errors.New("Invalid Public Mode: " + strconv.Itoa(mode)) + } + if mode != s.publicMode { + s.publicMode = mode + s.changed = true + } + return nil +} diff --git a/model_teams.go b/model_teams.go index 5f95935..97851c7 100644 --- a/model_teams.go +++ b/model_teams.go @@ -2,264 +2,44 @@ package main import ( "errors" + "fmt" "github.com/pborman/uuid" ) +/** + * Team + */ type Team struct { UUID string Name string Members []TeamMember Game *Game + + mPath []string // The path in the DB to this team } -// newTeam creates a team with name nm and stores it in the DB -func (db *gjDatabase) newTeam(nm string) error { - var err error - if err = db.open(); err != nil { - return err +// Create a team +func NewTeam(id string) *Team { + if id == "" { + id = uuid.New() } - defer db.close() - - // Generate a UUID - uuid := uuid.New() - teamPath := []string{"teams", uuid} - - if err := db.bolt.MkBucketPath(teamPath); err != nil { - return err + // Create an emtpy game for the team + gm, _ := NewGame(id) + return &Team{ + UUID: id, + Game: gm, + mPath: []string{"jam", "teams", id}, } - if err := db.bolt.SetValue(teamPath, "name", nm); err != nil { - return err - } - if err := db.bolt.MkBucketPath(append(teamPath, "members")); err != nil { - return err - } - gamePath := append(teamPath, "game") - if err := db.bolt.MkBucketPath(gamePath); err != nil { - return err - } - if err := db.bolt.SetValue(append(gamePath), "name", ""); err != nil { - return err - } - return db.bolt.MkBucketPath(append(gamePath, "screenshots")) } -// getTeam returns a team with the given id, or nil -func (db *gjDatabase) getTeam(id string) *Team { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - teamPath := []string{"teams", id} - tm := new(Team) - tm.UUID = id - if tm.Name, err = db.bolt.GetValue(teamPath, "name"); err != nil { - return nil - } - tm.Members = tm.getTeamMembers() - tm.Game = tm.getGame() - return tm -} - -// This function returns the team for a specific member -func (db *gjDatabase) getTeamForMember(mbrid string) (*Team, error) { - var err error - if err = db.open(); err != nil { - return nil, err - } - defer db.close() - - teams := db.getAllTeams() - for i := range teams { - var tmMbrs []TeamMember - tmMbrs = teams[i].getTeamMembers() - if err == nil { - for j := range tmMbrs { - if tmMbrs[j].UUID == mbrid { - return &teams[i], nil - } - } +func (gj *Gamejam) GetTeamById(id string) (*Team, error) { + for i := range gj.Teams { + if gj.Teams[i].UUID == id { + return &gj.Teams[i], nil } } - return nil, errors.New("Unable to find team member") -} - -// getAllTeams returns all teams in the database -func (db *gjDatabase) getAllTeams() []Team { - var ret []Team - var err error - if err = db.open(); err != nil { - return ret - } - defer db.close() - - teamPath := []string{"teams"} - var teamUids []string - if teamUids, err = db.bolt.GetBucketList(teamPath); err != nil { - return ret - } - for _, v := range teamUids { - if tm := db.getTeam(v); tm != nil { - ret = append(ret, *tm) - } - } - return ret -} - -// getTeamByName returns a team with the given name or nil -func (db *gjDatabase) getTeamByName(nm string) *Team { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - teamPath := []string{"teams"} - var teamUids []string - if teamUids, err = db.bolt.GetBucketList(teamPath); err != nil { - for _, v := range teamUids { - var name string - if name, err = db.bolt.GetValue(append(teamPath, v), "name"); name == nm { - return db.getTeam(v) - } - } - } - return nil -} - -// save saves the team to the db -func (tm *Team) save() error { - var err error - if err = db.open(); err != nil { - return err - } - defer db.close() - - teamPath := []string{"teams", tm.UUID} - if err = db.bolt.SetValue(teamPath, "name", tm.Name); err != nil { - return err - } - - // TODO: Save Team Members - // TODO: Save Team Game - return nil -} - -// delete removes the team from the database -func (tm *Team) delete() error { - var err error - if err = db.open(); err != nil { - return err - } - defer db.close() - - teamPath := []string{"teams"} - return db.bolt.DeleteBucket(teamPath, tm.UUID) -} - -func (tm *Team) getGame() *Game { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - gamePath := []string{"teams", tm.UUID, "game"} - gm := new(Game) - if gm.Name, err = db.bolt.GetValue(gamePath, "name"); err != nil { - gm.Name = "" - } - gm.TeamId = tm.UUID - if gm.Description, err = db.bolt.GetValue(gamePath, "description"); err != nil { - gm.Description = "" - } - if gm.Link, err = db.bolt.GetValue(gamePath, "link"); err != nil { - gm.Link = "" - } - gm.Screenshots = tm.getScreenshots() - return gm -} - -// Screenshots are saved as base64 encoded pngs -func (tm *Team) saveScreenshot(ss *Screenshot) error { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - ssPath := []string{"teams", tm.UUID, "game", "screenshots"} - // Generate a UUID for this screenshot - uuid := uuid.New() - ssPath = append(ssPath, uuid) - if err := db.bolt.MkBucketPath(ssPath); err != nil { - return err - } - if err := db.bolt.SetValue(ssPath, "description", ss.Description); err != nil { - return err - } - if err := db.bolt.SetValue(ssPath, "image", ss.Image); err != nil { - return err - } - if err := db.bolt.SetValue(ssPath, "thumbnail", ss.Thumbnail); err != nil { - return err - } - if err := db.bolt.SetValue(ssPath, "filetype", ss.Filetype); err != nil { - return err - } - return nil -} - -func (tm *Team) getScreenshots() []Screenshot { - var ret []Screenshot - var err error - ssPath := []string{"teams", tm.UUID, "game", "screenshots"} - var ssIds []string - if ssIds, err = db.bolt.GetBucketList(ssPath); err != nil { - return ret - } - for _, v := range ssIds { - if ss := tm.getScreenshot(v); ss != nil { - ret = append(ret, *ss) - } - } - return ret -} - -func (tm *Team) getScreenshot(ssId string) *Screenshot { - var err error - ssPath := []string{"teams", tm.UUID, "game", "screenshots", ssId} - ret := new(Screenshot) - ret.UUID = ssId - if ret.Description, err = db.bolt.GetValue(ssPath, "description"); err != nil { - return nil - } - if ret.Image, err = db.bolt.GetValue(ssPath, "image"); err != nil { - return nil - } - if ret.Thumbnail, err = db.bolt.GetValue(ssPath, "thumbnail"); err != nil { - return nil - } - if ret.Thumbnail == "" { - ret.Thumbnail = ret.Image - } - if ret.Filetype, err = db.bolt.GetValue(ssPath, "filetype"); err != nil { - return nil - } - return ret -} - -func (tm *Team) deleteScreenshot(ssId string) error { - var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - ssPath := []string{"teams", tm.UUID, "game", "screenshots"} - return db.bolt.DeleteBucket(ssPath, ssId) + return nil, errors.New("Invalid Team Id given") } type TeamMember struct { @@ -268,53 +48,131 @@ type TeamMember struct { SlackId string Twitter string Email string + + mPath []string // The path in the DB to this team member } -// Create a new team member, only a name is required -func newTeamMember(nm string) *TeamMember { - m := TeamMember{Name: nm} - return &m +// Create a new team member +func NewTeamMember(tmId, uId string) (*TeamMember, error) { + if tmId == "" { + return nil, errors.New("Team ID is required") + } + if uId == "" { + uId = uuid.New() + } + return &TeamMember{ + UUID: uId, + mPath: []string{"jam", "teams", tmId, "members", uId}, + }, nil } -func (tm *Team) getTeamMember(mbrId string) *TeamMember { +// AddTeamMember adds a new team member +func (tm *Team) AddTeamMember(mbr *TeamMember) error { + lkup, _ := tm.GetTeamMemberById(mbr.UUID) + if lkup != nil { + return errors.New("A Team Member with that Id already exists") + } + tm.Members = append(tm.Members, *mbr) + return nil +} + +// GetTeamMemberById returns a member with the given uuid +// or an error if it couldn't find it +func (tm *Team) GetTeamMemberById(uuid string) (*TeamMember, error) { + for i := range tm.Members { + if tm.Members[i].UUID == uuid { + return &tm.Members[i], nil + } + } + return nil, errors.New("Invalid Team Member Id given") +} + +func (tm *Team) RemoveTeamMemberById(id string) error { + idx := -1 + for i := range tm.Members { + if tm.Members[i].UUID == id { + idx = i + break + } + } + if idx < 0 { + return errors.New("Invalid Team Member ID given") + } + tm.Members = append(tm.Members[:idx], tm.Members[idx+1:]...) + return nil +} + +/** + * DB Functions + * These are generally just called when the app starts up, or when the periodic 'save' runs + */ + +// LoadAllTeams loads all teams for the jam out of the database +func (gj *Gamejam) LoadAllTeams() []Team { var err error - if err = db.open(); err != nil { - return nil - } - defer db.close() - - mbr := new(TeamMember) - mbr.UUID = mbrId - teamMbrPath := []string{"teams", tm.UUID, "members", mbr.UUID} - if mbr.Name, err = db.bolt.GetValue(teamMbrPath, "name"); err != nil { - return nil - } - if mbr.SlackId, err = db.bolt.GetValue(teamMbrPath, "slackid"); err != nil { - return nil - } - if mbr.Twitter, err = db.bolt.GetValue(teamMbrPath, "twitter"); err != nil { - return nil - } - if mbr.Email, err = db.bolt.GetValue(teamMbrPath, "email"); err != nil { - return nil - } - return mbr -} - -func (tm *Team) getTeamMembers() []TeamMember { - var ret []TeamMember - var err error - if err = db.open(); err != nil { + var ret []Team + if err = gj.m.openDB(); err != nil { return ret } - defer db.close() + defer gj.m.closeDB() - teamPath := []string{"teams", tm.UUID, "members"} + var tmUUIDs []string + tmsPath := append(gj.mPath, "teams") + if tmUUIDs, err = gj.m.bolt.GetBucketList(tmsPath); err != nil { + fmt.Println(err.Error()) + return ret + } + for _, v := range tmUUIDs { + tm, _ := gj.LoadTeam(v) + if tm != nil { + ret = append(ret, *tm) + } + } + return ret +} + +// Load a team out of the database +func (gj *Gamejam) LoadTeam(uuid string) (*Team, error) { + var err error + if err = gj.m.openDB(); err != nil { + return nil, err + } + defer gj.m.closeDB() + + // Team Data + tm := NewTeam(uuid) + if tm.Name, err = gj.m.bolt.GetValue(tm.mPath, "name"); err != nil { + return nil, errors.New("Error loading team: " + err.Error()) + } + + // Team Members + tm.Members = gj.LoadTeamMembers(uuid) + + // Team Game + if tm.Game, err = gj.LoadTeamGame(uuid); err != nil { + return nil, errors.New("Error loading team game: " + err.Error()) + } + + return tm, nil +} + +// Load the members of a team from the DB and return them +func (gj *Gamejam) LoadTeamMembers(tmId string) []TeamMember { + var err error + var ret []TeamMember + if err = gj.m.openDB(); err != nil { + return ret + } + defer gj.m.closeDB() + + // Team Members var memberUuids []string - if memberUuids, err = db.bolt.GetBucketList(teamPath); err == nil { + tm := NewTeam(tmId) + mbrsPath := append(tm.mPath, "members") + if memberUuids, err = gj.m.bolt.GetBucketList(mbrsPath); err == nil { for _, v := range memberUuids { - var mbr *TeamMember - if mbr = tm.getTeamMember(v); mbr != nil { + mbr, _ := gj.LoadTeamMember(tmId, v) + if mbr != nil { ret = append(ret, *mbr) } } @@ -322,53 +180,139 @@ func (tm *Team) getTeamMembers() []TeamMember { return ret } -func (tm *Team) updateTeamMember(mbr *TeamMember) error { +// Load a team member from the DB and return it +func (gj *Gamejam) LoadTeamMember(tmId, mbrId string) (*TeamMember, error) { var err error - if err = db.open(); err != nil { + if err = gj.m.openDB(); err != nil { + return nil, err + } + defer gj.m.closeDB() + + mbr, err := NewTeamMember(tmId, mbrId) + if err != nil { + return nil, errors.New("Error loading team member: " + err.Error()) + } + // Name is the only required field + if mbr.Name, err = gj.m.bolt.GetValue(mbr.mPath, "name"); err != nil { + return nil, errors.New("Error loading team member: " + err.Error()) + } + if mbr.SlackId, err = gj.m.bolt.GetValue(mbr.mPath, "slackid"); err != nil { + mbr.SlackId = "" + } + if mbr.Twitter, err = gj.m.bolt.GetValue(mbr.mPath, "twitter"); err != nil { + mbr.Twitter = "" + } + if mbr.Email, err = gj.m.bolt.GetValue(mbr.mPath, "email"); err != nil { + mbr.Email = "" + } + return mbr, nil +} + +func (gj *Gamejam) SaveTeam(tm *Team) error { + var err error + if err = gj.m.openDB(); err != nil { return err } - defer db.close() + defer gj.m.closeDB() - if mbr.UUID == "" { - mbrs := tm.getTeamMembers() - if len(mbrs) > 0 { - for i := range mbrs { - if mbrs[i].Name == mbr.Name { - mbr.UUID = mbrs[i].UUID - break - } - } + // Save team data + if err = gj.m.bolt.SetValue(tm.mPath, "name", tm.Name); err != nil { + return err + } + + // Save team members + for _, mbr := range tm.Members { + if err = gj.m.bolt.SetValue(mbr.mPath, "name", mbr.Name); err != nil { + return err + } + if err = gj.m.bolt.SetValue(mbr.mPath, "slackid", mbr.SlackId); err != nil { + return err + } + if err = gj.m.bolt.SetValue(mbr.mPath, "twitter", mbr.Twitter); err != nil { + return err + } + if err = gj.m.bolt.SetValue(mbr.mPath, "email", mbr.Email); err != nil { + return err } } - if mbr.UUID == "" { - // It's really a new one - mbr.UUID = uuid.New() - } - mbrPath := []string{"teams", tm.UUID, "members", mbr.UUID} - if db.bolt.SetValue(mbrPath, "name", mbr.Name) != nil { + // Save team game + return gj.SaveGame(tm.Game) +} + +// Delete the team tm +// TODO: Deletes should be done all at once when syncing memory to the DB +/* +func (gj *Gamejam) DeleteTeam(tm *Team) error { + var err error + if err = gj.m.openDB(); err != nil { return err } - if db.bolt.SetValue(mbrPath, "slackid", mbr.SlackId) != nil { + defer gj.m.closeDB() + + if len(tm.mPath) < 2 { + return errors.New("Invalid team path: " + string(tm.mPath)) + } + return gj.m.bolt.DeleteBucket(tm.mPath[:len(tm.mPath)-1], tm.UUID) +} +*/ + +// Delete the TeamMember mbr from Team tm +// TODO: Deletes should be done all at once when syncing memory to the DB +/* +func (gj *Gamejam) DeleteTeamMember(tm *Team, mbr *TeamMember) error { + var err error + if err = gj.m.openDB(); err != nil { return err } - if db.bolt.SetValue(mbrPath, "twitter", mbr.Twitter) != nil { - return err + defer gj.m.closeDB() + + if len(mbr.mPath) < 2 { + return errors.New("Invalid team path: " + string(tm.mPath)) } - if db.bolt.SetValue(mbrPath, "email", mbr.Email) != nil { - return err + return gj.m.bolt.DeleteBucket(mbr.mPath[:len(mbr.mPath)-1], mbr.UUID) +} +*/ + +/** + * In Memory functions + * This is generally how the app accesses data + */ + +// Add a team +func (gj *Gamejam) AddTeam(tm *Team) error { + if _, err := gj.GetTeamById(tm.UUID); err == nil { + return errors.New("A team with that ID already exists") } + if _, err := gj.GetTeamByName(tm.Name); err == nil { + return errors.New("A team with that Name already exists") + } + gj.Teams = append(gj.Teams, *tm) return nil } -// deleteTeamMember removes a member from the database -func (tm *Team) deleteTeamMember(mbr *TeamMember) error { - var err error - if err = db.open(); err != nil { - return err +// Find a team by name +func (gj *Gamejam) GetTeamByName(nm string) (*Team, error) { + for i := range gj.Teams { + if gj.Teams[i].Name == nm { + return &gj.Teams[i], nil + } } - defer db.close() - - teamPath := []string{"teams", tm.UUID, "members"} - return db.bolt.DeleteBucket(teamPath, mbr.UUID) + return nil, errors.New("Invalid team name given") +} + +// Remove a team by id +func (gj *Gamejam) RemoveTeamById(id string) error { + idx := -1 + for i := range gj.Teams { + if gj.Teams[i].UUID == id { + idx = i + break + } + } + if idx == -1 { + return errors.New("Invalid Team ID given") + } + gj.Teams = append(gj.Teams[:idx], gj.Teams[idx+1:]...) + return nil } diff --git a/model_users.go b/model_users.go index bcd051c..17df447 100644 --- a/model_users.go +++ b/model_users.go @@ -2,46 +2,52 @@ package main import "golang.org/x/crypto/bcrypt" -// dbHasUser +// These are all model functions that have to do with users +// Unlike gamejam functions, we manipulate the DB directly +// We want to make sure that we always use the most up-to-date user +// information. + // Returns true if there are any users in the database -func (db *gjDatabase) hasUser() bool { - return len(db.getAllUsers()) > 0 +func (m *model) hasUser() bool { + return len(m.getAllUsers()) > 0 } -func (db *gjDatabase) getAllUsers() []string { - if err := db.open(); err != nil { +func (m *model) getAllUsers() []string { + if err := m.openDB(); err != nil { return []string{} } - defer db.close() + defer m.closeDB() - usrs, err := db.bolt.GetBucketList([]string{"users"}) + usrs, err := m.bolt.GetBucketList([]string{"users"}) if err != nil { return []string{} } return usrs } -func (db *gjDatabase) isValidUserEmail(email string) bool { - if err := db.open(); err != nil { +// Is the given email one that is in our DB? +func (m *model) isValidUserEmail(email string) bool { + if err := m.openDB(); err != nil { return false } - defer db.close() + defer m.closeDB() usrPath := []string{"users", email} - _, err := db.bolt.GetValue(usrPath, "password") + _, err := m.bolt.GetValue(usrPath, "password") return err == nil } -func (db *gjDatabase) checkCredentials(email, pw string) error { +// Is the email and pw given valid? +func (m *model) checkCredentials(email, pw string) error { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return err } - defer db.close() + defer m.closeDB() var uPw string usrPath := []string{"users", email} - if uPw, err = db.bolt.GetValue(usrPath, "password"); err != nil { + if uPw, err = m.bolt.GetValue(usrPath, "password"); err != nil { return err } return bcrypt.CompareHashAndPassword([]byte(uPw), []byte(pw)) @@ -51,26 +57,26 @@ func (db *gjDatabase) checkCredentials(email, pw string) error { // Takes an email address and a password // Creates the user if it doesn't exist, encrypts the password // and updates it in the db -func (db *gjDatabase) updateUserPassword(email, password string) error { +func (m *model) updateUserPassword(email, password string) error { cryptPw, cryptError := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if cryptError != nil { return cryptError } - if err := db.open(); err != nil { + if err := m.openDB(); err != nil { return err } - defer db.close() + defer m.closeDB() usrPath := []string{"users", email} - return db.bolt.SetValue(usrPath, "password", string(cryptPw)) + return m.bolt.SetValue(usrPath, "password", string(cryptPw)) } -func (db *gjDatabase) deleteUser(email string) error { +func (m *model) deleteUser(email string) error { var err error - if err = db.open(); err != nil { + if err = m.openDB(); err != nil { return err } - defer db.close() + defer m.closeDB() - return db.bolt.DeleteBucket([]string{"users"}, email) + return m.bolt.DeleteBucket([]string{"users"}, email) } diff --git a/model_votes.go b/model_votes.go index f5520e6..677036c 100644 --- a/model_votes.go +++ b/model_votes.go @@ -1,6 +1,11 @@ package main -import "time" +import ( + "errors" + "fmt" + "strconv" + "time" +) // A Choice is a ranking of a game in a vote type GameChoice struct { @@ -13,19 +18,134 @@ type Vote struct { Timestamp time.Time ClientId string // UUID of client Choices []GameChoice + + mPath []string // The path in the DB to this team } -func (db *gjDatabase) getAllVotes() []Vote { - var ret []Vote +func NewVote(clId string, tm time.Time) (*Vote, error) { + if clId == "" { + return nil, errors.New("Client ID is required") + } + if tm.IsZero() { + tm = time.Now() + } + + vt := new(Vote) + + vt.Timestamp = tm + vt.mPath = []string{"jam", "votes", clId, tm.Format(time.RFC3339)} + + return vt, nil +} + +func (vt *Vote) SetChoices(ch []string) error { + // Clear any previous choices from this vote + vt.Choices = []GameChoice{} + for i, v := range ch { + vt.Choices = append(vt.Choices, GameChoice{Rank: i, Team: v}) + } + return nil +} + +func (gj *Gamejam) GetVoteWithTimeString(clId, ts string) (*Vote, error) { + timestamp, err := time.Parse(time.RFC3339, ts) + if err != nil { + return nil, err + } + return gj.GetVote(clId, timestamp) +} + +func (gj *Gamejam) GetVote(clId string, ts time.Time) (*Vote, error) { + for i := range gj.Votes { + if gj.Votes[i].ClientId == clId && gj.Votes[i].Timestamp == ts { + return &gj.Votes[i], nil + } + } + return nil, errors.New("Couldn't find requested vote") +} + +func (gj *Gamejam) AddVote(vt *Vote) error { + // Make sure that this isn't a duplicate + if _, err := gj.GetVote(vt.ClientId, vt.Timestamp); err == nil { + return errors.New("Duplicate Vote") + } + gj.Votes = append(gj.Votes, *vt) + return nil +} + +/** + * DB Functions + * These are generally just called when the app starts up or when the periodic 'save' runs + */ + +// LoadAllVotes loads all votes for the jam out of the database +func (gj *Gamejam) LoadAllVotes() []Vote { var err error - if err = db.open(); err != nil { + var ret []Vote + if err = gj.m.openDB(); err != nil { return ret } - defer db.close() + defer gj.m.closeDB() - clients := db.getAllClients() - for _, cl := range clients { - ret = append(ret, cl.getVotes()...) + votesPath := []string{"jam", "votes"} + var cliUUIDs []string + if cliUUIDs, err = gj.m.bolt.GetBucketList(votesPath); err != nil { + return ret + } + for _, cId := range cliUUIDs { + vtsPth := append(votesPath, cId) + var times []string + if times, err = gj.m.bolt.GetBucketList(vtsPth); err != nil { + // Error reading this bucket, move on to the next + continue + } + for _, t := range times { + fmt.Println("Loading Vote", cId, t) + if vt, err := gj.LoadVote(cId, t); err == nil { + ret = append(ret, *vt) + } + } } return ret } + +// Load a vote from the DB and return it +func (gj *Gamejam) LoadVote(clientId, t string) (*Vote, error) { + var tm time.Time + var err error + if tm, err = time.Parse(time.RFC3339, t); err != nil { + return nil, errors.New("Error loading vote: " + err.Error()) + } + vt, err := NewVote(clientId, tm) + if err != nil { + return nil, errors.New("Error creating vote: " + err.Error()) + } + var choices []string + if choices, err = gj.m.bolt.GetKeyList(vt.mPath); err != nil { + return nil, errors.New("Error creating vote: " + err.Error()) + } + for _, v := range choices { + ch := new(GameChoice) + var rank int + if rank, err = strconv.Atoi(v); err == nil { + ch.Rank = rank + if ch.Team, err = gj.m.bolt.GetValue(vt.mPath, v); err == nil { + vt.Choices = append(vt.Choices, *ch) + } + } + } + return vt, nil +} + +func (gj *Gamejam) SaveVote(vt *Vote) error { + var err error + if err = gj.m.openDB(); err != nil { + return err + } + defer gj.m.closeDB() + + for _, v := range vt.Choices { + m.bolt.SetValue(vt.mPath, strconv.Itoa(v.Rank), v.Team) + } + return nil +} diff --git a/page_session.go b/page_session.go index 99c99ff..215f421 100644 --- a/page_session.go +++ b/page_session.go @@ -43,7 +43,9 @@ func (p *pageSession) getClientId() string { fmt.Println(" Client IP:" + clientIp) if clientIp != "127.0.0.1" { fmt.Println(" Pulling data by IP") - cli = db.getClientByIp(clientIp) + if cli, err = m.GetClientByIp(clientIp); err != nil { + cli = NewClient(clientId) + } } if cli != nil { clientId = cli.UUID diff --git a/public_endpoints.go b/public_endpoints.go index 5d31c4a..52f98e1 100644 --- a/public_endpoints.go +++ b/public_endpoints.go @@ -2,6 +2,7 @@ package main import ( "encoding/base64" + "fmt" "math/rand" "net/http" "strings" @@ -17,7 +18,7 @@ func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData { func handleMain(w http.ResponseWriter, req *http.Request) { page := initPublicPage(w, req) - if db.getPublicSiteMode() == SiteModeWaiting { + if m.site.GetPublicMode() == SiteModeWaiting { page.SubTitle = "" page.show("public-waiting.html", w) } else { @@ -28,7 +29,7 @@ func handleMain(w http.ResponseWriter, req *http.Request) { func loadVotingPage(w http.ResponseWriter, req *http.Request) { page := initPublicPage(w, req) // Client authentication required - if (db.getAuthMode() == AuthModeAuthentication) && !page.ClientIsAuth { + if (m.site.GetAuthMode() == AuthModeAuthentication) && !page.ClientIsAuth { page.show("unauthorized.html", w) return } @@ -37,7 +38,8 @@ func loadVotingPage(w http.ResponseWriter, req *http.Request) { Timestamp string } vpd := new(votingPageData) - tms := db.getAllTeams() + tms := make([]Team, len(m.jam.Teams)) + copy(tms, m.jam.Teams) // Randomize the team list rand.Seed(time.Now().Unix()) @@ -55,7 +57,7 @@ func loadVotingPage(w http.ResponseWriter, req *http.Request) { func handlePublicSaveVote(w http.ResponseWriter, req *http.Request) { page := initPublicPage(w, req) // Client authentication required - if (db.getAuthMode() == AuthModeAuthentication) && !page.ClientIsAuth { + if (m.site.GetAuthMode() == AuthModeAuthentication) && !page.ClientIsAuth { page.show("unauthorized.html", w) return } @@ -66,23 +68,40 @@ func handlePublicSaveVote(w http.ResponseWriter, req *http.Request) { ts := req.FormValue("timestamp") timestamp, err := time.Parse(time.RFC3339, ts) if err != nil { - page.session.setFlashMessage("Error parsing timestamp: "+ts, "error") + page.session.setFlashMessage("Error creating vote", "error") + fmt.Println("Error parsing timestamp: " + ts) redirect("/", w, req) } - client := db.getClient(page.ClientId) - if _, err := client.getVote(timestamp); err == nil { + client, err := m.GetClient(page.ClientId) + if err != nil { + client = NewClient(page.ClientId) + } + + // voteSlice is an ordered string slice of the voters preferences + voteCSV := req.FormValue("uservote") + voteSlice := strings.Split(voteCSV, ",") + + if _, err = m.jam.GetVote(client.UUID, timestamp); err == nil { // Duplicate vote... Cancel it. page.session.setFlashMessage("Duplicate vote!", "error") redirect("/", w, req) } - // voteSlice is an ordered string slice of the voters preferences - voteCSV := req.FormValue("uservote") - voteSlice := strings.Split(voteCSV, ",") - if err := client.saveVote(timestamp, voteSlice); err != nil { - page.session.setFlashMessage("Error Saving Vote: "+err.Error(), "error") + + var vt *Vote + if vt, err = NewVote(client.UUID, timestamp); err != nil { + fmt.Println("Error creating vote: " + err.Error()) + page.session.setFlashMessage("Error creating vote", "error") + redirect("/", w, req) } - if newVote, err := client.getVote(timestamp); err == nil { - site.Votes = append(site.Votes, *newVote) + if err = vt.SetChoices(voteSlice); err != nil { + fmt.Println("Error creating vote: " + err.Error()) + page.session.setFlashMessage("Error creating vote", "error") + redirect("/", w, req) + } + if err := m.jam.AddVote(vt); err != nil { + fmt.Println("Error adding vote: " + err.Error()) + page.session.setFlashMessage("Error creating vote", "error") + redirect("/", w, req) } page.session.setFlashMessage("Vote Saved!", "success large fading") redirect("/", w, req) @@ -91,19 +110,22 @@ func handlePublicSaveVote(w http.ResponseWriter, req *http.Request) { func handleThumbnailRequest(w http.ResponseWriter, req *http.Request) { // Thumbnail requests are open even without client authentication vars := mux.Vars(req) - tm := db.getTeam(vars["teamid"]) - if tm == nil { + tm, err := m.jam.GetTeamById(vars["teamid"]) + if err != nil { + fmt.Println("handleThumbnailRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } - ss := tm.getScreenshot(vars["imageid"]) - if ss == nil { + ss, err := tm.Game.GetScreenshot(vars["imageid"]) + if err != nil { + fmt.Println("handleThumbnailRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } w.Header().Set("Content-Type", "image/"+ss.Filetype) dat, err := base64.StdEncoding.DecodeString(ss.Thumbnail) if err != nil { + fmt.Println("handleThumbnailRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } @@ -113,19 +135,22 @@ func handleThumbnailRequest(w http.ResponseWriter, req *http.Request) { func handleImageRequest(w http.ResponseWriter, req *http.Request) { // Image requests are open even without client authentication vars := mux.Vars(req) - tm := db.getTeam(vars["teamid"]) - if tm == nil { + tm, err := m.jam.GetTeamById(vars["teamid"]) + if err != nil { + fmt.Println("handleImageRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } - ss := tm.getScreenshot(vars["imageid"]) - if ss == nil { + ss, err := tm.Game.GetScreenshot(vars["imageid"]) + if err != nil { + fmt.Println("handleImageRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } w.Header().Set("Content-Type", "image/"+ss.Filetype) dat, err := base64.StdEncoding.DecodeString(ss.Image) if err != nil { + fmt.Println("handleImageRequest: " + err.Error()) http.Error(w, "Couldn't find image", 404) return } @@ -134,69 +159,78 @@ func handleImageRequest(w http.ResponseWriter, req *http.Request) { func handleTeamMgmtRequest(w http.ResponseWriter, req *http.Request) { // Team Management pages are open even without client authentication - if db.getPublicSiteMode() == SiteModeVoting { + if m.site.GetPublicMode() == SiteModeVoting { redirect("/", w, req) } page := initPublicPage(w, req) vars := mux.Vars(req) page.SubTitle = "Team Details" teamId := vars["id"] - tm := db.getTeam(teamId) - if tm != nil { + tm, err := m.jam.GetTeamById(teamId) + if err == nil { // Team self-management functions switch vars["function"] { case "": page.SubTitle = "Team Management" page.TemplateData = tm page.show("public-teammgmt.html", w) + case "savemember": - m := newTeamMember(req.FormValue("newmembername")) + m, err := NewTeamMember(tm.UUID, "") + if err != nil { + page.session.setFlashMessage("Error adding team member: "+err.Error(), "error") + redirect("/team/"+tm.UUID+"#members", w, req) + } + m.Name = req.FormValue("newmembername") m.SlackId = req.FormValue("newmemberslackid") m.Twitter = req.FormValue("newmembertwitter") m.Email = req.FormValue("newmemberemail") - if err := tm.updateTeamMember(m); err != nil { + if err := tm.AddTeamMember(m); err != nil { page.session.setFlashMessage("Error adding team member: "+err.Error(), "error") } else { page.session.setFlashMessage(m.Name+" added to team!", "success") } - refreshTeamsInMemory() redirect("/team/"+tm.UUID+"#members", w, req) + case "deletemember": mbrId := req.FormValue("memberid") - m := tm.getTeamMember(mbrId) - if m != nil { - if err := tm.deleteTeamMember(m); err != nil { - page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error") - } else { - page.session.setFlashMessage(m.Name+" deleted from team", "success") - } + err := tm.RemoveTeamMemberById(mbrId) + if err != nil { + page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error") } else { - page.session.setFlashMessage("Couldn't find member to delete", "error") + page.session.setFlashMessage("Team member removed", "success") } - refreshTeamsInMemory() redirect("/team/"+tm.UUID, w, req) + case "savegame": - gm := newGame(tm.UUID) - gm.Name = req.FormValue("gamename") - gm.Link = req.FormValue("gamelink") - gm.Description = req.FormValue("gamedesc") - if err := gm.save(); err != nil { + tm.Game.Name = req.FormValue("gamename") + tm.Game.Link = req.FormValue("gamelink") + tm.Game.Description = req.FormValue("gamedesc") + page.session.setFlashMessage("Team game updated", "success") + redirect("/team/"+tm.UUID, w, req) + + case "screenshotupload": + ss, err := ssFromRequest(tm, req) + if err != nil { + page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + redirect("/team/"+tm.UUID, w, req) + } + gm := tm.Game + gm.Screenshots = append(gm.Screenshots, *ss) + if err = m.jam.UpdateGame(tm.UUID, gm); err != nil { page.session.setFlashMessage("Error updating game: "+err.Error(), "error") } else { - page.session.setFlashMessage("Team game updated", "success") - } - redirect("/team/"+tm.UUID, w, req) - case "screenshotupload": - if err := saveScreenshots(tm, req); err != nil { - page.session.setFlashMessage("Error updating game: "+err.Error(), "error") + page.session.setFlashMessage("Screenshot Uploaded", "success") } redirect("/team/"+tm.UUID, w, req) + case "screenshotdelete": ssid := vars["subid"] - if err := tm.deleteScreenshot(ssid); err != nil { + if err := tm.Game.RemoveScreenshot(ssid); err != nil { page.session.setFlashMessage("Error deleting screenshot: "+err.Error(), "error") } redirect("/team/"+tm.UUID, w, req) + } } else { http.Error(w, "Page Not Found", 404) diff --git a/templates/admin-votes.html b/templates/admin-votes.html index d7e750c..203e447 100644 --- a/templates/admin-votes.html +++ b/templates/admin-votes.html @@ -31,7 +31,7 @@ - {{ len .Site.Votes }} Total Votes + {{ len .TemplateData.AllVotes }} Total Votes