Compare commits

...

10 Commits

29 changed files with 3996 additions and 165 deletions

147
flake.lock generated Normal file
View File

@ -0,0 +1,147 @@
{
"nodes": {
"build-gradle-application": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1761837331,
"narHash": "sha256-uqmuTLq3VJfPqD1frbzIv9EcmlKcl7pRjNFwKkWIr6I=",
"ref": "refs/heads/main",
"rev": "411fff172011aafc7b2afc365ced450ffe7d729d",
"revCount": 98,
"type": "git",
"url": "file:/home/mysaa/Documents/Projets/buildGradleApplication"
},
"original": {
"type": "git",
"url": "file:/home/mysaa/Documents/Projets/buildGradleApplication"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
"id": "flake-parts",
"type": "indirect"
}
},
"flake-utils": {
"locked": {
"lastModified": 1605370193,
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gradle2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1755902591,
"narHash": "sha256-mnPaPH9k6Mbr7O0KzBBdkiDDS88oB5NiFHVSFkCzswU=",
"owner": "tadfisher",
"repo": "gradle2nix",
"rev": "30cfe5889188524223364ee7919d94e83d6ee44a",
"type": "github"
},
"original": {
"owner": "tadfisher",
"ref": "v2",
"repo": "gradle2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1758035966,
"narHash": "sha256-qqIJ3yxPiB0ZQTT9//nFGQYn8X/PBoJbofA7hRKZnmE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8d4ddb19d03c65a36ad8d189d001dc32ffb0306b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1754788789,
"narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "a73b9c743612e4244d865a2fdee11865283c04e6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1763678758,
"narHash": "sha256-+hBiJ+kG5IoffUOdlANKFflTT5nO3FrrR2CA3178Y5s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "117cc7f94e8072499b0a7aa4c52084fa4e11cc9b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1763618868,
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"build-gradle-application": "build-gradle-application",
"gradle2nix": "gradle2nix",
"nixpkgs": "nixpkgs_3"
}
}
},
"root": "root",
"version": 7
}

84
flake.nix Normal file
View File

@ -0,0 +1,84 @@
{
description = "Misael server";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
build-gradle-application.url = "git+file:/home/mysaa/Documents/Projets/buildGradleApplication";#"github:raphiz/buildGradleApplication";
gradle2nix.url = "github:tadfisher/gradle2nix/v2";
};
outputs = { self, nixpkgs, build-gradle-application, gradle2nix, ... }@inputs:
let system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; overlays = [ build-gradle-application.overlays.default ]; };
lib = pkgs.lib;
jdk = pkgs.openjdk21;
gradle = pkgs.gradle;
misael = gradle2nix.builders.x86_64-linux.buildGradlePackage {
pname = "misael";
version = "1.0";
lockFile = ./gradle.lock;
gradleInstallFlags = [ "assemble" ];
inherit gradle;
buildJdk = jdk;
src = ./.;
buildPhase = ''
gradle assemble
'';
installPhase = ''
mkdir $out
cp ./build/libs/misael-beta.jar $out/misael.jar
cp -r ./build/resources/ $out/resources/
'';
};
misael-launcher = pkgs.writeScriptBin "misael" ''
echo "Checking Database Migration"
${pkgs.flyway} "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:${misael}/resources/main/db/migration/" -schemas=misael migrate
echo "Launching misael"
${jdk}/bin/java -jar ${misael}/misael.jar --spring.datasource.url=$MISAEL_DATABASE --spring.datasource.password=$MISAEL_PASSWORD
'';
in {
packages.${system} = {
default = self.packages.${system}.misael;
misael = misael;
};
apps.${system} = {
default = self.apps.${system}.misael;
misael = { type = "app"; program = "${misael-launcher}/bin/misael"; };
};
devShells.${system} = {
default = pkgs.mkShell {
packages = [
jdk
gradle
(pkgs.vscode-with-extensions.override {
vscode = pkgs.vscodium;
vscodeExtensions = with pkgs.vscode-extensions; [
redhat.java
vscjava.vscode-java-debug
vscjava.vscode-java-test
vscjava.vscode-gradle
vscjava.vscode-java-dependency
sonarsource.sonarlint-vscode
bbenoist.nix
redhat.vscode-yaml
];
})
];
shellHook = ''
echo "Starting Gradle daemon ..."
gradle
echo "Gradle daemon started."
'';
};
};
formatter.${system} = pkgs.nixpkgs-fmt;
};
}

2746
gradle.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import org.springframework.security.core.GrantedAuthority;
public enum Privilege implements GrantedAuthority {
LIST_USERS,ADD_USERS,LIST_QUIZZ;
LIST_USERS,ADD_USERS,LIST_QUIZZ,CREATE_QUIZZ,VIEW_ALL_FORMS;
@Override
public String getAuthority() {

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

@ -31,7 +31,7 @@ public class Quizz {
@Column(nullable=false)
@ColumnDefault("false")
private boolean isPublic;
private boolean isComplete;
@Column(nullable=false)
@ColumnDefault("0")
@ -39,4 +39,7 @@ public class Quizz {
@OneToMany(mappedBy="quizz")
private Set<Question> questions;
@Column(nullable=true)
private Integer publicQuestionCount;
}

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

@ -7,11 +7,14 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull;
import com.bernard.misael.model.Quizz;
import com.bernard.misael.model.User;
public interface QuizzRepository extends JpaRepository<Quizz,Long> {
public @NonNull Optional<Quizz> findById(@NonNull Long id);
public @NonNull Set<Quizz> findByIsPublicTrue();
public @NonNull Set<Quizz> findByPublicQuestionCountIsNotNullAndIsCompleteTrue();
public @NonNull Set<Quizz> findByOwnerAndIsCompleteTrue(@NonNull User owner);
public @NonNull Set<Quizz> findByOwnerAndIsCompleteFalse(@NonNull User owner);
}

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;
@ -12,11 +14,16 @@ public interface QuizzManager {
public JsonNode answer(User user, long quizzId,JsonNode data);
public JsonNode next(User user, long quizzId);
public Quizz newQuizz(User user);
public boolean canAccessQuizz(User user, long quizzId);
public List<Quizz> accessibleQuizz(User user);
public List<Quizz> editableQuizz(User user);
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);
public JsonNode addQuestion(User user, long quizzId);
@ -25,4 +32,9 @@ 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 getAllFormsData(User u, long id);
public JsonNode getQuizzFormAdvancments(User user, long quizzId);
public Quizz duplicateQuizz(User user, long quizzId);
}

View File

@ -14,23 +14,19 @@ 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;
import com.bernard.misael.web.QuestionsController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
@ -40,6 +36,9 @@ import jakarta.persistence.EntityNotFoundException;
@Service
public class QuizzManagerImpl implements QuizzManager {
@Autowired
UserRepository uRepository;
@Autowired
QuizzFormRepository qfRepository;
@ -52,6 +51,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())
@ -65,6 +69,8 @@ public class QuizzManagerImpl implements QuizzManager {
Optional<Quizz> oquizz = qRepository.findById(quizzId);
if(!oquizz.isPresent())
return errorNode("Could not find the quizz with id "+quizzId);
if(!oquizz.get().isComplete())
return errorNode("Quizz is not complete");
Quizz quizz = oquizz.get();
QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null)
@ -75,6 +81,8 @@ public class QuizzManagerImpl implements QuizzManager {
if(qindex != data.get("index").intValue())
return errorNode("You are not answering the right question (you answer question "+data.get("index").intValue()
+" where you should answer question "+qindex+")");
if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE))
return errorNode("La question suivante est encore bloquée");
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null)
return errorNode("Could not find question "+qindex);
@ -124,6 +132,8 @@ public class QuizzManagerImpl implements QuizzManager {
Optional<Quizz> oquizz = qRepository.findById(quizzId);
if(!oquizz.isPresent())
return errorNode("Could not find quizz with id "+quizzId);
if(!oquizz.get().isComplete())
return errorNode("Quizz is not complete");
Quizz quizz = oquizz.get();
QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null){
@ -133,6 +143,8 @@ public class QuizzManagerImpl implements QuizzManager {
if(qf.isDone())
return errorNode("No more questions");
int qindex = qf.getCurrentQuestion();
if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE))
return errorNode("La question suivante est encore bloquée");
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null)
return errorNode("Could not find question "+qindex);
@ -165,7 +177,7 @@ public class QuizzManagerImpl implements QuizzManager {
return out;
}
public QuizzForm newQuizzForm(User user, Quizz quizz) {
private QuizzForm newQuizzForm(User user, Quizz quizz) {
QuizzForm qf = new QuizzForm();
qf.setUser(user);
qf.setQuizz(quizz);
@ -176,7 +188,16 @@ public class QuizzManagerImpl implements QuizzManager {
return qf;
}
public static final JsonNode errorNode(String err){
@Override
public Quizz newQuizz(User user) {
Quizz q = new Quizz();
q.setName("Super questions de "+user.getName()+" ("+Integer.toHexString((int)(Math.random()*0xFFFFFFF))+")");
q.setOwner(user);
q = qRepository.save(q);
return q;
}
private static final JsonNode errorNode(String err){
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(false));
out.set("message", JsonNodeFactory.instance.textNode(err));
@ -187,16 +208,16 @@ public class QuizzManagerImpl implements QuizzManager {
public boolean canAccessQuizz(User user, long quizzId) {
try{
Quizz quizz = qRepository.getReferenceById(quizzId);
return quizz.isPublic() || quizz.getOwner().equals(user);
return quizz.getPublicQuestionCount()!=null || quizz.getOwner().equals(user);
} catch (EntityNotFoundException e) {
return false;
}
}
@Override
public List<Quizz> accessibleQuizz(User user) {
Set<Quizz> ownQuizz = user.getMyQuizzs();
Set<Quizz> publicQuizz = qRepository.findByIsPublicTrue();
public List<Quizz> answerableQuizz(User user) {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user);
Set<Quizz> publicQuizz = qRepository.findByPublicQuestionCountIsNotNullAndIsCompleteTrue();
publicQuizz.removeAll(ownQuizz);
return Stream.concat(
@ -207,6 +228,14 @@ public class QuizzManagerImpl implements QuizzManager {
).collect(Collectors.toList());
}
@Override
public List<Quizz> editableQuizz(User user) {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteFalse(user);
return ownQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
.collect(Collectors.toList());
}
Logger logger = LoggerFactory.getLogger(QuestionsController.class);
@Override
public boolean canEditQuizz(User user, long quizzId) {
@ -228,6 +257,8 @@ public class QuizzManagerImpl implements QuizzManager {
Optional<Quizz> oquizz = qRepository.findById(quizzId);
if(!oquizz.isPresent())
return Optional.of(errorNode("Could not find quizz with id "+quizzId));
if(oquizz.get().isComplete())
return Optional.of(errorNode("Quizz is complete, cannot edit, answers might have already been cast"));
return Optional.empty();
}
@ -245,12 +276,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);
}
@ -289,7 +315,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);
@ -297,6 +323,7 @@ public class QuizzManagerImpl implements QuizzManager {
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("id", JsonNodeFactory.instance.numberNode(q.getId()));
out.set("type", JsonNodeFactory.instance.textNode(DEFAULT_QTYPE.name()));
out.set("value", n);
return out;
@ -384,7 +411,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));
@ -414,14 +441,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();
@ -430,4 +452,150 @@ 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 getAllFormsData(User u, long quizzId) {
Optional<Quizz> oq = canViewQuizzFormsOfQuizz(u, quizzId);
if(oq.isEmpty())
return errorNode("Could not access the quizzform"); //TODO more precise error node
Quizz quizz = oq.get();
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("id",JsonNodeFactory.instance.numberNode(quizz.getId()));
out.set("name",JsonNodeFactory.instance.textNode(quizz.getName()));
ArrayNode questionNode = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount());
List<Question> questions = quizz.getQuestions().stream().sorted((p,q) -> Integer.valueOf(p.getIndex()).compareTo(q.getIndex())).toList();
for(int i=0;i<questions.size();i++) {
ObjectNode qNode = JsonNodeFactory.instance.objectNode();
qNode.set("id", JsonNodeFactory.instance.numberNode(questions.get(i).getId()));
qNode.set("type", JsonNodeFactory.instance.textNode(questions.get(i).getType().name()));
qNode.set("value",questions.get(i).getValue());
questionNode.add(qNode);
}
out.set("questions", questionNode);
ObjectNode qfzNodes = JsonNodeFactory.instance.objectNode();
for(QuizzForm qf : qfRepository.findByQuizz(quizz)) {
ObjectNode qfNode = JsonNodeFactory.instance.objectNode();
qfNode.set("id", JsonNodeFactory.instance.numberNode(qf.getId()));
qfNode.set("currentQuestion", JsonNodeFactory.instance.numberNode(qf.getCurrentQuestion()));
qfNode.set("answerStep", JsonNodeFactory.instance.numberNode(qf.getAnswerStep()));
qfNode.set("done", JsonNodeFactory.instance.booleanNode(qf.isDone()));
List<Answer> answers = answerRepository.findByFormAndQuestionIn(qf, questions);
ArrayNode answersNode = JsonNodeFactory.instance.arrayNode();
for(int i=0;i<answers.size();i++) {
answersNode.add(answers.get(i).getValue());
}
qfNode.set("answers", answersNode);
qfzNodes.set(qf.getUser().getName(), qfNode);
}
out.set("forms", qfzNodes);
out.set("success", JsonNodeFactory.instance.booleanNode(true));
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;
}
@Override
public Quizz duplicateQuizz(User u, long quizzId) {
if (!canEditQuizz(u, quizzId))
return null;
Optional<Quizz> oq = qRepository.findById(quizzId);
// CHECKED BEFORE if (oq.isEmpty()) return null;
Quizz q = oq.get();
Quizz nq = new Quizz();
nq.setName(q.getName() + "("+ Integer.toHexString((int)(Math.random()*0xFFFFFFF)) +")");
nq.setOwner(q.getOwner());
nq.setQuestionCount(q.getQuestionCount());
nq.setPublicQuestionCount(q.getPublicQuestionCount());
nq.setComplete(q.isComplete());
nq = qRepository.save(nq);
// We duplicate questions
for (Question qu : q.getQuestions()) {
Question nqu = new Question();
nqu.setIndex(qu.getIndex());
nqu.setType(qu.getType());
nqu.setValue(qu.getValue());
nqu.setQuizz(nq);
questionRepository.save(nqu);
}
return nq;
}
}

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

@ -6,7 +6,6 @@ import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -41,21 +40,8 @@ public class AuthController {
return "login";
}
public User getLoggedInUser() {
if(SecurityContextHolder.getContext().getAuthentication().getPrincipal()
instanceof org.springframework.security.core.userdetails.User){
org.springframework.security.core.userdetails.User user
= (org.springframework.security.core.userdetails.User)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userService.findUserByName(user.getUsername());
} else {
return null;
}
}
@GetMapping("/")
public String index(Model model) {
return "index";
}

View File

@ -11,13 +11,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.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;
@ -45,28 +48,137 @@ 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)
u = ur.findByName(p.getName());
if(u!=null) {
model.addAttribute("quizz",qm.accessibleQuizz(u));
model.addAttribute("answerableQuizz",qm.answerableQuizz(u));
model.addAttribute("editableQuizz",qm.editableQuizz(u));
}
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 forms for a specific quizz
*/
@GetMapping("/getallformsdata/{id}")
public Object getAllFormsData(@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.getAllFormsData(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);
}
/*
* API get the form
*/
@GetMapping("/duplicate-quizz/{id}")
public Object duplicateQuizz(@PathVariable("id") long id, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return "redirect:/login?restricted";
Quizz q = qm.duplicateQuizz(u, id);
if(q == null)
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/form/{q}")
public String formpage(@PathVariable("q") long quizzId, Principal p, Model m) {
if (p==null)
@ -98,6 +210,18 @@ public class QuestionsController {
return new ResponseEntity<>(out, HttpStatus.OK);
}
@GetMapping("/new-quizz")
@Secured("CREATE_QUIZZ")
public Object newQuizz(Principal p, Model m) {
if (p==null)
return "redirect:/login?restricted";
User u = ur.findByName(p.getName());
if (u==null)
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
Quizz q = qm.newQuizz(u);
return "redirect:/questions/quizz-edit/"+Long.toString(q.getId());
}
Logger logger = LoggerFactory.getLogger(QuestionsController.class);
@GetMapping("/quizz-edit/{q}")
@ -105,7 +229,6 @@ public class QuestionsController {
if (p==null)
return "redirect:/login?restricted";
User u = ur.findByName(p.getName());
logger.info("An INFO Message");
if (u==null || !qm.canEditQuizz(u, quizzId))
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
m.addAttribute("quizzId", quizzId);

View File

@ -0,0 +1 @@
alter table quizz add column "is_complete" BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -0,0 +1,8 @@
alter table role_privileges drop constraint role_privileges_privileges_check;
alter table role_privileges add constraint role_privileges_privileges_check
check (privileges in ('LIST_USERS','ADD_USERS','LIST_QUIZZ','CREATE_QUIZZ','VIEW_ALL_FORMS'));
insert into role_privileges VALUES
((select id from roles where "name" = 'ADMIN'),'CREATE_QUIZZ');
insert into role_privileges VALUES
((select id from roles where "name" = 'ADMIN'),'VIEW_ALL_FORMS');

View File

@ -0,0 +1,2 @@
alter table if exists quizz add column public_question_count integer default NULL;
alter table if exists quizz drop column is_public;

View File

@ -1,3 +1,22 @@
main h1 input, main h1 span{
font-size: 30pt;
font-weight: bold;
color: teal;
}
main h1 button{
width: 7em;
}
main .buttonbar button{
width: 7em;
}
main .buttonbar {
display: flex;
flex-direction: row;
}
ol#questions-list {
display: table;
border-spacing: 2px;

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

@ -44,13 +44,13 @@ nav .navbar .menu{
}
main{
display: flex;
height: 100vh;
width: 100%;
align-items: center;
justify-content: center;
color: #96c7e8;
font-size: 24px;
flex-direction: column;
margin-top: 115px;
}
.button a{
position: fixed;

View File

@ -15,11 +15,11 @@
<div class="buttonbox" id="answer-select">
<button class="squared-button bcol1" id="answer-select-duo">Duo</button>
<button class="squared-button bcol2" id="answer-select-carre">Carré</button>
<button class="squared-button bcol3" id="answer-select-cache">Cache</button>
<button class="squared-button bcol3" id="answer-select-cash">Cash</button>
</div>
<div class="buttonbox" id="answer-cache">
<textarea class="squared-input" id="answer-cache-input" type="text"></textarea>
<button class="squared-button bcol4" id="answer-cache-button">Envoyer la réponse</button>
<div class="buttonbox" id="answer-cash">
<textarea class="squared-input" id="answer-cash-input" type="text"></textarea>
<button class="squared-button bcol4" id="answer-cash-button">Envoyer la réponse</button>
</div>
<div class="buttonbox" id="answer-duo">
<button class="squared-button bcol1" id="answer-duo-button-1"></button>
@ -47,6 +47,9 @@
console.log(txt)
$("#error-textbox").text(txt)
}
function clearerror() {
$("#error-textbox").text("")
}
function next() {
$.ajax({
url: "/questions/question/"+qid,
@ -61,23 +64,24 @@
error(res["message"])
return
}
clearerror()
$("#question-counter").text((res["index"]+1)+"/"+qlength)
$("#question-text").text(res["data"]["text"])
if(qstep==0){
$("#answer-select").show()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cache").hide()
$("#answer-cash").hide()
}else if(qstep==1){
awrs = res["data"]["answers"]
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cache").hide()
$("#answer-cash").hide()
if(typeof awrs === 'undefined'){
$("#answer-cache").show()
$("#answer-cache-input").val("")
$("#answer-cash").show()
$("#answer-cash-input").val("")
} else if (awrs.length == 2) {
$("#answer-duo").show()
$("#answer-duo-button-1").text(res.data.answers[0])
@ -108,6 +112,7 @@
success: function(res) {
console.log(res)
if(res["success"]){
clearerror()
next()
} else {
error(res["message"])
@ -130,14 +135,15 @@
success: function(res) {
console.log(res)
if(res["success"]){
clearerror()
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cash").hide()
if(qindex+1 >= qlength) {
$("#question-text").text("Plus de questions !")
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cache").hide()
} else {
$("#question-text").text("Question suivante ...")
next()
}
} else {
@ -151,11 +157,11 @@
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cache").hide()
$("#answer-cash").hide()
$("#answer-select-duo").on('click',() => answer1(2))
$("#answer-select-carre").on('click',() => answer1(4))
$("#answer-select-cache").on('click',() => answer1(0))
$("#answer-cache-button").on('click',() => answer2($("#answer-cache-input").val()))
$("#answer-select-cash").on('click',() => answer1(0))
$("#answer-cash-button").on('click',() => answer2($("#answer-cash-input").val()))
$("#answer-duo-button-1").on('click',() => answer2(awrs[0]))
$("#answer-duo-button-2").on('click',() => answer2(awrs[1]))
$("#answer-carre-button-1").on('click',() => answer2(awrs[0]))

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

@ -10,10 +10,26 @@
<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>
<h1>Quizz <span id="question-name"></span><button id="edit-question-name">Edit</button></h1>
<h1>Quizz
<span id="question-name"></span>
<input id="edit-question-name"/>
<button id="edit-question-name-button">Edit</button>
<button id="edit-question-name-confirm">Valider</button>
<button id="edit-question-name-cancel">Annuler</button>
</h1>
<div class="buttonbar">
<button class="display-button" id="add-question">Ajouter</button>
<button class="display-button" id="reorder-questions">Réordonner</button>
<button class="reorder-button" id="reorder-questions-commit">Valider l'ordre</button>
<button class="reorder-button" id="reorder-questions-cancel">Annuler</button>
</div>
<ol id="questions-list">
</ol>
<span id="error-textbox" style="color: red"></span>
<script th:inline="javascript">
/*<![CDATA[*/
var quizzid = /*[[${quizzId}]]*/ -1;
@ -31,7 +47,16 @@
"read": readEditBlockBCC
}
}
// question position -> question data
questions={}
// Map question id -> question position
qpositions={}
// the name of the quizz
quizzName = ""
editing = new Set()
function getdata() {
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/get",
@ -45,79 +70,127 @@
return
}
questions=res["questions"]
qname=res["name"]
$("#question-name").text(qname)
quizzName=res["name"]
$("#question-name").text(quizzName)
$("#edit-question-name").val(quizzName)
for(var i=0;i<questions.length;i++){
qu = questions[i]
qid = qu["id"]
newhtml = `
<li id="question-display-${qid}">
<div class="content-box" id="question-display-${qid}-content"/>
<div class="content-box" id="question-display-${qid}-edit"/>
<div class="button-box">
<button id="question-display-${qid}-button-edit">Edit</button>
<button id="question-display-${qid}-type-edit">Type</button>
<button id="question-display-${qid}-validate-edit">Valider</button>
<button id="question-display-${qid}-cancel-edit">Annuler</button>
</div>
</li>
`
newdom = $('<li/>').html(newhtml).contents()
$("#questions-list").append(newdom)
const thei = i
const theqid = qid
$(`#question-display-${qid}-button-edit`).on('click',e => edit(thei,theqid))
$(`#question-display-${qid}-type-edit`).on('click',e => setType(thei,theqid))
$(`#question-display-${qid}-validate-edit`).on('click',e => commitEdit(thei,theqid))
$(`#question-display-${qid}-cancel-edit`).on('click',e => cancelEdit(thei,theqid))
newcontent = QTYPES[qu["type"]]["display"](qid,qu["value"])
$(`#question-display-${qid}-content`).replaceWith(newcontent)
newedit = QTYPES[qu["type"]]["edit"](qid,qu["value"])
$(`#question-display-${qid}-edit`).replaceWith(newedit)
contentMode(qid)
qpositions[questions[i]["id"]] = i
internalCreateQuestion(i)
}
$(`.reorder-button`).hide()
}
})
}
function contentMode(qid) {
$(`#question-display-${qid}-content`).show()
$(`#question-display-${qid}-button-edit`).show()
$(`#question-display-${qid}-type-edit`).show()
$(`#question-display-${qid}-edit`).hide()
$(`#question-display-${qid}-validate-edit`).hide()
$(`#question-display-${qid}-cancel-edit`).hide()
}
function editMode(qid) {
$(`#question-display-${qid}-content`).hide()
$(`#question-display-${qid}-button-edit`).hide()
$(`#question-display-${qid}-type-edit`).hide()
$(`#question-display-${qid}-edit`).show()
$(`#question-display-${qid}-validate-edit`).show()
$(`#question-display-${qid}-cancel-edit`).show()
}
function lockEdit(qid) {
$(`#question-display-${qid}-edit *`).prop('disabled', true)
$(`#question-display-${qid}-validate-edit`).prop('disabled', true)
$(`#question-display-${qid}-cancel-edit`).prop('disabled', true)
}
function unlockEdit(qid) {
$(`#question-display-${qid}-edit *`).prop('disabled', false)
$(`#question-display-${qid}-validate-edit`).prop('disabled', false)
$(`#question-display-${qid}-cancel-edit`).prop('disabled', false)
function internalCreateQuestion(i){
qu = questions[i]
qid = qu["id"]
newhtml = `
<li id="q-${qid}">
<div class="content-box" id="q-${qid}-content"/>
<div class="content-box" id="q-${qid}-edit"/>
<div class="button-box">
<button class="display-button display-button-${qid}" id="q-${qid}-button-edit">Edit</button>
<button class="display-button display-button-${qid}" id="q-${qid}-type-edit">Type</button>
<button class="display-button display-button-${qid}" id="q-${qid}-remove">Supprimer</button>
<button class="edit-button edit-button-${qid}" id="q-${qid}-validate-edit">Valider</button>
<button class="edit-button edit-button-${qid}" id="q-${qid}-cancel-edit">Annuler</button>
<button class="reorder-button" id="q-${qid}-reorder-uup">Uup</button>
<button class="reorder-button" id="q-${qid}-reorder-up">Up</button>
<button class="reorder-button" id="q-${qid}-reorder-down">Down</button>
<button class="reorder-button" id="q-${qid}-reorder-ddown">DDown</button>
</div>
</li>
`
newdom = $('<li/>').html(newhtml).contents()
$("#questions-list").append(newdom)
const theqid = qid
$(`#q-${qid}-button-edit`).on('click',e => edit(theqid))
$(`#q-${qid}-type-edit`).on('click',e => setType(theqid))
$(`#q-${qid}-remove`).on('click',e => removeQuestion(theqid))
$(`#q-${qid}-validate-edit`).on('click',e => commitEdit(theqid))
$(`#q-${qid}-cancel-edit`).on('click',e => cancelEdit(theqid))
$(`#q-${qid}-reorder-uup`).on('click',e => reorderMove(theqid,'uup'))
$(`#q-${qid}-reorder-up`).on('click',e => reorderMove(theqid,'up'))
$(`#q-${qid}-reorder-down`).on('click',e => reorderMove(theqid,'down'))
$(`#q-${qid}-reorder-ddown`).on('click',e => reorderMove(theqid,'ddown'))
newcontent = QTYPES[qu["type"]]["display"](qid,qu["value"])
$(`#q-${qid}-content`).replaceWith(newcontent)
newedit = QTYPES[qu["type"]]["edit"](qid,qu["value"])
$(`#q-${qid}-edit`).replaceWith(newedit)
contentMode(qid)
}
function setType(i,id) {
function contentMode(qid) {
$(`#q-${qid}-content`).show()
$(`.display-button-${qid}`).show()
$(`#q-${qid}-edit`).hide()
$(`.edit-button-${qid}`).hide()
$('.reorder-button').hide()
}
function editMode(qid) {
$(`#q-${qid}-content`).hide()
$(`.display-button-${qid}`).hide()
$(`#q-${qid}-edit`).show()
$(`.edit-button-${qid}`).show()
$('.reorder-button').hide()
}
function lockEdit(qid) {
$(`#q-${qid}-edit *`).prop('disabled', true)
$(`.edit-button-${qid}`).prop('disabled', true)
}
function unlockEdit(qid) {
$(`#q-${qid}-edit *`).prop('disabled', false)
$(`.edit-button-${qid}`).prop('disabled', false)
}
function contentNameMode() {
$("#question-name").show()
$(`#edit-question-name-button`).show()
$("#edit-question-name").hide()
$(`#edit-question-name-confirm`).hide()
$(`#edit-question-name-cancel`).hide()
}
function editNameMode() {
$("#question-name").hide()
$(`#edit-question-name-button`).hide()
$("#edit-question-name").show()
$(`#edit-question-name-confirm`).show()
$(`#edit-question-name-cancel`).show()
}
function reorderMode() {
$(`.display-button`).hide()
$(`.edit-button`).hide()
$(`.reorder-button`).show()
for (let qid of editing) {
lockEdit(qid)
}
}
function unReorderMode() {
$(`.reorder-button`).hide()
$(`.display-button`).show()
for (let qid of editing) {
$(`.display-button-${qid}`).hide()
$(`.edit-button-${qid}`).show()
editMode(qid)
unlockEdit(qid)
}
}
function setType(qid) {
//TODO à faire ^^
}
function edit(id, qid) {
function edit(qid) {
editing.add(qid)
editMode(qid)
}
function commitEdit(i,qid) {
function commitEdit(qid) {
const newvalue = readEditBlockBCC(qid)
lockEdit(qid)
$.ajax({
@ -134,34 +207,226 @@
unlockEdit(qid)
return
}
i = qpositions[qid]
questions[i]["value"] = newvalue
newcontent = QTYPES[questions[i]["type"]]["display"](qid,questions[i]["value"])
$(`#question-display-${qid}-content`).replaceWith(newcontent)
$(`#q-${qid}-content`).replaceWith(newcontent)
unlockEdit(qid)
editing.delete(qid)
contentMode(qid)
}
})
}
function cancelEdit(i,qid) {
function removeQuestion(qid) {
$(`#q-${qid}-remove`).prop('disabled',true)
if(!confirm(`Voulez-vous vraiment supprimer la question ${qpositions[qid]+1}`)) {
$(`#q-${qid}-remove`).prop('disabled', false)
return;
}
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/remove-question/"+qid,
type: "POST",
contentType: 'application/json',
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$(`#q-${qid}-remove`).prop('disabled', false)
return
}
$(`#q-${qid}-remove`).prop('disabled', false)
i = qpositions[qid]
qpositions[qid] = null
questions.splice(i,1)
$(`#q-${qid}`).remove()
}
})
}
function cancelEdit(qid) {
i = qpositions[qid]
newedit = QTYPES[questions[i]["type"]]["edit"](qid,questions[i]["value"])
$(`#question-display-${qid}-edit`).replaceWith(newedit)
$(`#q-${qid}-edit`).replaceWith(newedit)
editing.delete(qid)
contentMode(qid)
}
function commitEditName() {
const newname = $("#edit-question-name").val()
$("#edit-question-name").prop('disabled', true)
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/set-name",
type: "POST",
data: newname,
dataType: 'json',
contentType: 'text/plain',
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$("#edit-question-name").prop('disabled', false)
return
}
quizzName = newname
$(`#question-name`).text(quizzName)
$("#edit-question-name").prop('disabled', false)
contentNameMode()
}
})
}
function cancelEditName(i,qid) {
$("#edit-question-name").val(quizzName)
contentNameMode()
}
function addQuestion() {
$("#add-question").prop('disabled', true)
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/add-question",
type: "POST",
contentType: 'application/json',
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$("#add-question").prop('disabled', false)
return
}
q = {id: res["id"], type: res["type"], value: res["value"]}
qpositions[q["id"]] = questions.length
questions.push(q)
internalCreateQuestion(questions.length-1)
$("#add-question").prop('disabled', false)
}
})
}
// Map newindex -> oldindex
ordermapping = []
function startReorder() {
ordermapping = new Array(questions.length)
for (let i=0;i<questions.length;i++){
ordermapping[i] = i
}
reorderMode()
}
function reorderMove(qid,direction){
// Actual position of qid
i = ordermapping.indexOf(qpositions[qid])
switch(direction) {
case "uup":
j = 0
break
case "up":
j = Math.max(i-1,0)
break
case "down":
j = Math.min(i+1,ordermapping.length-1)
break
case "ddown":
j = ordermapping.length-1
break
}
moveQuestion(i,j)
}
// Move question from index i to index j
function moveQuestion(i,j) {
console.log(`Moving ${i} to ${j}`)
if (i == j) return
// The question we move
qid = questions[ordermapping[i]]['id']
oldomi = ordermapping[i]
if (i<j){
for (let k=i;k<j;k++){
ordermapping[k] = ordermapping[k+1]
}
} else {
for (let k=i;k>j;k--){
ordermapping[k] = ordermapping[k-1]
}
}
ordermapping[j] = oldomi
if(j==ordermapping.length-1) {
$(`#q-${qid}`).appendTo($("#questions-list"))
} else {
// We want the one _currently_ at position j+1
qid2 = questions[ordermapping[j+1]]['id']
console.log(`Moving ${qid} before ${qid2}`)
$(`#q-${qid}`).insertBefore($(`#q-${qid2}`))
}
}
function commitReorder() {
$(".reorder-button").prop('disabled', true)
newIdOrders = []
for(let p=0;p<ordermapping.length;p++) {
newIdOrders[p] = questions[ordermapping[p]]["id"]
}
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/reorder-questions",
type: "POST",
contentType: 'application/json',
data: JSON.stringify(newIdOrders),
dataType: 'json',
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$(".reorder-button").prop('disabled', false)
return
}
// question position -> question data
newquestions=new Array(questions.length)
newqpositions={}
for(let p=0;p<ordermapping.length;p++){
newquestions[p] = questions[ordermapping[p]]
newqpositions[newquestions[p]['id']] = p
}
questions = newquestions
qpositions = newqpositions
$(".reorder-button").prop('disabled', false)
unReorderMode()
}
})
}
function cancelReorder() {
questionsBlock = $("#questions-list")
// We just put all the blocks back in order
for (let q of questions) {
qid = q["id"]
$(`#q-${qid}`).appendTo(questionsBlock)
}
unReorderMode()
}
function makeEditBlockBCC(id,question) {
html = `
<div class="content-box dcc-box" id="question-display-${id}-edit">
<h5><input id="question-display-${id}-edit-question"
<div class="content-box dcc-box" id="q-${id}-edit">
<h5><input id="q-${id}-edit-question"
type="text" value="${_.escape(question["text"])}"/></h5>
<ol>
<li class="right">&#x2611; <input id="question-display-${id}-edit-good"
<li class="right">&#x2611; <input id="q-${id}-edit-good"
type="text" value="${_.escape(question["good"])}"/></li>
<li class="wrong">&#x2612; <input id="question-display-${id}-edit-wrong1"
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong1"
type="text" value="${_.escape(question["wrong"][0])}"/></li>
<li class="wrong">&#x2612; <input id="question-display-${id}-edit-wrong2"
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong2"
type="text" value="${_.escape(question["wrong"][1])}"/></li>
<li class="wrong">&#x2612; <input id="question-display-${id}-edit-wrong3"
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong3"
type="text" value="${_.escape(question["wrong"][2])}"/></li>
</ol>
</div>
@ -170,11 +435,11 @@
return out
}
function readEditBlockBCC(id) {
questionText = $(`#question-display-${id}-edit-question`).val()
goodText = $(`#question-display-${id}-edit-good`).val()
wrongText1 = $(`#question-display-${id}-edit-wrong1`).val()
wrongText2 = $(`#question-display-${id}-edit-wrong2`).val()
wrongText3 = $(`#question-display-${id}-edit-wrong3`).val()
questionText = $(`#q-${id}-edit-question`).val()
goodText = $(`#q-${id}-edit-good`).val()
wrongText1 = $(`#q-${id}-edit-wrong1`).val()
wrongText2 = $(`#q-${id}-edit-wrong2`).val()
wrongText3 = $(`#q-${id}-edit-wrong3`).val()
return {
"text": questionText,
"good": goodText,
@ -183,7 +448,7 @@
}
function makeAnswerBlockBCC(id,question) {
html = `
<div class="content-box dcc-box" id="question-display-${id}-content">
<div class="content-box dcc-box" id="q-${id}-content">
<h5>${_.escape(question["text"])}</h5>
<ol>
<li class="right">&#x2611; ${_.escape(question["good"])}</li>
@ -197,6 +462,14 @@
return out
}
contentNameMode()
$("#reorder-questions").on("click",startReorder)
$("#reorder-questions-commit").on("click",commitReorder)
$("#reorder-questions-cancel").on("click",cancelReorder)
$("#edit-question-name-button").on("click",editNameMode)
$("#edit-question-name-confirm").on("click",commitEditName)
$("#edit-question-name-cancel").on("click",cancelEditName)
$("#add-question").on("click",addQuestion)
getdata()
</script>
</main>

View File

@ -7,10 +7,17 @@
<body>
<div th:replace="~{header}"/>
<main>
<h3>Quizz à répondre</h3>
<ul>
<li th:if="${#lists.isEmpty(quizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${quizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></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>
</ul>
<h3>Quizz à éditer</h3>
<ul>
<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>
</ul>
<a sec:authorize="hasAuthority('CREATE_QUIZZ')" th:href="@{/questions/new-quizz}">Nouveau Quizz</a>
</main>
</body>
</html>

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 = "cash"
}
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>