Can ask questions during live sessions
This commit is contained in:
parent
002b9e618b
commit
942875536b
105
asks.go
Normal file
105
asks.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
router.POST("/api/surveys/:sid/ask", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse {
|
||||||
|
var ask Ask
|
||||||
|
if err := json.Unmarshal(body, &ask); err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := s.NewAsk(u.Id, ask.Content)
|
||||||
|
if err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Direct != nil {
|
||||||
|
s.WSAdminWriteAll(WSMessage{Action: "new_ask", UserId: &u.Id, QuestionId: &a.Id, Response: ask.Content})
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatApiResponse(a, err)
|
||||||
|
}), loggedUser))
|
||||||
|
router.GET("/api/surveys/:sid/ask", apiHandler(surveyHandler(
|
||||||
|
func(s Survey, _ []byte) HTTPResponse {
|
||||||
|
return formatApiResponse(s.GetAsks(true))
|
||||||
|
}), adminRestricted))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ask struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IdSurvey int64 `json:"id_survey"`
|
||||||
|
IdUser int64 `json:"id_user"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Answered bool `json:"answered,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) GetAsks(unansweredonly bool) (asks []Ask, err error) {
|
||||||
|
cmp := ""
|
||||||
|
if unansweredonly {
|
||||||
|
cmp = " AND answered = 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows, errr := DBQuery("SELECT id_ask, id_survey, id_user, date, content, answered FROM survey_asks WHERE id_survey=?"+cmp, s.Id); errr != nil {
|
||||||
|
return nil, errr
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var a Ask
|
||||||
|
if err = rows.Scan(&a.Id, &a.IdSurvey, &a.IdUser, &a.Date, &a.Content, &a.Answered); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
asks = append(asks, a)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAsk(id int) (a Ask, err error) {
|
||||||
|
err = DBQueryRow("SELECT id_ask, id_survey, id_user, date, content, answered FROM survey_asks WHERE id_ask = ?", id).Scan(&a.Id, &a.IdSurvey, &a.IdUser, &a.Date, &a.Content, &a.Answered)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) NewAsk(id_user int64, content string) (Ask, error) {
|
||||||
|
if res, err := DBExec("INSERT INTO survey_asks (id_survey, id_user, date, content) VALUES (?, ?, ?, ?)", s.Id, id_user, time.Now(), content); err != nil {
|
||||||
|
return Ask{}, err
|
||||||
|
} else if aid, err := res.LastInsertId(); err != nil {
|
||||||
|
return Ask{}, err
|
||||||
|
} else {
|
||||||
|
return Ask{aid, s.Id, id_user, time.Now(), content, false}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Ask) Update() error {
|
||||||
|
_, err := DBExec("UPDATE survey_asks SET id_survey = ?, id_user = ?, date = ?, content = ?, answered = ? WHERE id_ask = ?", a.IdSurvey, a.IdUser, a.Date, a.Content, a.Answered, a.Id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Ask) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM survey_asks WHERE id_ask = ?", a.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearAsks() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM survey_asks"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
14
db.go
14
db.go
@ -153,6 +153,20 @@ CREATE TABLE IF NOT EXISTS student_corrected(
|
|||||||
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
||||||
FOREIGN KEY(id_template) REFERENCES correction_templates(id_template)
|
FOREIGN KEY(id_template) REFERENCES correction_templates(id_template)
|
||||||
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS survey_asks(
|
||||||
|
id_ask INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
id_survey INTEGER NOT NULL,
|
||||||
|
id_user INTEGER NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
||||||
|
FOREIGN KEY(id_survey) REFERENCES surveys(id_survey)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
20
direct.go
20
direct.go
@ -328,6 +328,26 @@ func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if v.Action == "get_asks" {
|
||||||
|
if survey, err := getSurvey(sid); err != nil {
|
||||||
|
log.Println("Unable to retrieve survey:", err)
|
||||||
|
} else if asks, err := survey.GetAsks(v.Response == ""); err != nil {
|
||||||
|
log.Println("Unable to retrieve asks:", err)
|
||||||
|
} else {
|
||||||
|
for _, a := range asks {
|
||||||
|
wsjson.Write(context.Background(), ws, WSMessage{Action: "new_ask", UserId: &a.IdUser, QuestionId: &a.Id, Response: a.Content})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v.Action == "mark_answered" && v.QuestionId != nil {
|
||||||
|
if asks, err := GetAsk(int(*v.QuestionId)); err != nil {
|
||||||
|
log.Println("Unable to retrieve ask:", err)
|
||||||
|
} else {
|
||||||
|
asks.Answered = true
|
||||||
|
err = asks.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to update:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Println("Unknown admin action:", v.Action)
|
log.Println("Unknown admin action:", v.Action)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
responsesbyid = tmp;
|
responsesbyid = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let asks = [];
|
||||||
function wsconnect() {
|
function wsconnect() {
|
||||||
if (ws !== null) return;
|
if (ws !== null) return;
|
||||||
|
|
||||||
@ -72,6 +73,7 @@
|
|||||||
ws_up = true;
|
ws_up = true;
|
||||||
ws.send('{"action":"get_responses"}');
|
ws.send('{"action":"get_responses"}');
|
||||||
ws.send('{"action":"get_stats"}');
|
ws.send('{"action":"get_stats"}');
|
||||||
|
ws.send('{"action":"get_asks"}');
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("close", (e) => {
|
ws.addEventListener("close", (e) => {
|
||||||
@ -99,6 +101,9 @@
|
|||||||
} else if (data.action && data.action == "new_response") {
|
} else if (data.action && data.action == "new_response") {
|
||||||
if (!responses[data.question]) responses[data.question] = {};
|
if (!responses[data.question]) responses[data.question] = {};
|
||||||
responses[data.question][data.user] = data.value;
|
responses[data.question][data.user] = data.value;
|
||||||
|
} else if (data.action && data.action == "new_ask") {
|
||||||
|
asks.push({"id": data.question, "content": data.value, "userid": data.user});
|
||||||
|
asks = asks;
|
||||||
} else {
|
} else {
|
||||||
current_question = null;
|
current_question = null;
|
||||||
}
|
}
|
||||||
@ -224,6 +229,67 @@
|
|||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_asks"}'); asks = []; }}
|
||||||
|
title="Rafraîchir les réponses"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-question-diamond"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-light ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
|
||||||
|
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-question-diamond"></i>
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
Questions
|
||||||
|
{#if asks.length}
|
||||||
|
<small class="text-muted">
|
||||||
|
{asks.length} question{#if asks.length > 1}s{/if}
|
||||||
|
</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
{#if asks.length}
|
||||||
|
{#each asks as ask (ask.id)}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
{ask.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success float-end"
|
||||||
|
title="Marqué comme répondu"
|
||||||
|
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
|
||||||
|
>
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
Par
|
||||||
|
<a href="users/{ask.userid}" target="_blank">
|
||||||
|
{#if users && users[ask.userid]}
|
||||||
|
{users[ask.userid].login}
|
||||||
|
{:else}
|
||||||
|
{ask.userid}
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
Pas de question pour l'instant.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -91,6 +91,36 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let myQuestion = "";
|
||||||
|
let submitQuestionInProgress = false;
|
||||||
|
function askQuestion() {
|
||||||
|
if (!myQuestion) {
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Quel est ta question ?",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitQuestionInProgress = true;
|
||||||
|
fetch(`api/surveys/${survey.id}/ask`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
body: JSON.stringify({"content": myQuestion}),
|
||||||
|
}).then((r) => {
|
||||||
|
submitQuestionInProgress = false;
|
||||||
|
myQuestion = "";
|
||||||
|
ToastsStore.addToast({
|
||||||
|
msg: "Ta question a bien été envoyée.",
|
||||||
|
title: survey.title,
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
}, (error) => {
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Un problème est survenu : " + error.errmsg,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await surveyP then survey}
|
{#await surveyP then survey}
|
||||||
@ -139,9 +169,36 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
{:else if ws_up}
|
{:else if ws_up}
|
||||||
<h2 class="text-center">
|
<h2 class="text-center mb-4">
|
||||||
Pas de question actuellement
|
Pas de question actuellement.
|
||||||
</h2>
|
</h2>
|
||||||
|
<form on:submit|preventDefault={askQuestion}>
|
||||||
|
<h3 class="text-center text-muted mb-3"><label for="askquestion">Vous avez une question ?</label></h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="offset-md-1 col-md-10 offset-lg-2 col-lg-8 offset-xl-3 col-xl-6">
|
||||||
|
<textarea
|
||||||
|
id="askquestion"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={myQuestion}
|
||||||
|
autofocus
|
||||||
|
placeholder="Remarques, soucis, choses pas claire ? Demandez !"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if myQuestion}
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
disabled={submitQuestionInProgress}
|
||||||
|
>
|
||||||
|
{#if submitQuestionInProgress}
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
||||||
|
{/if}
|
||||||
|
Poser cette question
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<h2 class="text-center">
|
<h2 class="text-center">
|
||||||
La session est terminée. <small class="text-muted">On se retrouve une prochaine fois…</small>
|
La session est terminée. <small class="text-muted">On se retrouve une prochaine fois…</small>
|
||||||
|
Reference in New Issue
Block a user