Added basic forms for parties

This commit is contained in:
2023-07-09 18:29:20 +02:00
parent 52e1a72330
commit 9a95cbcbc3
10 changed files with 265 additions and 67 deletions
@@ -22,20 +22,20 @@ public class WebSecurityConfiguration {
public SecurityFilterChain securityFilterChain(@NonNull HttpSecurity http) throws Exception { // (2)
http
.authorizeHttpRequests()
.requestMatchers("/wahlrecht/v1/**")
.requestMatchers("/wahlrecht/v1/**", "/wahlrecht/version")
.permitAll()
.and()
.authorizeHttpRequests()
.requestMatchers("/resources/**")
.permitAll()
.and()
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/wahlrecht/v1/election", "/wahlrecht/v1/party")
.requestMatchers("/wahlrecht/v1/election",
"/wahlrecht/v1/party/**",
"/wahlrecht/v1/nomination/**")
.authenticated() // (3)
.and()
.formLogin(); // (7)
.httpBasic(); // (7)
return http.build();
}
@@ -15,7 +15,6 @@ import java.util.zip.ZipEntry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
@@ -30,65 +29,60 @@ import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping(value = "/wahlrecht")
public class VersionHtmlController {
@GetMapping(path = "/html/version.html")
@GetMapping(path = "/version")
public String version() {
return "version";
}
@ControllerAdvice
public static class VersionControllerAdvice {
@ModelAttribute("version")
private String getApplicationVersion() {
return getTitle() + " " + getVersion();
}
@ModelAttribute("version")
public String getApplicationVersion() {
return getTitle() + " " + getVersion();
@ModelAttribute("footerString")
private String getApplicationVersion(@RequestHeader("host") String hostName) {
return getTitle() + " " + getVersion() + " - " + hostName;
}
private String getTitle() {
return Optional.ofNullable(VersionHtmlController.class.getPackage().getImplementationTitle())
.filter(s -> !s.isBlank())
.orElse("application");
}
public String getVersion() {
return Optional.ofNullable(VersionHtmlController.class.getPackage().getImplementationVersion())
.filter(s -> !s.isBlank())
.orElse("DEVELOPER");
}
@ModelAttribute("hostname")
private String getHostname() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
log.warn(e.toString(), e);
}
return "";
}
@ModelAttribute("footerString")
public String getApplicationVersion(@RequestHeader("host") String hostName) {
return getTitle() + " " + getVersion() + " - " + hostName;
}
private String getTitle() {
return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationTitle())
.filter(s -> !s.isBlank())
.orElse("application");
}
public String getVersion() {
return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationVersion())
.filter(s -> !s.isBlank())
.orElse("DEVELOPER");
}
@ModelAttribute("hostname")
public String getHostname() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
log.warn(e.toString(), e);
}
return "";
}
@ModelAttribute("manifest")
private Collection<String> getManifest() {
try {
URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
String jarFileName = Paths.get(location.toURI()).toString();
try (JarFile jarFile = new JarFile(jarFileName)) {
ZipEntry entry = jarFile.getEntry(JarFile.MANIFEST_NAME);
try (InputStream in = jarFile.getInputStream(entry)) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().toList();
}
@ModelAttribute("manifest")
private Collection<String> getManifest() {
try {
URL location = getClass().getProtectionDomain().getCodeSource().getLocation();
String jarFileName = Paths.get(location.toURI()).toString();
try (JarFile jarFile = new JarFile(jarFileName)) {
ZipEntry entry = jarFile.getEntry(JarFile.MANIFEST_NAME);
try (InputStream in = jarFile.getInputStream(entry)) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().toList();
}
} catch (FileNotFoundException ignored) {
// do nothing if manifest file is not available
} catch (Exception e) {
log.info(e.toString(), e);
}
return List.of(getTitle() + " " + getVersion());
} catch (FileNotFoundException ignored) {
// do nothing if manifest file is not available
} catch (Exception e) {
log.info(e.toString(), e);
}
return List.of(getTitle() + " " + getVersion());
}
@@ -1,8 +1,12 @@
package de.twomartens.wahlrecht.controller.v1;
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper;
import de.twomartens.wahlrecht.mapper.v1.PartyInElectionMapper;
import de.twomartens.wahlrecht.model.dto.ErrorMessage;
import de.twomartens.wahlrecht.model.dto.v1.Election;
import de.twomartens.wahlrecht.model.dto.v1.PartyInElection;
import de.twomartens.wahlrecht.service.ConstituencyService;
import de.twomartens.wahlrecht.service.ElectionService;
import de.twomartens.wahlrecht.service.PartyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -20,22 +24,28 @@ import org.mapstruct.factory.Mappers;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@Controller
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@Tag(name = "Parties", description = "all requests relating to parties")
public class PartyController {
private final PartyInElectionMapper mapper = Mappers.getMapper(PartyInElectionMapper.class);
private final ElectionMapper electionMapper = Mappers.getMapper(ElectionMapper.class);
private final PartyService service;
private final ElectionService electionService;
private final ConstituencyService constituencyService;
@Operation(
summary = "Returns all stored parties for given election name",
@@ -85,10 +95,55 @@ public class PartyController {
)
@PutMapping("/party")
@SecurityRequirement(name = "basicAuth")
public ResponseEntity<?> putElection(@RequestBody PartyInElection party) {
public ResponseEntity<?> putParty(@RequestBody PartyInElection party) {
boolean createdNew = service.storeParty(mapper.mapToDB(party));
return createdNew
? new ResponseEntity<>(HttpStatus.CREATED)
: new ResponseEntity<>(HttpStatus.OK);
}
@PostMapping("/party")
public String submitCreatePartyForm(@ModelAttribute PartyInElection party, Model model) {
service.storeParty(mapper.mapToDB(party));
model.addAttribute("party", party);
model.addAttribute("isCreate", false);
return "partyUpdate";
}
@GetMapping("/party")
public String createPartyForm(Model model) {
model.addAttribute("party", new PartyInElection());
model.addAttribute("isCreate", true);
return "partyCreate";
}
@PostMapping("/party/{electionName}/{abbreviation}")
public String submitUpdatePartyForm(
@PathVariable String electionName,
@PathVariable String abbreviation,
@ModelAttribute PartyInElection party,
Model model) {
return submitCreatePartyForm(party, model);
}
@GetMapping("/party/{electionName}/{abbreviation}")
public String updatePartyForm(
@PathVariable String electionName,
@PathVariable String abbreviation,
Model model) {
PartyInElection party = mapper.mapToExternal(
service.getPartyByElectionNameAndAbbreviation(electionName, abbreviation));
model.addAttribute("party", party);
model.addAttribute("isCreate", false);
return "partyUpdate";
}
@ModelAttribute("elections")
Collection<Election> elections() {
return electionService.getElections().stream()
.map(electionMapper::mapToExternal)
.toList();
}
}
@@ -1,9 +1,22 @@
package de.twomartens.wahlrecht.model.dto.v1;
import java.util.List;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
public record Nomination(String name, String partyAbbreviation, String electionName,
boolean supportVotesOnNomination,
List<Candidate> candidates) {
@Getter
@Setter
@EqualsAndHashCode
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Nomination {
String electionName;
String partyAbbreviation;
String name;
boolean supportVotesOnNomination;
List<Candidate> candidates;
}
@@ -1,9 +1,27 @@
package de.twomartens.wahlrecht.model.dto.v1;
import java.util.HashMap;
import java.util.Map;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.ToString.Include;
import lombok.experimental.FieldDefaults;
import org.springframework.lang.NonNull;
public record PartyInElection(String electionName, String abbreviation, String name,
Nomination overallNomination,
Map<Integer, Nomination> constituencyNominations) {
@Getter
@Setter
@EqualsAndHashCode
@ToString(onlyExplicitlyIncluded = true)
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PartyInElection {
String electionName;
@Include
String abbreviation;
String name;
Nomination overallNomination;
@NonNull
Map<Integer, Nomination> constituencyNominations = new HashMap<>();
}
@@ -17,6 +17,10 @@ public class NominationService {
private final CandidateService candidateService;
public Nomination storeNomination(Nomination nomination) {
if (nomination == null) {
return null;
}
Optional<Nomination> existingOptional = repository
.findByNameAndElectionNameAndPartyAbbreviation(nomination.getName(),
nomination.getElectionName(), nomination.getPartyAbbreviation());
@@ -28,10 +28,21 @@ public class PartyService {
return parties;
}
public PartyInElection getPartyByElectionNameAndAbbreviation(String electionName,
String abbreviation) {
Optional<PartyInElection> optionalParty = partyRepository.findByAbbreviationAndElectionName(
abbreviation, electionName);
if (optionalParty.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"no party found for %s and %s".formatted(electionName, abbreviation));
}
return optionalParty.get();
}
public boolean storeParty(PartyInElection partyInElection) {
boolean createdNew = true;
Optional<PartyInElection> existingOptional = partyRepository
.findByAbbreviationAndElectionName(partyInElection.getName(),
.findByAbbreviationAndElectionName(partyInElection.getAbbreviation(),
partyInElection.getElectionName());
Map<Integer, Nomination> constituencyNominations = new HashMap<>();
partyInElection.getConstituencyNominations()
@@ -40,6 +51,9 @@ public class PartyService {
if (existingOptional.isPresent()) {
PartyInElection existing = existingOptional.get();
existing.setName(partyInElection.getName());
if (constituencyNominations.isEmpty()) {
constituencyNominations.putAll(existing.getConstituencyNominations());
}
partyInElection = existing;
createdNew = false;
}
@@ -0,0 +1,50 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="de">
<head th:fragment="head">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${party}"></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<form action="#" th:action="@{/wahlrecht/v1/party}"
th:object="${party}" method="post">
<div>
<label for="electionName" class="form-label">Wahlname</label>
<select name="electionName" id="electionName" class="form-select"
th:field="*{electionName}">
<option th:each="election : ${elections}" th:text="${election.name()}"
th:value="${election.name()}"></option>
</select>
</div>
<div>
<label for="partyAbbreviation" class="form-label">Parteiabkürzung</label>
<input type="text" name="abbreviation" id="partyAbbreviation" class="form-control"
th:field="*{abbreviation}">
</div>
<div>
<label for="partyName" class="form-label">Parteiname</label>
<input type="text" name="name" id="partyName" class="form-control"
th:field="*{name}">
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary"
th:text="${party.name} != null ? 'Aktualisieren' : 'Erstellen'">
Erstellen
</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="de">
<head th:fragment="head">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${party}"></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<form action="#" th:action="@{/wahlrecht/v1/party/{electionName}/{abbreviation}(electionName=${party.electionName}, abbreviation=${party.abbreviation})}"
th:object="${party}" method="post">
<div>
<label for="electionName" class="form-label">Wahlname</label>
<select name="electionName" id="electionName" class="form-select"
th:field="*{electionName}">
<option th:each="election : ${elections}" th:text="${election.name()}"
th:value="${election.name()}"></option>
</select>
</div>
<div>
<label for="partyAbbreviation" class="form-label">Parteiabkürzung</label>
<input type="text" name="abbreviation" id="partyAbbreviation" class="form-control"
th:field="*{abbreviation}">
</div>
<div>
<label for="partyName" class="form-label">Parteiname</label>
<input type="text" name="name" id="partyName" class="form-control"
th:field="*{name}">
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary"
th:text="${party.name} != null ? 'Aktualisieren' : 'Erstellen'">
Erstellen
</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
</body>
</html>
@@ -1,7 +1,7 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:fragment="head">
<title><th:block th:text="${version}"/></title>
<title th:text="${version}"></title>
<style>
@media (prefers-color-scheme: dark) {
html{background:#000000;color:#ffffff;font:15px/22px arial,sans-serif}