Compare commits

...
7 Commits
Author SHA1 Message Date
Mysaa Java 48fbc434a4 Code refactors 2026-07-15 03:37:49 +02:00
Mysaa Java 430488ceb8 Added controls of quizz on watch page 2026-07-14 21:11:28 +02:00
Mysaa Java 5131281613 Added oidc connection 2026-07-13 15:59:56 +02:00
Mysaa Java 56070c240f Quick fix for misael quizz 2026-06-28 18:22:15 +02:00
Mysaa Java 367677c7a8 Ordered everything in subprojects 2026-06-07 20:00:32 +02:00
Mysaa Java 5836157c57 Added the crew file 2026-06-07 04:16:17 +02:00
Mysaa Java 538231dbd4 Update flake with postgres server 2026-06-07 02:58:18 +02:00
58 changed files with 3199 additions and 1013 deletions
+3
View File
@@ -4,6 +4,9 @@ build/
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/ !**/src/main/**/build/
!**/src/test/**/build/ !**/src/test/**/build/
.postgres
/db-migration.sql
/thecrew-missions.json
### STS ### ### STS ###
.apt_generated .apt_generated
+27
View File
@@ -11,3 +11,30 @@ And finaly we run `./gradlew bootRun`
The compose file is enough. The compose file is enough.
`launcher.sh` checks that the migrations are applied and then runs the spring boot server on port 8080 `launcher.sh` checks that the migrations are applied and then runs the spring boot server on port 8080
## OIDC
In order to connect to oidc, you need to setup the following options, if `bcom` is your provider name:
```Yaml
spring:
security:
oauth2:
client:
activate: true
registration:
bcom:
provider: bcom
client-id: misael
client-secret: "myclientsecret"
authorization-grant-type: authorization_code
scope: openid,roles
provider:
bcom:
issuer-uri: "https://auth.example.com/realms/bcom"
```
## API
All API requests must be done with POST requests. They will always output a json object, with a boolean
filed `success`. If `success` is false, a field `message` can give more information on the error.
+1
View File
@@ -36,6 +36,7 @@ dependencies {
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:_' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:_'
implementation Spring.boot.web implementation Spring.boot.web
implementation 'org.springframework:spring-jdbc:_' implementation 'org.springframework:spring-jdbc:_'
implementation Spring.boot.oauth2Client
implementation 'org.flywaydb:flyway-core:_' implementation 'org.flywaydb:flyway-core:_'
implementation 'org.flywaydb:flyway-database-postgresql:_' implementation 'org.flywaydb:flyway-database-postgresql:_'
implementation Spring.session.jdbc implementation Spring.session.jdbc
+2
View File
@@ -5,6 +5,8 @@ services:
- 'POSTGRES_DB=misael' - 'POSTGRES_DB=misael'
- 'POSTGRES_PASSWORD=misael-dev' - 'POSTGRES_PASSWORD=misael-dev'
- 'POSTGRES_USER=misael' - 'POSTGRES_USER=misael'
ports:
- 5432:5432
networks: networks:
- misanetwork - misanetwork
+18 -1
View File
@@ -35,21 +35,36 @@
}; };
misael-launcher = pkgs.writeShellScriptBin "misael" '' misael-launcher = pkgs.writeShellScriptBin "misael" ''
set -e
echo "Checking Database Migration" echo "Checking Database Migration"
${pkgs.flyway}/bin/flyway "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:${misael}/resources/main/db/migration/" -schemas=misael migrate ${pkgs.flyway}/bin/flyway "-url=$MISAEL_DATABASE" -user=misael "-password=$MISAEL_PASSWORD" -locations="filesystem:${misael}/resources/main/db/migration/" -schemas=misael migrate
export THECREW_IMAGES_FOLDER=${thecrew-images}
echo "Launching misael" echo "Launching misael"
${jdk}/bin/java -jar ${misael}/misael.jar --spring.datasource.url=$MISAEL_DATABASE --spring.datasource.password=$MISAEL_PASSWORD "$@" ${jdk}/bin/java -jar ${misael}/misael.jar --spring.datasource.url=$MISAEL_DATABASE --spring.datasource.password=$MISAEL_PASSWORD "$@"
''; '';
postgres-server = pkgs.writeShellScriptBin "postgres-server" ''
mkdir -p .postgres
${pkgs.podman}/bin/podman run -e POSTGRES_USER=misael -e POSTGRES_PASSWORD=misael-dev -e POSTGRES_DB=misael --volume ./.postgres:/var/lib/postgresql -p 127.0.0.1:5432:5432 postgres:18.4 &> .postgres/postgres.log &
'';
thecrew-images = pkgs.runCommand "thecrew-images" {} ''
mkdir $out
cd $out
export PATH=${pkgs.inkscape}/bin:$PATH
${lib.getExe pkgs.python3} ${./src/main/resources/static/images/thecrew-make.py}\
${./src/main/resources/static/images/thecrew.svg}\
${./src/main/resources/static/images/thecrew-modifiers.svg}
'';
in { in {
packages.${system} = { packages.${system} = {
default = self.packages.${system}.misael; default = self.packages.${system}.misael;
misael = misael; inherit thecrew-images misael;
}; };
apps.${system} = { apps.${system} = {
default = self.apps.${system}.misael; default = self.apps.${system}.misael;
misael = { type = "app"; program = "${misael-launcher}/bin/misael"; }; misael = { type = "app"; program = "${misael-launcher}/bin/misael"; };
postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; };
}; };
devShells.${system} = { devShells.${system} = {
@@ -74,6 +89,8 @@
shellHook = '' shellHook = ''
echo "Starting Gradle daemon ..." echo "Starting Gradle daemon ..."
gradle gradle
export THECREW_IMAGES_FOLDER=${thecrew-images}
export THECREW_MISSIONS_FILE=$PWD/thecrew-missions.json
echo "Gradle daemon started." echo "Gradle daemon started."
''; '';
}; };
+90
View File
@@ -2742,5 +2742,95 @@
"url": "https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.pom", "url": "https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.pom",
"hash": "sha256-D1omWgYzGwBJ41K+MsoyLeGLF/PU27cGNdQNppLjWC8=" "hash": "sha256-D1omWgYzGwBJ41K+MsoyLeGLF/PU27cGNdQNppLjWC8="
} }
},
"org.springframework.boot:spring-boot-starter-oauth2-client:3.4.2": {
"spring-boot-starter-oauth2-client-3.4.2.jar": {
"url": "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-oauth2-client/3.4.2/spring-boot-starter-oauth2-client-3.4.2.jar",
"hash": "sha256-EwYpk9/1ifH9keeXH6N4R5sFaMSlsXeXTd0tDlq4H2E="
},
"spring-boot-starter-oauth2-client-3.4.2.pom": {
"url": "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-oauth2-client/3.4.2/spring-boot-starter-oauth2-client-3.4.2.pom",
"hash": "sha256-9v9mwu4cleFfyK8R0Us7d4nVdLZfz48/+2r3zEpQTfg="
}
},
"org.springframework.security:spring-security-oauth2-jose:6.4.2": {
"spring-security-oauth2-jose-6.4.2.jar": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-jose/6.4.2/spring-security-oauth2-jose-6.4.2.jar",
"hash": "sha256-ycDunqvSTIzqCYcyU9A1sffhUaZhNj2UeBtE4eP/w+Q="
},
"spring-security-oauth2-jose-6.4.2.pom": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-jose/6.4.2/spring-security-oauth2-jose-6.4.2.pom",
"hash": "sha256-StVzW45A0Y+BBLCruCru/mw7x+qAHSv4tfVvENU/hkc="
}
},
"com.nimbusds:nimbus-jose-jwt:9.37.3": {
"nimbus-jose-jwt-9.37.3.jar": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/nimbus-jose-jwt/9.37.3/nimbus-jose-jwt-9.37.3.jar",
"hash": "sha256-Eq5KOiYAldeuuirep645bouVcNuLe0CeCagkwhnMBEQ="
},
"nimbus-jose-jwt-9.37.3.pom": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/nimbus-jose-jwt/9.37.3/nimbus-jose-jwt-9.37.3.pom",
"hash": "sha256-r8Y7aJ2IFDm5XzQ7Hcp1A5HtrGO4c5K+TZDRnJTMr74="
}
},
"com.github.stephenc.jcip:jcip-annotations:1.0-1": {
"jcip-annotations-1.0-1.jar": {
"url": "https://repo1.maven.org/maven2/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar",
"hash": "sha256-T8z/g4Kq/FiZYsTtsmL2qlleNPHhHmEFfRxqluj8cyM="
},
"jcip-annotations-1.0-1.pom": {
"url": "https://repo1.maven.org/maven2/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.pom",
"hash": "sha256-aMKlqm6PNFdDCT5StbngGQuk1aUhXApZtNfTNkcgjLs="
}
},
"org.springframework.security:spring-security-oauth2-core:6.4.2": {
"spring-security-oauth2-core-6.4.2.jar": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-core/6.4.2/spring-security-oauth2-core-6.4.2.jar",
"hash": "sha256-V9wL4jZ30eJzty02hne3Odleg+jXMLVqD5yz0Y/8zEE="
},
"spring-security-oauth2-core-6.4.2.pom": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-core/6.4.2/spring-security-oauth2-core-6.4.2.pom",
"hash": "sha256-sis9eUTBHB8/oo4vNNDZB/rpFEauIVZe17YVGekUCPg="
}
},
"org.springframework.security:spring-security-oauth2-client:6.4.2": {
"spring-security-oauth2-client-6.4.2.jar": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-client/6.4.2/spring-security-oauth2-client-6.4.2.jar",
"hash": "sha256-N++OwQJFhito8CEoTO9RNrV4N/KQ4dJXkiWe2/2LhIo="
},
"spring-security-oauth2-client-6.4.2.pom": {
"url": "https://repo1.maven.org/maven2/org/springframework/security/spring-security-oauth2-client/6.4.2/spring-security-oauth2-client-6.4.2.pom",
"hash": "sha256-CUvCd3x9PHKtFyfEfBF4AaJyTver3gmbRafWK+hQkek="
}
},
"com.nimbusds:oauth2-oidc-sdk:9.43.4": {
"oauth2-oidc-sdk-9.43.4.jar": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/oauth2-oidc-sdk/9.43.4/oauth2-oidc-sdk-9.43.4.jar",
"hash": "sha256-qq6G2BhiRNvDVm+qHw9nzjgKz3d3QwLO0g1SL8YTsK0="
},
"oauth2-oidc-sdk-9.43.4.pom": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/oauth2-oidc-sdk/9.43.4/oauth2-oidc-sdk-9.43.4.pom",
"hash": "sha256-RnCDbjnZ0P3CDuGqpf8bk3WYOptPlDzxipNYaktjH1U="
}
},
"com.nimbusds:lang-tag:1.7": {
"lang-tag-1.7.jar": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar",
"hash": "sha256-6MHFlOJCW9vqLYYN5Vxptp/F1ZRURSRJoPCRPCpbijE="
},
"lang-tag-1.7.pom": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/lang-tag/1.7/lang-tag-1.7.pom",
"hash": "sha256-gksNfeeCer/GpH5u+UsP+qE4/vOK8IxWon9dOhUiEZ4="
}
},
"com.nimbusds:content-type:2.2": {
"content-type-2.2.jar": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/content-type/2.2/content-type-2.2.jar",
"hash": "sha256-cw8YFhlhReiCdQk8FH8ubaPD5UEges01A6GwYSm5vqk="
},
"content-type:2.2.pom": {
"url": "https://repo1.maven.org/maven2/com/nimbusds/content-type/2.2/content-type-2.2.pom",
"hash": "sha256-iznCFjD7tvBIqAflfjy/c17zDaPxHnAZ5muqa5fa8Gs="
}
} }
} }
@@ -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());
}
}
@@ -1,10 +1,9 @@
package com.bernard.misael.web; package com.bernard.misael.auth;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -14,11 +13,11 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import com.bernard.misael.model.Role; import com.bernard.misael.auth.repository.UserRepository;
import com.bernard.misael.model.User; import com.bernard.misael.auth.service.UserService;
import com.bernard.misael.repository.UserRepository; import com.bernard.misael.auth.model.Privilege;
import com.bernard.misael.service.UserService; import com.bernard.misael.auth.model.User;
import com.bernard.misael.service.dto.UserDto; import com.bernard.misael.auth.service.dto.UserDto;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.Getter; import lombok.Getter;
@@ -26,13 +25,12 @@ import lombok.Getter;
@Controller @Controller
public class AuthController { public class AuthController {
@Autowired private final UserService userService;
private UserService userService; private final UserRepository urepo;
@Autowired
private UserRepository urepo;
public AuthController(UserService userService) { public AuthController(UserService userService, UserRepository urepo) {
this.userService = userService; this.userService = userService;
this.urepo = urepo;
} }
@GetMapping("/login") @GetMapping("/login")
@@ -94,19 +92,40 @@ public class AuthController {
return "redirect:/change-password?success"; return "redirect:/change-password?success";
} }
public static String requiresLogin(String redirect) {
//TODO Make it so it really redirects after login
return "redirect:/login?restricted";
}
@Getter @Getter
public static class UserInfo implements Comparable<UserInfo> { public static class UserInfo implements Comparable<UserInfo> {
private long id; private long id;
private String oidcId;
private String pseudo; private String pseudo;
private String roles; private String roles;
public UserInfo(User u){ public UserInfo(User u){
this.id = u.getId(); this.id = u.getId();
this.oidcId = u.getOidcId();
this.pseudo = u.getName(); this.pseudo = u.getName();
this.roles = u.getRoles().stream().map(Role::getName).collect(Collectors.joining(";")); this.roles = u.getPrivileges().stream().map(Privilege::name).collect(Collectors.joining(";"));
} }
@Override @Override
public int compareTo(UserInfo other) { public int compareTo(UserInfo other) {
return this.pseudo.compareTo(other.pseudo); return this.pseudo.compareTo(other.pseudo);
} }
@Override
public boolean equals(Object obj) {
if(obj instanceof UserInfo other)
return this.pseudo.equals(other.pseudo);
return false;
}
@Override
public int hashCode() {
return this.pseudo.hashCode();
}
} }
} }
@@ -1,18 +1,12 @@
package com.bernard.misael; package com.bernard.misael.auth;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.misael.model.Privilege; import com.bernard.misael.auth.model.User;
import com.bernard.misael.model.Role; import com.bernard.misael.auth.repository.UserRepository;
import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@@ -28,16 +22,9 @@ public class CustomUserDetailsService implements UserDetailsService {
User user = userRepository.findByName(pseudo); User user = userRepository.findByName(pseudo);
if (user != null) { 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(), return new org.springframework.security.core.userdetails.User(user.getName(),
user.getPassword(), user.getPassword(),
Stream.concat(inducedPrivileges, roles).toList() user.getPrivileges()
); );
}else{ }else{
throw new UsernameNotFoundException("Invalid username or password."); throw new UsernameNotFoundException("Invalid username or password.");
@@ -0,0 +1,92 @@
package com.bernard.misael.auth;
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.core.env.Environment;
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.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.bernard.misael.auth.model.User;
import com.bernard.misael.auth.service.UserService;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SpringSecurity {
public static final Logger LOG = Logger.getLogger(SpringSecurity.class.getName());
public static final boolean oidcActivated(Environment env) {
String activate = env.getProperty("spring.security.oauth2.client.activate");
return (activate!=null) && activate.equalsIgnoreCase("true");
}
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, Environment env, final UserService us) throws Exception {
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/**").permitAll()
);
if(oidcActivated(env)) {
http.oauth2Login(oauth2 -> oauth2.
userInfoEndpoint(userInfo -> userInfo.
oidcUserService(this.oidcUserService(us))
)
);
} else {
http.formLogin(
form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.permitAll()
);
http.logout(
logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.permitAll()
);
}
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(UserService us) {
final OidcUserService delegate = new OidcUserService();
return userRequest -> {
// We create the original user
OidcUser oidcUser = delegate.loadUser(userRequest);
User u = us.ensureUser(oidcUser);
return new DefaultOidcUser(u.getPrivileges(), oidcUser.getIdToken());
};
}
}
@@ -0,0 +1,26 @@
package com.bernard.misael.auth.model;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
public enum Privilege implements GrantedAuthority {
LIST_USERS,ADD_USERS,LIST_QUIZZ,CREATE_QUIZZ,VIEW_ALL_FORMS;
public static final Map<String,Privilege> oidcRoles = Map.of(
"misael-view-all-forms",VIEW_ALL_FORMS,
"misael-list-users",LIST_USERS,
"misael-create-quizz",CREATE_QUIZZ,
"misael-list-quizz", LIST_QUIZZ
);
@Override
public String getAuthority() {
return this.name();
}
public static final Privilege ofOidcString(String s) {
return oidcRoles.getOrDefault(s, null);
}
}
@@ -1,19 +1,20 @@
package com.bernard.misael.model; package com.bernard.misael.auth.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Set; import java.util.Set;
import jakarta.persistence.CascadeType; import com.bernard.misael.quizz.model.Quizz;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -38,15 +39,18 @@ public class User
@Column(nullable=false,unique = true) @Column(nullable=false,unique = true)
private String name; private String name;
@Column(nullable=false) @Column(nullable=true, unique = true)
private String oidcId;
@Column(nullable=true)
private String password; private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL) @ElementCollection(fetch = FetchType.EAGER)
@JoinTable( @Enumerated(EnumType.STRING)
name="users_roles", @CollectionTable(name = "user_privileges",
joinColumns={@JoinColumn(name="USER_ID", referencedColumnName="ID")}, joinColumns = @JoinColumn(name = "id"))
inverseJoinColumns={@JoinColumn(name="ROLE_ID", referencedColumnName="ID")}) @Column(name = "privileges", nullable = false)
private List<Role> roles = new ArrayList<>(); private Set<Privilege> privileges;
@OneToMany(mappedBy="owner") @OneToMany(mappedBy="owner")
private Set<Quizz> myQuizzs; private Set<Quizz> myQuizzs;
@@ -1,11 +1,14 @@
package com.bernard.misael.repository; package com.bernard.misael.auth.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.User; import com.bernard.misael.auth.model.User;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String pseudo); User findByName(String pseudo);
Optional<User> findByOidcId(String oidcId);
} }
@@ -1,7 +1,6 @@
package com.bernard.misael.service; package com.bernard.misael.auth.service;
import java.util.Arrays; import java.util.Set;
import java.util.EnumSet;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -11,11 +10,9 @@ import org.springframework.lang.NonNull;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.bernard.misael.model.Privilege; import com.bernard.misael.auth.model.Privilege;
import com.bernard.misael.model.Role; import com.bernard.misael.auth.model.User;
import com.bernard.misael.model.User; import com.bernard.misael.auth.repository.UserRepository;
import com.bernard.misael.repository.RoleRepository;
import com.bernard.misael.repository.UserRepository;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@@ -28,9 +25,6 @@ public class AdminMaker implements
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired @Autowired
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
@@ -38,37 +32,21 @@ public class AdminMaker implements
@Transactional @Transactional
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) { public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
if (alreadySetup) if (alreadySetup) return;
return;
Logger log = Logger.getLogger("AdminMaker"); Logger log = Logger.getLogger("AdminMaker");
log.info("Checking that privileges and mysaa user exist"); 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"); User mysaa = userRepository.findByName("mysaa");
if (mysaa == null) { if (mysaa == null) {
User user = new User(); User user = new User();
user.setName("mysaa"); user.setName("mysaa");
user.setPassword(passwordEncoder.encode("super")); user.setPassword(passwordEncoder.encode("super"));
user.setRoles(Arrays.asList(adminRole)); user.setPrivileges(Set.of(Privilege.values()));
userRepository.save(user); userRepository.save(user);
} }
alreadySetup = true; alreadySetup = true;
log.info("Everything needed has been created"); 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;
}
} }
@@ -0,0 +1,26 @@
package com.bernard.misael.auth.service;
import java.security.Principal;
import java.util.List;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import com.bernard.misael.auth.model.Privilege;
import com.bernard.misael.auth.model.User;
import com.bernard.misael.auth.service.dto.UserDto;
public interface UserService {
User ofPrincipal(Principal p);
User ensureUser(OidcUser user);
void saveUser(UserDto userDto);
void changePassword(User user, String password);
User findUserByName(String name);
List<UserDto> findAllUsers();
boolean hasPrivilege(User u, Privilege p);
}
@@ -0,0 +1,135 @@
package com.bernard.misael.auth.service;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;
import com.bernard.misael.auth.repository.UserRepository;
import com.bernard.misael.auth.service.dto.UserDto;
import jakarta.validation.constraints.NotNull;
import com.bernard.misael.auth.model.Privilege;
import com.bernard.misael.auth.model.User;
@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
@NotNull
public User ensureUser(OidcUser oidcUser) {
String username = oidcUser.getPreferredUsername();
String id = oidcUser.getName();
Set<Privilege> privileges;
try {
Map<String,Map<String,List<String>>> resourceAccess = oidcUser
.<Map<String,Map<String,List<String>>>>getAttribute("resource_access");
if(resourceAccess == null)
throw new RuntimeException("Oidc user has no 'resource_access'");
privileges = resourceAccess
.getOrDefault("misael",Map.of())
.getOrDefault("roles",List.of())
.stream()
.map(Privilege::ofOidcString)
.filter(p -> p != null)
.collect(Collectors.toSet());
} catch (NullPointerException e) {
throw new RuntimeException(e);
}
Optional<User> u = userRepository.findByOidcId(id);
if(u.isEmpty()) {
Optional<User> uN = Optional.ofNullable(userRepository.findByName(username));
if(uN.isEmpty()) {
User u0 = new User();
u0.setOidcId(id);
u0.setName(username);
userRepository.save(u0);
u = Optional.of(u0);
} else {
User u0 = uN.get();
u0.setOidcId(id);
userRepository.save(u0);
u = uN;
}
}
User theu = u.get();
if(!theu.getPrivileges().equals(privileges)) {
theu.setPrivileges(privileges);
userRepository.save(theu);
}
return theu;
}
@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.setPrivileges(Set.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(this::mapToUserDto)
.toList();
}
private UserDto mapToUserDto(User user){
UserDto userDto = new UserDto();
userDto.setName(user.getName());
return userDto;
}
@Override
public boolean hasPrivilege(User u, Privilege p) {
return u.getPrivileges().contains(p);
}
@Override
public User ofPrincipal(Principal p) {
User u = null;
if(p instanceof OAuth2AuthenticationToken o) {
u = userRepository.findByOidcId(o.getName()).orElseThrow(() -> new RuntimeException("Oauth2 user was not in the database"));
}
if(p instanceof UsernamePasswordAuthenticationToken o) {
u = userRepository.findByName(o.getName());
}
return u;
}
}
@@ -1,4 +1,4 @@
package com.bernard.misael.service.dto; package com.bernard.misael.auth.service.dto;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -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();
}
}
@@ -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;
}
}
@@ -0,0 +1,23 @@
package com.bernard.misael.quizz;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class APIException extends RuntimeException {
private final String message;
@NonNull
private final HttpStatus status;
public APIException(String message) {
this.message = message;
this.status = HttpStatus.BAD_REQUEST;
}
}
@@ -0,0 +1,335 @@
package com.bernard.misael.quizz;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpMethod;
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.auth.service.UserService;
import com.bernard.misael.quizz.model.Quizz;
import com.bernard.misael.quizz.model.QuizzForm;
import com.bernard.misael.auth.AuthController;
import com.bernard.misael.auth.model.User;
import com.bernard.misael.quizz.questions.QTypes;
import com.bernard.misael.quizz.repository.QuizzFormRepository;
import com.bernard.misael.quizz.repository.QuizzRepository;
import com.bernard.misael.quizz.service.QuizzManager;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ExceptionHandler;
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 {
private final UserService us;
private final QuizzManager qm;
private final QuizzRepository qrepo;
private final QuizzFormRepository qfrepo;
public QuestionsController(UserService us, QuizzManager qm, QuizzRepository qrepo, QuizzFormRepository qfrepo) {
this.us = us;
this.qm = qm;
this.qrepo = qrepo;
this.qfrepo = qfrepo;
}
@ExceptionHandler(APIException.class)
public Object apiException(HttpServletRequest request, APIException e) {
if(request.getMethod().equals(HttpMethod.GET.toString()))
return new ResponseEntity<>(e.getMessage(), e.getStatus());
else {
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(false));
out.set("message", JsonNodeFactory.instance.textNode(e.getMessage()));
return new ResponseEntity<>(out, e.getStatus());
}
}
/*
* List all quizz
*/
@GetMapping("/quizz")
public String getQuizz(Model model, Principal p) {
User u = us.ofPrincipal(p);
if(u!=null) {
model.addAttribute("answerableQuizz",qm.answerableQuizz(u));
model.addAttribute("editableQuizz",qm.editableQuizz(u));
model.addAttribute("completedQuizz",qm.completedQuizz(u));
}
return "quizz.html";
}
/*
* List all forms started or finished by the user
*/
@GetMapping("/forms")
public String getForms(Model model, Principal p) {
User u = us.ofPrincipal(p);
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, HttpServletRequest request) {
User u = us.ofPrincipal(p);
QuizzForm qf = qm.getQuizzForm(u, id);
m.addAttribute("formId", qf.getId());
return "showform.html";
}
/*
* Watch advancements of all forms for a specific quizz id
*/
@GetMapping("/watch/{id}")
public Object showFormsAdvancements(@PathVariable("id") long id, Model m, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
Quizz q = qm.getQuizz4Watch(u, id);
m.addAttribute("quizzId", q.getId());
return "watchquizz.html";
}
/*
* API get the form
*/
@PostMapping("/getformdata/{id}")
public Object showFormApi(@PathVariable("id") long id, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.getQuizzFormData(u, id);
return new ResponseEntity<>(out, HttpStatus.OK);
}
/*
* API get the forms for a specific quizz
*/
@PostMapping("/getallformsdata/{id}")
public Object getAllFormsData(@PathVariable("id") long id, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.getAllFormsData(u, id);
return new ResponseEntity<>(out, HttpStatus.OK);
}
/*
* API get the form advancement for every form of a quizz
*/
@PostMapping("/watchdata/{id}")
public Object watchData(@PathVariable("id") long id, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.getQuizzFormAdvancments(u, id);
return new ResponseEntity<>(out, HttpStatus.OK);
}
/*
* API get the form
*/
@PostMapping("/duplicate-quizz/{id}")
public Object duplicateQuizz(@PathVariable("id") long id, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
Quizz q = qm.duplicateQuizz(u, id);
if(q == null)
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
/*
* API get the form
*/
@PostMapping("/mark-complete/{id}")
public Object markComplete(@PathVariable("id") long id, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
if(!qm.markComplete(u, id))
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
/*
* API set the last public question
*/
@PostMapping("/set-public-question-count/{id}")
public Object setPublicQuestionCount(@PathVariable("id") long id, @RequestBody JsonNode data, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode pqc = data.get("publicQuestionCount");
Integer pqcI = pqc.isNull() ? null : pqc.asInt();
if(!qm.setPublicQuestionCount(u, id, pqcI))
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/form/{q}")
public String formpage(@PathVariable("q") long quizzId, Principal p, Model m, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if (u==null)
return AuthController.requiresLogin(request.getRequestURI());
m.addAttribute("formid", quizzId);
Quizz q = qrepo.getReferenceById(quizzId);
m.addAttribute("quizzLength",q.getQuestionCount());
return "form";
}
@PostMapping("/question/{q}")
public Object question(@PathVariable("q") long quizzId, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.next(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/answer/{q}")
public Object answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
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, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if (u==null)
return AuthController.requiresLogin(request.getRequestURI());
Quizz q = qm.newQuizz(u);
return "redirect:/questions/quizz-edit/"+Long.toString(q.getId());
}
@GetMapping("/quizz-edit/{q}")
public Object quizzEdit(@PathVariable("q") long quizzId, Principal p, Model m, HttpServletRequest request) {
User u = us.ofPrincipal(p);
Quizz q = qm.getQuizz4Edit(u, quizzId);
m.addAttribute("quizzId", q.getId());
return "quizz-edit";
}
@PostMapping("/quizz-edit/{q}/get")
public Object quizzSetName(@PathVariable("q") long quizzId, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.getQuizzInfo(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/set-name")
public Object quizzSetName(@PathVariable("q") long quizzId, @RequestBody String data, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.setQuizzName(u, quizzId, data);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/add-question")
public Object quizzAddQuestion(@PathVariable("q") long quizzId, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.addQuestion(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/remove-question/{qi}")
public Object quizzSetName(@PathVariable("q") long quizzId, @PathVariable("qi") long questionId, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.removeQuestion(u, quizzId, questionId);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/reorder-questions")
public Object quizzReorderQuestions(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p, HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
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 Object quizzEditQuestion(@PathVariable("q") long quizzId,
@PathVariable("qi") long questionId, @RequestBody JsonNode data, Principal p,
HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
JsonNode out = qm.editQuestion(u, quizzId, questionId, data);
return new ResponseEntity<>(out, HttpStatus.OK);
}
@PostMapping("/quizz-edit/{q}/set-question-type/{qi}")
public Object quizzSetQuestionType(@PathVariable("q") long quizzId,
@PathVariable("qi") long questionId, @RequestBody JsonNode data, Principal p,
HttpServletRequest request) {
User u = us.ofPrincipal(p);
if(u==null)
return AuthController.requiresLogin(request.getRequestURI());
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);
}
}
@@ -1,6 +1,6 @@
package com.bernard.misael.model; package com.bernard.misael.quizz.model;
import com.bernard.misael.service.JsonNodeConverter; import com.bernard.misael.quizz.service.JsonNodeConverter;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
@@ -1,8 +1,8 @@
package com.bernard.misael.model; package com.bernard.misael.quizz.model;
import com.bernard.misael.questions.QTypes; import com.bernard.misael.quizz.questions.QTypes;
import com.bernard.misael.questions.QuestionType; import com.bernard.misael.quizz.questions.QuestionType;
import com.bernard.misael.service.JsonNodeConverter; import com.bernard.misael.quizz.service.JsonNodeConverter;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.*; import jakarta.persistence.*;
@@ -1,9 +1,11 @@
package com.bernard.misael.model; package com.bernard.misael.quizz.model;
import java.util.Set; import java.util.Set;
import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.ColumnDefault;
import com.bernard.misael.auth.model.User;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@@ -1,4 +1,6 @@
package com.bernard.misael.model; package com.bernard.misael.quizz.model;
import com.bernard.misael.auth.model.User;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -1,4 +1,4 @@
package com.bernard.misael.questions; package com.bernard.misael.quizz.questions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -13,9 +13,9 @@ import java.util.stream.StreamSupport;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.bernard.misael.service.exception.MalformedAnswerException; import com.bernard.misael.quizz.service.exception.MalformedAnswerException;
import com.bernard.misael.service.exception.MalformedClientAnswerException; import com.bernard.misael.quizz.service.exception.MalformedClientAnswerException;
import com.bernard.misael.service.exception.QuestionTypeException; import com.bernard.misael.quizz.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
@@ -1,4 +1,4 @@
package com.bernard.misael.questions; package com.bernard.misael.quizz. questions;
import java.util.function.Function; import java.util.function.Function;
@@ -1,6 +1,6 @@
package com.bernard.misael.questions; package com.bernard.misael.quizz. questions;
import com.bernard.misael.service.exception.QuestionTypeException; import com.bernard.misael.quizz.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -1,12 +1,12 @@
package com.bernard.misael.repository; package com.bernard.misael.quizz.repository;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Answer; import com.bernard.misael.quizz.model.Answer;
import com.bernard.misael.model.Question; import com.bernard.misael.quizz.model.Question;
import com.bernard.misael.model.QuizzForm; import com.bernard.misael.quizz.model.QuizzForm;
public interface AnswerRepository extends JpaRepository<Answer,Long> { public interface AnswerRepository extends JpaRepository<Answer,Long> {
@@ -1,12 +1,12 @@
package com.bernard.misael.repository; package com.bernard.misael.quizz.repository;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Question; import com.bernard.misael.quizz.model.Question;
import com.bernard.misael.model.Quizz; import com.bernard.misael.quizz.model.Quizz;
public interface QuestionRepository extends JpaRepository<Question,Long> { public interface QuestionRepository extends JpaRepository<Question,Long> {
@@ -1,12 +1,12 @@
package com.bernard.misael.repository; package com.bernard.misael.quizz.repository;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import com.bernard.misael.model.Quizz; import com.bernard.misael.quizz.model.Quizz;
import com.bernard.misael.model.QuizzForm; import com.bernard.misael.quizz.model.QuizzForm;
import com.bernard.misael.model.User; import com.bernard.misael.auth.model.User;
public interface QuizzFormRepository extends JpaRepository<QuizzForm,Long> { public interface QuizzFormRepository extends JpaRepository<QuizzForm,Long> {
@@ -1,4 +1,4 @@
package com.bernard.misael.repository; package com.bernard.misael.quizz.repository;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -6,8 +6,8 @@ import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import com.bernard.misael.model.Quizz; import com.bernard.misael.quizz.model.Quizz;
import com.bernard.misael.model.User; import com.bernard.misael.auth.model.User;
public interface QuizzRepository extends JpaRepository<Quizz,Long> { public interface QuizzRepository extends JpaRepository<Quizz,Long> {
@@ -1,4 +1,4 @@
package com.bernard.misael.service; package com.bernard.misael.quizz.service;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -1,12 +1,12 @@
package com.bernard.misael.service; package com.bernard.misael.quizz.service;
import java.util.List; import java.util.List;
import java.util.Optional;
import com.bernard.misael.model.Quizz; import com.bernard.misael.auth.model.User;
import com.bernard.misael.model.QuizzForm; import com.bernard.misael.quizz.APIException;
import com.bernard.misael.model.User; import com.bernard.misael.quizz.model.Quizz;
import com.bernard.misael.questions.QTypes; import com.bernard.misael.quizz.model.QuizzForm;
import com.bernard.misael.quizz.questions.QTypes;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
public interface QuizzManager { public interface QuizzManager {
@@ -16,13 +16,14 @@ public interface QuizzManager {
public Quizz newQuizz(User user); public Quizz newQuizz(User user);
public boolean canAccessQuizz(User user, long quizzId);
public List<Quizz> editableQuizz(User user); public List<Quizz> editableQuizz(User user);
public List<Quizz> completedQuizz(User user);
public List<Quizz> answerableQuizz(User user); public List<Quizz> answerableQuizz(User user);
public boolean canEditQuizz(User user, long quizzId); public Quizz getQuizz4Edit(User user, long quizzId) throws APIException;
public Optional<QuizzForm> canViewQuizzForm(User user, long quizzFormId); public QuizzForm getQuizzForm(User user, long quizzFormId) throws APIException;
public Optional<Quizz> canViewQuizzFormsOfQuizz(User user, long quizzId); public Quizz getQuizz4Watch(User user, long quizzId) throws APIException;
public Quizz getQuizz4Answer(User user, long quizzId) throws APIException;
public JsonNode getQuizzInfo(User user, long quizzId); public JsonNode getQuizzInfo(User user, long quizzId);
public JsonNode setQuizzName(User user, long quizzId, String newName); public JsonNode setQuizzName(User user, long quizzId, String newName);
@@ -37,4 +38,6 @@ public interface QuizzManager {
public JsonNode getQuizzFormAdvancments(User user, long quizzId); public JsonNode getQuizzFormAdvancments(User user, long quizzId);
public Quizz duplicateQuizz(User user, long quizzId); public Quizz duplicateQuizz(User user, long quizzId);
public boolean markComplete(User user, long quizzId);
public boolean setPublicQuestionCount(User u, long quizzId, Integer publicQuestionCount);
} }
@@ -1,112 +1,113 @@
package com.bernard.misael.service; package com.bernard.misael.quizz.service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.slf4j.Logger; import org.springframework.http.HttpStatus;
import org.slf4j.LoggerFactory; import org.springframework.lang.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.misael.model.Answer; import com.bernard.misael.auth.model.Privilege;
import com.bernard.misael.model.Privilege; import com.bernard.misael.auth.model.User;
import com.bernard.misael.model.Question; import com.bernard.misael.auth.service.UserService;
import com.bernard.misael.model.Quizz; import com.bernard.misael.quizz.APIException;
import com.bernard.misael.model.QuizzForm; import com.bernard.misael.quizz.model.Answer;
import com.bernard.misael.model.User; import com.bernard.misael.quizz.model.Question;
import com.bernard.misael.questions.QTypes; import com.bernard.misael.quizz.model.Quizz;
import com.bernard.misael.questions.QuestionType.AnswerResult; import com.bernard.misael.quizz.model.QuizzForm;
import com.bernard.misael.repository.*; import com.bernard.misael.quizz.questions.QTypes;
import com.bernard.misael.service.exception.MalformedAnswerException; import com.bernard.misael.quizz.questions.QuestionType.AnswerResult;
import com.bernard.misael.service.exception.MalformedClientAnswerException; import com.bernard.misael.quizz.repository.AnswerRepository;
import com.bernard.misael.service.exception.QuestionTypeException; import com.bernard.misael.quizz.repository.QuestionRepository;
import com.bernard.misael.web.QuestionsController; import com.bernard.misael.quizz.repository.QuizzFormRepository;
import com.bernard.misael.quizz.repository.QuizzRepository;
import com.bernard.misael.quizz.service.exception.MalformedAnswerException;
import com.bernard.misael.quizz.service.exception.MalformedClientAnswerException;
import com.bernard.misael.quizz.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotNull;
@Service @Service
public class QuizzManagerImpl implements QuizzManager { public class QuizzManagerImpl implements QuizzManager {
@Autowired private final QuizzFormRepository qfRepository;
UserRepository uRepository; private final QuizzRepository qRepository;
private final QuestionRepository questionRepository;
@Autowired private final AnswerRepository answerRepository;
QuizzFormRepository qfRepository; private final UserService uService;
@Autowired
QuizzRepository qRepository;
@Autowired
QuestionRepository questionRepository;
@Autowired
AnswerRepository answerRepository;
@Autowired
UserService uService;
public QuizzManagerImpl(
QuizzFormRepository qfRepository,
QuizzRepository qRepository,
QuestionRepository questionRepository,
AnswerRepository answerRepository,
UserService uService
) {
this.qfRepository = qfRepository;
this.qRepository = qRepository;
this.questionRepository = questionRepository;
this.answerRepository = answerRepository;
this.uService = uService;
}
private static final APIException LOGIN_REQUIRED_EXCEPTION =
new APIException("Must be logged in to do this action", HttpStatus.UNAUTHORIZED);
@Override @Override
public JsonNode answer(User user, long quizzId, JsonNode data) { public JsonNode answer(User user, long quizzId, JsonNode data) {
Quizz quizz = getQuizz4Answer(user, quizzId);
if(!data.has("index") || !data.get("index").isInt()) if(!data.has("index") || !data.get("index").isInt())
return errorNode("Request should contain the question index"); throw new APIException("Request should contain the question index");
if(!data.has("step") || !data.get("step").isInt()) if(!data.has("step") || !data.get("step").isInt())
return errorNode("Request should contain the answer step"); throw new APIException("Request should contain the answer step");
if(!data.has("data")) if(!data.has("data"))
return errorNode("Request should contain the answer data"); throw new APIException("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); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null) if(qf == null)
return errorNode("The quizzform does not exist, ask the question first"); throw new APIException("The quizzform does not exist, ask the question first", HttpStatus.NOT_FOUND);
if(qf.isDone()) if(qf.isDone())
return errorNode("You're done with the quizz, you cannot answer anymore"); throw new APIException("You're done with the quizz, you cannot answer anymore", HttpStatus.CONFLICT);
int qindex = qf.getCurrentQuestion(); int qindex = qf.getCurrentQuestion();
if(qindex != data.get("index").intValue()) if(qindex != data.get("index").intValue())
return errorNode("You are not answering the right question (you answer question "+data.get("index").intValue() throw new APIException(
+" where you should answer question "+qindex+")"); "You are not answering the right question (you answer question %d, but you should answer question %d)"
.formatted(data.get("index").intValue(), qindex), HttpStatus.CONFLICT);
if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE)) if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE))
return errorNode("La question suivante est encore bloquée"); throw new APIException("La question suivante est encore bloquée", HttpStatus.CONFLICT);
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null) if(q == null)
return errorNode("Could not find question "+qindex); throw new APIException("Could not find question %d".formatted(qindex), HttpStatus.NOT_FOUND);
int step = qf.getAnswerStep(); int step = qf.getAnswerStep();
if(step != data.get("step").intValue()) 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() throw new APIException(
+" where you should step question "+step+")"); "You are not answering the right step of the question (you answer step %d where you should be answering step %d)"
.formatted(data.get("step").intValue(),step));
Answer answer = answerRepository.findByFormAndQuestion(qf, q); Answer answer = answerRepository.findByFormAndQuestion(qf, q);
if(answer == null) if(answer == null)
return errorNode("The database answer object does not exist, ask the question first"); throw new APIException("The database answer object does not exist, ask the question first", HttpStatus.CONFLICT);
JsonNode answerData = answer.getValue(); JsonNode answerData = answer.getValue();
AnswerResult result; AnswerResult result;
try { try {
result = q.getQT().clientAnswers(step, answerData, data.get("data")); result = q.getQT().clientAnswers(step, answerData, data.get("data"));
} catch (MalformedAnswerException e) { } catch (MalformedAnswerException e) {
return errorNode("The previous answer stored in database is invalid"); throw new APIException("The previous answer stored in database is invalid", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (MalformedClientAnswerException e) { } catch (MalformedClientAnswerException e) {
return errorNode("This answer is not valid here"); throw new APIException("This answer is not valid here");
} catch (QuestionTypeException e) { } catch (QuestionTypeException e) {
return errorNode("Unknown error from the QuestionType"); throw new APIException("Unknown error from the QuestionType", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return errorNode("The QuestionType did not recognize the step of the question"); throw new APIException("The QuestionType did not recognize the step of the question");
} }
if(result.isNextQuestion()) { if(result.isNextQuestion()) {
qf.setCurrentQuestion(qindex+1); qf.setCurrentQuestion(qindex+1);
@@ -127,27 +128,20 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode next(User user, long quizzId) { public JsonNode next(User user, long quizzId) {
if(user == null) Quizz quizz = getQuizz4Answer(user, quizzId);
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); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null){ if(qf == null){
// We should create the quizzform // We should create the quizzform
qf = newQuizzForm(user, quizz); qf = newQuizzForm(user, quizz);
} }
if(qf.isDone()) if(qf.isDone())
return errorNode("No more questions"); throw new APIException("No more questions", HttpStatus.OK);
int qindex = qf.getCurrentQuestion(); int qindex = qf.getCurrentQuestion();
if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE)) if(qindex >= Optional.ofNullable(quizz.getPublicQuestionCount()).orElse(Integer.MAX_VALUE))
return errorNode("La question suivante est encore bloquée"); throw new APIException("La question suivante est encore bloquée", HttpStatus.OK);
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null) if(q == null)
return errorNode("Could not find question "+qindex); throw new APIException("Could not find question %d".formatted(qindex), HttpStatus.NOT_FOUND);
int step = qf.getAnswerStep(); int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q); Answer answer = answerRepository.findByFormAndQuestion(qf, q);
if(answer==null){ if(answer==null){
@@ -162,11 +156,11 @@ public class QuizzManagerImpl implements QuizzManager {
try { try {
qdata = q.getQT().clientQuestionData(step, answerData); qdata = q.getQT().clientQuestionData(step, answerData);
} catch (MalformedAnswerException e) { } catch (MalformedAnswerException e) {
return errorNode("The previous answer stored in database is invalid"); throw new APIException("The previous answer stored in database is invalid", HttpStatus.CONFLICT);
} catch (QuestionTypeException e) { } catch (QuestionTypeException e) {
return errorNode("Unknown error from the QuestionType"); throw new APIException("Unknown error from the QuestionType", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return errorNode("The QuestionType did not recognize the step of the question"); throw new APIException("The QuestionType did not recognize the step of the question");
} }
ObjectNode out = JsonNodeFactory.instance.objectNode(); ObjectNode out = JsonNodeFactory.instance.objectNode();
@@ -177,7 +171,7 @@ public class QuizzManagerImpl implements QuizzManager {
return out; return out;
} }
private QuizzForm newQuizzForm(User user, Quizz quizz) { private QuizzForm newQuizzForm(@NonNull User user, Quizz quizz) {
QuizzForm qf = new QuizzForm(); QuizzForm qf = new QuizzForm();
qf.setUser(user); qf.setUser(user);
qf.setQuizz(quizz); qf.setQuizz(quizz);
@@ -188,32 +182,16 @@ public class QuizzManagerImpl implements QuizzManager {
return qf; return qf;
} }
private static final Random rand = new Random();
@Override @Override
public Quizz newQuizz(User user) { public Quizz newQuizz(User user) {
Quizz q = new Quizz(); Quizz q = new Quizz();
q.setName("Super questions de "+user.getName()+" ("+Integer.toHexString((int)(Math.random()*0xFFFFFFF))+")"); q.setName("Super questions de %s (%X)".formatted(user.getName(),rand.nextInt(0xFFFFFFF)));
q.setOwner(user); q.setOwner(user);
q = qRepository.save(q); q = qRepository.save(q);
return 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 @Override
public List<Quizz> answerableQuizz(User user) { public List<Quizz> answerableQuizz(User user) {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user); Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user);
@@ -225,7 +203,7 @@ public class QuizzManagerImpl implements QuizzManager {
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())), .sorted((q1,q2) -> q1.getName().compareTo(q2.getName())),
publicQuizz.stream() publicQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())) .sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
).collect(Collectors.toList()); ).toList();
} }
@Override @Override
@@ -233,42 +211,141 @@ public class QuizzManagerImpl implements QuizzManager {
Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteFalse(user); Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteFalse(user);
return ownQuizz.stream() return ownQuizz.stream()
.sorted((q1,q2) -> q1.getName().compareTo(q2.getName())) .sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
.collect(Collectors.toList()); .toList();
} }
Logger logger = LoggerFactory.getLogger(QuestionsController.class);
@Override @Override
public boolean canEditQuizz(User user, long quizzId) { public List<Quizz> completedQuizz(User user) {
try { Set<Quizz> ownQuizz = qRepository.findByOwnerAndIsCompleteTrue(user);
Quizz quizz = qRepository.getReferenceById(quizzId); return ownQuizz.stream()
logger.info("Quizz owner is "+quizz.getOwner().getName()); .sorted((q1,q2) -> q1.getName().compareTo(q2.getName()))
return quizz.getOwner().equals(user); .toList();
} catch (EntityNotFoundException e) {
logger.info("Could not find quizz of id "+quizzId);
return false;
}
} }
private Optional<JsonNode> checkEditQuizz(User user, long quizzId) { /**
* Checks that the given user can edit the quizz whose id is provided.
* If not, throws an APIException
* @param user The user that makes the request
* @param quizzId The ID of the quizz we want to get
* @return The Quizz database object
* @throws APIException If something isn't right
*/
@NotNull
public Quizz getQuizz4Edit(User user, long quizzId) throws APIException {
if (user == null) if (user == null)
return Optional.of(errorNode("You need to be logged in to edit the quizz")); throw LOGIN_REQUIRED_EXCEPTION;
if(!canEditQuizz(user, quizzId))
return Optional.of(errorNode("User has no right to edit quizz"));
Optional<Quizz> oquizz = qRepository.findById(quizzId); Optional<Quizz> oquizz = qRepository.findById(quizzId);
if (!oquizz.isPresent()) if (!oquizz.isPresent())
return Optional.of(errorNode("Could not find quizz with id "+quizzId)); throw new APIException("Could not find quizz with id %d".formatted(quizzId), HttpStatus.NOT_FOUND);
if(oquizz.get().isComplete()) Quizz q = oquizz.get();
return Optional.of(errorNode("Quizz is complete, cannot edit, answers might have already been cast")); if (!q.getOwner().equals(user))
return Optional.empty(); throw new APIException("User has no right to edit quizz (must be author)", HttpStatus.FORBIDDEN);
if (q.isComplete())
throw new APIException("Quizz is complete, cannot edit, answers might have already been cast", HttpStatus.CONFLICT);
return q;
} }
/**
* Checks that the given user can answer the quizz whose id is provided.
* If not, throws an APIException
* @param user The user that makes the request
* @param quizzId The ID of the quizz we want to get
* @return The Quizz database object
* @throws APIException If something isn't right
*/
@NotNull
public Quizz getQuizz4Answer(User user, long quizzId) throws APIException {
if (user == null)
throw LOGIN_REQUIRED_EXCEPTION;
Optional<Quizz> oquizz = qRepository.findById(quizzId);
if (!oquizz.isPresent())
throw new APIException("Could not find quizz with id %d".formatted(quizzId), HttpStatus.NOT_FOUND);
Quizz q = oquizz.get();
if(!q.isComplete())
throw new APIException("Cannot answer a quizz that is not marked as complete", HttpStatus.CONFLICT);
if (!(
q.getOwner().equals(user) ||
q.getPublicQuestionCount() != null
))
throw new APIException("User has no right to answer quizz (must be author, or quizz must be public)", HttpStatus.FORBIDDEN);
return q;
}
/**
* Gets the specified quizz if user has the right to access all of its forms
* @param user The requesting user
* @param quizzFormId The ID of the quizz we want to get
* @return The Quizz database object
* @throws APIException If something isn't right
*/
@NotNull
public Quizz getQuizz4Watch(User user, long quizzId) throws APIException {
if (user == null)
throw LOGIN_REQUIRED_EXCEPTION;
Optional<Quizz> oquizz = qRepository.findById(quizzId);
if (!oquizz.isPresent())
throw new APIException("Could not find quizz with id %d".formatted(quizzId), HttpStatus.NOT_FOUND);
Quizz q = oquizz.get();
if (!(q.getOwner().equals(user) || uService.hasPrivilege(user, Privilege.VIEW_ALL_FORMS)))
throw new APIException((
"User %s cannot access all forms of quizz %d." +
"Must be either author of the quizz, or have VIEW_ALL_FORMS privilege")
.formatted(user.getName(), q.getId()));
return q;
}
/**
* Gets the specified quizz form if user has right to access it and if the quizz form is complete
* @param user The requesting user
* @param quizzFormId The ID of the quizz form we want to get
* @return The QuizzForm database object
* @throws APIException If something isn't right
*/
@NotNull
public QuizzForm getQuizzForm(User user, long quizzFormId) throws APIException {
if (user == null)
throw LOGIN_REQUIRED_EXCEPTION;
Optional<QuizzForm> oqf = qfRepository.findById(quizzFormId);
if(oqf.isEmpty())
throw new APIException("Could not find quizz form with id %d".formatted(quizzFormId), HttpStatus.NOT_FOUND);
QuizzForm qf = oqf.get();
if(!qf.isDone())
throw new APIException("Cannot access a quizz form that is not done");
if(!(
qf.getUser().equals(user) ||
qf.getQuizz().getOwner().equals(user) ||
uService.hasPrivilege(user, Privilege.VIEW_ALL_FORMS)
)) throw new APIException((
"User %s cannot access quizz form %d." +
"Must be either author of the form or author of the quizz, or have VIEW_ALL_FORMS privilege")
.formatted(user.getName(), qf.getId()));
return qf;
}
/**
* Gets the question and checks it is associated with the given quizz
* @param quizz The given quizz to check the question against
* @param questionId The ID of the question we want to get
* @return The Question database object
* @throws APIException If something isn't right
*/
@NotNull
private Question getQuestion(Quizz quizz, long questionId) throws APIException{
Optional<Question> oq = questionRepository.findById(questionId);
if(oq.isEmpty())
throw new APIException("Could not find question with id %d".formatted(questionId));
Question q = oq.get();
if(!q.getQuizz().equals(quizz))
throw new APIException("Question is not associated with the right quizzId (got %d, expected %d)"
.formatted(q.getQuizz().getId(), quizz.getId()));
return q;
}
@Override @Override
public JsonNode getQuizzInfo(User user, long quizzId) { public JsonNode getQuizzInfo(User user, long quizzId) throws APIException{
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); if(user == null)
if(authCheck.isPresent()) return authCheck.get(); throw LOGIN_REQUIRED_EXCEPTION;
Quizz quizz = getQuizz4Edit(user, quizzId);
Quizz quizz = qRepository.findById(quizzId).get();
ArrayNode n = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount()); ArrayNode n = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount());
for(int i = 0;i<quizz.getQuestionCount();i++)n.add(JsonNodeFactory.instance.nullNode()); for(int i = 0;i<quizz.getQuestionCount();i++)n.add(JsonNodeFactory.instance.nullNode());
@@ -289,13 +366,11 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode setQuizzName(User user, long quizzId, String newName) { 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) Quizz quizz = getQuizz4Edit(user, quizzId);
return errorNode("Le nom est invalide");
Quizz quizz = qRepository.findById(quizzId).get(); if(newName.isBlank() || newName.length()>255)
throw new APIException("Le nom est invalide");
quizz.setName(newName); quizz.setName(newName);
ObjectNode out = JsonNodeFactory.instance.objectNode(); ObjectNode out = JsonNodeFactory.instance.objectNode();
@@ -306,11 +381,8 @@ public class QuizzManagerImpl implements QuizzManager {
public static final QTypes DEFAULT_QTYPE = QTypes.DCC; public static final QTypes DEFAULT_QTYPE = QTypes.DCC;
@Override @Override
public JsonNode addQuestion(User user, long quizzId) { public JsonNode addQuestion(User user, long quizzId) throws APIException {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Quizz quizz = getQuizz4Edit(user, quizzId);
if(authCheck.isPresent()) return authCheck.get();
Quizz quizz = qRepository.findById(quizzId).get();
Question q = new Question(); Question q = new Question();
q.setType(DEFAULT_QTYPE); q.setType(DEFAULT_QTYPE);
@@ -330,18 +402,9 @@ public class QuizzManagerImpl implements QuizzManager {
} }
@Override @Override
public JsonNode removeQuestion(User user, long quizzId, long questionId) { public JsonNode removeQuestion(User user, long quizzId, long questionId) throws APIException {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Quizz quizz = getQuizz4Edit(user, quizzId);
if(authCheck.isPresent()) return authCheck.get(); Question q = getQuestion(quizz, questionId);
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 -> { questionRepository.findByQuizz(quizz).forEach(qq -> {
if(qq.getIndex()>q.getIndex()) if(qq.getIndex()>q.getIndex())
@@ -356,30 +419,21 @@ public class QuizzManagerImpl implements QuizzManager {
} }
@Override @Override
public JsonNode reorderQuestions(User user, long quizzId, List<Long> newOrder) { public JsonNode reorderQuestions(User user, long quizzId, List<Long> newOrder) throws APIException {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Quizz quizz = getQuizz4Edit(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 // We need that the base set of neworder is the list of ids
// 1) The length is right // 1) The length is right
if(quizz.getQuestionCount() != newOrder.size()) if(quizz.getQuestionCount() != newOrder.size())
return errorNode("You must put every question in order"); throw new APIException("You must put every question in order");
// 2) There is no duplicates // 2) There is no duplicates
if(new HashSet<Long>(newOrder).size() != newOrder.size()) if(new HashSet<Long>(newOrder).size() != newOrder.size())
return errorNode("You shouldn't put duplicates in the new order"); throw new APIException("You shouldn't put duplicates in the new order");
// 3) All ids correspond to an actual question of the right quizz // 3) All ids correspond to an actual question of the right quizz
List<Question> questions = new ArrayList<>(newOrder.size()); List<Question> questions = new ArrayList<>(newOrder.size());
for(int i = 0;i<newOrder.size();i++){ for(int i = 0;i<newOrder.size();i++){
try { Question q = getQuestion(quizz, newOrder.get(i));
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); questions.add(q);
} catch (EntityNotFoundException e){
return errorNode("Could not find question with id "+newOrder.get(i));
}
} }
// All Checks passed // All Checks passed
@@ -394,23 +448,11 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode editQuestion(User user, long quizzId, long questionId, JsonNode value) { public JsonNode editQuestion(User user, long quizzId, long questionId, JsonNode value) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Quizz quizz = getQuizz4Edit(user, quizzId);
if(authCheck.isPresent()) return authCheck.get(); Question q = getQuestion(quizz, questionId);
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)) if(!q.getType().validate(value))
return errorNode("Invalid question value"); throw new APIException("Invalid question value");
q.setValue(value); q.setValue(value);
ObjectNode out = JsonNodeFactory.instance.objectNode(); ObjectNode out = JsonNodeFactory.instance.objectNode();
@@ -420,20 +462,8 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode setQuestionType(User user, long quizzId, long questionId, QTypes type) { public JsonNode setQuestionType(User user, long quizzId, long questionId, QTypes type) {
Optional<JsonNode> authCheck = checkEditQuizz(user, quizzId); Quizz quizz = getQuizz4Edit(user, quizzId);
if(authCheck.isPresent()) return authCheck.get(); Question q = getQuestion(quizz, questionId);
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) // If type is the same, we don't change (and dont reset the value)
JsonNode n; JsonNode n;
@@ -452,33 +482,9 @@ public class QuizzManagerImpl implements QuizzManager {
return out; 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 @Override
public JsonNode getQuizzFormData(User user, long quizzFormId) { public JsonNode getQuizzFormData(User user, long quizzFormId) {
Optional<QuizzForm> oqf = canViewQuizzForm(user, quizzFormId); QuizzForm form = getQuizzForm(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<Question> questions = questionRepository.findByQuizzOrderByIndexAsc(form.getQuizz());
List<Answer> answers = answerRepository.findByFormAndQuestionIn(form, questions); List<Answer> answers = answerRepository.findByFormAndQuestionIn(form, questions);
assert questions.size() == answers.size(); assert questions.size() == answers.size();
@@ -500,16 +506,14 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode getAllFormsData(User u, long quizzId) { public JsonNode getAllFormsData(User u, long quizzId) {
Optional<Quizz> oq = canViewQuizzFormsOfQuizz(u, quizzId); Quizz quizz = getQuizz4Watch(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(); ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("id",JsonNodeFactory.instance.numberNode(quizz.getId())); out.set("id",JsonNodeFactory.instance.numberNode(quizz.getId()));
out.set("name",JsonNodeFactory.instance.textNode(quizz.getName())); out.set("name",JsonNodeFactory.instance.textNode(quizz.getName()));
ArrayNode questionNode = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount()); ArrayNode questionNode = JsonNodeFactory.instance.arrayNode(quizz.getQuestionCount());
List<Question> questions = quizz.getQuestions().stream().sorted((p,q) -> Integer.valueOf(p.getIndex()).compareTo(q.getIndex())).toList(); List<Question> questions = quizz.getQuestions().stream().sorted((p,q) -> Integer.compare(p.getIndex(),q.getIndex())).toList();
for(int i=0;i<questions.size();i++) { for(int i=0;i<questions.size();i++) {
ObjectNode qNode = JsonNodeFactory.instance.objectNode(); ObjectNode qNode = JsonNodeFactory.instance.objectNode();
qNode.set("id", JsonNodeFactory.instance.numberNode(questions.get(i).getId())); qNode.set("id", JsonNodeFactory.instance.numberNode(questions.get(i).getId()));
@@ -528,6 +532,7 @@ public class QuizzManagerImpl implements QuizzManager {
qfNode.set("done", JsonNodeFactory.instance.booleanNode(qf.isDone())); qfNode.set("done", JsonNodeFactory.instance.booleanNode(qf.isDone()));
List<Answer> answers = answerRepository.findByFormAndQuestionIn(qf, questions); List<Answer> answers = answerRepository.findByFormAndQuestionIn(qf, questions);
answers.sort((a1,a2) -> Integer.compare(a1.getQuestion().getIndex(),a2.getQuestion().getIndex()));
ArrayNode answersNode = JsonNodeFactory.instance.arrayNode(); ArrayNode answersNode = JsonNodeFactory.instance.arrayNode();
for(int i=0;i<answers.size();i++) { for(int i=0;i<answers.size();i++) {
answersNode.add(answers.get(i).getValue()); answersNode.add(answers.get(i).getValue());
@@ -543,10 +548,7 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode getQuizzFormAdvancments(User user, long quizzId) { public JsonNode getQuizzFormAdvancments(User user, long quizzId) {
Optional<Quizz> oq = canViewQuizzFormsOfQuizz(user, quizzId); Quizz quizz = getQuizz4Watch(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); List<QuizzForm> quizzForms = qfRepository.findByQuizz(quizz);
quizzForms.sort((qfa,qfb) -> qfa.getUser().getName().compareTo(qfb.getUser().getName())); quizzForms.sort((qfa,qfb) -> qfa.getUser().getName().compareTo(qfb.getUser().getName()));
@@ -562,21 +564,20 @@ public class QuizzManagerImpl implements QuizzManager {
} }
ObjectNode out = JsonNodeFactory.instance.objectNode(); ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("success", JsonNodeFactory.instance.booleanNode(true)); out.set("success", JsonNodeFactory.instance.booleanNode(true));
out.set("publicQuestionCount", quizz.getPublicQuestionCount()==null?
JsonNodeFactory.instance.nullNode():
JsonNodeFactory.instance.numberNode(quizz.getPublicQuestionCount()));
out.set("questionCount", JsonNodeFactory.instance.numberNode(quizz.getQuestionCount()));
out.set("data", formsNode); out.set("data", formsNode);
return out; return out;
} }
@Override @Override
public Quizz duplicateQuizz(User u, long quizzId) { public Quizz duplicateQuizz(User u, long quizzId) {
if (!canEditQuizz(u, quizzId)) Quizz q = getQuizz4Edit(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(); Quizz nq = new Quizz();
nq.setName(q.getName() + "("+ Integer.toHexString((int)(Math.random()*0xFFFFFFF)) +")"); nq.setName(q.getName() + "("+ Integer.toHexString(rand.nextInt(0xFFFFFFF)) +")");
nq.setOwner(q.getOwner()); nq.setOwner(q.getOwner());
nq.setQuestionCount(q.getQuestionCount()); nq.setQuestionCount(q.getQuestionCount());
nq.setPublicQuestionCount(q.getPublicQuestionCount()); nq.setPublicQuestionCount(q.getPublicQuestionCount());
@@ -596,6 +597,30 @@ public class QuizzManagerImpl implements QuizzManager {
return nq; return nq;
} }
@Override
public boolean markComplete(User u, long quizzId) {
Quizz q = getQuizz4Edit(u, quizzId);
q.setPublicQuestionCount(null);
q.setComplete(true);
qRepository.save(q);
return true;
}
@Override
public boolean setPublicQuestionCount(User u, long quizzId, Integer publicQuestionCount) {
Quizz q = getQuizz4Watch(u, quizzId);
if(!q.isComplete())
return false;
if(publicQuestionCount != null && (publicQuestionCount > q.getQuestionCount() || publicQuestionCount < 0))
return false;
q.setPublicQuestionCount(publicQuestionCount);
qRepository.save(q);
return true;
}
} }
@@ -1,4 +1,4 @@
package com.bernard.misael.service.exception; package com.bernard.misael.quizz.service.exception;
public class MalformedAnswerException extends QuestionTypeException{ public class MalformedAnswerException extends QuestionTypeException{
@@ -1,4 +1,4 @@
package com.bernard.misael.service.exception; package com.bernard.misael.quizz.service.exception;
public class MalformedClientAnswerException extends QuestionTypeException { public class MalformedClientAnswerException extends QuestionTypeException {
@@ -1,4 +1,4 @@
package com.bernard.misael.service.exception; package com.bernard.misael.quizz.service.exception;
public abstract class QuestionTypeException extends Exception{ public abstract class QuestionTypeException extends Exception{
@@ -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);
}
@@ -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);
}
@@ -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));
}
}
@@ -0,0 +1,17 @@
package com.bernard.misael.thecrew;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class TheCrewConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) {
String imgFolder = System.getenv("THECREW_IMAGES_FOLDER");
registry.addResourceHandler("/thecrew/**")
.addResourceLocations("file://"+imgFolder);
}
}
@@ -0,0 +1,35 @@
package com.bernard.misael.thecrew;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/thecrew")
public class TheCrewController {
@GetMapping("/")
public String getTheCrewPage() {
return "thecrew.html";
}
@GetMapping("/missions.json")
public ResponseEntity<String> getMissions() {
File missionsFile = new File(System.getenv("THECREW_MISSIONS_FILE"));
try {
String missionsData = FileCopyUtils.copyToString(new FileReader(missionsFile));
return ResponseEntity.ok()
.header("Content-Type","application/json")
.body(missionsData);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
@@ -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);
}
}
@@ -2,7 +2,13 @@ alter table role_privileges drop constraint role_privileges_privileges_check;
alter table role_privileges add 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')); check (privileges in ('LIST_USERS','ADD_USERS','LIST_QUIZZ','CREATE_QUIZZ','VIEW_ALL_FORMS'));
do $$
begin
if exists (select id from roles where "name" = 'ADMIN')
then
insert into role_privileges VALUES insert into role_privileges VALUES
((select id from roles where "name" = 'ADMIN'),'CREATE_QUIZZ'); ((select id from roles where "name" = 'ADMIN'),'CREATE_QUIZZ'),
insert into role_privileges VALUES
((select id from roles where "name" = 'ADMIN'),'VIEW_ALL_FORMS'); ((select id from roles where "name" = 'ADMIN'),'VIEW_ALL_FORMS');
end if;
end
$$
@@ -0,0 +1,8 @@
create table user_privileges (id bigint not null, privileges varchar(255) not null check (privileges in ('LIST_USERS','ADD_USERS','LIST_QUIZZ','CREATE_QUIZZ','VIEW_ALL_FORMS')), primary key (id, privileges));
drop table role_privileges cascade;
drop table roles cascade;
alter table if exists users alter column password drop not null;
alter table if exists users add column oidc_id varchar(255);
alter table if exists users drop constraint if exists UKs3fyv3dn1pq639igamsrq5vhj;
alter table if exists users add constraint UKs3fyv3dn1pq639igamsrq5vhj unique (oidc_id);
alter table if exists user_privileges add constraint FKqltalsck5q21fw7vcpsriacst foreign key (id) references users;
@@ -0,0 +1,66 @@
import xml.etree.ElementTree
import copy
import sys
import subprocess
import pathlib
thecrewSvgFile = sys.argv[1]
thecrewModifiersSvgFile = sys.argv[2]
def withShownHidden(origXML,toShow,toHide,recolorize={}):
outXML = copy.deepcopy(origXML)
for th in toHide:
node = outXML.find(f".//{{http://www.w3.org/2000/svg}}*[@{{http://www.inkscape.org/namespaces/inkscape}}label='{th}']")
if node is None:
print(f"Could not hide node with label {th} : Not Found")
exit(1)
node.attrib['style'] = "display: none"
for ts in toShow:
node = outXML.find(f".//{{http://www.w3.org/2000/svg}}*[@{{http://www.inkscape.org/namespaces/inkscape}}label='{ts}']")
if node is None:
print(f"Could not show node with label {ts} : Not Found")
exit(1)
nodes = outXML.findall(f".//{{http://www.w3.org/2000/svg}}*[@style]")
for node in nodes:
for rc in recolorize:
node.attrib['style'] = node.attrib['style'].replace(rc,recolorize[rc])
return outXML
def inkscapeToPng(fi,fo):
subprocess.run(["inkscape","--export-type","png","--export-dpi","300","--export-filename",fo,fi])
# Reading file:
origXML = xml.etree.ElementTree.parse(thecrewSvgFile)
suits = ["Hearts","Spades","Diamonds","Clubs","Square","Circle","Triangle","Cross"]
suitColors = ["e36a6a","545454","e36a6a","545454","e87caf","0085ca","94c11e","e6b436"]
numbers = ["One","Two","Three","Four","Five","Six","Seven","Eight","Nine"]
modifiers = numbers + ["Omega","OneArrow","TwoArrow","ThreeArrow","FourArrow"]
for s,sc in zip(suits,suitColors):
for n in numbers:
outFile = f"{n.lower()}-of-{s.lower()}.png"
toShow = [s,n]
toHide = list(set(suits+numbers) - set(toShow))
out = withShownHidden(origXML,toShow,toHide, recolorize = {
"123456": sc
})
out.write('card.tmp.svg')
inkscapeToPng('card.tmp.svg',outFile)
origXML = xml.etree.ElementTree.parse(thecrewModifiersSvgFile)
for m in modifiers:
outFile = f"{m.lower()}.png"
toShow = [m]
toHide = list(set(modifiers) - set(toShow))
out = withShownHidden(origXML,toShow,toHide)
out.write('card.tmp.svg')
inkscapeToPng('card.tmp.svg',outFile)
pathlib.Path.unlink('card.tmp.svg')
print("Done !")
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="thecrew-modifiers.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="11.313708"
inkscape:cx="17.191534"
inkscape:cy="25.411651"
inkscape:window-width="1680"
inkscape:window-height="1022"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true"><inkscape:grid
id="grid8"
units="mm"
originx="0"
originy="0"
spacingx="0.99999997"
spacingy="0.99999997"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" /></sodipodi:namedview><defs
id="defs1"><inkscape:path-effect
effect="tiling"
id="path-effect34"
is_visible="true"
lpeversion="1.3.1"
unit="px"
seed="1;1"
lpesatellites=""
num_rows="17"
num_cols="1"
gapx="-4"
gapy="0"
offset="0"
offset_type="false"
scale="0"
rotate="0"
mirrorrowsx="false"
mirrorrowsy="false"
mirrorcolsx="false"
mirrorcolsy="false"
mirrortrans="false"
shrink_interp="false"
split_items="false"
link_styles="false"
interpolate_scalex="false"
interpolate_scaley="true"
interpolate_rotatex="false"
interpolate_rotatey="true"
random_scale="false"
random_rotate="false"
random_gap_y="false"
random_gap_x="false"
transformorigin="" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath242"><path
style="opacity:1;fill:#415c12;fill-opacity:1;stroke:#000000;stroke-width:0.671709;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1709.2189,406.13284 c -4.2633,2.90065 -1.7186,8.85042 -2.4113,13.16379 0,48.65635 0,97.31269 0,145.96904 0.1247,4.23038 5.9488,5.23308 7.8742,1.65405 7.587,-10.10644 18.2878,-16.3931 28.9918,-22.67885 22.5621,-14.49643 36.3572,-41.58453 34.2383,-68.39796 -1.6209,-30.89618 -24.6859,-59.00274 -54.3368,-67.43023 -4.602,-1.35315 -9.578,-2.61297 -14.3562,-2.27984 z"
id="path243" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath243"><path
style="opacity:1;fill:#415c12;fill-opacity:1;stroke:#000000;stroke-width:0.671709;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1568.5723,615.59047 c -3.9338,3.0891 -1.483,8.76578 -2.166,13.04576 0,44.19669 0,88.39337 0,132.59005 0.4192,4.82868 5.839,4.15875 9.2599,3.4029 21.5358,-3.23562 41.2278,-16.72175 52.2964,-35.44977 6.5292,-10.63248 13.3334,-21.63647 23.9575,-28.67732 4.2146,-3.42916 9.8684,-4.63414 13.8018,-8.32172 1.9716,-4.68498 -3.5275,-6.40277 -6.75,-8.00905 -14.2712,-7.5491 -24.312,-20.8286 -32.1004,-34.61212 -12.2796,-19.18606 -33.756,-32.48789 -56.482,-34.284 -0.6058,0.10456 -1.2115,0.21019 -1.8172,0.31528 z"
id="path244" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath244"><path
style="opacity:1;fill:#415c12;fill-opacity:1;stroke:#000000;stroke-width:0.671709;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1644.3233,522.44617 c -3.748,3.17056 -1.3791,8.72096 -2.0457,12.98096 0,44.10374 0,88.20747 0,132.31121 0.3605,4.57601 5.4519,4.27785 8.7705,3.47115 36.1672,-4.81331 65.025,-39.63862 63.7253,-75.98111 -0.055,-16.95341 -6.5914,-33.48486 -17.4777,-46.3916 -12.665,-14.7911 -30.87,-25.70564 -50.5877,-26.88931 -0.7949,0.16622 -1.5898,0.33244 -2.3847,0.49866 z"
id="path245" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath251"><path
style="opacity:1;fill:#415c12;fill-opacity:1;stroke:#000000;stroke-width:0.94994;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1821.5575,363.26619 c -21.4621,-0.3126 -42.5982,7.61778 -59.6293,20.42026 -12.0506,9.58617 -21.3723,22.46414 -34.9745,29.97835 -4.3462,2.44276 -12.837,1.2715 -12.0875,8.27159 0,99.97469 0,199.9494 0,299.92411 0.7288,5.67004 7.1425,3.89104 9.9624,0.92125 59.3638,-40.45963 116.3623,-86.63884 158.6972,-145.27945 21.0582,-29.94486 39.0038,-63.9963 41.8724,-101.08789 1.3831,-21.46882 -1.8003,-43.77497 -13.2705,-62.34701 -17.7689,-29.99071 -51.1684,-51.25706 -86.4957,-50.78858 -1.358,-0.0241 -2.7164,-0.0228 -4.0745,-0.0126 z"
id="path251" /></clipPath></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><rect
style="display:inline;fill:#001aaa;fill-opacity:1;stroke:#001277;stroke-width:0.6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect13"
width="9.999999"
height="9.999999"
x="1.4999962"
y="0"
transform="translate(-1.4999962)" /><g
id="g27"
transform="translate(-0.22206006)"
inkscape:label="Numbers"
style="display:inline;fill:#123456;fill-opacity:1"><path
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 3.0555119,2.5671475 1.36312,-1.36312 h 2.265848 v 5.930925 h 0.704128 v 1.66102 h -3.538695 v -1.606856 h 0.848565 v -4.062278 l -0.839538,0.839538 z"
id="path1"
inkscape:label="One" /><path
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.6662109,8.689902 h 5.163605 V 7.245537 h -2.310984 l 2.15752,-2.157521 c 0,0 0,-2.390954 0,-2.78943 0,-0.398477 -0.553503,-0.988488 -0.988487,-0.988488 -0.434984,0 -2.554528,0 -3.082817,0 -0.528289,0 -0.990744,0.475441 -0.990744,0.990744 0,0.515304 0,1.821255 0,1.821255 h 1.821254 V 2.695786 h 1.173547 V 4.663734 L 2.6391289,7.63371 Z"
id="path17"
sodipodi:nodetypes="ccccczzzzccccccc"
inkscape:label="Two" /><path
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.7361059,3.5603965 v -2.323504 h 5.183202 v 1.391549 l -1.200052,1.404316 v 0.165964 c 0.677046,0 1.027704,0.471789 1.027704,1.027704 0,0 0,1.715691 0,2.214989 0,0.499298 -0.619358,1.321693 -1.222393,1.321693 -0.603035,0 -2.042752,0 -2.537343,0 -0.494591,0 -1.462412,-0.406745 -1.462412,-0.943624 v -1.514288 h 1.998605 v 0.893655 h 1.212818 v -1.736244 h -1.238351 v -0.919189 l 1.31495,-1.774544 h -1.366016 v 0.80429 z"
id="path18"
sodipodi:nodetypes="cccccczzzzzccccccccccc"
inkscape:label="Three" /><path
id="path19"
style="display:inline;opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 5.0597959,1.1994565 -2.636015,3.538802 v 1.697054 h 2.906799 v 0.75861 h -0.8847 v 1.606621 h 3.556372 v -1.606621 h -0.77618 v -0.812353 h 0.794267 v -1.498617 h -0.830441 v -3.683496 z m 0.234611,1.841748 v 1.931665 h -1.390096 z"
inkscape:label="Four" /><path
style="display:inline;opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.6432259,1.221114 h 5.119369 v 1.634113 h -3.076728 v 0.817056 c 0,0 1.346344,0 1.953275,0 0.606931,0 1.161752,0.391594 1.161752,1.161752 0,0.770159 0,2.127308 0,2.859698 0,0.664285 -0.426838,1.085153 -1.085152,1.085153 -0.658315,0 -2.06756,0 -2.821398,0 -0.753838,0 -1.212818,-0.602587 -1.212818,-1.110686 0,-0.508099 0,-1.480915 0,-1.480915 h 1.889442 v 1.136219 h 1.289417 V 5.306396 h -3.204393 z"
id="path20"
sodipodi:nodetypes="ccccczzszzzccccccc"
inkscape:label="Five" /><g
id="g22"
transform="translate(-9.1161521,-0.1206331)"
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.00036;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="Six"><path
id="path21"
style="opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.881901,1.381311 c -0.879342,0 -1.543575,0.2853348 -1.543575,1.5435751 0,1.2582403 -0.0052,3.6036771 -0.0052,4.6632812 0,1.0596041 0.68622,1.2510864 1.413351,1.2510864 h 2.784746 C 14.95819,7.5670265 15.64519,7.0621748 16.493018,6.698106 V 5.3712484 c 0,-0.5323266 -0.606087,-1.1647868 -1.164787,-1.1647868 H 13.43274 v -1.317749 h 1.173572 V 3.6111491 H 16.36176 V 2.5771037 c 0,-0.726466 -0.581711,-1.1957927 -1.195793,-1.1957927 h -2.284098 z m 0.550871,4.3511556 h 1.236617 v 1.6066203 h -1.236617 z"
sodipodi:nodetypes="czzzcczzccccczzzcccccc" /><circle
style="display:inline;opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path22"
cx="16.532625"
cy="8.0492821"
r="0.81067312" /></g><path
style="display:inline;opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 2.4644949,3.68505 V 1.157282 h 5.51513 v 1.200051 l -2.195839,6.485385 h -2.37457 l 2.37457,-6.000258 h -1.404315 v 0.868123 z"
id="path23"
inkscape:label="Seven" /><path
id="path24"
style="display:inline;opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 3.6048469,1.2694785 c -0.642741,0 -0.893486,0.525011 -0.893486,1.092439 v 1.715141 c 0,0.509739 0.437201,0.725072 0.83044,0.83044 -0.420935,0.11279 -0.927592,0.388208 -0.927592,1.065052 v 1.541508 c 0,0.661002 0.58919,1.216463 1.216464,1.216463 h 3.010669 c 0.485027,0 0.98857,-0.369975 0.98857,-0.98857 v -1.86862 c 0,-0.480076 -0.439475,-0.806637 -0.941028,-0.941028 0.465545,-0.124742 0.873332,-0.390235 0.873332,-0.873332 v -1.566312 c 0,-0.482508 -0.221212,-1.223181 -1.024744,-1.223181 z m 1.038179,1.489314 h 1.209745 v 1.453658 h -1.209745 z m -0.02687,2.825668 h 1.272791 v 1.634009 h -1.272791 z"
inkscape:label="Eight" /><g
id="g25"
transform="rotate(180,3.797988,10.438783)"
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="Nine"><g
id="g26"
transform="translate(-11.208993,10.902596)"
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-dasharray:none;stroke-opacity:1"><path
id="path25"
style="opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.881901,1.381311 c -0.117873,0 -0.23188,0.00513 -0.340941,0.017037 0.39752,1.1466262 -0.417494,1.7180355 -1.202634,1.6669695 0,1.2582403 -0.0052,3.4632455 -0.0052,4.5228496 0,1.0596041 0.68622,1.2510864 1.413351,1.2510864 h 2.68996 c 0.80934,0 1.056581,-0.6103529 1.056581,-1.2994692 V 5.3712484 c 0,-0.5323266 -0.606087,-1.1647868 -1.164787,-1.1647868 H 13.43274 v -1.317749 h 1.173572 V 3.6111491 H 16.36176 V 2.5771037 c 0,-0.726466 -0.581711,-1.1957927 -1.195793,-1.1957927 h -2.284098 z m 0.550871,4.3511556 h 1.236617 v 1.6066203 h -1.236617 z"
sodipodi:nodetypes="ccczzsszzccccczzzcccccc" /><circle
style="opacity:1;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.6;stroke-linecap:square;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="circle25"
cx="11.483473"
cy="1.9213587"
r="0.81067312" /></g></g><path
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.600001;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.22206,8.0000002 v 0.9999999 h 2.9999999 c 0,-2.9999998 -1.9999999,-6.470833 1.0000002,-6.4708329 2.9999997,0 0.9999998,3.4708331 0.9999998,6.4708329 H 9.2220597 V 8.0000002 h -2 c 0,0 1.9999999,-4.470833 0,-6.4708331 -0.9999998,-0.99999998 -2.9999999,-1.00000012 -3.9999999,0 -1.9999998,2 0,6.4708331 0,6.4708331 z"
id="path14"
sodipodi:nodetypes="cccsccccsscc"
inkscape:label="Omega" /></g><g
id="g5"
inkscape:label="FourArrow"
transform="translate(0.50000393)"
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-opacity:1"><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 4.9999997,0.99999995 2.9999998,4.9999997 4.9999997,8.9999996 H 3.9999999 l -2,-3.9999999 2,-3.99999975 z"
id="path2" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 6.9999996,0.99999997 4.9999997,4.9999997 6.9999996,8.9999996 H 5.9999998 l -2,-3.9999999 2,-3.99999973 z"
id="path3" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 8.9999997,0.99999997 6.9999998,4.9999997 8.9999997,8.9999996 H 7.9999999 l -2,-3.9999999 2,-3.99999973 z"
id="path4" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 2.9999997,0.99999997 0.99999984,4.9999997 2.9999997,8.9999996 H 1.9999999 L -6.3578292e-8,4.9999997 1.9999999,0.99999997 Z"
id="path5" /></g><g
id="g9"
inkscape:label="ThreeArrow"
transform="translate(-0.49999605)"
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-opacity:1"><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 4.9999997,0.99999995 2.9999998,4.9999997 4.9999997,8.9999996 H 3.9999999 l -2,-3.9999999 2,-3.99999975 z"
id="path6" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 6.9999996,0.99999997 4.9999997,4.9999997 6.9999996,8.9999996 H 5.9999998 l -2,-3.9999999 2,-3.99999973 z"
id="path7" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 8.9999997,0.99999997 6.9999998,4.9999997 8.9999997,8.9999996 H 7.9999999 l -2,-3.9999999 2,-3.99999973 z"
id="path8" /></g><g
id="g11"
inkscape:label="TwoArrow"
transform="translate(-1.499996)"
style="display:inline;fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-opacity:1"><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 6.9999996,0.99999997 4.9999997,4.9999997 6.9999996,8.9999996 H 5.9999998 l -2,-3.9999999 2,-3.99999973 z"
id="path10" /><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 8.9999997,0.99999997 6.9999998,4.9999997 8.9999997,8.9999996 H 7.9999999 l -2,-3.9999999 2,-3.99999973 z"
id="path11" /></g><g
id="g13"
inkscape:label="OneArrow"
transform="translate(-1.4999962)"
style="display:inline"><path
style="fill:#ffd700;fill-opacity:1;stroke:#d6b500;stroke-width:0.3;stroke-linejoin:round;stroke-opacity:1"
d="M 7.9999998,1.0000002 5.9999999,4.9999999 7.9999998,8.9999998 H 7 L 5,4.9999999 7,1.0000002 Z"
id="path13" /></g></g></svg>

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 91 KiB

+1 -1
View File
@@ -53,7 +53,7 @@
function next() { function next() {
$.ajax({ $.ajax({
url: "/questions/question/"+qid, url: "/questions/question/"+qid,
type: "GET", type: "POST",
dataType: "json", dataType: "json",
success: function(res) { success: function(res) {
console.log(res) console.log(res)
-1
View File
@@ -1,7 +1,6 @@
<nav> <nav>
<div class="navbar"> <div class="navbar">
<div class="logo"><a href="/">Misael</a></div> <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"> <ul class="menu">
<li><a href="/questions/quizz">Quizz</a></li> <li><a href="/questions/quizz">Quizz</a></li>
<li><a href="/questions/forms">Forms</a></li> <li><a href="/questions/forms">Forms</a></li>
@@ -28,6 +28,10 @@
<ol id="questions-list"> <ol id="questions-list">
</ol> </ol>
<div class="buttonbar">
<button id="mark-complete">Marquer le quizz comme terminé</button>
</div>
<span id="error-textbox" style="color: red"></span> <span id="error-textbox" style="color: red"></span>
<script th:inline="javascript"> <script th:inline="javascript">
@@ -413,6 +417,19 @@
unReorderMode() unReorderMode()
} }
function markComplete() {
if (confirm("Voulez-vous vraiment marquer ce quizz comme terminé ? Vous ne pourrez plus l'éditer !")) {
$.ajax({
url: "/questions/mark-complete/"+quizzid,
type: "POST",
success: function(res) {
console.log("SUPER")
//TODO redirect to the right page
}
})
}
}
function makeEditBlockBCC(id,question) { function makeEditBlockBCC(id,question) {
html = ` html = `
@@ -470,6 +487,7 @@
$("#edit-question-name-confirm").on("click",commitEditName) $("#edit-question-name-confirm").on("click",commitEditName)
$("#edit-question-name-cancel").on("click",cancelEditName) $("#edit-question-name-cancel").on("click",cancelEditName)
$("#add-question").on("click",addQuestion) $("#add-question").on("click",addQuestion)
$("#mark-complete").on("click",markComplete)
getdata() getdata()
</script> </script>
</main> </main>
+8 -2
View File
@@ -7,17 +7,23 @@
<body> <body>
<div th:replace="~{header}"/> <div th:replace="~{header}"/>
<main> <main>
<h3>Quizz à répondre</h3> <h3>Quizz disponibles</h3>
<ul> <ul>
<li th:if="${#lists.isEmpty(answerableQuizz)}">Aucun quizz de disponible malheureusement :(</li> <li th:if="${#lists.isEmpty(answerableQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${answerableQuizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li> <li th:each="q : ${answerableQuizz}"><a th:href="@{/questions/form/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul> </ul>
<h3>Quizz à éditer</h3> <h3>Quizz en édition</h3>
<ul> <ul>
<li th:if="${#lists.isEmpty(editableQuizz)}">Aucun quizz de disponible malheureusement :(</li> <li th:if="${#lists.isEmpty(editableQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${editableQuizz}"><a th:href="@{/questions/quizz-edit/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li> <li th:each="q : ${editableQuizz}"><a th:href="@{/questions/quizz-edit/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul> </ul>
<a sec:authorize="hasAuthority('CREATE_QUIZZ')" th:href="@{/questions/new-quizz}">Nouveau Quizz</a> <a sec:authorize="hasAuthority('CREATE_QUIZZ')" th:href="@{/questions/new-quizz}">Nouveau Quizz</a>
<h3>Quizz terminés</h3>
<ul>
<li th:if="${#lists.isEmpty(completedQuizz)}">Aucun quizz de disponible malheureusement :(</li>
<li th:each="q : ${completedQuizz}"><a th:href="@{/questions/watch/{id}(id=${q.id})}">Quizz <span th:text="${q.name}"/></a></li>
</ul>
</main> </main>
</body> </body>
</html> </html>
@@ -1,81 +0,0 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/showformadvancements.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}" type="text/javascript"></script>
<script th:src="@{/webjars/lodash/4.17.21/lodash.js}" type="text/javascript"></script>
<main>
<button id="refresh-data">Refresh</button>
<table id="forms-table">
<thead>
<tr>
<th>User</th>
<th>Position</th>
<th>Step</th>
</tr>
</thead>
<tbody id="forms-body">
</tbody>
</table>
<span id="error-textbox" style="color: red"></span>
<script th:inline="javascript">
/*<![CDATA[*/
var quizzid = /*[[${quizzId}]]*/ -1;
/*]]>*/
</script>
<script>
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
formsdata=[]
function getdata() {
$('#refresh-data').prop('disabled',true)
$.ajax({
url: "/questions/getformadvancements/"+quizzid,
type: "POST",
dataType: "json",
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$('#refresh-data').prop('disabled',false)
return
}
formsdata=res["data"]
$('.form-line').remove()
for(let qf of formsdata) {
html = `
<tr class="form-line done-${qf["done"]}" id="q-${qf["qfid"]}">
<td>${qf["username"]}</td>
<td>${qf["position"]}</td>
<td>${qf["step"]}</td>
</tr>
`
content = $('<li/>').html(html).contents()
$(`#forms-body`).append(content)
}
$('#refresh-data').prop('disabled',false)
}
})
}
$('#refresh-data').on('click',getdata)
getdata()
</script>
</main>
</body>
</html>
+305
View File
@@ -0,0 +1,305 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<title>The Crew</title>
<style>
form {
background: #CCCCCC;
}
.bordered {
border: 1ex solid black;
display: inline-block;
margin: 1ex;
padding: 1ex;
}
div.card.done {
opacity: .3;
}
ul {
list-style-type: none;
}
</style>
</head>
<body>
<main>
<form id="thecrew-config-form">
<label for="thecrew-player1">Player 1</label><input id="thecrew-player1" type="text"/><br/>
<label for="thecrew-player2">Player 2</label><input id="thecrew-player2" type="text"/><br/>
<label for="thecrew-player3">Player 3</label><input id="thecrew-player3" type="text"/><br/>
<label for="thecrew-player4">Player 4</label><input id="thecrew-player4" type="text"/><br/>
<label for="thecrew-player5">Player 5</label><input id="thecrew-player5" type="text"/><br/>
<label for="thecrew-cardtype">Cartes classiques</label><input id="thecrew-cardtype" type="checkbox"/><br/>
<input id="thecrew-validate" type="submit" value="Valider"/>
</form>
<form id="thecrew-run-form">
<select id="thecrew-level" name="thecrew-level">
</select>
<input id="thecrew-run" type="submit" value="Démarrer la partie"/>
</form>
<span id="thecrew-question">Sélectionnez les paramètres de base</span>
<div id="thecrew-select-captain">
</div>
<div id="thecrew-select-tasks">
</div>
<div id="thecrew-game">
<dl id="thecrew-special-rules"></dl>
<ul id="thecrew-assigned-tasks"></ul>
</div>
</main>
<script th:inline="javascript">
// Helpers
function listProd(l1,l2) {
var o = []
for (i in l1) {
for (j in l2) {
o.push([l1[i],l2[j]])
}
}
return o
}
function emptyListList(k) {
var o = []
for (let i=0;i<k;i++) {
o.push([])
}
return o
}
function takeNRandom(l,n) {
let ll = l.slice()
let o = []
for (let i=0; i<n; i++) {
k = Math.floor(Math.random() * ll.length)
o.push(ll[k])
ll.splice(k,1)
}
return o
}
function joinLists(l1,l2) {
let ll = l1.slice()
ll.push.apply(ll,l2)
return ll
}
function makeCardImg(l) {
s = l[1]
n = l[0]
b = l[2]
i = numbers.indexOf(n)
tn = numbersText[i]
cardFileName=tn+"-of-"+s+".png"
out = $('<div/>')
.addClass('bordered')
.addClass('card')
$('<img/>')
.attr('src','/thecrew/'+cardFileName)
.appendTo(out)
if(b){
modifierFileName=modifierMap[b].toLowerCase()+".png"
$('<img/>')
.attr('src','/thecrew/'+modifierFileName)
.appendTo(out)
}
return out
}
const thecrewSuits = ['square','triangle','circle','cross']
const regularSuits = ['hearts','spades','clubs','diamonds']
const numbers = ['1','2','3','4','5','6','7','8','9']
const numbersText = ['one','two','three','four','five','six','seven','eight','nine']
const trumpNumbers = ['1','2','3','4']
const otherThecrewCards = listProd(numbers,thecrewSuits)
const otherRegularCards = listProd(numbers,regularSuits)
const trumpCards = listProd(trumpNumbers, [null])
const allCards = joinLists
const modifierMap = {
">": "onearrow",
">>": "twoarrow",
">>>": "threearrow",
">>>>": "fourarrow",
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five",
"Ω": "omega",
}
</script>
<script>
var missionsData = {}
$.getJSON('missions.json',function(data) {
missionsData = data
for (md in missionsData['missions']) {
$('<option/>')
.data('thecrew-mission',md)
.text(md)
.appendTo('#thecrew-level')
}
});
function makeMissionRules(i) {
var out = {}
var missionRules = missionsData['missions'][i]
out['cards'] = takeNRandom(otherCards, missionRules['cards'].length)
for (c in out['cards']) {
out['cards'][c].push(missionRules['cards'][c])
}
out['specialRules'] = {}
for (mr in missionRules['special']) {
let specialRule = missionRules['special'][mr]
out['specialRules'][specialRule] = missionsData['specialRules'][specialRule]
}
return out
}
var joueureuses = []
var jl = 0
var otherCards = otherThecrewCards
function configGame(e) {
e.preventDefault()
joueureuses = []
for (let i=0;i<5;i++){
player = $('#thecrew-player'+(i+1)).val()
if (player) {
joueureuses.push(player)
}
}
jl = joueureuses.length
if ($('#thecrew-cardtype').is(":checked")) {
otherCards = otherRegularCards
} else {
otherCards = otherThecrewCards
}
$('#thecrew-config-form').hide()
$('#thecrew-run-form').show()
}
$('#thecrew-validate').click(configGame)
var missionRules = null
var captain = -1
var assignedTasks = emptyListList(jl)
var nextToAssign = -1
var toAssign = []
var specialRules = {}
function startMission(e) {
e.preventDefault()
missionNumber = $('#thecrew-level').val()
missionRules = makeMissionRules(missionNumber)
toAssign = missionRules['cards']
specialRules = missionRules['specialRules']
captain = -1
assignedTasks = emptyListList(jl)
nextToAssign = -1
$('#thecrew-question').text('Qui est capitaine ?')
$('#thecrew-select-captain').empty()
for (j in joueureuses) {
$('<button/>')
.text(joueureuses[j])
.click(selectedCaptain)
.appendTo('#thecrew-select-captain')
}
$('#thecrew-game').hide()
$('#thecrew-select-tasks').hide()
$('#thecrew-select-captain').show()
}
$('#thecrew-run').click(startMission)
function selectedCaptain(e) {
let captainName = e.currentTarget.innerText
captain = joueureuses.indexOf(captainName)
nextToAssign = captain
if (toAssign.length == 1) {
assignTask(0)
startPlaying()
} else if (toAssign.length == 0) {
startPlaying()
} else {
nextAssignTask()
}
$('#thecrew-select-captain').hide()
$('#thecrew-select-tasks').show()
}
function nextAssignTask(){
$('#thecrew-question').text(joueureuses[nextToAssign%jl]+': Quelle tâche choisit-tu ?')
$('#thecrew-select-tasks').empty()
for (at in toAssign) {
makeCardImg(toAssign[at])
.data("thecrew-task",at)
.click(chooseAssignation)
.appendTo('#thecrew-select-tasks')
}
}
function assignTask(tindex) {
assignedTasks[nextToAssign%jl].push(toAssign[tindex])
toAssign.splice(tindex,1)
nextToAssign += 1
}
function chooseAssignation(e) {
assignedIndex = $(e.currentTarget).data("thecrew-task")
assignTask(assignedIndex)
if(toAssign.length==1){
assignTask(0)
startPlaying()
} else {
nextAssignTask()
}
}
function startPlaying() {
$('#thecrew-question').text("Bon jeu !")
$('#thecrew-select-tasks').empty()
$('#thecrew-assigned-tasks').empty()
$('#thecrew-special-rules').empty()
for (at in assignedTasks) {
thePlayerLi = $('<li/>').addClass('bordered')
$('<h2/>')
.text("Tâches de "+joueureuses[at])
.appendTo(thePlayerLi)
playerList = $('<ul/>')
for (tt in assignedTasks[at]) {
theTaskLi = $('<li/>')
makeCardImg(assignedTasks[at][tt])
.data("thecrew-task",[at,tt])
.click(taskDone)
.appendTo(theTaskLi)
theTaskLi.appendTo(playerList)
}
playerList.appendTo(thePlayerLi)
thePlayerLi.appendTo('#thecrew-assigned-tasks')
}
for (sr in specialRules) {
$('<dt/>').text(sr).appendTo('#thecrew-special-rules')
$('<dd/>').text(specialRules[sr]).appendTo('#thecrew-special-rules')
}
$('#thecrew-select-captain').hide()
$('#thecrew-select-tasks').hide()
$('#thecrew-game').show()
}
function taskDone(e) {
ll = $(e.currentTarget).data("thecrew-task")
$(e.currentTarget).toggleClass('done')
}
$('#thecrew-run-form').hide()
$('#thecrew-select-captain').hide()
$('#thecrew-select-tasks').hide()
$('#thecrew-game').hide()
</script>
</body>
</html>
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
<link rel="stylesheet" th:href="@{/css/showformadvancements.css}"/>
</head>
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}" type="text/javascript"></script>
<script th:src="@{/webjars/lodash/4.17.21/lodash.js}" type="text/javascript"></script>
<main>
<div class="button-bar">
<button id="make-public">Rendre public</button>
<button id="make-private">Rendre privé</button>
<span id="question-count-number">Nombre de questions accessibles:</span>
<button id="set-question-begin">&lt;&lt;</button>
<button id="set-question-decrement">&lt;</button>
<span id="last-question-number">x</span>
<button id="set-question-increment">&gt;</button>
<button id="set-question-last">&gt;&gt;</button>
</div>
<button id="refresh-data">Refresh</button>
<table id="forms-table">
<thead>
<tr>
<th>User</th>
<th>Nombre de questions répondues</th>
<th>Étape</th>
</tr>
</thead>
<tbody id="forms-body">
</tbody>
</table>
<span id="error-textbox" style="color: red"></span>
<script th:inline="javascript">
/*<![CDATA[*/
var quizzid = /*[[${quizzId}]]*/ -1;
/*]]>*/
</script>
<script>
function error(txt) {
console.log(txt)
$("#error-textbox").text(txt)
}
formsdata = []
pqc = null
qCount = null
allButtons = [
$('#refresh-data'),
$('#make-public'),
$('#make-private'),
$('#set-question-begin'),
$('#set-question-decrement'),
$('#question-count-number'),
$('#last-question-number'),
$('#set-question-increment'),
$('#set-question-last')
]
function activateButtons() {
if(pqc === null) {
$('#make-public').show()
$('#make-private').hide()
$('#set-question-begin').hide()
$('#set-question-decrement').hide()
$('#question-count-number').hide()
$('#last-question-number').hide()
$('#set-question-increment').hide()
$('#set-question-last').hide()
} else {
$('#make-public').hide()
$('#make-private').show()
$('#question-count-number').show()
$('#set-question-begin').show()
$('#set-question-decrement').show()
if(pqc == 0) {
$('#set-question-begin').prop('disabled', true)
$('#set-question-decrement').prop('disabled', true)
} else {
$('#set-question-begin').prop('disabled', false)
$('#set-question-decrement').prop('disabled', false)
}
$('#last-question-number').show()
$('#last-question-number').text(pqc)
$('#set-question-last').show()
$('#set-question-increment').show()
if(pqc == qCount) {
$('#set-question-last').prop('disabled', true)
$('#set-question-increment').prop('disabled', true)
} else {
$('#set-question-last').prop('disabled', false)
$('#set-question-increment').prop('disabled', false)
}
}
}
function getdata() {
$('#refresh-data').prop('disabled',true)
$.ajax({
url: "/questions/watchdata/"+quizzid,
type: "POST",
dataType: "json",
success: function(res) {
console.log(res)
if(!res["success"]) {
console.log(res["message"])
error(res["message"])
$('#refresh-data').prop('disabled',false)
return
}
formsdata=res["data"]
pqc = res["publicQuestionCount"]
qCount = res["questionCount"]
$('.form-line').remove()
for(let qf of formsdata) {
html = `
<tr class="form-line done-${qf["done"]}" id="q-${qf["qfid"]}">
<td>${qf["username"]}</td>
<td>${qf["position"]}</td>
<td>${qf["step"]}</td>
</tr>
`
content = $('<li/>').html(html).contents()
$(`#forms-body`).append(content)
}
for(const b of allButtons) b.prop('disabled', false)
activateButtons()
}
})
}
function setPQC(n) {
for(const b of allButtons) b.prop('disabled', true)
$.ajax({
url: "/questions/set-public-question-count/"+quizzid,
type: "POST",
data: JSON.stringify({'publicQuestionCount': n}),
contentType: 'application/json',
success: function(res) {
getdata()
}
})
}
function setPublicQuestionCount(n) {
return function (e) { setPQC(n) }
}
function setPublicQuestionCountLast(n) {
return function (e) { setPQC(qCount) }
}
function setPublicQuestionCountRelative(n) {
return function (e) { setPQC(pqc + n) }
}
$('#refresh-data').on('click',getdata)
$('#make-private').on('click',setPublicQuestionCount(null))
$('#make-public').on('click',setPublicQuestionCount(0))
$('#set-question-begin').on('click',setPublicQuestionCount(0))
$('#set-question-decrement').on('click',setPublicQuestionCountRelative(-1))
$('#set-question-increment').on('click',setPublicQuestionCountRelative(+1))
$('#set-question-last').on('click',setPublicQuestionCountLast())
getdata()
</script>
</main>
</body>
</html>
@@ -3,7 +3,7 @@ package com.bernard.misael;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import com.bernard.misael.questions.QTypes; import com.bernard.misael.quizz.questions.QTypes;
@SpringBootTest @SpringBootTest
class MisaelApplicationTests { class MisaelApplicationTests {
+2
View File
@@ -17,6 +17,8 @@ version.org.springframework..spring-jdbc=6.2.3
version.org.springframework.boot..spring-boot-docker-compose=3.4.3 version.org.springframework.boot..spring-boot-docker-compose=3.4.3
version.org.springframework.boot..spring-boot-starter-oauth2-client=4.1.0
version.org.webjars..jquery=1.9.1 version.org.webjars..jquery=1.9.1
## # available=1.10.1 ## # available=1.10.1
## # available=1.10.2 ## # available=1.10.2