2017-06-22 Build

* Use 'https://github.com/mjibson/esc' for embedded assets
* Pull database name out of siteData object
* Load site config from database, if available
* Allow customizing site config from command line arguments
* Clean up some templates
* Update Readme (it still needs a lot of updating)
* Started work on vote accumulation/management
This commit is contained in:
Brian Buller 2017-06-22 10:34:57 -05:00
parent b283aacc6a
commit ba35073d95
21 changed files with 12325 additions and 136 deletions

View File

@ -1,2 +1,11 @@
# ictgj-voting
The ICT GameJam Voting Application
pass in the `-dev` flag to enable development mode (load assets from file system instead of embedded).
Uses 'esc' for embedding assets:
https://github.com/mjibson/esc
TODO: Build Instructions
TODO: Contribution Instructions

View File

@ -2,6 +2,7 @@ package main
import (
"net/http"
"strconv"
"github.com/gorilla/mux"
)
@ -40,8 +41,24 @@ func handleAdmin(w http.ResponseWriter, req *http.Request) {
handleAdminGames(w, req, page)
case "clients":
handleAdminClients(w, req, page)
case "votes":
handleAdminVotes(w, req, page)
case "mode":
handleAdminSetMode(w, req, page)
default:
page.show("admin-main.html", w)
}
}
}
func handleAdminSetMode(w http.ResponseWriter, req *http.Request, page *pageData) {
vars := mux.Vars(req)
newMode, err := strconv.Atoi(vars["id"])
if err != nil {
page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error")
}
if dbSetPublicSiteMode(newMode) != nil {
page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error")
}
redirect("/admin", w, req)
}

20
admin_votes.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func handleAdminVotes(w http.ResponseWriter, req *http.Request, page *pageData) {
vars := mux.Vars(req)
page.SubTitle = "Votes"
switch vars["function"] {
default:
type votesPageData struct {
Votes []Vote
}
page.TemplateData = votesPageData{Votes: dbGetAllVotes()}
page.show("admin-votes.html", w)
}
}

11962
assets.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -131,7 +131,23 @@ table.padding td {
.pure-button-error {
background-color: #DD0000;
color: #FFFFFF;
color: #FFF;
}
.pure-button-toggle-first {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.pure-button-toggle-middle {
border-radius: 0px;
margin-left: -5px;
border-left: 1px solid #CCC;
}
.pure-button-toggle-last {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
margin-left: -5px;
border-left: 1px solid #CCC;
}
#modal-overlay {
@ -187,6 +203,11 @@ table.padding td {
margin-left: 150px;
}
aside.flash.error {
background-color: #DD0000;
color: #FFFFFF;
}
aside.flash.success {
background-color: #229af9;
color: #FFFFFF;

View File

@ -1 +0,0 @@
{"title":"ICT GameJam Voting","port":8080,"session":"ict-gamejam","dir":"./","devmode":true,"db":"gjvote.db","CurrentJam":""}

143
main.go
View File

@ -1,11 +1,11 @@
package main
//go:generate esc -o assets.go assets
import (
"bufio"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
@ -21,15 +21,15 @@ import (
)
const AppName = "gjvote"
const DbName = AppName + ".db"
// SiteData is stuff that stays the same
type siteData struct {
Title string `json:"title"`
Port int `json:"port"`
SessionName string `json:"session"`
ServerDir string `json:"dir"`
DevMode bool `json:"devmode"`
DB string `json:"db"`
Title string
Port int
SessionName string
ServerDir string
DevMode bool
CurrentJam string
}
@ -56,6 +56,7 @@ type pageData struct {
ClientIsServer bool
TeamID string
PublicMode int
TemplateData interface{}
}
@ -70,19 +71,22 @@ var sessionSecret = "JCOP5e8ohkTcOzcSMe74"
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
var site *siteData
var r *mux.Router
var configFile string
func main() {
configFile = "./config.json"
loadConfig()
saveConfig()
dbSaveSiteConfig(site)
initialize()
r = mux.NewRouter()
r.StrictSlash(true)
s := http.StripPrefix("/assets/", http.FileServer(http.Dir(site.ServerDir+"assets/")))
r.PathPrefix("/assets/").Handler(s)
if 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)))
// Public Subrouter
pub := r.PathPrefix("/").Subrouter()
@ -109,27 +113,49 @@ func main() {
}
func loadConfig() {
site = new(siteData)
// Defaults:
site.Title = "ICT GameJam"
site.Port = 8080
site.SessionName = "ict-gamejam"
site.ServerDir = "./"
site.DevMode = false
site.DB = AppName + ".db"
site = dbGetSiteConfig()
jsonInp, err := ioutil.ReadFile(configFile)
if err == nil {
assertError(json.Unmarshal(jsonInp, &site))
if len(os.Args) > 1 {
for _, v := range os.Args {
key := v
val := ""
eqInd := strings.Index(v, "=")
if eqInd > 0 {
// It's a key/val argument
key = v[:eqInd]
val = v[eqInd+1:]
}
switch key {
case "-title":
site.Title = val
fmt.Print("Set site title: ", 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
}
// TODO: Make sure a valid port number is given
site.Port = tryPort
case "-session-name":
site.SessionName = val
case "-server-dir":
// TODO: Probably check if the given directory is valid
site.ServerDir = val
case "-help", "-h", "-?":
printHelp()
done()
case "-dev":
site.DevMode = true
case "-reset-defaults":
resetToDefaults()
done()
}
}
}
}
func saveConfig() {
var jsonInp []byte
jsonInp, _ = json.Marshal(site)
assertError(ioutil.WriteFile(configFile, jsonInp, 0644))
}
func initialize() {
// Check if the database has been created
assertError(initDatabase())
@ -221,9 +247,10 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
// Build the menu
if p.LoggedIn {
p.Menu = append(p.Menu, menuItem{"Admin", "/admin", "fa-key"})
p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"})
p.Menu = append(p.Menu, menuItem{"Teams", "/admin/teams", "fa-users"})
p.Menu = append(p.Menu, menuItem{"Games", "/admin/games", "fa-gamepad"})
p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"})
p.Menu = append(p.Menu, menuItem{"Clients", "/admin/clients", "fa-desktop"})
p.BottomMenu = append(p.BottomMenu, menuItem{"Users", "/admin/users", "fa-user"})
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/admin/dologout", "fa-sign-out"})
@ -240,8 +267,12 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
p.ClientID = p.session.getClientID()
p.ClientIsAuth = clientIsAuthenticated(p.ClientID, req)
p.ClientIsServer = clientIsServer(req)
// TeamID is for team self-administration
p.TeamID, _ = p.session.getStringValue("teamid")
// Public Mode
p.PublicMode = dbGetPublicSiteMode()
return p
}
@ -265,6 +296,7 @@ func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
// outputTemplate
// Spit out a template
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
// TODO: Use embedded files for these... Hopefully?
_, err := os.Stat("templates/" + tmplName)
if err == nil {
t := template.New(tmplName)
@ -279,12 +311,55 @@ func redirect(url string, w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, url, 303)
}
func printOutput(out string) {
if site.DevMode {
fmt.Print(out)
func resetToDefaults() {
def := GetDefaultSiteConfig()
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.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 dbSaveSiteConfig(def) != nil {
errorExit("Error resetting to defaults")
}
fmt.Println("Reset to defaults")
}
}
func printHelp() {
help := []string{
"Game Jam Voting Help",
" -help, -h, -? Print this message",
" -dev Development mode, load assets from file system",
" -port=<port num> Set the site port",
" -session-name=<session> Set the name of the session to be used",
" -server-dir=<directory> Set the server directory",
" This designates where the database will be saved",
" and where the app will look for files if you're",
" operating in 'development' mode (-dev)",
" -title=<title> Set the site title",
" -current-jam=<name> Change the name of the current jam",
" -reset-defaults Reset all configuration options to defaults",
"",
}
for _, v := range help {
fmt.Println(v)
}
}
func done() {
os.Exit(0)
}
func errorExit(msg string) {
fmt.Println(msg)
os.Exit(1)
}
func assertError(err error) {
if err != nil {
panic(err)

View File

@ -10,11 +10,26 @@ import (
var db *boltease.DB
var dbOpened int
const (
SiteModeWaiting = iota
SiteModeVoting
SiteModeError
)
func GetDefaultSiteConfig() *siteData {
ret := new(siteData)
ret.Title = "ICT GameJam"
ret.Port = 8080
ret.SessionName = "ict-gamejam"
ret.ServerDir = "./"
return ret
}
func openDatabase() error {
dbOpened += 1
if dbOpened == 1 {
var err error
db, err = boltease.Create(site.DB, 0600, nil)
db, err = boltease.Create(DbName, 0600, nil)
if err != nil {
return err
}
@ -82,3 +97,64 @@ func dbGetCurrentJam() (string, error) {
}
return ret, err
}
func dbGetSiteConfig() *siteData {
var ret *siteData
def := GetDefaultSiteConfig()
var err error
if err = openDatabase(); err != nil {
return def
}
defer closeDatabase()
ret = new(siteData)
siteConf := []string{"site"}
if ret.Title, err = db.GetValue(siteConf, "title"); err != nil {
ret.Title = def.Title
}
if ret.Port, err = db.GetInt(siteConf, "port"); err != nil {
ret.Port = def.Port
}
if ret.SessionName, err = db.GetValue(siteConf, "session-name"); err != nil {
ret.SessionName = def.SessionName
}
if ret.ServerDir, err = db.GetValue(siteConf, "server-dir"); err != nil {
ret.ServerDir = def.ServerDir
}
return ret
}
func dbSaveSiteConfig(dat *siteData) error {
var err error
if err = openDatabase(); err != nil {
return err
}
defer closeDatabase()
siteConf := []string{"site"}
if err = db.SetValue(siteConf, "title", dat.Title); err != nil {
return err
}
if err = db.SetInt(siteConf, "port", dat.Port); err != nil {
return err
}
if err = db.SetValue(siteConf, "session-name", dat.SessionName); err != nil {
return err
}
return db.SetValue(siteConf, "server-dir", dat.ServerDir)
}
func dbGetPublicSiteMode() int {
if ret, err := db.GetInt([]string{"site"}, "public-mode"); err != nil {
return SiteModeWaiting
} else {
return ret
}
}
func dbSetPublicSiteMode(mode int) error {
if mode < 0 || mode >= SiteModeError {
return errors.New("Invalid site mode")
}
return db.SetInt([]string{"site"}, "public-mode", mode)
}

44
model_votes.go Normal file
View File

@ -0,0 +1,44 @@
package main
import "time"
type Vote struct {
Timestamp time.Time
ClientId string
}
func dbGetAllVotes() []Vote {
var ret []Vote
var err error
if err = db.OpenDB(); err != nil {
return ret
}
defer db.CloseDB()
votesBkt := []string{"votes"}
return ret
}
func dbGetVote(clientId string, timestamp time.Time) *Vote {
var err error
if err = db.OpenDB(); err != nil {
return nil
}
defer db.CloseDB()
vt := new(Vote)
return vt
}
func dbSaveVote(clientId string, timestamp time.Time, votes []string) error {
var err error
if err = db.OpenDB(); err != nil {
return nil
}
defer db.CloseDB()
votesBkt := []string{"votes", clientId}
return err
}

View File

@ -1,30 +1,21 @@
package main
import (
"fmt"
"net/http"
)
func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData {
p := InitPageData(w, req)
return p
}
func handleMain(w http.ResponseWriter, req *http.Request) {
page := initPublicPage(w, req)
page.SubTitle = ""
for _, tmpl := range []string{
"htmlheader.html",
"admin-menu.html",
"header.html",
"main.html",
"footer.html",
"htmlfooter.html",
} {
if err := outputTemplate(tmpl, page, w); err != nil {
fmt.Printf("%s\n", err)
}
switch dbGetPublicSiteMode() {
case SiteModeWaiting:
page.show("public-waiting.html", w)
case SiteModeVoting:
page.show("public-voting.html", w)
}
}

View File

@ -5,45 +5,6 @@
<label for="teamname">Team Name</label>
<input id="teamname" name="teamname" type="text" placeholder="Team Name" value="" autofocus>
</div>
<h2>Members</h2>
<table>
<thead>
<th>Name</th>
<th>Slack ID</th>
<th>Twitter</th>
<th>Email</th>
<th></th>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td>
</td>
</tr>
</tbody>
</table>
<div class="team-member">
<div class="pure-control-group">
<label for="membername">Name</label>
<input id="membername" name="membername" type="text" placeholder="Name" value="">
</div>
<div class="pure-control-group">
<label for="memberslackid">Slack ID</label>
<input id="memberslackid" name="memberslackid" type="text" placeholder="@SlackID" value="">
</div>
<div class="pure-control-group">
<label for="membertwitter">Twitter</label>
<input id="membertwitter" name="membertwitter" type="text" placeholder="@TwitterID" value="">
</div>
<div class="pure-control-group">
<label for="memberemail">Email</label>
<input id="memberemail" name="memberemail" type="text" placeholder="user@email.com" value="">
</div>
</div>
<button type="submit" class="pure-button pure-button-primary">Add Team</button>
</fieldset>
</form>

View File

@ -1,3 +1,7 @@
<div>The hosting server is always an authenticated client</div>
{{ if not .TemplateData.Clients }}
<div>No additional clients have been authenticated</div>
{{ else }}
<table id="clients-table" class="hidden sortable pure-table pure-table-bordered center">
<thead>
<tr>
@ -17,12 +21,4 @@
{{ end }}
</tbody>
</table>
<script>
snack.ready(function() {
var tableBody = document.querySelector("#clients-table>tbody");
if(tableBody.children.length>0) {
// Show the table
document.getElementById('clients-table').classList.remove('hidden');
}
});
</script>
{{ end }}

View File

@ -1,23 +1,22 @@
<table id="games-table" class="hidden sortable pure-table pure-table-bordered center">
{{ if not .TemplateData.Games }}
<div>No games have been created</div>
{{ else }}
<table id="games-table" class="sortable pure-table pure-table-bordered center">
<thead>
<tr>
<th>Game Name</th>
</tr>
<tr>
<th>Game Name</th>
<th>Team Name</th>
<th>Screenshots</th>
</tr>
</thead>
<tbody>
{{ range $i, $v := .TemplateData.Games }}
<tr>
<td>{{ $v.Name }}</td>
</tr>
{{ end }}
{{ range $i, $v := .TemplateData.Games }}
<tr>
<td>{{ $v.Name }}</td>
<td></td>
<td>{{ len $v.Screenshots }}</td>
</tr>
{{ end }}
</tbody>
</table>
<script>
snack.ready(function() {
var tableBody = document.querySelector("#games-table>tbody");
if(tableBody.children.length>0) {
// Show the table
document.getElementById('games-table').classList.remove('hidden');
}
});
</script>
{{ end }}

View File

@ -1,6 +1,14 @@
<div class="content">
<button onclick="window.location.href='/admin/votes'">Votes</button>
<button onclick="window.location.href='/admin/teams'">Teams</button>
<button onclick="window.location.href='/admin/games'">Games</button>
<button onclick="window.location.href='/admin/users'">Users</button>
<div>
<h3>Public Mode</h3>
<button onclick="window.location.href='/admin/mode/0'" class="pure-button-toggle-first pure-button {{ if eq .PublicMode 0 }}pure-button-primary{{ end }}">Waiting</button>
<button onclick="window.location.href='/admin/mode/1'" class="pure-button-toggle-last pure-button {{ if eq .PublicMode 1 }}pure-button-primary{{ end }}">Voting</button>
</div>
<div>
<h3>Admin Sections</h3>
<button class="pure-button" onclick="window.location.href='/admin/votes'">Votes</button>
<button class="pure-button" onclick="window.location.href='/admin/teams'">Teams</button>
<button class="pure-button" onclick="window.location.href='/admin/games'">Games</button>
<button class="pure-button" onclick="window.location.href='/admin/users'">Users</button>
</div>
</div>

View File

@ -1,6 +1,5 @@
<div id="menu" class="{{if .HideAdminMenu}}hidden{{end}}">
<div class="pure-menu">
<a class="pure-menu-heading" href="/admin/">Admin</a>
<a href="#menu" class="menu-button">
<i class="fa fa-bars"></i>
</a>
@ -13,9 +12,7 @@
{{ end }}
</ul>
{{ if .ClientIsAuth }}
{{ if .ClientIsServer }}
<span class="pure-menu-nonlink"><i class="fa fa-server"></i> Server Mode</span>
{{ else }}
{{ if not .ClientIsServer }}
<a href="/admin/clients/{{.ClientID}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
{{ end }}
{{ else }}

View File

@ -1,12 +1,16 @@
<div class="bottom-space center">
<a id="btnAddTeam" class="pure-button pure-button-success" href="/admin/teams/new"><i class="fa fa-plus"></i> Add Team</a>
</div>
<table id="teams-table" class="hidden sortable pure-table pure-table-bordered center">
{{ if not .TemplateData.Teams }}
<div>No teams have been created</div>
{{ else }}
<table id="teams-table" class="sortable pure-table pure-table-bordered center">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Members</th>
<th>Game</th>
<th></th>
</tr>
</thead>
@ -16,6 +20,7 @@
<td>{{ $v.Name }}</td>
<td>{{ $v.UUID }}</td>
<td>{{ len $v.Members }}</td>
<td>{{ $v.Game.Name }}</td>
<td>
<a href="/admin/teams/{{ $v.UUID }}/edit" class="pure-button pure-button-plain"><i class="fa fa-pencil"></i></a>
<a href="/admin/teams/{{ $v.UUID }}/delete" class="pure-button pure-button-plain"><i class="fa fa-trash"></i></a>
@ -24,12 +29,4 @@
{{ end }}
</tbody>
</table>
<script>
snack.ready(function() {
var tableBody = document.querySelector("#teams-table>tbody");
if(tableBody.children.length>0) {
// Show the table
document.getElementById('teams-table').classList.remove('hidden');
}
});
</script>
{{ end }}

View File

@ -0,0 +1,14 @@
<table id="votes-table" class="sortable pure-table pure-table-bordered center">
<thead>
<tr>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{{ range $i, $v, := .TemplateData.Votes }}
<tr>
<td>{{ $v.Timestamp }}</td>
</tr>
{{ end }}
</tbody>
</table>

View File

@ -8,6 +8,8 @@ var clientID = "{{.ClientID}}";
<div class="header">
devICT Game Jam - {{.CurrentJam}}
</div>
{{ if .SubTitle }}
<div class="header-menu">
<h2>{{.SubTitle}}</h2>
</div>
{{ end }}

View File

@ -1 +0,0 @@
<div>Default Public Facing Page</div>

View File

@ -0,0 +1 @@
<h1>VOTING TIME</h1>

View File

@ -0,0 +1 @@
<div>ICT Game Jam</div>