From 63129e4ef92199e5858775da936e980171129dae Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 11:54:57 +0000 Subject: [PATCH 01/74] What horrors do we unearth when we try to gradle this build? --- build.gradle | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 build.gradle diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..4f11be13 --- /dev/null +++ b/build.gradle @@ -0,0 +1,90 @@ +buildscript { + ext.kotlin_version = '1.2.41' + + repositories { + mavenCentral() + mavenLocal() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" + // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin + + } +} + +apply plugin: 'java' +apply plugin: "kotlin" +apply plugin: "kotlin-spring" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +bootJar { + baseName = 'gs-spring-boot' + version = '0.1.0' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compile "org.springframework.boot:spring-boot-starter-web" + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + + compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" + compile "com.fasterxml.jackson.core:jackson-core:2.9.7" + compile "com.fasterxml.jackson.core:jackson-databind:2.9.7" + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + testImplementation('org.junit.jupiter:junit-jupiter-api') + testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine') + + compile "commons-fileupload:commons-fileupload:1.3.1" + compile "uk.co.squadlist:squadlist-api-client:0.0.1-SNAPSHOT" + compile "com.google.guava:guava:16.0" + compile "org.apache.commons:commons-csv:1.0" + compile "org.mnode.ical4j:ical4j:1.0.2" + compile "oro:oro:2.0.8" + compile "org.slf4j:slf4j-api:1.6.1" + compile "org.hibernate:hibernate-validator-annotation-processor:4.1.0.Final" + compile "org.codehaus.jackson:jackson-core-asl:1.9.4" + compile "org.codehaus.jackson:jackson-mapper-asl:1.9.4" + compile "joda-time:joda-time:2.1" + compile "javax.servlet:servlet-api:2.5" + compile "log4j:log4j:1.2.16" + compile "uk.co.eelpieconsulting.common:common-dates:1.0.10" + compile "uk.co.eelpieconsulting.common:common-views:1.0.11" + compile "jpwgen:jpwgen:1.2.0" + compile "commons-cli:commons-cli:1.2" + compile "com.restfb:restfb:1.46.0" + compile "commons-io:commons-io:2.4" + + testImplementation('org.junit.jupiter:junit-jupiter-api') + testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine') + testCompile "junit:junit:4.8.2" + testCompile "org.mockito:mockito-core:1.10.8" + testCompile "org.springframework:spring-mock:2.0.8" +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +test { + useJUnitPlatform() +} From 09591406755a5ca882ede13d4bd185421344eab2 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 17:00:04 +0000 Subject: [PATCH 02/74] Starts up after gradle build; latest spring boot does not support velocity out of the box. --- build.gradle | 35 ++- build.xml | 41 ---- pom.xml | 229 ------------------ squadlist.yaml | 36 --- src/main/java/uk/co/squadlist/web/Main.java | 181 +++++++------- .../web/controllers/AdminController.java | 8 +- .../web/controllers/ErrorController.java | 5 +- .../web/controllers/MyOutingsController.java | 177 ++++++-------- .../forms/AvailabilityOptionDetails.java | 52 ++-- .../web/services/github/GithubService.java | 77 ------ .../uk/co/squadlist/web/views/RssOuting.java | 54 ----- src/main/resources/log4j.properties | 5 + src/main/resources/templates/login.ftl | 101 ++++++++ 13 files changed, 302 insertions(+), 699 deletions(-) delete mode 100644 build.xml delete mode 100644 pom.xml delete mode 100644 squadlist.yaml delete mode 100644 src/main/java/uk/co/squadlist/web/services/github/GithubService.java delete mode 100644 src/main/java/uk/co/squadlist/web/views/RssOuting.java create mode 100644 src/main/resources/log4j.properties create mode 100644 src/main/resources/templates/login.ftl diff --git a/build.gradle b/build.gradle index 4f11be13..70e4f24e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,13 +3,11 @@ buildscript { repositories { mavenCentral() - mavenLocal() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" - // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin + classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin } } @@ -20,13 +18,9 @@ apply plugin: "kotlin-spring" // See https://kotlinlang.org/docs/reference/compi apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' -bootJar { - baseName = 'gs-spring-boot' - version = '0.1.0' -} - repositories { mavenCentral() + mavenLocal() } sourceCompatibility = 1.8 @@ -34,19 +28,18 @@ targetCompatibility = 1.8 dependencies { compile "org.springframework.boot:spring-boot-starter-web" + compile "org.springframework.boot:spring-boot-starter-validation" + compile "org.springframework.boot:spring-boot-starter-freemarker" + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" compile "com.fasterxml.jackson.core:jackson-core:2.9.7" compile "com.fasterxml.jackson.core:jackson-databind:2.9.7" compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testImplementation('org.junit.jupiter:junit-jupiter-api') - testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine') - compile "commons-fileupload:commons-fileupload:1.3.1" compile "uk.co.squadlist:squadlist-api-client:0.0.1-SNAPSHOT" compile "com.google.guava:guava:16.0" @@ -54,24 +47,24 @@ dependencies { compile "org.mnode.ical4j:ical4j:1.0.2" compile "oro:oro:2.0.8" compile "org.slf4j:slf4j-api:1.6.1" - compile "org.hibernate:hibernate-validator-annotation-processor:4.1.0.Final" compile "org.codehaus.jackson:jackson-core-asl:1.9.4" compile "org.codehaus.jackson:jackson-mapper-asl:1.9.4" compile "joda-time:joda-time:2.1" - compile "javax.servlet:servlet-api:2.5" - compile "log4j:log4j:1.2.16" compile "uk.co.eelpieconsulting.common:common-dates:1.0.10" - compile "uk.co.eelpieconsulting.common:common-views:1.0.11" compile "jpwgen:jpwgen:1.2.0" compile "commons-cli:commons-cli:1.2" compile "com.restfb:restfb:1.46.0" compile "commons-io:commons-io:2.4" + compile "org.apache.velocity:velocity:1.7" + + compile "org.springframework:spring-aop" + compile "org.aspectj:aspectjweaver:1.7.0" + + compile ('log4j:log4j:1.2.17') - testImplementation('org.junit.jupiter:junit-jupiter-api') - testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine') - testCompile "junit:junit:4.8.2" - testCompile "org.mockito:mockito-core:1.10.8" - testCompile "org.springframework:spring-mock:2.0.8" + testCompile("org.springframework.boot:spring-boot-starter-test") + testCompile("junit:junit:4.12") + testRuntime("org.junit.vintage:junit-vintage-engine:5.3.1") } compileKotlin { diff --git a/build.xml b/build.xml deleted file mode 100644 index ea67090f..00000000 --- a/build.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml deleted file mode 100644 index f0e277d0..00000000 --- a/pom.xml +++ /dev/null @@ -1,229 +0,0 @@ - - 4.0.0 - uk.co.squadlist - squadlist - jar - squadlist - 0.0.1-SNAPSHOT - squadlist - - - org.springframework.boot - spring-boot-starter-parent - 1.2.8.RELEASE - - - - - local-dependencies - file:${basedir}/lib/repository - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - 2.0.2 - - - org.apache.maven.plugins - maven-surefire-plugin - 2.17 - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - - uk.co.squadlist - squadlist-api-client - 0.0.1-SNAPSHOT - - - - com.google.guava - guava - 16.0 - - - - org.apache.commons - commons-csv - 1.0 - - - - org.mnode.ical4j - ical4j - 1.0.2 - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework - spring-context-support - - - - org.apache.velocity - velocity - - - oro - oro - 2.0.8 - - - - org.slf4j - slf4j-api - 1.6.1 - - - - org.hibernate - hibernate-validator-annotation-processor - 4.1.0.Final - - - - org.codehaus.jackson - jackson-core-asl - 1.9.4 - - - org.codehaus.jackson - jackson-mapper-asl - 1.9.4 - - - - joda-time - joda-time - 2.1 - - - - javax.servlet - servlet-api - 2.5 - jar - provided - - - - log4j - log4j - 1.2.16 - - - - org.kohsuke - github-api - 1.59 - - - - uk.co.eelpieconsulting.common - common-dates - 1.0.10 - - - uk.co.eelpieconsulting.common - common-views - 1.0.11 - - - - - jpwgen - jpwgen - 1.2.0 - - - - commons-cli - commons-cli - 1.2 - - - - com.restfb - restfb - 1.46.0 - - - - commons-io - commons-io - 2.4 - - - - - org.springframework - spring-aop - - - org.aspectj - aspectjweaver - 1.7.0 - - - cglib - cglib - 2.2.2 - - - - junit - junit - 4.8.2 - test - - - org.mockito - mockito-core - 1.10.8 - test - - - org.springframework - spring-mock - 2.0.8 - test - - - - org.glassfish.jaxb - jaxb-runtime - 2.3.0 - - - javax.activation - javax.activation-api - 1.2.0 - - - - diff --git a/squadlist.yaml b/squadlist.yaml deleted file mode 100644 index 449f06a0..00000000 --- a/squadlist.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: squadlist -spec: - type: NodePort - selector: - app: squadlist - ports: - - port: 8080 - nodePort: 32200 - name: squadlist ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: squadlist -spec: - replicas: 1 - template: - metadata: - labels: - app: squadlist - spec: - containers: - - name: squadlist - image: localhost:5000/squadlist:1.0 - ports: - - containerPort: 8080 - volumeMounts: - - name: config-volume - mountPath: /opt/squadlist/conf - volumes: - - name: config-volume - configMap: - name: squadlist-config diff --git a/src/main/java/uk/co/squadlist/web/Main.java b/src/main/java/uk/co/squadlist/web/Main.java index 1318a9f3..d6af0f24 100644 --- a/src/main/java/uk/co/squadlist/web/Main.java +++ b/src/main/java/uk/co/squadlist/web/Main.java @@ -1,124 +1,105 @@ package uk.co.squadlist.web; -import com.google.common.collect.Maps; import org.apache.log4j.Logger; -import org.apache.velocity.app.Velocity; -import org.apache.velocity.runtime.RuntimeConstants; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.embedded.FilterRegistrationBean; -import org.springframework.boot.context.embedded.MultipartConfigFactory; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.context.request.RequestContextListener; -import org.springframework.web.filter.CharacterEncodingFilter; -import org.springframework.web.multipart.commons.CommonsMultipartResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.view.velocity.VelocityConfigurer; -import org.springframework.web.servlet.view.velocity.VelocityViewResolver; -import uk.co.eelpieconsulting.common.views.EtagGenerator; -import uk.co.eelpieconsulting.common.views.ViewFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.urls.UrlBuilder; -import uk.co.squadlist.web.views.*; -import javax.servlet.MultipartConfigElement; -import java.io.IOException; -import java.util.Map; - -@ComponentScan(basePackages="uk.co.squadlist.web") +@ComponentScan(basePackages = "uk.co.squadlist.web") @Configuration @EnableWebMvc @EnableScheduling @EnableAutoConfiguration public class Main { - - private final static Logger log = Logger.getLogger(Main.class); - - private static ApplicationContext ctx; - public static void main(String[] args) throws Exception { - ctx = SpringApplication.run(Main.class, args); - } + private final static Logger log = Logger.getLogger(Main.class); + + private static ApplicationContext ctx; + + public static void main(String[] args) throws Exception { + ctx = SpringApplication.run(Main.class, args); + } + + /* + @Bean + public CommonsMultipartResolver multipartResolver(@Value("${maximum.upload}") Long maxUploadSize) throws IOException { + final CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); + multipartResolver.setMaxUploadSize(maxUploadSize); + return multipartResolver; + } + + @Bean + MultipartConfigElement multipartConfigElement() { + MultipartConfigFactory factory = new MultipartConfigFactory(); + factory.setMaxFileSize("10MB"); + factory.setMaxRequestSize("10MB"); + return factory.createMultipartConfig(); + } +*/ - @Bean - public CommonsMultipartResolver multipartResolver(@Value("${maximum.upload}") Long maxUploadSize) throws IOException { - final CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); - multipartResolver.setMaxUploadSize(maxUploadSize); - return multipartResolver; - } - - @Bean - MultipartConfigElement multipartConfigElement() { - MultipartConfigFactory factory = new MultipartConfigFactory(); - factory.setMaxFileSize("10MB"); - factory.setMaxRequestSize("10MB"); - return factory.createMultipartConfig(); - } - - @Bean - public ViewFactory viewFactory() throws IOException { - return new ViewFactory(new EtagGenerator()); - } + /* + @Bean + public FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + registrationBean.setFilter(characterEncodingFilter); + characterEncodingFilter.setEncoding("UTF-8"); + characterEncodingFilter.setForceEncoding(true); + registrationBean.setOrder(Integer.MIN_VALUE); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } + */ - @Bean - public FilterRegistrationBean filterRegistrationBean() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(); - CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); - registrationBean.setFilter(characterEncodingFilter); - characterEncodingFilter.setEncoding("UTF-8"); - characterEncodingFilter.setForceEncoding(true); - registrationBean.setOrder(Integer.MIN_VALUE); - registrationBean.addUrlPatterns("/*"); - return registrationBean; - } + /* + @Bean + public VelocityConfigurer velocityConfigurer() { + final VelocityConfigurer vc = new VelocityConfigurer(); + final Map velocityPropertiesMap = Maps.newHashMap(); + velocityPropertiesMap.put(Velocity.OUTPUT_ENCODING, "UTF-8"); + velocityPropertiesMap.put(Velocity.INPUT_ENCODING, "UTF-8"); + velocityPropertiesMap.put(RuntimeConstants.RESOURCE_LOADER, "classpath"); + velocityPropertiesMap.put("eventhandler.referenceinsertion.class", "org.apache.velocity.app.event.implement.EscapeHtmlReference"); + vc.setVelocityPropertiesMap(velocityPropertiesMap); + return vc; + } - @Bean - public VelocityConfigurer velocityConfigurer() { - final VelocityConfigurer vc = new VelocityConfigurer(); - final Map velocityPropertiesMap = Maps.newHashMap(); - velocityPropertiesMap.put(Velocity.OUTPUT_ENCODING, "UTF-8"); - velocityPropertiesMap.put(Velocity.INPUT_ENCODING, "UTF-8"); - velocityPropertiesMap.put(RuntimeConstants.RESOURCE_LOADER, "classpath"); - velocityPropertiesMap.put("eventhandler.referenceinsertion.class", "org.apache.velocity.app.event.implement.EscapeHtmlReference"); - vc.setVelocityPropertiesMap(velocityPropertiesMap); - return vc; - } + @Bean + public VelocityViewResolver velocityViewResolver(CssHelper cssHelper, DateFormatter dateFormatter, + DateHelper dateHelper, LoggedInUserService loggedInUserService, + PermissionsHelper permissionsHelper, SquadNamesHelper squadNamesHelper, + TextHelper textHelper, + UrlBuilder urlBuilder) { + final VelocityViewResolver viewResolver = new VelocityViewResolver(); + viewResolver.setCache(true); + viewResolver.setPrefix(""); + viewResolver.setSuffix(".vm"); + viewResolver.setContentType("text/html;charset=UTF-8"); - @Bean - public VelocityViewResolver velocityViewResolver(CssHelper cssHelper, DateFormatter dateFormatter, - DateHelper dateHelper, LoggedInUserService loggedInUserService, - PermissionsHelper permissionsHelper, SquadNamesHelper squadNamesHelper, - TextHelper textHelper, - UrlBuilder urlBuilder) { - final VelocityViewResolver viewResolver = new VelocityViewResolver(); - viewResolver.setCache(true); - viewResolver.setPrefix(""); - viewResolver.setSuffix(".vm"); - viewResolver.setContentType("text/html;charset=UTF-8"); - - final Map attributes = Maps.newHashMap(); - attributes.put("cssHelper", cssHelper); - attributes.put("dateFormatter", dateFormatter); - attributes.put("dateHelper", dateHelper); - attributes.put("loggedInUserService", loggedInUserService); - attributes.put("permissionsHelper", permissionsHelper); - attributes.put("squadNamesHelper", squadNamesHelper); - attributes.put("text", textHelper); - attributes.put("urlBuilder", urlBuilder); - viewResolver.setAttributesMap(attributes); - return viewResolver; - } + final Map attributes = Maps.newHashMap(); + attributes.put("cssHelper", cssHelper); + attributes.put("dateFormatter", dateFormatter); + attributes.put("dateHelper", dateHelper); + attributes.put("loggedInUserService", loggedInUserService); + attributes.put("permissionsHelper", permissionsHelper); + attributes.put("squadNamesHelper", squadNamesHelper); + attributes.put("text", textHelper); + attributes.put("urlBuilder", urlBuilder); + viewResolver.setAttributesMap(attributes); + return viewResolver; + } + */ - @Bean - @ConditionalOnMissingBean(RequestContextListener.class) - public RequestContextListener requestContextListener() { - return new RequestContextListener(); - } + /* + @Bean + @ConditionalOnMissingBean(RequestContextListener.class) + public RequestContextListener requestContextListener() { + return new RequestContextListener(); + } + */ } diff --git a/src/main/java/uk/co/squadlist/web/controllers/AdminController.java b/src/main/java/uk/co/squadlist/web/controllers/AdminController.java index b857a185..e773d4c8 100644 --- a/src/main/java/uk/co/squadlist/web/controllers/AdminController.java +++ b/src/main/java/uk/co/squadlist/web/controllers/AdminController.java @@ -23,7 +23,6 @@ import uk.co.squadlist.web.model.forms.InstanceDetails; import uk.co.squadlist.web.services.Permission; import uk.co.squadlist.web.services.filters.ActiveMemberFilter; -import uk.co.squadlist.web.services.github.GithubService; import uk.co.squadlist.web.urls.UrlBuilder; import uk.co.squadlist.web.views.CsvOutputRenderer; import uk.co.squadlist.web.views.DateFormatter; @@ -50,7 +49,6 @@ public class AdminController { private ActiveMemberFilter activeMemberFilter; private CsvOutputRenderer csvOutputRenderer; private UrlBuilder urlBuilder; - private GithubService githubService; private Context context; private DateFormatter dateFormatter; private GoverningBodyFactory governingBodyFactory; @@ -61,14 +59,13 @@ public AdminController() { @Autowired public AdminController(InstanceSpecificApiClient api, ViewFactory viewFactory, ActiveMemberFilter activeMemberFilter, CsvOutputRenderer csvOutputRenderer, - UrlBuilder urlBuilder, GithubService githubService, + UrlBuilder urlBuilder, Context context, DateFormatter dateFormatter, GoverningBodyFactory governingBodyFactory) { this.api = api; this.viewFactory = viewFactory; this.activeMemberFilter = activeMemberFilter; this.csvOutputRenderer = csvOutputRenderer; this.urlBuilder = urlBuilder; - this.githubService = githubService; this.context = context; this.dateFormatter = dateFormatter; this.governingBodyFactory = governingBodyFactory; @@ -90,9 +87,6 @@ public ModelAndView member() throws Exception { mv.addObject("governingBody", governingBodyFactory.getGoverningBody()); mv.addObject("statistics", api.statistics()); mv.addObject("boats", api.getBoats()); - - mv.addObject("openIssues", githubService.getOpenIssues()); - mv.addObject("closedIssues", githubService.getClosedIssues()); mv.addObject("language", context.getLanguage()); return mv; } diff --git a/src/main/java/uk/co/squadlist/web/controllers/ErrorController.java b/src/main/java/uk/co/squadlist/web/controllers/ErrorController.java index 7d899e60..e9682b72 100644 --- a/src/main/java/uk/co/squadlist/web/controllers/ErrorController.java +++ b/src/main/java/uk/co/squadlist/web/controllers/ErrorController.java @@ -8,8 +8,8 @@ import javax.servlet.http.HttpServletRequest; -@Controller -public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController { +//@Controller +public class ErrorController { private final static Logger log = Logger.getLogger(ErrorController.class); @@ -26,7 +26,6 @@ public ModelAndView error() throws Exception { return new ModelAndView("404"); } - @Override public String getErrorPath() { return "/error"; } diff --git a/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java b/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java index 49faea4e..74d317e5 100644 --- a/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java +++ b/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java @@ -1,20 +1,13 @@ package uk.co.squadlist.web.controllers; -import java.io.PrintWriter; -import java.util.Date; -import java.util.List; - -import javax.servlet.http.HttpServletResponse; - +import com.google.common.base.Strings; +import com.google.common.collect.Lists; import net.fortuna.ical4j.model.Calendar; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; - -import uk.co.eelpieconsulting.common.views.EtagGenerator; import uk.co.squadlist.web.annotations.RequiresSignedInMember; import uk.co.squadlist.web.api.InstanceSpecificApiClient; import uk.co.squadlist.web.api.SquadlistApi; @@ -27,121 +20,95 @@ import uk.co.squadlist.web.services.ical.OutingCalendarService; import uk.co.squadlist.web.urls.UrlBuilder; import uk.co.squadlist.web.views.DateHelper; -import uk.co.squadlist.web.views.RssOuting; import uk.co.squadlist.web.views.SquadNamesHelper; import uk.co.squadlist.web.views.ViewFactory; -import com.google.common.base.Strings; -import com.google.common.collect.Lists; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.util.Date; +import java.util.List; @Controller public class MyOutingsController { - - private final LoggedInUserService loggedInUserService; - private final InstanceSpecificApiClient instanceSpecificApiClient; - private final ViewFactory viewFactory; - private final OutingAvailabilityCountsService outingAvailabilityCountsService; - private final UrlBuilder urlBuilder; - private final SquadNamesHelper squadNamesHelper; - private final OutingCalendarService outingCalendarService; - private final SquadlistApiFactory squadlistApiFactory; - private final SquadlistApi squadlistApi; - - @Autowired - public MyOutingsController(LoggedInUserService loggedInUserService, InstanceSpecificApiClient instanceSpecificApiClient, ViewFactory viewFactory, - OutingAvailabilityCountsService outingAvailabilityCountsService, UrlBuilder urlBuilder, - SquadNamesHelper squadNamesHelper, - OutingCalendarService outingCalendarService, SquadlistApiFactory squadlistApiFactory) { - this.loggedInUserService = loggedInUserService; - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.viewFactory = viewFactory; - this.outingAvailabilityCountsService = outingAvailabilityCountsService; - this.urlBuilder = urlBuilder; - this.squadNamesHelper = squadNamesHelper; - this.outingCalendarService = outingCalendarService; - this.squadlistApiFactory = squadlistApiFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequiresSignedInMember - @RequestMapping("/") - public ModelAndView outings() throws Exception { - final String loggedInUser = loggedInUserService.getLoggedInMember().getId(); + + private final LoggedInUserService loggedInUserService; + private final InstanceSpecificApiClient instanceSpecificApiClient; + private final ViewFactory viewFactory; + private final OutingAvailabilityCountsService outingAvailabilityCountsService; + private final UrlBuilder urlBuilder; + private final SquadNamesHelper squadNamesHelper; + private final OutingCalendarService outingCalendarService; + private final SquadlistApiFactory squadlistApiFactory; + private final SquadlistApi squadlistApi; + + @Autowired + public MyOutingsController(LoggedInUserService loggedInUserService, InstanceSpecificApiClient instanceSpecificApiClient, ViewFactory viewFactory, + OutingAvailabilityCountsService outingAvailabilityCountsService, UrlBuilder urlBuilder, + SquadNamesHelper squadNamesHelper, + OutingCalendarService outingCalendarService, SquadlistApiFactory squadlistApiFactory) { + this.loggedInUserService = loggedInUserService; + this.instanceSpecificApiClient = instanceSpecificApiClient; + this.viewFactory = viewFactory; + this.outingAvailabilityCountsService = outingAvailabilityCountsService; + this.urlBuilder = urlBuilder; + this.squadNamesHelper = squadNamesHelper; + this.outingCalendarService = outingCalendarService; + this.squadlistApiFactory = squadlistApiFactory; + this.squadlistApi = squadlistApiFactory.createClient(); + } + + //@RequiresSignedInMember + @RequestMapping("/") + public ModelAndView outings() throws Exception { + final String loggedInUser = loggedInUserService.getLoggedInMember().getId(); SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); final ModelAndView mv = viewFactory.getViewForLoggedInUser("myOutings"); mv.addObject("member", loggedInUserApi.getMember(loggedInUser)); - final Date startDate = DateHelper.startOfCurrentOutingPeriod().toDate(); - final Date endDate = DateHelper.oneYearFromNow().toDate(); + final Date startDate = DateHelper.startOfCurrentOutingPeriod().toDate(); + final Date endDate = DateHelper.oneYearFromNow().toDate(); - mv.addObject("outings", loggedInUserApi.getAvailabilityFor(loggedInUser, startDate, endDate)); + mv.addObject("outings", loggedInUserApi.getAvailabilityFor(loggedInUser, startDate, endDate)); - mv.addObject("title", "My outings"); - mv.addObject("availabilityOptions", instanceSpecificApiClient.getAvailabilityOptions()); - mv.addObject("rssUrl", urlBuilder.outingsRss(loggedInUser, instanceSpecificApiClient.getInstance())); - mv.addObject("icalUrl", urlBuilder.outingsIcal(loggedInUser, instanceSpecificApiClient.getInstance())); - return mv; - } + mv.addObject("title", "My outings"); + mv.addObject("availabilityOptions", instanceSpecificApiClient.getAvailabilityOptions()); + mv.addObject("rssUrl", urlBuilder.outingsRss(loggedInUser, instanceSpecificApiClient.getInstance())); + mv.addObject("icalUrl", urlBuilder.outingsIcal(loggedInUser, instanceSpecificApiClient.getInstance())); + return mv; + } - @RequestMapping("/ical") - public void outingsIcal(@RequestParam(value="user", required=false) String user, HttpServletResponse response) throws Exception { - if (Strings.isNullOrEmpty(user)) { - throw new UnknownMemberException(); - } + @RequestMapping("/ical") + public void outingsIcal(@RequestParam(value = "user", required = false) String user, HttpServletResponse response) throws Exception { + if (Strings.isNullOrEmpty(user)) { + throw new UnknownMemberException(); + } - final Member member = squadlistApi.getMember(user); + final Member member = squadlistApi.getMember(user); - final Calendar calendar = outingCalendarService.buildCalendarFor(squadlistApi.getAvailabilityFor(member.getId(), - DateHelper.startOfCurrentOutingPeriod().toDate(), - DateHelper.oneYearFromNow().toDate()), - instanceSpecificApiClient.getInstance()); + final Calendar calendar = outingCalendarService.buildCalendarFor(squadlistApi.getAvailabilityFor(member.getId(), + DateHelper.startOfCurrentOutingPeriod().toDate(), + DateHelper.oneYearFromNow().toDate()), + instanceSpecificApiClient.getInstance()); - response.setStatus(HttpServletResponse.SC_OK); - response.setContentType("text/calendar"); + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("text/calendar"); - final PrintWriter writer = response.getWriter(); - writer.println(calendar.toString()); - writer.flush(); - } + final PrintWriter writer = response.getWriter(); + writer.println(calendar.toString()); + writer.flush(); + } - @RequestMapping("/rss") - public ModelAndView outingsRss(@RequestParam(value="user", required=false) String user) throws Exception { - if (Strings.isNullOrEmpty(user)) { - throw new UnknownMemberException(); - } - - final Member member = squadlistApi.getMember(user); - - final List availabilityFor = squadlistApi.getAvailabilityFor(member.getId(), - DateHelper.startOfCurrentOutingPeriod().toDate(), - DateHelper.oneYearFromNow().toDate()); - - final String title = instanceSpecificApiClient.getInstance().getName() + " outings"; - final ModelAndView mv = new ModelAndView(new uk.co.eelpieconsulting.common.views.ViewFactory(new EtagGenerator()).getRssView(title, urlBuilder.getBaseUrl(), - squadNamesHelper.list(member.getSquads()) + " outings")); - mv.addObject("data", buildRssItemsFor(availabilityFor)); - return mv; - } - - @RequiresSignedInMember - @RequestMapping("/myoutings/ajax") - public ModelAndView ajax() throws Exception { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("myOutingsAjax"); - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - int pendingOutingsCountFor = outingAvailabilityCountsService.getPendingOutingsCountFor(loggedInUserService.getLoggedInMember().getId(), loggedInUserApi); - if (pendingOutingsCountFor > 0) { - mv.addObject("pendingOutingsCount", pendingOutingsCountFor); - } - return mv; + @RequiresSignedInMember + @RequestMapping("/myoutings/ajax") + public ModelAndView ajax() throws Exception { + final ModelAndView mv = viewFactory.getViewForLoggedInUser("myOutingsAjax"); + SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); + int pendingOutingsCountFor = outingAvailabilityCountsService.getPendingOutingsCountFor(loggedInUserService.getLoggedInMember().getId(), loggedInUserApi); + if (pendingOutingsCountFor > 0) { + mv.addObject("pendingOutingsCount", pendingOutingsCountFor); } + return mv; + } - private List buildRssItemsFor(final List availabilityFor) { - List outings = Lists.newArrayList(); - for (OutingAvailability outingAvailability : availabilityFor) { - outings.add(new RssOuting(outingAvailability.getOuting(), urlBuilder.outingUrl(outingAvailability.getOuting()))); - } - return outings; - } - } diff --git a/src/main/java/uk/co/squadlist/web/model/forms/AvailabilityOptionDetails.java b/src/main/java/uk/co/squadlist/web/model/forms/AvailabilityOptionDetails.java index 53ad2508..0100dd18 100644 --- a/src/main/java/uk/co/squadlist/web/model/forms/AvailabilityOptionDetails.java +++ b/src/main/java/uk/co/squadlist/web/model/forms/AvailabilityOptionDetails.java @@ -3,30 +3,30 @@ import org.hibernate.validator.constraints.NotBlank; public class AvailabilityOptionDetails { - - @NotBlank - private String name, colour; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getColour() { - return colour; - } - - public void setColour(String colour) { - this.colour = colour; - } - - @Override - public String toString() { - return "AvailabilityOptionDetails [colour=" + colour + ", name=" + name - + "]"; - } - + + @NotBlank + private String name, colour; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getColour() { + return colour; + } + + public void setColour(String colour) { + this.colour = colour; + } + + @Override + public String toString() { + return "AvailabilityOptionDetails [colour=" + colour + ", name=" + name + + "]"; + } + } diff --git a/src/main/java/uk/co/squadlist/web/services/github/GithubService.java b/src/main/java/uk/co/squadlist/web/services/github/GithubService.java deleted file mode 100644 index 3b217733..00000000 --- a/src/main/java/uk/co/squadlist/web/services/github/GithubService.java +++ /dev/null @@ -1,77 +0,0 @@ -package uk.co.squadlist.web.services.github; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.apache.log4j.Logger; -import org.kohsuke.github.GHIssue; -import org.kohsuke.github.GHIssueState; -import org.kohsuke.github.GitHub; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import com.google.common.base.Strings; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.Lists; - -@Component -public class GithubService { - - private final static Logger log = Logger.getLogger(GithubService.class); - - private static final String GITHUB_REPO = "eelpie/squadlist"; - - private GitHub github; - - private Cache> cache; - - @Autowired - public GithubService(@Value("${github.username}") String username, @Value("${github.token}") String token) { - this.cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(); - try { - log.info("Github config: " + username + "/" + token); - if (!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(token)) { - this.github = GitHub.connect(username, token); - } else { - log.warn("Github service non configured"); - } - - } catch (IOException e) { - log.error(e); - } - } - - public List getOpenIssues() { - return getIssuesWhichAre(GHIssueState.OPEN); - } - - public List getClosedIssues() { - return getIssuesWhichAre(GHIssueState.CLOSED); - } - - private List getIssuesWhichAre(final GHIssueState state) { - List cached = cache.getIfPresent(state); - if (cached != null) { - log.info("Returning cached for: " + state.toString()); - return cached; - } - - if (github == null) { - return Lists.newArrayList(); - } - try { - log.info("Fetching issues from github: " + state); - List issues = github.getRepository(GITHUB_REPO).getIssues(state); - cache.put(state, issues); - return issues; - - } catch (IOException e) { - log.error(e); - } - return Lists.newArrayList(); - } - -} \ No newline at end of file diff --git a/src/main/java/uk/co/squadlist/web/views/RssOuting.java b/src/main/java/uk/co/squadlist/web/views/RssOuting.java deleted file mode 100644 index aab23877..00000000 --- a/src/main/java/uk/co/squadlist/web/views/RssOuting.java +++ /dev/null @@ -1,54 +0,0 @@ -package uk.co.squadlist.web.views; - -import java.util.Date; - -import uk.co.eelpieconsulting.common.geo.model.LatLong; -import uk.co.eelpieconsulting.common.views.rss.RssFeedable; -import uk.co.squadlist.web.model.Outing; - -public class RssOuting implements RssFeedable { - - private final Outing outing; - private final String url; - - public RssOuting(Outing outing, String url) { - this.outing = outing; - this.url = url; - } - - @Override - public String getAuthor() { - return null; - } - - @Override - public Date getDate() { - return outing.getDate(); - } - - @Override - public String getDescription() { - return outing.getNotes(); - } - - @Override - public String getHeadline() { - return outing.getSquad().getName() + " - " + outing.getDate(); // TODO format - } - - @Override - public String getImageUrl() { - return null; - } - - @Override - public LatLong getLatLong() { - return null; - } - - @Override - public String getWebUrl() { - return url; - } - -} diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 00000000..2bdf460f --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=info, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl new file mode 100644 index 00000000..0d535214 --- /dev/null +++ b/src/main/resources/templates/login.ftl @@ -0,0 +1,101 @@ +#parse('includes/head.vm') + +
+ +
+
+

Squadlist - $instance.name

+
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+ + +
+ +
+
+
+ + +
+
+ +
+
+
+
+ #if($errors) +
$text.text('incorrect.login')
+ #end +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + + +
+
+
+

+
+

$text.text('require.access', $instance.name)

+

+
+

+
+ +
+
+ +
+
+ #parse('data.vm') +
+
+
+
+ +
+ +
+ +#parse('includes/footer.vm') \ No newline at end of file From c09de5877c2f7ae6d292d9b0e31a7778c7f6461b Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 18:15:42 +0000 Subject: [PATCH 03/74] Spike; if we have to change the template engine, does freemarker work with Kotlin collections? Yes. --- .../uk/co/squadlist/web/controllers/Meh.kt | 3 +++ .../web/controllers/MehController.kt | 22 +++++++++++++++++++ src/main/resources/templates/meh.ftl | 8 +++++++ 3 files changed, 33 insertions(+) create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt create mode 100644 src/main/resources/templates/meh.ftl diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt new file mode 100644 index 00000000..6a29f02b --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt @@ -0,0 +1,3 @@ +package uk.co.squadlist.web.controllers + +data class Meh(val id: String, val name: String, val things: List) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt new file mode 100644 index 00000000..cbad456e --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt @@ -0,0 +1,22 @@ +package uk.co.squadlist.web.controllers + +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.servlet.ModelAndView + +@Controller +class MehController { + + private val log = Logger.getLogger(LoginController::class.java) + + @GetMapping(value = "/meh") + fun login(): ModelAndView { + val mv = ModelAndView("meh") + mv.addObject("meh", Meh("21323", "2323", things = listOf("dsjkd", "djsjfk2323"))) + return mv + } + + + +} diff --git a/src/main/resources/templates/meh.ftl b/src/main/resources/templates/meh.ftl new file mode 100644 index 00000000..ee8555f2 --- /dev/null +++ b/src/main/resources/templates/meh.ftl @@ -0,0 +1,8 @@ +${meh} +${meh.id} +${meh.name} + +
+<#list meh.things as x> +
  • ${x}
  • + \ No newline at end of file From c3c8cf951b3d0de40b34fbcfe2181d84addbfc59 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 21:09:17 +0000 Subject: [PATCH 04/74] Template work in progress. --- .../uk/co/squadlist/web/controllers/Meh.kt | 3 --- .../web/controllers/MehController.kt | 22 ------------------- src/main/resources/templates/500.ftl | 1 + .../includes/footer.ftl} | 0 .../head.vm => templates/includes/head.ftl} | 9 ++++---- src/main/resources/templates/login.ftl | 4 ++-- src/main/resources/templates/meh.ftl | 8 ------- 7 files changed, 7 insertions(+), 40 deletions(-) delete mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt delete mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt create mode 100644 src/main/resources/templates/500.ftl rename src/main/resources/{includes/footer.vm => templates/includes/footer.ftl} (100%) rename src/main/resources/{includes/head.vm => templates/includes/head.ftl} (91%) delete mode 100644 src/main/resources/templates/meh.ftl diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt deleted file mode 100644 index 6a29f02b..00000000 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/Meh.kt +++ /dev/null @@ -1,3 +0,0 @@ -package uk.co.squadlist.web.controllers - -data class Meh(val id: String, val name: String, val things: List) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt deleted file mode 100644 index cbad456e..00000000 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/MehController.kt +++ /dev/null @@ -1,22 +0,0 @@ -package uk.co.squadlist.web.controllers - -import org.apache.log4j.Logger -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.servlet.ModelAndView - -@Controller -class MehController { - - private val log = Logger.getLogger(LoginController::class.java) - - @GetMapping(value = "/meh") - fun login(): ModelAndView { - val mv = ModelAndView("meh") - mv.addObject("meh", Meh("21323", "2323", things = listOf("dsjkd", "djsjfk2323"))) - return mv - } - - - -} diff --git a/src/main/resources/templates/500.ftl b/src/main/resources/templates/500.ftl new file mode 100644 index 00000000..17416ae3 --- /dev/null +++ b/src/main/resources/templates/500.ftl @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/src/main/resources/includes/footer.vm b/src/main/resources/templates/includes/footer.ftl similarity index 100% rename from src/main/resources/includes/footer.vm rename to src/main/resources/templates/includes/footer.ftl diff --git a/src/main/resources/includes/head.vm b/src/main/resources/templates/includes/head.ftl similarity index 91% rename from src/main/resources/includes/head.vm rename to src/main/resources/templates/includes/head.ftl index edb0ca4f..ec0143ed 100644 --- a/src/main/resources/includes/head.vm +++ b/src/main/resources/templates/includes/head.ftl @@ -1,4 +1,3 @@ -#parse('macros.vm') @@ -13,11 +12,11 @@ - Squadlist - $!title + Squadlist - ${title!} - #if($rssUrl) - - #end + <#if rssUrl??> + + diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 0d535214..032191aa 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -1,4 +1,4 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'>
    @@ -98,4 +98,4 @@
    -#parse('includes/footer.vm') \ No newline at end of file +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/meh.ftl b/src/main/resources/templates/meh.ftl deleted file mode 100644 index ee8555f2..00000000 --- a/src/main/resources/templates/meh.ftl +++ /dev/null @@ -1,8 +0,0 @@ -${meh} -${meh.id} -${meh.name} - -
    -<#list meh.things as x> -
  • ${x}
  • - \ No newline at end of file From a66b7d84c3d572f53dc6cbc20ffae648d3c70bdf Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 21:36:10 +0000 Subject: [PATCH 05/74] Kotlin conversion. --- .../web/controllers/LoginController.java | 74 ------------------- .../web/controllers/LoginController.kt | 64 ++++++++++++++++ 2 files changed, 64 insertions(+), 74 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/LoginController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/LoginController.java b/src/main/java/uk/co/squadlist/web/controllers/LoginController.java deleted file mode 100644 index 7538fced..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/LoginController.java +++ /dev/null @@ -1,74 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.RedirectView; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.exceptions.UnknownInstanceException; -import uk.co.squadlist.web.model.Instance; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.urls.UrlBuilder; - -@Controller -public class LoginController { - - private final static Logger log = Logger.getLogger(LoginController.class); - - private final InstanceSpecificApiClient api; - private final LoggedInUserService loggedInUserService; - private final UrlBuilder urlBuilder; - - @Autowired - public LoginController(InstanceSpecificApiClient api, LoggedInUserService loggedInUserService, UrlBuilder urlBuilder) { - this.api = api; - this.loggedInUserService = loggedInUserService; - this.urlBuilder = urlBuilder; - } - - @RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.HEAD}) // TODO SEO this onto the root url - public ModelAndView login() throws Exception { - return renderLoginScreen(false, null); - } - - @RequestMapping(value = "/login", method = {RequestMethod.POST}) - public ModelAndView loginSubmit( - @RequestParam(value = "username", required = true) String username, - @RequestParam(value = "password", required = true) String password) throws Exception { - - log.info("Attempting to auth user: " + username); - final String authenticatedUsersAccessToken = api.auth(username, password); - if (authenticatedUsersAccessToken != null) { - Member authenticatedMember = api.verify(authenticatedUsersAccessToken); - if (authenticatedMember != null) { - log.info("Auth successful for user: " + username); - loggedInUserService.setSignedIn(authenticatedUsersAccessToken); - return new ModelAndView(new RedirectView(urlBuilder.getBaseUrl())); - } - } - - return renderLoginScreen(true, username); - } - - @RequestMapping(value = "/logout", method = {RequestMethod.GET}) - public ModelAndView logout() throws Exception { - loggedInUserService.cleanSignedIn(); - return new ModelAndView(new RedirectView(urlBuilder.loginUrl())); - } - - private ModelAndView renderLoginScreen(boolean errors, String username) throws UnknownInstanceException { - final ModelAndView mv = new ModelAndView("login"); - final Instance instance = api.getInstance(); - mv.addObject("title", instance.getName()); - mv.addObject("instance", instance); - mv.addObject("username", username); - mv.addObject("errors", errors); - return mv; - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt new file mode 100644 index 00000000..1a69a8de --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -0,0 +1,64 @@ +package uk.co.squadlist.web.controllers + +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.* +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.view.RedirectView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.model.Member +import uk.co.squadlist.web.urls.UrlBuilder + +@Controller +class LoginController(val api: InstanceSpecificApiClient, val loggedInUserService: LoggedInUserService, val urlBuilder: UrlBuilder) { + + private val log = Logger.getLogger(LoginController::class.java) + + @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url + fun login(): ModelAndView { + return renderLoginScreen(false, null) + } + + @PostMapping(value = "/login") + fun loginSubmit( + @RequestParam(value = "username", required = true) username: String, + @RequestParam(value = "password", required = true) password: String): ModelAndView { + + log.info("Attempting to auth user: $username") + auth(username, password).let { token -> + verify(token!!).let { user -> + log.info("Auth successful for user: " + user!!.username) + loggedInUserService.setSignedIn(token) + return ModelAndView(RedirectView(urlBuilder.baseUrl)) + } + } + + return renderLoginScreen(true, username) + } + + @GetMapping(value = "/logout") + fun logout(): ModelAndView { + loggedInUserService.cleanSignedIn() + return ModelAndView(RedirectView(urlBuilder.loginUrl())) + } + + private fun renderLoginScreen(errors: Boolean, username: String?): ModelAndView { + val instance = api.instance + return ModelAndView("login").addAllObjects(mapOf( + "title" to instance.name, + "username" to username, + "errors" to errors + )) + } + + private fun auth(username: String, password: String): String? { // TODO inline once api is migrated + val auth: String? = api.auth(username, password) + return auth + } + + private fun verify(token: String): Member? { + return api.verify(token) + } + +} From 96c28394d70e4b57122fa749e1887eeceb6e072d Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 21:40:52 +0000 Subject: [PATCH 06/74] Fixes tests. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 70e4f24e..c523fbbb 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,7 @@ dependencies { testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("junit:junit:4.12") testRuntime("org.junit.vintage:junit-vintage-engine:5.3.1") + testCompile 'org.mockito:mockito-core:1.10.8' } compileKotlin { From e17e993f1a728fe92844e70b37fa96e2b78fb1ea Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 21:44:37 +0000 Subject: [PATCH 07/74] Wiring cleverness --- .../uk/co/squadlist/web/controllers/LoginController.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 1a69a8de..e53d872d 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -16,9 +16,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic private val log = Logger.getLogger(LoginController::class.java) @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url - fun login(): ModelAndView { - return renderLoginScreen(false, null) - } + fun login() = ::renderLoginScreen @PostMapping(value = "/login") fun loginSubmit( @@ -43,7 +41,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic return ModelAndView(RedirectView(urlBuilder.loginUrl())) } - private fun renderLoginScreen(errors: Boolean, username: String?): ModelAndView { + private fun renderLoginScreen(errors: Boolean = false, username: String? = null): ModelAndView { val instance = api.instance return ModelAndView("login").addAllObjects(mapOf( "title" to instance.name, From 711588fa726180891e6a62ecfd12b5b976aaa5e6 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Fri, 16 Nov 2018 22:09:41 +0000 Subject: [PATCH 08/74] Kotlin conversion. --- .../web/controllers/BoatsController.java | 32 --------------- .../controllers/GoverningBodyController.java | 41 ------------------- .../web/controllers/BoatsController.kt | 18 ++++++++ .../controllers/GoverningBodyController.kt | 26 ++++++++++++ .../web/controllers/LoginController.kt | 3 +- 5 files changed, 45 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/BoatsController.java delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/GoverningBodyController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/BoatsController.kt create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/BoatsController.java b/src/main/java/uk/co/squadlist/web/controllers/BoatsController.java deleted file mode 100644 index 80b98113..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/BoatsController.java +++ /dev/null @@ -1,32 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.views.ViewFactory; - -@Controller -public class BoatsController { - - private InstanceSpecificApiClient api; - private ViewFactory viewFactory; - - public BoatsController() { - } - - @Autowired - public BoatsController(InstanceSpecificApiClient api, ViewFactory viewFactory) { - this.api = api; - this.viewFactory = viewFactory; - } - - @RequestMapping("/boats/{id}") - public ModelAndView outing(@PathVariable String id) throws Exception { - return viewFactory.getViewForLoggedInUser("boat").addObject("boat", api.getBoat(id)); - } - -} diff --git a/src/main/java/uk/co/squadlist/web/controllers/GoverningBodyController.java b/src/main/java/uk/co/squadlist/web/controllers/GoverningBodyController.java deleted file mode 100644 index a705823b..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/GoverningBodyController.java +++ /dev/null @@ -1,41 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.context.GoverningBodyFactory; -import uk.co.squadlist.web.localisation.GoverningBody; -import uk.co.squadlist.web.views.ViewFactory; - -@Controller -public class GoverningBodyController { - - private GoverningBodyFactory governingBodyFactory; - private ViewFactory viewFactory; - - public GoverningBodyController() { - } - - @Autowired - public GoverningBodyController(GoverningBodyFactory governingBodyFactory, ViewFactory viewFactory) { - this.governingBodyFactory = governingBodyFactory; - this.viewFactory = viewFactory; - } - - @RequestMapping(value="/governing-body/british-rowing", method=RequestMethod.GET) - public ModelAndView member() throws Exception { - final GoverningBody governingBody = governingBodyFactory.getGoverningBody(); - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("governingBody"); - mv.addObject("governingBody", governingBody); - mv.addObject("title", governingBody.getName()); - mv.addObject("ageGrades", governingBody.getAgeGrades()); - mv.addObject("statuses", governingBody.getStatusPoints()); - mv.addObject("boatSizes", governingBody.getBoatSizes()); - return mv; - } - -} \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/BoatsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/BoatsController.kt new file mode 100644 index 00000000..08b1291d --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/BoatsController.kt @@ -0,0 +1,18 @@ +package uk.co.squadlist.web.controllers + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.views.ViewFactory + +@Controller +class BoatsController(val api: InstanceSpecificApiClient, val viewFactory: ViewFactory){ + + @GetMapping("/boats/{id}") + fun outing(@PathVariable id: String): ModelAndView { + return viewFactory.getViewForLoggedInUser("boat").addObject("boat", api.getBoat(id)) + } + +} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt new file mode 100644 index 00000000..91bf14d1 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt @@ -0,0 +1,26 @@ +package uk.co.squadlist.web.controllers + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.context.GoverningBodyFactory +import uk.co.squadlist.web.views.ViewFactory + +@Controller +public class GoverningBodyController(val governingBodyFactory: GoverningBodyFactory, val viewFactory: ViewFactory) { + + @GetMapping(value = "/governing-body/british-rowing") + fun member(): ModelAndView { + val governingBody = governingBodyFactory.governingBody + + return viewFactory.getViewForLoggedInUser("governingBody").addAllObjects( + mapOf("governingBody" to governingBody, + "title" to governingBody.name, + "ageGrades" to governingBody.ageGrades, + "statuses" to governingBody.statusPoints, + "boatSizes" to governingBody.boatSizes) + ) + } + +} + diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index e53d872d..5505ca2c 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -51,8 +51,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic } private fun auth(username: String, password: String): String? { // TODO inline once api is migrated - val auth: String? = api.auth(username, password) - return auth + return api.auth(username, password) } private fun verify(token: String): Member? { From 5ec76c0d8bb734cd9695b8fa5f95199aae839816 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:11:12 +0000 Subject: [PATCH 09/74] url builder works if explictly added. --- .../uk/co/squadlist/web/controllers/LoginController.kt | 5 +++-- src/main/resources/templates/login.ftl | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 5505ca2c..4476c6b9 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -16,7 +16,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic private val log = Logger.getLogger(LoginController::class.java) @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url - fun login() = ::renderLoginScreen + fun login(): ModelAndView = renderLoginScreen() @PostMapping(value = "/login") fun loginSubmit( @@ -46,7 +46,8 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic return ModelAndView("login").addAllObjects(mapOf( "title" to instance.name, "username" to username, - "errors" to errors + "errors" to errors, + "urlBuilder" to urlBuilder )) } diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 032191aa..ea0ac016 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -4,14 +4,14 @@
    -

    Squadlist - $instance.name

    +

    Squadlist - ${instance.name!}


    - +
    -
    +
    From b1e8813506508a4572c8b496addbeca0114b46f2 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:13:12 +0000 Subject: [PATCH 10/74] Correct option like mapping of a null =) --- .../uk/co/squadlist/web/controllers/LoginController.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 4476c6b9..a1f1d4ce 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -24,9 +24,9 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic @RequestParam(value = "password", required = true) password: String): ModelAndView { log.info("Attempting to auth user: $username") - auth(username, password).let { token -> - verify(token!!).let { user -> - log.info("Auth successful for user: " + user!!.username) + auth(username, password)?.let { token -> + verify(token)?.let { user -> + log.info("Auth successful for user: " + user.username) loggedInUserService.setSignedIn(token) return ModelAndView(RedirectView(urlBuilder.baseUrl)) } From 59b10a5304bc674e13973165850f3fe9a805bb4f Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:14:15 +0000 Subject: [PATCH 11/74] Yes this cleverness is allowed. --- .../kotlin/uk/co/squadlist/web/controllers/LoginController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index a1f1d4ce..4e7714ec 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -16,7 +16,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic private val log = Logger.getLogger(LoginController::class.java) @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url - fun login(): ModelAndView = renderLoginScreen() + fun login() = ::renderLoginScreen @PostMapping(value = "/login") fun loginSubmit( From 7f1769aeb2a91e53fe22d639b790d9c799ba0914 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:23:53 +0000 Subject: [PATCH 12/74] Urls and ifs. --- .../uk/co/squadlist/web/controllers/LoginController.kt | 2 +- src/main/resources/templates/login.ftl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 4e7714ec..26430cbe 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -16,7 +16,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic private val log = Logger.getLogger(LoginController::class.java) @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url - fun login() = ::renderLoginScreen + fun login() = renderLoginScreen() @PostMapping(value = "/login") fun loginSubmit( diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index ea0ac016..88dfe0fb 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -15,7 +15,7 @@
    - +
    @@ -23,7 +23,7 @@
    @@ -39,9 +39,9 @@
    - #if($errors) + <#if errors>
    $text.text('incorrect.login')
    - #end +
    @@ -62,7 +62,7 @@
    From 39b17a7e60e813f2ee9caa7047f9371e8e91d51f Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:30:35 +0000 Subject: [PATCH 13/74] Port my outings for template fixing. --- .../web/controllers/MyOutingsController.java | 114 ------------------ .../web/controllers/MyOutingsController.kt | 81 +++++++++++++ .../{myOutings.vm => templates/myOutings.ftl} | 0 3 files changed, 81 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt rename src/main/resources/{myOutings.vm => templates/myOutings.ftl} (100%) diff --git a/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java b/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java deleted file mode 100644 index 74d317e5..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/MyOutingsController.java +++ /dev/null @@ -1,114 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import net.fortuna.ical4j.model.Calendar; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; -import uk.co.squadlist.web.annotations.RequiresSignedInMember; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.exceptions.UnknownMemberException; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.OutingAvailability; -import uk.co.squadlist.web.services.OutingAvailabilityCountsService; -import uk.co.squadlist.web.services.ical.OutingCalendarService; -import uk.co.squadlist.web.urls.UrlBuilder; -import uk.co.squadlist.web.views.DateHelper; -import uk.co.squadlist.web.views.SquadNamesHelper; -import uk.co.squadlist.web.views.ViewFactory; - -import javax.servlet.http.HttpServletResponse; -import java.io.PrintWriter; -import java.util.Date; -import java.util.List; - -@Controller -public class MyOutingsController { - - private final LoggedInUserService loggedInUserService; - private final InstanceSpecificApiClient instanceSpecificApiClient; - private final ViewFactory viewFactory; - private final OutingAvailabilityCountsService outingAvailabilityCountsService; - private final UrlBuilder urlBuilder; - private final SquadNamesHelper squadNamesHelper; - private final OutingCalendarService outingCalendarService; - private final SquadlistApiFactory squadlistApiFactory; - private final SquadlistApi squadlistApi; - - @Autowired - public MyOutingsController(LoggedInUserService loggedInUserService, InstanceSpecificApiClient instanceSpecificApiClient, ViewFactory viewFactory, - OutingAvailabilityCountsService outingAvailabilityCountsService, UrlBuilder urlBuilder, - SquadNamesHelper squadNamesHelper, - OutingCalendarService outingCalendarService, SquadlistApiFactory squadlistApiFactory) { - this.loggedInUserService = loggedInUserService; - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.viewFactory = viewFactory; - this.outingAvailabilityCountsService = outingAvailabilityCountsService; - this.urlBuilder = urlBuilder; - this.squadNamesHelper = squadNamesHelper; - this.outingCalendarService = outingCalendarService; - this.squadlistApiFactory = squadlistApiFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - //@RequiresSignedInMember - @RequestMapping("/") - public ModelAndView outings() throws Exception { - final String loggedInUser = loggedInUserService.getLoggedInMember().getId(); - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("myOutings"); - mv.addObject("member", loggedInUserApi.getMember(loggedInUser)); - - final Date startDate = DateHelper.startOfCurrentOutingPeriod().toDate(); - final Date endDate = DateHelper.oneYearFromNow().toDate(); - - mv.addObject("outings", loggedInUserApi.getAvailabilityFor(loggedInUser, startDate, endDate)); - - mv.addObject("title", "My outings"); - mv.addObject("availabilityOptions", instanceSpecificApiClient.getAvailabilityOptions()); - mv.addObject("rssUrl", urlBuilder.outingsRss(loggedInUser, instanceSpecificApiClient.getInstance())); - mv.addObject("icalUrl", urlBuilder.outingsIcal(loggedInUser, instanceSpecificApiClient.getInstance())); - return mv; - } - - @RequestMapping("/ical") - public void outingsIcal(@RequestParam(value = "user", required = false) String user, HttpServletResponse response) throws Exception { - if (Strings.isNullOrEmpty(user)) { - throw new UnknownMemberException(); - } - - final Member member = squadlistApi.getMember(user); - - final Calendar calendar = outingCalendarService.buildCalendarFor(squadlistApi.getAvailabilityFor(member.getId(), - DateHelper.startOfCurrentOutingPeriod().toDate(), - DateHelper.oneYearFromNow().toDate()), - instanceSpecificApiClient.getInstance()); - - response.setStatus(HttpServletResponse.SC_OK); - response.setContentType("text/calendar"); - - final PrintWriter writer = response.getWriter(); - writer.println(calendar.toString()); - writer.flush(); - } - - @RequiresSignedInMember - @RequestMapping("/myoutings/ajax") - public ModelAndView ajax() throws Exception { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("myOutingsAjax"); - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - int pendingOutingsCountFor = outingAvailabilityCountsService.getPendingOutingsCountFor(loggedInUserService.getLoggedInMember().getId(), loggedInUserApi); - if (pendingOutingsCountFor > 0) { - mv.addObject("pendingOutingsCount", pendingOutingsCountFor); - } - return mv; - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt new file mode 100644 index 00000000..b9362391 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt @@ -0,0 +1,81 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Strings +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.annotations.RequiresSignedInMember +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.exceptions.UnknownMemberException +import uk.co.squadlist.web.services.OutingAvailabilityCountsService +import uk.co.squadlist.web.services.ical.OutingCalendarService +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.DateHelper +import uk.co.squadlist.web.views.ViewFactory +import javax.servlet.http.HttpServletResponse + +@Controller +class MyOutingsController(val loggedInUserService: LoggedInUserService, val instanceSpecificApiClient: InstanceSpecificApiClient, val viewFactory: ViewFactory, + val outingAvailabilityCountsService: OutingAvailabilityCountsService, private val urlBuilder: UrlBuilder, + val outingCalendarService: OutingCalendarService, val squadlistApiFactory: SquadlistApiFactory) { + + private val squadlistApi = squadlistApiFactory.createClient() + + @RequiresSignedInMember + @RequestMapping("/") + fun outings(): ModelAndView { + val loggedInUser = loggedInUserService.loggedInMember.id + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val mv = viewFactory.getViewForLoggedInUser("myOutings") + mv.addObject("member", loggedInUserApi.getMember(loggedInUser)) + + val startDate = DateHelper.startOfCurrentOutingPeriod().toDate() + val endDate = DateHelper.oneYearFromNow().toDate() + + mv.addObject("outings", loggedInUserApi.getAvailabilityFor(loggedInUser, startDate, endDate)) + + mv.addObject("title", "My outings") + mv.addObject("availabilityOptions", instanceSpecificApiClient.availabilityOptions) + mv.addObject("rssUrl", urlBuilder.outingsRss(loggedInUser, instanceSpecificApiClient.instance)) + mv.addObject("icalUrl", urlBuilder.outingsIcal(loggedInUser, instanceSpecificApiClient.instance)) + return mv + } + + @RequestMapping("/ical") + fun outingsIcal(@RequestParam(value = "user", required = false) user: String, response: HttpServletResponse) { + if (Strings.isNullOrEmpty(user)) { + throw UnknownMemberException() + } + + val member = squadlistApi.getMember(user) + + val calendar = outingCalendarService.buildCalendarFor(squadlistApi.getAvailabilityFor(member.id, + DateHelper.startOfCurrentOutingPeriod().toDate(), + DateHelper.oneYearFromNow().toDate()), + instanceSpecificApiClient.instance) + + response.status = HttpServletResponse.SC_OK + response.contentType = "text/calendar" + + val writer = response.writer + writer.println(calendar.toString()) + writer.flush() + } + + @RequiresSignedInMember + @RequestMapping("/myoutings/ajax") + fun ajax(): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("myOutingsAjax") + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + val pendingOutingsCountFor = outingAvailabilityCountsService.getPendingOutingsCountFor(loggedInUserService.loggedInMember.id, loggedInUserApi) + if (pendingOutingsCountFor > 0) { + mv.addObject("pendingOutingsCount", pendingOutingsCountFor) + } + return mv + } + +} diff --git a/src/main/resources/myOutings.vm b/src/main/resources/templates/myOutings.ftl similarity index 100% rename from src/main/resources/myOutings.vm rename to src/main/resources/templates/myOutings.ftl From e92f660444e16a90c84f8b64802434290fe63fd6 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:33:12 +0000 Subject: [PATCH 14/74] Templating. --- src/main/resources/templates/myOutings.ftl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 33a3c557..69c70be3 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -1,4 +1,4 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'> #navTabs('My outings') @@ -11,7 +11,7 @@
    - iCal + iCal
    @@ -119,4 +119,4 @@ }); -#parse('includes/footer.vm') \ No newline at end of file +<#include 'includes/footer.ftl'> From d236f364cc50c4371a143731432e1e5bdcb372b9 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:38:22 +0000 Subject: [PATCH 15/74] Ifs. --- src/main/resources/templates/myOutings.ftl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 69c70be3..7e710498 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -17,13 +17,13 @@
    - #if($member.squads.isEmpty()) + <#if member.squads.isEmpty()>
    $text.text('not.assigned.to.any.squads')

    $text.text('no.outing.because.not.assigned.to.any.squads')

    - #else + <#else> - #if($outings.isEmpty()) + <#if outings.isEmpty>
    $text.text('no.current.outings')
    @@ -31,7 +31,7 @@
    - #else + <#else>
    @@ -79,8 +79,8 @@
    - #end - #end + +
    From 4632c5178e2de4a46ce2116cf454e9a980386904 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 09:48:10 +0000 Subject: [PATCH 16/74] List --- src/main/resources/templates/myOutings.ftl | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 7e710498..7c5c9c6c 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -16,45 +16,45 @@
    - + <#if member.squads.isEmpty()>
    $text.text('not.assigned.to.any.squads')

    $text.text('no.outing.because.not.assigned.to.any.squads')

    <#else> - <#if outings.isEmpty> + <#if outings.isEmpty()>
    -
    +
    $text.text('no.current.outings')
    -

    $text.text('no.current.outings.for.your.squads')

    +

    $text.text('no.current.outings.for.your.squads')

    - + <#else>
    -
    +
    - +

    $squadNamesHelper.list($member.squads) outings.

    - + - #foreach($outingAvailability in $outings) + <#list outings as outingAvailability> - - + - - #end +
    + #set($outing = $outingAvailability.outing) - #parse('includes/outing.vm') - + #parse('includes/outing.vm') + #if($outing.closed) #parse('includes/closedOuting.vm') - - #else + + #else #end - #if ($outingAvailability.availabilityOption) + + #if ($outingAvailability.availabilityOption) #set($availability = $outingAvailability.availabilityOption) - #parse('includes/availability.vm') + #parse('includes/availability.vm') - + #else TBA - #end + #end
    -
    +
    From b90cab6d09bdb53e21816ff983207c97a5e479b8 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 19:02:13 +0000 Subject: [PATCH 17/74] Correct is empty asserts. --- src/main/resources/templates/myOutings.ftl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 7c5c9c6c..42dfc6b6 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -7,7 +7,7 @@
    -

    $title

    +

    ${title!}

    @@ -17,13 +17,13 @@

    - <#if member.squads.isEmpty()> + <#if member.squads?size == 0>
    $text.text('not.assigned.to.any.squads')

    $text.text('no.outing.because.not.assigned.to.any.squads')

    <#else> - <#if outings.isEmpty()> + <#if outings?size == 0>
    $text.text('no.current.outings')
    From b085e352c1bef2fb512c44f5d4c3cf3bed9899f3 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 19:10:45 +0000 Subject: [PATCH 18/74] Port. --- .../web/controllers/ContactsController.java | 60 ------------------- .../web/controllers/ContactsController.kt | 39 ++++++++++++ 2 files changed, 39 insertions(+), 60 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/ContactsController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/ContactsController.java b/src/main/java/uk/co/squadlist/web/controllers/ContactsController.java deleted file mode 100644 index bb07dbde..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/ContactsController.java +++ /dev/null @@ -1,60 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.util.List; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.services.PreferedSquadService; -import uk.co.squadlist.web.views.ViewFactory; - -@Controller -public class ContactsController { - - private static Logger log = Logger.getLogger(ContactsController.class); - - private InstanceSpecificApiClient api; - private PreferedSquadService preferedSquadService; - private ViewFactory viewFactory; - private ContactsModelPopulator contactsModelPopulator; - - public ContactsController() { - } - - @Autowired - public ContactsController(InstanceSpecificApiClient api, PreferedSquadService preferedSquadService, ViewFactory viewFactory, ContactsModelPopulator contactsModelPopulator) { - this.api = api; - this.preferedSquadService = preferedSquadService; - this.viewFactory = viewFactory; - this.contactsModelPopulator = contactsModelPopulator; - } - - @RequestMapping("/contacts") - public ModelAndView contacts() throws Exception { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("contacts"); - final List allSquads = api.getSquads(); - mv.addObject("squads", allSquads); // TODO leaves squad null on view - return mv; - } - - @RequestMapping("/contacts/{squadId}") - public ModelAndView squadContacts(@PathVariable String squadId) throws Exception { - final Squad squadToShow = preferedSquadService.resolveSquad(squadId); - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("contacts"); - final List allSquads = api.getSquads(); - mv.addObject("squads", allSquads); - if (!allSquads.isEmpty()) { - log.info("Squad to show: " + squadToShow); - contactsModelPopulator.populateModel(squadToShow, mv); - } - return mv; - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt new file mode 100644 index 00000000..503c5083 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt @@ -0,0 +1,39 @@ +package uk.co.squadlist.web.controllers + +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.views.ViewFactory + +@Controller +class ContactsController(val api: InstanceSpecificApiClient, val preferedSquadService: PreferedSquadService, val viewFactory: ViewFactory, val contactsModelPopulator: ContactsModelPopulator) { + + private val log = Logger.getLogger(ContactsController::class.java) + + @GetMapping("/contacts") + fun contacts(): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("contacts") + val allSquads = api.squads + mv.addObject("squads", allSquads) // TODO leaves squad null on view + return mv + } + + @GetMapping("/contacts/{squadId}") + fun squadContacts(@PathVariable squadId: String): ModelAndView { + val squadToShow = preferedSquadService.resolveSquad(squadId) + + val mv = viewFactory.getViewForLoggedInUser("contacts") + val allSquads = api.squads + mv.addObject("squads", allSquads) + if (!allSquads.isEmpty()) { + log.info("Squad to show: $squadToShow") + contactsModelPopulator.populateModel(squadToShow, mv) + } + return mv + } + +} From 592cbf619db156a038a2dd734f5cdf73311734ab Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 19:13:14 +0000 Subject: [PATCH 19/74] Port. --- .../controllers/ResetPasswordController.java | 74 ------------------- .../controllers/ResetPasswordController.kt | 66 +++++++++++++++++ 2 files changed, 66 insertions(+), 74 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/ResetPasswordController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/ResetPasswordController.java b/src/main/java/uk/co/squadlist/web/controllers/ResetPasswordController.java deleted file mode 100644 index a7d8aad6..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/ResetPasswordController.java +++ /dev/null @@ -1,74 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.exceptions.UnknownMemberException; - -import com.google.common.base.Strings; - -@Controller -public class ResetPasswordController { - - private final static Logger log = Logger.getLogger(ResetPasswordController.class); - - private final InstanceSpecificApiClient api; - - @Autowired - public ResetPasswordController(InstanceSpecificApiClient api) { - this.api = api; - } - - @RequestMapping(value = "/reset-password", method = RequestMethod.GET) - public ModelAndView resetPasswordPrompt() throws Exception { - final ModelAndView mv = new ModelAndView("resetPassword"); - mv.addObject("title", "Reset password"); - return mv; - } - - @RequestMapping(value = "/reset-password", method = RequestMethod.POST) - public ModelAndView resetPassword(@RequestParam(value = "username", required = false) String username) throws Exception { - if (Strings.isNullOrEmpty(username)) { - final ModelAndView mv = new ModelAndView("resetPassword"); - mv.addObject("errors", true); - mv.addObject("title", "Reset password"); - return mv; - } - - log.info("Reseting password for: " + username); - try { - api.resetPassword(username); // TODO errors - log.info("Reset password call successful for: " + username); - final ModelAndView mv = new ModelAndView("resetPasswordSent"); - mv.addObject("title", "Reset password"); - return mv; - - } catch (UnknownMemberException e) { - final ModelAndView mv = new ModelAndView("resetPassword"); - mv.addObject("title", "Reset password"); - mv.addObject("errors", true); - return mv; - } - } - - @RequestMapping(value = "/reset-password/confirm", method = RequestMethod.GET) - public ModelAndView confirmPasswordReset(@RequestParam String token) throws Exception { - try { - final String newPassword = api.confirmResetPassword(token); - - final ModelAndView mv = new ModelAndView("resetPasswordConfirm"); - mv.addObject("newPassword", newPassword); - return mv; - - } catch (Exception e) { - return new ModelAndView("resetPasswordInvalidToken"); - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt new file mode 100644 index 00000000..eb167d1e --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt @@ -0,0 +1,66 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Strings +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.exceptions.UnknownMemberException + +@Controller +class ResetPasswordController(val api: InstanceSpecificApiClient) { + + private val log = Logger.getLogger(ResetPasswordController::class.java) + + @GetMapping(value = "/reset-password") + fun resetPasswordPrompt(): ModelAndView { + val mv = ModelAndView("resetPassword") + mv.addObject("title", "Reset password") + return mv + } + + @PostMapping(value = "/reset-password") + fun resetPassword(@RequestParam(value = "username", required = false) username: String): ModelAndView { + if (Strings.isNullOrEmpty(username)) { + val mv = ModelAndView("resetPassword") + mv.addObject("errors", true) + mv.addObject("title", "Reset password") + return mv + } + + log.info("Reseting password for: $username") + try { + api.resetPassword(username) // TODO errors + log.info("Reset password call successful for: $username") + val mv = ModelAndView("resetPasswordSent") + mv.addObject("title", "Reset password") + return mv + + } catch (e: UnknownMemberException) { + val mv = ModelAndView("resetPassword") + mv.addObject("title", "Reset password") + mv.addObject("errors", true) + return mv + } + + } + + @GetMapping(value = "/reset-password/confirm") + fun confirmPasswordReset(@RequestParam token: String): ModelAndView { + try { + val newPassword = api.confirmResetPassword(token) + + val mv = ModelAndView("resetPasswordConfirm") + mv.addObject("newPassword", newPassword) + return mv + + } catch (e: Exception) { + return ModelAndView("resetPasswordInvalidToken") + } + + } + +} \ No newline at end of file From ddbebd3457472fa14c9d593218a9108ad59cbcb7 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 19:14:40 +0000 Subject: [PATCH 20/74] Setting up to port the whole reset password flow; simplest set of API calls to spike dropping the API client jar for native calls. --- src/main/resources/{ => templates}/resetPassword.vm | 0 src/main/resources/{ => templates}/resetPasswordConfirm.vm | 0 src/main/resources/{ => templates}/resetPasswordInvalidToken.vm | 0 src/main/resources/{ => templates}/resetPasswordSent.vm | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/{ => templates}/resetPassword.vm (100%) rename src/main/resources/{ => templates}/resetPasswordConfirm.vm (100%) rename src/main/resources/{ => templates}/resetPasswordInvalidToken.vm (100%) rename src/main/resources/{ => templates}/resetPasswordSent.vm (100%) diff --git a/src/main/resources/resetPassword.vm b/src/main/resources/templates/resetPassword.vm similarity index 100% rename from src/main/resources/resetPassword.vm rename to src/main/resources/templates/resetPassword.vm diff --git a/src/main/resources/resetPasswordConfirm.vm b/src/main/resources/templates/resetPasswordConfirm.vm similarity index 100% rename from src/main/resources/resetPasswordConfirm.vm rename to src/main/resources/templates/resetPasswordConfirm.vm diff --git a/src/main/resources/resetPasswordInvalidToken.vm b/src/main/resources/templates/resetPasswordInvalidToken.vm similarity index 100% rename from src/main/resources/resetPasswordInvalidToken.vm rename to src/main/resources/templates/resetPasswordInvalidToken.vm diff --git a/src/main/resources/resetPasswordSent.vm b/src/main/resources/templates/resetPasswordSent.vm similarity index 100% rename from src/main/resources/resetPasswordSent.vm rename to src/main/resources/templates/resetPasswordSent.vm From ffa1f470bbb640364186cfde80e049bae105c255 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 17 Nov 2018 22:08:11 +0000 Subject: [PATCH 21/74] Convert templates. --- src/main/resources/templates/resetPassword.vm | 6 +++--- src/main/resources/templates/resetPasswordConfirm.vm | 8 ++++---- .../resources/templates/resetPasswordInvalidToken.vm | 8 ++++---- src/main/resources/templates/resetPasswordSent.vm | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/resources/templates/resetPassword.vm b/src/main/resources/templates/resetPassword.vm index 79ee092a..6b0e97ef 100644 --- a/src/main/resources/templates/resetPassword.vm +++ b/src/main/resources/templates/resetPassword.vm @@ -1,10 +1,10 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'>
    -

    Squadlist - $instance.name

    +

    Squadlist - ${instance.name}


    @@ -48,4 +48,4 @@
    -#parse('includes/footer.vm') \ No newline at end of file +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/resetPasswordConfirm.vm b/src/main/resources/templates/resetPasswordConfirm.vm index dcd23eac..688af682 100644 --- a/src/main/resources/templates/resetPasswordConfirm.vm +++ b/src/main/resources/templates/resetPasswordConfirm.vm @@ -1,10 +1,10 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'>
    -

    Squadlist - $instance.name

    +

    Squadlist - ${instance.name}


    @@ -19,10 +19,10 @@
    -#parse('includes/footer.vm') +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/resetPasswordInvalidToken.vm b/src/main/resources/templates/resetPasswordInvalidToken.vm index cc8b8d30..2ad9fc51 100644 --- a/src/main/resources/templates/resetPasswordInvalidToken.vm +++ b/src/main/resources/templates/resetPasswordInvalidToken.vm @@ -1,10 +1,10 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'>
    -

    Squadlist - $instance.name

    +

    Squadlist - ${instance.name}


    @@ -17,10 +17,10 @@
    -#parse('includes/footer.vm') \ No newline at end of file +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/resetPasswordSent.vm b/src/main/resources/templates/resetPasswordSent.vm index d82385aa..24e4edec 100644 --- a/src/main/resources/templates/resetPasswordSent.vm +++ b/src/main/resources/templates/resetPasswordSent.vm @@ -1,17 +1,17 @@ -#parse('includes/head.vm') +<#include 'includes/head.ftl'>
    -

    Squadlist - $instance.name

    +

    Squadlist - ${instance.name}


    -

    Reseting your password

    +

    $text.text('resetting.your.password')

    An email containing password reset instructions has been sent to your email address.

    It should arrive within a few minutes. @@ -21,10 +21,10 @@


    -#parse('includes/footer.vm') \ No newline at end of file +<#include 'includes/footer.ftl'> \ No newline at end of file From 2a09bde77adae922ebe9b1b7a267ba8e79415486 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Sat, 24 Nov 2018 22:10:39 +0000 Subject: [PATCH 22/74] Convert templates. --- .../resources/templates/{resetPassword.vm => resetPassword.ftl} | 0 .../{resetPasswordConfirm.vm => resetPasswordConfirm.ftl} | 0 ...resetPasswordInvalidToken.vm => resetPasswordInvalidToken.ftl} | 0 .../templates/{resetPasswordSent.vm => resetPasswordSent.ftl} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/templates/{resetPassword.vm => resetPassword.ftl} (100%) rename src/main/resources/templates/{resetPasswordConfirm.vm => resetPasswordConfirm.ftl} (100%) rename src/main/resources/templates/{resetPasswordInvalidToken.vm => resetPasswordInvalidToken.ftl} (100%) rename src/main/resources/templates/{resetPasswordSent.vm => resetPasswordSent.ftl} (100%) diff --git a/src/main/resources/templates/resetPassword.vm b/src/main/resources/templates/resetPassword.ftl similarity index 100% rename from src/main/resources/templates/resetPassword.vm rename to src/main/resources/templates/resetPassword.ftl diff --git a/src/main/resources/templates/resetPasswordConfirm.vm b/src/main/resources/templates/resetPasswordConfirm.ftl similarity index 100% rename from src/main/resources/templates/resetPasswordConfirm.vm rename to src/main/resources/templates/resetPasswordConfirm.ftl diff --git a/src/main/resources/templates/resetPasswordInvalidToken.vm b/src/main/resources/templates/resetPasswordInvalidToken.ftl similarity index 100% rename from src/main/resources/templates/resetPasswordInvalidToken.vm rename to src/main/resources/templates/resetPasswordInvalidToken.ftl diff --git a/src/main/resources/templates/resetPasswordSent.vm b/src/main/resources/templates/resetPasswordSent.ftl similarity index 100% rename from src/main/resources/templates/resetPasswordSent.vm rename to src/main/resources/templates/resetPasswordSent.ftl From f8304cd059e7724668f634b08a5f90cb71174a70 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 08:01:23 +0000 Subject: [PATCH 23/74] Reset password screen. --- .../java/uk/co/squadlist/web/urls/UrlBuilder.java | 2 ++ .../web/controllers/ResetPasswordController.kt | 12 ++++++++++-- src/main/resources/templates/resetPassword.ftl | 8 ++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java b/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java index 56103d73..52aa2673 100644 --- a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java +++ b/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java @@ -43,6 +43,8 @@ public String loginUrl() { return applicationUrl("/login"); } + public String resetPassword() { return applicationUrl("/reset-password"); } + public String staticUrl(String uri) { return applicationUrl("/assets/") + uri; } diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt index eb167d1e..c8eb224c 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt @@ -9,9 +9,10 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.servlet.ModelAndView import uk.co.squadlist.web.api.InstanceSpecificApiClient import uk.co.squadlist.web.exceptions.UnknownMemberException +import uk.co.squadlist.web.urls.UrlBuilder @Controller -class ResetPasswordController(val api: InstanceSpecificApiClient) { +class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder: UrlBuilder) { private val log = Logger.getLogger(ResetPasswordController::class.java) @@ -19,6 +20,8 @@ class ResetPasswordController(val api: InstanceSpecificApiClient) { fun resetPasswordPrompt(): ModelAndView { val mv = ModelAndView("resetPassword") mv.addObject("title", "Reset password") + mv.addObject("errors", false) + mv.addObject("urlBuilder", urlBuilder) return mv } @@ -28,6 +31,7 @@ class ResetPasswordController(val api: InstanceSpecificApiClient) { val mv = ModelAndView("resetPassword") mv.addObject("errors", true) mv.addObject("title", "Reset password") + mv.addObject("urlBuilder", urlBuilder) return mv } @@ -37,12 +41,14 @@ class ResetPasswordController(val api: InstanceSpecificApiClient) { log.info("Reset password call successful for: $username") val mv = ModelAndView("resetPasswordSent") mv.addObject("title", "Reset password") + mv.addObject("urlBuilder", urlBuilder) return mv } catch (e: UnknownMemberException) { val mv = ModelAndView("resetPassword") mv.addObject("title", "Reset password") mv.addObject("errors", true) + mv.addObject("urlBuilder", urlBuilder) return mv } @@ -55,10 +61,12 @@ class ResetPasswordController(val api: InstanceSpecificApiClient) { val mv = ModelAndView("resetPasswordConfirm") mv.addObject("newPassword", newPassword) + mv.addObject("urlBuilder", urlBuilder) return mv } catch (e: Exception) { - return ModelAndView("resetPasswordInvalidToken") + return ModelAndView("resetPasswordInvalidToken"). + addObject("urlBuilder", urlBuilder) } } diff --git a/src/main/resources/templates/resetPassword.ftl b/src/main/resources/templates/resetPassword.ftl index 6b0e97ef..2767a0cd 100644 --- a/src/main/resources/templates/resetPassword.ftl +++ b/src/main/resources/templates/resetPassword.ftl @@ -18,7 +18,7 @@
    -
    +
    @@ -29,10 +29,10 @@
    -
    - #if($errors) +
    + <#if errors>
    $text.text('unknown.username.or.email.address')
    - #end +
    From 5d46fd20f3926019a25944a1ceaccd5de097fc3e Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 11:06:06 +0000 Subject: [PATCH 24/74] How to user spring macro to get at the localisation values. --- .../{en.properties => messages.properties} | 0 .../resources/templates/includes/head.ftl | 1 + src/main/resources/templates/login.ftl | 19 +++++++++---------- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/main/resources/{en.properties => messages.properties} (100%) diff --git a/src/main/resources/en.properties b/src/main/resources/messages.properties similarity index 100% rename from src/main/resources/en.properties rename to src/main/resources/messages.properties diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index ec0143ed..8a8580e9 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -1,3 +1,4 @@ +<#import "/spring.ftl" as spring/> diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 88dfe0fb..9b729a76 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -8,22 +8,21 @@

    -
    - +
    - + " value="${username!}">
    - +
    @@ -40,7 +39,7 @@
    <#if errors> -
    $text.text('incorrect.login')
    +
    <@spring.message "incorrect.login"/>
    @@ -49,7 +48,7 @@
    - + " class="btn btn-primary" />
    @@ -62,7 +61,7 @@
    @@ -70,7 +69,7 @@

    -
    +

    $text.text('require.access', $instance.name)


    From c4dae86fa7083f7724da7f66f42a60252747dd13 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 11:21:40 +0000 Subject: [PATCH 25/74] Place holder example. --- src/main/resources/messages.properties | 2 +- src/main/resources/templates/login.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 89e17ad6..b30981d3 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -4,7 +4,7 @@ forgotten.your.password=Forgotten your password? login=Login incorrect.login=Incorrect username or password login.with.linked.facebook.account=Or sign in using a previously linked Facebook account -require.access=If you are a {} member who requires access to this copy of Squadlist please contact your coach. +require.access=If you are a {0} member who requires access to this copy of Squadlist please contact your coach. my.outings=My outings my.details=My details diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 9b729a76..5e282473 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -70,7 +70,7 @@


    -

    $text.text('require.access', $instance.name)

    +

    <@spring.messageText "require.access", ["${instance.name}"] />


    From 6f8285f8a0d8d4df5eb9d19acfe9ebf17aabc017 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:13:22 +0000 Subject: [PATCH 26/74] Analytics. --- .../analytics.vm => templates/includes/analytics.ftl} | 9 +++++---- src/main/resources/templates/includes/footer.ftl | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/main/resources/{includes/analytics.vm => templates/includes/analytics.ftl} (71%) diff --git a/src/main/resources/includes/analytics.vm b/src/main/resources/templates/includes/analytics.ftl similarity index 71% rename from src/main/resources/includes/analytics.vm rename to src/main/resources/templates/includes/analytics.ftl index f5d6baad..736b569a 100644 --- a/src/main/resources/includes/analytics.vm +++ b/src/main/resources/templates/includes/analytics.ftl @@ -1,12 +1,13 @@ -#if($googleAnalyticsAccount) +<#if googleAnalyticsAccount??> -#end + + diff --git a/src/main/resources/templates/includes/footer.ftl b/src/main/resources/templates/includes/footer.ftl index 09bc863f..205c9616 100644 --- a/src/main/resources/templates/includes/footer.ftl +++ b/src/main/resources/templates/includes/footer.ftl @@ -1,4 +1,3 @@ -#parse('includes/analytics.vm') - + <#include 'analytics.ftl'> From 9aa29cb7ff9a016d2168fd2ddb9601645dd6e5d5 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:13:41 +0000 Subject: [PATCH 27/74] Data policy. --- src/main/resources/{data.vm => templates/data.ftl} | 0 src/main/resources/templates/login.ftl | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/main/resources/{data.vm => templates/data.ftl} (100%) diff --git a/src/main/resources/data.vm b/src/main/resources/templates/data.ftl similarity index 100% rename from src/main/resources/data.vm rename to src/main/resources/templates/data.ftl diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index 5e282473..70ca87f1 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -87,7 +87,7 @@
    - #parse('data.vm') + <#include 'data.ftl'>
    From 8b4d1ca7a6214282aaa0ba7a6975a36ac16e1ea5 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:18:01 +0000 Subject: [PATCH 28/74] Text. --- src/main/resources/templates/resetPassword.ftl | 12 ++++++------ .../resources/templates/resetPasswordConfirm.ftl | 6 +++--- .../templates/resetPasswordInvalidToken.ftl | 2 +- src/main/resources/templates/resetPasswordSent.ftl | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/resources/templates/resetPassword.ftl b/src/main/resources/templates/resetPassword.ftl index 2767a0cd..e386e2c5 100644 --- a/src/main/resources/templates/resetPassword.ftl +++ b/src/main/resources/templates/resetPassword.ftl @@ -11,8 +11,8 @@
    -

    $text.text('resetting.your.password')

    -

    $text.text('reset.password.instructions')

    +

    <@spring.message 'resetting.your.password' />

    +

    <@spring.message 'reset.password.instructions' />

    @@ -20,9 +20,9 @@
    - +
    - +
    @@ -31,14 +31,14 @@
    <#if errors> -
    $text.text('unknown.username.or.email.address')
    +
    <@spring.message 'unknown.username.or.email.address' />
    - + #cancel($urlBuilder.loginUrl())
    diff --git a/src/main/resources/templates/resetPasswordConfirm.ftl b/src/main/resources/templates/resetPasswordConfirm.ftl index 688af682..b2bb650d 100644 --- a/src/main/resources/templates/resetPasswordConfirm.ftl +++ b/src/main/resources/templates/resetPasswordConfirm.ftl @@ -11,15 +11,15 @@
    -

    $text.text('resetting.your.password')

    +

    <@spring.message 'resetting.your.password' />

    -

    $text.text('your.new.password.is') $newPassword

    +

    <@spring.message 'your.new.password.is' />$newPassword


    diff --git a/src/main/resources/templates/resetPasswordInvalidToken.ftl b/src/main/resources/templates/resetPasswordInvalidToken.ftl index 2ad9fc51..800686c9 100644 --- a/src/main/resources/templates/resetPasswordInvalidToken.ftl +++ b/src/main/resources/templates/resetPasswordInvalidToken.ftl @@ -17,7 +17,7 @@
    diff --git a/src/main/resources/templates/resetPasswordSent.ftl b/src/main/resources/templates/resetPasswordSent.ftl index 24e4edec..6b5a0b66 100644 --- a/src/main/resources/templates/resetPasswordSent.ftl +++ b/src/main/resources/templates/resetPasswordSent.ftl @@ -11,7 +11,7 @@
    -

    $text.text('resetting.your.password')

    +

    <@spring.message 'resetting.your.password' />

    An email containing password reset instructions has been sent to your email address.

    It should arrive within a few minutes. @@ -21,7 +21,7 @@


    From 3f8d0d311a75c959d5946df2f595fe28d459bd1a Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:19:17 +0000 Subject: [PATCH 29/74] Text. --- src/main/resources/templates/myOutings.ftl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 42dfc6b6..bd056fcb 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -18,16 +18,16 @@
    <#if member.squads?size == 0> -
    $text.text('not.assigned.to.any.squads')
    -

    $text.text('no.outing.because.not.assigned.to.any.squads')

    +
    <@spring.message 'not.assigned.to.any.squads' />
    +

    <@spring.message 'no.outing.because.not.assigned.to.any.squads' />

    <#else> <#if outings?size == 0>
    -
    $text.text('no.current.outings')
    -

    $text.text('no.current.outings.for.your.squads')

    +
    <@spring.message 'no.current.outings' />
    +

    <@spring.message 'no.current.outings.for.your.squads' />

    From 5fa90e663042b0f92907ecabdc276bc411306603 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:21:26 +0000 Subject: [PATCH 30/74] URL. --- src/main/resources/templates/myOutings.ftl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index bd056fcb..208e6b1a 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -88,34 +88,34 @@ $('.availabilityDropdown').change(function() { outingId = $(this).attr('id'); newAvailability = $(this).val(); - + $("#currentAvailability" + outingId).html($('#spinner').html()) - - updateUrl = "$urlBuilder.applicationUrl('/availability/ajax')"; + + updateUrl = "${urlBuilder.applicationUrl('/availability/ajax')}"; postData = { outing: outingId, availability: newAvailability }; redrawPendingOutingBadge = function(data) {$("#pendingOutings").html(data)}; pendingOutingsCountUrl = '$urlBuilder.applicationUrl('/myoutings/ajax')'; - + redrawCurrentOutingAvailabilityAndPendingOutingBadge = function(data) { - $("#currentAvailability" + outingId).html(data); + $("#currentAvailability" + outingId).html(data); $.post(pendingOutingsCountUrl, {}, redrawPendingOutingBadge); }; - + $.ajax( { url: updateUrl, type: 'POST', data: postData, - dataType: 'text', - success: function(data, status, xhr) { + dataType: 'text', + success: function(data, status, xhr) { redrawCurrentOutingAvailabilityAndPendingOutingBadge(data); }, error: function(xhr, status, error) { - $("#currentAvailability" + outingId).html("Error: could not be updated"); + $("#currentAvailability" + outingId).html("Error: could not be updated"); } } ); - + }); From afef749ee7ca67be84bc9ea40f41f5e232a6fe0c Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:25:36 +0000 Subject: [PATCH 31/74] Port cancel macro. --- src/main/resources/macros.vm | 4 ---- src/main/resources/templates/includes/head.ftl | 3 +++ src/main/resources/templates/resetPassword.ftl | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/resources/macros.vm b/src/main/resources/macros.vm index bedbbfec..80968019 100644 --- a/src/main/resources/macros.vm +++ b/src/main/resources/macros.vm @@ -160,7 +160,3 @@
    #end #end - -#macro(cancel $action) - -#end \ No newline at end of file diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index 8a8580e9..8c28b22c 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -21,3 +21,6 @@ +<#macro cancel to> + + diff --git a/src/main/resources/templates/resetPassword.ftl b/src/main/resources/templates/resetPassword.ftl index e386e2c5..2640248e 100644 --- a/src/main/resources/templates/resetPassword.ftl +++ b/src/main/resources/templates/resetPassword.ftl @@ -39,7 +39,7 @@
    - #cancel($urlBuilder.loginUrl()) + <@cancel urlBuilder.loginUrl() />
    From 2249324f1aad720bdae9684b0d5cd58cd57a9bca Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 12:59:41 +0000 Subject: [PATCH 32/74] Ported nav macro. --- .../uk/co/squadlist/web/urls/UrlBuilder.java | 4 ++ .../web/controllers/MyOutingsController.kt | 5 +- src/main/resources/macros.vm | 52 ------------------- src/main/resources/templates/TODO.vm | 1 - .../resources/templates/includes/head.ftl | 51 ++++++++++++++++++ src/main/resources/templates/myOutings.ftl | 4 +- 6 files changed, 61 insertions(+), 56 deletions(-) delete mode 100644 src/main/resources/templates/TODO.vm diff --git a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java b/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java index 52aa2673..8993f9a7 100644 --- a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java +++ b/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java @@ -124,6 +124,10 @@ public String outingEditUrl(Outing outing) { return outingUrl(outing) + "/edit"; } + public String entryDetails(Squad squad) { + return applicationUrl("/entry-details?squad=" + squad.getId()); + } + public String outings(Squad squad) { return applicationUrl("/outings?squad=" + squad.getId()); } diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt index b9362391..3b3b2e9f 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MyOutingsController.kt @@ -14,13 +14,14 @@ import uk.co.squadlist.web.services.OutingAvailabilityCountsService import uk.co.squadlist.web.services.ical.OutingCalendarService import uk.co.squadlist.web.urls.UrlBuilder import uk.co.squadlist.web.views.DateHelper +import uk.co.squadlist.web.views.PermissionsHelper import uk.co.squadlist.web.views.ViewFactory import javax.servlet.http.HttpServletResponse @Controller class MyOutingsController(val loggedInUserService: LoggedInUserService, val instanceSpecificApiClient: InstanceSpecificApiClient, val viewFactory: ViewFactory, val outingAvailabilityCountsService: OutingAvailabilityCountsService, private val urlBuilder: UrlBuilder, - val outingCalendarService: OutingCalendarService, val squadlistApiFactory: SquadlistApiFactory) { + val outingCalendarService: OutingCalendarService, val squadlistApiFactory: SquadlistApiFactory, val permissionsHelper: PermissionsHelper) { private val squadlistApi = squadlistApiFactory.createClient() @@ -42,6 +43,8 @@ class MyOutingsController(val loggedInUserService: LoggedInUserService, val inst mv.addObject("availabilityOptions", instanceSpecificApiClient.availabilityOptions) mv.addObject("rssUrl", urlBuilder.outingsRss(loggedInUser, instanceSpecificApiClient.instance)) mv.addObject("icalUrl", urlBuilder.outingsIcal(loggedInUser, instanceSpecificApiClient.instance)) + mv.addObject("permissionsHelper", permissionsHelper) + mv.addObject("urlBuilder", urlBuilder) return mv } diff --git a/src/main/resources/macros.vm b/src/main/resources/macros.vm index 80968019..f5357acf 100644 --- a/src/main/resources/macros.vm +++ b/src/main/resources/macros.vm @@ -26,58 +26,6 @@ #end -#macro(navTabs $selectedTab) - -#end - #macro(inputRow $field $label) #springBind($field)
    diff --git a/src/main/resources/templates/TODO.vm b/src/main/resources/templates/TODO.vm deleted file mode 100644 index e2efa2b9..00000000 --- a/src/main/resources/templates/TODO.vm +++ /dev/null @@ -1 +0,0 @@ -Disable template path check \ No newline at end of file diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index 8c28b22c..a0667539 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -24,3 +24,54 @@ <#macro cancel to> + +<#macro navTabs selectedTab> + + \ No newline at end of file diff --git a/src/main/resources/templates/myOutings.ftl b/src/main/resources/templates/myOutings.ftl index 208e6b1a..bb4bef65 100644 --- a/src/main/resources/templates/myOutings.ftl +++ b/src/main/resources/templates/myOutings.ftl @@ -1,5 +1,5 @@ <#include 'includes/head.ftl'> -#navTabs('My outings') +<@navTabs 'My outings' /> @@ -94,7 +94,7 @@ updateUrl = "${urlBuilder.applicationUrl('/availability/ajax')}"; postData = { outing: outingId, availability: newAvailability }; redrawPendingOutingBadge = function(data) {$("#pendingOutings").html(data)}; - pendingOutingsCountUrl = '$urlBuilder.applicationUrl('/myoutings/ajax')'; + pendingOutingsCountUrl = '${urlBuilder.applicationUrl('/myoutings/ajax')}'; redrawCurrentOutingAvailabilityAndPendingOutingBadge = function(data) { $("#currentAvailability" + outingId).html(data); From 2cd2d782501469727dfb703f1c30716aef7cedd3 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 13:06:25 +0000 Subject: [PATCH 33/74] Fixes. --- src/main/resources/templates/includes/head.ftl | 6 +++--- src/main/resources/templates/resetPasswordConfirm.ftl | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index a0667539..f2aa4152 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -65,9 +65,9 @@ diff --git a/src/main/resources/templates/resetPasswordConfirm.ftl b/src/main/resources/templates/resetPasswordConfirm.ftl index b2bb650d..7e754351 100644 --- a/src/main/resources/templates/resetPasswordConfirm.ftl +++ b/src/main/resources/templates/resetPasswordConfirm.ftl @@ -12,8 +12,7 @@

    <@spring.message 'resetting.your.password' />

    - -

    <@spring.message 'your.new.password.is' />$newPassword

    +

    <@spring.message 'your.new.password.is' />${newPassword}

    From 538d52cd1feb13cedb9f9cbe0156e4d0ef3cb326 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 13:51:04 +0000 Subject: [PATCH 34/74] Inline ? functions. --- .../co/squadlist/web/controllers/LoginController.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 26430cbe..78b7f0be 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -7,7 +7,6 @@ import org.springframework.web.servlet.ModelAndView import org.springframework.web.servlet.view.RedirectView import uk.co.squadlist.web.api.InstanceSpecificApiClient import uk.co.squadlist.web.auth.LoggedInUserService -import uk.co.squadlist.web.model.Member import uk.co.squadlist.web.urls.UrlBuilder @Controller @@ -24,8 +23,8 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic @RequestParam(value = "password", required = true) password: String): ModelAndView { log.info("Attempting to auth user: $username") - auth(username, password)?.let { token -> - verify(token)?.let { user -> + api.auth(username, password)?.let { token -> + api.verify(token)?.let { user -> log.info("Auth successful for user: " + user.username) loggedInUserService.setSignedIn(token) return ModelAndView(RedirectView(urlBuilder.baseUrl)) @@ -51,12 +50,4 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic )) } - private fun auth(username: String, password: String): String? { // TODO inline once api is migrated - return api.auth(username, password) - } - - private fun verify(token: String): Member? { - return api.verify(token) - } - } From b405e8259a329556107263426563f0da7153d029 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 16:31:27 +0000 Subject: [PATCH 35/74] Change password moves. --- build.gradle | 4 ++ .../web/controllers/MembersController.java | 31 ---------- .../controllers/ChangePasswordController.kt | 61 +++++++++++++++++++ src/main/resources/changePassword.vm | 60 ------------------ .../resources/templates/changePassword.ftl | 58 ++++++++++++++++++ .../resources/templates/includes/head.ftl | 2 +- 6 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt delete mode 100644 src/main/resources/changePassword.vm create mode 100644 src/main/resources/templates/changePassword.ftl diff --git a/build.gradle b/build.gradle index c523fbbb..e8294392 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { ext.kotlin_version = '1.2.41' repositories { + jcenter() mavenCentral() } dependencies { @@ -66,6 +67,9 @@ dependencies { testCompile("junit:junit:4.12") testRuntime("org.junit.vintage:junit-vintage-engine:5.3.1") testCompile 'org.mockito:mockito-core:1.10.8' + + compile "org.springframework.boot:spring-boot-devtools" + } compileKotlin { diff --git a/src/main/java/uk/co/squadlist/web/controllers/MembersController.java b/src/main/java/uk/co/squadlist/web/controllers/MembersController.java index a3d2a338..84d59289 100644 --- a/src/main/java/uk/co/squadlist/web/controllers/MembersController.java +++ b/src/main/java/uk/co/squadlist/web/controllers/MembersController.java @@ -133,29 +133,6 @@ public ModelAndView newMemberSubmit(@Valid @ModelAttribute("memberDetails") Memb } } - @RequestMapping(value = "/change-password", method = RequestMethod.GET) - public ModelAndView changePassword() throws Exception { - return renderChangePasswordForm(new ChangePassword()); - } - - @RequestMapping(value = "/change-password", method = RequestMethod.POST) - public ModelAndView editMember(@Valid @ModelAttribute("changePassword") ChangePassword changePassword, BindingResult result) throws Exception { - if (result.hasErrors()) { - return renderChangePasswordForm(changePassword); - } - - final Member member = loggedInUserService.getLoggedInMember(); - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - log.info("Requesting change password for member: " + member.getId()); - if (loggedInUserApi.changePassword(member.getId(), changePassword.getCurrentPassword(), changePassword.getNewPassword())) { - return new ModelAndView(new RedirectView(urlBuilder.memberUrl(member))); - } else { - result.addError(new ObjectError("changePassword", "Change password failed")); - return renderChangePasswordForm(changePassword); - } - } - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) @RequestMapping(value = "/member/{id}/edit", method = RequestMethod.GET) public ModelAndView updateMember(@PathVariable String id, @RequestParam(required = false) Boolean invalidImage) throws Exception { @@ -399,14 +376,6 @@ private ModelAndView renderEditMemberDetailsForm(MemberDetails memberDetails, St return mv; } - private ModelAndView renderChangePasswordForm(ChangePassword changePassword) throws UnknownMemberException, SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("changePassword"); - mv.addObject("member", loggedInUserService.getLoggedInMember()); - mv.addObject("changePassword", changePassword); - mv.addObject("title", "Change password"); - return mv; - } - private List extractAndValidateRequestedSquads(MemberDetails memberDetails, BindingResult result) { final List squads = Lists.newArrayList(); if (memberDetails.getSquads() == null) { diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt new file mode 100644 index 00000000..e588562b --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt @@ -0,0 +1,61 @@ +package uk.co.squadlist.web.controllers + +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.validation.BindingResult +import org.springframework.validation.ObjectError +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.view.RedirectView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.model.forms.ChangePassword +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.PermissionsHelper +import uk.co.squadlist.web.views.ViewFactory +import javax.validation.Valid + +@Controller +class ChangePasswordController(val api: InstanceSpecificApiClient, val urlBuilder: UrlBuilder, val viewFactory: ViewFactory, + val loggedInUserService: LoggedInUserService, val squadlistApiFactory: SquadlistApiFactory, val permissionsHelper: PermissionsHelper) { + + private val log = Logger.getLogger(ChangePasswordController::class.java) + + @GetMapping(value = "/change-password") + fun changePassword(): ModelAndView { + return renderChangePasswordForm(ChangePassword()) + } + + @PostMapping(value = "/change-password") + fun changePasswordSubmit(@Valid @ModelAttribute("changePassword") changePassword: ChangePassword, result: BindingResult): ModelAndView { + if (result.hasErrors()) { + return renderChangePasswordForm(changePassword) + } + + val member = loggedInUserService.getLoggedInMember() + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()) + + log.info("Requesting change password for member: " + member.getId()) + if (loggedInUserApi.changePassword(member.getId(), changePassword.currentPassword, changePassword.newPassword)) { + return ModelAndView(RedirectView(urlBuilder.memberUrl(member))) + } else { + result.addError(ObjectError("changePassword", "Change password failed")) + return renderChangePasswordForm(changePassword) + } + } + + + private fun renderChangePasswordForm(changePassword: ChangePassword): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("changePassword") + mv.addObject("member", loggedInUserService.getLoggedInMember()) + mv.addObject("changePassword", changePassword) + mv.addObject("title", "Change password") + mv.addObject("urlBuilder", urlBuilder) + mv.addObject("permissionsHelper", permissionsHelper) + return mv + } + +} \ No newline at end of file diff --git a/src/main/resources/changePassword.vm b/src/main/resources/changePassword.vm deleted file mode 100644 index 6024380b..00000000 --- a/src/main/resources/changePassword.vm +++ /dev/null @@ -1,60 +0,0 @@ -#parse('includes/head.vm') -#navTabs('Outings') - -
    - -
    -
    -

    $text.text('change.password')

    -
    -
    -
    - -
    - -
    - - #springBind("changePassword") - #foreach($error in $status.errorMessages) -
    $error
    - #end - -
    - - #springBind("changePassword.currentPassword") -
    - -
    - - #foreach($error in $status.errorMessages) -
    $error
    - #end -
    -
    - - #springBind("changePassword.newPassword") -
    - -
    - - #foreach($error in $status.errorMessages) -
    $error
    - #end -
    -
    - -
    -
    -
    -
    - - #cancel($urlBuilder.memberUrl($member)) -
    -
    -
    -
    -
    -
    -
    - -#parse('includes/footer.vm') diff --git a/src/main/resources/templates/changePassword.ftl b/src/main/resources/templates/changePassword.ftl new file mode 100644 index 00000000..e74b6d1c --- /dev/null +++ b/src/main/resources/templates/changePassword.ftl @@ -0,0 +1,58 @@ +<#include 'includes/head.ftl'> +<@navTabs '' /> + +
    + +
    +
    +

    <@spring.message 'change.password' />

    +
    +
    +
    + +
    +
    + <@spring.bind "changePassword" /> + + <#list spring.status.errorMessages as error> +
    ${error}
    + + +
    + <@spring.bind "changePassword.currentPassword" /> +
    + +
    + + <#list spring.status.errorMessages as error> +
    ${error}
    + +
    +
    + + <@spring.bind "changePassword.newPassword" /> +
    + +
    + + <#list spring.status.errorMessages as error> +
    ${error}
    + +
    +
    + +
    +
    +
    +
    + + <@cancel urlBuilder.memberUrl(member) /> +
    +
    +
    +
    +
    +
    +
    + +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index f2aa4152..dbf23a34 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -4,7 +4,7 @@ - + From cab7b49e731bc720ae92c89c8cfdd2f6f6ab3b11 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 18:05:18 +0000 Subject: [PATCH 36/74] Deduplicate. --- .../controllers/ResetPasswordController.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt index c8eb224c..f484ad7c 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt @@ -18,24 +18,16 @@ class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder @GetMapping(value = "/reset-password") fun resetPasswordPrompt(): ModelAndView { - val mv = ModelAndView("resetPassword") - mv.addObject("title", "Reset password") - mv.addObject("errors", false) - mv.addObject("urlBuilder", urlBuilder) - return mv + return resetPasswordScreen().addObject("errors", false) } @PostMapping(value = "/reset-password") fun resetPassword(@RequestParam(value = "username", required = false) username: String): ModelAndView { if (Strings.isNullOrEmpty(username)) { - val mv = ModelAndView("resetPassword") - mv.addObject("errors", true) - mv.addObject("title", "Reset password") - mv.addObject("urlBuilder", urlBuilder) - return mv + return resetPasswordScreen().addObject("errors", true) } - log.info("Reseting password for: $username") + log.info("Resetting password for: $username") try { api.resetPassword(username) // TODO errors log.info("Reset password call successful for: $username") @@ -45,13 +37,8 @@ class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder return mv } catch (e: UnknownMemberException) { - val mv = ModelAndView("resetPassword") - mv.addObject("title", "Reset password") - mv.addObject("errors", true) - mv.addObject("urlBuilder", urlBuilder) - return mv + return resetPasswordScreen().addObject("errors", true) } - } @GetMapping(value = "/reset-password/confirm") @@ -71,4 +58,11 @@ class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder } + private fun resetPasswordScreen(): ModelAndView { + val mv = ModelAndView("resetPassword") + mv.addObject("title", "Reset password") + mv.addObject("urlBuilder", urlBuilder) + return mv + } + } \ No newline at end of file From 712491264c3f469f6f9d9e2d10ef24940b61efbc Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 18:18:24 +0000 Subject: [PATCH 37/74] Move to Kotlin. --- .../web/controllers/MembersController.java | 424 ------------------ .../web/controllers/MemberController.kt | 390 ++++++++++++++++ 2 files changed, 390 insertions(+), 424 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/MembersController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/MembersController.java b/src/main/java/uk/co/squadlist/web/controllers/MembersController.java deleted file mode 100644 index 84d59289..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/MembersController.java +++ /dev/null @@ -1,424 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.IllegalFieldValueException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartHttpServletRequest; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.RedirectView; -import uk.co.squadlist.web.annotations.RequiresMemberPermission; -import uk.co.squadlist.web.annotations.RequiresPermission; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.context.GoverningBodyFactory; -import uk.co.squadlist.web.exceptions.*; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.model.forms.ChangePassword; -import uk.co.squadlist.web.model.forms.MemberDetails; -import uk.co.squadlist.web.model.forms.MemberSquad; -import uk.co.squadlist.web.services.PasswordGenerator; -import uk.co.squadlist.web.services.Permission; -import uk.co.squadlist.web.services.PermissionsService; -import uk.co.squadlist.web.urls.UrlBuilder; -import uk.co.squadlist.web.views.ViewFactory; - -import javax.validation.Valid; -import java.io.IOException; -import java.util.Date; -import java.util.List; - -@Controller -public class MembersController { - - private final static Logger log = Logger.getLogger(MembersController.class); - - private static final List GENDER_OPTIONS = Lists.newArrayList("female", "male"); - private static final List ROLES_OPTIONS = Lists.newArrayList("Rower", "Rep", "Coach", "Cox", "Non rowing"); - private static final List SWEEP_OAR_SIDE_OPTIONS = Lists.newArrayList("Bow", "Stroke", "Bow/Stroke", "Stroke/Bow"); - private static final List YES_NO_OPTIONS = Lists.newArrayList("Y", "N"); - - private InstanceSpecificApiClient instanceSpecificApiClient; - private LoggedInUserService loggedInUserService; - private UrlBuilder urlBuilder; - private ViewFactory viewFactory; - private PasswordGenerator passwordGenerator; - private PermissionsService permissionsService; - private GoverningBodyFactory governingBodyFactory; - private SquadlistApiFactory squadlistApiFactory; - private SquadlistApi squadlistApi; - - public MembersController() { - } - - @Autowired - public MembersController(InstanceSpecificApiClient instanceSpecificApiClient, LoggedInUserService loggedInUserService, UrlBuilder urlBuilder, - ViewFactory viewFactory, - PasswordGenerator passwordGenerator, - PermissionsService permissionsService, - GoverningBodyFactory governingBodyFactory, - SquadlistApiFactory squadlistApiFactory) { - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.loggedInUserService = loggedInUserService; - this.urlBuilder = urlBuilder; - this.viewFactory = viewFactory; - this.passwordGenerator = passwordGenerator; - this.permissionsService = permissionsService; - this.governingBodyFactory = governingBodyFactory; - this.squadlistApiFactory = squadlistApiFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequiresMemberPermission(permission = Permission.VIEW_MEMBER_DETAILS) - @RequestMapping("/member/{id}") - public ModelAndView member(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member members = loggedInUserApi.getMember(id); - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("memberDetails"); - mv.addObject("member", members); - mv.addObject("title", members.getFirstName() + " " + members.getLastName()); - mv.addObject("governingBody", governingBodyFactory.getGoverningBody()); - return mv; - } - - @RequiresPermission(permission = Permission.ADD_MEMBER) - @RequestMapping(value = "/member/new", method = RequestMethod.GET) - public ModelAndView newMember(@ModelAttribute("memberDetails") MemberDetails memberDetails) throws Exception { - return renderNewMemberForm(); - } - - @RequiresPermission(permission = Permission.ADD_MEMBER) - @RequestMapping(value = "/member/new", method = RequestMethod.POST) - public ModelAndView newMemberSubmit(@Valid @ModelAttribute("memberDetails") MemberDetails memberDetails, BindingResult result) throws Exception { - final List requestedSquads = extractAndValidateRequestedSquads(memberDetails, result); - - if (result.hasErrors()) { - log.info("New member submission has errors: " + result.getAllErrors()); - return renderNewMemberForm(); - } - - final String initialPassword = passwordGenerator.generateRandomPassword(10); - - try { - final Member newMember = instanceSpecificApiClient.createMember(memberDetails.getFirstName(), - memberDetails.getLastName(), - requestedSquads, - memberDetails.getEmailAddress(), - initialPassword, - null, - memberDetails.getRole() - ); - - return viewFactory.getViewForLoggedInUser("memberAdded"). - addObject("member", newMember). - addObject("initialPassword", initialPassword); - - } catch (InvalidMemberException e) { - log.warn("Invalid member exception: " + e.getMessage()); - result.addError(new ObjectError("memberDetails", e.getMessage())); - return renderNewMemberForm(); - } - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/edit", method = RequestMethod.GET) - public ModelAndView updateMember(@PathVariable String id, @RequestParam(required = false) Boolean invalidImage) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - - final MemberDetails memberDetails = new MemberDetails(); - memberDetails.setFirstName(member.getFirstName()); - memberDetails.setLastName(member.getLastName()); - memberDetails.setKnownAs(member.getKnownAs()); - memberDetails.setGender(member.getGender()); - - memberDetails.setDateOfBirthDay(member.getDateOfBirth() != null ? new DateTime(member.getDateOfBirth()).getDayOfMonth() : null); - memberDetails.setDateOfBirthMonth(member.getDateOfBirth() != null ? new DateTime(member.getDateOfBirth()).getMonthOfYear() : null); - memberDetails.setDateOfBirthYear(member.getDateOfBirth() != null ? new DateTime(member.getDateOfBirth()).getYear() : null); - - memberDetails.setWeight(member.getWeight() != null ? Integer.toString(member.getWeight()) : null); - memberDetails.setEmailAddress(member.getEmailAddress()); - memberDetails.setContactNumber(member.getContactNumber()); - memberDetails.setRegistrationNumber(member.getRegistrationNumber()); - memberDetails.setRowingPoints(member.getRowingPoints()); - memberDetails.setSculling(member.getSculling()); - memberDetails.setScullingPoints(member.getScullingPoints()); - memberDetails.setSweepOarSide(member.getSweepOarSide()); - - memberDetails.setPostcode(member.getAddress() != null ? member.getAddress().get("postcode") : null); - - List memberSquads = Lists.newArrayList(); - for (Squad squad : member.getSquads()) { - memberSquads.add(new MemberSquad(squad.getId())); - } - - memberDetails.setSquads(memberSquads); - memberDetails.setEmergencyContactName(member.getEmergencyContactName()); - memberDetails.setEmergencyContactNumber(member.getEmergencyContactNumber()); - memberDetails.setRole(member.getRole()); - memberDetails.setProfileImage(member.getProfileImage()); - - ModelAndView mv = renderEditMemberDetailsForm(memberDetails, member.getId(), member.getFirstName() + " " + member.getLastName(), member); - mv.addObject("invalidImage", invalidImage); - return mv; - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/edit", method = RequestMethod.POST) - public ModelAndView updateMemberSubmit(@PathVariable String id, @Valid @ModelAttribute("member") MemberDetails memberDetails, BindingResult result) throws Exception { - log.info("Received edit member request: " + memberDetails); - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - - final List squads = extractAndValidateRequestedSquads(memberDetails, result); - if (!Strings.isNullOrEmpty(memberDetails.getScullingPoints())) { - if (!governingBodyFactory.getGoverningBody().getPointsOptions().contains(memberDetails.getScullingPoints())) { - result.addError(new ObjectError("member.scullingPoints", "Invalid points option")); - } - } - if (!Strings.isNullOrEmpty(memberDetails.getRowingPoints())) { - if (!governingBodyFactory.getGoverningBody().getPointsOptions().contains(memberDetails.getRowingPoints())) { - result.addError(new ObjectError("member.rowingPoints", "Invalid points option")); - } - } - - Date updatedDateOfBirth = null; - if (memberDetails.getDateOfBirthDay() != null && - memberDetails.getDateOfBirthMonth() != null && - memberDetails.getDateOfBirthYear() != null) { - try { - updatedDateOfBirth = new DateTime(memberDetails.getDateOfBirthYear(), - memberDetails.getDateOfBirthMonth(), memberDetails.getDateOfBirthDay(), 0, 0, 0, - DateTimeZone.UTC).toDate(); - member.setDateOfBirth(updatedDateOfBirth); - } catch (IllegalFieldValueException e) { - result.addError(new ObjectError("member.dateOfBirthYear", "Invalid date")); - } - } - - if (result.hasErrors()) { - return renderEditMemberDetailsForm(memberDetails, member.getId(), member.getFirstName() + " " + member.getLastName(), member); - } - - log.info("Updating member details: " + member.getId()); - member.setFirstName(memberDetails.getFirstName()); - member.setLastName(memberDetails.getLastName()); - member.setKnownAs(memberDetails.getKnownAs()); - member.setGender(memberDetails.getGender()); - - member.setDateOfBirth(updatedDateOfBirth); - - try { - member.setWeight(!Strings.isNullOrEmpty(memberDetails.getWeight()) ? Integer.parseInt(memberDetails.getWeight()) : null); // TODO validate - } catch (IllegalArgumentException iae) { - log.warn(iae); - } - - member.setEmailAddress(memberDetails.getEmailAddress()); - member.setContactNumber(memberDetails.getContactNumber()); - member.setRowingPoints(!Strings.isNullOrEmpty(memberDetails.getRowingPoints()) ? memberDetails.getRowingPoints() : null); - member.setSculling(memberDetails.getSculling()); - member.setScullingPoints(!Strings.isNullOrEmpty(memberDetails.getScullingPoints()) ? memberDetails.getScullingPoints() : null); - member.setRegistrationNumber(memberDetails.getRegistrationNumber()); - member.setEmergencyContactName(memberDetails.getEmergencyContactName()); - member.setEmergencyContactNumber(memberDetails.getEmergencyContactNumber()); - member.setSweepOarSide(memberDetails.getSweepOarSide()); - - final boolean canChangeRole = permissionsService.canChangeRoleFor(loggedInUserService.getLoggedInMember(), member); - if (canChangeRole) { - member.setRole(memberDetails.getRole()); - } - - final boolean canChangeSquads = canChangeRole; - if (canChangeSquads) { - member.setSquads(squads); - } - - member.getAddress().put("postcode", memberDetails.getPostcode()); - - log.info("Submitting updated member: " + member); - loggedInUserApi.updateMemberDetails(member); - return new ModelAndView(new RedirectView(urlBuilder.memberUrl(member))); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/make-inactive", method = RequestMethod.GET) - public ModelAndView makeInactivePrompt(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - return viewFactory.getViewForLoggedInUser("makeMemberInactivePrompt"). - addObject("member", member). - addObject("title", "Make member inactive - " + member.getDisplayName()); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/make-inactive", method = RequestMethod.POST) - public ModelAndView makeInactive(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - log.info("Making member inactive: " + id); - final Member member = loggedInUserApi.getMember(id); - member.setInactive(true); - loggedInUserApi.updateMemberDetails(member); - - return redirectToAdminScreen(); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/delete", method = RequestMethod.GET) - public ModelAndView deletePrompt(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - return viewFactory.getViewForLoggedInUser("deleteMemberPrompt"). - addObject("member", member). - addObject("title", "Delete member - " + member.getDisplayName()); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/delete", method = RequestMethod.POST) - public ModelAndView delete(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - - loggedInUserApi.deleteMember(member); - return redirectToAdminScreen(); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/make-active", method = RequestMethod.GET) - public ModelAndView makeActivePrompt(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - - return viewFactory.getViewForLoggedInUser("makeMemberActivePrompt"). - addObject("member", member). - addObject("title", "Make member active - " + member.getDisplayName()); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/make-active", method = RequestMethod.POST) - public ModelAndView makeActive(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - log.info("Making member active: " + id); - final Member member = loggedInUserApi.getMember(id); - member.setInactive(false); - loggedInUserApi.updateMemberDetails(member); - return redirectToAdminScreen(); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/edit/profileimage", method = RequestMethod.POST) - public ModelAndView updateMemberProfileImageSubmit(@PathVariable String id, MultipartHttpServletRequest request) throws UnknownMemberException, IOException { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - log.info("Received update member profile image request: " + id); - final Member member = loggedInUserApi.getMember(id); - - final MultipartFile file = request.getFile("image"); - - log.info("Submitting updated member: " + member); - try { - loggedInUserApi.updateMemberProfileImage(member, file.getBytes()); - } catch (InvalidImageException e) { - log.warn("Invalid image file submitted"); - return new ModelAndView(new RedirectView(urlBuilder.editMemberUrl(member) + "?invalidImage=true")); - } - return new ModelAndView(new RedirectView(urlBuilder.memberUrl(member))); - } - - private ModelAndView renderNewMemberForm() throws SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("newMember"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - mv.addObject("title", "Adding a new member"); - mv.addObject("rolesOptions", ROLES_OPTIONS); - return mv; - } - - private ModelAndView renderEditMemberDetailsForm(MemberDetails memberDetails, String memberId, String title, Member member) throws UnknownMemberException, SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("editMemberDetails"); - mv.addObject("member", memberDetails); - mv.addObject("memberId", memberId); - mv.addObject("title", title); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - mv.addObject("governingBody", governingBodyFactory.getGoverningBody()); - - mv.addObject("genderOptions", GENDER_OPTIONS); - mv.addObject("pointsOptions", governingBodyFactory.getGoverningBody().getPointsOptions()); - mv.addObject("rolesOptions", ROLES_OPTIONS); - mv.addObject("sweepOarSideOptions", SWEEP_OAR_SIDE_OPTIONS); - mv.addObject("yesNoOptions", YES_NO_OPTIONS); - - final boolean canChangeRole = permissionsService.canChangeRoleFor(loggedInUserService.getLoggedInMember(), member); - mv.addObject("canChangeRole", canChangeRole); - mv.addObject("canChangeSquads", canChangeRole); - - mv.addObject("memberSquads", member.getSquads()); // TODO would not be needed id member.squads would form bind - return mv; - } - - private List extractAndValidateRequestedSquads(MemberDetails memberDetails, BindingResult result) { - final List squads = Lists.newArrayList(); - if (memberDetails.getSquads() == null) { - return squads; - } - - for (MemberSquad requestedSquad : memberDetails.getSquads()) { - log.info("Requested squad: " + requestedSquad); - try { - squads.add(squadlistApi.getSquad(requestedSquad.getId())); // TODO Validate instance - } catch (UnknownSquadException e) { - log.warn("Rejecting unknown squad: " + requestedSquad); - result.addError(new ObjectError("memberDetails.squad", "Unknown squad")); - } - } - log.info("Assigned squads: " + squads); - return squads; - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/reset", method = RequestMethod.GET) - public ModelAndView resetMemberPasswordPrompt(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - final Member member = loggedInUserApi.getMember(id); - return viewFactory.getViewForLoggedInUser("memberPasswordResetPrompt").addObject("member", member); - } - - @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/reset", method = RequestMethod.POST) - public ModelAndView resetMemberPassword(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Member member = loggedInUserApi.getMember(id); - - final String newPassword = instanceSpecificApiClient.resetMemberPassword(member); - - return viewFactory.getViewForLoggedInUser("memberPasswordReset"). - addObject("member", member). - addObject("password", newPassword); - } - - private ModelAndView redirectToAdminScreen() { - return new ModelAndView(new RedirectView(urlBuilder.adminUrl())); - } - -} \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt new file mode 100644 index 00000000..bfe4a70d --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt @@ -0,0 +1,390 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Strings +import com.google.common.collect.Lists +import org.apache.log4j.Logger +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.IllegalFieldValueException +import org.springframework.stereotype.Controller +import org.springframework.validation.BindingResult +import org.springframework.validation.ObjectError +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartHttpServletRequest +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.view.RedirectView +import uk.co.squadlist.web.annotations.RequiresMemberPermission +import uk.co.squadlist.web.annotations.RequiresPermission +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.context.GoverningBodyFactory +import uk.co.squadlist.web.exceptions.InvalidImageException +import uk.co.squadlist.web.exceptions.InvalidMemberException +import uk.co.squadlist.web.exceptions.UnknownSquadException +import uk.co.squadlist.web.model.Member +import uk.co.squadlist.web.model.Squad +import uk.co.squadlist.web.model.forms.MemberDetails +import uk.co.squadlist.web.model.forms.MemberSquad +import uk.co.squadlist.web.services.PasswordGenerator +import uk.co.squadlist.web.services.Permission +import uk.co.squadlist.web.services.PermissionsService +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.ViewFactory +import java.util.* +import javax.validation.Valid + +@Controller +public class MembersController(val instanceSpecificApiClient: InstanceSpecificApiClient, val loggedInUserService: LoggedInUserService, val urlBuilder: UrlBuilder, + val viewFactory: ViewFactory, + val passwordGenerator: PasswordGenerator, + val permissionsService: PermissionsService, + val governingBodyFactory: GoverningBodyFactory, + val squadlistApiFactory: SquadlistApiFactory) { + + private val log = Logger.getLogger(MembersController::class.java) + + private val GENDER_OPTIONS = Lists.newArrayList("female", "male") + private val ROLES_OPTIONS = Lists.newArrayList("Rower", "Rep", "Coach", "Cox", "Non rowing") + private val SWEEP_OAR_SIDE_OPTIONS = Lists.newArrayList("Bow", "Stroke", "Bow/Stroke", "Stroke/Bow") + private val YES_NO_OPTIONS = Lists.newArrayList("Y", "N") + + private val squadlistApi = squadlistApiFactory.createClient() + + @RequiresMemberPermission(permission = Permission.VIEW_MEMBER_DETAILS) + @GetMapping("/member/{id}") + fun member(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val members = loggedInUserApi.getMember(id) + + val mv = viewFactory.getViewForLoggedInUser("memberDetails") + mv.addObject("member", members) + mv.addObject("title", members.firstName + " " + members.lastName) + mv.addObject("governingBody", governingBodyFactory.governingBody) + return mv + } + + @RequiresPermission(permission = Permission.ADD_MEMBER) + @GetMapping(value = "/member/new") + fun newMember(@ModelAttribute("memberDetails") memberDetails: MemberDetails): ModelAndView { + return renderNewMemberForm() + } + + @RequiresPermission(permission = Permission.ADD_MEMBER) + @PostMapping(value = "/member/new") + @Throws(Exception::class) + fun newMemberSubmit(@Valid @ModelAttribute("memberDetails") memberDetails: MemberDetails, result: BindingResult): ModelAndView { + val requestedSquads = extractAndValidateRequestedSquads(memberDetails, result) + + if (result.hasErrors()) { + log.info("New member submission has errors: " + result.allErrors) + return renderNewMemberForm() + } + + val initialPassword = passwordGenerator.generateRandomPassword(10) + + try { + val newMember = instanceSpecificApiClient.createMember(memberDetails.firstName, + memberDetails.lastName, + requestedSquads, + memberDetails.emailAddress, + initialPassword, + null, + memberDetails.role + ) + + return viewFactory.getViewForLoggedInUser("memberAdded").addObject("member", newMember).addObject("initialPassword", initialPassword) + + } catch (e: InvalidMemberException) { + log.warn("Invalid member exception: " + e.message) + result.addError(ObjectError("memberDetails", e.message)) + return renderNewMemberForm() + } + + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @GetMapping(value = "/member/{id}/edit") + fun updateMember(@PathVariable id: String, @RequestParam(required = false) invalidImage: Boolean?): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + + val memberDetails = MemberDetails() + memberDetails.firstName = member.firstName + memberDetails.lastName = member.lastName + memberDetails.knownAs = member.knownAs + memberDetails.gender = member.gender + + memberDetails.dateOfBirthDay = if (member.dateOfBirth != null) DateTime(member.dateOfBirth).dayOfMonth else null + memberDetails.dateOfBirthMonth = if (member.dateOfBirth != null) DateTime(member.dateOfBirth).monthOfYear else null + memberDetails.dateOfBirthYear = if (member.dateOfBirth != null) DateTime(member.dateOfBirth).year else null + + memberDetails.weight = if (member.weight != null) Integer.toString(member.weight!!) else null + memberDetails.emailAddress = member.emailAddress + memberDetails.contactNumber = member.contactNumber + memberDetails.registrationNumber = member.registrationNumber + memberDetails.rowingPoints = member.rowingPoints + memberDetails.sculling = member.sculling + memberDetails.scullingPoints = member.scullingPoints + memberDetails.sweepOarSide = member.sweepOarSide + + memberDetails.postcode = if (member.address != null) member.address["postcode"] else null + + val memberSquads = Lists.newArrayList() + for (squad in member.squads) { + memberSquads.add(MemberSquad(squad.id)) + } + + memberDetails.squads = memberSquads + memberDetails.emergencyContactName = member.emergencyContactName + memberDetails.emergencyContactNumber = member.emergencyContactNumber + memberDetails.role = member.role + memberDetails.profileImage = member.profileImage + + val mv = renderEditMemberDetailsForm(memberDetails, member.id, member.firstName + " " + member.lastName, member) + mv.addObject("invalidImage", invalidImage) + return mv + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @RequestMapping(value = "/member/{id}/edit") + fun updateMemberSubmit(@PathVariable id: String, @Valid @ModelAttribute("member") memberDetails: MemberDetails, result: BindingResult): ModelAndView { + log.info("Received edit member request: $memberDetails") + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + + val squads = extractAndValidateRequestedSquads(memberDetails, result) + if (!Strings.isNullOrEmpty(memberDetails.scullingPoints)) { + if (!governingBodyFactory.governingBody.pointsOptions.contains(memberDetails.scullingPoints)) { + result.addError(ObjectError("member.scullingPoints", "Invalid points option")) + } + } + if (!Strings.isNullOrEmpty(memberDetails.rowingPoints)) { + if (!governingBodyFactory.governingBody.pointsOptions.contains(memberDetails.rowingPoints)) { + result.addError(ObjectError("member.rowingPoints", "Invalid points option")) + } + } + + var updatedDateOfBirth: Date? = null + if (memberDetails.dateOfBirthDay != null && + memberDetails.dateOfBirthMonth != null && + memberDetails.dateOfBirthYear != null) { + try { + updatedDateOfBirth = DateTime(memberDetails.dateOfBirthYear!!, + memberDetails.dateOfBirthMonth!!, memberDetails.dateOfBirthDay!!, 0, 0, 0, + DateTimeZone.UTC).toDate() + member.dateOfBirth = updatedDateOfBirth + } catch (e: IllegalFieldValueException) { + result.addError(ObjectError("member.dateOfBirthYear", "Invalid date")) + } + + } + + if (result.hasErrors()) { + return renderEditMemberDetailsForm(memberDetails, member.id, member.firstName + " " + member.lastName, member) + } + + log.info("Updating member details: " + member.id) + member.firstName = memberDetails.firstName + member.lastName = memberDetails.lastName + member.knownAs = memberDetails.knownAs + member.gender = memberDetails.gender + + member.dateOfBirth = updatedDateOfBirth + + try { + member.weight = if (!Strings.isNullOrEmpty(memberDetails.weight)) Integer.parseInt(memberDetails.weight) else null // TODO validate + } catch (iae: IllegalArgumentException) { + log.warn(iae) + } + + member.emailAddress = memberDetails.emailAddress + member.contactNumber = memberDetails.contactNumber + member.rowingPoints = if (!Strings.isNullOrEmpty(memberDetails.rowingPoints)) memberDetails.rowingPoints else null + member.sculling = memberDetails.sculling + member.scullingPoints = if (!Strings.isNullOrEmpty(memberDetails.scullingPoints)) memberDetails.scullingPoints else null + member.registrationNumber = memberDetails.registrationNumber + member.emergencyContactName = memberDetails.emergencyContactName + member.emergencyContactNumber = memberDetails.emergencyContactNumber + member.sweepOarSide = memberDetails.sweepOarSide + + val canChangeRole = permissionsService.canChangeRoleFor(loggedInUserService.loggedInMember, member) + if (canChangeRole) { + member.role = memberDetails.role + } + + if (canChangeRole) { + member.squads = squads + } + + member.address["postcode"] = memberDetails.postcode + + log.info("Submitting updated member: $member") + loggedInUserApi.updateMemberDetails(member) + return ModelAndView(RedirectView(urlBuilder.memberUrl(member))) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @GetMapping(value = "/member/{id}/make-inactive") + fun makeInactivePrompt(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + return viewFactory.getViewForLoggedInUser("makeMemberInactivePrompt").addObject("member", member).addObject("title", "Make member inactive - " + member.displayName) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @PostMapping(value = "/member/{id}/make-inactive") + @Throws(Exception::class) + fun makeInactive(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + log.info("Making member inactive: $id") + val member = loggedInUserApi.getMember(id) + member.inactive = true + loggedInUserApi.updateMemberDetails(member) + + return redirectToAdminScreen() + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @GetMapping(value = "/member/{id}/delete") + fun deletePrompt(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + return viewFactory.getViewForLoggedInUser("deleteMemberPrompt").addObject("member", member).addObject("title", "Delete member - " + member.displayName) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @PostMapping(value = "/member/{id}/delete") + fun delete(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + + loggedInUserApi.deleteMember(member) + return redirectToAdminScreen() + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @GetMapping(value = "/member/{id}/make-active") + fun makeActivePrompt(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + + return viewFactory.getViewForLoggedInUser("makeMemberActivePrompt").addObject("member", member).addObject("title", "Make member active - " + member.displayName) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @PostMapping(value = "/member/{id}/make-active") + fun makeActive(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + log.info("Making member active: $id") + val member = loggedInUserApi.getMember(id) + member.inactive = false + loggedInUserApi.updateMemberDetails(member) + return redirectToAdminScreen() + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @PostMapping(value = "/member/{id}/edit/profileimage") + fun updateMemberProfileImageSubmit(@PathVariable id: String, request: MultipartHttpServletRequest): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + log.info("Received update member profile image request: $id") + val member = loggedInUserApi.getMember(id) + + val file = request.getFile("image") + + log.info("Submitting updated member: $member") + try { + loggedInUserApi.updateMemberProfileImage(member, file!!.bytes) + } catch (e: InvalidImageException) { + log.warn("Invalid image file submitted") + return ModelAndView(RedirectView(urlBuilder.editMemberUrl(member) + "?invalidImage=true")) + } + + return ModelAndView(RedirectView(urlBuilder.memberUrl(member))) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @GetMapping(value = "/member/{id}/reset") + fun resetMemberPasswordPrompt(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + val member = loggedInUserApi.getMember(id) + return viewFactory.getViewForLoggedInUser("memberPasswordResetPrompt").addObject("member", member) + } + + @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) + @PostMapping(value = "/member/{id}/reset") + fun resetMemberPassword(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val member = loggedInUserApi.getMember(id) + + val newPassword = instanceSpecificApiClient.resetMemberPassword(member) + + return viewFactory.getViewForLoggedInUser("memberPasswordReset").addObject("member", member).addObject("password", newPassword) + } + + private fun redirectToAdminScreen(): ModelAndView { + return ModelAndView(RedirectView(urlBuilder.adminUrl())) + } + + private fun renderNewMemberForm(): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("newMember") + mv.addObject("squads", instanceSpecificApiClient.squads) + mv.addObject("title", "Adding a new member") + mv.addObject("rolesOptions", ROLES_OPTIONS) + return mv + } + + private fun renderEditMemberDetailsForm(memberDetails: MemberDetails, memberId: String, title: String, member: Member): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("editMemberDetails") + mv.addObject("member", memberDetails) + mv.addObject("memberId", memberId) + mv.addObject("title", title) + mv.addObject("squads", instanceSpecificApiClient.squads) + mv.addObject("governingBody", governingBodyFactory.governingBody) + + mv.addObject("genderOptions", GENDER_OPTIONS) + mv.addObject("pointsOptions", governingBodyFactory.governingBody.pointsOptions) + mv.addObject("rolesOptions", ROLES_OPTIONS) + mv.addObject("sweepOarSideOptions", SWEEP_OAR_SIDE_OPTIONS) + mv.addObject("yesNoOptions", YES_NO_OPTIONS) + + val canChangeRole = permissionsService.canChangeRoleFor(loggedInUserService.loggedInMember, member) + mv.addObject("canChangeRole", canChangeRole) + mv.addObject("canChangeSquads", canChangeRole) + + mv.addObject("memberSquads", member.squads) // TODO would not be needed id member.squads would form bind + return mv + } + + private fun extractAndValidateRequestedSquads(memberDetails: MemberDetails, result: BindingResult): List { + val squads = Lists.newArrayList() + if (memberDetails.squads == null) { + return squads + } + + for (requestedSquad in memberDetails.squads) { + log.info("Requested squad: $requestedSquad") + try { + squads.add(squadlistApi.getSquad(requestedSquad.id)) // TODO Validate instance + } catch (e: UnknownSquadException) { + log.warn("Rejecting unknown squad: $requestedSquad") + result.addError(ObjectError("memberDetails.squad", "Unknown squad")) + } + + } + log.info("Assigned squads: $squads") + return squads + } + +} From 5a81bce45f81134225dcdb52d9b6acf4526721f1 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 20:55:12 +0000 Subject: [PATCH 38/74] Clean ups. --- .../kotlin/uk/co/squadlist/web/controllers/MemberController.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt index bfe4a70d..3c2039f0 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt @@ -73,7 +73,6 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp @RequiresPermission(permission = Permission.ADD_MEMBER) @PostMapping(value = "/member/new") - @Throws(Exception::class) fun newMemberSubmit(@Valid @ModelAttribute("memberDetails") memberDetails: MemberDetails, result: BindingResult): ModelAndView { val requestedSquads = extractAndValidateRequestedSquads(memberDetails, result) @@ -238,7 +237,6 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) @PostMapping(value = "/member/{id}/make-inactive") - @Throws(Exception::class) fun makeInactive(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) From 674d20ca87e0abeb2fb6b434532074bcad732f86 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 20:56:20 +0000 Subject: [PATCH 39/74] listOf. --- .../uk/co/squadlist/web/controllers/MemberController.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt index 3c2039f0..3a9c8a09 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt @@ -44,10 +44,10 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp private val log = Logger.getLogger(MembersController::class.java) - private val GENDER_OPTIONS = Lists.newArrayList("female", "male") - private val ROLES_OPTIONS = Lists.newArrayList("Rower", "Rep", "Coach", "Cox", "Non rowing") - private val SWEEP_OAR_SIDE_OPTIONS = Lists.newArrayList("Bow", "Stroke", "Bow/Stroke", "Stroke/Bow") - private val YES_NO_OPTIONS = Lists.newArrayList("Y", "N") + private val GENDER_OPTIONS = listOf("female", "male") + private val ROLES_OPTIONS = listOf("Rower", "Rep", "Coach", "Cox", "Non rowing") + private val SWEEP_OAR_SIDE_OPTIONS = listOf("Bow", "Stroke", "Bow/Stroke", "Stroke/Bow") + private val YES_NO_OPTIONS = listOf("Y", "N") private val squadlistApi = squadlistApiFactory.createClient() From 6b5dae1beb1464ed42e0c847fff0792019fefd32 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 26 Nov 2018 21:29:18 +0000 Subject: [PATCH 40/74] Migrate to Kotlin. --- .../controllers/EntryDetailsController.java | 146 ------------------ .../web/controllers/EntryDetailsController.kt | 122 +++++++++++++++ 2 files changed, 122 insertions(+), 146 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/EntryDetailsController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsController.java b/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsController.java deleted file mode 100644 index 51cb2553..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsController.java +++ /dev/null @@ -1,146 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import com.google.common.base.Splitter; -import com.google.common.collect.Lists; -import org.apache.log4j.Logger; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.ObjectMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.ModelAndView; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.context.GoverningBodyFactory; -import uk.co.squadlist.web.localisation.GoverningBody; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.services.PreferedSquadService; -import uk.co.squadlist.web.views.CsvOutputRenderer; -import uk.co.squadlist.web.views.ViewFactory; - -import javax.servlet.http.HttpServletResponse; -import java.util.Date; -import java.util.Iterator; -import java.util.List; - -@Controller -public class EntryDetailsController { - - private final static Logger log = Logger.getLogger(EntryDetailsController.class); - - private final InstanceSpecificApiClient instanceSpecificApiClient; - private final PreferedSquadService preferedSquadService; - private final ViewFactory viewFactory; - private final EntryDetailsModelPopulator entryDetailsModelPopulator; - private final CsvOutputRenderer csvOutputRenderer; - private final GoverningBodyFactory governingBodyFactory; - private final SquadlistApi squadlistApi; - private final LoggedInUserService loggedInUserService; - private final SquadlistApiFactory squadlistApiFactory; - - @Autowired - public EntryDetailsController(InstanceSpecificApiClient instanceSpecificApiClient, PreferedSquadService preferedSquadService, ViewFactory viewFactory, - EntryDetailsModelPopulator entryDetailsModelPopulator, - CsvOutputRenderer csvOutputRenderer, GoverningBodyFactory governingBodyFactory, - SquadlistApiFactory squadlistApiFactory, LoggedInUserService loggedInUserService) { - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.preferedSquadService = preferedSquadService; - this.viewFactory = viewFactory; - this.entryDetailsModelPopulator = entryDetailsModelPopulator; - this.csvOutputRenderer = csvOutputRenderer; - this.governingBodyFactory = governingBodyFactory; - this.loggedInUserService = loggedInUserService; - this.squadlistApiFactory = squadlistApiFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequestMapping("/entrydetails/{squadId}") - public ModelAndView entrydetails(@PathVariable String squadId) throws Exception { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("entryDetails"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - mv.addObject("governingBody", governingBodyFactory.getGoverningBody()); - - final Squad squadToShow = preferedSquadService.resolveSquad(squadId); - entryDetailsModelPopulator.populateModel(squadToShow, mv); - return mv; - } - - @RequestMapping("/entrydetails/ajax") - public ModelAndView ajax(@RequestBody String json) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - List selectedMembers = Lists.newArrayList(); - - JsonNode readTree = new ObjectMapper().readTree(json); - Iterator iterator = readTree.iterator(); - while (iterator.hasNext()) { - selectedMembers.add(loggedInUserApi.getMember(iterator.next().asText())); - } - - List rowingPoints = Lists.newArrayList(); - List scullingPoints = Lists.newArrayList(); - for (Member member : selectedMembers) { - rowingPoints.add(member.getRowingPoints()); - scullingPoints.add(member.getScullingPoints()); - } - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("entryDetailsAjax"); - if (!selectedMembers.isEmpty()) { - mv.addObject("members", selectedMembers); - - final GoverningBody governingBody = governingBodyFactory.getGoverningBody(); - - int crewSize = selectedMembers.size(); - final boolean isFullBoat = governingBody.getBoatSizes().contains(crewSize); - mv.addObject("ok", isFullBoat); - if (isFullBoat) { - mv.addObject("rowingPoints", governingBody.getTotalPoints(rowingPoints)); - mv.addObject("rowingStatus", governingBody.getRowingStatus(rowingPoints)); - - mv.addObject("scullingPoints", governingBody.getTotalPoints(scullingPoints)); - mv.addObject("scullingStatus", governingBody.getScullingStatus(scullingPoints)); - - List datesOfBirth = Lists.newArrayList(); - for (Member member : selectedMembers) { - datesOfBirth.add(member.getDateOfBirth()); - } - - Integer effectiveAge = governingBody.getEffectiveAge(datesOfBirth); - if (effectiveAge != null) { - mv.addObject("effectiveAge", effectiveAge); - mv.addObject("ageGrade", governingBody.getAgeGrade(effectiveAge)); - } - } - } - return mv; - } - - @RequestMapping(value = "/entrydetails/{squadId}.csv", method = RequestMethod.GET) - public void entrydetailsCSV(@PathVariable String squadId, HttpServletResponse response) throws Exception { - viewFactory.getViewForLoggedInUser("entryDetails"); // TODO - - final Squad squadToShow = preferedSquadService.resolveSquad(squadId); - final List squadMembers = squadlistApi.getSquadMembers(squadToShow.getId()); - - List> entryDetailsRows = entryDetailsModelPopulator.getEntryDetailsRows(squadMembers); - csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.getEntryDetailsHeaders(), entryDetailsRows); - } - - @RequestMapping(value = "/entrydetails/selected.csv", method = RequestMethod.GET) // TODO Unused - public void entrydetailsSelectedCSV(@RequestParam String members, HttpServletResponse response) throws Exception { - log.info("Selected members: " + members); - List selectedMembers = Lists.newArrayList(); - final Iterator iterator = Splitter.on(",").split(members).iterator(); - while (iterator.hasNext()) { - final String selectedMemberId = iterator.next(); - log.info("Selected member id: " + selectedMemberId); - selectedMembers.add(squadlistApi.getMember(selectedMemberId)); - } - - csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.getEntryDetailsHeaders(), entryDetailsModelPopulator.getEntryDetailsRows(selectedMembers)); - } - -} \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt new file mode 100644 index 00000000..8464901b --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt @@ -0,0 +1,122 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Splitter +import com.google.common.collect.Lists +import org.apache.log4j.Logger +import org.codehaus.jackson.map.ObjectMapper +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.context.GoverningBodyFactory +import uk.co.squadlist.web.model.Member +import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.views.CsvOutputRenderer +import uk.co.squadlist.web.views.ViewFactory +import java.util.* +import javax.servlet.http.HttpServletResponse + +@Controller +public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpecificApiClient, + val preferedSquadService: PreferedSquadService, + val viewFactory: ViewFactory, + val entryDetailsModelPopulator: EntryDetailsModelPopulator, + val csvOutputRenderer: CsvOutputRenderer, val governingBodyFactory: GoverningBodyFactory, + val squadlistApiFactory: SquadlistApiFactory, val loggedInUserService: LoggedInUserService) { + + private val log = Logger.getLogger(EntryDetailsController::class.java) + + private val squadlistApi = squadlistApiFactory.createClient() + + @GetMapping("/entrydetails/{squadId}") + fun entrydetails(@PathVariable squadId: String): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("entryDetails") + mv.addObject("squads", instanceSpecificApiClient.squads) + mv.addObject("governingBody", governingBodyFactory.governingBody) + + val squadToShow = preferedSquadService.resolveSquad(squadId) + entryDetailsModelPopulator.populateModel(squadToShow, mv) + return mv + } + + @GetMapping("/entrydetails/ajax") + fun ajax(@RequestBody json: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val selectedMembers = Lists.newArrayList() + + val readTree = ObjectMapper().readTree(json) + val iterator = readTree.iterator() + while (iterator.hasNext()) { + selectedMembers.add(loggedInUserApi.getMember(iterator.next().asText())) + } + + val rowingPoints = Lists.newArrayList() + val scullingPoints = Lists.newArrayList() + for (member in selectedMembers) { + rowingPoints.add(member.rowingPoints) + scullingPoints.add(member.scullingPoints) + } + + val mv = viewFactory.getViewForLoggedInUser("entryDetailsAjax") + if (!selectedMembers.isEmpty()) { + mv.addObject("members", selectedMembers) + + val governingBody = governingBodyFactory.governingBody + + val crewSize = selectedMembers.size + val isFullBoat = governingBody.boatSizes.contains(crewSize) + mv.addObject("ok", isFullBoat) + if (isFullBoat) { + mv.addObject("rowingPoints", governingBody.getTotalPoints(rowingPoints)) + mv.addObject("rowingStatus", governingBody.getRowingStatus(rowingPoints)) + + mv.addObject("scullingPoints", governingBody.getTotalPoints(scullingPoints)) + mv.addObject("scullingStatus", governingBody.getScullingStatus(scullingPoints)) + + val datesOfBirth = Lists.newArrayList() + for (member in selectedMembers) { + datesOfBirth.add(member.dateOfBirth) + } + + val effectiveAge = governingBody.getEffectiveAge(datesOfBirth) + if (effectiveAge != null) { + mv.addObject("effectiveAge", effectiveAge) + mv.addObject("ageGrade", governingBody.getAgeGrade(effectiveAge)) + } + } + } + return mv + } + + @GetMapping(value = "/entrydetails/{squadId}.csv") + fun entrydetailsCSV(@PathVariable squadId: String, response: HttpServletResponse) { + viewFactory.getViewForLoggedInUser("entryDetails") // TODO + + val squadToShow = preferedSquadService.resolveSquad(squadId) + val squadMembers = squadlistApi.getSquadMembers(squadToShow.id) + + val entryDetailsRows = entryDetailsModelPopulator.getEntryDetailsRows(squadMembers) + csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.entryDetailsHeaders, entryDetailsRows) + } + + @GetMapping(value = "/entrydetails/selected.csv") // TODO Unused + fun entrydetailsSelectedCSV(@RequestParam members: String, response: HttpServletResponse) { + log.info("Selected members: $members") + val selectedMembers = Lists.newArrayList() + val iterator = Splitter.on(",").split(members).iterator() + while (iterator.hasNext()) { + val selectedMemberId = iterator.next() + log.info("Selected member id: $selectedMemberId") + selectedMembers.add(squadlistApi.getMember(selectedMemberId)) + } + + csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.entryDetailsHeaders, entryDetailsModelPopulator.getEntryDetailsRows(selectedMembers)) + } + +} \ No newline at end of file From a89f686cfa5ef57ea416163ab3069529692b3781 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Tue, 27 Nov 2018 09:16:32 +0000 Subject: [PATCH 41/74] Port contacts template. --- .../web/controllers/ContactsController.kt | 11 ++- src/main/resources/contacts.vm | 67 ------------------ src/main/resources/includes/member.vm | 8 --- src/main/resources/templates/contacts.ftl | 69 +++++++++++++++++++ .../resources/templates/includes/member.ftl | 8 +++ 5 files changed, 87 insertions(+), 76 deletions(-) delete mode 100644 src/main/resources/contacts.vm delete mode 100644 src/main/resources/includes/member.vm create mode 100644 src/main/resources/templates/contacts.ftl create mode 100644 src/main/resources/templates/includes/member.ftl diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt index 503c5083..384fa585 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsController.kt @@ -7,10 +7,14 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.servlet.ModelAndView import uk.co.squadlist.web.api.InstanceSpecificApiClient import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.PermissionsHelper import uk.co.squadlist.web.views.ViewFactory @Controller -class ContactsController(val api: InstanceSpecificApiClient, val preferedSquadService: PreferedSquadService, val viewFactory: ViewFactory, val contactsModelPopulator: ContactsModelPopulator) { +class ContactsController(val api: InstanceSpecificApiClient, val preferedSquadService: PreferedSquadService, + val viewFactory: ViewFactory, val contactsModelPopulator: ContactsModelPopulator, + val urlBuilder: UrlBuilder, val permissionsHelper: PermissionsHelper) { private val log = Logger.getLogger(ContactsController::class.java) @@ -19,6 +23,8 @@ class ContactsController(val api: InstanceSpecificApiClient, val preferedSquadSe val mv = viewFactory.getViewForLoggedInUser("contacts") val allSquads = api.squads mv.addObject("squads", allSquads) // TODO leaves squad null on view + mv.addObject("urlBuilder", urlBuilder) + mv.addObject("permissionsHelper", permissionsHelper) return mv } @@ -33,6 +39,9 @@ class ContactsController(val api: InstanceSpecificApiClient, val preferedSquadSe log.info("Squad to show: $squadToShow") contactsModelPopulator.populateModel(squadToShow, mv) } + + mv.addObject("urlBuilder", urlBuilder) + mv.addObject("permissionsHelper", permissionsHelper) return mv } diff --git a/src/main/resources/contacts.vm b/src/main/resources/contacts.vm deleted file mode 100644 index 0137a854..00000000 --- a/src/main/resources/contacts.vm +++ /dev/null @@ -1,67 +0,0 @@ -#parse('includes/head.vm') -#navTabs('Contacts') - -
    - - #if(!$squads.isEmpty()) - -
    -
    -

    $text.text('contacts')

    -
    -
    -
    - #squadSelect('/contacts/', 'Show contacts') -
    -
    -
    -
    - - #if($members.isEmpty()) -

    $text.text('no.members.assigned.to.squad')

    - #else - - #if($emails) -

    $text.text('email.members')

    - #end - - - - - - - - - - - #foreach( $member in $members ) - - - - - - #end -
    $text.text('email')$text.text('phone')
    - #parse('includes/member.vm') - #if($member.role && $member.role != 'Rower') - ($!member.role) - #end - - #if($member.emailAddress) - $member.emailAddress - #end - - #if($member.contactNumber) - $member.contactNumber - #end -
    - #end - - #else -

    $text.text('contacts')

    -
    -

    There are currently no squads setup.

    - #end - -
    -
    \ No newline at end of file diff --git a/src/main/resources/includes/member.vm b/src/main/resources/includes/member.vm deleted file mode 100644 index 7c7183c8..00000000 --- a/src/main/resources/includes/member.vm +++ /dev/null @@ -1,8 +0,0 @@ -#if($permissionsHelper.hasMemberPermission($member, 'EDIT_MEMBER_DETAILS')) - - $member.displayName - -#else - $member.displayName -#end diff --git a/src/main/resources/templates/contacts.ftl b/src/main/resources/templates/contacts.ftl new file mode 100644 index 00000000..76be5be9 --- /dev/null +++ b/src/main/resources/templates/contacts.ftl @@ -0,0 +1,69 @@ +<#include 'includes/head.ftl'> +<@navTabs 'Contacts' /> + +
    + + <#if squads?size != 0> +
    +
    +

    <@spring.message 'contacts' />

    +
    +
    +
    + #squadSelect('/contacts/', 'Show contacts') +
    +
    +
    +
    + + <#if members?size== 0> +

    <@spring.message 'no.members.assigned.to.squad' />

    + <#else> + + <#if emails?? > +

    <@spring.message 'email.members' />

    + + + + + + + + + + + + <#list members as member> + + + + + + +
    <@spring.message 'email' /><@spring.message 'phone' />
    + <#include 'includes/member.ftl'> + <#if member.role?? && member.role != 'Rower' > + (${member.role}) + + + <#if member.emailAddress?? > + ${member.emailAddress} + + + <#if member.contactNumber?? > + ${member.contactNumber} + +
    + + + <#else> +

    <@spring.message 'contacts' />

    +
    +

    There are currently no squads setup.

    + + +
    +
    + +<#include 'includes/footer.ftl'> + diff --git a/src/main/resources/templates/includes/member.ftl b/src/main/resources/templates/includes/member.ftl new file mode 100644 index 00000000..715abd3d --- /dev/null +++ b/src/main/resources/templates/includes/member.ftl @@ -0,0 +1,8 @@ +<#if permissionsHelper.hasMemberPermission(member, 'EDIT_MEMBER_DETAILS') > + + ${member.displayName} + +<#else> + ${member.displayName} + From c73f3b56cd5a98eb4b8513e8eb6766829f9a534a Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Tue, 27 Nov 2018 09:26:44 +0000 Subject: [PATCH 42/74] Port squad dropdown macro. --- src/main/resources/macros.vm | 17 +-- src/main/resources/templates/contacts.ftl | 2 +- .../resources/templates/includes/head.ftl | 111 ++++++++++-------- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/src/main/resources/macros.vm b/src/main/resources/macros.vm index f5357acf..1932ca19 100644 --- a/src/main/resources/macros.vm +++ b/src/main/resources/macros.vm @@ -92,19 +92,4 @@
    -#end - -#macro(squadSelect $action $label) - #if ($squads) -
    - - -
    - #end -#end +#end \ No newline at end of file diff --git a/src/main/resources/templates/contacts.ftl b/src/main/resources/templates/contacts.ftl index 76be5be9..3cfaca44 100644 --- a/src/main/resources/templates/contacts.ftl +++ b/src/main/resources/templates/contacts.ftl @@ -10,7 +10,7 @@
    - #squadSelect('/contacts/', 'Show contacts') + <@squadSelect '/contacts/' 'Show contacts' />
    diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index dbf23a34..8ff597d2 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -26,52 +26,69 @@ <#macro navTabs selectedTab> - + + +<#macro squadSelect action label> + <#if squads??> +
    + + +
    + + + + From 964f0853dc8c88b0811d8b63777d4b26d93a2539 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 10:34:12 +0000 Subject: [PATCH 43/74] Clean up; no need to use named parameter on single argument methods. --- .../controllers/ChangePasswordController.kt | 4 +-- .../web/controllers/EntryDetailsController.kt | 4 +-- .../controllers/GoverningBodyController.kt | 2 +- .../web/controllers/LoginController.kt | 4 +-- .../web/controllers/MemberController.kt | 26 +++++++++---------- .../controllers/ResetPasswordController.kt | 6 ++--- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt index e588562b..43fc91bc 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ChangePasswordController.kt @@ -24,12 +24,12 @@ class ChangePasswordController(val api: InstanceSpecificApiClient, val urlBuilde private val log = Logger.getLogger(ChangePasswordController::class.java) - @GetMapping(value = "/change-password") + @GetMapping("/change-password") fun changePassword(): ModelAndView { return renderChangePasswordForm(ChangePassword()) } - @PostMapping(value = "/change-password") + @PostMapping("/change-password") fun changePasswordSubmit(@Valid @ModelAttribute("changePassword") changePassword: ChangePassword, result: BindingResult): ModelAndView { if (result.hasErrors()) { return renderChangePasswordForm(changePassword) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt index 8464901b..b913824f 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt @@ -94,7 +94,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci return mv } - @GetMapping(value = "/entrydetails/{squadId}.csv") + @GetMapping("/entrydetails/{squadId}.csv") fun entrydetailsCSV(@PathVariable squadId: String, response: HttpServletResponse) { viewFactory.getViewForLoggedInUser("entryDetails") // TODO @@ -105,7 +105,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.entryDetailsHeaders, entryDetailsRows) } - @GetMapping(value = "/entrydetails/selected.csv") // TODO Unused + @GetMapping("/entrydetails/selected.csv") // TODO Unused fun entrydetailsSelectedCSV(@RequestParam members: String, response: HttpServletResponse) { log.info("Selected members: $members") val selectedMembers = Lists.newArrayList() diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt index 91bf14d1..15d6e873 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/GoverningBodyController.kt @@ -9,7 +9,7 @@ import uk.co.squadlist.web.views.ViewFactory @Controller public class GoverningBodyController(val governingBodyFactory: GoverningBodyFactory, val viewFactory: ViewFactory) { - @GetMapping(value = "/governing-body/british-rowing") + @GetMapping("/governing-body/british-rowing") fun member(): ModelAndView { val governingBody = governingBodyFactory.governingBody diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 78b7f0be..2c916ccb 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -17,7 +17,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic @RequestMapping(value = "/login", method = arrayOf(RequestMethod.GET, RequestMethod.HEAD)) // TODO SEO this onto the root url fun login() = renderLoginScreen() - @PostMapping(value = "/login") + @PostMapping("/login") fun loginSubmit( @RequestParam(value = "username", required = true) username: String, @RequestParam(value = "password", required = true) password: String): ModelAndView { @@ -34,7 +34,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic return renderLoginScreen(true, username) } - @GetMapping(value = "/logout") + @GetMapping("/logout") fun logout(): ModelAndView { loggedInUserService.cleanSignedIn() return ModelAndView(RedirectView(urlBuilder.loginUrl())) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt index 3a9c8a09..9b621df2 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/MemberController.kt @@ -66,13 +66,13 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresPermission(permission = Permission.ADD_MEMBER) - @GetMapping(value = "/member/new") + @GetMapping("/member/new") fun newMember(@ModelAttribute("memberDetails") memberDetails: MemberDetails): ModelAndView { return renderNewMemberForm() } @RequiresPermission(permission = Permission.ADD_MEMBER) - @PostMapping(value = "/member/new") + @PostMapping("/member/new") fun newMemberSubmit(@Valid @ModelAttribute("memberDetails") memberDetails: MemberDetails, result: BindingResult): ModelAndView { val requestedSquads = extractAndValidateRequestedSquads(memberDetails, result) @@ -104,7 +104,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @GetMapping(value = "/member/{id}/edit") + @GetMapping("/member/{id}/edit") fun updateMember(@PathVariable id: String, @RequestParam(required = false) invalidImage: Boolean?): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -148,7 +148,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @RequestMapping(value = "/member/{id}/edit") + @PostMapping("/member/{id}/edit") fun updateMemberSubmit(@PathVariable id: String, @Valid @ModelAttribute("member") memberDetails: MemberDetails, result: BindingResult): ModelAndView { log.info("Received edit member request: $memberDetails") val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -227,7 +227,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @GetMapping(value = "/member/{id}/make-inactive") + @GetMapping("/member/{id}/make-inactive") fun makeInactivePrompt(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -236,7 +236,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @PostMapping(value = "/member/{id}/make-inactive") + @PostMapping("/member/{id}/make-inactive") fun makeInactive(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -249,7 +249,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @GetMapping(value = "/member/{id}/delete") + @GetMapping("/member/{id}/delete") fun deletePrompt(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -258,7 +258,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @PostMapping(value = "/member/{id}/delete") + @PostMapping("/member/{id}/delete") fun delete(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -269,7 +269,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @GetMapping(value = "/member/{id}/make-active") + @GetMapping("/member/{id}/make-active") fun makeActivePrompt(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -279,7 +279,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @PostMapping(value = "/member/{id}/make-active") + @PostMapping("/member/{id}/make-active") fun makeActive(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -291,7 +291,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @PostMapping(value = "/member/{id}/edit/profileimage") + @PostMapping("/member/{id}/edit/profileimage") fun updateMemberProfileImageSubmit(@PathVariable id: String, request: MultipartHttpServletRequest): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -312,7 +312,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @GetMapping(value = "/member/{id}/reset") + @GetMapping("/member/{id}/reset") fun resetMemberPasswordPrompt(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) val member = loggedInUserApi.getMember(id) @@ -320,7 +320,7 @@ public class MembersController(val instanceSpecificApiClient: InstanceSpecificAp } @RequiresMemberPermission(permission = Permission.EDIT_MEMBER_DETAILS) - @PostMapping(value = "/member/{id}/reset") + @PostMapping("/member/{id}/reset") fun resetMemberPassword(@PathVariable id: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt index f484ad7c..baa85c50 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ResetPasswordController.kt @@ -16,12 +16,12 @@ class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder private val log = Logger.getLogger(ResetPasswordController::class.java) - @GetMapping(value = "/reset-password") + @GetMapping("/reset-password") fun resetPasswordPrompt(): ModelAndView { return resetPasswordScreen().addObject("errors", false) } - @PostMapping(value = "/reset-password") + @PostMapping("/reset-password") fun resetPassword(@RequestParam(value = "username", required = false) username: String): ModelAndView { if (Strings.isNullOrEmpty(username)) { return resetPasswordScreen().addObject("errors", true) @@ -41,7 +41,7 @@ class ResetPasswordController(val api: InstanceSpecificApiClient, val urlBuilder } } - @GetMapping(value = "/reset-password/confirm") + @GetMapping("/reset-password/confirm") fun confirmPasswordReset(@RequestParam token: String): ModelAndView { try { val newPassword = api.confirmResetPassword(token) From 9953cb25ce37f169e815f14e2078c828ed8813f8 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 10:34:53 +0000 Subject: [PATCH 44/74] Remove usused copy pf password gen jar. --- .../jpwgen/jpwgen/1.2.0/jpwgen-1.2.0.jar | Bin 60191 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/repository/jpwgen/jpwgen/1.2.0/jpwgen-1.2.0.jar diff --git a/lib/repository/jpwgen/jpwgen/1.2.0/jpwgen-1.2.0.jar b/lib/repository/jpwgen/jpwgen/1.2.0/jpwgen-1.2.0.jar deleted file mode 100644 index 08dfd9b19fdf8d422b4ab30700501828d73fe546..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60191 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 VN9}ZGX6;Yg>GPdB42xs?^#fFQ5P<*y From b2de5fdef7a21bcde86218e6aa1ee0416a99a8a2 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 10:47:51 +0000 Subject: [PATCH 45/74] Move to Kotlin. --- .../web/controllers/SquadsController.java | 158 ------------------ .../web/controllers/SquadsController.kt | 124 ++++++++++++++ 2 files changed, 124 insertions(+), 158 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/SquadsController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/SquadsController.java b/src/main/java/uk/co/squadlist/web/controllers/SquadsController.java deleted file mode 100644 index 00122b45..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/SquadsController.java +++ /dev/null @@ -1,158 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.io.IOException; -import java.util.List; -import java.util.Set; - -import javax.validation.Valid; - -import org.apache.log4j.Logger; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.map.JsonMappingException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.RedirectView; - -import uk.co.eelpieconsulting.common.http.HttpBadRequestException; -import uk.co.eelpieconsulting.common.http.HttpFetchException; -import uk.co.eelpieconsulting.common.http.HttpForbiddenException; -import uk.co.eelpieconsulting.common.http.HttpNotFoundException; -import uk.co.squadlist.web.annotations.RequiresPermission; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.context.InstanceConfig; -import uk.co.squadlist.web.exceptions.InvalidSquadException; -import uk.co.squadlist.web.exceptions.SignedInMemberRequiredException; -import uk.co.squadlist.web.exceptions.UnknownInstanceException; -import uk.co.squadlist.web.exceptions.UnknownSquadException; -import uk.co.squadlist.web.model.Instance; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.model.forms.SquadDetails; -import uk.co.squadlist.web.services.Permission; -import uk.co.squadlist.web.urls.UrlBuilder; -import uk.co.squadlist.web.views.ViewFactory; - -import com.google.common.base.Splitter; -import com.google.common.collect.Sets; - -@Controller -public class SquadsController { - - private final static Logger log = Logger.getLogger(SquadsController.class); - - private final static Splitter COMMA_SPLITTER = Splitter.on(","); - - private InstanceSpecificApiClient instanceSpecificApiClient; - private UrlBuilder urlBuilder; - private ViewFactory viewFactory; - private SquadlistApi squadlistApi; - private InstanceConfig instanceConfig; - - public SquadsController() { - } - - @Autowired - public SquadsController(InstanceSpecificApiClient instanceSpecificApiClient, - UrlBuilder urlBuilder, ViewFactory viewFactory, SquadlistApiFactory squadlistApiFactory, - InstanceConfig instanceConfig) { - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.urlBuilder = urlBuilder; - this.viewFactory = viewFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - this.instanceConfig = instanceConfig; - } - - @RequestMapping(value="/squad/new", method=RequestMethod.GET) - public ModelAndView newSquad(@ModelAttribute("squadDetails") SquadDetails squadDetails) throws Exception { - return renderNewSquadForm(new SquadDetails()); - } - - @RequestMapping(value="/squad/new", method=RequestMethod.POST) - public ModelAndView newSquadSubmit(@Valid @ModelAttribute("squadDetails") SquadDetails squadDetails, BindingResult result) throws UnknownInstanceException, SignedInMemberRequiredException { - if (result.hasErrors()) { - return renderNewSquadForm(squadDetails); - } - - try { - Instance instance = squadlistApi.getInstance(instanceConfig.getInstance()); - squadlistApi.createSquad(instance, squadDetails.getName()); - - return new ModelAndView(new RedirectView(urlBuilder.adminUrl())); - - } catch (InvalidSquadException e) { - log.info("Invalid squad"); - result.rejectValue("name", null, "squad name is already in use"); - return renderNewSquadForm(squadDetails); - } - } - - @RequiresPermission(permission=Permission.VIEW_ADMIN_SCREEN) - @RequestMapping(value="/squad/{id}/delete", method=RequestMethod.GET) - public ModelAndView deletePrompt(@PathVariable String id) throws UnknownSquadException, SignedInMemberRequiredException { - final Squad squad = squadlistApi.getSquad(id); - return viewFactory.getViewForLoggedInUser("deleteSquadPrompt").addObject("squad", squad).addObject("title", "Delete squad"); - } - - @RequiresPermission(permission=Permission.VIEW_ADMIN_SCREEN) - @RequestMapping(value="/squad/{id}/delete", method=RequestMethod.POST) - public ModelAndView delete(@PathVariable String id) throws UnknownSquadException { - final Squad squad = squadlistApi.getSquad(id); - squadlistApi.deleteSquad(squad); - return new ModelAndView(new RedirectView(urlBuilder.adminUrl())); - } - - @RequestMapping(value="/squad/{id}/edit", method=RequestMethod.GET) - public ModelAndView editSquad(@PathVariable String id) throws UnknownSquadException, SignedInMemberRequiredException { - final Squad squad = squadlistApi.getSquad(id); - - final SquadDetails squadDetails = new SquadDetails(); - squadDetails.setName(squad.getName()); - - return renderEditSquadForm(squad, squadDetails); - } - - @RequestMapping(value="/squad/{id}/edit", method=RequestMethod.POST) - public ModelAndView editSquadSubmit(@PathVariable String id, @Valid @ModelAttribute("squadDetails") SquadDetails squadDetails, BindingResult result) throws UnknownSquadException, JsonGenerationException, JsonMappingException, HttpNotFoundException, HttpBadRequestException, HttpForbiddenException, IOException, HttpFetchException, SignedInMemberRequiredException { - final Squad squad = squadlistApi.getSquad(id); - if (result.hasErrors()) { - return renderEditSquadForm(squad, squadDetails); - } - - squad.setName(squadDetails.getName()); - log.info("Updating squad: " + squad); - squadlistApi.updateSquad(squad); - - final Set updatedSquadMembers = Sets.newHashSet(COMMA_SPLITTER.split(squadDetails.getMembers()).iterator()); - log.info("Setting squad members to " + updatedSquadMembers.size() + " members: " + updatedSquadMembers); - squadlistApi.setSquadMembers(squad.getId(), updatedSquadMembers); - - return new ModelAndView(new RedirectView(urlBuilder.adminUrl())); - } - - private ModelAndView renderNewSquadForm(SquadDetails squadDetails) throws SignedInMemberRequiredException { - return viewFactory.getViewForLoggedInUser("newSquad").addObject("squadDetails", squadDetails); - } - - private ModelAndView renderEditSquadForm(final Squad squad, final SquadDetails squadDetails) throws SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("editSquad"); - mv.addObject("squad", squad); - mv.addObject("squadDetails", squadDetails); - - final List squadMembers = squadlistApi.getSquadMembers(squad.getId()); - mv.addObject("squadMembers", squadMembers); - - final List availableMembers = instanceSpecificApiClient.getMembers(); - availableMembers.removeAll(squadMembers); - mv.addObject("availableMembers", availableMembers); - return mv; - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt new file mode 100644 index 00000000..f68f20c3 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt @@ -0,0 +1,124 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Splitter +import com.google.common.collect.Sets +import org.apache.log4j.Logger +import org.springframework.stereotype.Controller +import org.springframework.validation.BindingResult +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.view.RedirectView +import uk.co.squadlist.web.annotations.RequiresPermission +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.context.InstanceConfig +import uk.co.squadlist.web.exceptions.InvalidSquadException +import uk.co.squadlist.web.model.Squad +import uk.co.squadlist.web.model.forms.SquadDetails +import uk.co.squadlist.web.services.Permission +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.ViewFactory +import javax.validation.Valid + +@Controller +class SquadsController( + val instanceSpecificApiClient: InstanceSpecificApiClient, + val urlBuilder: UrlBuilder, + val viewFactory: ViewFactory, + val squadlistApi: SquadlistApi, + val instanceConfig: InstanceConfig) { + + private val log = Logger.getLogger(SquadsController::class.java) + + private val COMMA_SPLITTER = Splitter.on(",") + + @GetMapping("/squad/new") + fun newSquad(@ModelAttribute("squadDetails") squadDetails: SquadDetails): ModelAndView { + return renderNewSquadForm(SquadDetails()) + } + + @PostMapping("/squad/new") + fun newSquadSubmit(@Valid @ModelAttribute("squadDetails") squadDetails: SquadDetails, result: BindingResult): ModelAndView { + if (result.hasErrors()) { + return renderNewSquadForm(squadDetails) + } + + try { + val instance = squadlistApi.getInstance(instanceConfig.instance) + squadlistApi.createSquad(instance, squadDetails.name) + + return ModelAndView(RedirectView(urlBuilder.adminUrl())) + + } catch (e: InvalidSquadException) { + log.info("Invalid squad") + result.rejectValue("name", null, "squad name is already in use") + return renderNewSquadForm(squadDetails) + } + + } + + @RequiresPermission(permission = Permission.VIEW_ADMIN_SCREEN) + @GetMapping("/squad/{id}/delete") + fun deletePrompt(@PathVariable id: String): ModelAndView { + val squad = squadlistApi.getSquad(id) + return viewFactory.getViewForLoggedInUser("deleteSquadPrompt").addObject("squad", squad).addObject("title", "Delete squad") + } + + @RequiresPermission(permission = Permission.VIEW_ADMIN_SCREEN) + @PostMapping("/squad/{id}/delete") + fun delete(@PathVariable id: String): ModelAndView { + val squad = squadlistApi.getSquad(id) + squadlistApi.deleteSquad(squad) + return ModelAndView(RedirectView(urlBuilder.adminUrl())) + } + + @GetMapping("/squad/{id}/edit") + fun editSquad(@PathVariable id: String): ModelAndView { + val squad = squadlistApi.getSquad(id) + + val squadDetails = SquadDetails() + squadDetails.name = squad.name + + return renderEditSquadForm(squad, squadDetails) + } + + @PostMapping("/squad/{id}/edit") + fun editSquadSubmit(@PathVariable id: String, @Valid @ModelAttribute("squadDetails") squadDetails: SquadDetails, result: BindingResult): ModelAndView { + val squad = squadlistApi.getSquad(id) + if (result.hasErrors()) { + return renderEditSquadForm(squad, squadDetails) + } + + squad.name = squadDetails.name + log.info("Updating squad: $squad") + squadlistApi.updateSquad(squad) + + val updatedSquadMembers = Sets.newHashSet(COMMA_SPLITTER.split(squadDetails.members).iterator()) + log.info("Setting squad members to " + updatedSquadMembers.size + " members: " + updatedSquadMembers) + squadlistApi.setSquadMembers(squad.id, updatedSquadMembers) + + return ModelAndView(RedirectView(urlBuilder.adminUrl())) + } + + private fun renderNewSquadForm(squadDetails: SquadDetails): ModelAndView { + return viewFactory.getViewForLoggedInUser("newSquad").addObject("squadDetails", squadDetails) + } + + private fun renderEditSquadForm(squad: Squad, squadDetails: SquadDetails): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("editSquad") + mv.addObject("squad", squad) + mv.addObject("squadDetails", squadDetails) + + val squadMembers = squadlistApi.getSquadMembers(squad.id) + mv.addObject("squadMembers", squadMembers) + + val availableMembers = instanceSpecificApiClient.members + availableMembers.removeAll(squadMembers) + mv.addObject("availableMembers", availableMembers) + return mv + } + +} From aeadf06246309eac5a610bafc89750f6c92cfe94 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 11:01:00 +0000 Subject: [PATCH 46/74] Move url builder to Kotlin; move interesting autowiring situation than the controllers. --- build.gradle | 2 +- .../uk/co/squadlist/web/urls/UrlBuilder.java | 297 ------------------ .../web/controllers/LoginController.kt | 2 +- .../uk/co/squadlist/web/urls/UrlBuilder.kt | 284 +++++++++++++++++ .../co/squadlist/web/urls/UrlBuilderTest.java | 12 +- 5 files changed, 292 insertions(+), 305 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt diff --git a/build.gradle b/build.gradle index e8294392..30e3b27b 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - + compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" compile "com.fasterxml.jackson.core:jackson-core:2.9.7" compile "com.fasterxml.jackson.core:jackson-databind:2.9.7" diff --git a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java b/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java deleted file mode 100644 index 8993f9a7..00000000 --- a/src/main/java/uk/co/squadlist/web/urls/UrlBuilder.java +++ /dev/null @@ -1,297 +0,0 @@ -package uk.co.squadlist.web.urls; - -import com.google.common.base.Joiner; -import com.google.common.collect.Lists; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.http.client.utils.URIBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import uk.co.squadlist.web.context.InstanceConfig; -import uk.co.squadlist.web.localisation.GoverningBody; -import uk.co.squadlist.web.model.*; - -import java.net.URISyntaxException; -import java.util.List; - -@Component("urlBuilder") -public class UrlBuilder { - - private final String baseUrl; - private final InstanceConfig instanceConfig; - private final SeoLinkBuilder seoLinkBuilder; - private final String apiUrl; - private final String profilePicturesUrl; - - @Autowired - public UrlBuilder(@Value("${baseUrl}") String baseUrl, InstanceConfig instanceConfig, SeoLinkBuilder seoLinkBuilder, - @Value("${apiUrl}") String apiUrl, - @Value("${profilePicturesUrl}") String profilePicturesUrl - ) { - this.baseUrl = baseUrl; - this.instanceConfig = instanceConfig; - this.seoLinkBuilder = seoLinkBuilder; - this.apiUrl = apiUrl; - this.profilePicturesUrl = profilePicturesUrl; - } - - public String applicationUrl(String uri) { - return getBaseUrl() + uri; - } - - public String loginUrl() { - return applicationUrl("/login"); - } - - public String resetPassword() { return applicationUrl("/reset-password"); } - - public String staticUrl(String uri) { - return applicationUrl("/assets/") + uri; - } - - public String boatUrl(Boat boat) { - return applicationUrl("/boats/" + boat.getId()); - } - public String memberUrl(Member member) { - return memberUrl(member.getId()); - } - - public String memberResetPasswordUrl(Member member) { - return memberUrl(member.getId()) + "/reset"; - } - - public String memberUrl(String memberId) { - return applicationUrl("/member/" + memberId); - } - - public String makeActive(Member member) { - return memberUrl(member.getId()) + "/make-active"; - } - - public String makeInactive(Member member) { - return memberUrl(member.getId()) + "/make-inactive"; - } - - public String delete(Member member) { - return memberUrl(member.getId()) + "/delete"; - } - - public String delete(Squad squad) { - return applicationUrl("/squad/" + squad.getId() + "/delete"); - } - - public String newMemberUrl() { - return applicationUrl("/member/new"); - } - - public String newSquadUrl() { - return applicationUrl("/squad/new"); - } - - public String editSquadUrl(Squad squad) { - return applicationUrl("/squad/" + squad.getId() + "/edit"); - } - - public String adminUrl() { - return applicationUrl("/admin"); - } - - public String outingsUrl() { - return applicationUrl("/outings"); - } - - public String outingUrl(Outing outing) { - return outingsUrl() + "/" + outing.getId(); - } - - public String outingAvailabilityCsv(Outing outing) { - return outingUrl(outing) + ".csv"; - } - - public String outingCloseUrl(Outing outing) { - return outingUrl(outing) + "/close"; - } - - public String deleteOuting(Outing outing) { - return outingUrl(outing) + "/delete"; - } - - public String outingReopenUrl(Outing outing) { - return outingUrl(outing) + "/reopen"; - } - - public String outingEditUrl(Outing outing) { - return outingUrl(outing) + "/edit"; - } - - public String entryDetails(Squad squad) { - return applicationUrl("/entry-details?squad=" + squad.getId()); - } - - public String outings(Squad squad) { - return applicationUrl("/outings?squad=" + squad.getId()); - } - - public String outings(Squad squad, String month) { - return outings(squad) + "&month=" + month; - } - - public String availability(Squad squad) { - return applicationUrl("/availability/" + squad.getId()); - } - - public String availability(Squad squad, String month) { - return availability(squad) + "?month=" + month; - } - - public String editInstanceSettings() { - return adminUrl() + "/instance"; - } - - public String editMemberUrl(Member member) { - return memberUrl(member) + "/edit"; - } - - public String editMemberUrl(String memberId) { - return memberUrl(memberId) + "/edit"; - } - - public String entryDetailsCsv(Squad squad) { - return applicationUrl("/entrydetails/" + squad.getId() + ".csv"); - } - - public String entryDetailsCsv(List members) { - try { - final URIBuilder url = new URIBuilder(applicationUrl("/entrydetails/selected.csv")); - - final List memberIds = Lists.newArrayList(); - for (Member member : members) { - memberIds.add(member.getId()); - } - url.addParameter("members", Joiner.on(",").join(memberIds)); - - return url.toString(); - - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - public String changePassword() { - return applicationUrl("/change-password"); - } - - public String socialMediaAccounts() { - return applicationUrl("/social"); - } - - public String linkFacebookUrl() { - return applicationUrl("/social/facebook/link"); - } - - public String removeFacebookUrl() { - return applicationUrl("/social/facebook/remove"); - } - - public String getLinkFacebookCallbackUrl() { - return applicationUrl("/social/facebook/link/callback"); - } - - public String facebookSigninCallbackUrl() { - return applicationUrl("/social/facebook/signin/callback"); - } - - public String facebookSignin() { - return applicationUrl("/social/facebook/signin"); - } - - public String getBaseUrl() { - return baseUrl.replace("INSTANCE", instanceConfig.getVhost()); - } - - public String contactsUrl() { - return applicationUrl("/contacts"); - } - public String contactsUrl(Squad prefferredSquad) { - return appendSquad(prefferredSquad, contactsUrl()); - } - - public String outingsUrl(Squad prefferredSquad) throws URISyntaxException { - final URIBuilder url = new URIBuilder(outingsUrl()); - if (prefferredSquad != null) { - url.addParameter("squad", prefferredSquad.getId()); - } - return url.build().toString(); - } - - public String outingsRss(String userid, Instance instance) throws URISyntaxException { - final URIBuilder url = new URIBuilder(applicationUrl("/rss")); - url.addParameter("user", userid); - url.addParameter("key", generateFeedKeyFor(userid, instance)); - return url.build().toString(); - } - - public String outingsIcal(String userid, Instance instance) throws URISyntaxException { - String webcalBaseUrl = getBaseUrl().replaceAll("^https?://", "webcal://"); - final URIBuilder url = new URIBuilder(webcalBaseUrl + "/ical"); - url.addParameter("user", userid); - url.addParameter("key", generateFeedKeyFor(userid, instance)); - return url.build().toString(); - } - - public String availabilityUrl(Squad prefferredSquad) { - return appendSquad(prefferredSquad, availabilityUrl()); - } - - public String governingBody(GoverningBody governingBody) { - return applicationUrl("/governing-body/" + seoLinkBuilder.makeSeoLinkFor(governingBody.getName())); - } - - public String staticImage(String filename) { - return apiUrl + "/static/" + filename + ".jpg"; - } - - public String profileImage(String filename) { - return profilePicturesUrl + "/" + filename + ".jpg"; // TODO ideally all of this would be provided by the API - } - - public String newAvailabilityOptionUrl() { - return applicationUrl("/availability-option/new"); - } - - public String deleteAvailabilityOptionUrl(AvailabilityOption availabilityOption) { - return applicationUrl("/availability-option/" + availabilityOption.getId() + "/delete"); - } - - public String editAvailabilityOptionUrl(AvailabilityOption availabilityOption) { - return applicationUrl("/availability-option/" + availabilityOption.getId() + "/edit"); - } - - public String editAdmins() { - return adminUrl() + "/admins"; - } - - public String adminExportMembersAsCSV() { - return adminUrl() + "/export/members.csv"; - } - - private String availabilityUrl() { - return applicationUrl("/availability"); - } - - private String appendSquad(Squad prefferredSquad, final String baseUrl) { - return prefferredSquad != null ? baseUrl + "/" + prefferredSquad.getId() : baseUrl; - } - - private String generateFeedKeyFor(String userid, Instance instance) { - return DigestUtils.md5Hex(instance.getId() + userid); - } - - public String mailto(List emails) { - if (emails.isEmpty()) { - return null; - } - return "mailto:" + Joiner.on(",").join(emails); - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt index 2c916ccb..c601dfe8 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/LoginController.kt @@ -27,7 +27,7 @@ class LoginController(val api: InstanceSpecificApiClient, val loggedInUserServic api.verify(token)?.let { user -> log.info("Auth successful for user: " + user.username) loggedInUserService.setSignedIn(token) - return ModelAndView(RedirectView(urlBuilder.baseUrl)) + return ModelAndView(RedirectView(urlBuilder.getBaseUrl())) } } diff --git a/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt new file mode 100644 index 00000000..64881fed --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt @@ -0,0 +1,284 @@ +package uk.co.squadlist.web.urls + +import com.google.common.base.Joiner +import com.google.common.collect.Lists +import org.apache.commons.codec.digest.DigestUtils +import org.apache.http.client.utils.URIBuilder +import org.springframework.beans.factory.annotation.Value +import uk.co.squadlist.web.context.InstanceConfig +import uk.co.squadlist.web.localisation.GoverningBody +import uk.co.squadlist.web.model.* +import java.net.URISyntaxException + +class UrlBuilder (@param:Value("\${baseUrl}") private val baseUrl: String, val instanceConfig: InstanceConfig, + val seoLinkBuilder: SeoLinkBuilder, + @param:Value("\${apiUrl}") val apiUrl: String, + @param:Value("\${profilePicturesUrl}") val profilePicturesUrl: String +) { + + val linkFacebookCallbackUrl: String + get() = applicationUrl("/social/facebook/link/callback") + + fun applicationUrl(uri: String): String { + return getBaseUrl() + uri + } + + fun loginUrl(): String { + return applicationUrl("/login") + } + + fun resetPassword(): String { + return applicationUrl("/reset-password") + } + + fun staticUrl(uri: String): String { + return applicationUrl("/assets/") + uri + } + + fun boatUrl(boat: Boat): String { + return applicationUrl("/boats/" + boat.id) + } + + fun memberUrl(member: Member): String { + return memberUrl(member.id) + } + + fun memberResetPasswordUrl(member: Member): String { + return memberUrl(member.id) + "/reset" + } + + fun memberUrl(memberId: String): String { + return applicationUrl("/member/$memberId") + } + + fun makeActive(member: Member): String { + return memberUrl(member.id) + "/make-active" + } + + fun makeInactive(member: Member): String { + return memberUrl(member.id) + "/make-inactive" + } + + fun delete(member: Member): String { + return memberUrl(member.id) + "/delete" + } + + fun delete(squad: Squad): String { + return applicationUrl("/squad/" + squad.id + "/delete") + } + + fun newMemberUrl(): String { + return applicationUrl("/member/new") + } + + fun newSquadUrl(): String { + return applicationUrl("/squad/new") + } + + fun editSquadUrl(squad: Squad): String { + return applicationUrl("/squad/" + squad.id + "/edit") + } + + fun adminUrl(): String { + return applicationUrl("/admin") + } + + fun outingsUrl(): String { + return applicationUrl("/outings") + } + + fun outingUrl(outing: Outing): String { + return outingsUrl() + "/" + outing.id + } + + fun outingAvailabilityCsv(outing: Outing): String { + return outingUrl(outing) + ".csv" + } + + fun outingCloseUrl(outing: Outing): String { + return outingUrl(outing) + "/close" + } + + fun deleteOuting(outing: Outing): String { + return outingUrl(outing) + "/delete" + } + + fun outingReopenUrl(outing: Outing): String { + return outingUrl(outing) + "/reopen" + } + + fun outingEditUrl(outing: Outing): String { + return outingUrl(outing) + "/edit" + } + + fun entryDetails(squad: Squad): String { + return applicationUrl("/entry-details?squad=" + squad.id) + } + + fun outings(squad: Squad): String { + return applicationUrl("/outings?squad=" + squad.id) + } + + fun outings(squad: Squad, month: String): String { + return outings(squad) + "&month=" + month + } + + fun availability(squad: Squad): String { + return applicationUrl("/availability/" + squad.id) + } + + fun availability(squad: Squad, month: String): String { + return availability(squad) + "?month=" + month + } + + fun editInstanceSettings(): String { + return adminUrl() + "/instance" + } + + fun editMemberUrl(member: Member): String { + return memberUrl(member) + "/edit" + } + + fun editMemberUrl(memberId: String): String { + return memberUrl(memberId) + "/edit" + } + + fun entryDetailsCsv(squad: Squad): String { + return applicationUrl("/entrydetails/" + squad.id + ".csv") + } + + fun entryDetailsCsv(members: List): String { + try { + val url = URIBuilder(applicationUrl("/entrydetails/selected.csv")) + + val memberIds = Lists.newArrayList() + for (member in members) { + memberIds.add(member.id) + } + url.addParameter("members", Joiner.on(",").join(memberIds)) + + return url.toString() + + } catch (e: URISyntaxException) { + throw RuntimeException(e) + } + + } + + fun changePassword(): String { + return applicationUrl("/change-password") + } + + fun socialMediaAccounts(): String { + return applicationUrl("/social") + } + + fun linkFacebookUrl(): String { + return applicationUrl("/social/facebook/link") + } + + fun removeFacebookUrl(): String { + return applicationUrl("/social/facebook/remove") + } + + fun facebookSigninCallbackUrl(): String { + return applicationUrl("/social/facebook/signin/callback") + } + + fun facebookSignin(): String { + return applicationUrl("/social/facebook/signin") + } + + fun getBaseUrl(): String { + return baseUrl.replace("INSTANCE", instanceConfig.vhost) + } + + fun contactsUrl(): String { + return applicationUrl("/contacts") + } + + fun contactsUrl(prefferredSquad: Squad): String { + return appendSquad(prefferredSquad, contactsUrl()) + } + + @Throws(URISyntaxException::class) + fun outingsUrl(prefferredSquad: Squad?): String { + val url = URIBuilder(outingsUrl()) + if (prefferredSquad != null) { + url.addParameter("squad", prefferredSquad.id) + } + return url.build().toString() + } + + @Throws(URISyntaxException::class) + fun outingsRss(userid: String, instance: Instance): String { + val url = URIBuilder(applicationUrl("/rss")) + url.addParameter("user", userid) + url.addParameter("key", generateFeedKeyFor(userid, instance)) + return url.build().toString() + } + + @Throws(URISyntaxException::class) + fun outingsIcal(userid: String, instance: Instance): String { + val webcalBaseUrl = getBaseUrl().replace("^https?://".toRegex(), "webcal://") + val url = URIBuilder("$webcalBaseUrl/ical") + url.addParameter("user", userid) + url.addParameter("key", generateFeedKeyFor(userid, instance)) + return url.build().toString() + } + + fun availabilityUrl(prefferredSquad: Squad): String { + return appendSquad(prefferredSquad, availabilityUrl()) + } + + fun governingBody(governingBody: GoverningBody): String { + return applicationUrl("/governing-body/" + seoLinkBuilder.makeSeoLinkFor(governingBody.name)) + } + + fun staticImage(filename: String): String { + return "$apiUrl/static/$filename.jpg" + } + + fun profileImage(filename: String): String { + return "$profilePicturesUrl/$filename.jpg" // TODO ideally all of this would be provided by the API + } + + fun newAvailabilityOptionUrl(): String { + return applicationUrl("/availability-option/new") + } + + fun deleteAvailabilityOptionUrl(availabilityOption: AvailabilityOption): String { + return applicationUrl("/availability-option/" + availabilityOption.id + "/delete") + } + + fun editAvailabilityOptionUrl(availabilityOption: AvailabilityOption): String { + return applicationUrl("/availability-option/" + availabilityOption.id + "/edit") + } + + fun editAdmins(): String { + return adminUrl() + "/admins" + } + + fun adminExportMembersAsCSV(): String { + return adminUrl() + "/export/members.csv" + } + + private fun availabilityUrl(): String { + return applicationUrl("/availability") + } + + private fun appendSquad(prefferredSquad: Squad?, baseUrl: String): String { + return if (prefferredSquad != null) baseUrl + "/" + prefferredSquad.id else baseUrl + } + + private fun generateFeedKeyFor(userid: String, instance: Instance): String { + return DigestUtils.md5Hex(instance.id + userid) + } + + fun mailto(emails: List): String? { + return if (emails.isEmpty()) { + null + } else "mailto:" + Joiner.on(",").join(emails) + } + +} diff --git a/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java b/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java index eb7fde81..2a607b68 100644 --- a/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java +++ b/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java @@ -35,37 +35,37 @@ public void setup() { @Test public void canInsertInstanceIntoBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); assertEquals("https://twrc.squadlist.co.uk/meh", urlBuilder.applicationUrl("/meh")); } @Test public void canComposeWebcalFeedUrlFromBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("http://localhost", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("http://localhost", instanceConfig, seoLinkBuilder, "", ""); assertEquals("webcal://localhost/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); } @Test public void webcalFeedsAreHttpEvenIfTheMainSiteIsHttps() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); assertEquals("webcal://twrc.squadlist.co.uk/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); } @Test public void canComposeRssFeedUrlFromBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); assertEquals("https://twrc.squadlist.co.uk/rss?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsRss("a-user", instance)); } @Test public void canLinkToGoverningBodyPage() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); assertEquals("https://localhost/governing-body/british-rowing", urlBuilder.governingBody(new BritishRowing())); } @Test public void canComposeMailtoListUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, null, null); + final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); final List emails = Lists.newArrayList("auser@localhost", "anotheruser@localhost"); assertEquals("mailto:auser@localhost,anotheruser@localhost", urlBuilder.mailto(emails)); } From 1b0c4c2b670fd2bc68aaad5be049271cf683dbb4 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 11:03:32 +0000 Subject: [PATCH 47/74] Formatting. --- .../co/squadlist/web/urls/UrlBuilderTest.java | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java b/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java index 2a607b68..dbcffa46 100644 --- a/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java +++ b/src/test/java/uk/co/squadlist/web/urls/UrlBuilderTest.java @@ -9,7 +9,6 @@ import uk.co.squadlist.web.localisation.BritishRowing; import uk.co.squadlist.web.model.Instance; import uk.co.squadlist.web.model.Subscription; -import uk.co.squadlist.web.model.Tariff; import java.util.List; @@ -18,56 +17,56 @@ public class UrlBuilderTest { - @Mock - private InstanceConfig instanceConfig; - private SeoLinkBuilder seoLinkBuilder; + @Mock + private InstanceConfig instanceConfig; + private SeoLinkBuilder seoLinkBuilder; - private Instance instance; + private Instance instance; - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - when(instanceConfig.getVhost()).thenReturn("twrc"); - seoLinkBuilder = new SeoLinkBuilder(); + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(instanceConfig.getVhost()).thenReturn("twrc"); + seoLinkBuilder = new SeoLinkBuilder(); - instance = new Instance("aninstance", "An instance", null, true, Lists.newArrayList(), "british-rowing"); - } - - @Test - public void canInsertInstanceIntoBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); - assertEquals("https://twrc.squadlist.co.uk/meh", urlBuilder.applicationUrl("/meh")); - } + instance = new Instance("aninstance", "An instance", null, true, Lists.newArrayList(), "british-rowing"); + } - @Test - public void canComposeWebcalFeedUrlFromBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("http://localhost", instanceConfig, seoLinkBuilder, "", ""); - assertEquals("webcal://localhost/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); - } + @Test + public void canInsertInstanceIntoBaseUrl() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); + assertEquals("https://twrc.squadlist.co.uk/meh", urlBuilder.applicationUrl("/meh")); + } - @Test - public void webcalFeedsAreHttpEvenIfTheMainSiteIsHttps() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); - assertEquals("webcal://twrc.squadlist.co.uk/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); - } + @Test + public void canComposeWebcalFeedUrlFromBaseUrl() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("http://localhost", instanceConfig, seoLinkBuilder, "", ""); + assertEquals("webcal://localhost/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); + } - @Test - public void canComposeRssFeedUrlFromBaseUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); - assertEquals("https://twrc.squadlist.co.uk/rss?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsRss("a-user", instance)); - } + @Test + public void webcalFeedsAreHttpEvenIfTheMainSiteIsHttps() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); + assertEquals("webcal://twrc.squadlist.co.uk/ical?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsIcal("a-user", instance)); + } - @Test - public void canLinkToGoverningBodyPage() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); - assertEquals("https://localhost/governing-body/british-rowing", urlBuilder.governingBody(new BritishRowing())); - } + @Test + public void canComposeRssFeedUrlFromBaseUrl() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("https://INSTANCE.squadlist.co.uk", instanceConfig, seoLinkBuilder, "", ""); + assertEquals("https://twrc.squadlist.co.uk/rss?user=a-user&key=9329bb2beff6556bf2698163732bb9a2", urlBuilder.outingsRss("a-user", instance)); + } - @Test - public void canComposeMailtoListUrl() throws Exception { - final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); - final List emails = Lists.newArrayList("auser@localhost", "anotheruser@localhost"); - assertEquals("mailto:auser@localhost,anotheruser@localhost", urlBuilder.mailto(emails)); - } + @Test + public void canLinkToGoverningBodyPage() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); + assertEquals("https://localhost/governing-body/british-rowing", urlBuilder.governingBody(new BritishRowing())); + } + + @Test + public void canComposeMailtoListUrl() throws Exception { + final UrlBuilder urlBuilder = new UrlBuilder("https://localhost", instanceConfig, seoLinkBuilder, "", ""); + final List emails = Lists.newArrayList("auser@localhost", "anotheruser@localhost"); + assertEquals("mailto:auser@localhost,anotheruser@localhost", urlBuilder.mailto(emails)); + } } \ No newline at end of file From 0526a4146618410c5e8a2192935d899e8eb507b6 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 11:12:37 +0000 Subject: [PATCH 48/74] Move to Kotlin. --- .../uk/co/squadlist/web/urls/SeoLinkBuilder.java | 15 --------------- .../uk/co/squadlist/web/urls/SeoLinkBuilder.kt | 12 ++++++++++++ 2 files changed, 12 insertions(+), 15 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/urls/SeoLinkBuilder.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt diff --git a/src/main/java/uk/co/squadlist/web/urls/SeoLinkBuilder.java b/src/main/java/uk/co/squadlist/web/urls/SeoLinkBuilder.java deleted file mode 100644 index c53e176c..00000000 --- a/src/main/java/uk/co/squadlist/web/urls/SeoLinkBuilder.java +++ /dev/null @@ -1,15 +0,0 @@ -package uk.co.squadlist.web.urls; - -import org.springframework.stereotype.Component; - -@Component -public class SeoLinkBuilder { - - public String makeSeoLinkFor(String name) { - String result = name.toLowerCase().trim().replaceAll("\\s", "-"); - result = result.replaceAll("[^\\-a-z0-9_]", ""); - result = result.replaceAll("--+", "-"); - return result; - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt b/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt new file mode 100644 index 00000000..a0a23a25 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt @@ -0,0 +1,12 @@ +package uk.co.squadlist.web.urls + +class SeoLinkBuilder { + + fun makeSeoLinkFor(name: String): String { + var result = name.toLowerCase().trim { it <= ' ' }.replace("\\s".toRegex(), "-") + result = result.replace("[^\\-a-z0-9_]".toRegex(), "") + result = result.replace("--+".toRegex(), "-") + return result + } + +} From a2e9710299488e2d01db84200c0d50e457255578 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 11:36:30 +0000 Subject: [PATCH 49/74] Fix Spring wirings; still need Component annotations to be discovered. --- .../web/api/SquadlistApiFactory.java | 32 +++++++++---------- .../web/controllers/SquadsController.kt | 7 ++-- .../co/squadlist/web/urls/SeoLinkBuilder.kt | 3 ++ .../uk/co/squadlist/web/urls/UrlBuilder.kt | 12 ++++--- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/uk/co/squadlist/web/api/SquadlistApiFactory.java b/src/main/java/uk/co/squadlist/web/api/SquadlistApiFactory.java index e649c43e..6b037bae 100644 --- a/src/main/java/uk/co/squadlist/web/api/SquadlistApiFactory.java +++ b/src/main/java/uk/co/squadlist/web/api/SquadlistApiFactory.java @@ -7,22 +7,22 @@ @Component public class SquadlistApiFactory { - private final String apiUrl; - private final String clientAccessToken; - - @Autowired - public SquadlistApiFactory(@Value("${apiUrl}") String apiUrl, - @Value("${apiAccessToken}") String clientAccessToken) { - this.apiUrl = apiUrl; - this.clientAccessToken = clientAccessToken; - } - - public SquadlistApi createClient() { - return createForToken(clientAccessToken); - } + private final String apiUrl; + private final String clientAccessToken; - public SquadlistApi createForToken(String token) { - return new SquadlistApi(apiUrl, token); - } + @Autowired + public SquadlistApiFactory(@Value("${apiUrl}") String apiUrl, + @Value("${apiAccessToken}") String clientAccessToken) { + this.apiUrl = apiUrl; + this.clientAccessToken = clientAccessToken; + } + + public SquadlistApi createClient() { + return createForToken(clientAccessToken); + } + + public SquadlistApi createForToken(String token) { + return new SquadlistApi(apiUrl, token); + } } diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt index f68f20c3..aa64a95b 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/SquadsController.kt @@ -14,6 +14,7 @@ import org.springframework.web.servlet.view.RedirectView import uk.co.squadlist.web.annotations.RequiresPermission import uk.co.squadlist.web.api.InstanceSpecificApiClient import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.api.SquadlistApiFactory import uk.co.squadlist.web.context.InstanceConfig import uk.co.squadlist.web.exceptions.InvalidSquadException import uk.co.squadlist.web.model.Squad @@ -28,13 +29,15 @@ class SquadsController( val instanceSpecificApiClient: InstanceSpecificApiClient, val urlBuilder: UrlBuilder, val viewFactory: ViewFactory, - val squadlistApi: SquadlistApi, - val instanceConfig: InstanceConfig) { + val instanceConfig: InstanceConfig, + val squadlistApiFactory: SquadlistApiFactory) { private val log = Logger.getLogger(SquadsController::class.java) private val COMMA_SPLITTER = Splitter.on(",") + private val squadlistApi: SquadlistApi = squadlistApiFactory.createClient() + @GetMapping("/squad/new") fun newSquad(@ModelAttribute("squadDetails") squadDetails: SquadDetails): ModelAndView { return renderNewSquadForm(SquadDetails()) diff --git a/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt b/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt index a0a23a25..a0b70efb 100644 --- a/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt +++ b/src/main/kotlin/uk/co/squadlist/web/urls/SeoLinkBuilder.kt @@ -1,5 +1,8 @@ package uk.co.squadlist.web.urls +import org.springframework.stereotype.Component + +@Component class SeoLinkBuilder { fun makeSeoLinkFor(name: String): String { diff --git a/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt index 64881fed..607d1e09 100644 --- a/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt +++ b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt @@ -5,16 +5,18 @@ import com.google.common.collect.Lists import org.apache.commons.codec.digest.DigestUtils import org.apache.http.client.utils.URIBuilder import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component import uk.co.squadlist.web.context.InstanceConfig import uk.co.squadlist.web.localisation.GoverningBody import uk.co.squadlist.web.model.* import java.net.URISyntaxException -class UrlBuilder (@param:Value("\${baseUrl}") private val baseUrl: String, val instanceConfig: InstanceConfig, - val seoLinkBuilder: SeoLinkBuilder, - @param:Value("\${apiUrl}") val apiUrl: String, - @param:Value("\${profilePicturesUrl}") val profilePicturesUrl: String -) { +@Component +class UrlBuilder (@param:Value("\${baseUrl}") private val baseUrl: String, + val instanceConfig: InstanceConfig, + val seoLinkBuilder: SeoLinkBuilder, + @param:Value("\${apiUrl}") private val apiUrl: String, + @param:Value("\${profilePicturesUrl}") private val profilePicturesUrl: String) { val linkFacebookCallbackUrl: String get() = applicationUrl("/social/facebook/link/callback") From 8e2243777dd86e8fb6871990fdd0dc01237acfb9 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 17:24:57 +0000 Subject: [PATCH 50/74] Migrated to Kotlin. --- .../web/controllers/OutingsController.java | 363 ------------------ .../web/controllers/OutingsController.kt | 324 ++++++++++++++++ 2 files changed, 324 insertions(+), 363 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/OutingsController.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/OutingsController.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/OutingsController.java b/src/main/java/uk/co/squadlist/web/controllers/OutingsController.java deleted file mode 100644 index 8056eff3..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/OutingsController.java +++ /dev/null @@ -1,363 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; - -import org.apache.log4j.Logger; -import org.codehaus.jackson.JsonParseException; -import org.codehaus.jackson.map.JsonMappingException; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDateTime; -import org.joda.time.format.ISODateTimeFormat; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.RedirectView; - -import uk.co.eelpieconsulting.common.http.HttpFetchException; -import uk.co.squadlist.web.annotations.RequiresOutingPermission; -import uk.co.squadlist.web.annotations.RequiresPermission; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.exceptions.*; -import uk.co.squadlist.web.model.AvailabilityOption; -import uk.co.squadlist.web.model.Instance; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Outing; -import uk.co.squadlist.web.model.OutingAvailability; -import uk.co.squadlist.web.model.OutingWithSquadAvailability; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.model.forms.OutingDetails; -import uk.co.squadlist.web.services.OutingAvailabilityCountsService; -import uk.co.squadlist.web.services.Permission; -import uk.co.squadlist.web.services.PreferedSquadService; -import uk.co.squadlist.web.services.filters.ActiveMemberFilter; -import uk.co.squadlist.web.urls.UrlBuilder; -import uk.co.squadlist.web.views.CsvOutputRenderer; -import uk.co.squadlist.web.views.DateFormatter; -import uk.co.squadlist.web.views.DateHelper; -import uk.co.squadlist.web.views.ViewFactory; - -import com.google.common.base.Strings; -import com.google.common.collect.Lists; - -@Controller -public class OutingsController { - - private final static Logger log = Logger.getLogger(OutingsController.class); - - private LoggedInUserService loggedInUserService; - private InstanceSpecificApiClient instanceSpecificApiClient; - private UrlBuilder urlBuilder; - private DateFormatter dateFormatter; - private PreferedSquadService preferedSquadService; - private ViewFactory viewFactory; - private OutingAvailabilityCountsService outingAvailabilityCountsService; - private ActiveMemberFilter activeMemberFilter; - private CsvOutputRenderer csvOutputRenderer; - private SquadlistApiFactory squadlistApiFactory; - private SquadlistApi squadlistApi; - - public OutingsController() { - } - - @Autowired - public OutingsController(LoggedInUserService loggedInUserService, InstanceSpecificApiClient instanceSpecificApiClient, UrlBuilder urlBuilder, - DateFormatter dateFormatter, PreferedSquadService preferedSquadService, ViewFactory viewFactory, - OutingAvailabilityCountsService outingAvailabilityCountsService, ActiveMemberFilter activeMemberFilter, - CsvOutputRenderer csvOutputRenderer, SquadlistApiFactory squadlistApiFactory) { - this.loggedInUserService = loggedInUserService; - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.urlBuilder = urlBuilder; - this.dateFormatter = dateFormatter; - this.preferedSquadService = preferedSquadService; - this.viewFactory = viewFactory; - this.outingAvailabilityCountsService = outingAvailabilityCountsService; - this.activeMemberFilter = activeMemberFilter; - this.csvOutputRenderer = csvOutputRenderer; - this.squadlistApiFactory = squadlistApiFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequestMapping("/outings") - public ModelAndView outings(@RequestParam(required = false, value = "squad") String squadId, - @RequestParam(value = "month", required = false) String month) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Squad squadToShow = preferedSquadService.resolveSquad(squadId); - final ModelAndView mv = viewFactory.getViewForLoggedInUser("outings"); - if (squadToShow == null) { - mv.addObject("title", "Outings"); - return mv; - } - - Date startDate = DateHelper.startOfCurrentOutingPeriod().toDate(); - Date endDate = DateHelper.endOfCurrentOutingPeriod().toDate(); - - String title = squadToShow.getName() + " outings"; - if (month != null) { - final DateTime monthDateTime = ISODateTimeFormat.yearMonth().parseDateTime(month); // TODO Can be moved to spring? - startDate = monthDateTime.toDate(); - endDate = monthDateTime.plusMonths(1).toDate(); - title = squadToShow.getName() + " outings - " + dateFormatter.fullMonthYear(startDate); - } else { - mv.addObject("current", true); - } - - mv.addObject("title", title); - mv.addObject("squad", squadToShow); - mv.addObject("startDate", startDate); - mv.addObject("endDate", endDate); - mv.addObject("month", month); - mv.addObject("outingMonths", getOutingMonthsFor(squadToShow)); - - final List squadOutings = squadlistApi.getSquadAvailability(squadToShow.getId(), startDate, endDate); - mv.addObject("outings", squadOutings); - mv.addObject("outingAvailabilityCounts", outingAvailabilityCountsService.buildOutingAvailabilityCounts(squadOutings)); - - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - return mv; - } - - @RequestMapping("/outings/{id}") - public ModelAndView outing(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - - final ModelAndView mv = viewFactory.getViewForLoggedInUser("outing"); - mv.addObject("title", outing.getSquad().getName() + " - " + dateFormatter.dayMonthYearTime(outing.getDate())); - mv.addObject("outing", outing); - mv.addObject("outingMonths", getOutingMonthsFor(outing.getSquad())); - mv.addObject("squad", outing.getSquad()); - mv.addObject("squadAvailability", loggedInUserApi.getOutingAvailability(outing.getId())); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(outing.getSquad().getId()))); - mv.addObject("month", ISODateTimeFormat.yearMonth().print(outing.getDate().getTime())); // TODO push to date parser - local time - return mv; - } - - @RequestMapping("/outings/{id}.csv") - public void outingCsv(@PathVariable String id, HttpServletResponse response) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - final List squadMembers = squadlistApi.getSquadMembers(outing.getSquad().getId()); - final Map outingAvailability = loggedInUserApi.getOutingAvailability(outing.getId()); - - final List> rows = Lists.newArrayList(); - for (Member member : squadMembers) { - rows.add(Arrays.asList( - dateFormatter.dayMonthYearTime(outing.getDate()), - outing.getSquad().getName(), - member.getDisplayName(), - member.getRole(), - outingAvailability.get(member.getId()) != null ? outingAvailability.get(member.getId()).getLabel() : null - )); - } - csvOutputRenderer.renderCsvResponse(response, Lists.newArrayList("Date", "Squad", "Member", "Role", "Availability"), rows); - } - - @RequiresPermission(permission = Permission.ADD_OUTING) - @RequestMapping(value = "/outings/new", method = RequestMethod.GET) // TODO fails hard if no squads are available - public ModelAndView newOuting() throws Exception { - final LocalDateTime defaultOutingDateTime = DateHelper.defaultOutingStartDateTime(); - final OutingDetails outingDefaults = new OutingDetails(defaultOutingDateTime); - outingDefaults.setSquad(preferedSquadService.resolveSquad(null).getId()); - return renderNewOutingForm(outingDefaults); - } - - @RequiresPermission(permission = Permission.ADD_OUTING) - @RequestMapping(value = "/outings/new", method = RequestMethod.POST) - public ModelAndView newOutingSubmit(@Valid @ModelAttribute("outing") OutingDetails outingDetails, BindingResult result) throws Exception { - if (result.hasErrors()) { - return renderNewOutingForm(outingDetails); - } - - try { - final Outing newOuting = buildOutingFromOutingDetails(outingDetails, instanceSpecificApiClient.getInstance()); - if (outingDetails.getRepeats() != null && outingDetails.getRepeats() && outingDetails.getRepeatsCount() != null) { - squadlistApi.createOuting(newOuting, outingDetails.getRepeatsCount()); - } else { - squadlistApi.createOuting(newOuting, null); - } - - final String outingsViewForNewOutingsSquadAndMonth = urlBuilder.outings(newOuting.getSquad(), new DateTime(newOuting.getDate()).toString("yyyy-MM")); - return new ModelAndView(new RedirectView(outingsViewForNewOutingsSquadAndMonth)); - - } catch (InvalidOutingException e) { - result.addError(new ObjectError("outing", e.getMessage())); - return renderNewOutingForm(outingDetails); - - } catch (Exception e) { - result.addError(new ObjectError("outing", "Unknown error")); - return renderNewOutingForm(outingDetails); - } - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/edit", method = RequestMethod.GET) - public ModelAndView outingEdit(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - - final OutingDetails outingDetails = new OutingDetails(new LocalDateTime(outing.getDate())); - outingDetails.setSquad(outing.getSquad().getId()); - outingDetails.setNotes(outing.getNotes()); - - return renderEditOutingForm(outingDetails, outing); - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/delete", method = RequestMethod.GET) - public ModelAndView deleteOutingPrompt(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - final Outing outing = loggedInUserApi.getOuting(id); - return viewFactory.getViewForLoggedInUser("deleteOuting").addObject("outing", outing); - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/delete", method = RequestMethod.POST) - public ModelAndView deleteOuting(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - final Outing outing = loggedInUserApi.getOuting(id); - - squadlistApi.deleteOuting(outing.getId()); - - final String exitUrl = outing.getSquad() == null ? urlBuilder.outings(outing.getSquad()) : urlBuilder.outingsUrl(); - return new ModelAndView(new RedirectView(exitUrl)); - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/close", method = RequestMethod.GET) - public ModelAndView closeOuting(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - - log.info("Closing outing: " + outing); - outing.setClosed(true); - loggedInUserApi.updateOuting(outing); - - return redirectToOuting(outing); - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/reopen", method = RequestMethod.GET) - public ModelAndView reopenOuting(@PathVariable String id) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - - log.info("Reopening outing: " + outing); - outing.setClosed(false); - loggedInUserApi.updateOuting(outing); - - return redirectToOuting(outing); - } - - @RequiresOutingPermission(permission = Permission.EDIT_OUTING) - @RequestMapping(value = "/outings/{id}/edit", method = RequestMethod.POST) - public ModelAndView editOutingSubmit(@PathVariable String id, - @Valid @ModelAttribute("outing") OutingDetails outingDetails, BindingResult result) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(id); - if (result.hasErrors()) { - return renderEditOutingForm(outingDetails, outing); - } - try { - final Outing updatedOuting = buildOutingFromOutingDetails(outingDetails, instanceSpecificApiClient.getInstance()); - updatedOuting.setId(id); - - loggedInUserApi.updateOuting(updatedOuting); - return redirectToOuting(updatedOuting); - - } catch (InvalidOutingException e) { - result.addError(new ObjectError("outing", e.getMessage())); - return renderEditOutingForm(outingDetails, outing); - - } catch (Exception e) { - log.error(e); - result.addError(new ObjectError("outing", "Unknown exception")); - return renderEditOutingForm(outingDetails, outing); - } - } - - @RequestMapping(value = "/availability/ajax", method = RequestMethod.POST) - public ModelAndView updateAvailability( - @RequestParam(value = "outing", required = true) String outingId, - @RequestParam(value = "availability", required = true) String availability) throws Exception { - SquadlistApi loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.getLoggedInMembersToken()); - - final Outing outing = loggedInUserApi.getOuting(outingId); - - if (!outing.isClosed()) { - final OutingAvailability result = loggedInUserApi.setOutingAvailability(loggedInUserService.getLoggedInMember(), outing, getAvailabilityOptionById(availability)); - return viewFactory.getViewForLoggedInUser("includes/availability").addObject("availability", result.getAvailabilityOption()); - } - - throw new OutingClosedException(); - } - - private ModelAndView redirectToOuting(final Outing updatedOuting) { - return new ModelAndView(new RedirectView(urlBuilder.outingUrl(updatedOuting))); - } - - private ModelAndView renderNewOutingForm(OutingDetails outingDetails) throws UnknownMemberException, UnknownSquadException, UnknownInstanceException, SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("newOuting"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - final Squad squad = preferedSquadService.resolveSquad(null); - mv.addObject("squad", squad); - mv.addObject("outingMonths", getOutingMonthsFor(squad)); - mv.addObject("outing", outingDetails); - return mv; - } - - private ModelAndView renderEditOutingForm(OutingDetails outingDetails, Outing outing) throws UnknownMemberException, UnknownSquadException, UnknownInstanceException, SignedInMemberRequiredException { - final ModelAndView mv = viewFactory.getViewForLoggedInUser("editOuting"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - mv.addObject("squad", outing.getSquad()); - mv.addObject("outing", outingDetails); - mv.addObject("outingObject", outing); - mv.addObject("outingMonths", getOutingMonthsFor(outing.getSquad())); - mv.addObject("month", ISODateTimeFormat.yearMonth().print(outing.getDate().getTime())); // TODO push to date parser - local time - return mv; - } - - private AvailabilityOption getAvailabilityOptionById(String availabilityId) throws JsonParseException, JsonMappingException, HttpFetchException, IOException, UnknownAvailabilityOptionException { - if (Strings.isNullOrEmpty(availabilityId)) { - return null; - } - return instanceSpecificApiClient.getAvailabilityOption(availabilityId); - } - - private Map getOutingMonthsFor(final Squad squad) { - return instanceSpecificApiClient.getOutingMonths(squad); - } - - private Outing buildOutingFromOutingDetails(OutingDetails outingDetails, Instance instance) throws UnknownSquadException { - Date date = outingDetails.toLocalTime().toDateTime(DateTimeZone.forID(instance.getTimeZone())).toDate(); - Squad squad = outingDetails.getSquad() != null ? squadlistApi.getSquad(outingDetails.getSquad()) : null; // TODO validation - String notes = outingDetails.getNotes(); - return new Outing(squad, date, notes); - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/OutingsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/OutingsController.kt new file mode 100644 index 00000000..2bc86cbc --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/OutingsController.kt @@ -0,0 +1,324 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Strings +import com.google.common.collect.Lists +import org.apache.log4j.Logger +import org.codehaus.jackson.JsonParseException +import org.codehaus.jackson.map.JsonMappingException +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.LocalDateTime +import org.joda.time.format.ISODateTimeFormat +import org.springframework.stereotype.Controller +import org.springframework.validation.BindingResult +import org.springframework.validation.ObjectError +import org.springframework.web.bind.annotation.* +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.view.RedirectView +import uk.co.eelpieconsulting.common.http.HttpFetchException +import uk.co.squadlist.web.annotations.RequiresOutingPermission +import uk.co.squadlist.web.annotations.RequiresPermission +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.exceptions.InvalidOutingException +import uk.co.squadlist.web.exceptions.OutingClosedException +import uk.co.squadlist.web.exceptions.UnknownAvailabilityOptionException +import uk.co.squadlist.web.model.AvailabilityOption +import uk.co.squadlist.web.model.Instance +import uk.co.squadlist.web.model.Outing +import uk.co.squadlist.web.model.Squad +import uk.co.squadlist.web.model.forms.OutingDetails +import uk.co.squadlist.web.services.OutingAvailabilityCountsService +import uk.co.squadlist.web.services.Permission +import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.services.filters.ActiveMemberFilter +import uk.co.squadlist.web.urls.UrlBuilder +import uk.co.squadlist.web.views.CsvOutputRenderer +import uk.co.squadlist.web.views.DateFormatter +import uk.co.squadlist.web.views.DateHelper +import uk.co.squadlist.web.views.ViewFactory +import java.io.IOException +import java.util.* +import javax.servlet.http.HttpServletResponse +import javax.validation.Valid + +@Controller +class OutingsController(val loggedInUserService: LoggedInUserService, val instanceSpecificApiClient: InstanceSpecificApiClient, val urlBuilder: UrlBuilder, + val dateFormatter: DateFormatter, val preferedSquadService: PreferedSquadService, val viewFactory: ViewFactory, + val outingAvailabilityCountsService: OutingAvailabilityCountsService, val activeMemberFilter: ActiveMemberFilter, + val csvOutputRenderer: CsvOutputRenderer, val squadlistApiFactory: SquadlistApiFactory) { + + private val log = Logger.getLogger(OutingsController::class.java) + + private val squadlistApi: SquadlistApi = squadlistApiFactory.createClient() + + @GetMapping("/outings") + fun outings(@RequestParam(required = false, value = "squad") squadId: String, + @RequestParam(value = "month", required = false) month: String?): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) // TODO why is this unused! + + val squadToShow = preferedSquadService.resolveSquad(squadId) + val mv = viewFactory.getViewForLoggedInUser("outings") + if (squadToShow == null) { + mv.addObject("title", "Outings") + return mv + } + + var startDate = DateHelper.startOfCurrentOutingPeriod().toDate() + var endDate = DateHelper.endOfCurrentOutingPeriod().toDate() + + var title = squadToShow.name + " outings" + if (month != null) { + val monthDateTime = ISODateTimeFormat.yearMonth().parseDateTime(month) // TODO Can be moved to spring? + startDate = monthDateTime.toDate() + endDate = monthDateTime.plusMonths(1).toDate() + title = squadToShow.name + " outings - " + dateFormatter.fullMonthYear(startDate) + } else { + mv.addObject("current", true) + } + + mv.addObject("title", title) + mv.addObject("squad", squadToShow) + mv.addObject("startDate", startDate) + mv.addObject("endDate", endDate) + mv.addObject("month", month) + mv.addObject("outingMonths", getOutingMonthsFor(squadToShow)) + + val squadOutings = squadlistApi.getSquadAvailability(squadToShow.id, startDate, endDate) + mv.addObject("outings", squadOutings) + mv.addObject("outingAvailabilityCounts", outingAvailabilityCountsService.buildOutingAvailabilityCounts(squadOutings)) + + mv.addObject("squads", instanceSpecificApiClient.squads) + return mv + } + + @GetMapping("/outings/{id}") + fun outing(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + + val mv = viewFactory.getViewForLoggedInUser("outing") + mv.addObject("title", outing.squad.name + " - " + dateFormatter.dayMonthYearTime(outing.date)) + mv.addObject("outing", outing) + mv.addObject("outingMonths", getOutingMonthsFor(outing.squad)) + mv.addObject("squad", outing.squad) + mv.addObject("squadAvailability", loggedInUserApi.getOutingAvailability(outing.id)) + mv.addObject("squads", instanceSpecificApiClient.squads) + mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(outing.squad.id))) + mv.addObject("month", ISODateTimeFormat.yearMonth().print(outing.date.time)) // TODO push to date parser - local time + return mv + } + + @GetMapping("/outings/{id}.csv") + fun outingCsv(@PathVariable id: String, response: HttpServletResponse) { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + val squadMembers = squadlistApi.getSquadMembers(outing.squad.id) + val outingAvailability = loggedInUserApi.getOutingAvailability(outing.id) + + val rows = Lists.newArrayList>() + for (member in squadMembers) { + rows.add(Arrays.asList( + dateFormatter.dayMonthYearTime(outing.date), + outing.squad.name, + member.displayName, + member.role, + if (outingAvailability[member.id] != null) outingAvailability[member.id]!!.getLabel() else null + )) + } + csvOutputRenderer.renderCsvResponse(response, Lists.newArrayList("Date", "Squad", "Member", "Role", "Availability"), rows) + } + + @RequiresPermission(permission = Permission.ADD_OUTING) + @GetMapping("/outings/new") // TODO fails hard if no squads are available + fun newOuting(): ModelAndView { + val defaultOutingDateTime = DateHelper.defaultOutingStartDateTime() + val outingDefaults = OutingDetails(defaultOutingDateTime) + outingDefaults.squad = preferedSquadService.resolveSquad(null).id + return renderNewOutingForm(outingDefaults) + } + + @RequiresPermission(permission = Permission.ADD_OUTING) + @PostMapping("/outings/new") + fun newOutingSubmit(@Valid @ModelAttribute("outing") outingDetails: OutingDetails, result: BindingResult): ModelAndView { + if (result.hasErrors()) { + return renderNewOutingForm(outingDetails) + } + + try { + val newOuting = buildOutingFromOutingDetails(outingDetails, instanceSpecificApiClient.instance) + if (outingDetails.repeats != null && outingDetails.repeats!! && outingDetails.repeatsCount != null) { + squadlistApi.createOuting(newOuting, outingDetails.repeatsCount) + } else { + squadlistApi.createOuting(newOuting, null) + } + + val outingsViewForNewOutingsSquadAndMonth = urlBuilder.outings(newOuting.squad, DateTime(newOuting.date).toString("yyyy-MM")) + return ModelAndView(RedirectView(outingsViewForNewOutingsSquadAndMonth)) + + } catch (e: InvalidOutingException) { + result.addError(ObjectError("outing", e.message)) + return renderNewOutingForm(outingDetails) + + } catch (e: Exception) { + result.addError(ObjectError("outing", "Unknown error")) + return renderNewOutingForm(outingDetails) + } + + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @GetMapping("/outings/{id}/edit") + fun outingEdit(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + + val outingDetails = OutingDetails(LocalDateTime(outing.date)) + outingDetails.squad = outing.squad.id + outingDetails.notes = outing.notes + + return renderEditOutingForm(outingDetails, outing) + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @RequestMapping("/outings/{id}/delete", method = arrayOf(RequestMethod.GET)) + fun deleteOutingPrompt(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + val outing = loggedInUserApi.getOuting(id) + return viewFactory.getViewForLoggedInUser("deleteOuting").addObject("outing", outing) + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @PostMapping("/outings/{id}/delete") + fun deleteOuting(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + val outing = loggedInUserApi.getOuting(id) + + squadlistApi.deleteOuting(outing.id) + + val exitUrl = if (outing.squad == null) urlBuilder.outings(outing.squad) else urlBuilder.outingsUrl() + return ModelAndView(RedirectView(exitUrl)) + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @GetMapping("/outings/{id}/close") + fun closeOuting(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + + log.info("Closing outing: $outing") + outing.isClosed = true + loggedInUserApi.updateOuting(outing) + + return redirectToOuting(outing) + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @GetMapping("/outings/{id}/reopen") + fun reopenOuting(@PathVariable id: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + + log.info("Reopening outing: $outing") + outing.isClosed = false + loggedInUserApi.updateOuting(outing) + + return redirectToOuting(outing) + } + + @RequiresOutingPermission(permission = Permission.EDIT_OUTING) + @PostMapping("/outings/{id}/edit") + fun editOutingSubmit(@PathVariable id: String, + @Valid @ModelAttribute("outing") outingDetails: OutingDetails, result: BindingResult): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(id) + if (result.hasErrors()) { + return renderEditOutingForm(outingDetails, outing) + } + try { + val updatedOuting = buildOutingFromOutingDetails(outingDetails, instanceSpecificApiClient.instance) + updatedOuting.id = id + + loggedInUserApi.updateOuting(updatedOuting) + return redirectToOuting(updatedOuting) + + } catch (e: InvalidOutingException) { + result.addError(ObjectError("outing", e.message)) + return renderEditOutingForm(outingDetails, outing) + + } catch (e: Exception) { + log.error(e) + result.addError(ObjectError("outing", "Unknown exception")) + return renderEditOutingForm(outingDetails, outing) + } + + } + + @PostMapping("/availability/ajax") + fun updateAvailability( + @RequestParam(value = "outing", required = true) outingId: String, + @RequestParam(value = "availability", required = true) availability: String): ModelAndView { + val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) + + val outing = loggedInUserApi.getOuting(outingId) + + if (!outing.isClosed) { + val result = loggedInUserApi.setOutingAvailability(loggedInUserService.loggedInMember, outing, getAvailabilityOptionById(availability)) + return viewFactory.getViewForLoggedInUser("includes/availability").addObject("availability", result.availabilityOption) + } + + throw OutingClosedException() + } + + private fun redirectToOuting(updatedOuting: Outing): ModelAndView { + return ModelAndView(RedirectView(urlBuilder.outingUrl(updatedOuting))) + } + + private fun renderNewOutingForm(outingDetails: OutingDetails): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("newOuting") + mv.addObject("squads", instanceSpecificApiClient.squads) + val squad = preferedSquadService.resolveSquad(null) + mv.addObject("squad", squad) + mv.addObject("outingMonths", getOutingMonthsFor(squad)) + mv.addObject("outing", outingDetails) + return mv + } + + private fun renderEditOutingForm(outingDetails: OutingDetails, outing: Outing): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("editOuting") + mv.addObject("squads", instanceSpecificApiClient.squads) + mv.addObject("squad", outing.squad) + mv.addObject("outing", outingDetails) + mv.addObject("outingObject", outing) + mv.addObject("outingMonths", getOutingMonthsFor(outing.squad)) + mv.addObject("month", ISODateTimeFormat.yearMonth().print(outing.date.time)) // TODO push to date parser - local time + return mv + } + + @Throws(JsonParseException::class, JsonMappingException::class, HttpFetchException::class, IOException::class, UnknownAvailabilityOptionException::class) + private fun getAvailabilityOptionById(availabilityId: String): AvailabilityOption? { + return if (Strings.isNullOrEmpty(availabilityId)) { + null + } else instanceSpecificApiClient.getAvailabilityOption(availabilityId) + } + + private fun getOutingMonthsFor(squad: Squad?): Map { + return instanceSpecificApiClient.getOutingMonths(squad) + } + + private fun buildOutingFromOutingDetails(outingDetails: OutingDetails, instance: Instance): Outing { + val date = outingDetails.toLocalTime().toDateTime(DateTimeZone.forID(instance.timeZone)).toDate() + val squad = if (outingDetails.squad != null) squadlistApi.getSquad(outingDetails.squad) else null // TODO validation + val notes = outingDetails.notes + return Outing(squad, date, notes) + } + +} From 8fede7b68adefd7e586f507c68a88d8bef20b3f0 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Mon, 3 Dec 2018 17:55:32 +0000 Subject: [PATCH 51/74] Migrate Guava orderings to Kotlin. --- .../controllers/ContactsModelPopulator.java | 114 ------------------ .../web/controllers/ContactsModelPopulator.kt | 75 ++++++++++++ 2 files changed, 75 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/ContactsModelPopulator.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/ContactsModelPopulator.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/ContactsModelPopulator.java b/src/main/java/uk/co/squadlist/web/controllers/ContactsModelPopulator.java deleted file mode 100644 index 0dd19233..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/ContactsModelPopulator.java +++ /dev/null @@ -1,114 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.util.List; -import java.util.Set; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.annotations.RequiresSquadPermission; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.auth.LoggedInUserService; -import uk.co.squadlist.web.exceptions.SignedInMemberRequiredException; -import uk.co.squadlist.web.exceptions.UnknownInstanceException; -import uk.co.squadlist.web.exceptions.UnknownMemberException; -import uk.co.squadlist.web.model.Instance; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.services.Permission; -import uk.co.squadlist.web.services.PermissionsService; -import uk.co.squadlist.web.services.filters.ActiveMemberFilter; - -import com.google.common.base.Function; -import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; - -@Component -public class ContactsModelPopulator { - - private static Function roleName = new Function() { - @Override - public String apply(Member obj) { - return obj.getRole(); - } - }; - - public ContactsModelPopulator() { - } - - private static Function firstName = new Function() { - @Override - public String apply(Member obj) { - return obj.getFirstName(); - } - }; - - private static Function lastName = new Function() { - @Override - public String apply(Member obj) { - return obj.getLastName(); - } - }; - - private final static Ordering byLastName = Ordering.natural().nullsLast().onResultOf(lastName); - private final static Ordering byFirstName = Ordering.natural().nullsLast().onResultOf(firstName); - private final static Ordering byRole = Ordering.natural().nullsLast().onResultOf(roleName); - private final static Ordering byRoleThenFirstName = byRole.compound(byFirstName); - private final static Ordering byRoleThenLastName = byRole.compound(byLastName); - - private InstanceSpecificApiClient instanceSpecificApiClient; - private LoggedInUserService loggedInUserService; - private PermissionsService permissionsService; - private ActiveMemberFilter activeMemberFilter; - private SquadlistApi squadlistApi; - - @Autowired - public ContactsModelPopulator(InstanceSpecificApiClient instanceSpecificApiClient, LoggedInUserService loggedInUserService, PermissionsService permissionsService, - ActiveMemberFilter activeMemberFilter, SquadlistApiFactory squadlistApiFactory) { - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.loggedInUserService = loggedInUserService; - this.permissionsService = permissionsService; - this.activeMemberFilter = activeMemberFilter; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequiresSquadPermission(permission=Permission.VIEW_SQUAD_CONTACT_DETAILS) - public void populateModel(final Squad squad, final ModelAndView mv) throws UnknownMemberException, UnknownInstanceException, SignedInMemberRequiredException { - mv.addObject("title", squad.getName() + " contacts"); - mv.addObject("squad", squad); - - Instance instance = instanceSpecificApiClient.getInstance(); - Ordering byRoleThenName = instance.getMemberOrdering() != null && instance.getMemberOrdering().equals("firstName") ? byRoleThenFirstName : byRoleThenLastName; - - final List activeMembers = byRoleThenName.sortedCopy(activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squad.getId()))); - final List redactedMembers = redactContentDetailsForMembers(loggedInUserService.getLoggedInMember(), activeMembers); - mv.addObject("members", redactedMembers); - - final Set emails = Sets.newHashSet(); - for (Member member : redactedMembers) { - if (!Strings.isNullOrEmpty(member.getEmailAddress())) { - emails.add(member.getEmailAddress()); - } - } - if (!emails.isEmpty()) { - mv.addObject("emails", Lists.newArrayList(emails)); - } - } - - private List redactContentDetailsForMembers(Member loggedInMember, List members) { - List redactedMembers = Lists.newArrayList(); - for (Member member : members) { - if (!permissionsService.canSeePhoneNumberForRower(loggedInMember, member)) { - member.setContactNumber(null); - } - redactedMembers.add(member); - } - return redactedMembers; - } - -} \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsModelPopulator.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsModelPopulator.kt new file mode 100644 index 00000000..6f5d8146 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/ContactsModelPopulator.kt @@ -0,0 +1,75 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.base.Function +import com.google.common.base.Strings +import com.google.common.collect.Lists +import com.google.common.collect.Ordering +import com.google.common.collect.Sets +import org.springframework.stereotype.Component +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.annotations.RequiresSquadPermission +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.auth.LoggedInUserService +import uk.co.squadlist.web.model.Member +import uk.co.squadlist.web.model.Squad +import uk.co.squadlist.web.services.Permission +import uk.co.squadlist.web.services.PermissionsService +import uk.co.squadlist.web.services.filters.ActiveMemberFilter + +@Component +class ContactsModelPopulator(val instanceSpecificApiClient: InstanceSpecificApiClient, + val loggedInUserService: LoggedInUserService, + val permissionsService: PermissionsService, + val activeMemberFilter: ActiveMemberFilter, val squadlistApiFactory: SquadlistApiFactory) { + + private val squadlistApi: SquadlistApi = squadlistApiFactory.createClient() + + val role = Function { i -> i?.getRole() } + val firstName = Function { i -> i?.getFirstName() } + val lastName = Function { i -> i?.getLastName() } + + val byLastName = Ordering.natural().nullsLast().onResultOf(lastName) + val byFirstName = Ordering.natural().nullsLast().onResultOf(firstName) + + val byRole = Ordering.natural().nullsLast().onResultOf(role) + val byRoleThenFirstName = byRole.compound(byFirstName) + val byRoleThenLastName = byRole.compound(byLastName) + + + @RequiresSquadPermission(permission = Permission.VIEW_SQUAD_CONTACT_DETAILS) + fun populateModel(squad: Squad, mv: ModelAndView) { + mv.addObject("title", squad.name + " contacts") + mv.addObject("squad", squad) + + val instance = instanceSpecificApiClient.instance + val byRoleThenName = if (instance.memberOrdering != null && instance.memberOrdering == "firstName") byRoleThenFirstName else byRoleThenLastName + + val activeMembers = byRoleThenName.sortedCopy(activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squad.id))) + val redactedMembers = redactContentDetailsForMembers(loggedInUserService.loggedInMember, activeMembers) + mv.addObject("members", redactedMembers) + + val emails = Sets.newHashSet() + for (member in redactedMembers) { + if (!Strings.isNullOrEmpty(member.emailAddress)) { + emails.add(member.emailAddress) + } + } + if (!emails.isEmpty()) { + mv.addObject("emails", Lists.newArrayList(emails)) + } + } + + private fun redactContentDetailsForMembers(loggedInMember: Member, members: List): List { + val redactedMembers = Lists.newArrayList() + for (member in members) { + if (!permissionsService.canSeePhoneNumberForRower(loggedInMember, member)) { + member.contactNumber = null + } + redactedMembers.add(member) + } + return redactedMembers + } + +} \ No newline at end of file From a776efe66896a921cf9b95e850f78b8cfe30efa5 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Tue, 4 Dec 2018 09:28:05 +0000 Subject: [PATCH 52/74] Moves to Kotlin. --- .../controllers/AvailabilityController.java | 104 ------------------ .../EntryDetailsModelPopulator.java | 84 -------------- .../web/controllers/AvailabilityController.kt | 82 ++++++++++++++ .../web/controllers/EntryDetailsController.kt | 8 +- .../controllers/EntryDetailsModelPopulator.kt | 57 ++++++++++ 5 files changed, 145 insertions(+), 190 deletions(-) delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/AvailabilityController.java delete mode 100644 src/main/java/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.java create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/AvailabilityController.kt create mode 100644 src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.kt diff --git a/src/main/java/uk/co/squadlist/web/controllers/AvailabilityController.java b/src/main/java/uk/co/squadlist/web/controllers/AvailabilityController.java deleted file mode 100644 index 65ff2dc8..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/AvailabilityController.java +++ /dev/null @@ -1,104 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.model.AvailabilityOption; -import uk.co.squadlist.web.model.Outing; -import uk.co.squadlist.web.model.OutingWithSquadAvailability; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.services.PreferedSquadService; -import uk.co.squadlist.web.services.filters.ActiveMemberFilter; -import uk.co.squadlist.web.views.DateHelper; -import uk.co.squadlist.web.views.ViewFactory; - -import com.google.common.collect.Maps; - -@Controller -public class AvailabilityController { - - private InstanceSpecificApiClient instanceSpecificApiClient; - private PreferedSquadService preferedSquadService; - private ViewFactory viewFactory; - private ActiveMemberFilter activeMemberFilter; - private SquadlistApi squadlistApi; - - @Autowired - public AvailabilityController(InstanceSpecificApiClient instanceSpecificApiClient, PreferedSquadService preferedSquadService, ViewFactory viewFactory, - ActiveMemberFilter activeMemberFilter, SquadlistApiFactory squadlistApiFactory) { - this.instanceSpecificApiClient = instanceSpecificApiClient; - this.preferedSquadService = preferedSquadService; - this.viewFactory = viewFactory; - this.activeMemberFilter = activeMemberFilter; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequestMapping("/availability") - public ModelAndView availability() throws Exception { - ModelAndView mv = viewFactory.getViewForLoggedInUser("availability"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - return mv; - } - - @RequestMapping("/availability/{squadId}") - public ModelAndView squadAvailability(@PathVariable String squadId, @RequestParam(value = "month", required = false) String month) throws Exception { - ModelAndView mv = viewFactory.getViewForLoggedInUser("availability"); - mv.addObject("squads", instanceSpecificApiClient.getSquads()); - - final Squad squad = preferedSquadService.resolveSquad(squadId); - - if (squad != null) { - mv.addObject("squad", squad); - mv.addObject("title", squad.getName() + " availability"); - mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squad.getId()))); - - if (squadlistApi.getSquadMembers(squad.getId()).isEmpty()) { - return mv; - } - - Date startDate = DateHelper.startOfCurrentOutingPeriod().toDate(); - Date endDate = DateHelper.endOfCurrentOutingPeriod().toDate(); - if (month != null) { - final DateTime monthDateTime = ISODateTimeFormat.yearMonth().parseDateTime(month); // TODO Can be moved to spring? - startDate = monthDateTime.toDate(); - endDate = monthDateTime.plusMonths(1).toDate(); - } else { - mv.addObject("current", true); - } - - final List squadAvailability = squadlistApi.getSquadAvailability(squad.getId(), startDate, endDate); - final List outings = instanceSpecificApiClient.getSquadOutings(squad, startDate, endDate); - - mv.addObject("squadAvailability", decorateOutingsWithMembersAvailability(squadAvailability, outings)); - mv.addObject("outings", outings); - mv.addObject("outingMonths", instanceSpecificApiClient.getOutingMonths(squad)); - mv.addObject("month", month); - } - return mv; - } - - private Map decorateOutingsWithMembersAvailability(final List squadAvailability, final List outings) { - final Map allAvailability = Maps.newHashMap(); - for (OutingWithSquadAvailability outingWithSquadAvailability : squadAvailability) { - final Map outingAvailability = outingWithSquadAvailability.getAvailability(); - for (String member : outingAvailability.keySet()) { - allAvailability.put(outingWithSquadAvailability.getOuting().getId() + "-" + member, outingAvailability.get(member)); - } - } - return allAvailability; - } - -} diff --git a/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.java b/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.java deleted file mode 100644 index 65f66ed6..00000000 --- a/src/main/java/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.java +++ /dev/null @@ -1,84 +0,0 @@ -package uk.co.squadlist.web.controllers; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.ModelAndView; - -import uk.co.squadlist.web.annotations.RequiresSquadPermission; -import uk.co.squadlist.web.api.InstanceSpecificApiClient; -import uk.co.squadlist.web.api.SquadlistApi; -import uk.co.squadlist.web.api.SquadlistApiFactory; -import uk.co.squadlist.web.context.GoverningBodyFactory; -import uk.co.squadlist.web.localisation.GoverningBody; -import uk.co.squadlist.web.model.Member; -import uk.co.squadlist.web.model.Squad; -import uk.co.squadlist.web.services.Permission; -import uk.co.squadlist.web.services.filters.ActiveMemberFilter; -import uk.co.squadlist.web.views.DateFormatter; - -import com.google.common.collect.Lists; - -@Component -public class EntryDetailsModelPopulator { - - private DateFormatter dateFormatter; - private ActiveMemberFilter activeMemberFilter; - private GoverningBodyFactory governingBodyFactory; - private SquadlistApi squadlistApi; - - public EntryDetailsModelPopulator() { - } - - @Autowired - public EntryDetailsModelPopulator(DateFormatter dateFormatter, ActiveMemberFilter activeMemberFilter, - GoverningBodyFactory governingBodyFactory, SquadlistApiFactory squadlistApiFactory) { - this.dateFormatter = dateFormatter; - this.activeMemberFilter = activeMemberFilter; - this.governingBodyFactory = governingBodyFactory; - this.squadlistApi = squadlistApiFactory.createClient(); - } - - @RequiresSquadPermission(permission=Permission.VIEW_SQUAD_ENTRY_DETAILS) - public void populateModel(final Squad squadToShow, final ModelAndView mv) { - mv.addObject("squad", squadToShow); - mv.addObject("title", squadToShow.getName() + " entry details"); - mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squadToShow.getId()))); - } - - @RequiresSquadPermission(permission=Permission.VIEW_SQUAD_ENTRY_DETAILS) - public List> getEntryDetailsRows(Squad squadToShow) { - return getEntryDetailsRows(activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squadToShow.getId()))); - } - - public List> getEntryDetailsRows(List members) { // TOOD permissions - final GoverningBody governingBody = governingBodyFactory.getGoverningBody(); - - final List> rows = Lists.newArrayList(); - for (Member member : members) { - final Integer effectiveAge = member.getDateOfBirth() != null ? governingBody.getEffectiveAge(member.getDateOfBirth()) : null; - final String ageGrade = effectiveAge != null ? governingBody.getAgeGrade(effectiveAge) : null; - - rows.add(Arrays.asList(member.getFirstName(), member.getLastName(), - member.getDateOfBirth() != null ? dateFormatter.dayMonthYear(member.getDateOfBirth()) : "", - effectiveAge != null ? effectiveAge.toString() : "", - ageGrade != null ? ageGrade : "", - member.getWeight() != null ? member.getWeight().toString() : "", - member.getRowingPoints(), - governingBody.getRowingStatus(member.getRowingPoints()), - member.getScullingPoints(), - governingBody.getScullingStatus(member.getScullingPoints()), - member.getRegistrationNumber())); - } - return rows; - } - - public List getEntryDetailsHeaders() { - return Lists.newArrayList("First name", "Last name", "Date of birth", "Effective age", "Age grade", - "Weight", "Rowing points", "Rowing status", - "Sculling points", "Sculling status", "Registration number"); - } - -} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/AvailabilityController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/AvailabilityController.kt new file mode 100644 index 00000000..eb53cc24 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/AvailabilityController.kt @@ -0,0 +1,82 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.collect.Maps +import org.joda.time.format.ISODateTimeFormat +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.api.InstanceSpecificApiClient +import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.model.AvailabilityOption +import uk.co.squadlist.web.model.Outing +import uk.co.squadlist.web.model.OutingWithSquadAvailability +import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.services.filters.ActiveMemberFilter +import uk.co.squadlist.web.views.DateHelper +import uk.co.squadlist.web.views.ViewFactory + +@Controller +class AvailabilityController(private val instanceSpecificApiClient: InstanceSpecificApiClient, private val preferedSquadService: PreferedSquadService, + private val viewFactory: ViewFactory, + private val activeMemberFilter: ActiveMemberFilter, squadlistApiFactory: SquadlistApiFactory) { + + private val squadlistApi: SquadlistApi = squadlistApiFactory.createClient() + + @GetMapping("/availability") + fun availability(): ModelAndView { + return viewFactory.getViewForLoggedInUser("availability"). + addObject("squads", instanceSpecificApiClient.squads) + } + + @GetMapping("/availability/{squadId}") + fun squadAvailability(@PathVariable squadId: String, @RequestParam(value = "month", required = false) month: String?): ModelAndView { + val mv = viewFactory.getViewForLoggedInUser("availability") + mv.addObject("squads", instanceSpecificApiClient.squads) + + val squad = preferedSquadService.resolveSquad(squadId) + + if (squad != null) { + mv.addObject("squad", squad) + mv.addObject("title", squad.name + " availability") + mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squad.id))) + + if (squadlistApi.getSquadMembers(squad.id).isEmpty()) { + return mv + } + + var startDate = DateHelper.startOfCurrentOutingPeriod().toDate() + var endDate = DateHelper.endOfCurrentOutingPeriod().toDate() + if (month != null) { + val monthDateTime = ISODateTimeFormat.yearMonth().parseDateTime(month) // TODO Can be moved to spring? + startDate = monthDateTime.toDate() + endDate = monthDateTime.plusMonths(1).toDate() + } else { + mv.addObject("current", true) + } + + val squadAvailability = squadlistApi.getSquadAvailability(squad.id, startDate, endDate) + val outings = instanceSpecificApiClient.getSquadOutings(squad, startDate, endDate) + + mv.addObject("squadAvailability", decorateOutingsWithMembersAvailability(squadAvailability, outings)) + mv.addObject("outings", outings) + mv.addObject("outingMonths", instanceSpecificApiClient.getOutingMonths(squad)) + mv.addObject("month", month) + } + return mv + } + + private fun decorateOutingsWithMembersAvailability(squadAvailability: List, outings: List): Map { + val allAvailability = Maps.newHashMap() + for (outingWithSquadAvailability in squadAvailability) { + val outingAvailability = outingWithSquadAvailability.availability + for (member in outingAvailability.keys) { + allAvailability[outingWithSquadAvailability.outing.id + "-" + member] = outingAvailability[member] + } + } + return allAvailability + } + +} diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt index b913824f..e27f8f80 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt @@ -33,6 +33,10 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci private val squadlistApi = squadlistApiFactory.createClient() + private val entryDetailsHeaders: List = Lists.newArrayList("First name", "Last name", "Date of birth", "Effective age", "Age grade", + "Weight", "Rowing points", "Rowing status", + "Sculling points", "Sculling status", "Registration number") + @GetMapping("/entrydetails/{squadId}") fun entrydetails(@PathVariable squadId: String): ModelAndView { val mv = viewFactory.getViewForLoggedInUser("entryDetails") @@ -102,7 +106,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci val squadMembers = squadlistApi.getSquadMembers(squadToShow.id) val entryDetailsRows = entryDetailsModelPopulator.getEntryDetailsRows(squadMembers) - csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.entryDetailsHeaders, entryDetailsRows) + csvOutputRenderer.renderCsvResponse(response, entryDetailsHeaders, entryDetailsRows) } @GetMapping("/entrydetails/selected.csv") // TODO Unused @@ -116,7 +120,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci selectedMembers.add(squadlistApi.getMember(selectedMemberId)) } - csvOutputRenderer.renderCsvResponse(response, entryDetailsModelPopulator.entryDetailsHeaders, entryDetailsModelPopulator.getEntryDetailsRows(selectedMembers)) + csvOutputRenderer.renderCsvResponse(response, entryDetailsHeaders, entryDetailsModelPopulator.getEntryDetailsRows(selectedMembers)) } } \ No newline at end of file diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.kt new file mode 100644 index 00000000..60eb0173 --- /dev/null +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsModelPopulator.kt @@ -0,0 +1,57 @@ +package uk.co.squadlist.web.controllers + +import com.google.common.collect.Lists +import org.springframework.stereotype.Component +import org.springframework.web.servlet.ModelAndView +import uk.co.squadlist.web.annotations.RequiresSquadPermission +import uk.co.squadlist.web.api.SquadlistApi +import uk.co.squadlist.web.api.SquadlistApiFactory +import uk.co.squadlist.web.context.GoverningBodyFactory +import uk.co.squadlist.web.model.Member +import uk.co.squadlist.web.model.Squad +import uk.co.squadlist.web.services.Permission +import uk.co.squadlist.web.services.filters.ActiveMemberFilter +import uk.co.squadlist.web.views.DateFormatter +import java.util.* + +@Component +class EntryDetailsModelPopulator (val dateFormatter: DateFormatter, val activeMemberFilter: ActiveMemberFilter, + val governingBodyFactory: GoverningBodyFactory, squadlistApiFactory: SquadlistApiFactory) { + + private val squadlistApi: SquadlistApi = squadlistApiFactory.createClient() + + @RequiresSquadPermission(permission = Permission.VIEW_SQUAD_ENTRY_DETAILS) + fun populateModel(squadToShow: Squad, mv: ModelAndView) { + mv.addObject("squad", squadToShow) + mv.addObject("title", squadToShow.name + " entry details") + mv.addObject("members", activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squadToShow.id))) + } + + @RequiresSquadPermission(permission = Permission.VIEW_SQUAD_ENTRY_DETAILS) + fun getEntryDetailsRows(squadToShow: Squad): List> { + return getEntryDetailsRows(activeMemberFilter.extractActive(squadlistApi.getSquadMembers(squadToShow.id))) + } + + fun getEntryDetailsRows(members: List): List> { // TOOD permissions + val governingBody = governingBodyFactory.governingBody + + val rows = Lists.newArrayList>() + for (member in members) { + val effectiveAge = if (member.dateOfBirth != null) governingBody.getEffectiveAge(member.dateOfBirth) else null + val ageGrade = if (effectiveAge != null) governingBody.getAgeGrade(effectiveAge) else null + + rows.add(Arrays.asList(member.firstName, member.lastName, + if (member.dateOfBirth != null) dateFormatter.dayMonthYear(member.dateOfBirth) else "", + effectiveAge?.toString() ?: "", + ageGrade ?: "", + if (member.weight != null) member.weight!!.toString() else "", + member.rowingPoints, + governingBody.getRowingStatus(member.rowingPoints), + member.scullingPoints, + governingBody.getScullingStatus(member.scullingPoints), + member.registrationNumber)) + } + return rows + } + +} From 9a24ad4cc892b4fc206b74dcf8a5e16baf496a89 Mon Sep 17 00:00:00 2001 From: Tony McCrae Date: Tue, 4 Dec 2018 09:53:49 +0000 Subject: [PATCH 53/74] Entry details work in progress. --- .../web/controllers/EntryDetailsController.kt | 15 ++- .../uk/co/squadlist/web/urls/UrlBuilder.kt | 6 +- src/main/resources/entryDetails.vm | 122 ------------------ src/main/resources/templates/entryDetails.ftl | 112 ++++++++++++++++ .../resources/templates/includes/head.ftl | 32 ++--- 5 files changed, 141 insertions(+), 146 deletions(-) delete mode 100644 src/main/resources/entryDetails.vm create mode 100644 src/main/resources/templates/entryDetails.ftl diff --git a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt index e27f8f80..225b3158 100644 --- a/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt +++ b/src/main/kotlin/uk/co/squadlist/web/controllers/EntryDetailsController.kt @@ -16,7 +16,9 @@ import uk.co.squadlist.web.auth.LoggedInUserService import uk.co.squadlist.web.context.GoverningBodyFactory import uk.co.squadlist.web.model.Member import uk.co.squadlist.web.services.PreferedSquadService +import uk.co.squadlist.web.urls.UrlBuilder import uk.co.squadlist.web.views.CsvOutputRenderer +import uk.co.squadlist.web.views.PermissionsHelper import uk.co.squadlist.web.views.ViewFactory import java.util.* import javax.servlet.http.HttpServletResponse @@ -27,7 +29,8 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci val viewFactory: ViewFactory, val entryDetailsModelPopulator: EntryDetailsModelPopulator, val csvOutputRenderer: CsvOutputRenderer, val governingBodyFactory: GoverningBodyFactory, - val squadlistApiFactory: SquadlistApiFactory, val loggedInUserService: LoggedInUserService) { + val squadlistApiFactory: SquadlistApiFactory, val loggedInUserService: LoggedInUserService, + val urlBuilder: UrlBuilder, val permissionsHelper: PermissionsHelper) { private val log = Logger.getLogger(EntryDetailsController::class.java) @@ -37,18 +40,20 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci "Weight", "Rowing points", "Rowing status", "Sculling points", "Sculling status", "Registration number") - @GetMapping("/entrydetails/{squadId}") + @GetMapping("/entry-details/{squadId}") fun entrydetails(@PathVariable squadId: String): ModelAndView { val mv = viewFactory.getViewForLoggedInUser("entryDetails") mv.addObject("squads", instanceSpecificApiClient.squads) mv.addObject("governingBody", governingBodyFactory.governingBody) + mv.addObject("urlBuilder", urlBuilder) + mv.addObject("permissionsHelper", permissionsHelper) val squadToShow = preferedSquadService.resolveSquad(squadId) entryDetailsModelPopulator.populateModel(squadToShow, mv) return mv } - @GetMapping("/entrydetails/ajax") + @GetMapping("/entry-details/ajax") fun ajax(@RequestBody json: String): ModelAndView { val loggedInUserApi = squadlistApiFactory.createForToken(loggedInUserService.loggedInMembersToken) @@ -98,7 +103,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci return mv } - @GetMapping("/entrydetails/{squadId}.csv") + @GetMapping("/entry-details/{squadId}.csv") fun entrydetailsCSV(@PathVariable squadId: String, response: HttpServletResponse) { viewFactory.getViewForLoggedInUser("entryDetails") // TODO @@ -109,7 +114,7 @@ public class EntryDetailsController(val instanceSpecificApiClient: InstanceSpeci csvOutputRenderer.renderCsvResponse(response, entryDetailsHeaders, entryDetailsRows) } - @GetMapping("/entrydetails/selected.csv") // TODO Unused + @GetMapping("/entry-details/selected.csv") // TODO Unused fun entrydetailsSelectedCSV(@RequestParam members: String, response: HttpServletResponse) { log.info("Selected members: $members") val selectedMembers = Lists.newArrayList() diff --git a/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt index 607d1e09..09616b92 100644 --- a/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt +++ b/src/main/kotlin/uk/co/squadlist/web/urls/UrlBuilder.kt @@ -114,7 +114,7 @@ class UrlBuilder (@param:Value("\${baseUrl}") private val baseUrl: String, } fun entryDetails(squad: Squad): String { - return applicationUrl("/entry-details?squad=" + squad.id) + return applicationUrl("/entry-details/" + squad.id) } fun outings(squad: Squad): String { @@ -146,12 +146,12 @@ class UrlBuilder (@param:Value("\${baseUrl}") private val baseUrl: String, } fun entryDetailsCsv(squad: Squad): String { - return applicationUrl("/entrydetails/" + squad.id + ".csv") + return applicationUrl("/entry-details/" + squad.id + ".csv") } fun entryDetailsCsv(members: List): String { try { - val url = URIBuilder(applicationUrl("/entrydetails/selected.csv")) + val url = URIBuilder(applicationUrl("/entry-details/selected.csv")) val memberIds = Lists.newArrayList() for (member in members) { diff --git a/src/main/resources/entryDetails.vm b/src/main/resources/entryDetails.vm deleted file mode 100644 index e7e3710c..00000000 --- a/src/main/resources/entryDetails.vm +++ /dev/null @@ -1,122 +0,0 @@ -#parse('includes/head.vm') -#navTabs('Entry details') - - -
    - - - -#parse('includes/footer.vm') \ No newline at end of file diff --git a/src/main/resources/templates/entryDetails.ftl b/src/main/resources/templates/entryDetails.ftl new file mode 100644 index 00000000..afa9d7c8 --- /dev/null +++ b/src/main/resources/templates/entryDetails.ftl @@ -0,0 +1,112 @@ +<#include 'includes/head.ftl'> +<@navTabs 'Entry details' /> + +
    + + <#if squads?size != 0> +
    +
    +

    <@spring.message 'entry.details' />

    +
    +
    +
    + <@squadSelect '/entry-details/' 'Show entry details' /> +
    +
    +
    +
    + + <#if members?size == 0> +

    <@spring.message 'no.members.assigned.to.squad' />

    + + <#else> +
    + + + + + + + + + + + + + + <#list members as member> + + + + + + + + + + + +
    <@spring.message 'effective.age' /><@spring.message 'weight' /><@spring.message 'rowing' /><@spring.message 'sculling' /><@spring.message 'registration' />
    <#include 'includes/member.ftl'> + #if($member.dateOfBirth) + $dateFormatter.dayMonthYear($member.dateOfBirth) +
    + #set($ageGrade = "") + #set($ageGrade = $governingBody.getAgeGrade($governingBody.getEffectiveAge($member.dateOfBirth))) + $governingBody.getEffectiveAge($member.dateOfBirth) + #if($ageGrade)
    $ageGrade #end + #end +
    $!member.weight$!member.rowingPoints +
    #if($member.rowingPoints) $!governingBody.getRowingStatus($member.rowingPoints) #end +
    $!member.scullingPoints + #if($member.scullingPoints)
    $!governingBody.getScullingStatus($member.scullingPoints) #end +
    + #set($registrationNumberProblems = '') + #set($registrationNumberProblems = $governingBody.checkRegistrationNumber($member.registrationNumber)) + #if($registrationNumberProblems && $registrationNumberProblems != '') + $!member.registrationNumber +
    + $registrationNumberProblems +
    + #end +
    + + + <#else> +

    <@spring.message 'entry.details' />

    +
    +

    There are currently no squads setup.

    + + +
    +
    +

    <@spring.message 'governing.body' />: ${governingBody.name}

    +

    + +
    +
    +
    + + + +<#include 'includes/footer.ftl'> \ No newline at end of file diff --git a/src/main/resources/templates/includes/head.ftl b/src/main/resources/templates/includes/head.ftl index 8ff597d2..4e51a2f2 100644 --- a/src/main/resources/templates/includes/head.ftl +++ b/src/main/resources/templates/includes/head.ftl @@ -2,27 +2,27 @@ - - - + + + - - - + + + - - - - Squadlist - ${title!} - - <#if rssUrl??> + + + + Squadlist - ${title!} + + <#if rssUrl??> - + <#macro cancel to> - + <#macro navTabs selectedTab> @@ -52,7 +52,7 @@
  • class="active" ><@spring.message 'availability' />
  • class="active" ><@spring.message 'contacts' />
  • <#if permissionsHelper.hasPermission('VIEW_ENTRY_DETAILS') > -
  • class="active" ><@spring.message 'entry.details' />
  • +
  • class="active" ><@spring.message 'entry.details' />
  • <#if permissionsHelper.hasPermission('VIEW_ADMIN_SCREEN') >
  • class="active" ><@spring.message 'admin'/>
  • @@ -77,7 +77,7 @@ <#macro squadSelect action label> - <#if squads??> + <#if squads??>