Skip to content

Commit 425ffcc

Browse files
add support for qualifications
1 parent 2452e0a commit 425ffcc

File tree

98 files changed

+1655
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+1655
-199
lines changed

backend/src/main/java/org/eventplanner/importer/service/UserExcelImporter.java

+329-83
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.eventplanner.qualifications;
2+
3+
import org.eventplanner.exceptions.NotImplementedException;
4+
import org.eventplanner.qualifications.adapter.QualificationRepository;
5+
import org.eventplanner.qualifications.entities.Qualification;
6+
import org.eventplanner.qualifications.values.QualificationKey;
7+
import org.eventplanner.users.entities.SignedInUser;
8+
import org.eventplanner.users.values.Permission;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.lang.NonNull;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.util.List;
14+
15+
@Service
16+
public class QualificationUseCase {
17+
18+
private final QualificationRepository qualificationRepository;
19+
20+
public QualificationUseCase(@Autowired QualificationRepository qualificationRepository) {
21+
this.qualificationRepository = qualificationRepository;
22+
}
23+
24+
public List<Qualification> getQualifications(@NonNull SignedInUser signedInUser) {
25+
signedInUser.assertHasPermission(Permission.READ_QUALIFICATIONS);
26+
27+
return this.qualificationRepository.findAll();
28+
}
29+
30+
public Qualification createQualification(@NonNull SignedInUser signedInUser, Qualification qualification) {
31+
signedInUser.assertHasPermission(Permission.WRITE_QUALIFICATIONS);
32+
33+
throw new NotImplementedException("Qualifications are still hard coded in this version");
34+
}
35+
36+
public Qualification updateQualification(@NonNull SignedInUser signedInUser, QualificationKey qualificationKey, Qualification qualification) {
37+
signedInUser.assertHasPermission(Permission.WRITE_QUALIFICATIONS);
38+
39+
throw new NotImplementedException("Qualifications are still hard coded in this version");
40+
}
41+
42+
public void deleteQualification(@NonNull SignedInUser signedInUser, QualificationKey qualificationKey) {
43+
signedInUser.assertHasPermission(Permission.WRITE_QUALIFICATIONS);
44+
45+
throw new NotImplementedException("Qualifications are still hard coded in this version");
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.eventplanner.qualifications.adapter;
2+
3+
import org.eventplanner.qualifications.entities.Qualification;
4+
5+
import java.util.List;
6+
7+
public interface QualificationRepository {
8+
List<Qualification> findAll();
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.eventplanner.qualifications.adapter.filesystem;
2+
3+
import org.apache.commons.io.FileUtils;
4+
import org.eventplanner.qualifications.adapter.QualificationRepository;
5+
import org.eventplanner.qualifications.entities.Qualification;
6+
import org.eventplanner.utils.FileSystemJsonRepository;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.core.io.ResourceLoader;
11+
import org.springframework.stereotype.Repository;
12+
13+
import java.io.File;
14+
import java.io.IOException;
15+
import java.util.List;
16+
17+
@Repository
18+
public class QualificationFileSystemRepository implements QualificationRepository {
19+
20+
private final Logger log = LoggerFactory.getLogger(this.getClass());
21+
private final FileSystemJsonRepository<QualificationJsonEntity> fs;
22+
23+
public QualificationFileSystemRepository(
24+
@Value("${custom.data-directory}") String dataDirectory,
25+
ResourceLoader resourceLoader
26+
) {
27+
var dir = new File(dataDirectory + "/qualifications");
28+
if (!dir.exists()) {
29+
try {
30+
var resource = resourceLoader.getResource("classpath:data/qualifications");
31+
if (resource.exists()) {
32+
var sourceDir = resource.getFile();
33+
FileUtils.copyDirectory(sourceDir, dir);
34+
}
35+
} catch (IOException e) {
36+
log.error("Failed to copy bundled qualifications into data directory", e);
37+
}
38+
}
39+
this.fs = new FileSystemJsonRepository<>(QualificationJsonEntity.class, dir);
40+
}
41+
42+
@Override
43+
public List<Qualification> findAll() {
44+
return fs.findAll().stream().map(QualificationJsonEntity::toDomain).toList();
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.eventplanner.qualifications.adapter.filesystem;
2+
3+
import org.eventplanner.qualifications.entities.Qualification;
4+
import org.eventplanner.qualifications.values.QualificationKey;
5+
import org.springframework.lang.NonNull;
6+
7+
import java.io.Serializable;
8+
9+
public record QualificationJsonEntity(
10+
@NonNull String key,
11+
@NonNull String name,
12+
@NonNull String icon,
13+
@NonNull String description,
14+
boolean expires
15+
16+
) implements Serializable {
17+
public Qualification toDomain() {
18+
return new Qualification(new QualificationKey(key), name, icon, description, expires);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package org.eventplanner.qualifications.entities;
22

3+
import lombok.*;
34
import org.eventplanner.qualifications.values.QualificationKey;
45
import org.springframework.lang.NonNull;
56
import org.springframework.lang.Nullable;
67

7-
public record Qualification(
8-
@NonNull QualificationKey key,
9-
@NonNull String name,
10-
@Nullable String description,
11-
boolean expires
12-
) {
8+
@Getter
9+
@Setter
10+
@EqualsAndHashCode
11+
@RequiredArgsConstructor
12+
@AllArgsConstructor
13+
public class Qualification {
14+
private @NonNull QualificationKey key;
15+
private @NonNull String name;
16+
private @Nullable String icon;
17+
private @Nullable String description;
18+
private boolean expires;
1319
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.eventplanner.qualifications.rest;
2+
3+
import org.eventplanner.qualifications.QualificationUseCase;
4+
import org.eventplanner.qualifications.entities.Qualification;
5+
import org.eventplanner.qualifications.values.QualificationKey;
6+
import org.eventplanner.qualifications.rest.dto.QualificationRepresentation;
7+
import org.eventplanner.users.UserUseCase;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
12+
import org.springframework.security.core.context.SecurityContextHolder;
13+
import org.springframework.web.bind.annotation.*;
14+
15+
import java.util.List;
16+
17+
@RestController
18+
@RequestMapping("/api/v1/qualifications")
19+
@EnableMethodSecurity(securedEnabled = true)
20+
public class QualificationController {
21+
22+
private final UserUseCase userUseCase;
23+
private final QualificationUseCase qualificationUseCase;
24+
25+
public QualificationController(
26+
@Autowired UserUseCase userUseCase,
27+
@Autowired QualificationUseCase qualificationUseCase
28+
) {
29+
this.userUseCase = userUseCase;
30+
this.qualificationUseCase = qualificationUseCase;
31+
}
32+
33+
@RequestMapping(method = RequestMethod.POST, path = "")
34+
public ResponseEntity<QualificationRepresentation> createQualification(@RequestBody QualificationRepresentation spec) {
35+
var signedInUser = userUseCase.getSignedInUser(SecurityContextHolder.getContext().getAuthentication());
36+
37+
var qualificationSpec = new Qualification(new QualificationKey(""), spec.name(), spec.icon(), spec.description(), spec.expires());
38+
var qualification = qualificationUseCase.createQualification(signedInUser, qualificationSpec);
39+
return ResponseEntity.status(HttpStatus.CREATED).body(QualificationRepresentation.fromDomain(qualification));
40+
}
41+
42+
@RequestMapping(method = RequestMethod.GET, path = "")
43+
public ResponseEntity<List<QualificationRepresentation>> getQualifications() {
44+
var signedInUser = userUseCase.getSignedInUser(SecurityContextHolder.getContext().getAuthentication());
45+
46+
var qualifications = qualificationUseCase.getQualifications(signedInUser).stream()
47+
.map(QualificationRepresentation::fromDomain)
48+
.toList();
49+
return ResponseEntity.ok(qualifications);
50+
}
51+
52+
@RequestMapping(method = RequestMethod.PUT, path = "/{qualificationKey}")
53+
public ResponseEntity<QualificationRepresentation> updateQualification(
54+
@PathVariable String qualificationKey,
55+
@RequestBody QualificationRepresentation spec
56+
) {
57+
var signedInUser = userUseCase.getSignedInUser(SecurityContextHolder.getContext().getAuthentication());
58+
59+
var qualificationSpec = new Qualification(new QualificationKey(qualificationKey), spec.name(), spec.icon(), spec.description(), spec.expires());
60+
var qualification = qualificationUseCase.updateQualification(signedInUser, qualificationSpec.getKey(), qualificationSpec);
61+
return ResponseEntity.ok(QualificationRepresentation.fromDomain(qualification));
62+
}
63+
64+
@RequestMapping(method = RequestMethod.DELETE, path = "/{qualificationKey}")
65+
public ResponseEntity<Void> deleteQualification(@PathVariable String qualificationKey) {
66+
var signedInUser = userUseCase.getSignedInUser(SecurityContextHolder.getContext().getAuthentication());
67+
68+
qualificationUseCase.deleteQualification(signedInUser, new QualificationKey(qualificationKey));
69+
return ResponseEntity.ok().build();
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.eventplanner.qualifications.rest.dto;
2+
3+
import org.eventplanner.qualifications.entities.Qualification;
4+
import org.springframework.lang.NonNull;
5+
import org.springframework.lang.Nullable;
6+
7+
import java.io.Serializable;
8+
9+
public record QualificationRepresentation(
10+
@NonNull String key,
11+
@NonNull String name,
12+
@Nullable String icon,
13+
@Nullable String description,
14+
boolean expires
15+
) implements Serializable {
16+
17+
public static QualificationRepresentation fromDomain(@NonNull Qualification qualification) {
18+
return new QualificationRepresentation(
19+
qualification.getKey().value(),
20+
qualification.getName(),
21+
qualification.getIcon(),
22+
qualification.getDescription(),
23+
qualification.isExpires()
24+
);
25+
}
26+
}

backend/src/main/java/org/eventplanner/users/entities/EncryptedUserDetails.java

+1
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ public class EncryptedUserDetails implements Serializable {
3838
private @Nullable EncryptedString placeOfBirth;
3939
private @Nullable EncryptedString passNr;
4040
private @Nullable EncryptedString comment;
41+
private @Nullable EncryptedString nationality;
4142
}

backend/src/main/java/org/eventplanner/users/entities/UserDetails.java

+26
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.List;
66

77
import org.eventplanner.positions.values.PositionKey;
8+
import org.eventplanner.qualifications.values.QualificationKey;
89
import org.eventplanner.users.values.Address;
910
import org.eventplanner.users.values.AuthKey;
1011
import org.eventplanner.users.values.Role;
@@ -41,6 +42,7 @@ public class UserDetails {
4142
private @Nullable String placeOfBirth;
4243
private @Nullable String passNr;
4344
private @Nullable String comment;
45+
private @Nullable String nationality;
4446

4547
public @NonNull String getFullName() {
4648
StringBuilder stb = new StringBuilder();
@@ -58,4 +60,28 @@ public class UserDetails {
5860
public @NonNull User cropToUser() {
5961
return new User(key, firstName, lastName, positions);
6062
}
63+
64+
public void addQualification(QualificationKey qualificationKey, ZonedDateTime expirationDate) {
65+
var maybeExistingQualification = qualifications.stream().filter(it -> it.getQualificationKey().equals(qualificationKey)).findFirst();
66+
if (maybeExistingQualification.isPresent()) {
67+
var existingQualification = maybeExistingQualification.get();
68+
if (existingQualification.getExpiresAt() != null
69+
&& expirationDate != null
70+
&& expirationDate.isAfter(existingQualification.getExpiresAt())
71+
) {
72+
qualifications.remove(existingQualification);
73+
qualifications.add(new UserQualification(qualificationKey, expirationDate));
74+
}
75+
if (existingQualification.getExpiresAt() == null && expirationDate != null) {
76+
qualifications.remove(existingQualification);
77+
qualifications.add(new UserQualification(qualificationKey, expirationDate));
78+
}
79+
} else {
80+
qualifications.add(new UserQualification(qualificationKey, expirationDate));
81+
}
82+
}
83+
84+
public void addQualification(QualificationKey qualificationKey) {
85+
addQualification(qualificationKey, null);
86+
}
6187
}

backend/src/main/java/org/eventplanner/users/rest/UserController.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import org.eventplanner.users.UserUseCase;
6+
import org.eventplanner.users.rest.dto.UserAdminListRepresentation;
67
import org.eventplanner.users.rest.dto.UserDetailsRepresentation;
78
import org.eventplanner.users.rest.dto.UserRepresentation;
89
import org.eventplanner.users.rest.dto.UserSelfRepresentation;
@@ -33,9 +34,12 @@ public UserController(@Autowired UserUseCase userUseCase) {
3334
@RequestMapping(method = RequestMethod.GET, path = "")
3435
public ResponseEntity<List<?>> getUsers(@RequestParam(name = "details", required = false) boolean details) {
3536
var signedInUser = userUseCase.getSignedInUser(SecurityContextHolder.getContext().getAuthentication());
36-
if (details && signedInUser.hasPermission(Permission.READ_USER_DETAILS)) {
37+
if (signedInUser.hasPermission(Permission.READ_USER_DETAILS)) {
3738
var users = userUseCase.getDetailedUsers(signedInUser).stream();
38-
return ResponseEntity.ok(users.map(UserDetailsRepresentation::fromDomain).toList());
39+
if (details) {
40+
return ResponseEntity.ok(users.map(UserDetailsRepresentation::fromDomain).toList());
41+
}
42+
return ResponseEntity.ok(users.map(UserAdminListRepresentation::fromDomain).toList());
3943
} else {
4044
var users = userUseCase.getUsers(signedInUser).stream();
4145
return ResponseEntity.ok(users.map(UserRepresentation::fromDomain).toList());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.eventplanner.users.rest.dto;
2+
3+
import org.eventplanner.positions.values.PositionKey;
4+
import org.eventplanner.users.entities.User;
5+
import org.eventplanner.users.entities.UserDetails;
6+
import org.eventplanner.users.entities.UserQualification;
7+
import org.springframework.lang.NonNull;
8+
9+
import java.io.Serializable;
10+
import java.time.ZonedDateTime;
11+
import java.util.List;
12+
import java.util.Objects;
13+
14+
public record UserAdminListRepresentation(
15+
@NonNull String key,
16+
@NonNull String firstName,
17+
@NonNull String lastName,
18+
@NonNull List<String> positions,
19+
long expiredQualificationCount,
20+
long soonExpiringQualificationCount
21+
) implements Serializable {
22+
public static UserAdminListRepresentation fromDomain(@NonNull UserDetails user) {
23+
return new UserAdminListRepresentation(
24+
user.getKey().value(),
25+
user.getFirstName(),
26+
user.getLastName(),
27+
user.getPositions().stream().map(PositionKey::value).toList(),
28+
user.getQualifications().stream()
29+
.map(UserQualification::getExpiresAt)
30+
.filter(Objects::nonNull)
31+
.filter(d -> d.isBefore(ZonedDateTime.now()))
32+
.count(),
33+
user.getQualifications().stream()
34+
.map(UserQualification::getExpiresAt)
35+
.filter(Objects::nonNull)
36+
.filter(d -> d.isBefore(ZonedDateTime.now().plusMonths(3)))
37+
.count()
38+
);
39+
}
40+
}

backend/src/main/java/org/eventplanner/users/service/UserEncryptionService.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public UserEncryptionService(
9393
.orElse(null),
9494
encryptNullable(user.getPlaceOfBirth()),
9595
encryptNullable(user.getPassNr()),
96-
encryptNullable(user.getComment())
96+
encryptNullable(user.getComment()),
97+
encryptNullable(user.getNationality())
9798
);
9899
}
99100

@@ -128,7 +129,8 @@ public UserEncryptionService(
128129
.orElse(null),
129130
decryptNullable(user.getPlaceOfBirth()),
130131
decryptNullable(user.getPassNr()),
131-
decryptNullable(user.getComment())
132+
decryptNullable(user.getComment()),
133+
decryptNullable(user.getNationality())
132134
);
133135
}
134136

backend/src/main/java/org/eventplanner/users/values/Permission.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ public enum Permission {
1414
READ_USER_DETAILS("user-details:read"),
1515
WRITE_USERS("user-details:write"),
1616
READ_POSITIONS("positions:read"),
17-
WRITE_POSITIONS("positions:write");
17+
WRITE_POSITIONS("positions:write"),
18+
READ_QUALIFICATIONS("qualifications:read"),
19+
WRITE_QUALIFICATIONS("qualifications:write");
1820

1921
private final String value;
2022

0 commit comments

Comments
 (0)