Added informations about forms filled and being filled up

This commit is contained in:
Samy Avrillon 2025-06-14 15:24:56 +02:00
parent 08262f3dfd
commit d411d41cff
Signed by: Mysaa
GPG Key ID: 0220AC4A3D6A328B
15 changed files with 441 additions and 44 deletions

View File

@ -2,9 +2,8 @@ package com.bernard.misael.model;
import com.bernard.misael.questions.QTypes;
import com.bernard.misael.questions.QuestionType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.bernard.misael.service.JsonNodeConverter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.*;
import lombok.Getter;
@ -36,19 +35,13 @@ public class Question {
private Quizz quizz;
@Setter
private String value;
@Convert(converter = JsonNodeConverter.class)
private JsonNode value;
transient QuestionType qtype = null;
public QuestionType getQT() {
if(qtype==null){
ObjectMapper om = new ObjectMapper();
JsonNode jsNode;
try {
jsNode = om.readTree(value);
qtype = type.construct(jsNode);
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
qtype = type.construct(value);
}
return qtype;
}

View File

@ -1,5 +1,7 @@
package com.bernard.misael.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Answer;
@ -9,5 +11,6 @@ import com.bernard.misael.model.QuizzForm;
public interface AnswerRepository extends JpaRepository<Answer,Long> {
public Answer findByFormAndQuestion(QuizzForm qf, Question q);
public List<Answer> findByFormAndQuestionIn(QuizzForm qf, List<Question> qz);
}

View File

@ -1,5 +1,6 @@
package com.bernard.misael.repository;
import java.util.List;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
@ -12,5 +13,6 @@ public interface QuestionRepository extends JpaRepository<Question,Long> {
public Question findByQuizzAndIndex(Quizz quizz, int index);
public Set<Question> findByQuizz(Quizz quizz);
public List<Question> findByQuizzOrderByIndexAsc(Quizz quizz);
}

View File

@ -1,5 +1,7 @@
package com.bernard.misael.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Quizz;
@ -8,6 +10,10 @@ import com.bernard.misael.model.User;
public interface QuizzFormRepository extends JpaRepository<QuizzForm,Long> {
public List<QuizzForm> findByQuizz(Quizz q);
public QuizzForm findByUserAndQuizz(User u, Quizz q);
public List<QuizzForm> findByUserAndDoneTrue(User u);
public List<QuizzForm> findByUserAndDoneFalse(User u);
}

View File

@ -1,8 +1,10 @@
package com.bernard.misael.service;
import java.util.List;
import java.util.Optional;
import com.bernard.misael.model.Quizz;
import com.bernard.misael.model.QuizzForm;
import com.bernard.misael.model.User;
import com.bernard.misael.questions.QTypes;
import com.fasterxml.jackson.databind.JsonNode;
@ -19,6 +21,8 @@ public interface QuizzManager {
public List<Quizz> answerableQuizz(User user);
public boolean canEditQuizz(User user, long quizzId);
public Optional<QuizzForm> canViewQuizzForm(User user, long quizzFormId);
public Optional<Quizz> canViewQuizzFormsOfQuizz(User user, long quizzId);
public JsonNode getQuizzInfo(User user, long quizzId);
public JsonNode setQuizzName(User user, long quizzId, String newName);
@ -28,4 +32,7 @@ public interface QuizzManager {
public JsonNode editQuestion(User user, long quizzId, long questionId, JsonNode value);
public JsonNode setQuestionType(User user, long quizzId, long questionId, QTypes type);
public JsonNode getQuizzFormData(User user, long quizzFormId);
public JsonNode getQuizzFormAdvancments(User user, long quizzId);
}

View File

@ -14,16 +14,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bernard.misael.model.Answer;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.Question;
import com.bernard.misael.model.Quizz;
import com.bernard.misael.model.QuizzForm;
import com.bernard.misael.model.User;
import com.bernard.misael.questions.QTypes;
import com.bernard.misael.questions.QuestionType.AnswerResult;
import com.bernard.misael.repository.AnswerRepository;
import com.bernard.misael.repository.QuestionRepository;
import com.bernard.misael.repository.QuizzFormRepository;
import com.bernard.misael.repository.QuizzRepository;
import com.bernard.misael.repository.*;
import com.bernard.misael.service.exception.MalformedAnswerException;
import com.bernard.misael.service.exception.MalformedClientAnswerException;
import com.bernard.misael.service.exception.QuestionTypeException;
@ -40,6 +38,9 @@ import jakarta.persistence.EntityNotFoundException;
@Service
public class QuizzManagerImpl implements QuizzManager {
@Autowired
UserRepository uRepository;
@Autowired
QuizzFormRepository qfRepository;
@ -52,6 +53,11 @@ public class QuizzManagerImpl implements QuizzManager {
@Autowired
AnswerRepository answerRepository;
@Autowired
UserService uService;
@Override
public JsonNode answer(User user, long quizzId, JsonNode data) {
if(!data.has("index") || !data.get("index").isInt())
@ -268,12 +274,7 @@ public class QuizzManagerImpl implements QuizzManager {
ObjectNode nn = JsonNodeFactory.instance.objectNode();
nn.set("id",JsonNodeFactory.instance.numberNode(q.getId()));
nn.set("type", JsonNodeFactory.instance.textNode(q.getType().name()));
ObjectMapper om = new ObjectMapper();
try {
nn.set("value",om.readTree(q.getValue()));
} catch (JsonProcessingException e) {
throw new IllegalStateException("Value stored for question "+q.getId()+" is illegal (non-json)",e);
}
nn.set("value",q.getValue());
n.set(q.getIndex(),nn);
}
@ -312,7 +313,7 @@ public class QuizzManagerImpl implements QuizzManager {
Question q = new Question();
q.setType(DEFAULT_QTYPE);
JsonNode n = DEFAULT_QTYPE.getDefaultQuestion();
q.setValue(n.toString());
q.setValue(n);
q.setQuizz(quizz);
q.setIndex(quizz.getQuestionCount());
quizz.setQuestionCount(quizz.getQuestionCount()+1);
@ -408,7 +409,7 @@ public class QuizzManagerImpl implements QuizzManager {
if(!q.getType().validate(value))
return errorNode("Invalid question value");
q.setValue(value.toString());
q.setValue(value);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
@ -438,14 +439,9 @@ public class QuizzManagerImpl implements QuizzManager {
// Then we need to change
q.setType(type);
n = type.getDefaultQuestion();
q.setValue(n.toString());
q.setValue(n);
} else {
ObjectMapper om = new ObjectMapper();
try {
n = om.readTree(q.getValue());
} catch (JsonProcessingException e) {
throw new IllegalStateException("Value stored for question "+questionId+" is illegal (non-json)",e);
}
n =q.getValue();
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
@ -454,4 +450,77 @@ public class QuizzManagerImpl implements QuizzManager {
return out;
}
@Override
public Optional<QuizzForm> canViewQuizzForm(User user, long quizzFormId) {
Optional<QuizzForm> oqf = qfRepository.findById(quizzFormId);
if(oqf.isEmpty()) return oqf;
QuizzForm qf = oqf.get();
if(!qf.isDone()) return Optional.empty();
if(qf.getUser().equals(user)) return oqf;
if(uService.hasPrivilege(user, Privilege.VIEW_ALL_FORMS)) return oqf;
return Optional.empty();
}
@Override
public Optional<Quizz> canViewQuizzFormsOfQuizz(User user, long quizzId) {
Optional<Quizz> oq = qRepository.findById(quizzId);
if(oq.isEmpty()) return oq;
Quizz q = oq.get();
if(q.getOwner().equals(user)) return oq;
if(uService.hasPrivilege(user, Privilege.VIEW_ALL_FORMS)) return oq;
return Optional.empty();
}
@Override
public JsonNode getQuizzFormData(User user, long quizzFormId) {
Optional<QuizzForm> oqf = canViewQuizzForm(user, quizzFormId);
if(oqf.isEmpty())
return errorNode("Could not access the quizzform"); //TODO more precise error node
QuizzForm form = oqf.get();
List<Question> questions = questionRepository.findByQuizzOrderByIndexAsc(form.getQuizz());
List<Answer> answers = answerRepository.findByFormAndQuestionIn(form, questions);
assert questions.size() == answers.size();
ArrayNode answersNode = JsonNodeFactory.instance.arrayNode();
for(int i=0;i<questions.size();i++) {
ObjectNode anode = JsonNodeFactory.instance.objectNode();
anode.set("qid", JsonNodeFactory.instance.numberNode(questions.get(i).getId()));
anode.set("type", JsonNodeFactory.instance.textNode(questions.get(i).getType().name()));
anode.set("qvalue", questions.get(i).getValue());
anode.set("aid", JsonNodeFactory.instance.numberNode(answers.get(i).getId()));
anode.set("avalue", answers.get(i).getValue());
answersNode.add(anode);
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("data", answersNode);
return out;
}
@Override
public JsonNode getQuizzFormAdvancments(User user, long quizzId) {
Optional<Quizz> oq = canViewQuizzFormsOfQuizz(user, quizzId);
if(oq.isEmpty())
return errorNode("Could not access the forms for this quizz"); //TODO more precise error node
Quizz quizz = oq.get();
List<QuizzForm> quizzForms = qfRepository.findByQuizz(quizz);
quizzForms.sort((qfa,qfb) -> qfa.getUser().getName().compareTo(qfb.getUser().getName()));
ArrayNode formsNode = JsonNodeFactory.instance.arrayNode();
for(QuizzForm qf : quizzForms) {
ObjectNode anode = JsonNodeFactory.instance.objectNode();
anode.set("qfid", JsonNodeFactory.instance.numberNode(qf.getId()));
anode.set("done", JsonNodeFactory.instance.booleanNode(qf.isDone()));
anode.set("position", JsonNodeFactory.instance.numberNode(qf.getCurrentQuestion()));
anode.set("step", JsonNodeFactory.instance.numberNode(qf.getAnswerStep()));
anode.set("username", JsonNodeFactory.instance.textNode(qf.getUser().getName()));
formsNode.add(anode);
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("data", formsNode);
return out;
}
}

View File

@ -2,6 +2,7 @@ package com.bernard.misael.service;
import java.util.List;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.User;
import com.bernard.misael.service.dto.UserDto;
@ -13,4 +14,6 @@ public interface UserService {
User findUserByName(String name);
List<UserDto> findAllUsers();
boolean hasPrivilege(User u, Privilege p);
}

View File

@ -5,6 +5,7 @@ import java.util.stream.Collectors;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository;
import com.bernard.misael.service.dto.UserDto;
@ -54,4 +55,10 @@ public class UserServiceImpl implements UserService {
userDto.setName(user.getName());
return userDto;
}
@Override
public boolean hasPrivilege(User u, Privilege p) {
//TODO faire une query sql propre avec ça
return u.getRoles().stream().anyMatch(r -> r.getPrivileges().contains(p));
}
}

View File

@ -43,7 +43,6 @@ public class AuthController {
@GetMapping("/")
public String index(Model model) {
return "index";
}

View File

@ -10,16 +10,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.Quizz;
import com.bernard.misael.model.QuizzForm;
import com.bernard.misael.model.User;
import com.bernard.misael.questions.QTypes;
import com.bernard.misael.repository.QuizzFormRepository;
import com.bernard.misael.repository.QuizzRepository;
import com.bernard.misael.repository.UserRepository;
import com.bernard.misael.service.QuizzManager;
@ -47,10 +49,13 @@ public class QuestionsController {
@Autowired
QuizzRepository qrepo;
@GetMapping("/quizz")
@Autowired
QuizzFormRepository qfrepo;
/*
* List all quizz
*/
* List all quizz
*/
@GetMapping("/quizz")
public String getQuizz(Model model, Principal p) {
User u = null;
if (p!=null)
@ -63,14 +68,88 @@ public class QuestionsController {
return "quizz.html";
}
@GetMapping("/forms")
/*
* List all forms started or finished by the user
*/
public String getForms() {
* List all forms started or finished by the user
*/
@GetMapping("/forms")
public String getForms(Model model, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u!=null) {
model.addAttribute("finishedForms",qfrepo.findByUserAndDoneTrue(u));
model.addAttribute("openForms",qfrepo.findByUserAndDoneFalse(u));
}
return "forms.html";
}
/*
* Show one (completed) form of one user
*/
@GetMapping("/showform/{id}")
public Object showForm(@PathVariable("id") long id, Model m, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return "redirect:/login?restricted";
Optional<QuizzForm> oqf = qm.canViewQuizzForm(u, id);
if (oqf.isEmpty())
//TODO Faire un mesasge d'erreur dépendant des circonstances (unatuhorized, not found, not complete ...)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
m.addAttribute("formId", id);
return "showform.html";
}
/*
* Show one (completed) form of one user
*/
@GetMapping("/showformsadvancements/{id}")
public Object showFormsAdvancements(@PathVariable("id") long id, Model m, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return "redirect:/login?restricted";
Optional<Quizz> oq = qm.canViewQuizzFormsOfQuizz(u, id);
if (oq.isEmpty())
//TODO Faire un mesasge d'erreur dépendant des circonstances (unatuhorized, not found, not complete ...)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
m.addAttribute("quizzId", id);
return "showformadvancements.html";
}
/*
* API get the form
*/
@PostMapping("/getformdata/{id}")
public Object showFormApi(@PathVariable("id") long id, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return "redirect:/login?restricted";
JsonNode out = qm.getQuizzFormData(u, id);
return new ResponseEntity<>(out, HttpStatus.OK);
}
/*
* API get the form
*/
@PostMapping("/getformadvancements/{id}")
public Object showFormAdvancements(@PathVariable("id") long id, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return "redirect:/login?restricted";
JsonNode out = qm.getQuizzFormAdvancments(u, id);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@GetMapping("/form/{q}")
public String formpage(@PathVariable("q") long quizzId, Principal p, Model m) {
if (p==null)

View File

@ -0,0 +1,28 @@
main h1 input, main h1 span{
font-size: 30pt;
font-weight: bold;
color: teal;
}
ol#questions-list {
width: 90%;
}
ol#questions-list li {
width: 90%;
background-color: lemonchiffon;
list-style-type: none;
}
/* DCC */
ol#questions-list li.dcc-box {
background-color: gainsboro;
color: black;
font-size: 12pt;
}
li.dcc-box h5 {
font-size: 12pt;
font-weight: bold;
width: 100%;
}

View File

@ -0,0 +1,33 @@
main h1 input, main h1 span{
font-size: 30pt;
font-weight: bold;
color: teal;
}
table#forms-table {
width: 90%;
}
table#forms-table tr {
width: 90%;
background-color: lemonchiffon;
list-style-type: none;
}
/* DCC */
table#forms-table tbody tr.done-true {
background-color: rgb(161, 161, 161);
color: black;
font-size: 12pt;
font-style: italic;
}
table#forms-table tbody tr {
background-color: gainsboro;
color: black;
font-size: 12pt;
}
table#forms-table thead tr {
font-size: 14pt;
font-weight: bold;
}

View File

@ -7,11 +7,15 @@
<body>
<div th:replace="~{header}"/>
<main>
<h3>Formulaires terminés</h3>
<ul>
<li><a href="/questions/form?f=132">Formulaire de réponse au quizz numéro 1</a></li>
<li><a href="/questions/form?f=177">Formulaire de réponse au quizz numéro 2</a></li>
<li><a href="/questions/form?f=16818">Formulaire de réponse au quizz numéro 3</a></li>
<li th:if="${#lists.isEmpty(finishedForms)}">Vous n'avez fini aucun formulaire !</li>
<li th:each="qf : ${finishedForms}"><a th:href="@{/questions/showform/{id}(id=${qf.id})}">Quizz <span th:text="${qf.quizz.name}"/></a></li>
</ul>
<h3>Formulaires non terminés</h3>
<ul>
<li th:if="${#lists.isEmpty(openForms)}">Aucun formulaire ici !</li>
<li th:each="qf : ${openForms}"><a th:href="@{/questions/form/{id}(id=${qf.quizz.id})}">Quizz <span th:text="${qf.quizz.name}"/></a></li>
</ul>
</main>
</body>

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/showform.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}" type="text/javascript"></script>
<script th:src="@{/webjars/lodash/4.17.21/lodash.js}" type="text/javascript"></script>
<main>
<ol id="questions-list">
</ol>
<span id="error-textbox" style="color: red"></span>
<script th:inline="javascript">
/*<![CDATA[*/
var formid = /*[[${formId}]]*/ -1;
/*]]>*/
</script>
<script>
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
QTYPES = {
"DCC": makeAnswerBlockBCC,
}
// question position -> question data
questiondata=[]
function getdata() {
$.ajax({
url: "/questions/getformdata/"+formid,
type: "POST",
dataType: "json",
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
return
}
questiondata=res["data"]
for(let qd of questiondata) {
content = QTYPES[qd["type"]](qd["qid"],qd["qvalue"],qd["avalue"])
$(`#questions-list`).append(content)
}
}
})
}
function makeAnswerBlockBCC(id,qvalue,avalue) {
switch(avalue["type"]){
case 4:
dcc = "carré"
break
case 2:
dcc = "duo"
break
default:
dcc = "cache"
}
html = `
<li class="dcc-box" id="q-${id}">
<h5>${_.escape(qvalue["text"])}</h5>
Réponse ${dcc}: ${_.escape(avalue["answer"])}
</li>
`
out = $('<li/>').html(html).contents()
return out
}
getdata()
</script>
</main>
</body>
</html>

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/showformadvancements.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}" type="text/javascript"></script>
<script th:src="@{/webjars/lodash/4.17.21/lodash.js}" type="text/javascript"></script>
<main>
<button id="refresh-data">Refresh</button>
<table id="forms-table">
<thead>
<tr>
<th>User</th>
<th>Position</th>
<th>Step</th>
</tr>
</thead>
<tbody id="forms-body">
</tbody>
</table>
<span id="error-textbox" style="color: red"></span>
<script th:inline="javascript">
/*<![CDATA[*/
var quizzid = /*[[${quizzId}]]*/ -1;
/*]]>*/
</script>
<script>
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
formsdata=[]
function getdata() {
$('#refresh-data').prop('disabled',true)
$.ajax({
url: "/questions/getformadvancements/"+quizzid,
type: "POST",
dataType: "json",
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$('#refresh-data').prop('disabled',false)
return
}
formsdata=res["data"]
$('.form-line').remove()
for(let qf of formsdata) {
html = `
<tr class="form-line done-${qf["done"]}" id="q-${qf["qfid"]}">
<td>${qf["username"]}</td>
<td>${qf["position"]}</td>
<td>${qf["step"]}</td>
</tr>
`
content = $('<li/>').html(html).contents()
$(`#forms-body`).append(content)
}
$('#refresh-data').prop('disabled',false)
}
})
}
$('#refresh-data').on('click',getdata)
getdata()
</script>
</main>
</body>
</html>