Added logic for quizz, need to connect everything

This commit is contained in:
Samy Avrillon 2025-02-27 13:20:13 +01:00
parent 001ff8b5cb
commit be01c925a3
Signed by: Mysaa
GPG Key ID: 0220AC4A3D6A328B
20 changed files with 500 additions and 23 deletions

44
ERD-Misael.erd Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<diagram version="1" name="ERD-Misael.erd">
<entities>
<data-source id="postgres-jdbc-195261858a3-67acab6d92533937">
<entity id="1" name="answers" fq-name="misael.answers" order="0" font="Segoe UI:9:0" x="40" y="50">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="2" name="questions" fq-name="misael.questions" order="1" font="Segoe UI:9:0" x="279" y="40">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="3" name="quizz" fq-name="misael.quizz" order="2" font="Segoe UI:9:0" x="510" y="127">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="4" name="quizzf" fq-name="misael.quizzf" order="3" font="Segoe UI:9:0" x="279" y="224">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="5" name="roles" fq-name="misael.roles" order="4" font="Segoe UI:9:0" x="709" y="418">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="6" name="users" fq-name="misael.users" order="5" font="Segoe UI:9:0" x="709" y="254">
<path name="misael"/>
<path name="misael"/>
</entity>
<entity id="7" name="users_roles" fq-name="misael.users_roles" order="6" font="Segoe UI:9:0" x="510" y="352">
<path name="misael"/>
<path name="misael"/>
</entity>
</data-source>
</entities>
<relations>
<relation name="fk54dobrdq2u51m4u8s7kg0as8v" fq-name="misael.answers.fk54dobrdq2u51m4u8s7kg0as8v" type="fk" pk-ref="2" fk-ref="1"/>
<relation name="fkq12h25ynjok1m497gwos511te" fq-name="misael.questions.fkq12h25ynjok1m497gwos511te" type="fk" pk-ref="3" fk-ref="2"/>
<relation name="fk4ukbg2yaa93gs5nx1s5d9rqu4" fq-name="misael.quizzf.fk4ukbg2yaa93gs5nx1s5d9rqu4" type="fk" pk-ref="3" fk-ref="4"/>
<relation name="fkj6m8fwv7oqv74fcehir1a9ffy" fq-name="misael.users_roles.fkj6m8fwv7oqv74fcehir1a9ffy" type="fk" pk-ref="5" fk-ref="7"/>
<relation name="fkfeoogns8m4m4hvno1ttqb30wm" fq-name="misael.quizz.fkfeoogns8m4m4hvno1ttqb30wm" type="fk" pk-ref="6" fk-ref="3"/>
<relation name="fkuj3h8r3i4lnxukdqobapwbuq" fq-name="misael.quizzf.fkuj3h8r3i4lnxukdqobapwbuq" type="fk" pk-ref="6" fk-ref="4"/>
<relation name="fk2o0jvgh89lemvvo17cbqvdxaa" fq-name="misael.users_roles.fk2o0jvgh89lemvvo17cbqvdxaa" type="fk" pk-ref="6" fk-ref="7"/>
</relations>
</diagram>

View File

@ -1,6 +1,10 @@
package com.bernard.misael;
import java.security.Principal;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -21,10 +25,14 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.bernard.misael.model.User;
import com.bernard.misael.service.UserService;
import jakarta.annotation.PostConstruct;
@Configuration
@EnableWebSecurity
public class SpringSecurity {
public static final Logger LOG = Logger.getLogger(SpringSecurity.class.getName());
@Autowired
private UserDetailsService userDetailsService;

View File

@ -31,7 +31,10 @@ public class Answer {
@JoinColumn(name = "question",nullable = false)
private Question question;
@Lob
@ManyToOne
@JoinColumn(name = "form",nullable = false)
private QuizzForm form;
private String value;
}

View File

@ -1,5 +1,11 @@
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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -19,6 +25,9 @@ public class Question {
@Column(name="id")
private long id;
@Column(nullable=false)
QTypes type;
@Column(nullable=false)
private int index;
@ -26,7 +35,21 @@ public class Question {
@JoinColumn(name = "quizz",nullable = false)
private Quizz quizz;
@Lob
private String value;
transient QuestionType qtype = null;
public QuestionType getQT() {
if(qtype==null){
ObjectMapper om = new ObjectMapper();
JsonNode jsNode;
try {
jsNode = om.readTree(value);
qtype = type.getConstructor().apply(jsNode);
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
return qtype;
}
}

View File

@ -0,0 +1,79 @@
package com.bernard.misael.questions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class DccQuestion implements QuestionType{
String text;
String goodAnswer;
String[] wrongAnswers;
public DccQuestion(JsonNode data){
text = data.get("text").asText();
goodAnswer = data.get("good").asText();
JsonNode wrong = data.get("wrong");
wrongAnswers = new String[wrong.size()];
for(int i = 0; i<wrong.size();i++)
wrongAnswers[i] = wrong.get(i).asText();
}
@Override
public JsonNode clientQuestionData(int step, JsonNode answer) {
ObjectNode o = JsonNodeFactory.instance.objectNode();
switch (step) {
case 0:
o.put("text", this.text);
return o;
case 1:
Random r = new Random();
int k = answer.get("type").asInt();
switch(k) {
case 0:
return o;
case 2:
case 4:
List<String> answers = new ArrayList<>(k);
answers.add(goodAnswer);
for(int i = 0;i<k;i++)
answers.add(wrongAnswers[i]);
Collections.shuffle(answers, r);
o.putArray("answers").addAll(answers.stream().map(JsonNodeFactory.instance::textNode).collect(Collectors.toList()));
return o;
default:
throw new IllegalStateException("Invalid Answer type");
}
default:
throw new IllegalArgumentException("Question step invalid : "+step);
}
}
@Override
public AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer) {
ObjectNode o = JsonNodeFactory.instance.objectNode();
switch (step) {
case 0:
int answerType = clientAnswer.asInt();
if(answerType != 0 && answerType != 2 && answerType != 4)
throw new IllegalArgumentException("Illegal answer type: "+answerType);
o.put("type", answerType);
return new AnswerResult(o, false, 1);
case 1:
String answer = clientAnswer.asText();
o = oldAnswer.deepCopy();
o.put("answer", answer);
return new AnswerResult(o, true, -1);
default:
throw new IllegalArgumentException("Question step invalid : "+step);
}
}
}

View File

@ -0,0 +1,27 @@
package com.bernard.misael.questions;
import java.util.function.Function;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum QTypes {
DCC(1,DccQuestion.class,DccQuestion::new);
private int id;
private Class<? extends QuestionType> type;
private Function<JsonNode,QuestionType> constructor;
public static QTypes findById(final int id) {
for(QTypes value : QTypes.values())
if (value.id == id)
return value;
return null;
}
}

View File

@ -0,0 +1,22 @@
package com.bernard.misael.questions;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Getter;
public interface QuestionType {
JsonNode clientQuestionData(int step, JsonNode answer);
AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer);
@AllArgsConstructor
@Getter
public static class AnswerResult {
private JsonNode newAnswer;
private boolean nextQuestion;
private int nextStep;
}
}

View File

@ -0,0 +1,13 @@
package com.bernard.misael.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Answer;
import com.bernard.misael.model.Question;
import com.bernard.misael.model.QuizzForm;
public interface AnswerRepository extends JpaRepository<Answer,Long> {
public Answer findByFormAndQuestion(QuizzForm qf, Question q);
}

View File

@ -0,0 +1,13 @@
package com.bernard.misael.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Question;
import com.bernard.misael.model.Quizz;
public interface QuestionRepository extends JpaRepository<Question,Long> {
public Question findByQuizzAndIndex(Quizz quizz, int index);
}

View File

@ -0,0 +1,13 @@
package com.bernard.misael.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Quizz;
import com.bernard.misael.model.QuizzForm;
import com.bernard.misael.model.User;
public interface QuizzFormRepository extends JpaRepository<QuizzForm,Long> {
public QuizzForm findByUserAndQuizz(User u, Quizz q);
}

View File

@ -0,0 +1,14 @@
package com.bernard.misael.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull;
import com.bernard.misael.model.Quizz;
public interface QuizzRepository extends JpaRepository<Quizz,Long> {
public @NonNull Optional<Quizz> findById(@NonNull Long id);
}

View File

@ -0,0 +1,11 @@
package com.bernard.misael.service;
import com.bernard.misael.model.User;
import com.fasterxml.jackson.databind.JsonNode;
public interface QuizzManager {
public JsonNode answer(User user, long quizzId,JsonNode data);
public JsonNode next(User user, long quizzId);
}

View File

@ -0,0 +1,115 @@
package com.bernard.misael.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bernard.misael.model.Answer;
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.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.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
@Service
public class QuizzManagerImpl implements QuizzManager {
@Autowired
QuizzFormRepository qfRepository;
@Autowired
QuizzRepository qRepository;
@Autowired
QuestionRepository questionRepository;
@Autowired
AnswerRepository answerRepository;
@Override
public JsonNode answer(User user, long quizzId, JsonNode data) {
//TODO replace conversions String <-> JsonNode to Answer type
Quizz quizz = qRepository.findById(quizzId).get();
QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf.isDone())
throw new UnsupportedOperationException();//TODO add more precise exceptions here
int qindex = qf.getCurrentQuestion();
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q);
ObjectMapper om = new ObjectMapper();
JsonNode answerData;
try {
answerData = om.readTree(answer.getValue());
AnswerResult result = q.getQT().clientAnswers(step, answerData, data);
if(result.isNextQuestion()) {
qf.setCurrentQuestion(qindex+1);
} else {
qf.setAnswerStep(result.getNextStep());
}
answer.setValue(om.writeValueAsString(result.getNewAnswer()));
//XXX Persist QF ?
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("result", JsonNodeFactory.instance.textNode("ok"));
return out;
} catch (JsonProcessingException e) {
e.printStackTrace();
//TODO autogenerated
return null;
}
}
@Override
public JsonNode next(User user, long quizzId) {
Quizz quizz = qRepository.findById(quizzId).get();
QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null){
// We should create the quizzform
qf = newQuizzForm(user, quizz);
}
if(qf.isDone())
throw new UnsupportedOperationException();//TODO add more precise exceptions here
int qindex = qf.getCurrentQuestion();
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q);
ObjectMapper om = new ObjectMapper();
JsonNode answerData;
try {
answerData = om.readTree(answer.getValue());
JsonNode qdata = q.getQT().clientQuestionData(step, answerData);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("index", JsonNodeFactory.instance.numberNode(qindex));
out.set("step", JsonNodeFactory.instance.numberNode(step));
out.set("index", qdata);
return out;
} catch (JsonProcessingException e){
//XXX autogenerated
return null;
}
}
public QuizzForm newQuizzForm(User user, Quizz quizz) {
QuizzForm qf = new QuizzForm();
qf.setUser(user);
qf.setQuizz(quizz);
qf.setDone(false);
qf.setCurrentQuestion(0);
qf.setAnswerStep(0);
//XXX Persist QF ?
return qf;
}
}

View File

@ -0,0 +1,5 @@
package com.bernard.misael.service.exception;
public class ShouldNotAnswerNowException extends Exception{
}

View File

@ -1,5 +1,14 @@
package com.bernard.misael.web;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.logging.Logger;
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.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -17,6 +26,7 @@ import jakarta.validation.Valid;
@Controller
public class AuthController {
@Autowired
private UserService userService;
public AuthController(UserService userService) {
@ -71,4 +81,19 @@ public class AuthController {
userService.saveUser(userDto);
return "redirect:/adduser?success";
}
@GetMapping("/create-mysaa-user")
public ResponseEntity<String> checkMysaaExists() {
if(userService.findUserByName("mysaa") == null) {
UserDto u = new UserDto();
Random r = new Random();
UUID pass = new UUID(r.nextLong(),r.nextLong());
u.setName("mysaa");
u.setPassword(pass.toString());
userService.saveUser(u);
Logger.getLogger(AuthController.class.getName()).warning("Created user mysaa with password "+pass.toString());
}
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -3,15 +3,22 @@ package com.bernard.misael.web;
import java.security.Principal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository;
import com.bernard.misael.service.UserService;
import com.bernard.misael.service.QuizzManager;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@Controller
@ -21,6 +28,9 @@ public class QuestionsController {
@Autowired
UserRepository ur;
@Autowired
QuizzManager qm;
@GetMapping("/quizz")
/*
* List all quizz
@ -45,4 +55,35 @@ public class QuestionsController {
return "forms.html";
}
@GetMapping("/form/{q}")
public String formpage(@PathVariable("q") long quizzId, Principal p, Model m) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
//XXX test that user can answer quizz
m.addAttribute("formid", quizzId);
return "form";
}
@GetMapping("/question/{q}")
public ResponseEntity<JsonNode> question(@PathVariable("q") long quizzId, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
JsonNode out = qm.next(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/answer/{q}")
public JsonNode answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
JsonNode out = qm.answer(u, quizzId, data);
return out;
}
}

View File

@ -1,6 +1,6 @@
spring:
datasource:
url: jdbc:mysql://127.0.0.1:10051/misael
url: jdbc:postgres://127.0.0.1:10051/misael
username: misael
password: misael-dev
hikari:
@ -10,18 +10,25 @@ spring:
locations: classpath:db/migration/structure, classpath:db/migration/data
validate-on-migrate: true
default-schema: misael
create-schemas: true
session:
jdbc:
initialize-schema: always
security:
user:
name: mysaa
# jpa:
# properties:
# javax:
# persistence:
# schema-generation:
# create-source: metadata
# scripts:
# action: update
# create-target: db-migration.sql
# database:
# action: none
# hibernate:
# ddl-auto: none
# generate-ddl: true
jpa:
properties:
javax:
persistence:
schema-generation:
create-source: metadata
scripts:
action: update
create-target: db-migration.sql
database:
action: none
hibernate:
ddl-auto: none
generate-ddl: true

View File

@ -1,11 +1,12 @@
create table answers (id bigint generated by default as identity, value oid, question bigint not null, primary key (id));
create table questions (id bigint generated by default as identity, index integer not null, value oid, quizz bigint not null, primary key (id));
create table answers (id bigint generated by default as identity, value varchar(255), form bigint not null, question bigint not null, primary key (id));
create table questions (id bigint generated by default as identity, index integer not null, type smallint not null check (type between 0 and 0), value varchar(255), quizz bigint not null, primary key (id));
create table quizz (id bigint generated by default as identity, is_public boolean default false not null, name varchar(255) not null, question_count integer default 0 not null, owner bigint, primary key (id));
create table quizzf (id bigint generated by default as identity, answer_step integer not null, current_question integer not null, done boolean not null, quizz bigint not null, answerer bigint not null, primary key (id));
alter table if exists quizz drop constraint if exists UKc1plspc0ecmwqqpfaf24avb4c;
alter table if exists quizz add constraint UKc1plspc0ecmwqqpfaf24avb4c unique (name);
alter table if exists answers add constraint FKjtutlsv5n10071rq261lcjovm foreign key (form) references quizzf;
alter table if exists answers add constraint FK54dobrdq2u51m4u8s7kg0as8v foreign key (question) references questions;
alter table if exists questions add constraint FKq12h25ynjok1m497gwos511te foreign key (quizz) references quizz;
alter table if exists quizz add constraint FKfeoogns8m4m4hvno1ttqb30wm foreign key (owner) references users;
alter table if exists quizzf add constraint FK4ukbg2yaa93gs5nx1s5d9rqu4 foreign key (quizz) references quizz;
alter table if exists quizzf add constraint FKuj3h8r3i4lnxukdqobapwbuq foreign key (answerer) references users;
alter table if exists quizzf add constraint FK7gvxodb5t2n68ot577ig9u3w6 foreign key (answerer) references users;

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
Youhou ! (Y devrait y avoir le form ici ^^) <span th:text="${formid}"/>
</main>
</body>
</html>

View File

@ -9,7 +9,7 @@
<main>
<ul>
<li>Premier item ?</li>
<li th:each="q : ${quizz}"><a th:href="@{/questions/newform/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
<li th:each="q : ${quizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul>
</main>
</body>