Accept answers
This commit is contained in:
@@ -126,3 +126,33 @@ func (app *application) nextQuestion(w http.ResponseWriter, r *http.Request, par
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *application) answer(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
var playerUid uuid.UUID
|
||||||
|
if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil {
|
||||||
|
playerUid = uid
|
||||||
|
} else {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var choiceUid uuid.UUID
|
||||||
|
if uid, err := uuid.Parse(params.ByName("choiceUid")); err == nil {
|
||||||
|
choiceUid = uid
|
||||||
|
} else {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := app.model.SaveAnswer(playerUid, choiceUid, r.Context()); err == nil {
|
||||||
|
// TODO notify organisers
|
||||||
|
w.WriteHeader(http.StatusCreated) // TODO or StatusNoContent?
|
||||||
|
return
|
||||||
|
} else if errors.Is(err, model.NoSuchEntity) {
|
||||||
|
app.clientError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
app.serverError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func main() {
|
|||||||
mux.POST("/organise/:code", app.play)
|
mux.POST("/organise/:code", app.play)
|
||||||
mux.GET("/game/:playerUid", app.game)
|
mux.GET("/game/:playerUid", app.game)
|
||||||
mux.POST("/game/:playerUid/rpc/next", app.nextQuestion)
|
mux.POST("/game/:playerUid/rpc/next", app.nextQuestion)
|
||||||
|
mux.POST("/game/:playerUid/answers/:choiceUid", app.answer)
|
||||||
|
|
||||||
mux.GET("/ws/:playerUid", app.processWebSocket)
|
mux.GET("/ws/:playerUid", app.processWebSocket)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"time"
|
"time"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent"
|
"vkane.cz/tinyquiz/pkg/model/ent"
|
||||||
|
"vkane.cz/tinyquiz/pkg/model/ent/answer"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/askedquestion"
|
"vkane.cz/tinyquiz/pkg/model/ent/askedquestion"
|
||||||
|
"vkane.cz/tinyquiz/pkg/model/ent/choice"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/game"
|
"vkane.cz/tinyquiz/pkg/model/ent/game"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/player"
|
"vkane.cz/tinyquiz/pkg/model/ent/player"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/question"
|
"vkane.cz/tinyquiz/pkg/model/ent/question"
|
||||||
@@ -215,3 +217,51 @@ func (m *Model) NextQuestion(sessionId uuid.UUID, c context.Context) error {
|
|||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var QuestionClosed = errors.New("the deadline for answers to this question has passed")
|
||||||
|
var AlreadyAnswered = errors.New("the player has already answered the question")
|
||||||
|
|
||||||
|
func (m *Model) SaveAnswer(playerId uuid.UUID, choiceId uuid.UUID, c context.Context) (*ent.Answer, error) {
|
||||||
|
tx, err := m.c.BeginTx(c, &sql.TxOptions{
|
||||||
|
Isolation: sql.LevelSerializable,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// check whether the player could pick this choice
|
||||||
|
if exists, err := tx.Choice.Query().Where(choice.HasQuestionWith(question.HasGameWith(game.HasSessionsWith(session.HasPlayersWith(player.ID(playerId))))), choice.ID(choiceId)).Exist(c); err == nil && !exists {
|
||||||
|
return nil, NoSuchEntity
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var q *ent.Question
|
||||||
|
// find the most recent question
|
||||||
|
if q, err = tx.Player.Query().Where(player.ID(playerId)).QuerySession().QueryGame().QueryQuestions().Where(question.HasAsked()).WithAsked().Order(ent.Desc(question.FieldOrder)).First(c); ent.IsNotFound(err) {
|
||||||
|
return nil, NoSuchEntity
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the question is open
|
||||||
|
// Asked[0] is guaranteed to exist thanks to the previous query
|
||||||
|
if !q.Edges.Asked[0].Ended.After(time.Now()) {
|
||||||
|
return nil, QuestionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the player has not answered yet
|
||||||
|
if exists, err := tx.Answer.Query().Where(answer.HasAnswererWith(player.ID(playerId)), answer.HasChoiceWith(choice.HasQuestionWith(question.ID(q.ID)))).Exist(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
return nil, AlreadyAnswered
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, err := tx.Answer.Create().SetID(uuid.New()).SetAnswered(time.Now()).SetChoiceID(choiceId).SetAnswererID(playerId).Save(c); err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
return a, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const next = document.querySelector('#controls .next');
|
const next = document.querySelector('#controls .next');
|
||||||
next.addEventListener("click", () => {
|
next.addEventListener("click", () => {
|
||||||
let url = window.location.pathname + '/rpc/next';
|
const url = window.location.pathname + '/rpc/next';
|
||||||
fetch(url, {method: "POST"})
|
fetch(url, {method: "POST"})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.warn("Setting next question failed")
|
console.warn("Setting next question failed")
|
||||||
@@ -87,9 +87,24 @@
|
|||||||
const organiser = document.body.classList.contains('organiser');
|
const organiser = document.body.classList.contains('organiser');
|
||||||
for (const answer of data.question.answers) {
|
for (const answer of data.question.answers) {
|
||||||
const answerClone = answerTemplate.content.cloneNode(true);
|
const answerClone = answerTemplate.content.cloneNode(true);
|
||||||
answerClone.querySelector('.answer').innerText = answer.title;
|
const button = answerClone.querySelector('.answer');
|
||||||
|
button.innerText = answer.title;
|
||||||
|
button.dataset.id = answer.id;
|
||||||
if (organiser) {
|
if (organiser) {
|
||||||
answerClone.querySelector('.answer').disabled = true;
|
button.disabled = true;
|
||||||
|
} else {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const url = window.location.pathname + '/answers/' + encodeURIComponent(id);
|
||||||
|
fetch(url, {method: 'POST'})
|
||||||
|
.then(() => {
|
||||||
|
e.target.classList.add('selected');
|
||||||
|
for (const button of document.getElementsByClassName('answer')) {
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err)) // TODO proper error handling
|
||||||
|
});
|
||||||
}
|
}
|
||||||
answers.appendChild(answerClone);
|
answers.appendChild(answerClone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.answer.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 2px 1px 5px #0003;
|
||||||
|
}
|
||||||
|
|
||||||
/*body.organiser #question > .answers {
|
/*body.organiser #question > .answers {
|
||||||
display: none;
|
display: none;
|
||||||
}*/
|
}*/
|
||||||
|
|||||||
Reference in New Issue
Block a user