From 688f87922d867de9f9ee1e3ff8d7177219f180bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20K=C3=A1n=C4=9B?= Date: Thu, 1 Apr 2021 16:36:38 +0200 Subject: [PATCH] Display results --- cmd/web/handlers.go | 38 ++++++++++++++++++ cmd/web/main.go | 1 + pkg/model/model.go | 70 ++++++++++++++++++++++++++++++++++ pkg/rtcomm/stateUpdate.go | 13 ++++--- ui/html/game.page.tmpl.html | 4 ++ ui/html/results.page.tmpl.html | 22 +++++++++++ 6 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 ui/html/results.page.tmpl.html diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index f02b7e6..d067b01 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -10,6 +10,7 @@ import ( "time" "vkane.cz/tinyquiz/pkg/model" "vkane.cz/tinyquiz/pkg/model/ent" + "vkane.cz/tinyquiz/pkg/rtcomm" ) func (app *application) home(w http.ResponseWriter, r *http.Request, params httprouter.Params) { @@ -148,6 +149,10 @@ func (app *application) nextQuestion(w http.ResponseWriter, r *http.Request, par app.serverError(w, err) return } + } else if errors.Is(err, model.NoNextQuestion) { + app.rtClients.SendToAll(sessionId, rtcomm.StateUpdate{Results: true}) + w.WriteHeader(http.StatusNoContent) + return } else { app.serverError(w, err) return @@ -190,3 +195,36 @@ func (app *application) answer(w http.ResponseWriter, r *http.Request, params ht return } } + +func (app *application) resultsGeneral(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + type resultsData struct { + Results []model.PlayerResult + Session *ent.Session + Player *ent.Player + templateData + } + td := &resultsData{} + setDefaultTemplateData(&td.templateData) + + var playerUid uuid.UUID + if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil { + playerUid = uid + } else { + app.clientError(w, http.StatusBadRequest) + return + } + + if results, session, player, err := app.model.GetResults(playerUid, r.Context()); err == nil { + td.Results = results + td.Session = session + td.Player = player + } else if errors.Is(err, model.NoSuchEntity) { + app.clientError(w, http.StatusNotFound) + return + } else { + app.serverError(w, err) + return + } + + app.render(w, r, "results.page.tmpl.html", td) +} diff --git a/cmd/web/main.go b/cmd/web/main.go index fd6ea66..bfc41d5 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -70,6 +70,7 @@ func main() { mux.GET("/game/:playerUid", app.game) mux.POST("/game/:playerUid/rpc/next", app.nextQuestion) mux.POST("/game/:playerUid/answers/:choiceUid", app.answer) + mux.GET("/results/:playerUid", app.resultsGeneral) mux.GET("/ws/:playerUid", app.processWebSocket) diff --git a/pkg/model/model.go b/pkg/model/model.go index ef0a569..387de9e 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -7,6 +7,7 @@ import ( "database/sql" "errors" "github.com/google/uuid" + "sort" "time" "vkane.cz/tinyquiz/pkg/codeGenerator" "vkane.cz/tinyquiz/pkg/model/ent" @@ -305,6 +306,75 @@ func (m *Model) SaveAnswer(playerId uuid.UUID, choiceId uuid.UUID, now time.Time } } +type PlayerResult struct { + Player *ent.Player + place uint64 + correct int64 +} + +func (r PlayerResult) Points() int64 { + return r.correct +} + +func (r PlayerResult) Place() uint64 { + return r.place +} + +func (m *Model) GetResults(playerId uuid.UUID, c context.Context) ([]PlayerResult, *ent.Session, *ent.Player, error) { + tx, err := m.c.BeginTx(c, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: true, + }) + if err != nil { + return nil, nil, nil, err + } + defer tx.Commit() + + s, err := tx.Session.Query().WithGame().Where(session.HasPlayersWith(player.ID(playerId))).Only(c) + if ent.IsNotFound(err) { + return nil, nil, nil, NoSuchEntity + } else if err != nil { + return nil, nil, nil, err + } + + p, err := tx.Player.Query().Where(player.ID(playerId)).Only(c) + if ent.IsNotFound(err) { + return nil, nil, nil, NoSuchEntity + } else if err != nil { + return nil, nil, nil, err + } + + if players, err := tx.Player.Query().Where(player.HasSessionWith(session.ID(s.ID))).Where(player.Organiser(false)).Order(ent.Asc(player.FieldName)).WithAnswers(func(q *ent.AnswerQuery) { q.WithChoice() }).All(c); err == nil { + var results = make([]PlayerResult, 0, len(players)) + for _, p := range players { + var res PlayerResult + res.Player = p + for _, a := range p.Edges.Answers { + if a.Edges.Choice.Correct { + res.correct++ + } + } + results = append(results, res) + } + sort.SliceStable(results, func(i, j int) bool { return results[i].correct > results[j].correct }) // sort in reverse + if len(results) > 0 { + results[0].place = 1 + } + var place uint64 = 2 + for i := 1; i < len(results); i++ { + if results[i].Points() == results[i-1].Points() { + results[i].place = results[i-1].place + } else { + results[i].place = place + } + place++ + } + return results, s, p, nil + } else { + return nil, nil, nil, err + } +} + const codeRandomPartLength uint8 = 3 func (m *Model) getCodeIncremental(c context.Context) (uint64, error) { diff --git a/pkg/rtcomm/stateUpdate.go b/pkg/rtcomm/stateUpdate.go index c119e2e..82d3529 100644 --- a/pkg/rtcomm/stateUpdate.go +++ b/pkg/rtcomm/stateUpdate.go @@ -1,21 +1,22 @@ package rtcomm type StateUpdate struct { - Players []Player `json:"players"` + Players []Player `json:"players,omitempty"` Question *QuestionUpdate `json:"question,omitempty"` + Results bool `json:"results,omitempty"` } type Player struct { - Organiser bool `json:"organiser"` - Name string `json:"name"` + Organiser bool `json:"organiser"` + Name string `json:"name"` } type QuestionUpdate struct { - Title string `json:"title"` + Title string `json:"title"` Answers []Answer `json:"answers"` } -type Answer struct{ - ID string `json:"id"` +type Answer struct { + ID string `json:"id"` Title string `json:"title"` } diff --git a/ui/html/game.page.tmpl.html b/ui/html/game.page.tmpl.html index b326502..5f0e21d 100644 --- a/ui/html/game.page.tmpl.html +++ b/ui/html/game.page.tmpl.html @@ -111,6 +111,10 @@ questionSection.appendChild(questionClone); } } + + if ('results' in data && data.results === true) { + window.location.pathname = "/results/" + encodeURIComponent(playerId); + } }); {{ end -}} diff --git a/ui/html/results.page.tmpl.html b/ui/html/results.page.tmpl.html new file mode 100644 index 0000000..76f20f1 --- /dev/null +++ b/ui/html/results.page.tmpl.html @@ -0,0 +1,22 @@ +{{- template "base" . -}} + +{{- define "additional-css" -}} +{{ end -}} + +{{- define "additional-js" -}} +{{ end -}} + +{{- define "header" }} +

{{ .Session.Edges.Game.Name }} hrána {{ .Session.Started.Format "2006.01.02 15:04:05 MST" }}

+

{{ if .Player.Organiser }}Organizátor{{ else }}Hráč{{ end }} {{ .Player.Name }}

+{{ end -}} + +{{- define "main" }} + + + {{ range .Results }} + + {{ end }} + +
{{ .Place }}. místo{{ .Player.Name }}{{ .Points }}b
+{{ end -}}