Added controls of quizz on watch page

This commit is contained in:
Mysaa Java
2026-07-14 21:11:28 +02:00
parent 5131281613
commit 430488ceb8
10 changed files with 293 additions and 99 deletions
+1
View File
@@ -35,6 +35,7 @@
}; };
misael-launcher = pkgs.writeShellScriptBin "misael" '' misael-launcher = pkgs.writeShellScriptBin "misael" ''
set -e
echo "Checking Database Migration" echo "Checking Database Migration"
${pkgs.flyway}/bin/flyway "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:${misael}/resources/main/db/migration/" -schemas=misael migrate ${pkgs.flyway}/bin/flyway "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:${misael}/resources/main/db/migration/" -schemas=misael migrate
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -19,7 +18,6 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -41,6 +41,8 @@ public class UserServiceImpl implements UserService {
String id = oidcUser.getName(); String id = oidcUser.getName();
Set<Privilege> privileges; Set<Privilege> privileges;
try { try {
if(oidcUser.getAttribute("resource_access") != null)
throw new RuntimeException("Oidc user has no 'resource_access'");
privileges = oidcUser privileges = oidcUser
.<Map<String,Map<String,List<String>>>>getAttribute("resource_access") .<Map<String,Map<String,List<String>>>>getAttribute("resource_access")
.getOrDefault("misael",Map.of()) .getOrDefault("misael",Map.of())
@@ -57,6 +57,7 @@ public class QuestionsController {
if(u!=null) { if(u!=null) {
model.addAttribute("answerableQuizz",qm.answerableQuizz(u)); model.addAttribute("answerableQuizz",qm.answerableQuizz(u));
model.addAttribute("editableQuizz",qm.editableQuizz(u)); model.addAttribute("editableQuizz",qm.editableQuizz(u));
model.addAttribute("completedQuizz",qm.completedQuizz(u));
} }
return "quizz.html"; return "quizz.html";
} }
@@ -93,20 +94,20 @@ public class QuestionsController {
} }
/* /*
* Show one (completed) form of one user * Watch advancements of all forms for a specific quizz id
*/ */
@GetMapping("/showformsadvancements/{id}") @GetMapping("/watch/{id}")
public Object showFormsAdvancements(@PathVariable("id") long id, Model m, Principal p) { public Object showFormsAdvancements(@PathVariable("id") long id, Model m, Principal p) {
User u = us.ofPrincipal(p); User u = us.ofPrincipal(p);
if(u==null) if(u==null)
return "redirect:/login?restricted"; return "redirect:/login?restricted";
Optional<Quizz> oq = qm.canViewQuizzFormsOfQuizz(u, id); Optional<Quizz> oq = qm.canViewQuizzFormsOfQuizz(u, id);
if (oq.isEmpty()) if (oq.isEmpty())
//TODO Faire un mesasge d'erreur dépendant des circonstances (unatuhorized, not found, not complete ...) //TODO Faire un message d'erreur dépendant des circonstances (unatuhorized, not found, not complete ...)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED); return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
m.addAttribute("quizzId", id); m.addAttribute("quizzId", id);
return "showformadvancements.html"; return "watchquizz.html";
} }
/* /*
@@ -134,10 +135,10 @@ public class QuestionsController {
} }
/* /*
* API get the form * API get the form advancement for every form of a quizz
*/ */
@PostMapping("/getformadvancements/{id}") @PostMapping("/watchdata/{id}")
public Object showFormAdvancements(@PathVariable("id") long id, Principal p) { public Object watchData(@PathVariable("id") long id, Principal p) {
User u = us.ofPrincipal(p); User u = us.ofPrincipal(p);
if(u==null) if(u==null)
return "redirect:/login?restricted"; return "redirect:/login?restricted";
@@ -159,6 +160,33 @@ public class QuestionsController {
return new ResponseEntity<>(HttpStatus.OK); return new ResponseEntity<>(HttpStatus.OK);
} }
/*
* API get the form
*/
@GetMapping("/mark-complete/{id}")
public Object markComplete(@PathVariable("id") long id, Principal p) {
User u = us.ofPrincipal(p);
if(u==null)
return "redirect:/login?restricted";
if(!qm.markComplete(u, id))
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
/*
* API set the last public question
*/
@PostMapping("/set-public-question-count/{id}")
public Object setPublicQuestionCount(@PathVariable("id") long id, @RequestBody JsonNode data, Principal p) {
User u = us.ofPrincipal(p);
if(u==null)
return "redirect:/login?restricted";
JsonNode pqc = data.get("publicQuestionCount");
Integer pqcI = pqc.isNull() ? null : pqc.asInt();
if(!qm.setPublicQuestionCount(u, id, pqcI))
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/form/{q}") @GetMapping("/form/{q}")
public String formpage(@PathVariable("q") long quizzId, Principal p, Model m) { public String formpage(@PathVariable("q") long quizzId, Principal p, Model m) {
User u = us.ofPrincipal(p); User u = us.ofPrincipal(p);
@@ -18,6 +18,7 @@ public interface QuizzManager {
public boolean canAccessQuizz(User user, long quizzId); public boolean canAccessQuizz(User user, long quizzId);
public List<Quizz> editableQuizz(User user); public List<Quizz> editableQuizz(User user);
public List<Quizz> completedQuizz(User user);
public List<Quizz> answerableQuizz(User user); public List<Quizz> answerableQuizz(User user);
public boolean canEditQuizz(User user, long quizzId); public boolean canEditQuizz(User user, long quizzId);
@@ -37,4 +38,6 @@ public interface QuizzManager {
public JsonNode getQuizzFormAdvancments(User user, long quizzId); public JsonNode getQuizzFormAdvancments(User user, long quizzId);
public Quizz duplicateQuizz(User user, long quizzId); public Quizz duplicateQuizz(User user, long quizzId);
public boolean markComplete(User user, long quizzId);
public boolean setPublicQuestionCount(User u, long quizzId, Integer publicQuestionCount);
} }
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -11,6 +12,7 @@ import java.util.stream.Stream;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.misael.auth.repository.UserRepository; import com.bernard.misael.auth.repository.UserRepository;
@@ -190,10 +192,11 @@ public class QuizzManagerImpl implements QuizzManager {
return qf; return qf;
} }
private static final Random rand = new Random();
@Override @Override
public Quizz newQuizz(User user) { public Quizz newQuizz(User user) {
Quizz q = new Quizz(); Quizz q = new Quizz();
q.setName("Super questions de "+user.getName()+" ("+Integer.toHexString((int)(Math.random()*0xFFFFFFF))+")"); q.setName("Super questions de "+user.getName()+" ("+Integer.toHexString(rand.nextInt(0xFFFFFFF))+")");
q.setOwner(user); q.setOwner(user);
q = qRepository.save(q); q = qRepository.save(q);
return q; return q;
@@ -227,7 +230,7 @@ public class QuizzManagerImpl implements QuizzManager {
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())), .sorted((q1,q2) -> q1.getName().compareTo(q2.getName())),
publicQuizz.stream() publicQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())) .sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
).collect(Collectors.toList()); ).toList();
} }
@Override @Override
@@ -235,18 +238,25 @@ public class QuizzManagerImpl implements QuizzManager {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteFalse(user); Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteFalse(user);
return ownQuizz.stream() return ownQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())) .sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
.collect(Collectors.toList()); .toList();
} }
Logger logger = LoggerFactory.getLogger(QuestionsController.class); @Override
public List<Quizz> completedQuizz(User user) {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user);
return ownQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
.toList();
}
Logger logger = LoggerFactory.getLogger(QuizzManagerImpl.class);
@Override @Override
public boolean canEditQuizz(User user, long quizzId) { public boolean canEditQuizz(User user, long quizzId) {
try { try {
Quizz quizz = qRepository.getReferenceById(quizzId); Quizz quizz = qRepository.getReferenceById(quizzId);
logger.info("Quizz owner is "+quizz.getOwner().getName());
return quizz.getOwner().equals(user); return quizz.getOwner().equals(user);
} catch (EntityNotFoundException e) { } catch (EntityNotFoundException e) {
logger.info("Could not find quizz of id "+quizzId); logger.info("Could not find quizz of id {}",quizzId);
return false; return false;
} }
} }
@@ -294,7 +304,7 @@ public class QuizzManagerImpl implements QuizzManager {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get(); if(authCheck.isPresent()) return authCheck.get();
if(newName.isBlank() | newName.length()>255) if(newName.isBlank() || newName.length()>255)
return errorNode("Le nom est invalide"); return errorNode("Le nom est invalide");
Quizz quizz = qRepository.findById(quizzId).get(); Quizz quizz = qRepository.findById(quizzId).get();
@@ -565,6 +575,10 @@ public class QuizzManagerImpl implements QuizzManager {
} }
ObjectNode out = JsonNodeFactory.instance.objectNode(); ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true)); out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("publicQuestionCount", quizz.getPublicQuestionCount()==null?
JsonNodeFactory.instance.nullNode():
JsonNodeFactory.instance.numberNode(quizz.getPublicQuestionCount()));
out.set("questionCount", JsonNodeFactory.instance.numberNode(quizz.getQuestionCount()));
out.set("data", formsNode); out.set("data", formsNode);
return out; return out;
} }
@@ -599,6 +613,40 @@ public class QuizzManagerImpl implements QuizzManager {
return nq; return nq;
} }
@Override
public boolean markComplete(User u, long quizzId) {
if (!canEditQuizz(u, quizzId))
return false;
Optional<Quizz> oq = qRepository.findById(quizzId);
if(oq.isEmpty())
return false;
Quizz q = oq.get();
q.setPublicQuestionCount(null);
q.setComplete(true);
qRepository.save(q);
return true;
}
@Override
public boolean setPublicQuestionCount(User u, long quizzId, Integer publicQuestionCount) {
if (!canEditQuizz(u, quizzId))
return false;
Optional<Quizz> oq = qRepository.findById(quizzId);
if(oq.isEmpty())
return false;
Quizz q = oq.get();
if(!q.isComplete())
return false;
if(publicQuestionCount != null && (publicQuestionCount > q.getQuestionCount() || publicQuestionCount < 0))
return false;
q.setPublicQuestionCount(publicQuestionCount);
qRepository.save(q);
return true;
}
} }
@@ -27,6 +27,10 @@
<ol id="questions-list"> <ol id="questions-list">
</ol> </ol>
<div class="buttonbar">
<button id="mark-complete">Marquer le quizz comme terminé</button>
</div>
<span id="error-textbox" style="color: red"></span> <span id="error-textbox" style="color: red"></span>
@@ -412,6 +416,18 @@
} }
unReorderMode() unReorderMode()
} }
function markComplete() {
if (confirm("Voulez-vous vraiment marquer ce quizz comme terminé ? Vous ne pourrez plus l'éditer !")) {
$.ajax({
url: "/questions/mark-complete/"+quizzid,
type: "GET",
success: function(res) {
console.log("SUPER")
}
})
}
}
function makeEditBlockBCC(id,question) { function makeEditBlockBCC(id,question) {
@@ -470,6 +486,7 @@
$("#edit-question-name-confirm").on("click",commitEditName) $("#edit-question-name-confirm").on("click",commitEditName)
$("#edit-question-name-cancel").on("click",cancelEditName) $("#edit-question-name-cancel").on("click",cancelEditName)
$("#add-question").on("click",addQuestion) $("#add-question").on("click",addQuestion)
$("#mark-complete").on("click",markComplete)
getdata() getdata()
</script> </script>
</main> </main>
+8 -2
View File
@@ -7,17 +7,23 @@
<body> <body>
<div th:replace="~{header}"/> <div th:replace="~{header}"/>
<main> <main>
<h3>Quizz à répondre</h3> <h3>Quizz disponibles</h3>
<ul> <ul>
<li th:if="${#lists.isEmpty(answerableQuizz)}">Aucun quizz de disponible malheureusement :(</li> <li th:if="${#lists.isEmpty(answerableQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${answerableQuizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li> <li th:each="q : ${answerableQuizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul> </ul>
<h3>Quizz à éditer</h3> <h3>Quizz en édition</h3>
<ul> <ul>
<li th:if="${#lists.isEmpty(editableQuizz)}">Aucun quizz de disponible malheureusement :(</li> <li th:if="${#lists.isEmpty(editableQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${editableQuizz}"><a th:href="@{/questions/quizz-edit/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li> <li th:each="q : ${editableQuizz}"><a th:href="@{/questions/quizz-edit/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul> </ul>
<a sec:authorize="hasAuthority('CREATE_QUIZZ')" th:href="@{/questions/new-quizz}">Nouveau Quizz</a> <a sec:authorize="hasAuthority('CREATE_QUIZZ')" th:href="@{/questions/new-quizz}">Nouveau Quizz</a>
<h3>Quizz terminés</h3>
<ul>
<li th:if="${#lists.isEmpty(completedQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${completedQuizz}"><a th:href="@{/questions/watch/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul>
</main> </main>
</body> </body>
</html> </html>
@@ -1,81 +0,0 @@
<!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>
@@ -0,0 +1,172 @@
<!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>
<div class="button-bar">
<button id="make-public">Rendre public</button>
<button id="make-private">Rendre privé</button>
<span id="question-count-number">Nombre de questions accessibles:</span>
<button id="set-question-begin">&lt;&lt;</button>
<button id="set-question-decrement">&lt;</button>
<span id="last-question-number">x</span>
<button id="set-question-increment">&gt;</button>
<button id="set-question-last">&gt;&gt;</button>
</div>
<button id="refresh-data">Refresh</button>
<table id="forms-table">
<thead>
<tr>
<th>User</th>
<th>Nombre de questions répondues</th>
<th>Étape</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 = []
pqc = null
qCount = null
allButtons = [
$('#refresh-data'),
$('#make-public'),
$('#make-private'),
$('#set-question-begin'),
$('#set-question-decrement'),
$('#question-count-number'),
$('#last-question-number'),
$('#set-question-increment'),
$('#set-question-last')
]
function activateButtons() {
if(pqc === null) {
$('#make-public').show()
$('#make-private').hide()
$('#set-question-begin').hide()
$('#set-question-decrement').hide()
$('#question-count-number').hide()
$('#last-question-number').hide()
$('#set-question-increment').hide()
$('#set-question-last').hide()
} else {
$('#make-public').hide()
$('#make-private').show()
$('#question-count-number').show()
$('#set-question-begin').show()
$('#set-question-decrement').show()
if(pqc == 0) {
$('#set-question-begin').prop('disabled', true)
$('#set-question-decrement').prop('disabled', true)
} else {
$('#set-question-begin').prop('disabled', false)
$('#set-question-decrement').prop('disabled', false)
}
$('#last-question-number').show()
$('#last-question-number').text(pqc)
$('#set-question-last').show()
$('#set-question-increment').show()
if(pqc == qCount) {
$('#set-question-last').prop('disabled', true)
$('#set-question-increment').prop('disabled', true)
} else {
$('#set-question-last').prop('disabled', false)
$('#set-question-increment').prop('disabled', false)
}
}
}
function getdata() {
$('#refresh-data').prop('disabled',true)
$.ajax({
url: "/questions/watchdata/"+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"]
pqc = res["publicQuestionCount"]
qCount = res["questionCount"]
$('.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)
}
for(const b of allButtons) b.prop('disabled', false)
activateButtons()
}
})
}
function setPQC(n) {
for(const b of allButtons) b.prop('disabled', true)
$.ajax({
url: "/questions/set-public-question-count/"+quizzid,
type: "POST",
data: JSON.stringify({'publicQuestionCount': n}),
contentType: 'application/json',
success: function(res) {
getdata()
}
})
}
function setPublicQuestionCount(n) {
return function (e) { setPQC(n) }
}
function setPublicQuestionCountRelative(n) {
return function (e) { setPQC(pqc + n) }
}
$('#refresh-data').on('click',getdata)
$('#make-private').on('click',setPublicQuestionCount(null))
$('#make-public').on('click',setPublicQuestionCount(0))
$('#set-question-begin').on('click',setPublicQuestionCount(0))
$('#set-question-decrement').on('click',setPublicQuestionCountRelative(-1))
$('#set-question-increment').on('click',setPublicQuestionCountRelative(+1))
$('#set-question-end').on('click',setPublicQuestionCount(qCount))
getdata()
</script>
</main>
</body>
</html>