diff --git a/src/main/java/com/uniovi/WiqEs04bApplication.java b/src/main/java/com/uniovi/WiqEs04bApplication.java index 5bfb46bc..fc0a8ac7 100644 --- a/src/main/java/com/uniovi/WiqEs04bApplication.java +++ b/src/main/java/com/uniovi/WiqEs04bApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class WiqEs04bApplication { public static void main(String[] args) { SpringApplication.run(WiqEs04bApplication.class, args); diff --git a/src/main/java/com/uniovi/components/MultipleQuestionGenerator.java b/src/main/java/com/uniovi/components/MultipleQuestionGenerator.java deleted file mode 100644 index e5001403..00000000 --- a/src/main/java/com/uniovi/components/MultipleQuestionGenerator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.uniovi.components; - -import com.uniovi.components.generators.QuestionGenerator; -import com.uniovi.entities.Question; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class MultipleQuestionGenerator { - private QuestionGenerator[] generators; - - public MultipleQuestionGenerator(QuestionGenerator... generators) { - this.generators = generators; - } - - public List getQuestions() throws InterruptedException, IOException { - List questions = new ArrayList<>(); - for (QuestionGenerator generator : generators) { - questions.addAll(generator.getQuestions()); - } - return questions; - } -} diff --git a/src/main/java/com/uniovi/components/QuestionGeneratorTestController.java b/src/main/java/com/uniovi/components/QuestionGeneratorTestController.java deleted file mode 100644 index 89a28e69..00000000 --- a/src/main/java/com/uniovi/components/QuestionGeneratorTestController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.uniovi.components; - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class QuestionGeneratorTestController { - - /*@RequestMapping("/test") - public void test() { - List q = qgen.getQuestions(); - for(Question question : q){ - System.out.println(question); - } - }*/ -} diff --git a/src/main/java/com/uniovi/components/generators/AbstractQuestionGenerator.java b/src/main/java/com/uniovi/components/generators/AbstractQuestionGenerator.java deleted file mode 100644 index fd192279..00000000 --- a/src/main/java/com/uniovi/components/generators/AbstractQuestionGenerator.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.uniovi.components.generators; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.uniovi.entities.Answer; -import com.uniovi.entities.Category; -import com.uniovi.entities.Question; -import com.uniovi.services.CategoryService; - -import java.io.IOException; -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -public abstract class AbstractQuestionGenerator implements QuestionGenerator{ - private List questions = new ArrayList<>(); - protected final CategoryService categoryService; - - protected Random random = new SecureRandom(); - - protected String statement; - protected String language; - - protected AbstractQuestionGenerator(CategoryService categoryService) { - this.categoryService = categoryService; - } - - public void questionGenerator(String statement, List options, String correctAnswer, Category category){ - List answers = new ArrayList<>(); - //Generamos las respuestas y las añadimos a la lista - for(String s: options){ - Answer answer = new Answer(s, false); - answers.add(answer); - } - //Generamos la respuesta correcta y la añadimos a la lista - Answer correct = new Answer(correctAnswer, true); - answers.add(correct); - - Question question = new Question(statement, answers, correct, category, language); - //question.scrambleOptions(); - questions.add(question); - } - - public List getQuestions() throws InterruptedException, IOException { - HttpClient client = HttpClient.newHttpClient(); - String endpointUrl = "https://query.wikidata.org/sparql?query=" + - URLEncoder.encode(this.getQuery(), StandardCharsets.UTF_8) + - "&format=json"; - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(endpointUrl)) - .header("Accept", "application/json") - .build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - - // Process the JSON response using Jackson ObjectMapper - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode jsonResponse = objectMapper.readTree(response.body()); - - // Access the data from the JSON response - JsonNode resultsNode = jsonResponse.path("results").path("bindings"); - - for (JsonNode result : resultsNode) { - - List options = this.generateOptions(resultsNode, result); - String correctAnswer = this.generateCorrectAnswer(result); - String questionStatement = this.getQuestionSubject(result); - questionGenerator(questionStatement, options, correctAnswer, this.getCategory()); - - } - return questions; - } - - protected abstract List generateOptions(JsonNode results, JsonNode result); - protected abstract String generateCorrectAnswer(JsonNode result); - - protected abstract String getQuestionSubject(JsonNode result); - -} diff --git a/src/main/java/com/uniovi/components/generators/QuestionGenerator.java b/src/main/java/com/uniovi/components/generators/QuestionGenerator.java index fd9356fa..b91061a4 100644 --- a/src/main/java/com/uniovi/components/generators/QuestionGenerator.java +++ b/src/main/java/com/uniovi/components/generators/QuestionGenerator.java @@ -1,5 +1,6 @@ package com.uniovi.components.generators; +import com.fasterxml.jackson.databind.JsonNode; import com.uniovi.entities.Category; import com.uniovi.entities.Question; import org.springframework.stereotype.Component; @@ -9,11 +10,7 @@ @Component public interface QuestionGenerator { + List getQuestions(String language) throws IOException, InterruptedException; - String getQuery(); - List getQuestions() throws InterruptedException, IOException; - - Category getCategory(); - - + List getQuestions(String language, JsonNode question, Category cat) throws IOException, InterruptedException; } diff --git a/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java b/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java new file mode 100644 index 00000000..7decf49b --- /dev/null +++ b/src/main/java/com/uniovi/components/generators/QuestionGeneratorV2.java @@ -0,0 +1,163 @@ +package com.uniovi.components.generators; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.uniovi.dto.AnswerDto; +import com.uniovi.dto.CategoryDto; +import com.uniovi.dto.QuestionDto; +import com.uniovi.entities.Answer; +import com.uniovi.entities.Category; +import com.uniovi.entities.Question; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class QuestionGeneratorV2 implements QuestionGenerator{ + + private JsonNode jsonNode; + private String language_placeholder; + private String question_placeholder; + private String answer_placeholder; + private String language; + + private Random random = new SecureRandom(); + + public QuestionGeneratorV2(JsonNode jsonNode) { + this.jsonNode = jsonNode; + this.language_placeholder = jsonNode.get("language_placeholder").textValue(); + this.question_placeholder = jsonNode.get("question_placeholder").textValue(); + this.answer_placeholder = jsonNode.get("answer_placeholder").textValue(); + } + + @Override + public List getQuestions(String language) throws IOException, InterruptedException { + this.language = language; + List questions = new ArrayList<>(); + JsonNode categories = jsonNode.findValue("categories"); + for(JsonNode category : categories){ + String categoryName = category.get("name").textValue(); + Category cat = new Category(categoryName); + JsonNode questionsNode = category.findValue("questions"); + for(JsonNode question : questionsNode){ + questions.addAll(this.generateQuestion(question, cat)); + } + } + return questions; + } + + @Override + public List getQuestions(String language, JsonNode question, Category cat) throws IOException, InterruptedException { + this.language = language; + return this.generateQuestion(question, cat); + } + + private List generateQuestion(JsonNode question, Category cat) throws IOException, InterruptedException { + // Get the SPARQL query from the JSON + String query = question.get("sparqlQuery").textValue(); + + // Get the question and answer words from the JSON + String questionLabel = question.get("question").textValue(); + String answerLabel= question.get("answer").textValue(); + + // Replace the placeholders in the query with the actual values + query = query.replace(language_placeholder, language). + replace(question_placeholder, questionLabel). + replace(answer_placeholder, answerLabel); + + // Execute the query and get the results + JsonNode results = getQueryResult(query); + List questions = new ArrayList<>(); + + // Prepare the statement base based on the language + String statement = this.prepareStatement(question); + + for (JsonNode result : results) { + // Generate the correct answer + String correctAnswer = result.path(answerLabel).path("value").asText(); + Answer correct = new Answer(correctAnswer, true); + + // Generate the options + List options = this.generateOptions(results, correctAnswer, answerLabel); + options.add(correct); + + if (statement != null) { + // Generate the question statement + String questionStatement = statement.replace(question_placeholder, result.path(questionLabel).path("value").asText()); + + // Generate the question + Question q = new Question(questionStatement, options, correct, cat, language); + + // Add the question to the list + questions.add(q); + } + } + return questions; + } + + private List generateOptions(JsonNode results, String correctAnswer, String answerLabel) { + List options = new ArrayList<>(); + List usedOptions = new ArrayList<>(); + int size = results.size(); + int tries = 0; + + while (options.size() < 3 && tries < 10) { + int random = (int) (this.random.nextFloat() * size); + String option = results.get(random).path(answerLabel).path("value").asText(); + if (!option.equals(correctAnswer) && !usedOptions.contains(option) ) { + usedOptions.add(option); + options.add(new Answer(option, false)); + } + tries++; + } + return options; + } + + /** + * Generates a statement based on the language of the question + * @param question The question node + * @return The statement in the language of the question or null if the language is not found + */ + private String prepareStatement(JsonNode question) { + JsonNode statementNode = question.findValue("statements"); + for (JsonNode statement : statementNode) { + if (statement.get("language").textValue().equals(language)) { + return statement.get("statement").textValue(); + } + } + return null; + } + + private JsonNode getQueryResult(String query) throws IOException, InterruptedException { + + System.out.println("Query: " + query); + HttpClient client = HttpClient.newHttpClient(); + JsonNode resultsNode; + String endpointUrl = "https://query.wikidata.org/sparql?query=" + + URLEncoder.encode(query, StandardCharsets.UTF_8) + + "&format=json"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(endpointUrl)) + .header("Accept", "application/json") + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + // Process the JSON response using Jackson ObjectMapper + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonResponse = objectMapper.readTree(response.body()); + + // Access the data from the JSON response + resultsNode = jsonResponse.path("results").path("bindings"); + return resultsNode; + } +} diff --git a/src/main/java/com/uniovi/components/generators/geography/AbstractGeographyGenerator.java b/src/main/java/com/uniovi/components/generators/geography/AbstractGeographyGenerator.java deleted file mode 100644 index d01919e9..00000000 --- a/src/main/java/com/uniovi/components/generators/geography/AbstractGeographyGenerator.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.uniovi.components.generators.geography; - -import com.uniovi.components.generators.AbstractQuestionGenerator; -import com.uniovi.entities.Category; -import com.uniovi.services.CategoryService; - -public abstract class AbstractGeographyGenerator extends AbstractQuestionGenerator { - - protected AbstractGeographyGenerator(CategoryService categoryService) { - super(categoryService); - } - - @Override - public Category getCategory() { - return categoryService.getCategoryByName("Geography"); - } -} diff --git a/src/main/java/com/uniovi/components/generators/geography/BorderQuestionGenerator.java b/src/main/java/com/uniovi/components/generators/geography/BorderQuestionGenerator.java deleted file mode 100644 index 539b1c13..00000000 --- a/src/main/java/com/uniovi/components/generators/geography/BorderQuestionGenerator.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.uniovi.components.generators.geography; - -import com.fasterxml.jackson.databind.JsonNode; -import com.uniovi.services.CategoryService; - -import java.util.*; - -public class BorderQuestionGenerator extends AbstractGeographyGenerator{ - private static Map STATEMENTS = null; - private Set usedCountries = new HashSet<>(); - - public BorderQuestionGenerator(CategoryService categoryService, String language) { - super(categoryService); - if (STATEMENTS == null) { - STATEMENTS = new HashMap<>(); - STATEMENTS.put("en", "Which countries share a border with "); - STATEMENTS.put("es", "¿Con qué países comparte frontera "); - STATEMENTS.put("fr", "Avec quels pays partage-t-il une frontière "); - } - - this.statement = STATEMENTS.get(language); - this.language = language; - } - - private List getAllBorderingCountries(JsonNode resultsNode, String correctCountry) { - List allBorderingCountries = new ArrayList<>(); - for (JsonNode result : resultsNode) { - String borderingCountry = result.path("borderingCountryLabel").path("value").asText(); - if (!borderingCountry.equals(correctCountry)) { - allBorderingCountries.add(borderingCountry); - } - } - return allBorderingCountries; - } - - private List selectRandomIncorrectBorderingCountries(List allBorderingCountries, String correctCountry, int count) { - List incorrectBorderingCountries = new ArrayList<>(); - while (incorrectBorderingCountries.size() < count && allBorderingCountries.size() > 0) { - int randomIndex = random.nextInt(allBorderingCountries.size()); - String selectedBorderingCountry = allBorderingCountries.remove(randomIndex); - if (!selectedBorderingCountry.equals(correctCountry) && !incorrectBorderingCountries.contains(selectedBorderingCountry)) { - incorrectBorderingCountries.add(selectedBorderingCountry); - } - } - return incorrectBorderingCountries; - } - - @Override - protected List generateOptions(JsonNode results, JsonNode result) { - String borderingCountryLabel = result.path("borderingCountryLabel").path("value").asText(); - return selectRandomIncorrectBorderingCountries( - getAllBorderingCountries(results, borderingCountryLabel), - borderingCountryLabel, 3); - } - - @Override - protected String generateCorrectAnswer(JsonNode result) { - return result.path("borderingCountryLabel").path("value").asText(); - } - - @Override - protected String getQuestionSubject(JsonNode result) { - return this.statement + result.path("countryLabel").path("value").asText() + "?"; - } - - @Override - public String getQuery() { - return "SELECT DISTINCT ?country ?countryLabel ?borderingCountry ?borderingCountryLabel\n" + - "WHERE {" + - " ?country wdt:P31 wd:Q3624078 ." + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240}" + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q28171280}" + - " ?country wdt:P47 ?borderingCountry ." + - " SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]," + language + "\" }" + - "}"; - } -} diff --git a/src/main/java/com/uniovi/components/generators/geography/CapitalQuestionGenerator.java b/src/main/java/com/uniovi/components/generators/geography/CapitalQuestionGenerator.java deleted file mode 100644 index 924ef3cf..00000000 --- a/src/main/java/com/uniovi/components/generators/geography/CapitalQuestionGenerator.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.uniovi.components.generators.geography; - -import com.fasterxml.jackson.databind.JsonNode; -import com.uniovi.services.CategoryService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.*; - -public class CapitalQuestionGenerator extends AbstractGeographyGenerator{ - private static Map STATEMENTS = null; - - public CapitalQuestionGenerator(CategoryService categoryService, String language) { - super(categoryService); - if (STATEMENTS == null) { - STATEMENTS = new HashMap<>(); - STATEMENTS.put("en", "What is the capital of "); - STATEMENTS.put("es", "¿Cuál es la capital de "); - STATEMENTS.put("fr", "Quelle est la capitale de "); - } - - this.statement = STATEMENTS.get(language); - this.language = language; - } - - @Override - public String getQuery() { - return "SELECT DISTINCT ?country ?countryLabel ?capital ?capitalLabel\n" + - "WHERE {" + - " ?country wdt:P31 wd:Q3624078 ." + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240}" + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q28171280}" + - " OPTIONAL { ?country wdt:P36 ?capital } ." + - " SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]," + language + "\" }" + - "}" + - "ORDER BY ?countryLabel"; - } - - private List getAllCapitals(JsonNode resultsNode, String correctCapital) { - // Obtener todas las capitales del JSON (distintas a la capital correcta) - List allCapitals = new ArrayList<>(); - for (JsonNode result : resultsNode) { - String capital = result.path("capitalLabel").path("value").asText(); - if (!capital.equals(correctCapital)) { - allCapitals.add(capital); - } - } - return allCapitals; - } - - private List selectRandomIncorrectCapitals(List allCapitals, String correctCapital, int count) { - List incorrectCapitals = new ArrayList<>(); - while (incorrectCapitals.size() < count && allCapitals.size() > 0) { - int randomIndex = random.nextInt(allCapitals.size()); - String selectedCapital = allCapitals.remove(randomIndex); - if (!selectedCapital.equals(correctCapital) && !incorrectCapitals.contains(selectedCapital)) { - incorrectCapitals.add(selectedCapital); - } - } - return incorrectCapitals; - } - - @Override - protected List generateOptions(JsonNode results, JsonNode result) { - String capitalLabel = result.path("capitalLabel").path("value").asText(); - return selectRandomIncorrectCapitals(getAllCapitals(results, capitalLabel), capitalLabel, 3); - } - - @Override - protected String generateCorrectAnswer(JsonNode result) { - return result.path("capitalLabel").path("value").asText(); - } - - @Override - protected String getQuestionSubject(JsonNode result) { - return this.statement + result.path("countryLabel").path("value").asText() + "?"; - } -} diff --git a/src/main/java/com/uniovi/components/generators/geography/ContinentQuestionGeneration.java b/src/main/java/com/uniovi/components/generators/geography/ContinentQuestionGeneration.java deleted file mode 100644 index df48ec41..00000000 --- a/src/main/java/com/uniovi/components/generators/geography/ContinentQuestionGeneration.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.uniovi.components.generators.geography; - -import com.fasterxml.jackson.databind.JsonNode; -import com.uniovi.services.CategoryService; -import org.springframework.scheduling.annotation.Scheduled; - -import java.util.*; - -public class ContinentQuestionGeneration extends AbstractGeographyGenerator{ - private static Map STATEMENTS = null; - - public ContinentQuestionGeneration(CategoryService categoryService, String language) { - super(categoryService); - - if (STATEMENTS == null) { - STATEMENTS = new HashMap<>(); - STATEMENTS.put("en", "In which continent is "); - STATEMENTS.put("es", "¿En qué continente se encuentra "); - STATEMENTS.put("fr", "Sur quel continent est-il situé "); - } - - this.statement = STATEMENTS.get(language); - this.language = language; - } - - private List getAllContinents(JsonNode resultsNode, String correctContinent) { - // Obtener todas las capitales del JSON (distintas a la capital correcta) - List allContinents = new ArrayList<>(); - for (JsonNode result : resultsNode) { - String continent = result.path("continentLabel").path("value").asText(); - if (!continent.equals(correctContinent)) { - allContinents.add(continent); - } - } - return allContinents; - } - - private List selectRandomIncorrectContinents(List allContinents, String correctContinent, int count) { - List incorrectContinents = new ArrayList<>(); - while (incorrectContinents.size() < count && allContinents.size() > 0) { - int randomIndex = random.nextInt(allContinents.size()); - String selectedCapital = allContinents.remove(randomIndex); - if (!selectedCapital.equals(correctContinent) && !incorrectContinents.contains(selectedCapital)) { - incorrectContinents.add(selectedCapital); - } - } - return incorrectContinents; - } - - @Override - protected List generateOptions(JsonNode results, JsonNode result) { - String continentLabel = result.path("continentLabel").path("value").asText(); - return selectRandomIncorrectContinents(getAllContinents(results, continentLabel), continentLabel, 3); - } - - @Override - protected String generateCorrectAnswer(JsonNode result) { - return result.path("continentLabel").path("value").asText(); - } - - @Override - protected String getQuestionSubject(JsonNode result) { - return this.statement + result.path("countryLabel").path("value").asText() + "?"; - } - - @Override - public String getQuery() { - return "SELECT DISTINCT ?country ?countryLabel ?continent ?continentLabel\n" + - "WHERE {" + - " ?country wdt:P31 wd:Q3624078 " + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240}" + - " FILTER NOT EXISTS {?country wdt:P31 wd:Q28171280}" + - " OPTIONAL { ?country wdt:P30 ?continent } ." + - " SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]," + language + "\" }" + - "}" + - "ORDER BY ?countryLabel"; - } -} diff --git a/src/main/java/com/uniovi/configuration/SecurityConfig.java b/src/main/java/com/uniovi/configuration/SecurityConfig.java index c0af5d20..068364b0 100644 --- a/src/main/java/com/uniovi/configuration/SecurityConfig.java +++ b/src/main/java/com/uniovi/configuration/SecurityConfig.java @@ -48,6 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/**").permitAll() .requestMatchers("/game/**").authenticated() .requestMatchers("/ranking/playerRanking").authenticated() + .requestMatchers("/player/admin/**").hasAuthority("ROLE_ADMIN") .requestMatchers("/**").permitAll() ).formLogin( form -> form diff --git a/src/main/java/com/uniovi/controllers/GameController.java b/src/main/java/com/uniovi/controllers/GameController.java index ad8a7506..f8c0ffa3 100644 --- a/src/main/java/com/uniovi/controllers/GameController.java +++ b/src/main/java/com/uniovi/controllers/GameController.java @@ -79,32 +79,23 @@ public String getCheckResult(@PathVariable Long idQuestion, @PathVariable Long i if(idAnswer == -1 || getRemainingTime(gameSession) <= 0) { - //model.addAttribute("correctAnswer", gameSession.getCurrentQuestion().getCorrectAnswer()); - //model.addAttribute("messageKey", "timeRunOut.result"); - //model.addAttribute("logoImage", "/images/logo_incorrect.svg"); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); gameSession.addQuestion(false, 0); } else if(questionService.checkAnswer(idQuestion, idAnswer)) { - //model.addAttribute("messageKey", "correctAnswer.result"); - //model.addAttribute("logoImage", "/images/logo_correct.svg"); - if (!gameSession.isAnswered(gameSession.getCurrentQuestion())) { gameSession.addQuestion(true, getRemainingTime(gameSession)); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); } } else { - //model.addAttribute("correctAnswer", gameSession.getCurrentQuestion().getCorrectAnswer()); - //model.addAttribute("messageKey", "failedAnswer.result"); - //model.addAttribute("logoImage", "/images/logo_incorrect.svg"); gameSession.addAnsweredQuestion(gameSession.getCurrentQuestion()); gameSession.addQuestion(false, 0); } session.setAttribute("hasJustAnswered", true); gameSession.getNextQuestion(); - //return "game/fragments/questionResult"; + return updateGame(model, session); } diff --git a/src/main/java/com/uniovi/controllers/PlayersController.java b/src/main/java/com/uniovi/controllers/PlayersController.java index d291b6c6..0df52d19 100644 --- a/src/main/java/com/uniovi/controllers/PlayersController.java +++ b/src/main/java/com/uniovi/controllers/PlayersController.java @@ -1,42 +1,56 @@ package com.uniovi.controllers; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.uniovi.configuration.SecurityConfig; +import com.uniovi.dto.RoleDto; +import com.uniovi.entities.Associations; import com.uniovi.entities.GameSession; import com.uniovi.entities.Player; -import com.uniovi.services.GameSessionService; -import com.uniovi.services.PlayerService; +import com.uniovi.entities.Role; +import com.uniovi.services.*; import com.uniovi.validators.SignUpValidator; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.*; import com.uniovi.dto.PlayerDto; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.security.Principal; import java.util.Optional; +import java.util.List; @Controller public class PlayersController { private final PlayerService playerService; + private final RoleService roleService; + private QuestionService questionService; private final SignUpValidator signUpValidator; private final GameSessionService gameSessionService; @Autowired - public PlayersController(PlayerService playerService, SignUpValidator signUpValidator, GameSessionService gameSessionService) { + public PlayersController(PlayerService playerService, SignUpValidator signUpValidator, GameSessionService gameSessionService, + RoleService roleService, QuestionService questionService) { this.playerService = playerService; this.signUpValidator = signUpValidator; this.gameSessionService = gameSessionService; + this.roleService = roleService; } @GetMapping("/signup") @@ -122,4 +136,154 @@ public String showPlayerRanking(Pageable pageable, Model model, Principal princi return "ranking/playerRanking"; } + + // ----- Admin endpoints ----- + + @GetMapping("/player/admin") + public String showAdminPanel(Model model) { + return "player/admin/admin"; + } + + @GetMapping("/player/admin/userManagement") + public String showUserManagementFragment(Model model, Pageable pageable) { + model.addAttribute("endpoint", "/player/admin/userManagement"); + Page users = playerService.getPlayersPage(pageable); + model.addAttribute("page", users); + model.addAttribute("users", users.getContent()); + + return "player/admin/userManagement"; + } + + @GetMapping("/player/admin/deleteUser") + @ResponseBody + public String deleteUser(HttpServletResponse response, @RequestParam String username, Principal principal) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + if (principal.getName().equals(player.getUsername())) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return "You can't delete yourself"; + } + + playerService.deletePlayer(player.getId()); + return "User deleted"; + } + + @GetMapping("/player/admin/changePassword") + @ResponseBody + public String changePassword(HttpServletResponse response, @RequestParam String username, @RequestParam String password) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + playerService.updatePassword(player, password); + return "User password changed"; + } + + @GetMapping("/player/admin/getRoles") + @ResponseBody + public String getRoles(@RequestParam String username) { + List roles = roleService.getAllRoles(); + Player player = playerService.getUserByUsername(username).orElse(null); + + roles.remove(roleService.getRole("ROLE_USER")); + + if (player == null) { + return "{}"; + } + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode rolesJson = mapper.createObjectNode(); + for (Role role : roles) { + boolean hasRole = player.getRoles().contains(role); + rolesJson.put(role.getName(), hasRole); + } + + return rolesJson.toString(); + } + + @GetMapping("/player/admin/changeRoles") + @ResponseBody + public String changeRoles(HttpServletResponse response, @RequestParam String username, @RequestParam String roles) { + Player player = playerService.getUserByUsername(username).orElse(null); + if (player == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return "User not found"; + } + + JsonNode rolesJson; + try { + rolesJson = new ObjectMapper().readTree(roles); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return "Invalid roles"; + } + + rolesJson.fieldNames().forEachRemaining(roleName -> { + boolean hasRole = rolesJson.get(roleName).asBoolean(); + + Role role = roleService.getRole(roleName); + if (role == null && !hasRole) { + return; + } else if (role == null) { + role = roleService.addRole(new RoleDto(roleName)); + } + + if (hasRole) { + Associations.PlayerRole.addRole(player, role); + } else { + Associations.PlayerRole.removeRole(player, role); + } + }); + + playerService.savePlayer(player); + return "User roles changed"; + } + + @GetMapping("/player/admin/questionManagement") + public String showQuestionManagementFragment(Model model) throws IOException { + File jsonFile = new File(QuestionGeneratorService.jsonFilePath); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode json = objectMapper.readTree(jsonFile); + model.addAttribute("jsonContent", json.toString()); + + return "player/admin/questionManagement"; + } + + @GetMapping("/player/admin/deleteAllQuestions") + @ResponseBody + public String deleteAllQuestions() { + questionService.deleteAllQuestions(); + return "Questions deleted"; + } + + @GetMapping("/player/admin/saveQuestions") + @ResponseBody + public String saveQuestions(HttpServletResponse response, @RequestParam String json) throws IOException { + try { + JsonNode node = new ObjectMapper().readTree(json); + DefaultPrettyPrinter printer = new DefaultPrettyPrinter(); + DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(); + printer.indentObjectsWith(indenter); // Indent JSON objects + printer.indentArraysWith(indenter); // Indent JSON arrays + + ObjectMapper mapper = new ObjectMapper(); + mapper.writer(printer).writeValue(new FileOutputStream(QuestionGeneratorService.jsonFilePath), node); + return "Questions saved"; + } + catch (Exception e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return "Invalid JSON"; + } + } + + @GetMapping("/player/admin/monitoring") + public String showMonitoring(Model model) { + return "player/admin/monitoring"; + } } diff --git a/src/main/java/com/uniovi/dto/AnswerDto.java b/src/main/java/com/uniovi/dto/AnswerDto.java index 026eede6..304ee640 100644 --- a/src/main/java/com/uniovi/dto/AnswerDto.java +++ b/src/main/java/com/uniovi/dto/AnswerDto.java @@ -6,6 +6,7 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @ToString public class AnswerDto { diff --git a/src/main/java/com/uniovi/dto/CategoryDto.java b/src/main/java/com/uniovi/dto/CategoryDto.java index fc87530e..57aef22e 100644 --- a/src/main/java/com/uniovi/dto/CategoryDto.java +++ b/src/main/java/com/uniovi/dto/CategoryDto.java @@ -11,6 +11,7 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor public class CategoryDto { @Schema(description = "The name of the category", example = "Geography") diff --git a/src/main/java/com/uniovi/dto/QuestionDto.java b/src/main/java/com/uniovi/dto/QuestionDto.java index d97efb5f..7d4ce0e0 100644 --- a/src/main/java/com/uniovi/dto/QuestionDto.java +++ b/src/main/java/com/uniovi/dto/QuestionDto.java @@ -1,8 +1,10 @@ package com.uniovi.dto; +import com.uniovi.entities.Question; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; +import java.util.ArrayList; import java.util.List; @Getter @@ -26,4 +28,13 @@ public class QuestionDto { @Schema(description = "The language of the question") private String language; + + public QuestionDto (Question question) { + statement = question.getStatement(); + options = question.getOptions().stream().map(a -> new AnswerDto(a.getText(), a.isCorrect())).toList(); + correctAnswer = new AnswerDto(question.getCorrectAnswer().getText(), question.getCorrectAnswer().isCorrect()); + category = new CategoryDto(question.getCategory().getName(), question.getCategory().getDescription(), new ArrayList<>()); + language = question.getLanguage(); + } + } diff --git a/src/main/java/com/uniovi/entities/Associations.java b/src/main/java/com/uniovi/entities/Associations.java index 73218e2f..bb35ebd0 100644 --- a/src/main/java/com/uniovi/entities/Associations.java +++ b/src/main/java/com/uniovi/entities/Associations.java @@ -110,6 +110,9 @@ public static class QuestionAnswers { public static void addAnswer(Question question, List answer) { for (Answer a : answer) { a.setQuestion(question); + if (a.isCorrect()) { + question.setCorrectAnswer(a); + } } question.getOptions().addAll(answer); } @@ -125,6 +128,7 @@ public static void removeAnswer(Question question, List answer) { for (Answer a : answer) { a.setQuestion(null); } + question.setCorrectAnswer(null); } //public static void removeAnswer(Question question, List answer) { // question.getOptions().remove(answer); diff --git a/src/main/java/com/uniovi/entities/Category.java b/src/main/java/com/uniovi/entities/Category.java index 28e798c3..f68ba88f 100644 --- a/src/main/java/com/uniovi/entities/Category.java +++ b/src/main/java/com/uniovi/entities/Category.java @@ -33,6 +33,10 @@ public Category(String name, String description) { this.description = description; } + public Category(String categoryName) { + this.name = categoryName; + } + @Override public String toString() { return name; diff --git a/src/main/java/com/uniovi/entities/Role.java b/src/main/java/com/uniovi/entities/Role.java index c767c425..0d4d45c0 100644 --- a/src/main/java/com/uniovi/entities/Role.java +++ b/src/main/java/com/uniovi/entities/Role.java @@ -22,4 +22,9 @@ public class Role { public Role(String name) { this.name = name; } + + @Override + public String toString() { + return name; + } } diff --git a/src/main/java/com/uniovi/repositories/PlayerRepository.java b/src/main/java/com/uniovi/repositories/PlayerRepository.java index 77c2d9f8..cf4f0a98 100644 --- a/src/main/java/com/uniovi/repositories/PlayerRepository.java +++ b/src/main/java/com/uniovi/repositories/PlayerRepository.java @@ -1,9 +1,13 @@ package com.uniovi.repositories; import com.uniovi.entities.Player; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; public interface PlayerRepository extends CrudRepository { Player findByEmail(String email); Player findByUsername(String nickname); + + Page findAll(Pageable pageable); } diff --git a/src/main/java/com/uniovi/services/InsertSampleDataService.java b/src/main/java/com/uniovi/services/InsertSampleDataService.java index f51a0ed8..0b2b1b83 100644 --- a/src/main/java/com/uniovi/services/InsertSampleDataService.java +++ b/src/main/java/com/uniovi/services/InsertSampleDataService.java @@ -1,30 +1,20 @@ package com.uniovi.services; -import com.uniovi.components.MultipleQuestionGenerator; -import com.uniovi.components.generators.QuestionGenerator; -import com.uniovi.components.generators.geography.BorderQuestionGenerator; -import com.uniovi.components.generators.geography.CapitalQuestionGenerator; -import com.uniovi.components.generators.geography.ContinentQuestionGeneration; import com.uniovi.dto.PlayerDto; -import com.uniovi.entities.Associations; -import com.uniovi.entities.GameSession; -import com.uniovi.entities.Player; import com.uniovi.entities.Question; import com.uniovi.repositories.GameSessionRepository; import com.uniovi.repositories.QuestionRepository; -import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import java.io.IOException; -import java.time.LocalDateTime; -import java.util.*; +import java.util.Arrays; +import java.util.List; @Service public class InsertSampleDataService { @@ -51,58 +41,17 @@ public InsertSampleDataService(PlayerService playerService, QuestionService ques @Transactional @EventListener(ApplicationReadyEvent.class) // Uncomment this line to insert sample data on startup public void insertSampleQuestions() throws InterruptedException, IOException { - if (!playerService.getUserByEmail("test@test.com").isPresent()) { + if (playerService.getUserByEmail("test@test.com").isEmpty()) { PlayerDto player = new PlayerDto(); player.setEmail("test@test.com"); player.setUsername("test"); player.setPassword("test"); - player.setRoles(new String[]{"ROLE_USER"}); + if (Arrays.asList(environment.getActiveProfiles()).contains("test")) + player.setRoles(new String[]{"ROLE_USER", "ROLE_ADMIN"}); + else + player.setRoles(new String[]{"ROLE_USER"}); playerService.generateApiKey(playerService.addNewPlayer(player)); } - - if (Arrays.stream(environment.getActiveProfiles()).anyMatch(env -> (env.equalsIgnoreCase("test")))) { - log.info("Test profile active, skipping sample data insertion"); - return; - } - - generateSampleData(); } - @Transactional - public void generateTestQuestions() { - questionRepository.deleteAll(); - questionService.testQuestions(4); - } - - @Transactional - public void generateSampleData() throws InterruptedException, IOException { - - questionRepository.deleteAll(); - - MultipleQuestionGenerator allQuestionGenerator = new MultipleQuestionGenerator( - new ContinentQuestionGeneration(categoryService, Question.ENGLISH), - new CapitalQuestionGenerator(categoryService, Question.ENGLISH), - new BorderQuestionGenerator(categoryService, Question.ENGLISH) - ); - List questionsEn = allQuestionGenerator.getQuestions(); - questionsEn.forEach(questionService::addNewQuestion); - - allQuestionGenerator = new MultipleQuestionGenerator( - new ContinentQuestionGeneration(categoryService, Question.SPANISH), - new CapitalQuestionGenerator(categoryService, Question.SPANISH), - new BorderQuestionGenerator(categoryService, Question.SPANISH) - ); - List questionsEs = allQuestionGenerator.getQuestions(); - questionsEs.forEach(questionService::addNewQuestion); - - allQuestionGenerator = new MultipleQuestionGenerator( - new ContinentQuestionGeneration(categoryService, Question.FRENCH), - new CapitalQuestionGenerator(categoryService, Question.FRENCH), - new BorderQuestionGenerator(categoryService, Question.FRENCH) - ); - List questionsFr = allQuestionGenerator.getQuestions(); - questionsFr.forEach(questionService::addNewQuestion); - - log.info("Sample questions inserted"); - } -} +} \ No newline at end of file diff --git a/src/main/java/com/uniovi/services/PlayerService.java b/src/main/java/com/uniovi/services/PlayerService.java index f59669e3..fc9da433 100644 --- a/src/main/java/com/uniovi/services/PlayerService.java +++ b/src/main/java/com/uniovi/services/PlayerService.java @@ -4,6 +4,8 @@ import com.uniovi.entities.Player; import com.uniovi.repositories.PlayerRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @@ -72,4 +74,24 @@ public interface PlayerService { * @param id The id of the player to delete */ void deletePlayer(Long id); + + /** + * Get a page with all the players in the database + * @param pageable The page information + * @return A page with all the players + */ + Page getPlayersPage(Pageable pageable); + + /** + * Update the password of a player + * @param player The player to update the password + * @param password The new password + */ + void updatePassword(Player player, String password); + + /** + * Save a player in the database + * @param player The player to save + */ + void savePlayer(Player player); } diff --git a/src/main/java/com/uniovi/services/QuestionGeneratorService.java b/src/main/java/com/uniovi/services/QuestionGeneratorService.java new file mode 100644 index 00000000..e92a4ce0 --- /dev/null +++ b/src/main/java/com/uniovi/services/QuestionGeneratorService.java @@ -0,0 +1,141 @@ +package com.uniovi.services; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.uniovi.components.generators.QuestionGenerator; +import com.uniovi.components.generators.QuestionGeneratorV2; +import com.uniovi.dto.QuestionDto; +import com.uniovi.entities.Answer; +import com.uniovi.entities.Category; +import com.uniovi.entities.Question; +import com.uniovi.services.impl.QuestionServiceImpl; +import jakarta.transaction.Transactional; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +@Service +public class QuestionGeneratorService { + + private final QuestionService questionService; + + public static final String jsonFilePath = "src/main/resources/static/JSON/QuestionTemplates.json"; + + private Deque types = new ArrayDeque<>(); + + private JsonNode json; + + @Autowired + private Environment environment; + + private Logger log = LoggerFactory.getLogger(InsertSampleDataService.class); + + private boolean started; + + public QuestionGeneratorService(QuestionService questionService) { + this.questionService = questionService; + ((QuestionServiceImpl)questionService).setQuestionGeneratorService(this); + parseQuestionTypes(); + this.started = true; + } + + private void parseQuestionTypes() { + try { + File jsonFile = new File(jsonFilePath); + ObjectMapper objectMapper = new ObjectMapper(); + json = objectMapper.readTree(jsonFile); + JsonNode categories = json.findValue("categories"); + for (JsonNode category : categories) { + String categoryName = category.get("name").textValue(); + Category cat = new Category(categoryName); + JsonNode questionsNode = category.findValue("questions"); + for (JsonNode question : questionsNode) { + types.push(new QuestionType(question, cat)); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Scheduled(fixedRate = 86400000, initialDelay = 86400000) + public void generateAllQuestions(){ + } + + @Scheduled(fixedRate = 150000) + @Transactional + public void generateQuestions() throws IOException, InterruptedException { + if (types.isEmpty()) { + return; + } + + if (started){ + started = false; + questionService.deleteAllQuestions(); + } + + if (Arrays.stream(environment.getActiveProfiles()).anyMatch(env -> (env.equalsIgnoreCase("test")))) { + log.info("Test profile active, skipping sample data insertion"); + return; + } + + QuestionGenerator qgen = new QuestionGeneratorV2(json); + QuestionType type = types.pop(); + List questions; + + List qsp = qgen.getQuestions(Question.SPANISH, type.getQuestion(), type.getCategory()); + questions = qsp.stream().map(QuestionDto::new).toList(); + questions.forEach(questionService::addNewQuestion); + + List qen = qgen.getQuestions(Question.ENGLISH, type.getQuestion(), type.getCategory()); + questions = qen.stream().map(QuestionDto::new).toList(); + questions.forEach(questionService::addNewQuestion); + + List qfr = qgen.getQuestions(Question.FRENCH, type.getQuestion(), type.getCategory()); + questions = qfr.stream().map(QuestionDto::new).toList(); + questions.forEach(questionService::addNewQuestion); + } + + @Transactional + public void generateTestQuestions() throws IOException, InterruptedException { + QuestionGenerator qgen = new QuestionGeneratorV2(json); + QuestionType type = types.pop(); + List questions; + + List qsp = qgen.getQuestions(Question.SPANISH, type.getQuestion(), type.getCategory()); + questions = qsp.stream().map(QuestionDto::new).toList(); + questions.forEach(questionService::addNewQuestion); + } + + @Transactional + public void generateTestQuestions(String cat) throws IOException { + Answer a1 = new Answer("1", true); + List answers = List.of(a1, new Answer("2", false), new Answer("3", false), new Answer("4", false)); + Question q = new Question("Statement", answers, a1, new Category(cat), "es"); + questionService.addNewQuestion(new QuestionDto(q)); + } + + public void resetGeneration() { + types.clear(); + parseQuestionTypes(); + } + + @Getter + @AllArgsConstructor + private static class QuestionType { + private final JsonNode question; + private final Category category; + } +} diff --git a/src/main/java/com/uniovi/services/QuestionService.java b/src/main/java/com/uniovi/services/QuestionService.java index 7eb7d422..34801782 100644 --- a/src/main/java/com/uniovi/services/QuestionService.java +++ b/src/main/java/com/uniovi/services/QuestionService.java @@ -3,11 +3,10 @@ import com.uniovi.dto.QuestionDto; import com.uniovi.entities.Category; import com.uniovi.entities.Question; -import jakarta.transaction.Transactional; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; @@ -99,10 +98,7 @@ public interface QuestionService { void deleteQuestion(Long id); /** - * Get some test questions - * - * @param num The number of questions to get - * @return The questions selected + * Delete all the questions */ - List testQuestions(int num); + void deleteAllQuestions(); } diff --git a/src/main/java/com/uniovi/services/RoleService.java b/src/main/java/com/uniovi/services/RoleService.java index e620105f..fd196940 100644 --- a/src/main/java/com/uniovi/services/RoleService.java +++ b/src/main/java/com/uniovi/services/RoleService.java @@ -20,4 +20,10 @@ public interface RoleService { * @return The role with the given name */ Role getRole(String name); + + /** + * Get all the roles in the database + * @return A list with all the roles + */ + List getAllRoles(); } diff --git a/src/main/java/com/uniovi/services/impl/CategoryServiceImpl.java b/src/main/java/com/uniovi/services/impl/CategoryServiceImpl.java index 636bffe5..cf3c3545 100644 --- a/src/main/java/com/uniovi/services/impl/CategoryServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/CategoryServiceImpl.java @@ -52,4 +52,4 @@ public void init() { } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java b/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java index f4fa462d..9d0fb497 100644 --- a/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/PlayerServiceImpl.java @@ -10,6 +10,8 @@ import com.uniovi.services.PlayerService; import com.uniovi.services.RoleService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.uniovi.entities.Role; @@ -136,4 +138,20 @@ public void updatePlayer(Long id, PlayerDto playerDto) { public void deletePlayer(Long id) { playerRepository.deleteById(id); } + + @Override + public Page getPlayersPage(Pageable pageable) { + return playerRepository.findAll(pageable); + } + + @Override + public void updatePassword(Player player, String password) { + player.setPassword(passwordEncoder.encode(password)); + playerRepository.save(player); + } + + @Override + public void savePlayer(Player player) { + playerRepository.save(player); + } } diff --git a/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java b/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java index f0ec9f7e..163fc20a 100644 --- a/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/QuestionServiceImpl.java @@ -9,23 +9,22 @@ import com.uniovi.repositories.QuestionRepository; import com.uniovi.services.AnswerService; import com.uniovi.services.CategoryService; +import com.uniovi.services.QuestionGeneratorService; import com.uniovi.services.QuestionService; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; +import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.querydsl.QPageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.security.SecureRandom; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Random; -import org.springframework.data.domain.Pageable; @Service public class QuestionServiceImpl implements QuestionService { @@ -35,6 +34,9 @@ public class QuestionServiceImpl implements QuestionService { private final AnswerRepository answerRepository; private final EntityManager entityManager; + @Setter + private QuestionGeneratorService questionGeneratorService; + private final Random random = new SecureRandom(); public QuestionServiceImpl(QuestionRepository questionRepository, CategoryService categoryService, @@ -171,29 +173,8 @@ public void deleteQuestion(Long id) { } @Override - public List testQuestions(int num) { - List res = new ArrayList<>(); - Category c = new Category("Test category", "Test category"); - categoryService.addNewCategory(c); - for (int i = 0; i < num; i++) { - Question q = new Question(); - q.setStatement("Test question " + i); - q.setLanguage(LocaleContextHolder.getLocale().getLanguage()); - Associations.QuestionsCategory.addCategory(q, c); - List answers = new ArrayList<>(); - for (int j = 0; j < 4; j++) { - Answer a = new Answer(); - a.setText("Test answer " + j); - a.setCorrect(j == 0); - if(j==0) q.setCorrectAnswer(a); - answerService.addNewAnswer(a); - answers.add(a); - } - Associations.QuestionAnswers.addAnswer(q, answers); - addNewQuestion(q); - res.add(q); - } - return res; + public void deleteAllQuestions() { + questionGeneratorService.resetGeneration(); + questionRepository.deleteAll(); } - } diff --git a/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java b/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java index b10bbcc1..bec8464c 100644 --- a/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java +++ b/src/main/java/com/uniovi/services/impl/RoleServiceImpl.java @@ -35,4 +35,11 @@ public Role addRole(RoleDto role) { public Role getRole(String name) { return roleRepository.findById(name).orElse(null); } + + @Override + public List getAllRoles() { + List roles = new ArrayList<>(); + roleRepository.findAll().forEach(roles::add); + return roles; + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 660a6bfa..563c3468 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ # Port 3000 for testing, local deployment -server.port=3100 +server.port=3000 server.address=0.0.0.0 # HSQL db diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2b91abb1..e2304c4b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -13,6 +13,7 @@ navbar.toEnglish=Inglés navbar.toSpanish=Español navbar.toFrench=Francés navbar.currentLanguage=Español +navbar.section.admin=Panel de administración # Buttons for non-authenticated users navbar.signup=Regístrate @@ -22,6 +23,7 @@ navbar.login=Inicia sesión navbar.profile=Perfil navbar.logout=Cerrar sesión navbar.profile.apikey=Clave de la API +navbar.admin.zone=Zona de administración # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Grupo 04 B @@ -114,4 +116,29 @@ game.continue=Siguiente pregunta answer.correct=La respuesta correcta era: game.points=Puntos: game.currentQuestion=Pregunta: -game.finish=El juego ha terminado. Tu puntuación ha sido: \ No newline at end of file +game.finish=El juego ha terminado. Tu puntuación ha sido: + + +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Administración de usuarios +admin.section.question.management=Administración de preguntas +role.label=Roles +user.details=Acciones +admin.user.delete=Eliminar usuario +admin.user.delete.title=Confirmar borrado de usuario +admin.user.delete.message=¿Está seguro de que desea eliminar este usuario?\nTodos los datos asociados con esta cuenta se eliminarán.\nLa acción es irreversible. +admin.changepassword=Cambiar contraseña +admin.changeroles=Modificar roles +modal.password.title=Confirmar cambio de contraseña para +admin.password.change.input=Nueva contraseña +admin.roles.change=Confirmar cambio de roles para +modal.new.role=Nuevo rol +modal.close=Cerrar +modal.confirm=Confirmar +admin.questions.delete.title=Borrar todas las preguntas +admin.questions.delete=¿Está seguro de que desea eliminar todas las preguntas?\nEsta acción es irreversible.\nSe generaran de nuevo según pase el tiempo. +admin.monitoring=Monitorización de la aplicación + +# -------------------Statements for the page management--------------------- +page.first=Primera +page.last=Última \ No newline at end of file diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 97f0f133..331afdb3 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -13,6 +13,7 @@ navbar.toEnglish=English navbar.toSpanish=Spanish navbar.toFrench=French navbar.currentLanguage=English +navbar.section.admin=Administration Section # Buttons for non-authenticated users navbar.signup=Sign Up @@ -22,6 +23,7 @@ navbar.login=Log In navbar.profile=Profile navbar.logout=Log Out navbar.profile.apikey=API Key +navbar.admin.zone=Admin Zone # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Group 04 B @@ -117,4 +119,28 @@ game.points=Points: game.currentQuestion=Question: game.finish=The game has finished. Your score is: +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Users management +admin.section.question.management=Questions management +role.label=Roles +user.details=Details +admin.user.delete=Delete user +admin.user.delete.title=Confirm deleting user +admin.user.delete.message=Are you sure you want to delete this user?\nAll data associated with this account will be erased\nThis action cannot be undone +admin.changepassword=Change password +admin.changeroles=Modify roles +modal.password.title=Confirm password change for +admin.password.change.input=New password +admin.roles.change=Confirm role change for +modal.new.role=New role +modal.close=Close +modal.confirm=Save changes +admin.questions.delete.title=Delete all questions +admin.questions.delete=Are you sure you want to delete all questions?\nThis action cannot be undone.\nQuestions will generate again as time passes. +admin.monitoring=Monitoring + +# -------------------Statements for the page management--------------------- +page.first=First +page.last=Last + diff --git a/src/main/resources/messages_es.properties b/src/main/resources/messages_es.properties index 174dad48..f4e31807 100644 --- a/src/main/resources/messages_es.properties +++ b/src/main/resources/messages_es.properties @@ -13,6 +13,7 @@ navbar.toEnglish=Inglés navbar.toSpanish=Español navbar.toFrench=Francés navbar.currentLanguage=Español +navbar.section.admin=Panel de administración # Buttons for non-authenticated users navbar.signup=Regístrate @@ -22,7 +23,7 @@ navbar.profile.apikey=Clave de la API # Buttons for authenticated users navbar.profile=Perfil navbar.logout=Cerrar sesión - +navbar.admin.zone=Zona de administración # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Grupo 04 B @@ -116,4 +117,25 @@ game.continue=Siguiente pregunta answer.correct=La respuesta correcta era: game.points=Puntos: game.currentQuestion=Pregunta: -game.finish=El juego ha terminado. Tu puntuación ha sido: \ No newline at end of file +game.finish=El juego ha terminado. Tu puntuación ha sido: + +# -------------------Statements for the admin section--------------------- +admin.section.user.management=Administración de usuarios +admin.section.question.management=Administración de preguntas +role.label=Roles +user.details=Acciones +admin.user.delete=Eliminar usuario +admin.user.delete.title=Confirmar borrado de usuario +admin.user.delete.message=¿Está seguro de que desea eliminar este usuario?\nTodos los datos asociados con esta cuenta se eliminarán.\nLa acción es irreversible. +admin.changepassword=Cambiar contraseña +admin.changeroles=Modificar roles +modal.password.title=Confirmar cambio de contraseña para +admin.password.change.input=Nueva contraseña +admin.roles.change=Confirmar cambio de roles para +modal.new.role=Nuevo rol +modal.close=Cerrar +modal.confirm=Confirmar + +# -------------------Statements for the page management--------------------- +page.first=Primera +page.last=Última \ No newline at end of file diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties index 1f5d2b22..7f233db5 100644 --- a/src/main/resources/messages_fr.properties +++ b/src/main/resources/messages_fr.properties @@ -12,6 +12,7 @@ navbar.toEnglish=Anglais navbar.toSpanish=Espagnol navbar.toFrench=Français navbar.currentLanguage=Français +navbar.section.admin=Espace administrateur navbar.signup=S'inscrire navbar.login=Se connecter @@ -20,7 +21,7 @@ navbar.profile.apikey=Clé d'API # Buttons for authenticated users navbar.profile=Profil navbar.logout=Se déconnecter - +navbar.admin.zone=Espace administrateur # -------------------Statements for the footer.html file--------------------- footer.copyright=© ASW - Groupe 04 B @@ -112,3 +113,27 @@ game.points=Points: game.currentQuestion=Question: game.finish=Le jeu est terminé. Votre score est : +# -------------------Déclarations pour la section administrateur--------------------- +admin.section.user.management=Gestion des utilisateurs +admin.section.question.management=Gestion des questions +role.label=Rôles +user.details=Actions +admin.user.delete=Supprimer l'utilisateur +admin.user.delete.title=Confirmer la suppression de l'utilisateur +admin.user.delete.message=Êtes-vous sûr de vouloir supprimer cet utilisateur ?\nToutes les données associées à ce compte seront supprimées.\nL'action est irréversible. +admin.changepassword=Changer le mot de passe +admin.changeroles=Modifier les rôles +modal.password.title=Confirmer le changement de mot de passe pour +admin.password.change.input=Nouveau mot de passe +admin.roles.change=Confirmer le changement de rôles pour +modal.new.role=Nouveau rôle +modal.close=Fermer +modal.confirm=Confirmer +admin.questions.delete.title=Confirmer la suppression de toutes les questions +admin.questions.delete=Vous êtes sur le point de supprimer toutes les questions. Êtes-vous sûr de vouloir continuer ? +admin.monitoring=Surveillance + +# -------------------Déclarations pour la gestion de la page--------------------- +page.first=Première +page.last=Dernière + diff --git a/src/main/resources/static/JSON/QuestionTemplates.json b/src/main/resources/static/JSON/QuestionTemplates.json new file mode 100644 index 00000000..a910f19b --- /dev/null +++ b/src/main/resources/static/JSON/QuestionTemplates.json @@ -0,0 +1,97 @@ +{ + "language_placeholder" : "[LANGUAGE]", + "question_placeholder" : "[QUESTION]", + "answer_placeholder" : "[ANSWER]", + "categories" : [ + { + "name" : "Geography", + "questions" : [ + { + "type" : "capital", + "statements" : [ + { + "language" : "es", + "statement" : "¿Cuál es la capital de [QUESTION]?" + }, + { + "language" : "en", + "statement" : "What is the capital of [QUESTION]?" + }, + { + "language" : "fr", + "statement" : "Quelle est la capitale de [QUESTION]?" + } + ], + "question" : "countryLabel", + "answer" : "capitalLabel", + "sparqlQuery" : "select distinct ?country ?[QUESTION] ?capital ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?capital wdt:P31 wd:Q5119 .\n ?country wdt:P36 ?capital .\n ?country rdfs:label ?[QUESTION] .\n ?capital rdfs:label ?[ANSWER] .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" + }, + { + "type" : "currency", + "statements" : [ + { + "language" : "es", + "statement" : "¿Cuál es la moneda de [QUESTION]?" + }, + { + "language" : "en", + "statement" : "What is the currency of [QUESTION]?" + }, + { + "language" : "fr", + "statement" : "Quelle est la monnaie de [QUESTION]?" + } + ], + "question" : "countryLabel", + "answer" : "currencyLabel", + "sparqlQuery" : "select distinct ?country ?[QUESTION] ?currency ?[ANSWER] where {\n ?country wdt:P31 wd:Q6256 .\n ?currency wdt:P31 wd:Q8142 .\n ?country wdt:P38 ?currency .\n ?country rdfs:label ?[QUESTION] .\n ?currency rdfs:label ?[ANSWER] .\n FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\" && LANG(?[ANSWER])=\"[LANGUAGE]\")\n }" + } + ] + }, + { + "name" : "Science", + "questions" : [ + { + "type" : "element", + "statements" : [ + { + "language" : "es", + "statement" : "¿Cuál es el símbolo químico del [QUESTION]?" + }, + { + "language" : "en", + "statement" : "What is the chemical symbol of [QUESTION]?" + }, + { + "language" : "fr", + "statement" : "Quel est le symbole chimique du [QUESTION]?" + } + ], + "question" : "elementLabel", + "answer" : "symbol", + "sparqlQuery" : "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P246 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" + }, + { + "type" : "atomic_number", + "statements" : [ + { + "language" : "es", + "statement" : "¿Cuál es el número atómico del [QUESTION]?" + }, + { + "language" : "en", + "statement" : "What is the atomic number of [QUESTION]?" + }, + { + "language" : "fr", + "statement" : "Quel est le numéro atomique du [QUESTION]?" + } + ], + "question" : "elementLabel", + "answer" : "atomicNumber", + "sparqlQuery" : "select distinct ?element ?[QUESTION] ?[ANSWER] where {\n ?element wdt:P31 wd:Q11344 .\n ?element wdt:P1086 ?[ANSWER] .\n ?element rdfs:label ?[QUESTION] .\n FILTER NOT EXISTS {?element wdt:P31 wd:Q1299291} .\n FILTER(LANG(?[QUESTION])=\"[LANGUAGE]\")\n }" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/static/css/admin.css b/src/main/resources/static/css/admin.css new file mode 100644 index 00000000..db24eb8e --- /dev/null +++ b/src/main/resources/static/css/admin.css @@ -0,0 +1,26 @@ +.nav .nav-link { + color: white; + border-color: white; +} + +.nav .nav-item { + margin: 0 5px; + flex: 1; +} + +.nav .nav-link.active { + color: black !important; +} + +.nav-tabs { + border-bottom: 0px; +} + +.separator { + border-bottom: 1px solid white; + margin: 10px 0; +} + +.text-danger-light { + color: #ff5e5e; +} \ No newline at end of file diff --git a/src/main/resources/static/css/custom.css b/src/main/resources/static/css/custom.css index 1499a864..18ebb97e 100644 --- a/src/main/resources/static/css/custom.css +++ b/src/main/resources/static/css/custom.css @@ -8,14 +8,6 @@ body { margin-bottom: 60px; color: #fff; } -footer { - position: absolute; - bottom: 0; - width: 100%; - height: 60px; - text-align:center; - line-height:60px -} .bg-primary { background-color: rgb(1, 85, 20) !important; @@ -66,4 +58,64 @@ footer { .prueba { font-weight: bold; +} + +.button-container { + display: flex; + justify-content: space-between; +} + +.button-container a { + flex: 1; + margin: 0 5px; +} + +.modal { + color: black; +} + +.modal .btn.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-close { + box-sizing: content-box; + width: 1em; + height: 1em; + padding: .25em .25em; + color: #000; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + border: 0; + border-radius: .25rem; + opacity: .5 +} + +.btn-close:hover { + color: #000; + text-decoration: none; + opacity: .75 +} + +.btn-close:focus { + outline: 0; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); + opacity: 1 +} + +.btn-close.disabled, +.btn-close:disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + opacity: .25 +} + +.modal-body { + white-space: pre; +} + +.container, .container-fluid { + flex: 1 1 auto !important; } \ No newline at end of file diff --git a/src/main/resources/static/css/footer.css b/src/main/resources/static/css/footer.css index 77a89c6c..e9e67a72 100644 --- a/src/main/resources/static/css/footer.css +++ b/src/main/resources/static/css/footer.css @@ -1,14 +1,9 @@ /* Estilo del footer */ .footer { - position: absolute; bottom: 0; - min-height: 10%; width: 100%; - padding: 0; /* Eliminar relleno */ - text-align: center; /* Alineación del texto */ - display: flex; /* Usar flexbox para centrar verticalmente */ - /*align-items: center; /* Centrar verticalmente */ - margin-top: 5%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ background-color: transparent !important; /* Hace que el footer sea transparente */ } diff --git a/src/main/resources/static/css/nav.css b/src/main/resources/static/css/nav.css index 79c8ca3c..102f4e28 100644 --- a/src/main/resources/static/css/nav.css +++ b/src/main/resources/static/css/nav.css @@ -18,7 +18,7 @@ /* Estilo para los desplegables */ .dropdown-menu { color: #fff; - background-color: black; + background-color: rgb(19, 19, 19); border: 2px solid #fff; } diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 84d2796c..a28db0d5 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -6,4 +6,12 @@ #apiKeyDiv form { margin: 5% 20% 0 20%; +} + +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px; /* Margin bottom by footer height */ } \ No newline at end of file diff --git a/src/main/resources/static/script/adminModals.js b/src/main/resources/static/script/adminModals.js new file mode 100644 index 00000000..aa468c2e --- /dev/null +++ b/src/main/resources/static/script/adminModals.js @@ -0,0 +1,112 @@ +function setupUserEvents() { + $("#deleteUserAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#deleteModalConfirm").attr('data-bs-username', username); + }); + + $("#deleteModalConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + $.ajax({ + url: "/player/admin/deleteUser", + type: "GET", + data: { + username: username + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#deleteUserAdminModal").modal('hide'); + } + }); + }); + + $("#changePasswordAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#changePasswordConfirm").attr('data-bs-username', username); + }); + + $("#changePasswordConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + let newPass = $("#changePasswordInput").val(); + $.ajax({ + url: "/player/admin/changePassword", + type: "GET", + data: { + username: username, + password: newPass + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#changePasswordAdminModal").modal('hide'); + } + }); + }); + + $("#changeRolesAdminModal").on('show.bs.modal', function (event) { + let button = $(event.relatedTarget); + let username = button.attr('data-bs-username'); + $(".modal-title b").text('"' + username + '"'); + $("#changeRolesConfirm").attr('data-bs-username', username); + $.ajax({ + url: "/player/admin/getRoles", + type: "GET", + data: { + username: username + }, + success: function (data) { + let roles = JSON.parse(data); + let rolesContainer = $("#rolesContainer"); + rolesContainer.empty(); + let i = 0; + for (const role in roles) { + let hasRole = roles[role]; + let div = $('
'); + let input = $(''); + let label = $(''); + div.append(input); + div.append(label); + rolesContainer.append(div); + i = i + 1; + } + }, + error: function (data) { + alert("Error: " + data); + } + }); + }); + + $("#changeRolesConfirm").click(function () { + let username = $(this).attr('data-bs-username'); + + let allRoles = $("#rolesContainer input"); + let roles = {}; + allRoles.each(function() { + roles[$(this).val()] = $(this).is(':checked'); + }); + let newRoleInput = $("#newRole").val(); + if (newRoleInput !== "") { + roles[newRoleInput] = true; + } + + let rolesString = JSON.stringify(roles); + + $.ajax({ + url: "/player/admin/changeRoles", + type: "GET", + data: { + username: username, + roles: rolesString + }, + success: function (data) { + $('#tab-content').load('/player/admin/userManagement'); + $("#changeRolesAdminModal").modal('hide'); + }, + error: function (data) { + alert("Error: " + data); + } + }); + }); +} \ No newline at end of file diff --git a/src/main/resources/static/script/questionManagement.js b/src/main/resources/static/script/questionManagement.js new file mode 100644 index 00000000..1d3280dd --- /dev/null +++ b/src/main/resources/static/script/questionManagement.js @@ -0,0 +1,38 @@ +function setupQuestionManagement() { + var editor; + $("#deleteQuestionsConfirm").on("click", function () { + $.ajax({ + url: "/player/admin/deleteAllQuestions", + type: "GET", + success: function () { + $('#tab-content').load('/player/admin/questionManagement'); + } + }); + }); + + $("#saveButton").on("click", function () { + $.ajax({ + url: "/player/admin/saveQuestions", + type: "GET", + data: { + json: JSON.stringify(editor.get()) + }, + contentType: "application/json" + }); + }); + + $.ajax({ + url: '/JSON/QuestionTemplates.json', + type: 'GET', + success: function (data) { + let json = data; + const element = document.getElementById('jsonEditorElement'); + const options = {} + editor = new JSONEditor(element, options) + editor.set(json) + }, + error: function (error) { + console.log(error); + } + }); +} \ No newline at end of file diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 66d23f12..e4ac7290 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -1,7 +1,7 @@ - + diff --git a/src/main/resources/templates/fragments/adminModals.html b/src/main/resources/templates/fragments/adminModals.html new file mode 100644 index 00000000..d5c19e74 --- /dev/null +++ b/src/main/resources/templates/fragments/adminModals.html @@ -0,0 +1,76 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html index 0579216d..bebd50d9 100644 --- a/src/main/resources/templates/fragments/footer.html +++ b/src/main/resources/templates/fragments/footer.html @@ -1,5 +1,5 @@ -