Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

92 changed files with 411 additions and 7101 deletions

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

38
.gitignore vendored
View File

@ -1,37 +1 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
__pycache__/

View File

@ -1,23 +0,0 @@
FROM gradle:jdk21 AS builder
WORKDIR /build/
ADD src /build/src/
ADD build.gradle /build/
ADD settings.gradle /build/
ADD versions.properties /build/
RUN gradle assemble
FROM eclipse-temurin:21-noble
WORKDIR /opt/
ADD "https://download.red-gate.com/maven/release/com/redgate/flyway/flyway-commandline/11.9.1/flyway-commandline-11.9.1-linux-x64.tar.gz" /opt/flyway.tar.gz
RUN tar -xvzf /opt/flyway.tar.gz -C /opt/
RUN ln -s /opt/flyway-11.9.1/flyway /usr/local/bin
WORKDIR /app/
COPY --from=builder /build/build/libs/misael-beta.jar /app/misael.jar
COPY --from=builder /build/build/resources/ /app/resources/
ADD launcher.sh /app/
ENTRYPOINT ["bash","./launcher.sh"]

View File

@ -1,44 +0,0 @@
<?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,13 +0,0 @@
# Misael
## For dev
You need to setup a postgres server at localhost:5432, with user misael with password misael-dev
Then, you run the database migration `./gradlew flywayMigrate`
And finaly we run `./gradlew bootRun`
## For deployment
The compose file is enough.
`launcher.sh` checks that the migrations are applied and then runs the spring boot server on port 8080

View File

@ -1,71 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.flywaydb:flyway-database-postgresql:_'
classpath 'org.postgresql:postgresql:_'
}
}
plugins {
id 'java'
id 'org.springframework.boot'
id 'org.flywaydb.flyway'
id 'io.spring.dependency-management'
}
group = 'com.bernard'
version = 'beta'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation Spring.boot.data.jpa
implementation Spring.boot.jdbc
implementation Spring.boot.thymeleaf
implementation Spring.boot.security
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:_'
implementation Spring.boot.web
implementation 'org.springframework:spring-jdbc:_'
implementation 'org.flywaydb:flyway-core:_'
implementation 'org.flywaydb:flyway-database-postgresql:_'
implementation Spring.session.jdbc
implementation 'jakarta.validation:jakarta.validation-api:_'
runtimeOnly 'org.webjars:jquery:_'
runtimeOnly 'org.webjars:lodash:_'
developmentOnly Spring.boot.devTools
runtimeOnly 'org.postgresql:postgresql:_'
testImplementation Spring.boot.test
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:_'
implementation 'com.fasterxml.jackson.core:jackson-databind:_'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:_'
//Lombok
compileOnly 'org.projectlombok:lombok:_'
annotationProcessor 'org.projectlombok:lombok:_'
testCompileOnly 'org.projectlombok:lombok:_'
testAnnotationProcessor 'org.projectlombok:lombok:_'
}
flyway {
url = "jdbc:postgresql://127.0.0.1:5432/misael"
user = 'misael'
password = 'misael-dev'
driver = 'org.postgresql.Driver'
schemas = ['misael']
}
tasks.named('test') {
useJUnitPlatform()
}

View File

@ -1,24 +0,0 @@
services:
postgres:
image: 'postgres:latest'
environment:
- 'POSTGRES_DB=misael'
- 'POSTGRES_PASSWORD=misael-dev'
- 'POSTGRES_USER=misael'
networks:
- misanetwork
misael:
image: 'mysaa/misael:latest'
build: .
environment:
- 'MISAEL_DATABASE=jdbc:postgresql://postgres:5432/misael'
- 'MISAEL_PASSWORD=misael-dev'
ports:
- 8080:8080
networks:
- misanetwork
networks:
misanetwork:
driver: bridge

147
flake.lock generated
View File

@ -1,147 +0,0 @@
{
"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
}

View File

@ -1,84 +0,0 @@
{
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.writeShellScriptBin "misael" ''
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
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;
};
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored
View File

@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored
View File

@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,6 +0,0 @@
echo "Checking Database Migration"
flyway "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:resources/main/db/migration/" -schemas=misael migrate
echo "Running" java -jar misael.jar --spring.datasource.url=$MISAEL_DATABASE --spring.datasource.password=$MISAEL_PASSWORD
java -jar misael.jar --spring.datasource.url=$MISAEL_DATABASE --spring.datasource.password=$MISAEL_PASSWORD

0
lists/__init__.py Normal file
View File

11
lists/admin.py Normal file
View File

@ -0,0 +1,11 @@
from django.contrib import admin
from .models import *
admin.site.register(Oeuvre)
admin.site.register(JeuVideo)
admin.site.register(Film)
admin.site.register(Serie)
admin.site.register(Livre)
admin.site.register(Avis)
admin.site.register(Todo)

6
lists/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ListsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lists'

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.1 on 2024-01-16 01:18
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='JeuVideo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('sortie', models.DateTimeField(verbose_name='Date de publication')),
],
),
]

View File

@ -0,0 +1,75 @@
# Generated by Django 5.0.1 on 2024-01-16 13:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lists', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Oeuvre',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
],
),
migrations.RemoveField(
model_name='jeuvideo',
name='id',
),
migrations.RemoveField(
model_name='jeuvideo',
name='name',
),
migrations.CreateModel(
name='Film',
fields=[
('oeuvre_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lists.oeuvre')),
('sortie', models.DateTimeField(verbose_name='Date de publication')),
],
bases=('lists.oeuvre',),
),
migrations.CreateModel(
name='Livre',
fields=[
('oeuvre_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lists.oeuvre')),
('sortie', models.DateTimeField(verbose_name='Date de publication')),
],
bases=('lists.oeuvre',),
),
migrations.CreateModel(
name='Serie',
fields=[
('oeuvre_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lists.oeuvre')),
('sortie', models.DateTimeField(verbose_name='Date de publication')),
],
bases=('lists.oeuvre',),
),
migrations.CreateModel(
name='Avis',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('texte', models.CharField()),
('oeuvre', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lists.oeuvre')),
],
),
migrations.AddField(
model_name='jeuvideo',
name='oeuvre_ptr',
field=models.OneToOneField(auto_created=True, default=0, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lists.oeuvre'),
preserve_default=False,
),
migrations.CreateModel(
name='Todo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rank', models.IntegerField()),
('oeuvre', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lists.oeuvre')),
],
),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 5.0.1 on 2024-01-16 15:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lists', '0002_oeuvre_remove_jeuvideo_id_remove_jeuvideo_name_film_and_more'),
]
operations = [
migrations.RemoveField(
model_name='film',
name='sortie',
),
migrations.RemoveField(
model_name='jeuvideo',
name='sortie',
),
migrations.RemoveField(
model_name='livre',
name='sortie',
),
migrations.RemoveField(
model_name='serie',
name='sortie',
),
migrations.AddField(
model_name='oeuvre',
name='sortie',
field=models.IntegerField(default=0, verbose_name='Année de sortie'),
preserve_default=False,
),
migrations.AlterField(
model_name='avis',
name='texte',
field=models.TextField(),
),
migrations.AlterField(
model_name='oeuvre',
name='name',
field=models.CharField(max_length=200, verbose_name='Nom'),
),
]

View File

29
lists/models.py Normal file
View File

@ -0,0 +1,29 @@
from django.db import models
class Oeuvre(models.Model):
name = models.CharField("Nom",max_length=200)
sortie = models.IntegerField("Année de sortie")
def __str__(self):
return self.name + (" (" + str(self.sortie) + ")" if self.sortie else "")
class JeuVideo(Oeuvre):
pass
class Film(Oeuvre):
pass
class Serie(Oeuvre):
pass
class Livre(Oeuvre):
pass
class Avis(models.Model):
oeuvre = models.ForeignKey(Oeuvre, on_delete=models.CASCADE)
texte = models.TextField()
class Todo(models.Model):
oeuvre = models.ForeignKey(Oeuvre, on_delete=models.CASCADE)
rank = models.IntegerField()

3
lists/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
lists/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
]

6
lists/views.py Normal file
View File

@ -0,0 +1,6 @@
from django.shortcuts import render
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'misael.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
misael/__init__.py Normal file
View File

16
misael/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for misael project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'misael.settings')
application = get_asgi_application()

128
misael/settings.py Normal file
View File

@ -0,0 +1,128 @@
"""
Django settings for misael project.
Generated by 'django-admin startproject' using Django 5.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-xww0xj_dh_2xtex&q%y6zlsa_$anz&4ijs(94skyhu_ijt43m@'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'lists.apps.ListsConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'misael.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'misael.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'misael',
'USER': 'misael',
'PASSWORD': 'UCmRhUBXS6HX9NeUs15oKxLZAZvixE7N+ONLHWRk1ag=',
'HOST': 'localhost',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

23
misael/urls.py Normal file
View File

@ -0,0 +1,23 @@
"""
URL configuration for misael project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include,path
urlpatterns = [
path("lists/", include("lists.urls")),
path('admin/', admin.site.urls),
]

16
misael/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for misael project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'misael.settings')
application = get_wsgi_application()

View File

@ -1,4 +0,0 @@
plugins {
id 'de.fayard.refreshVersions' version '0.60.5'
}
rootProject.name = 'misael'

View File

@ -1,46 +0,0 @@
package com.bernard.misael;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.Role;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String pseudo) throws UsernameNotFoundException {
User user = userRepository.findByName(pseudo);
if (user != null) {
Stream<Privilege> inducedPrivileges = user.getRoles().stream()
.map(Role::getPrivileges)
.map(Set<Privilege>::stream)
.flatMap(Function.identity())
.sorted()
.distinct();
Stream<Role> roles = user.getRoles().stream();
return new org.springframework.security.core.userdetails.User(user.getName(),
user.getPassword(),
Stream.concat(inducedPrivileges, roles).toList()
);
}else{
throw new UsernameNotFoundException("Invalid username or password.");
}
}
}

View File

@ -1,13 +0,0 @@
package com.bernard.misael;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MisaelApplication {
public static void main(String[] args) {
SpringApplication.run(MisaelApplication.class, args);
}
}

View File

@ -1,60 +0,0 @@
package com.bernard.misael;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SpringSecurity {
public static final Logger LOG = Logger.getLogger(SpringSecurity.class.getName());
@Autowired
private UserDetailsService userDetailsService;
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers("/**").permitAll()
).formLogin(
form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.permitAll()
).logout(
logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.permitAll()
);
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}

View File

@ -1,14 +0,0 @@
package com.bernard.misael;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
@Configuration
public class ThymeleafConfig {
@Bean
public SpringSecurityDialect springSecurityDialect(){
return new SpringSecurityDialect();
}
}

View File

@ -1,52 +0,0 @@
package com.bernard.misael.model;
import com.bernard.misael.service.JsonNodeConverter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="answers")
public class Answer {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="id")
private long id;
@ManyToOne
@JoinColumn(name = "question",nullable = false)
private Question question;
@ManyToOne
@JoinColumn(name = "form",nullable = false)
private QuizzForm form;
@Convert(converter = JsonNodeConverter.class)
private JsonNode value;
public Answer(QuizzForm qf, Question q) {
super();
this.setQuestion(q);
this.setForm(qf);
this.setValue(JsonNodeFactory.instance.objectNode());
}
}

View File

@ -1,14 +0,0 @@
package com.bernard.misael.model;
import org.springframework.security.core.GrantedAuthority;
public enum Privilege implements GrantedAuthority {
LIST_USERS,ADD_USERS,LIST_QUIZZ,CREATE_QUIZZ,VIEW_ALL_FORMS;
@Override
public String getAuthority() {
return this.name();
}
}

View File

@ -1,49 +0,0 @@
package com.bernard.misael.model;
import com.bernard.misael.questions.QTypes;
import com.bernard.misael.questions.QuestionType;
import com.bernard.misael.service.JsonNodeConverter;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@NoArgsConstructor
@Entity
@Table(name="questions")
public class Question {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="id")
private long id;
@Column(nullable=false)
@Setter
QTypes type;
@Column(nullable=false)
@Setter
private int index;
@ManyToOne
@JoinColumn(name = "quizz",nullable = false)
@Setter
private Quizz quizz;
@Setter
@Convert(converter = JsonNodeConverter.class)
private JsonNode value;
transient QuestionType qtype = null;
public QuestionType getQT() {
if(qtype==null){
qtype = type.construct(value);
}
return qtype;
}
}

View File

@ -1,45 +0,0 @@
package com.bernard.misael.model;
import java.util.Set;
import org.hibernate.annotations.ColumnDefault;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="quizz")
public class Quizz {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, unique=true)
private String name;
@ManyToOne
@JoinColumn(name="owner")
private User owner;
@Column(nullable=false)
@ColumnDefault("false")
private boolean isComplete;
@Column(nullable=false)
@ColumnDefault("0")
private int questionCount;
@OneToMany(mappedBy="quizz")
private Set<Question> questions;
@Column(nullable=true)
private Integer publicQuestionCount;
}

View File

@ -1,38 +0,0 @@
package com.bernard.misael.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="quizzf")
public class QuizzForm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne
@JoinColumn(name = "answerer",nullable = false)
private User user;
@ManyToOne
@JoinColumn(name = "quizz",nullable=false)
private Quizz quizz;
private boolean done;
private int currentQuestion;
private int answerStep;
}

View File

@ -1,62 +0,0 @@
package com.bernard.misael.model;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="roles")
public class Role implements GrantedAuthority
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, unique=true)
private String name;
@ManyToMany(mappedBy="roles")
private List<User> users;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
@CollectionTable(name = "role_privileges"
, joinColumns = @JoinColumn(name = "id"))
@Column(name = "privileges", nullable = false)
private Set<Privilege> privileges;
public Role(String name){
super();
this.setName(name);
this.setPrivileges(EnumSet.noneOf(Privilege.class));
}
@Override
public String getAuthority() {
return "ROLE_"+name;
}
}

View File

@ -1,54 +0,0 @@
package com.bernard.misael.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Entity
@Table(name="users")
public class User
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false,unique = true)
private String name;
@Column(nullable=false)
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(
name="users_roles",
joinColumns={@JoinColumn(name="USER_ID", referencedColumnName="ID")},
inverseJoinColumns={@JoinColumn(name="ROLE_ID", referencedColumnName="ID")})
private List<Role> roles = new ArrayList<>();
@OneToMany(mappedBy="owner")
private Set<Quizz> myQuizzs;
}

View File

@ -1,117 +0,0 @@
package com.bernard.misael.questions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.bernard.misael.service.exception.MalformedAnswerException;
import com.bernard.misael.service.exception.MalformedClientAnswerException;
import com.bernard.misael.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
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) throws QuestionTypeException{
ObjectNode o = JsonNodeFactory.instance.objectNode();
o.put("text", this.text);
switch (step) {
case 0:
return o;
case 1:
Random r = new Random();
if(!answer.has("type") || !answer.get("type").isInt()) throw new MalformedAnswerException();
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-1;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 MalformedAnswerException();
}
default:
throw new IllegalArgumentException("Question step invalid : "+step);
}
}
@Override
public AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer) throws QuestionTypeException {
ObjectNode o = JsonNodeFactory.instance.objectNode();
switch (step) {
case 0:
if(!clientAnswer.isInt()) throw new MalformedClientAnswerException();
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:
if(!clientAnswer.isTextual()) throw new MalformedClientAnswerException();
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);
}
}
public static Logger LOG = LoggerFactory.getLogger(DccQuestion.class);
public static boolean validate(JsonNode data) {
LOG.info(data.toPrettyString());
LOG.info(Stream.of(data.get("wrong")).map(p -> p.isTextual()+"->"+p).collect(Collectors.joining(";")));
return data.isObject() &&
data.has("text") && data.get("text").isTextual() &&
data.has("good") && data.get("good").isTextual() &&
data.has("wrong") && data.get("wrong").isArray() &&
data.get("wrong").size() >= 3 &&
StreamSupport.stream(Spliterators.spliterator(data.get("wrong").elements(),data.get("wrong").size(),Spliterator.SIZED),false).allMatch(p ->
p.isTextual()
);
}
public static final JsonNode getDefaultDccQuestion() {
ObjectNode q = JsonNodeFactory.instance.objectNode();
q.set("text",JsonNodeFactory.instance.textNode(""));
q.set("good",JsonNodeFactory.instance.textNode(""));
ArrayNode wrong = JsonNodeFactory.instance.arrayNode(3);
for(int i=0;i<3;i++)wrong.add(JsonNodeFactory.instance.textNode(""));
q.set("wrong",wrong);
return q;
}
}

View File

@ -1,38 +0,0 @@
package com.bernard.misael.questions;
import java.util.function.Function;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum QTypes {
DCC(1,DccQuestion.class,DccQuestion::new,DccQuestion::validate,DccQuestion.getDefaultDccQuestion());
private int id;
@SuppressWarnings("unused")
private Class<? extends QuestionType> type;
private Function<JsonNode,QuestionType> constructor;
private Function<JsonNode,Boolean> validator;
@Getter
private JsonNode defaultQuestion;
public static QTypes findById(final int id) {
for(QTypes value : QTypes.values())
if (value.id == id)
return value;
return null;
}
public QuestionType construct(JsonNode data){
return this.constructor.apply(data);
}
public boolean validate(JsonNode data){
return this.validator.apply(data);
}
}

View File

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

View File

@ -1,16 +0,0 @@
package com.bernard.misael.repository;
import java.util.List;
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);
public List<Answer> findByFormAndQuestionIn(QuizzForm qf, List<Question> qz);
}

View File

@ -1,18 +0,0 @@
package com.bernard.misael.repository;
import java.util.List;
import java.util.Set;
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);
public Set<Question> findByQuizz(Quizz quizz);
public List<Question> findByQuizzOrderByIndexAsc(Quizz quizz);
}

View File

@ -1,19 +0,0 @@
package com.bernard.misael.repository;
import java.util.List;
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 List<QuizzForm> findByQuizz(Quizz q);
public QuizzForm findByUserAndQuizz(User u, Quizz q);
public List<QuizzForm> findByUserAndDoneTrue(User u);
public List<QuizzForm> findByUserAndDoneFalse(User u);
}

View File

@ -1,20 +0,0 @@
package com.bernard.misael.repository;
import java.util.Optional;
import java.util.Set;
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> findByPublicQuestionCountIsNotNullAndIsCompleteTrue();
public @NonNull Set<Quizz> findByOwnerAndIsCompleteTrue(@NonNull User owner);
public @NonNull Set<Quizz> findByOwnerAndIsCompleteFalse(@NonNull User owner);
}

View File

@ -1,10 +0,0 @@
package com.bernard.misael.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Role;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
}

View File

@ -1,11 +0,0 @@
package com.bernard.misael.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.User;
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String pseudo);
}

View File

@ -1,74 +0,0 @@
package com.bernard.misael.service;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.lang.NonNull;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.bernard.misael.model.Privilege;
import com.bernard.misael.model.Role;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.RoleRepository;
import com.bernard.misael.repository.UserRepository;
import jakarta.transaction.Transactional;
@Component
public class AdminMaker implements
ApplicationListener<ContextRefreshedEvent> {
boolean alreadySetup = false;
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
if (alreadySetup)
return;
Logger log = Logger.getLogger("AdminMaker");
log.info("Checking that privileges and mysaa user exist");
Role adminRole = createRoleIfNotFound("ADMIN", EnumSet.allOf(Privilege.class));
createRoleIfNotFound("USER", EnumSet.noneOf(Privilege.class));
User mysaa = userRepository.findByName("mysaa");
if (mysaa == null) {
User user = new User();
user.setName("mysaa");
user.setPassword(passwordEncoder.encode("super"));
user.setRoles(Arrays.asList(adminRole));
userRepository.save(user);
}
alreadySetup = true;
log.info("Everything needed has been created");
}
@Transactional
private Role createRoleIfNotFound(
String name, EnumSet<Privilege> privileges) {
Role role = roleRepository.findByName(name);
if (role == null) {
role = new Role(name);
role.setPrivileges(privileges);
roleRepository.save(role);
}
return role;
}
}

View File

@ -1,32 +0,0 @@
package com.bernard.misael.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
public class JsonNodeConverter implements AttributeConverter<JsonNode, String> {
private ObjectMapper om;
public JsonNodeConverter() {
this.om = new ObjectMapper();
}
@Override
public String convertToDatabaseColumn(JsonNode attribute) {
return attribute.toString();
}
@Override
public JsonNode convertToEntityAttribute(String dbData) {
try {
return om.readTree(dbData);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@ -1,40 +0,0 @@
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;
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> 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);
public JsonNode removeQuestion(User user, long quizzId, long questionId);
public JsonNode reorderQuestions(User user, long quizzId, List<Long> newOrder);
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

@ -1,601 +0,0 @@
package com.bernard.misael.service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.*;
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.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.persistence.EntityNotFoundException;
@Service
public class QuizzManagerImpl implements QuizzManager {
@Autowired
UserRepository uRepository;
@Autowired
QuizzFormRepository qfRepository;
@Autowired
QuizzRepository qRepository;
@Autowired
QuestionRepository questionRepository;
@Autowired
AnswerRepository answerRepository;
@Autowired
UserService uService;
@Override
public JsonNode answer(User user, long quizzId, JsonNode data) {
if(!data.has("index") || !data.get("index").isInt())
return errorNode("Request should contain the question index");
if(!data.has("step") || !data.get("step").isInt())
return errorNode("Request should contain the answer step");
if(!data.has("data"))
return errorNode("Request should contain the answer data");
if(user == null)
return errorNode("You must be logged in to answer");
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)
return errorNode("The quizzform does not exist, ask the question first");
if(qf.isDone())
return errorNode("You're done with the quizz, you cannot answer anymore");
int qindex = qf.getCurrentQuestion();
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);
int step = qf.getAnswerStep();
if(step != data.get("step").intValue())
return errorNode("You are not answering the right step of the question (you answer step "+data.get("step").intValue()
+" where you should step question "+step+")");
Answer answer = answerRepository.findByFormAndQuestion(qf, q);
if(answer == null)
return errorNode("The database answer object does not exist, ask the question first");
JsonNode answerData = answer.getValue();
AnswerResult result;
try {
result = q.getQT().clientAnswers(step, answerData, data.get("data"));
} catch (MalformedAnswerException e) {
return errorNode("The previous answer stored in database is invalid");
} catch (MalformedClientAnswerException e) {
return errorNode("This answer is not valid here");
} catch (QuestionTypeException e) {
return errorNode("Unknown error from the QuestionType");
} catch (IllegalArgumentException e) {
return errorNode("The QuestionType did not recognize the step of the question");
}
if(result.isNextQuestion()) {
qf.setCurrentQuestion(qindex+1);
qf.setAnswerStep(0);
if(qf.getCurrentQuestion() >= quizz.getQuestionCount()){
// The quizz is done
qf.setDone(true);
}
} else {
qf.setAnswerStep(result.getNextStep());
}
answer.setValue(result.getNewAnswer());
answerRepository.save(answer);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
return out;
}
@Override
public JsonNode next(User user, long quizzId) {
if(user == null)
return errorNode("You need to be logged in to discover the questions");
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){
// We should create the quizzform
qf = newQuizzForm(user, quizz);
}
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);
int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q);
if(answer==null){
// We construct the blank answer
answer = new Answer(qf,q);
answerRepository.save(answer);
}
JsonNode answerData;
answerData = answer.getValue();
JsonNode qdata;
try {
qdata = q.getQT().clientQuestionData(step, answerData);
} catch (MalformedAnswerException e) {
return errorNode("The previous answer stored in database is invalid");
} catch (QuestionTypeException e) {
return errorNode("Unknown error from the QuestionType");
} catch (IllegalArgumentException e) {
return errorNode("The QuestionType did not recognize the step of the question");
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("index", JsonNodeFactory.instance.numberNode(qindex));
out.set("step", JsonNodeFactory.instance.numberNode(step));
out.set("data", qdata);
return out;
}
private 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);
qfRepository.save(qf);
return qf;
}
@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));
return out;
}
@Override
public boolean canAccessQuizz(User user, long quizzId) {
try{
Quizz quizz = qRepository.getReferenceById(quizzId);
return quizz.getPublicQuestionCount()!=null || quizz.getOwner().equals(user);
} catch (EntityNotFoundException e) {
return false;
}
}
@Override
public List<Quizz> answerableQuizz(User user) {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user);
Set<Quizz> publicQuizz = qRepository.findByPublicQuestionCountIsNotNullAndIsCompleteTrue();
publicQuizz.removeAll(ownQuizz);
return Stream.concat(
ownQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())),
publicQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
).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) {
try {
Quizz quizz = qRepository.getReferenceById(quizzId);
logger.info("Quizz owner is "+quizz.getOwner().getName());
return quizz.getOwner().equals(user);
} catch (EntityNotFoundException e) {
logger.info("Could not find quizz of id "+quizzId);
return false;
}
}
private Optional<JsonNode> checkEditQuizz(User user, long quizzId) {
if(user == null)
return Optional.of(errorNode("You need to be logged in to edit the quizz"));
if(!canEditQuizz(user, quizzId))
return Optional.of(errorNode("User has no right to edit quizz"));
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();
}
@Override
public JsonNode getQuizzInfo(User user, long quizzId) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
ArrayNode n = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount());
for(int i = 0;i<quizz.getQuestionCount();i++)n.add(JsonNodeFactory.instance.nullNode());
for(Question q : quizz.getQuestions()) {
ObjectNode nn = JsonNodeFactory.instance.objectNode();
nn.set("id",JsonNodeFactory.instance.numberNode(q.getId()));
nn.set("type", JsonNodeFactory.instance.textNode(q.getType().name()));
nn.set("value",q.getValue());
n.set(q.getIndex(),nn);
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("questions", n);
out.set("name", JsonNodeFactory.instance.textNode(quizz.getName()));
return out;
}
@Override
public JsonNode setQuizzName(User user, long quizzId, String newName) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
if(newName.isBlank() | newName.length()>255)
return errorNode("Le nom est invalide");
Quizz quizz = qRepository.findById(quizzId).get();
quizz.setName(newName);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
return out;
}
public static final QTypes DEFAULT_QTYPE = QTypes.DCC;
@Override
public JsonNode addQuestion(User user, long quizzId) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
Question q = new Question();
q.setType(DEFAULT_QTYPE);
JsonNode n = DEFAULT_QTYPE.getDefaultQuestion();
q.setValue(n);
q.setQuizz(quizz);
q.setIndex(quizz.getQuestionCount());
quizz.setQuestionCount(quizz.getQuestionCount()+1);
questionRepository.save(q);
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;
}
@Override
public JsonNode removeQuestion(User user, long quizzId, long questionId) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
final Question q;
try {
q = questionRepository.getReferenceById(questionId);
} catch (EntityNotFoundException e){
return errorNode("Could not find question with id "+questionId);
}
questionRepository.findByQuizz(quizz).forEach(qq -> {
if(qq.getIndex()>q.getIndex())
qq.setIndex(qq.getIndex()-1);
});
questionRepository.delete(q);
quizz.setQuestionCount(quizz.getQuestionCount()-1);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
return out;
}
@Override
public JsonNode reorderQuestions(User user, long quizzId, List<Long> newOrder) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
// We need that the base set of neworder is the list of ids
// 1) The length is right
if(quizz.getQuestionCount() != newOrder.size())
return errorNode("You must put every question in order");
// 2) There is no duplicates
if(new HashSet<Long>(newOrder).size() != newOrder.size())
return errorNode("You shouldn't put duplicates in the new order");
// 3) All ids correspond to an actual question of the right quizz
List<Question> questions = new ArrayList<>(newOrder.size());
for(int i = 0;i<newOrder.size();i++){
try {
Question q = questionRepository.getReferenceById(newOrder.get(i));
if(!q.getQuizz().equals(quizz))
return errorNode("The question id "+newOrder.get(i)+" is associated to another quizz");
questions.add(q);
} catch (EntityNotFoundException e){
return errorNode("Could not find question with id "+newOrder.get(i));
}
}
// All Checks passed
for(int i=0;i<questions.size();i++){
questions.get(i).setIndex(i);
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
return out;
}
@Override
public JsonNode editQuestion(User user, long quizzId, long questionId, JsonNode value) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
final Question q;
try {
q = questionRepository.getReferenceById(questionId);
} catch (EntityNotFoundException e){
return errorNode("Could not find question with id "+questionId);
}
if(!q.getQuizz().equals(quizz))
return errorNode("Question is not associated with the right quizzId");
if(!q.getType().validate(value))
return errorNode("Invalid question value");
q.setValue(value);
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
return out;
}
@Override
public JsonNode setQuestionType(User user, long quizzId, long questionId, QTypes type) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
final Question q;
try {
q = questionRepository.getReferenceById(questionId);
} catch (EntityNotFoundException e){
return errorNode("Could not find question with id "+questionId);
}
if(!q.getQuizz().equals(quizz))
return errorNode("Question is not associated with the right quizzId");
// If type is the same, we don't change (and dont reset the value)
JsonNode n;
if(!type.equals(q.getType())) {
// Then we need to change
q.setType(type);
n = type.getDefaultQuestion();
q.setValue(n);
} else {
n =q.getValue();
}
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("new", n);
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

@ -1,19 +0,0 @@
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;
public interface UserService {
void saveUser(UserDto userDto);
void changePassword(User user, String password);
User findUserByName(String name);
List<UserDto> findAllUsers();
boolean hasPrivilege(User u, Privilege p);
}

View File

@ -1,64 +0,0 @@
package com.bernard.misael.service;
import java.util.List;
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;
@Service
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public void saveUser(UserDto userDto) {
User user = new User();
user.setName(userDto.getName());
// encrypt the password using spring security
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
user.setRoles(List.of());
userRepository.save(user);
}
@Override
public void changePassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
}
@Override
public User findUserByName(String name) {
return userRepository.findByName(name);
}
@Override
public List<UserDto> findAllUsers() {
List<User> users = userRepository.findAll();
return users.stream()
.map((user) -> mapToUserDto(user))
.collect(Collectors.toList());
}
private UserDto mapToUserDto(User user){
UserDto userDto = new UserDto();
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

@ -1,20 +0,0 @@
package com.bernard.misael.service.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto
{
private Long id;
@NotEmpty(message = "User name should not be empty")
private String name;
@NotEmpty(message = "Password should not be empty")
private String password;
}

View File

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

View File

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

View File

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

View File

@ -1,112 +0,0 @@
package com.bernard.misael.web;
import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.bernard.misael.model.Role;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository;
import com.bernard.misael.service.UserService;
import com.bernard.misael.service.dto.UserDto;
import jakarta.validation.Valid;
import lombok.Getter;
@Controller
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private UserRepository urepo;
public AuthController(UserService userService) {
this.userService = userService;
}
@GetMapping("/login")
public String loginPage() {
return "login";
}
@GetMapping("/")
public String index(Model model) {
return "index";
}
@GetMapping("/users")
@Secured("LIST_USERS")
public String listUsers(Model model, Principal p){
// User should have MANAGE_USERS now
UserDto newUser = new UserDto();
model.addAttribute("newuser",newUser);
List<UserInfo> userz = urepo.findAll().stream().map(UserInfo::new).sorted().toList();
model.addAttribute("users", userz);
return "users";
}
@PostMapping("/adduser")
@Secured("ADD_USERS")
public String registration(@Valid @ModelAttribute("newuser") UserDto userDto,
BindingResult result,
Model model,
Principal p){
User existingUser = userService.findUserByName(userDto.getName());
if(existingUser != null){
result.reject("User is already registered");
}
if(result.hasErrors()){
List<UserInfo> userz = urepo.findAll().stream().map(UserInfo::new).sorted().toList();
model.addAttribute("users", userz);
model.addAttribute("newuser",userDto);
return "redirect:/users?duplicate";
}
userService.saveUser(userDto);
return "redirect:/users?success";
}
@GetMapping("/change-password")
public String showChangePassword(){
return "change-password";
}
@PostMapping("/change-password/change")
public String changePassword(@RequestParam("new-password") String newPassword, Principal p) {
User u = null;
if (p==null)
return "redirect:/login?restricted";
u = userService.findUserByName(p.getName());
userService.changePassword(u, newPassword);
return "redirect:/change-password?success";
}
@Getter
public static class UserInfo implements Comparable<UserInfo> {
private long id;
private String pseudo;
private String roles;
public UserInfo(User u){
this.id = u.getId();
this.pseudo = u.getName();
this.roles = u.getRoles().stream().map(Role::getName).collect(Collectors.joining(";"));
}
@Override
public int compareTo(UserInfo other) {
return this.pseudo.compareTo(other.pseudo);
}
}
}

View File

@ -1,344 +0,0 @@
package com.bernard.misael.web;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
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;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
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
@RequestMapping("/questions")
public class QuestionsController {
@Autowired
UserRepository ur;
@Autowired
QuizzManager qm;
@Autowired
QuizzRepository qrepo;
@Autowired
QuizzFormRepository qfrepo;
/*
* 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("answerableQuizz",qm.answerableQuizz(u));
model.addAttribute("editableQuizz",qm.editableQuizz(u));
}
return "quizz.html";
}
/*
* 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)
return "redirect:/login?restricted";
m.addAttribute("formid", quizzId);
Quizz q = qrepo.getReferenceById(quizzId);
m.addAttribute("quizzLength",q.getQuestionCount());
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());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.next(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/answer/{q}")
public ResponseEntity<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 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}")
public Object quizzEdit(@PathVariable("q") long quizzId, Principal p, Model m) {
if (p==null)
return "redirect:/login?restricted";
User u = ur.findByName(p.getName());
if (u==null || !qm.canEditQuizz(u, quizzId))
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
m.addAttribute("quizzId", quizzId);
return "quizz-edit";
}
@PostMapping("/quizz-edit/{q}/get")
public ResponseEntity<JsonNode> quizzSetName(@PathVariable("q") long quizzId, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.getQuizzInfo(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/set-name")
public ResponseEntity<JsonNode> quizzSetName(@PathVariable("q") long quizzId, @RequestBody String data, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.setQuizzName(u, quizzId, data);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/add-question")
public ResponseEntity<JsonNode> quizzAddQuestion(@PathVariable("q") long quizzId, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.addQuestion(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/remove-question/{qi}")
public ResponseEntity<JsonNode> quizzSetName(@PathVariable("q") long quizzId, @PathVariable("qi") long questionId, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.removeQuestion(u, quizzId, questionId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/reorder-questions")
public ResponseEntity<JsonNode> quizzReorderQuestions(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
if(!data.isArray())
return new ResponseEntity<>(
JsonNodeFactory.instance.textNode("Data should be an array"),
HttpStatus.BAD_REQUEST);
List<Long> idz = new ArrayList<>(data.size());
for(int i=0;i<data.size();i++)
if(data.get(i).isNumber())
idz.add(data.get(i).asLong());
else
return new ResponseEntity<>(
JsonNodeFactory.instance.textNode("Data should be an array of numbers"),
HttpStatus.BAD_REQUEST);
JsonNode out = qm.reorderQuestions(u, quizzId, idz);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/edit-question/{qi}")
public ResponseEntity<JsonNode> quizzEditQuestion(@PathVariable("q") long quizzId,
@PathVariable("qi") long questionId, @RequestBody JsonNode data, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.editQuestion(u, quizzId, questionId, data);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/set-question-type/{qi}")
public ResponseEntity<JsonNode> quizzSetQuestionType(@PathVariable("q") long quizzId,
@PathVariable("qi") long questionId, @RequestBody JsonNode data, Principal p) {
User u = null;
if (p!=null)
u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
if(!data.isTextual())
return new ResponseEntity<>(
JsonNodeFactory.instance.textNode("Data should be a string"),
HttpStatus.BAD_REQUEST);
Optional<QTypes> qtype = Arrays.stream(QTypes.values())
.filter(q -> q.name().equals(data.textValue()))
.findAny();
if(qtype.isEmpty())
return new ResponseEntity<>(
JsonNodeFactory.instance.textNode("Unknown qtype"),
HttpStatus.BAD_REQUEST);
JsonNode out = qm.setQuestionType(u, quizzId, questionId, qtype.get());
return new ResponseEntity<>(out, HttpStatus.OK);
}
}

View File

@ -1,39 +0,0 @@
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:5432/misael
username: misael
password: misael-dev
driver-class-name: org.postgresql.Driver
hikari:
schema: misael
flyway:
enabled: true
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
logging:
level:
root: INFO

View File

@ -1,7 +0,0 @@
create table roles (id bigint generated by default as identity, name varchar(255) not null, primary key (id));
create table users (id bigint generated by default as identity, name varchar(255) not null, password varchar(255) not null, primary key (id));
create table users_roles (user_id bigint not null, role_id bigint not null);
alter table if exists roles drop constraint if exists UKofx66keruapi6vyqpv6f2or37;
alter table if exists roles add constraint UKofx66keruapi6vyqpv6f2or37 unique (name);
alter table if exists users_roles add constraint FKj6m8fwv7oqv74fcehir1a9ffy foreign key (role_id) references roles;
alter table if exists users_roles add constraint FK2o0jvgh89lemvvo17cbqvdxaa foreign key (user_id) references users;

View File

@ -1,12 +0,0 @@
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 FK7gvxodb5t2n68ot577ig9u3w6 foreign key (answerer) references users;

View File

@ -1,2 +0,0 @@
create table role_privileges (id bigint not null, privileges varchar(255) not null check (privileges in ('LIST_USERS','ADD_USERS','LIST_QUIZZ')), primary key (id, privileges));
alter table if exists role_privileges add constraint FK7xfa4foqs58j7rhk6ex78hpf3 foreign key (id) references roles;

View File

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

View File

@ -1,8 +0,0 @@
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

@ -1,2 +0,0 @@
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,98 +0,0 @@
/* Button box */
div.buttonbox {
display: flex;
flex-direction: column;
}
textarea.squared-input {
border-radius: 5px;
padding: 4px;
font-size: 1rem;
margin-bottom: 12px;
width: 400px;
min-height: 65px;
}
/* Common to all the answer buttons*/
button.squared-button {
border: 0;
border-radius: 5px;
color: #1F1A25;
font-family: Arial, sans-serif;
font-weight: 800;
text-transform: uppercase;
padding: 4px;
font-size: 1rem;
margin-bottom: 12px;
width: 400px;
min-height: 65px;
}
button.squared-button:hover {
cursor: pointer;
}
button.squared-button:focus {
outline: none;
}
button.squared-button:active {
transform: translateY(4px);
}
/*@import url("https://fonts.googleapis.com/css?family=Muli:400,700,800&display=swap");
body {
background: #1F1A25;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
*/
/* Color 1 */
button.squared-button.bcol1 {
background-color: #78E0DC;
box-shadow: 0 8px #2dc5bf;
}
button.squared-button.bcol1:hover, button.squared-button.bcol1:focus {
background-color: #4fd6d1;
}
button.squared-button.bcol1:active {
box-shadow: 0 3px #2dc5bf;
}
/* Color 2 */
button.squared-button.bcol2 {
background-color: #FDE74C;
box-shadow: 0 8px #d7bd02;
}
button.squared-button.bcol2:hover, button.squared-button.bcol2:focus {
background-color: #fcde0d;
}
button.squared-button.bcol2:active {
box-shadow: 0 3px #d7bd02;
}
/* Color 3 */
button.squared-button.bcol3 {
background-color: #B8B3E9;
box-shadow: 0 8px #867ec8;
}
button.squared-button.bcol3:hover, button.squared-button.bcol3:focus {
background-color: #b0acd9;
}
button.squared-button.bcol3:active {
box-shadow: 0 3px #867ec8;
}
/* Color 4 */
button.squared-button.bcol4 {
background-color: #4fd337;
box-shadow: 0 8px #339721;
}
button.squared-button.bcol4:hover, button.squared-button.bcol4:focus {
background-color: #40be29;
}
button.squared-button.bcol4:active {
box-shadow: 0 3px #339721;
}

View File

@ -1,60 +0,0 @@
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;
width: 90%;
}
ol#questions-list li {
width: 90%;
background-color: lemonchiffon;
border: 2pt solid;
display: table-row;
flex-direction: row;
align-items: stretch;
margin: auto;
}
ol#questions-list li div.button-box {
display:table-cell;
text-align: center;
vertical-align: middle;
width: 7em;
}
ol#questions-list li div.button-box button{
width: 7em;
}
ol#questions-list li div.content-box {
display:table-cell;
background-color: gainsboro;
color: black;
font-size: 12pt;
}
/* DCC */
div.dcc-box h5, div.dcc-box h5 input{
font-size: 16pt;
font-weight: bold;
text-decoration: underline;
width: 100%;
}

View File

@ -1,28 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,81 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap');
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
nav{
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 75px;
background: #2980b9;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
nav .navbar{
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
max-width: 90%;
background: #2980b9;
margin: auto;
}
nav .navbar .logo a{
color: #fff;
font-size: 27px;
font-weight: 600;
text-decoration: none;
}
nav .navbar .menu{
display: flex;
}
.navbar .menu li{
list-style: none;
margin: 0 15px;
}
.navbar .menu li a{
color: #fff;
font-size: 17px;
font-weight: 500;
text-decoration: none;
}
main{
display: flex;
width: 100%;
align-items: center;
justify-content: center;
color: #96c7e8;
font-size: 24px;
flex-direction: column;
margin-top: 115px;
}
.button a{
position: fixed;
bottom: 20px;
right: 20px;
color: #fff;
background: #2980b9;
padding: 7px 12px;;
font-size: 18px;
border-radius: 6px;
box-shadow: rgba(0, 0, 0, 0.15);
}
div.alert {
background: #FFDDEE;
font-size: 20px;
font-style: italic;
}
div.welcome{
color: #fff;
font-size: 20px;
font-style: italic;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: 4px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<div th:if="${param.success}">
<div class="alert alert-success"> Votre mot de passe a été changé</div><br/>
</div>
<form
method="post"
role="form"
th:action="@{/change-password/change}">
<label for="new-password">Nouveau mot de passe :</label>
<input type="password" id="new-password" name="new-password"/>
<br/>
<input id="connect" type="submit" value="Changer de mot de passe"/>
</form>
</main>
</body>
</html>

View File

@ -1,175 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/form.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}"></script>
<main>
<span id="question-text"></span>
<span id="question-counter">0/0</span>
<div id="answers">
<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-cash">Cash</button>
</div>
<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>
<button class="squared-button bcol2" id="answer-duo-button-2"></button>
</div>
<div class="buttonbox" id="answer-carre">
<button class="squared-button bcol1" id="answer-carre-button-1"></button>
<button class="squared-button bcol2" id="answer-carre-button-2"></button>
<button class="squared-button bcol3" id="answer-carre-button-3"></button>
<button class="squared-button bcol4" id="answer-carre-button-4"></button>
</div>
</div>
<span id="error-textbox" style="color: red"></span>
<button id="question-button">Poser la question</button>
<script th:inline="javascript">
/*<![CDATA[*/
var qid = /*[[${formid}]]*/ -1;
var qlength = /*[[${quizzLength}]]*/ -1;
/*]]>*/
</script>
<script>
qindex = 0
qstep = 0
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
function clearerror() {
$("#error-textbox").text("")
}
function next() {
$.ajax({
url: "/questions/question/"+qid,
type: "GET",
dataType: "json",
success: function(res) {
console.log(res)
qindex=res["index"]
qstep=res["step"]
if(!res["success"]) {
console.log(res["message"])
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-cash").hide()
}else if(qstep==1){
awrs = res["data"]["answers"]
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cash").hide()
if(typeof awrs === 'undefined'){
$("#answer-cash").show()
$("#answer-cash-input").val("")
} else if (awrs.length == 2) {
$("#answer-duo").show()
$("#answer-duo-button-1").text(res.data.answers[0])
$("#answer-duo-button-2").text(res.data.answers[1])
} else if (awrs.length == 4) {
$("#answer-carre").show()
$("#answer-carre-button-1").text(res.data.answers[0])
$("#answer-carre-button-2").text(res.data.answers[1])
$("#answer-carre-button-3").text(res.data.answers[2])
$("#answer-carre-button-4").text(res.data.answers[3])
}
}
}
})
}
function answer1(q) {
asw={
index: qindex,
step: 0,
data: q
}
$.ajax({
contentType: 'application/json',
type: "POST",
url: "/questions/answer/"+qid,
data: JSON.stringify(asw),
dataType: "json",
success: function(res) {
console.log(res)
if(res["success"]){
clearerror()
next()
} else {
error(res["message"])
}
}
})
}
function answer2(value) {
asw={
index: qindex,
step: 1,
data: value
}
$.ajax({
contentType: 'application/json',
type: "POST",
url: "/questions/answer/"+qid,
data: JSON.stringify(asw),
dataType: "json",
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 !")
} else {
$("#question-text").text("Question suivante ...")
next()
}
} else {
isdone = false
error(res["message"])
}
}
})
}
$("#answer-select").hide()
$("#answer-duo").hide()
$("#answer-carre").hide()
$("#answer-cash").hide()
$("#answer-select-duo").on('click',() => answer1(2))
$("#answer-select-carre").on('click',() => answer1(4))
$("#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]))
$("#answer-carre-button-2").on('click',() => answer2(awrs[1]))
$("#answer-carre-button-3").on('click',() => answer2(awrs[2]))
$("#answer-carre-button-4").on('click',() => answer2(awrs[3]))
$("#question-button").on('click',next)
</script>
</main>
</body>
</html>

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<h3>Formulaires terminés</h3>
<ul>
<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>
</html>

View File

@ -1,13 +0,0 @@
<nav>
<div class="navbar">
<div class="logo"><a href="/">Misael</a></div>
<div class="welcome" sec:authorize="isAuthenticated()">Logged in as <span sec:authentication="name"/></div>
<ul class="menu">
<li><a href="/questions/quizz">Quizz</a></li>
<li><a href="/questions/forms">Forms</a></li>
<li sec:authorize="isAnonymous()"><a href="/login">Log in</a></li>
<li sec:authorize="isAuthenticated()"><a href="/logout">Log out</div></a></li>
</ul>
</div>
</nav>

View File

@ -1,6 +0,0 @@
<meta charset="UTF-8">
<title>Misael</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
<!-- Fontawesome CDN Link -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
Youhou ! (Y a rien ici)
</main>
</body>
</html>

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<div th:if="${param.error}">
<div class="alert alert-danger">Invalid Email or Password</div><br/>
</div>
<div th:if="${param.logout}">
<div class="alert alert-success"> You have been logged out.</div><br/>
</div>
<div th:if="${param.restricted}">
<div class="alert alert-success"> Vous devez vous connecter pour vous accéder à cette page</div><br/>
</div>
<form
method="post"
role="form"
th:action="@{/login}">
<label for="username">Pseudo :</label>
<input type="text" id="username" name="username"/>
<br/>
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password"/>
<br/>
<input id="connect" type="submit" value="Se connecter"/>
</form>
</main>
</body>
</html>

View File

@ -1,477 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/quizz-edit.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>
<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;
/*]]>*/
</script>
<script>
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
QTYPES = {
"DCC": {
"display" : makeAnswerBlockBCC,
"edit": makeEditBlockBCC,
"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",
type: "POST",
dataType: "json",
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
return
}
questions=res["questions"]
quizzName=res["name"]
$("#question-name").text(quizzName)
$("#edit-question-name").val(quizzName)
for(var i=0;i<questions.length;i++){
qpositions[questions[i]["id"]] = i
internalCreateQuestion(i)
}
$(`.reorder-button`).hide()
}
})
}
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 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(qid) {
editing.add(qid)
editMode(qid)
}
function commitEdit(qid) {
const newvalue = readEditBlockBCC(qid)
lockEdit(qid)
$.ajax({
url: "/questions/quizz-edit/"+quizzid+"/edit-question/"+qid,
type: "POST",
data: JSON.stringify(newvalue),
dataType: "json",
contentType: 'application/json',
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
unlockEdit(qid)
return
}
i = qpositions[qid]
questions[i]["value"] = newvalue
newcontent = QTYPES[questions[i]["type"]]["display"](qid,questions[i]["value"])
$(`#q-${qid}-content`).replaceWith(newcontent)
unlockEdit(qid)
editing.delete(qid)
contentMode(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"])
$(`#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="q-${id}-edit">
<h5><input id="q-${id}-edit-question"
type="text" value="${_.escape(question["text"])}"/></h5>
<ol>
<li class="right">&#x2611; <input id="q-${id}-edit-good"
type="text" value="${_.escape(question["good"])}"/></li>
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong1"
type="text" value="${_.escape(question["wrong"][0])}"/></li>
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong2"
type="text" value="${_.escape(question["wrong"][1])}"/></li>
<li class="wrong">&#x2612; <input id="q-${id}-edit-wrong3"
type="text" value="${_.escape(question["wrong"][2])}"/></li>
</ol>
</div>
`
out = $('<div/>').html(html).contents()
return out
}
function readEditBlockBCC(id) {
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,
"wrong": [wrongText1,wrongText2,wrongText3]
}
}
function makeAnswerBlockBCC(id,question) {
html = `
<div class="content-box dcc-box" id="q-${id}-content">
<h5>${_.escape(question["text"])}</h5>
<ol>
<li class="right">&#x2611; ${_.escape(question["good"])}</li>
<li class="wrong">&#x2612; ${_.escape(question["wrong"][0])}</li>
<li class="wrong">&#x2612; ${_.escape(question["wrong"][1])}</li>
<li class="wrong">&#x2612; ${_.escape(question["wrong"][2])}</li>
</ol>
</div>
`
out = $('<div/>').html(html).contents()
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>
</body>
</html>

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<h3>Quizz à répondre</h3>
<ul>
<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

@ -1,83 +0,0 @@
<!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

@ -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>

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<div th:if="${param.success}">
Utilisteurice ajouté·e avec succès !
</div>
<div th:if="${param.duplicate}">
Le nom d'utilisateur·ice est déjà utilisé
</div>
<form
method="post"
role="form"
th:action="@{/adduser}"
th:object="${newuser}"
>
<label for="name">Nom</label>
<input
id="name"
name="name"
placeholder="Le pseudo"
th:field="*{name}"
type="text"
/>
<p th:errors = "*{name}" th:if="${#fields.hasErrors('name')}"></p>
<label for="password">Password</label>
<input
id="password"
name="password"
placeholder="Enter password"
th:field="*{password}"
type="password"
/>
<p th:errors = "*{password}" class="text-danger"
th:if="${#fields.hasErrors('password')}"></p>
<button class="btn btn-primary" type="submit">Register</button>
</form>
<table>
<tr>
<th>ID</th>
<th>Pseudo</th>
<th>Roles</th>
</tr>
<tr th:each="u : ${users}">
<td th:text="${u.id}"></td>
<td th:text="${u.pseudo}"></td>
<td th:text="${u.roles}"></td>
</tr>
</table>
</main>
</body>
</html>

View File

@ -1,22 +0,0 @@
package com.bernard.misael;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.bernard.misael.questions.QTypes;
@SpringBootTest
class MisaelApplicationTests {
@Test
void contextLoads() {
}
@Test
void validateDefaultQuestions() {
for(QTypes qt : QTypes.values()){
assert qt.validate(qt.getDefaultQuestion());
}
}
}

View File

@ -1,100 +0,0 @@
#### Dependencies and Plugin versions with their available updates.
#### Generated by `./gradlew refreshVersions` version 0.60.5
####
#### Don't manually edit or split the comments that start with four hashtags (####),
#### they will be overwritten by refreshVersions.
####
#### suppress inspection "SpellCheckingInspection" for whole file
#### suppress inspection "UnusedProperty" for whole file
version.com.fasterxml.jackson.core..jackson-databind=2.18.3
version.org.junit.platform..junit-platform-launcher=1.12.0
version.org.springframework..spring-jdbc=6.2.3
## # available=7.0.0-M1
## # available=7.0.0-M2
version.org.springframework.boot..spring-boot-docker-compose=3.4.3
version.org.webjars..jquery=1.9.1
## # available=1.10.1
## # available=1.10.2
## # available=1.10.2-1
## # available=1.11.0
## # available=1.11.0-1
## # available=1.11.1
## # available=1.11.2
## # available=1.12.0
## # available=1.12.1
## # available=1.12.2
## # available=1.12.3
## # available=1.12.4
## # available=2.0.0
## # available=2.0.2
## # available=2.0.3
## # available=2.0.3-1
## # available=2.1.0
## # available=2.1.0-1
## # available=2.1.0-2
## # available=2.1.0-3
## # available=2.1.1
## # available=2.1.3
## # available=2.2.0
## # available=2.2.1
## # available=2.2.2
## # available=2.2.3
## # available=2.2.4
## # available=3.0.0-alpha1
## # available=3.0.0-rc1
## # available=3.0.0
## # available=3.1.0
## # available=3.1.1
## # available=3.1.1-1
## # available=3.2.0
## # available=3.2.1
## # available=3.3.0
## # available=3.3.1
## # available=3.3.1-1
## # available=3.3.1-2
## # available=3.4.0
## # available=3.4.1
## # available=3.5.0
## # available=3.5.1
## # available=3.6.0
## # available=3.6.1
## # available=3.6.2
## # available=3.6.3
## # available=3.6.4
## # available=3.7.0
## # available=3.7.1
version.org.thymeleaf.extras..thymeleaf-extras-springsecurity6=3.1.3.RELEASE
version.org.projectlombok..lombok=1.18.36
version.org.postgresql..postgresql=42.7.5
version.org.flywaydb..flyway-database-postgresql=11.3.2
## # available=11.3.3
## # available=11.3.4
version.org.flywaydb..flyway-core=11.3.2
## # available=11.3.3
## # available=11.3.4
version.jakarta.validation..jakarta.validation-api=3.1.1
version.com.fasterxml.jackson.datatype..jackson-datatype-hibernate5=2.18.2
## # available=2.18.3
plugin.org.springframework.boot=3.4.2
## # available=3.4.3
plugin.org.flywaydb.flyway=11.3.2
## # available=11.3.3
## # available=11.3.4
plugin.io.spring.dependency-management=1.1.7
version.org.webjars..lodash=4.17.21