logic done
This commit is contained in:
parent
ef52fa1c58
commit
960dd25bbd
34 changed files with 822 additions and 45 deletions
7
config.yaml
Normal file
7
config.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Port: 8080
|
||||||
|
DataService:
|
||||||
|
IP: 127.0.0.1
|
||||||
|
Port: 27017
|
||||||
|
Username: haveachin
|
||||||
|
Password: vKz8VmTTB4HZx2
|
||||||
|
Database: scoreboard
|
|
@ -37,4 +37,7 @@ func setDefaults() {
|
||||||
viper.SetDefault("Port", "8080")
|
viper.SetDefault("Port", "8080")
|
||||||
viper.SetDefault("DataService.IP", "127.0.0.1")
|
viper.SetDefault("DataService.IP", "127.0.0.1")
|
||||||
viper.SetDefault("DataService.Port", "27017")
|
viper.SetDefault("DataService.Port", "27017")
|
||||||
|
viper.SetDefault("DataService.Username", "haveachin")
|
||||||
|
viper.SetDefault("DataService.Password", "<changeme>")
|
||||||
|
viper.SetDefault("DataService.Database", "scoreboard")
|
||||||
}
|
}
|
||||||
|
|
8
data/game.go
Normal file
8
data/game.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
ID primitive.ObjectID `bson:"_id,omitempty"`
|
||||||
|
Name string `bson:"name,omitempty"`
|
||||||
|
}
|
14
data/group.go
Normal file
14
data/group.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import "go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
ID primitive.ObjectID `bson:"_id,omitempty"`
|
||||||
|
Name string `bson:"name,omitempty"`
|
||||||
|
Scores []Score `bson:"scores,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Score struct {
|
||||||
|
GameID primitive.ObjectID `bson:"_gameId,omitempty"`
|
||||||
|
Points int `bson:"points,omitempty"`
|
||||||
|
}
|
58
features/game/handler.go
Normal file
58
features/game/handler.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features/weberror"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/response"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/filter"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/id"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func queryOneHandler(s features.Service, t *template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.WebHandler(w, response.WebHandlerFunc(func() (response.Page, int) {
|
||||||
|
if chi.URLParam(r, "id") == "" {
|
||||||
|
return weberror.NewPage(*t, errors.New("No id in request"), http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := id.ParseFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var game data.Game
|
||||||
|
|
||||||
|
if err := s.QueryOne(&game, &filter.Filter{"_id": *id}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []data.Group
|
||||||
|
|
||||||
|
if err := s.Query(&groups, &filter.Filter{}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDetailPage(*t, game, groups), http.StatusOK
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryHandler(s features.Service, t *template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.WebHandler(w, response.WebHandlerFunc(func() (response.Page, int) {
|
||||||
|
var games []data.Game
|
||||||
|
|
||||||
|
if err := s.Query(&games, &filter.Filter{}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMasterPage(*t, games), http.StatusOK
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
79
features/game/page.go
Normal file
79
features/game/page.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type previewGame struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type master struct {
|
||||||
|
Template template.Template
|
||||||
|
Games []previewGame
|
||||||
|
}
|
||||||
|
|
||||||
|
type previewGroup struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Points int
|
||||||
|
}
|
||||||
|
|
||||||
|
type detail struct {
|
||||||
|
Template template.Template
|
||||||
|
Game data.Game
|
||||||
|
Groups []previewGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMasterPage(t template.Template, games []data.Game) *master {
|
||||||
|
previewGames := []previewGame{}
|
||||||
|
|
||||||
|
for _, game := range games {
|
||||||
|
previewGames = append(previewGames, previewGame{
|
||||||
|
ID: game.ID.Hex(),
|
||||||
|
Name: game.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &master{
|
||||||
|
Template: *t.Lookup("gameMaster.html"),
|
||||||
|
Games: previewGames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDetailPage(t template.Template, game data.Game, groups []data.Group) *detail {
|
||||||
|
previewGroups := []previewGroup{}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
points := 0
|
||||||
|
for _, score := range group.Scores {
|
||||||
|
if score.GameID == game.ID {
|
||||||
|
points = score.Points
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previewGroups = append(previewGroups, previewGroup{
|
||||||
|
ID: group.ID.Hex(),
|
||||||
|
Name: group.Name,
|
||||||
|
Points: points,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &detail{
|
||||||
|
Template: *t.Lookup("gameDetail.html"),
|
||||||
|
Game: game,
|
||||||
|
Groups: previewGroups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page master) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page detail) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
23
features/game/routes.go
Normal file
23
features/game/routes.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(s features.Service, t *template.Template) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
s.RegisterCollection(reflect.TypeOf(data.Game{}), "game")
|
||||||
|
|
||||||
|
router.Post("/", features.CreateHandler(&data.Game{}, s))
|
||||||
|
router.Get("/{id}", queryOneHandler(s, t))
|
||||||
|
router.Get("/", queryHandler(s, t))
|
||||||
|
router.Put("/{id}", features.UpdateHandler(&data.Game{}, s))
|
||||||
|
router.Delete("/{id}", features.DeleteHandler(&data.Game{}, s))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
58
features/group/handler.go
Normal file
58
features/group/handler.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features/weberror"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/response"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/filter"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/id"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func queryOneHandler(s features.Service, t *template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.WebHandler(w, response.WebHandlerFunc(func() (response.Page, int) {
|
||||||
|
if chi.URLParam(r, "id") == "" {
|
||||||
|
return weberror.NewPage(*t, errors.New("No id in request"), http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := id.ParseFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var group data.Group
|
||||||
|
|
||||||
|
if err := s.QueryOne(&group, &filter.Filter{"_id": *id}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var games []data.Game
|
||||||
|
|
||||||
|
if err := s.Query(&games, &filter.Filter{}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDetailPage(*t, group, games), http.StatusOK
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryHandler(s features.Service, t *template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.WebHandler(w, response.WebHandlerFunc(func() (response.Page, int) {
|
||||||
|
var groups []data.Group
|
||||||
|
|
||||||
|
if err := s.Query(&groups, &filter.Filter{}); err != nil {
|
||||||
|
return weberror.NewPage(*t, err, http.StatusBadRequest), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMasterPage(*t, groups), http.StatusOK
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
62
features/group/page.go
Normal file
62
features/group/page.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type previewGroup struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
PointSum int
|
||||||
|
}
|
||||||
|
|
||||||
|
type master struct {
|
||||||
|
Template template.Template
|
||||||
|
Groups []previewGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type detail struct {
|
||||||
|
Template template.Template
|
||||||
|
Group data.Group
|
||||||
|
Games []data.Game
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMasterPage(t template.Template, groups []data.Group) *master {
|
||||||
|
previewGroups := []previewGroup{}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
sum := 0
|
||||||
|
for _, score := range group.Scores {
|
||||||
|
sum += score.Points
|
||||||
|
}
|
||||||
|
previewGroups = append(previewGroups, previewGroup{
|
||||||
|
ID: group.ID.Hex(),
|
||||||
|
Name: group.Name,
|
||||||
|
PointSum: sum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &master{
|
||||||
|
Template: *t.Lookup("groupMaster.html"),
|
||||||
|
Groups: previewGroups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDetailPage(t template.Template, group data.Group, games []data.Game) *detail {
|
||||||
|
return &detail{
|
||||||
|
Template: *t.Lookup("groupDetail.html"),
|
||||||
|
Group: group,
|
||||||
|
Games: games,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page master) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page detail) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
23
features/group/routes.go
Normal file
23
features/group/routes.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/data"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(s features.Service, t *template.Template) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
s.RegisterCollection(reflect.TypeOf(data.Group{}), "group")
|
||||||
|
|
||||||
|
router.Post("/", features.CreateHandler(&data.Group{}, s))
|
||||||
|
router.Get("/{id}", queryOneHandler(s, t))
|
||||||
|
router.Get("/", queryHandler(s, t))
|
||||||
|
router.Put("/{id}", features.UpdateHandler(&data.Group{}, s))
|
||||||
|
router.Delete("/{id}", features.DeleteHandler(&data.Group{}, s))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
69
features/handler.go
Normal file
69
features/handler.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package features
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/ioutil"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/request/meta"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/response"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/filter"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateHandler(v interface{}, s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.APIHandler(w, response.APIHandlerFunc(func() (int, error) {
|
||||||
|
if err := ioutil.ExtractAndValidateJSON(r.Body, v); err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Create(v); err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(v, meta.ParseFromRequest(r)).Send(w, http.StatusCreated)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateHandler(v interface{}, s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.APIHandler(w, response.APIHandlerFunc(func() (int, error) {
|
||||||
|
if err := ioutil.ExtractAndValidateJSON(r.Body, v); err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := id.ParseFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &filter.Filter{"_id": *id}
|
||||||
|
|
||||||
|
if err := s.Update(v, filter); err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(nil, meta.ParseFromRequest(r)).Send(w, http.StatusOK)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteHandler(v interface{}, s Service) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.APIHandler(w, response.APIHandlerFunc(func() (int, error) {
|
||||||
|
id, err := id.ParseFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &filter.Filter{"_id": *id}
|
||||||
|
|
||||||
|
if err := s.Delete(v, filter); err != nil {
|
||||||
|
return http.StatusNotFound, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.New(nil, meta.ParseFromRequest(r)).Send(w, http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
17
features/index/handler.go
Normal file
17
features/index/handler.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func queryHandler(s features.Service, t *template.Template) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.WebHandler(w, response.WebHandlerFunc(func() (response.Page, int) {
|
||||||
|
return newPage(*t), http.StatusOK
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
20
features/index/page.go
Normal file
20
features/index/page.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type index struct {
|
||||||
|
Template template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPage(t template.Template) *index {
|
||||||
|
return &index{
|
||||||
|
Template: *t.Lookup("index.html"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page index) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
16
features/index/routes.go
Normal file
16
features/index/routes.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(s features.Service, t *template.Template) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/", queryHandler(s, t))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
16
features/service.go
Normal file
16
features/service.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package features
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service/filter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Create(v interface{}) error
|
||||||
|
Query(v interface{}, f *filter.Filter) error
|
||||||
|
QueryOne(v interface{}, f *filter.Filter) error
|
||||||
|
Update(v interface{}, f *filter.Filter) error
|
||||||
|
Delete(v interface{}, f *filter.Filter) error
|
||||||
|
RegisterCollection(t reflect.Type, collection string)
|
||||||
|
}
|
24
features/weberror/page.go
Normal file
24
features/weberror/page.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package weberror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorPage struct {
|
||||||
|
Template template.Template
|
||||||
|
Error error
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPage(t template.Template, err error, statusCode int) *ErrorPage {
|
||||||
|
return &ErrorPage{
|
||||||
|
Template: *t.Lookup("error.html"),
|
||||||
|
Error: err,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (page ErrorPage) Send(w io.Writer) {
|
||||||
|
page.Template.Execute(w, page)
|
||||||
|
}
|
35
gui/template.go
Normal file
35
gui/template.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadTemplates() (*template.Template, error) {
|
||||||
|
return loadTemplates("templates/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates(path string) (*template.Template, error) {
|
||||||
|
dir, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ff []string
|
||||||
|
|
||||||
|
for _, file := range dir {
|
||||||
|
filename := file.Name()
|
||||||
|
if strings.HasSuffix(filename, ".html") {
|
||||||
|
ff = append(ff, fmt.Sprintf("%s%s", path, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates, err := template.ParseFiles(ff...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates, nil
|
||||||
|
}
|
45
ioutil/reader.go
Normal file
45
ioutil/reader.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package ioutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
validator "gopkg.in/go-playground/validator.v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Validate = validator.New()
|
||||||
|
|
||||||
|
func ExtractJSON(reader io.Reader, vv ...interface{}) error {
|
||||||
|
data, err := ioutil.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vv {
|
||||||
|
if err := json.Unmarshal(data, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractAndValidateJSON(reader io.Reader, vv ...interface{}) error {
|
||||||
|
data, err := ioutil.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range vv {
|
||||||
|
if err := json.Unmarshal(data, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Validate.Struct(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
47
main.go
47
main.go
|
@ -2,37 +2,35 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.cliffbreak.de/Cliffbreak/tsviewer/config"
|
"git.cliffbreak.de/haveachin/scoreboard/config"
|
||||||
"git.cliffbreak.de/Cliffbreak/tsviewer/service"
|
"git.cliffbreak.de/haveachin/scoreboard/features/game"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features/group"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/features/index"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/gui"
|
||||||
|
"git.cliffbreak.de/haveachin/scoreboard/service"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
"github.com/go-chi/cors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Routes(s *service.Service) *chi.Mux {
|
func Routes(s *service.DataService, t *template.Template) *chi.Mux {
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
cors := cors.New(cors.Options{
|
|
||||||
AllowedOrigins: []string{"*"},
|
|
||||||
AllowedMethods: []string{"Get"},
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Use(
|
router.Use(
|
||||||
middleware.Logger,
|
middleware.Logger,
|
||||||
middleware.Timeout(5*time.Second),
|
middleware.Timeout(5*time.Second),
|
||||||
middleware.DefaultCompress,
|
|
||||||
middleware.RedirectSlashes,
|
middleware.RedirectSlashes,
|
||||||
middleware.Recoverer,
|
middleware.Recoverer,
|
||||||
|
|
||||||
cors.Handler,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
router.Route("/", func(r chi.Router) {
|
router.Route("/", func(r chi.Router) {
|
||||||
//r.Mount("/channels", channel.APIRoutes(s))
|
r.Mount("/", index.Routes(s, t))
|
||||||
|
r.Mount("/groups", group.Routes(s, t))
|
||||||
|
r.Mount("/games", game.Routes(s, t))
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
@ -43,21 +41,28 @@ func main() {
|
||||||
config, err := config.New()
|
config, err := config.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
log.Println("Configurations loaded!")
|
log.Println("Configurations loaded!")
|
||||||
|
|
||||||
log.Println("Starting query service...")
|
log.Println("Starting query service...")
|
||||||
service, err := service.New(*config)
|
service, err := service.New(config.DataService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer service.TSClient.Close()
|
log.Println("Query service connected to", fmt.Sprintf("%s:%d!", config.DataService.IP, config.DataService.Port))
|
||||||
log.Println("Query service connected to", fmt.Sprintf("%s:%d!", config.ServerTS.IP, config.ServerTS.PortQuery))
|
|
||||||
|
|
||||||
router := Routes(service)
|
log.Println("Loading templates...")
|
||||||
|
templates, err := gui.LoadTemplates()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("All templates loaded!")
|
||||||
|
|
||||||
log.Println("Starting the web server locally on port", config.ServerWeb.Port)
|
router := Routes(service, templates)
|
||||||
log.Fatal("Handler: ", http.ListenAndServe(fmt.Sprintf(":%d", config.ServerWeb.Port), router))
|
router.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.StripPrefix("/static", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println("Starting the web server locally on port", config.Port)
|
||||||
|
log.Fatal("Handler: ", http.ListenAndServe(fmt.Sprintf(":%d", config.Port), router))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.cliffbreak.de/Cliffbreak/tsviewer/request/meta"
|
"git.cliffbreak.de/haveachin/scoreboard/request/meta"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandlerFunc func() (int, error)
|
type APIHandlerFunc func() (int, error)
|
||||||
|
|
||||||
// Responder is a service for responses
|
// Responder is a service for responses
|
||||||
type Responder interface {
|
type Responder interface {
|
||||||
|
@ -26,11 +26,9 @@ type Envelope struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Response
|
// New creates a new Response
|
||||||
func New(content interface{}, r *http.Request) *Response {
|
func New(content interface{}, meta *meta.Meta) *Response {
|
||||||
var resp Response
|
var resp Response
|
||||||
|
|
||||||
meta := meta.NewFromRequest(r)
|
|
||||||
|
|
||||||
if meta.Envelope {
|
if meta.Envelope {
|
||||||
resp = Response{
|
resp = Response{
|
||||||
Content: Envelope{
|
Content: Envelope{
|
||||||
|
@ -74,14 +72,14 @@ func (resp Response) Send(w http.ResponseWriter, status int) (int, error) {
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handler(w http.ResponseWriter, r *http.Request, hf HandlerFunc) {
|
func APIHandler(w http.ResponseWriter, hf APIHandlerFunc) {
|
||||||
status, err := hf()
|
status, err := hf()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("HTTP %d: %q", status, err)
|
log.Printf("HTTP %d: %q", status, err)
|
||||||
if status, err = New(NewError(status, err), r).Send(w, status); err != nil {
|
if status, err = NewError(status, err).Send(w, status); err != nil {
|
||||||
http.Error(w, http.StatusText(status), status)
|
http.Error(w, http.StatusText(status), status)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,10 +10,12 @@ type Error struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewError creates a new Response fill with an error
|
// NewError creates a new Response fill with an error
|
||||||
func NewError(status int, err error) *Error {
|
func NewError(status int, err error) *Response {
|
||||||
return &Error{
|
return &Response{
|
||||||
|
Content: Error{
|
||||||
Status: status,
|
Status: status,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
Timestamp: time.Now().Format("2006-01-02T15:04:05Z"), //.Format("02 Jan 2006, 15:04:05 MST"),
|
Timestamp: time.Now().Format("2006-01-02T15:04:05Z"), //.Format("02 Jan 2006, 15:04:05 MST"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
response/web.go
Normal file
19
response/web.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebHandlerFunc func() (Page, int)
|
||||||
|
|
||||||
|
type Page interface {
|
||||||
|
Send(w io.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebHandler(w http.ResponseWriter, hf WebHandlerFunc) {
|
||||||
|
page, status := hf()
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
page.Send(w)
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/haveachin/sawtexapi/service/mongodb/filter"
|
"git.cliffbreak.de/haveachin/scoreboard/service/filter"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,18 +25,10 @@ func (s DataService) Create(v interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s DataService) Query(v interface{}, filter *filter.Filter, multiQuery bool) error {
|
func (s DataService) Query(v interface{}, filter *filter.Filter) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if !multiQuery {
|
|
||||||
if err := s.collection(reflect.TypeOf(v)).FindOne(ctx, filter).Decode(v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor, err := s.collection(reflect.TypeOf(v)).Find(ctx, filter)
|
cursor, err := s.collection(reflect.TypeOf(v)).Find(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -45,6 +37,17 @@ func (s DataService) Query(v interface{}, filter *filter.Filter, multiQuery bool
|
||||||
return cursor.All(ctx, v)
|
return cursor.All(ctx, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s DataService) QueryOne(v interface{}, filter *filter.Filter) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := s.collection(reflect.TypeOf(v)).FindOne(ctx, filter).Decode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s DataService) Update(v interface{}, filter *filter.Filter) error {
|
func (s DataService) Update(v interface{}, filter *filter.Filter) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
40
service/filter/filter.go
Normal file
40
service/filter/filter.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter bson.M
|
||||||
|
|
||||||
|
func ParseFromRequest(r *http.Request) (*Filter, error) {
|
||||||
|
var filter Filter
|
||||||
|
|
||||||
|
if err := bson.UnmarshalExtJSON([]byte(chi.URLParam(r, "filter")), true, &filter); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *Filter) Set(key string, value interface{}) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &Filter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*filter)[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *Filter) Del(key string) {
|
||||||
|
if filter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := (*filter)[key]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(*filter, key)
|
||||||
|
}
|
18
service/id/id.go
Normal file
18
service/id/id.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package id
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseFromRequest(r *http.Request) (*primitive.ObjectID, error) {
|
||||||
|
id, err := primitive.ObjectIDFromHex(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &id, nil
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ func New(ds config.DataService) (*DataService, error) {
|
||||||
return &DataService{
|
return &DataService{
|
||||||
client: client,
|
client: client,
|
||||||
db: client.Database(ds.Database),
|
db: client.Database(ds.Database),
|
||||||
|
collections: map[reflect.Type]*mongo.Collection{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
static/styles.css
Executable file
22
static/styles.css
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
*{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2{
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
small{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.client{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
11
templates/error.html
Normal file
11
templates/error.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Error</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Oops, an error occurred!</h1>
|
||||||
|
<p1>Error {{.StatusCode}}</p1>
|
||||||
|
<p1>{{.Error}}</p1>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
templates/gameDetail.html
Normal file
24
templates/gameDetail.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Abistreiche</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/groups">Gruppen</a>
|
||||||
|
<h1>{{.Group.Name}}</h1>
|
||||||
|
<input type="text" placeholder="Search...">
|
||||||
|
<ul>
|
||||||
|
{{range .Group.Scores}}
|
||||||
|
<li>
|
||||||
|
{{$gameID := .GameID}}
|
||||||
|
{{range $.Games}}
|
||||||
|
{{if eq .ID $gameID}}
|
||||||
|
<a href="/games/{{$gameID}}">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<p>{{.Points}}</p>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
templates/gameMaster.html
Normal file
15
templates/gameMaster.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Abistreiche</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/">Abistreiche</a>
|
||||||
|
<input type="text" placeholder="Search...">
|
||||||
|
<ul>
|
||||||
|
{{range .Games}}
|
||||||
|
<li><a href="/games/{{.ID}}">{{.Name}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
templates/groupDetail.html
Normal file
15
templates/groupDetail.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Abistreiche</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/groups">Gruppen</a>
|
||||||
|
<h1>{{.Group.Name}}</h1>
|
||||||
|
<ul>
|
||||||
|
{{range .Group}}
|
||||||
|
<li><a href="/groups/{{.ID}}">{{.Name}}</a><span>{{.Points}}</span>/li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
templates/groupMaster.html
Normal file
16
templates/groupMaster.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Abistreiche</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/">Abistreiche</a>
|
||||||
|
<h1>Gruppen</h1>
|
||||||
|
<input type="text" placeholder="Search...">
|
||||||
|
<ul>
|
||||||
|
{{range .Groups}}
|
||||||
|
<li><a href="/groups/{{.ID}}">{{.Name}}</a><span>{{.PointSum}}</span></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
templates/index.html
Normal file
11
templates/index.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Abistreiche</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Abistreiche</h1>
|
||||||
|
<a href="/games">Spiele</a>
|
||||||
|
<a href="/groups">Gruppen</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in a new issue