Can share survey results with a secret shared key
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
9fd73ce235
commit
fff8b821c5
1
api.go
1
api.go
|
@ -17,6 +17,7 @@ func declareAPIRoutes(router *gin.Engine) {
|
||||||
declareAPISurveysRoutes(apiRoutes)
|
declareAPISurveysRoutes(apiRoutes)
|
||||||
declareAPIWorksRoutes(apiRoutes)
|
declareAPIWorksRoutes(apiRoutes)
|
||||||
declareAPIKeysRoutes(apiRoutes)
|
declareAPIKeysRoutes(apiRoutes)
|
||||||
|
declareAPISharesRoutes(apiRoutes)
|
||||||
declareCallbacksRoutes(apiRoutes)
|
declareCallbacksRoutes(apiRoutes)
|
||||||
|
|
||||||
authRoutes := router.Group("")
|
authRoutes := router.Group("")
|
||||||
|
|
11
db.go
11
db.go
|
@ -255,6 +255,17 @@ CREATE TABLE IF NOT EXISTS categories(
|
||||||
promo MEDIUMINT NOT NULL,
|
promo MEDIUMINT NOT NULL,
|
||||||
expand BOOLEAN NOT NULL DEFAULT FALSE
|
expand BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
) 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_shared(
|
||||||
|
id_share INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
id_survey INTEGER NOT NULL,
|
||||||
|
secret BLOB NOT NULL,
|
||||||
|
count MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(id_survey) REFERENCES surveys(id_survey)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func declareAPISharesRoutes(router *gin.RouterGroup) {
|
||||||
|
surveysRoutes := router.Group("/s/surveys/:sid")
|
||||||
|
surveysRoutes.Use(surveyHandler)
|
||||||
|
surveysRoutes.Use(sharesAccessHandler)
|
||||||
|
|
||||||
|
surveysRoutes.GET("/", func(c *gin.Context) {
|
||||||
|
share := c.MustGet("survey_share").(*SurveyShared)
|
||||||
|
share.Count += 1
|
||||||
|
share.Update()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, c.MustGet("survey").(*Survey))
|
||||||
|
})
|
||||||
|
|
||||||
|
surveysRoutes.GET("/questions", func(c *gin.Context) {
|
||||||
|
s := c.MustGet("survey").(*Survey)
|
||||||
|
|
||||||
|
if questions, err := s.GetQuestions(); err != nil {
|
||||||
|
log.Println("Unable to getQuestions:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve questions. Please try again later."})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, questions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
questionsRoutes := surveysRoutes.Group("/questions/:qid")
|
||||||
|
questionsRoutes.Use(questionHandler)
|
||||||
|
|
||||||
|
questionsRoutes.GET("/", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, c.MustGet("question").(*Question))
|
||||||
|
})
|
||||||
|
|
||||||
|
questionsRoutes.GET("/proposals", func(c *gin.Context) {
|
||||||
|
q := c.MustGet("question").(*Question)
|
||||||
|
|
||||||
|
proposals, err := q.GetProposals()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to GetProposals(qid=%d): %s", q.Id, err.Error())
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during proposals retrieving"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, proposals)
|
||||||
|
})
|
||||||
|
|
||||||
|
questionsRoutes.GET("/responses", func(c *gin.Context) {
|
||||||
|
q := c.MustGet("question").(*Question)
|
||||||
|
|
||||||
|
res, err := q.GetResponses()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to GetResponses(qid=%d): %s", q.Id, err.Error())
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during responses retrieval."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sharesAccessHandler(c *gin.Context) {
|
||||||
|
s := c.MustGet("survey").(*Survey)
|
||||||
|
secret := c.Query("secret")
|
||||||
|
|
||||||
|
shares, err := s.getShares()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to getShares(sid=%d): %s", s.Id, err.Error())
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when authenticating the query."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, share := range shares {
|
||||||
|
if share.Authenticate(secret) {
|
||||||
|
c.Set("survey_share", share)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Not authorized"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type SurveyShared struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IdSurvey int64 `json:"id_survey"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
secret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) Share() (*SurveyShared, error) {
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := DBExec("INSERT INTO survey_shared (id_survey, secret) VALUES (?, ?)", s.Id, secret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if sid, err := res.LastInsertId(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &SurveyShared{sid, s.Id, 0, secret}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *SurveyShared) getMAC() []byte {
|
||||||
|
mac := hmac.New(sha512.New, sh.secret)
|
||||||
|
mac.Write([]byte(fmt.Sprintf("%d", sh.IdSurvey)))
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *SurveyShared) GetURL() (*url.URL, error) {
|
||||||
|
u, err := url.Parse(oidcRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Path = filepath.Join(baseURL, "results")
|
||||||
|
u.RawQuery = url.Values{
|
||||||
|
"secret": []string{base64.RawURLEncoding.EncodeToString(sh.getMAC())},
|
||||||
|
"survey": []string{fmt.Sprintf("%d", sh.IdSurvey)},
|
||||||
|
}.Encode()
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) getShares() (shares []*SurveyShared, err error) {
|
||||||
|
if rows, errr := DBQuery("SELECT id_share, id_survey, secret, count FROM survey_shared"); errr != nil {
|
||||||
|
return nil, errr
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var sh SurveyShared
|
||||||
|
if err = rows.Scan(&sh.Id, &sh.IdSurvey, &sh.secret, &sh.Count); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shares = append(shares, &sh)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *SurveyShared) Authenticate(secret string) bool {
|
||||||
|
messageMAC, err := base64.RawURLEncoding.DecodeString(secret)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hmac.Equal(messageMAC, sh.getMAC())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *SurveyShared) Update() (*SurveyShared, error) {
|
||||||
|
if _, err := DBExec("UPDATE survey_shared SET id_survey = ?, secret = ?, count = ? WHERE id_share = ?", sh.IdSurvey, sh.secret, sh.Count, sh.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return sh, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh SurveyShared) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM survey_shared WHERE id_share = ?", sh.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,6 +59,7 @@ func declareStaticRoutes(router *gin.Engine) {
|
||||||
router.GET("/grades", serveOrReverse("/"))
|
router.GET("/grades", serveOrReverse("/"))
|
||||||
router.GET("/help", serveOrReverse("/"))
|
router.GET("/help", serveOrReverse("/"))
|
||||||
router.GET("/keys", serveOrReverse("/"))
|
router.GET("/keys", serveOrReverse("/"))
|
||||||
|
router.GET("/results", serveOrReverse("/"))
|
||||||
router.GET("/surveys", serveOrReverse("/"))
|
router.GET("/surveys", serveOrReverse("/"))
|
||||||
router.GET("/surveys/*_", serveOrReverse("/"))
|
router.GET("/surveys/*_", serveOrReverse("/"))
|
||||||
router.GET("/users", serveOrReverse("/"))
|
router.GET("/users", serveOrReverse("/"))
|
||||||
|
|
28
surveys.go
28
surveys.go
|
@ -204,6 +204,33 @@ func declareAPIAdminSurveysRoutes(router *gin.RouterGroup) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
surveysRoutes.GET("shares", func(c *gin.Context) {
|
||||||
|
survey := c.MustGet("survey").(*Survey)
|
||||||
|
|
||||||
|
if sh, err := survey.getShares(); err != nil {
|
||||||
|
log.Println("Unable to getShares survey:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey shares listing: %s", err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, sh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
surveysRoutes.POST("shares", func(c *gin.Context) {
|
||||||
|
survey := c.MustGet("survey").(*Survey)
|
||||||
|
|
||||||
|
if sh, err := survey.Share(); err != nil {
|
||||||
|
log.Println("Unable to Share survey:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey sharing: %s", err.Error())})
|
||||||
|
return
|
||||||
|
} else if url, err := sh.GetURL(); err != nil {
|
||||||
|
log.Println("Unable to GetURL share:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during survey sharing: %s", err.Error())})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, url.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
declareAPIAdminAsksRoutes(surveysRoutes)
|
declareAPIAdminAsksRoutes(surveysRoutes)
|
||||||
declareAPIAdminDirectRoutes(surveysRoutes)
|
declareAPIAdminDirectRoutes(surveysRoutes)
|
||||||
declareAPIAdminQuestionsRoutes(surveysRoutes)
|
declareAPIAdminQuestionsRoutes(surveysRoutes)
|
||||||
|
@ -364,6 +391,7 @@ func (s *Survey) Update() (*Survey, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Survey) Delete() (int64, error) {
|
func (s Survey) Delete() (int64, error) {
|
||||||
|
DBExec("DELETE FROM survey_shared WHERE id_survey = ?", s.Id)
|
||||||
if res, err := DBExec("DELETE FROM surveys WHERE id_survey = ?", s.Id); err != nil {
|
if res, err := DBExec("DELETE FROM surveys WHERE id_survey = ?", s.Id); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
} else if nb, err := res.RowsAffected(); err != nil {
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
export let question = null;
|
export let question = null;
|
||||||
|
|
||||||
function refreshProposals() {
|
function refreshProposals() {
|
||||||
let req = question.getProposals();
|
let req = question.getProposals(secret);
|
||||||
|
|
||||||
req.then((proposals) => {
|
req.then((proposals) => {
|
||||||
const proposal_idx = { };
|
const proposal_idx = { };
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
proposal_idx[proposal.id] = new String(data.labels.length - 1);
|
proposal_idx[proposal.id] = new String(data.labels.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
req_responses = question.getResponses();
|
req_responses = question.getResponses(secret);
|
||||||
req_responses.then((responses) => {
|
req_responses.then((responses) => {
|
||||||
for (const res of responses) {
|
for (const res of responses) {
|
||||||
const rsplt = res.value.split(',');
|
const rsplt = res.value.split(',');
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
}
|
}
|
||||||
let req_proposals = null;
|
let req_proposals = null;
|
||||||
export let proposals = null;
|
export let proposals = null;
|
||||||
|
export let secret = null;
|
||||||
let req_responses = null;
|
let req_responses = null;
|
||||||
let mean = null;
|
let mean = null;
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@
|
||||||
|
|
||||||
if (!proposals) {
|
if (!proposals) {
|
||||||
if (question.kind && (question.kind == "int" || question.kind.startsWith("list"))) {
|
if (question.kind && (question.kind == "int" || question.kind.startsWith("list"))) {
|
||||||
req_responses = question.getResponses();
|
req_responses = question.getResponses(secret);
|
||||||
req_responses.then((responses) => {
|
req_responses.then((responses) => {
|
||||||
const values = [];
|
const values = [];
|
||||||
const proposal_idx = { };
|
const proposal_idx = { };
|
||||||
|
|
|
@ -62,8 +62,10 @@ export class Question {
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProposals() {
|
async getProposals(secret) {
|
||||||
const res = await fetch(`api/questions/${this.id}/proposals`, {
|
let url = `/questions/${this.id}/proposals`;
|
||||||
|
if (secret) url = `/s/surveys/${this.id_survey}` + url + `?secret=${secret}`;
|
||||||
|
const res = await fetch('api' + url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
});
|
});
|
||||||
|
@ -91,8 +93,10 @@ export class Question {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResponses() {
|
async getResponses(secret) {
|
||||||
const res = await fetch(`api/surveys/${this.id_survey}/questions/${this.id}/responses`, {
|
let url = `/surveys/${this.id_survey}/questions/${this.id}/responses`;
|
||||||
|
if (secret) url = `/s` + url + `?secret=${secret}`;
|
||||||
|
const res = await fetch('api' + url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
});
|
});
|
||||||
|
@ -161,3 +165,17 @@ export async function getQuestions(sid) {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSharedQuestions(sid, secret) {
|
||||||
|
const res = await fetch(`api/s/surveys/${sid}/questions?secret=${secret}`, {headers: {'Accept': 'application/json'}})
|
||||||
|
if (res.status == 200) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data === null) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return (data).map((q) => new Question(q))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error((await res.json()).errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -73,6 +73,18 @@ export class Survey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async share() {
|
||||||
|
const res = await fetch(`api/surveys/${this.id}/shares`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Accept': 'application/json'}
|
||||||
|
});
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error((await res.json()).errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
const res = await fetch(this.id?`api/surveys/${this.id}`:'api/surveys', {
|
const res = await fetch(this.id?`api/surveys/${this.id}`:'api/surveys', {
|
||||||
method: this.id?'PUT':'POST',
|
method: this.id?'PUT':'POST',
|
||||||
|
@ -190,3 +202,12 @@ export async function getSurvey(sid) {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSharedSurvey(sid, secret) {
|
||||||
|
const res = await fetch(`api/s/surveys/${sid}?secret=${secret}`, {headers: {'Accept': 'application/json'}})
|
||||||
|
if (res.status == 200) {
|
||||||
|
return new Survey(await res.json());
|
||||||
|
} else {
|
||||||
|
throw new Error((await res.json()).errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script context="module">
|
||||||
|
export async function load({ url }) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
secret: url.searchParams.get("secret"),
|
||||||
|
idsurvey: url.searchParams.get("survey"),
|
||||||
|
exportview_list: url.searchParams.get("graph_list")?false:true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getSharedQuestions } from '$lib/questions';
|
||||||
|
import { getSharedSurvey } from '$lib/surveys';
|
||||||
|
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
|
||||||
|
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
|
||||||
|
|
||||||
|
export let secret;
|
||||||
|
export let idsurvey;
|
||||||
|
|
||||||
|
let surveyP = getSharedSurvey(idsurvey, secret);
|
||||||
|
export let exportview_list = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await surveyP then survey}
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<h2>
|
||||||
|
{survey.title}
|
||||||
|
<small class="text-muted">Réponses</small>
|
||||||
|
</h2>
|
||||||
|
<SurveyBadge class="ms-2" {survey} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#await getSharedQuestions(survey.id, secret)}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des questions …</span>
|
||||||
|
</div>
|
||||||
|
{:then questions}
|
||||||
|
{#each questions as question (question.id)}
|
||||||
|
<h3>{question.title}</h3>
|
||||||
|
{#if question.kind == "text" || (exportview_list && question.kind.indexOf("list") == 0)}
|
||||||
|
{#await question.getResponses(secret) then responses}
|
||||||
|
{#each responses as response (response.id)}
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text" style:white-space="pre-line">
|
||||||
|
{response.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<CorrectionPieChart {question} {secret} />
|
||||||
|
{/if}
|
||||||
|
<hr class="mb-3">
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
{/await}
|
|
@ -30,6 +30,15 @@
|
||||||
let edit = false;
|
let edit = false;
|
||||||
let exportview = false;
|
let exportview = false;
|
||||||
let exportview_list = false;
|
let exportview_list = false;
|
||||||
|
|
||||||
|
let sharing_link = null;
|
||||||
|
async function shareResults(survey) {
|
||||||
|
const res = await survey.share();
|
||||||
|
sharing_link = res;
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('shareModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await surveyP then survey}
|
{#await surveyP then survey}
|
||||||
|
@ -40,6 +49,11 @@
|
||||||
class="ms-1 float-end"
|
class="ms-1 float-end"
|
||||||
on:update={() => goto(`surveys/${survey.id}/admin`)}
|
on:update={() => goto(`surveys/${survey.id}/admin`)}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-dark ms-1 float-end"
|
||||||
|
title="Partager les résultats"
|
||||||
|
on:click={() => shareResults(survey)}
|
||||||
|
><i class="bi bi-share-fill"></i></button>
|
||||||
<button
|
<button
|
||||||
class="btn ms-1 float-end"
|
class="btn ms-1 float-end"
|
||||||
class:btn-dark={exportview}
|
class:btn-dark={exportview}
|
||||||
|
@ -144,3 +158,20 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
|
<div class="modal fade" tabindex="-1" id="shareModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Partage de résultats</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
Voici le lien de partage des résultats de ce sondage :
|
||||||
|
</p>
|
||||||
|
<pre>{sharing_link}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
Reference in New Issue