diff --git a/CHANGELOG.md b/CHANGELOG.md index 81487e3c93f238c18a141aa9fa5020795a89893f..29940aa9ac2d2967010188797e8c681b1ca42634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] ### Added - ### Changed - ### Fixed +## [0.0.2] + +### Added + +- Allow users to see a list of featured puzzles from other users on their profile. (@kradziwilowicz) +### Changed + +- Extend the Puzzle overviews to show creator and number of attempts (@cbaraya) +- Add links to /attempts page to user profiles for admins (@MrHug) +- Make link to /attempts page for admins (@MrHug) + +### Fixed diff --git a/build.gradle.kts b/build.gradle.kts index 295f71d98a2e4564320b9993b25b4b241521dd43..67797ff801ec127ead6ab5b74b811ba76b89c9dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,20 +2,20 @@ import com.diffplug.gradle.spotless.SpotlessExtension import org.springframework.boot.gradle.tasks.bundling.BootJar import nl.javadude.gradle.plugins.license.DownloadLicensesExtension import nl.javadude.gradle.plugins.license.LicenseExtension +import java.nio.file.Files group = "nl.tudelft.pupple" -version = "0.0.1" +version = "0.0.2" val javaVersion = JavaVersion.VERSION_17 -val labradoorVersion = "1.4.1-SNAPSHOT" -val libradorVersion = "1.0.3-SNAPSHOT7" -val chihuahUIVersion = "1.0.2" +val labradoorVersion = "1.4.1" +val libradorVersion = "1.3.0" +val chihuahUIVersion = "1.1.0" +val guavaVersion = "32.1.3-jre" val genSourceDir = file("$buildDir/pupple/src/main/java") -ext["log4j2.version"] = "2.16.0" - repositories { mavenLocal() mavenCentral() @@ -46,11 +46,11 @@ plugins { jacoco `maven-publish` - id("org.springframework.boot").version("2.5.6") - id("io.spring.dependency-management").version("1.0.11.RELEASE") - id("com.github.ben-manes.versions").version("0.39.0") + id("org.springframework.boot").version("2.7.18") + id("io.spring.dependency-management").version("1.1.4") + id("com.github.ben-manes.versions").version("0.50.0") - id("com.diffplug.spotless").version("6.0.1") + id("com.diffplug.spotless").version("6.23.3") id("com.github.hierynomus.license").version("0.16.1") } @@ -140,17 +140,43 @@ val jacocoTestReport by tasks.getting(JacocoReport::class) { xml.required.set(true) csv.required.set(true) - //html.destination = file("$buildDir/reports/coverage") + html.outputLocation.set(file("$buildDir/reports/coverage")) + } +} + +tasks.register("ensureDirectory") { + // Store target directory into a variable to avoid project reference in the configuration cache + val directory = file("src/main/resources/static/css") + + doLast { + Files.createDirectories(directory.toPath()) } } + task<Exec>("sassCompile") { - if (System.getProperty("os.name").contains("windows", true)) { - commandLine("cmd", "/c", "npm", "run", "sassCompile") + dependsOn.add(tasks.getByName("ensureDirectory")) + if (System.getProperty("os.name").contains("windows",true)) { + commandLine("cmd", "/c", "sass", "src/main/resources/scss:src/main/resources/static/css") } else { - commandLine("npm", "run", "sassCompile") + commandLine("echo", "Checking for sass or sassc...") + doLast { + val res = exec { + isIgnoreExitValue = true + executable = "bash" + args = listOf("-l", "-c", "sass --version") + } + if (res.exitValue == 0) { + exec { commandLine("sass", "src/main/resources/scss:src/main/resources/static/css") } + } else { + File("src/main/resources/scss").listFiles()!!.filter { it.extension == "scss" && !it.name.startsWith("_") }.forEach { + exec { commandLine("sassc", "src/main/resources/scss/${it.name}", "src/main/resources/static/css/${it. nameWithoutExtension}.css") } + } + } + } } } + task<Exec>("tsCompile") { if (System.getProperty("os.name").contains("windows", true)) { commandLine("cmd", "/c", "npm", "run", "tsCompile") @@ -231,7 +257,7 @@ dependencies { // DB Drivers / Migration implementation("org.liquibase:liquibase-core") implementation("com.h2database:h2") - implementation("mysql:mysql-connector-java") + implementation("com.mysql:mysql-connector-j") implementation("org.mariadb.jdbc:mariadb-java-client") implementation("org.postgresql:postgresql") @@ -256,17 +282,15 @@ dependencies { implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect") // Guava - implementation("com.google.guava:guava:31.0.1-jre") + implementation("com.google.guava:guava:$guavaVersion") // Modemapper implementation("org.modelmapper:modelmapper:2.4.5") // Webjars implementation("org.webjars:webjars-locator-core") - implementation("org.webjars:jquery:3.6.0") + implementation("org.webjars:jquery:3.7.1") implementation("org.webjars:js-cookie:2.2.1") - // Better DateTime handling in JavaScript - implementation("org.webjars.npm:luxon:2.3.2") //// Websockets //implementation("org.webjars:sockjs-client:1.5.1") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254e91df704a9acc0f2745c0e340d9b582..a59520664252cb0fcb9587b12a18e36e6aa70253 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882ed57263a19bf3a504977da292d009345f..1b6c787337ffb79f0e3cf8b1e9f00f680a959de1 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/src/main/java/nl/tudelft/pupple/DevDatabaseLoader.java b/src/main/java/nl/tudelft/pupple/DevDatabaseLoader.java index 0e67df9fa8c2b50d49a534d3fba61474e345f22c..595b37527b44ad57b7ff8c1c79fbafcf3a83db3d 100644 --- a/src/main/java/nl/tudelft/pupple/DevDatabaseLoader.java +++ b/src/main/java/nl/tudelft/pupple/DevDatabaseLoader.java @@ -24,6 +24,10 @@ import java.util.List; import javax.annotation.PostConstruct; import javax.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + import nl.tudelft.labracore.api.CourseControllerApi; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.RoleControllerApi; @@ -40,10 +44,6 @@ import nl.tudelft.pupple.repository.PuzzleAttemptRepository; import nl.tudelft.pupple.repository.PuzzleRepository; import nl.tudelft.pupple.repository.labracore.PersonRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - @Service @Profile("dev") @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") // Can be disabled as this class is not tested. diff --git a/src/main/java/nl/tudelft/pupple/PuppleApplication.java b/src/main/java/nl/tudelft/pupple/PuppleApplication.java index a324057ca698fd01927c1aba8c0afaf44671a183..935958ee176a69be2190a03fb6e9fabe1c01dbe5 100644 --- a/src/main/java/nl/tudelft/pupple/PuppleApplication.java +++ b/src/main/java/nl/tudelft/pupple/PuppleApplication.java @@ -17,13 +17,13 @@ */ package nl.tudelft.pupple; -import nl.tudelft.labracore.lib.LabracoreApiConfig; -import nl.tudelft.librador.EnableLibrador; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; +import nl.tudelft.labracore.lib.LabracoreApiConfig; +import nl.tudelft.librador.EnableLibrador; + @EnableLibrador @Import(LabracoreApiConfig.class) @SpringBootApplication(scanBasePackageClasses = PuppleApplication.class) diff --git a/src/main/java/nl/tudelft/pupple/cache/PersonCacheManager.java b/src/main/java/nl/tudelft/pupple/cache/PersonCacheManager.java index 55438c93dfb95934761eb506eba870011540bfae..aed780071c5a25f686d4b77b3519fa22de54556f 100644 --- a/src/main/java/nl/tudelft/pupple/cache/PersonCacheManager.java +++ b/src/main/java/nl/tudelft/pupple/cache/PersonCacheManager.java @@ -21,15 +21,15 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import nl.tudelft.labracore.api.PersonControllerApi; -import nl.tudelft.labracore.api.dto.PersonSummaryDTO; - import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.ApplicationScope; +import nl.tudelft.labracore.api.PersonControllerApi; +import nl.tudelft.labracore.api.dto.PersonSummaryDTO; + @Component @ApplicationScope public class PersonCacheManager extends TimedCacheManager<Long, PersonSummaryDTO> { diff --git a/src/main/java/nl/tudelft/pupple/cache/RoleCacheManager.java b/src/main/java/nl/tudelft/pupple/cache/RoleCacheManager.java index b40f926969d3df91c231ea1b49c4dd8941ff62a0..558903a7be36affc02ba4616ebdf6c265cb86269 100644 --- a/src/main/java/nl/tudelft/pupple/cache/RoleCacheManager.java +++ b/src/main/java/nl/tudelft/pupple/cache/RoleCacheManager.java @@ -20,14 +20,14 @@ package nl.tudelft.pupple.cache; import java.util.List; import java.util.stream.Collectors; -import nl.tudelft.labracore.api.RoleControllerApi; -import nl.tudelft.labracore.api.dto.Id; -import nl.tudelft.labracore.api.dto.RoleDetailsDTO; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.RequestScope; +import nl.tudelft.labracore.api.RoleControllerApi; +import nl.tudelft.labracore.api.dto.Id; +import nl.tudelft.labracore.api.dto.RoleDetailsDTO; + @Component @RequestScope public class RoleCacheManager extends CoreCacheManager<Id, RoleDetailsDTO> { diff --git a/src/main/java/nl/tudelft/pupple/config/LibradorConfiguration.java b/src/main/java/nl/tudelft/pupple/config/LibradorConfiguration.java index 00412106260a0688ced8e4a08cbe487550d73fd2..0cb40ef6b4b641e680d16a36bea0e707d08f17a4 100644 --- a/src/main/java/nl/tudelft/pupple/config/LibradorConfiguration.java +++ b/src/main/java/nl/tudelft/pupple/config/LibradorConfiguration.java @@ -19,14 +19,14 @@ package nl.tudelft.pupple.config; import static org.modelmapper.convention.MatchingStrategies.STRICT; -import nl.tudelft.librador.EnableLibrador; -import nl.tudelft.librador.LibradorConfigAdapter; -import nl.tudelft.librador.dto.id.IdMapperBuilder; - import org.modelmapper.ModelMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import nl.tudelft.librador.EnableLibrador; +import nl.tudelft.librador.LibradorConfigAdapter; +import nl.tudelft.librador.dto.id.IdMapperBuilder; + @Configuration @EnableLibrador public class LibradorConfiguration extends LibradorConfigAdapter { diff --git a/src/main/java/nl/tudelft/pupple/controller/HomeController.java b/src/main/java/nl/tudelft/pupple/controller/HomeController.java index fe88caa21d0e18d915f340fec8d6acdfc3b4a69e..107912cf7a9275e46e32a2dc3b2f14bdc25d0bd1 100644 --- a/src/main/java/nl/tudelft/pupple/controller/HomeController.java +++ b/src/main/java/nl/tudelft/pupple/controller/HomeController.java @@ -19,6 +19,11 @@ package nl.tudelft.pupple.controller; import javax.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.RoleControllerApi; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; @@ -26,11 +31,6 @@ import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.pupple.repository.PuzzleRepository; import nl.tudelft.pupple.repository.labracore.PersonRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; - @Controller public class HomeController { diff --git a/src/main/java/nl/tudelft/pupple/controller/PersonController.java b/src/main/java/nl/tudelft/pupple/controller/PersonController.java index 847cbd89ba2af4d2eaee5bce5d95182a8b16f8f3..f852dc96329af0e888e5f3a196cb8b0aa6364fe3 100644 --- a/src/main/java/nl/tudelft/pupple/controller/PersonController.java +++ b/src/main/java/nl/tudelft/pupple/controller/PersonController.java @@ -17,15 +17,13 @@ */ package nl.tudelft.pupple.controller; -import nl.tudelft.labracore.api.PersonControllerApi; -import nl.tudelft.labracore.api.dto.PersonDetailsDTO; -import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.pupple.model.labracore.PupplePerson; -import nl.tudelft.pupple.repository.labracore.*; -import nl.tudelft.pupple.security.AuthorisationService; +import java.time.LocalDate; +import java.util.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; @@ -33,19 +31,41 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; +import nl.tudelft.labracore.api.PersonControllerApi; +import nl.tudelft.labracore.api.dto.PersonDetailsDTO; +import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; +import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.pupple.controller.utility.PageUtil; +import nl.tudelft.pupple.model.Puzzle; +import nl.tudelft.pupple.model.labracore.PupplePerson; +import nl.tudelft.pupple.repository.PuzzleAttemptRepository; +import nl.tudelft.pupple.repository.PuzzleRepository; +import nl.tudelft.pupple.repository.labracore.PersonRepository; +import nl.tudelft.pupple.security.AuthorisationService; +import nl.tudelft.pupple.service.PuzzleAttemptService; + @Controller @RequestMapping("profile") public class PersonController { private final PersonRepository personRepository; + private final PuzzleRepository puzzleRepository; + private final PuzzleAttemptRepository puzzleAttemptRepository; private final AuthorisationService authorisationService; + private final PuzzleAttemptService puzzleAttemptService; private final PersonControllerApi personControllerApi; @Autowired public PersonController(PersonRepository personRepository, + PuzzleRepository puzzleRepository, + PuzzleAttemptRepository puzzleAttemptRepository, AuthorisationService authorisationService, + PuzzleAttemptService puzzleAttemptService, PersonControllerApi personControllerApi) { this.personRepository = personRepository; + this.puzzleRepository = puzzleRepository; + this.puzzleAttemptRepository = puzzleAttemptRepository; this.authorisationService = authorisationService; + this.puzzleAttemptService = puzzleAttemptService; this.personControllerApi = personControllerApi; } @@ -57,18 +77,67 @@ public class PersonController { @GetMapping("{id}") @PreAuthorize("@authorisationService.isAuthenticated()") - public String getProfile(@AuthenticatedPerson Person person, @PathVariable("id") long personId, + public String getProfile(@AuthenticatedPerson Person person, + @PageableDefault(value = 7) Pageable pageable, + @PathVariable("id") long personId, + @RequestParam(name = "unassigned", required = false) String unassigned, Model model) { PupplePerson pupplePerson = personRepository.findByIdOrThrow(personId); - if (authorisationService.isAdmin() || person.getId() == personId || pupplePerson.getPublicProfile()) { - PersonDetailsDTO otherPerson = personControllerApi.getPersonById(personId).block(); + if (!(authorisationService.isAdmin() || person.getId() == personId + || pupplePerson.getPublicProfile())) { + return "profile/forbidden"; + } + + PersonDetailsDTO otherPerson = personControllerApi.getPersonById(personId).block(); - model.addAttribute("displayName", otherPerson.getDisplayName()); - model.addAttribute("pupplePerson", pupplePerson); - model.addAttribute("isOwner", person.getId() == personId); - return "profile/view"; + List<Puzzle> puzzlesFeatured; + boolean isOwner = person.getId() == personId; + boolean canViewFuturePuzzles = isOwner || authorisationService.isAdmin(); + + if (unassigned != null) { + if (canViewFuturePuzzles) { + puzzlesFeatured = puzzleRepository.findByDayUsedIsNullAndCreator(pupplePerson); + } else { + puzzlesFeatured = new ArrayList<>(); + } + } else { + if (canViewFuturePuzzles) { + puzzlesFeatured = puzzleRepository.findByCreator(pupplePerson); + } else { + puzzlesFeatured = puzzleRepository.findByDayUsedIsBeforeAndCreator(LocalDate + .now().plusDays(1), pupplePerson); + } } - return "profile/forbidden"; + + HashMap<Long, String> displayNames = new HashMap<>(); + puzzlesFeatured.stream() + .filter(puzzle -> puzzle.getCreator() != null && puzzle.getCreator().getPublicProfile()) + .map(puzzle -> puzzle.getCreator().getId()) + .forEach((id) -> displayNames.put(id, + personControllerApi.getPersonById(id).block().getDisplayName())); + HashMap<Long, Long> attempts = new HashMap<>(); + puzzlesFeatured.stream() + .filter(puzzle -> puzzleAttemptRepository.getByPersonIdAndPuzzleId(person.getId(), + puzzle.getId()) != null) + .forEach((puzzle) -> attempts.put(puzzle.getId(), + (long) puzzleAttemptRepository.getByPersonIdAndPuzzleId( + person.getId(), puzzle.getId()).getGuesses().size())); + + puzzlesFeatured.sort(Comparator + .comparing(Puzzle::getDayUsed, Comparator.nullsFirst(Comparator.naturalOrder())) + .reversed()); + Page<Puzzle> puzzlePage = PageUtil.pageFromList(puzzlesFeatured, pageable); + Set<Puzzle> puzzlesSolved = new HashSet<>(puzzleAttemptService + .filterSolvedPuzzles(person, puzzlesFeatured)); + + model.addAttribute("displayName", otherPerson.getDisplayName()); + model.addAttribute("pupplePerson", pupplePerson); + model.addAttribute("solved", puzzlesSolved); + model.addAttribute("puzzles", puzzlePage); + model.addAttribute("isOwner", isOwner); + model.addAttribute("displayNames", displayNames); + model.addAttribute("guessAmounts", attempts); + return "profile/view"; } @PostMapping("{id}") diff --git a/src/main/java/nl/tudelft/pupple/controller/PuzzleController.java b/src/main/java/nl/tudelft/pupple/controller/PuzzleController.java index e3656e83441d2a71c259806fda3af7586b0086f7..2577f866ffb860f640dc916b7c78faf63f62aadd 100644 --- a/src/main/java/nl/tudelft/pupple/controller/PuzzleController.java +++ b/src/main/java/nl/tudelft/pupple/controller/PuzzleController.java @@ -23,6 +23,17 @@ import java.util.stream.Collectors; import javax.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + import nl.tudelft.labracore.api.PersonControllerApi; import nl.tudelft.labracore.api.dto.PersonDetailsDTO; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; @@ -43,17 +54,6 @@ import nl.tudelft.pupple.security.AuthorisationService; import nl.tudelft.pupple.service.PuzzleAttemptService; import nl.tudelft.pupple.service.PuzzleService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - @Controller @RequestMapping("puzzle") public class PuzzleController { @@ -112,12 +112,27 @@ public class PuzzleController { pupplePerson); } } + HashMap<Long, String> displayNames = new HashMap<>(); + puzzles.stream() + .filter(puzzle -> puzzle.getCreator() != null && puzzle.getCreator().getPublicProfile()) + .map(puzzle -> puzzle.getCreator().getId()) + .forEach((id) -> displayNames.put(id, + personControllerApi.getPersonById(id).block().getDisplayName())); + HashMap<Long, Long> attempts = new HashMap<>(); + puzzles.stream() + .filter(puzzle -> puzzleAttemptRepository.getByPersonIdAndPuzzleId(person.getId(), + puzzle.getId()) != null) + .forEach((puzzle) -> attempts.put(puzzle.getId(), + (long) puzzleAttemptRepository.getByPersonIdAndPuzzleId( + person.getId(), puzzle.getId()).getGuesses().size())); Collections.sort(puzzles, Comparator .comparing(Puzzle::getDayUsed, Comparator.nullsFirst(Comparator.naturalOrder())).reversed()); Set<Puzzle> puzzlesSolved = new HashSet<>(puzzleAttemptService.filterSolvedPuzzles(person, puzzles)); Page<Puzzle> pageOfPuzzles = PageUtil.pageFromList(puzzles, pageable); model.addAttribute("puzzles", pageOfPuzzles); model.addAttribute("solved", puzzlesSolved); + model.addAttribute("displayNames", displayNames); + model.addAttribute("guessAmounts", attempts); return "puzzle/all"; } diff --git a/src/main/java/nl/tudelft/pupple/dto/patch/PuzzlePatchDTO.java b/src/main/java/nl/tudelft/pupple/dto/patch/PuzzlePatchDTO.java index 1ebbfcaae61f73813fc04329818956762144a9c1..244e78d2076397e0f3c8ebe568154894ce39f63c 100644 --- a/src/main/java/nl/tudelft/pupple/dto/patch/PuzzlePatchDTO.java +++ b/src/main/java/nl/tudelft/pupple/dto/patch/PuzzlePatchDTO.java @@ -25,12 +25,12 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import org.springframework.format.annotation.DateTimeFormat; + import lombok.*; import nl.tudelft.librador.dto.patch.Patch; import nl.tudelft.pupple.model.Puzzle; -import org.springframework.format.annotation.DateTimeFormat; - @Data @Builder @NoArgsConstructor diff --git a/src/main/java/nl/tudelft/pupple/model/Puzzle.java b/src/main/java/nl/tudelft/pupple/model/Puzzle.java index dface6934271c429fb7a1a820f1596bcb814277c..9c14ae1056c90173fb4818223290f704fc467ef9 100644 --- a/src/main/java/nl/tudelft/pupple/model/Puzzle.java +++ b/src/main/java/nl/tudelft/pupple/model/Puzzle.java @@ -25,12 +25,12 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; -import lombok.*; -import nl.tudelft.pupple.model.labracore.PupplePerson; - import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; +import lombok.*; +import nl.tudelft.pupple.model.labracore.PupplePerson; + @Data @Entity @Builder diff --git a/src/main/java/nl/tudelft/pupple/model/PuzzleAttempt.java b/src/main/java/nl/tudelft/pupple/model/PuzzleAttempt.java index 9eb6d6d870c971b71ceeec5a958f8b688400995e..cfcf1fca481a99b926e7c4fa8e9f968727f54b4d 100644 --- a/src/main/java/nl/tudelft/pupple/model/PuzzleAttempt.java +++ b/src/main/java/nl/tudelft/pupple/model/PuzzleAttempt.java @@ -23,12 +23,12 @@ import java.util.List; import javax.persistence.*; import javax.validation.constraints.NotNull; -import lombok.*; -import nl.tudelft.pupple.model.labracore.PupplePerson; - import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; +import lombok.*; +import nl.tudelft.pupple.model.labracore.PupplePerson; + @Data @Entity @Builder diff --git a/src/main/java/nl/tudelft/pupple/repository/GuessRepository.java b/src/main/java/nl/tudelft/pupple/repository/GuessRepository.java index 2f0afe773d7a1f4adec4bce2e02d51c68cac8a15..d41e6e078bfc87ce7b2366e4fecb2f52fc13fe11 100644 --- a/src/main/java/nl/tudelft/pupple/repository/GuessRepository.java +++ b/src/main/java/nl/tudelft/pupple/repository/GuessRepository.java @@ -20,11 +20,11 @@ package nl.tudelft.pupple.repository; import java.util.Collection; import java.util.List; -import nl.tudelft.pupple.model.Guess; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.web.client.ResourceAccessException; +import nl.tudelft.pupple.model.Guess; + public interface GuessRepository extends JpaRepository<Guess, Long> { default Guess findByIdOrThrow(long id) { return findById(id).orElseThrow(() -> new ResourceAccessException("Guess was not found: " + id)); diff --git a/src/main/java/nl/tudelft/pupple/repository/HintRepository.java b/src/main/java/nl/tudelft/pupple/repository/HintRepository.java index 70bc3d6cb1287171c11d48b9e1e40880a301685d..3516e771560e98f435101e94302bc9f51c074c83 100644 --- a/src/main/java/nl/tudelft/pupple/repository/HintRepository.java +++ b/src/main/java/nl/tudelft/pupple/repository/HintRepository.java @@ -20,11 +20,11 @@ package nl.tudelft.pupple.repository; import java.util.Collection; import java.util.List; -import nl.tudelft.pupple.model.Hint; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.web.client.ResourceAccessException; +import nl.tudelft.pupple.model.Hint; + public interface HintRepository extends JpaRepository<Hint, Long> { default Hint findByIdOrThrow(long id) { return findById(id).orElseThrow(() -> new ResourceAccessException("Hint was not found: " + id)); diff --git a/src/main/java/nl/tudelft/pupple/repository/PuzzleAttemptRepository.java b/src/main/java/nl/tudelft/pupple/repository/PuzzleAttemptRepository.java index d68f2eaa444a1348e198c524e17f450529b6d5e7..d89e5778d44567f25eda56bd6801b3b52b62dd3b 100644 --- a/src/main/java/nl/tudelft/pupple/repository/PuzzleAttemptRepository.java +++ b/src/main/java/nl/tudelft/pupple/repository/PuzzleAttemptRepository.java @@ -20,11 +20,11 @@ package nl.tudelft.pupple.repository; import java.util.List; import java.util.Set; -import nl.tudelft.pupple.model.PuzzleAttempt; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.web.client.ResourceAccessException; +import nl.tudelft.pupple.model.PuzzleAttempt; + public interface PuzzleAttemptRepository extends JpaRepository<PuzzleAttempt, Long> { default PuzzleAttempt findByIdOrThrow(long id) { return findById(id) diff --git a/src/main/java/nl/tudelft/pupple/repository/PuzzleRepository.java b/src/main/java/nl/tudelft/pupple/repository/PuzzleRepository.java index 72157054d2f02390b5c43f9c89aef6e0c02257e6..15463fee15ca7c78f7f613da047100a98c57010d 100644 --- a/src/main/java/nl/tudelft/pupple/repository/PuzzleRepository.java +++ b/src/main/java/nl/tudelft/pupple/repository/PuzzleRepository.java @@ -21,23 +21,31 @@ import java.time.LocalDate; import java.util.Collection; import java.util.List; -import nl.tudelft.pupple.model.Puzzle; -import nl.tudelft.pupple.model.labracore.PupplePerson; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.web.client.ResourceAccessException; +import nl.tudelft.pupple.model.Puzzle; +import nl.tudelft.pupple.model.labracore.PupplePerson; + public interface PuzzleRepository extends JpaRepository<Puzzle, Long> { default Puzzle findByIdOrThrow(long id) { return findById(id).orElseThrow(() -> new ResourceAccessException("Puzzle was not found: " + id)); } + long countByCreator(PupplePerson creator); + + long countByDayUsedIsBeforeAndCreator(LocalDate date, PupplePerson creator); + List<Puzzle> findAllByIdIn(Collection<Long> id); + List<Puzzle> findByCreator(PupplePerson creator); + Puzzle findByDayUsed(LocalDate date); List<Puzzle> findByDayUsedIsNotNullAndDayUsedIsBefore(LocalDate date); + List<Puzzle> findByDayUsedIsBeforeAndCreator(LocalDate date, PupplePerson creator); + List<Puzzle> findByDayUsedIsBeforeOrCreator(LocalDate date, PupplePerson creator); List<Puzzle> findByDayUsedIsNull(); diff --git a/src/main/java/nl/tudelft/pupple/repository/labracore/PersonRepository.java b/src/main/java/nl/tudelft/pupple/repository/labracore/PersonRepository.java index 54e4ea2248db91cda043df7b3b9920ad96f167f3..f2dcde756777c21c022ce68fc4d276bf686e9873 100644 --- a/src/main/java/nl/tudelft/pupple/repository/labracore/PersonRepository.java +++ b/src/main/java/nl/tudelft/pupple/repository/labracore/PersonRepository.java @@ -17,11 +17,11 @@ */ package nl.tudelft.pupple.repository.labracore; -import nl.tudelft.pupple.model.labracore.PupplePerson; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import nl.tudelft.pupple.model.labracore.PupplePerson; + public interface PersonRepository extends JpaRepository<PupplePerson, Long> { default PupplePerson findByIdOrThrow(Long id) { diff --git a/src/main/java/nl/tudelft/pupple/security/AuthorisationService.java b/src/main/java/nl/tudelft/pupple/security/AuthorisationService.java index 4de96e97beb3b8f9ace26e059c5344f6c45f4ffc..4a2a9d4bec59370669cbaba9580422a5b098d019 100644 --- a/src/main/java/nl/tudelft/pupple/security/AuthorisationService.java +++ b/src/main/java/nl/tudelft/pupple/security/AuthorisationService.java @@ -19,16 +19,17 @@ package nl.tudelft.pupple.security; import java.time.LocalDate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + import nl.tudelft.labracore.api.CourseControllerApi; import nl.tudelft.labracore.lib.security.LabradorUserDetails; import nl.tudelft.labracore.lib.security.user.DefaultRole; import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.pupple.cache.RoleCacheManager; import nl.tudelft.pupple.model.Puzzle; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; +import nl.tudelft.pupple.model.labracore.PupplePerson; @Service public class AuthorisationService { @@ -111,6 +112,16 @@ public class AuthorisationService { return false; } + public boolean canEditPuzzlesOfUser(PupplePerson user) { + if (!isAuthenticated()) { + return false; + } + if (isAdmin()) { + return true; + } + return user != null && user.getId().equals(getAuthPerson().getId()); + } + public boolean canViewPuzzle(Puzzle puzzle) { if (!isAuthenticated()) { return false; diff --git a/src/main/java/nl/tudelft/pupple/security/LoginSecurityConfigurerAdapter.java b/src/main/java/nl/tudelft/pupple/security/LoginSecurityConfigurerAdapter.java index 8aade5bb8168a397153822ed2f96b1375e8de0fc..80f8eeb2ed4c4d357645066b2c4f17d218b085bd 100644 --- a/src/main/java/nl/tudelft/pupple/security/LoginSecurityConfigurerAdapter.java +++ b/src/main/java/nl/tudelft/pupple/security/LoginSecurityConfigurerAdapter.java @@ -17,14 +17,14 @@ */ package nl.tudelft.pupple.security; -import nl.tudelft.labracore.lib.security.LabradorSecurityConfigurerAdapter; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import nl.tudelft.labracore.lib.security.LabradorSecurityConfigurerAdapter; + /** * Security configuration adapter required to make use of the LabraDoor login. * diff --git a/src/main/java/nl/tudelft/pupple/security/LoginUserHandler.java b/src/main/java/nl/tudelft/pupple/security/LoginUserHandler.java index a2dbafb16cfc6f04b5560d6fb803c0d056deaf5f..aa497e758d4c68a25d906c491c254907eb0eb4b8 100644 --- a/src/main/java/nl/tudelft/pupple/security/LoginUserHandler.java +++ b/src/main/java/nl/tudelft/pupple/security/LoginUserHandler.java @@ -17,15 +17,14 @@ */ package nl.tudelft.pupple.security; -import nl.tudelft.labracore.lib.security.LabradorUserHandler; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.pupple.model.labracore.PupplePerson; -import nl.tudelft.pupple.repository.labracore.PersonRepository; - import org.springframework.stereotype.Service; import io.sentry.Sentry; import io.sentry.protocol.User; +import nl.tudelft.labracore.lib.security.LabradorUserHandler; +import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.pupple.model.labracore.PupplePerson; +import nl.tudelft.pupple.repository.labracore.PersonRepository; /** * Interface for handling user logins on the client implementation. diff --git a/src/main/java/nl/tudelft/pupple/security/LoginUserProvider.java b/src/main/java/nl/tudelft/pupple/security/LoginUserProvider.java index 30bd0cd547577dd09f9b311917ea069f59ae4264..30ec87985639a5f99f4d228b0168ab78cd0c3ac7 100644 --- a/src/main/java/nl/tudelft/pupple/security/LoginUserProvider.java +++ b/src/main/java/nl/tudelft/pupple/security/LoginUserProvider.java @@ -19,11 +19,11 @@ package nl.tudelft.pupple.security; import static nl.tudelft.labracore.lib.security.user.DefaultRole.*; -import nl.tudelft.labracore.lib.security.memory.InMemoryUserProvider; - import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import nl.tudelft.labracore.lib.security.memory.InMemoryUserProvider; + /** * Provider for in-memory user information. * diff --git a/src/main/java/nl/tudelft/pupple/service/PuzzleAttemptService.java b/src/main/java/nl/tudelft/pupple/service/PuzzleAttemptService.java index 77e56a01a83efbe3489f6cedbe7fade7d30a34a3..e25781f6701a2b096d390d71791ca83aa8796082 100644 --- a/src/main/java/nl/tudelft/pupple/service/PuzzleAttemptService.java +++ b/src/main/java/nl/tudelft/pupple/service/PuzzleAttemptService.java @@ -19,6 +19,11 @@ package nl.tudelft.pupple.service; import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.pupple.model.Guess; import nl.tudelft.pupple.model.Puzzle; @@ -30,11 +35,6 @@ import nl.tudelft.pupple.repository.PuzzleRepository; import nl.tudelft.pupple.repository.labracore.PersonRepository; import nl.tudelft.pupple.security.AuthorisationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; - @Service public class PuzzleAttemptService { private final GuessRepository guessRepository; diff --git a/src/main/java/nl/tudelft/pupple/service/PuzzleService.java b/src/main/java/nl/tudelft/pupple/service/PuzzleService.java index e4dea73e3b122c105f4d50b6bfc4248d4797fe4f..778a698cb14ed987d81f07df1805c51076933208 100644 --- a/src/main/java/nl/tudelft/pupple/service/PuzzleService.java +++ b/src/main/java/nl/tudelft/pupple/service/PuzzleService.java @@ -19,12 +19,12 @@ package nl.tudelft.pupple.service; import java.time.LocalDate; -import nl.tudelft.pupple.model.Puzzle; -import nl.tudelft.pupple.repository.PuzzleRepository; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import nl.tudelft.pupple.model.Puzzle; +import nl.tudelft.pupple.repository.PuzzleRepository; + @Service public class PuzzleService { private final PuzzleRepository puzzleRepository; diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 76b8a130ea3a57224f781bd2052a8273365b2061..d5588185a31ac3d31004411af3152ec0bfd3a053 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -27,10 +27,9 @@ <script src="/webjars/jquery/jquery.min.js" defer></script> <script src="/webjars/js-cookie/js.cookie.js"></script> - <script src="/webjars/luxon/build/global/luxon.js"></script> <link rel="stylesheet" href="/webjars/chihuahui/main.css" /> - <script src="/webjars/chihuahui/1.0.0/theme.js"></script> + <script src="/webjars/chihuahui/1.1.0/theme.js"></script> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width initial-scale=1" /> diff --git a/src/main/resources/templates/privacy.html b/src/main/resources/templates/privacy.html index 7d641e6792129e0412d2919bb9852616de96915e..99e9c4b11df015b910e9d7d7a3b5997eb814f2ea 100644 --- a/src/main/resources/templates/privacy.html +++ b/src/main/resources/templates/privacy.html @@ -59,10 +59,8 @@ <ul> <li>Name</li> <li>Email</li> - <li>Student number</li> <li>NetID</li> <li>affiliation (if the user is a student or teacher)</li> - <li>which department/faculty the user is part of</li> </ul> <p> Furthermore, through their interactions with the system, we store the following data diff --git a/src/main/resources/templates/profile/view.html b/src/main/resources/templates/profile/view.html index d7589ba1cd0f61fdf3989fd660af2ab4300ea7c7..e50eab10332ede1bf5b0a93b65ac930b9e4f881f 100644 --- a/src/main/resources/templates/profile/view.html +++ b/src/main/resources/templates/profile/view.html @@ -31,16 +31,6 @@ <div layout:fragment="content"> <h1 class="font-800 mb-5" th:text="${displayName}" /> - <table class="table"> - <tr> - <td>Number of puzzles solved:</td> - <td th:text="${pupplePerson.numberOfPuzzlesSolved()}"></td> - </tr> - <tr> - <td>Number of puzzles featured:</td> - <td th:text="${pupplePerson.numberOfPuzzlesFeatured()}"></td> - </tr> - </table> <form th:if="${isOwner}" action="#" @@ -54,6 +44,24 @@ th:checked="${pupplePerson.getPublicProfile()}" /> <button type="submit" class="button">Save</button> </form> + <table class="table"> + <tr> + <td>Number of puzzles solved:</td> + <td th:text="${pupplePerson.numberOfPuzzlesSolved()}"></td> + </tr> + <tr> + <td>Number of puzzles featured:</td> + <td th:text="${pupplePerson.numberOfPuzzlesFeatured()}"></td> + </tr> + <tr> + <td>Puzzles featured:</td> + </tr> + </table> + + <div class="pl-4"> + <th:block + layout:replace="~{puzzleList :: puzzleList(puzzles=${puzzles}, solved=${solved}, user=${pupplePerson})}"></th:block> + </div> </div> </body> </html> diff --git a/src/main/resources/templates/puzzle/all.html b/src/main/resources/templates/puzzle/all.html index 6fa16452630e2f61a3f230cf1761945d0a48f98e..28082797c159f0208b60df63610f1cff2261acba 100644 --- a/src/main/resources/templates/puzzle/all.html +++ b/src/main/resources/templates/puzzle/all.html @@ -31,59 +31,8 @@ <div layout:fragment="content"> <h1 class="font-800 mb-5">All puzzles</h1> - <table class="table" data-style="surface"> - <tr class="table__header"> - <th>Day</th> - <th>Solved?</th> - <th:block - th:if="${@authorisationService.canAssignToDay() and param.showSolution != null}"> - <th>Solution</th> - </th:block> - </tr> - <th:block th:each="puzzle : ${puzzles}"> - <tr> - <td> - <a - th:href="@{/puzzle/{id}/(id=${puzzle.id})}" - th:text="${puzzle.getDayUsed() == null ? 'Puzzle not used yet, id: ' + puzzle.id : puzzle.getDayUsed()}" /> - </td> - <td> - <span - class="fa-solid" - th:classappend="${solved.contains(puzzle) ? 'fa-square-check colour-accept' : 'fa-square-xmark colour-error'}" /> - </td> - <th:block - th:if="${@authorisationService.canAssignToDay() and param.showSolution != null}"> - <td> - <span th:text="${puzzle.getSolution()}"></span> - </td> - </th:block> - </tr> - </th:block> - </table> <th:block - layout:replace="~{pagination :: pagination(page=${puzzles}, size=5)}"></th:block> - - <th-block th:if="${param.unassigned == null}"> - <a class="link" href="?unassigned"> - <button class="button">Show unassigned puzzles</button> - </a> - </th-block> - <th-block th:if="${param.unassigned != null}"> - <a class="link" href="?"> - <button class="button">Show all puzzles</button> - </a> - </th-block> - <th-block th:if="${@authorisationService.canAssignToDay()}"> - <th-block th:if="${param.showSolution == null}"> - <a class="link" href="?showSolution"> - <button class="button">Show solutions</button> - </a> - </th-block> - <th-block th:if="${param.showSolution != null}"> - <a class="link" href="?"><button class="button">Hide solutions</button></a> - </th-block> - </th-block> + layout:replace="~{puzzleList :: puzzleList(puzzles=${puzzles}, solved=${solved}, user=${null})}"></th:block> </div> </body> </html> diff --git a/src/main/resources/templates/puzzle/attempts.html b/src/main/resources/templates/puzzle/attempts.html index e285732da8e5ed11d411263f94a63eced5193abc..9841501092cfd87b985f4601e930845bfe8f5dc7 100644 --- a/src/main/resources/templates/puzzle/attempts.html +++ b/src/main/resources/templates/puzzle/attempts.html @@ -48,8 +48,10 @@ <th:block th:each="attempt : ${attempts}"> <tr> <td> - <span - th:text="${people.get(attempt.getPerson().getId()).getDisplayName()}"></span> + <a th:href="@{/profile/{id}(id=${attempt.getPerson().getId()})}"> + <span + th:text="${people.get(attempt.getPerson().getId()).getDisplayName()}"></span> + </a> </td> <td> <span th:text="${attempt.getGuesses().size()}"></span> diff --git a/src/main/resources/templates/puzzle/view.html b/src/main/resources/templates/puzzle/view.html index 7c71b06c6c9dfba8530655df180221659d8b7e3f..c18ee48afe29a22d64c273393992b86e726ace80 100644 --- a/src/main/resources/templates/puzzle/view.html +++ b/src/main/resources/templates/puzzle/view.html @@ -48,6 +48,11 @@ th:text="${puzzleCreator}"></a> </h2> </th:block> + <th:block th:if="${@authorisationService.isAdmin()}"> + <a class="link" th:href="@{/puzzle/{id}/attempts(id=${puzzle.getId()})}"> + View attempts + </a> + </th:block> <div class="grid col-2 font-800 align-center justify-center mb-5"> <th:block th:each="hint: ${puzzle.hints}"> <div class="hint align-center flex justify-center center-text p-5"> diff --git a/src/main/resources/templates/puzzleList.html b/src/main/resources/templates/puzzleList.html new file mode 100644 index 0000000000000000000000000000000000000000..dbc23497f9799de7f6fd0a7e6814a25703534d25 --- /dev/null +++ b/src/main/resources/templates/puzzleList.html @@ -0,0 +1,108 @@ +<!-- + + Pupple + Copyright (C) 2023 - Delft University of Technology + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +--> +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div layout:fragment="puzzleList(puzzles, solved, user)"> + <table class="table" data-style="surface"> + <tr th:if="${puzzles.getNumberOfElements() == 0}" class="table__header"> + <th colspan="2">No puzzles to show</th> + </tr> + <tr th:if="${puzzles.getNumberOfElements() > 0}" class="table__header"> + <th>✓</th> + <th>Day</th> + <th>Creator</th> + <th>Tries</th> + <th:block + th:if="${@authorisationService.canAssignToDay() and param.showSolution != null}"> + <th>Solution</th> + </th:block> + </tr> + <th:block th:each="puzzle : ${puzzles}"> + <tr> + <td style="text-align: right"> + <!--Solved?--> + <span + style="font-size: 21px" + class="fa-solid" + th:classappend="${solved.contains(puzzle) ? 'fa-square-check colour-accept' : 'fa-square-xmark colour-error'}" /> + </td> + <td> + <!--Day--> + <a + th:href="@{/puzzle/{id}/(id=${puzzle.id})}" + th:text="${puzzle.getDayUsed() == null ? 'Puzzle not used yet, id: ' + puzzle.id : puzzle.getDayUsed()}" /> + </td> + <td> + <!--Creator--> + <th:block + th:if="${puzzle.getCreator() != null && puzzle.getCreator().getPublicProfile()}"> + <a + th:href="@{/profile/{id}/(id=${puzzle.getCreator().getId()})}" + th:text="${displayNames.get(puzzle.getCreator().getId())}" /> + </th:block> + <th:block + th:unless="${puzzle.getCreator() != null && puzzle.getCreator().getPublicProfile()}"> + <span th:text="Anonymous" style="font-style: italic" /> + </th:block> + </td> + <td style="text-align: center"> + <!--Guesses--> + <th:block th:if="${guessAmounts.get(puzzle.getId()) > 0}"> + <span th:text="${guessAmounts.get(puzzle.getId())}" /> + </th:block> + </td> + <th:block + th:if="${@authorisationService.canAssignToDay() and param.showSolution != null}"> + <td> + <span th:text="${puzzle.getSolution()}"></span> + </td> + </th:block> + </tr> + </th:block> + </table> + <th-block + th:if="${@authorisationService.canAssignToDay() || @authorisationService.canEditPuzzlesOfUser(user)}"> + <th:block + layout:replace="~{pagination :: pagination(page=${puzzles}, size=5)}"></th:block> + + <th-block th:if="${param.unassigned == null}"> + <a class="link" href="?unassigned"> + <button class="button">Show unassigned puzzles</button> + </a> + </th-block> + <th-block th:if="${param.unassigned != null}"> + <a class="link" href="?"> + <button class="button">Show all puzzles</button> + </a> + </th-block> + <th-block th:if="${param.showSolution == null}"> + <a class="link" href="?showSolution"> + <button class="button">Show solutions</button> + </a> + </th-block> + <th-block th:if="${param.showSolution != null}"> + <a class="link" href="?"><button class="button">Hide solutions</button></a> + </th-block> + </th-block> + </div> +</html>