From a8565a7e0b766ba80affaa4efd2b6d37b3e92b05 Mon Sep 17 00:00:00 2001 From: Pyry Koivisto Date: Tue, 29 Oct 2024 20:36:43 +0200 Subject: [PATCH] VKT(Backend&Frontend): Change backend to match frontend requirements --- .../fi/oph/vkt/api/dto/MunicipalityDTO.java | 8 ++ .../examiner/ExaminerDetailsCreateDTO.java | 11 --- .../api/dto/examiner/ExaminerDetailsDTO.java | 8 +- .../examiner/ExaminerDetailsUpsertDTO.java | 17 ++++ .../examiner/ExaminerDetailsController.java | 10 +-- .../main/java/fi/oph/vkt/model/Examiner.java | 25 +++--- .../oph/vkt/model/ExaminerMunicipality.java | 38 -------- .../java/fi/oph/vkt/model/Municipality.java | 31 +++++++ .../repository/MunicipalityRepository.java | 10 +++ .../vkt/service/ExaminerDetailsService.java | 31 +++++-- .../oph/vkt/service/MunicipalityService.java | 68 ++++++++++++++ .../vkt/service/PublicExaminerService.java | 6 +- .../db/changelog/db.changelog-1.0.xml | 88 ++++++++++++++----- .../pages/examiner/ExaminerDetailsPage.tsx | 2 +- .../redux/reducers/examinerDetailsUpsert.ts | 3 + 15 files changed, 260 insertions(+), 96 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java delete mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsCreateDTO.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java delete mode 100644 backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerMunicipality.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java new file mode 100644 index 000000000..0a68340c2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record MunicipalityDTO(@NonNull @NotNull String code) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsCreateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsCreateDTO.java deleted file mode 100644 index 56c1f87b2..000000000 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsCreateDTO.java +++ /dev/null @@ -1,11 +0,0 @@ -package fi.oph.vkt.api.dto.examiner; - -import lombok.Builder; -import lombok.NonNull; - -@Builder -public record ExaminerDetailsCreateDTO( - @NonNull String email, - @NonNull Boolean examLanguageFinnish, - @NonNull Boolean examLanguageSwedish -) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java index 65c5506c0..9d8e92975 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java @@ -1,5 +1,8 @@ package fi.oph.vkt.api.dto.examiner; +import fi.oph.vkt.api.dto.MunicipalityDTO; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; import lombok.Builder; import lombok.NonNull; @@ -9,8 +12,11 @@ public record ExaminerDetailsDTO( @NonNull Integer version, @NonNull String oid, @NonNull String email, + @NonNull String phoneNumber, @NonNull String lastName, @NonNull String firstName, @NonNull Boolean examLanguageFinnish, - @NonNull Boolean examLanguageSwedish + @NonNull Boolean examLanguageSwedish, + @NonNull Boolean isPublic, + @NonNull @NotEmpty List municipalities ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java new file mode 100644 index 000000000..4be29ef15 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java @@ -0,0 +1,17 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerDetailsUpsertDTO( + @NonNull String email, + @NonNull String phoneNumber, + @NonNull Boolean examLanguageFinnish, + @NonNull Boolean examLanguageSwedish, + @NonNull Boolean isPublic, + @NonNull @NotEmpty List municipalities +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java index 88d48dcc9..503024294 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java @@ -1,8 +1,8 @@ package fi.oph.vkt.api.examiner; -import fi.oph.vkt.api.dto.examiner.ExaminerDetailsCreateDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsInitDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsUpsertDTO; import fi.oph.vkt.service.ExaminerDetailsService; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; @@ -25,12 +25,12 @@ public ExaminerDetailsDTO getExaminerDetails(@PathVariable("oid") String oid) { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - @Operation(tags = TAG_EXAMINER, summary = "Create examiner") - public ExaminerDetailsDTO createExaminer( + @Operation(tags = TAG_EXAMINER, summary = "Create or update examiner") + public ExaminerDetailsDTO upsertExaminer( @PathVariable("oid") String oid, - @RequestBody ExaminerDetailsCreateDTO examinerDetailsCreateDTO + @RequestBody ExaminerDetailsUpsertDTO examinerDetailsUpsertDTO ) { - return examinerDetailsService.createExaminer(oid, examinerDetailsCreateDTO); + return examinerDetailsService.upsertExaminer(oid, examinerDetailsUpsertDTO); } @GetMapping(path = "/init") diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java index bb6c838c4..1649bb93c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java @@ -1,12 +1,6 @@ package fi.oph.vkt.model; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; +import jakarta.persistence.*; import jakarta.validation.constraints.Size; import java.util.ArrayList; import java.util.List; @@ -32,6 +26,10 @@ public class Examiner extends BaseEntity { @Column(name = "email", nullable = false) private String email; + @Size(max = 255) + @Column(name = "phone_number", nullable = false) + private String phoneNumber; + @Column(name = "last_name", nullable = false) private String lastName; @@ -50,7 +48,14 @@ public class Examiner extends BaseEntity { @OneToMany(mappedBy = "examiner") private List examEvents = new ArrayList<>(); - // TODO Consider using a separate join table instead? - @OneToMany(mappedBy = "examiner") - private List municipalities = new ArrayList<>(); + @Column(name = "is_public", nullable = false) + private boolean isPublic; + + @ManyToMany + @JoinTable( + name = "examiner_municipality", + joinColumns = @JoinColumn(name = "examiner_id", referencedColumnName = "examiner_id"), + inverseJoinColumns = @JoinColumn(name = "municipality_id") + ) + private List municipalities = new ArrayList<>(); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerMunicipality.java b/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerMunicipality.java deleted file mode 100644 index dbdc43e12..000000000 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerMunicipality.java +++ /dev/null @@ -1,38 +0,0 @@ -package fi.oph.vkt.model; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Entity -@Table(name = "examiner_municipality") -public class ExaminerMunicipality { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "municipality_id", nullable = false) - private long id; - - @Size(max = 255) - @Column(name = "name_fi", nullable = false) - private String nameFi; - - @Size(max = 255) - @Column(name = "name_sv", nullable = false) - private String nameSv; - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "examiner_id", referencedColumnName = "examiner_id", nullable = false) - private Examiner examiner; -} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java new file mode 100644 index 000000000..536d9c3ab --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java @@ -0,0 +1,31 @@ +package fi.oph.vkt.model; + +import jakarta.persistence.*; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "municipality") +public class Municipality extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "municipality_id", nullable = false) + private long id; + + // Code should match the koodiArvo of an entry in koodisto + @Column(name = "code", nullable = false, unique = true) + private String code; + + @Column(name = "name_fi", nullable = false) + private String nameFI; + + @Column(name = "name_sv", nullable = false) + private String nameSV; + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "municipalities") + private List examiners; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java new file mode 100644 index 000000000..0a4a9b53c --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java @@ -0,0 +1,10 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.model.Municipality; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public interface MunicipalityRepository extends BaseRepository { + Optional findByCode(String code); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java index 5abe78282..990380d5e 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java @@ -1,17 +1,21 @@ package fi.oph.vkt.service; -import fi.oph.vkt.api.dto.examiner.ExaminerDetailsCreateDTO; +import fi.oph.vkt.api.dto.MunicipalityDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; import fi.oph.vkt.api.dto.examiner.ExaminerDetailsInitDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsUpsertDTO; import fi.oph.vkt.audit.AuditService; import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.Municipality; import fi.oph.vkt.repository.ExaminerRepository; import fi.oph.vkt.service.onr.OnrService; import fi.oph.vkt.service.onr.PersonalData; import fi.oph.vkt.util.exception.APIException; import fi.oph.vkt.util.exception.APIExceptionType; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,6 +25,7 @@ public class ExaminerDetailsService { private final ExaminerRepository examinerRepository; + private final MunicipalityService municipalityService; private final OnrService onrService; private final AuditService auditService; @@ -29,6 +34,10 @@ private PersonalData getOnrPersonalData(final String oid) { return oidToData.get(oid); } + private static MunicipalityDTO toMunicipalityDTO(final Municipality municipality) { + return MunicipalityDTO.builder().code(municipality.getCode()).build(); + } + private static ExaminerDetailsDTO toExaminerDetailsDTO(final Examiner examiner) { return ExaminerDetailsDTO .builder() @@ -38,6 +47,9 @@ private static ExaminerDetailsDTO toExaminerDetailsDTO(final Examiner examiner) .lastName(examiner.getLastName()) .firstName(examiner.getFirstName()) .email(examiner.getEmail()) + .phoneNumber(examiner.getPhoneNumber()) + .municipalities(examiner.getMunicipalities().stream().map(ExaminerDetailsService::toMunicipalityDTO).toList()) + .isPublic(examiner.isPublic()) .examLanguageFinnish(examiner.isExamLanguageFinnish()) .examLanguageSwedish(examiner.isExamLanguageSwedish()) .build(); @@ -62,8 +74,9 @@ public ExaminerDetailsInitDTO getInitialExaminerPersonalData(final String oid) { } @Transactional - public ExaminerDetailsDTO createExaminer(final String oid, ExaminerDetailsCreateDTO examinerDetailsCreateDTO) { + public ExaminerDetailsDTO upsertExaminer(final String oid, ExaminerDetailsUpsertDTO examinerDetailsUpsertDTO) { // TODO Audit log entry + // TODO Throws when trying to update existing examiner - figure out if we want separate methods for updating vs. creating?? if (examinerRepository.findByOid(oid).isPresent()) { throw new APIException(APIExceptionType.EXAMINER_ALREADY_INITIALIZED); } @@ -76,9 +89,17 @@ public ExaminerDetailsDTO createExaminer(final String oid, ExaminerDetailsCreate examiner.setLastName(personalData.getLastName()); examiner.setFirstName(personalData.getFirstName()); examiner.setNickname(personalData.getNickname()); - examiner.setEmail(examinerDetailsCreateDTO.email()); - examiner.setExamLanguageFinnish(examinerDetailsCreateDTO.examLanguageFinnish()); - examiner.setExamLanguageSwedish(examinerDetailsCreateDTO.examLanguageSwedish()); + examiner.setEmail(examinerDetailsUpsertDTO.email()); + examiner.setPhoneNumber(examinerDetailsUpsertDTO.phoneNumber()); + examiner.setMunicipalities( + examinerDetailsUpsertDTO + .municipalities() + .stream() + .map(municipality -> municipalityService.getOrCreateByCode(municipality.code())) + .toList() + ); + examiner.setExamLanguageFinnish(examinerDetailsUpsertDTO.examLanguageFinnish()); + examiner.setExamLanguageSwedish(examinerDetailsUpsertDTO.examLanguageSwedish()); examinerRepository.saveAndFlush(examiner); return toExaminerDetailsDTO(examiner); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java new file mode 100644 index 000000000..1041d833f --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java @@ -0,0 +1,68 @@ +package fi.oph.vkt.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import fi.oph.vkt.model.Municipality; +import fi.oph.vkt.repository.MunicipalityRepository; +import jakarta.annotation.PostConstruct; +import jakarta.transaction.Transactional; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MunicipalityService { + + private final MunicipalityRepository municipalityRepository; + private Map codeToFi; + private Map codeToSv; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String KOODISTO_MUNICIPALITIES_JSON = "koodisto/koodisto_kunnat.json"; + + @PostConstruct + public void init() { + codeToFi = new HashMap<>(); + codeToSv = new HashMap<>(); + + try (final InputStream is = new ClassPathResource(KOODISTO_MUNICIPALITIES_JSON).getInputStream()) { + final List koodisto = deserializeJson(is); + koodisto.forEach(koodistoEntry -> { + codeToFi.put(koodistoEntry.koodiArvo(), koodistoEntry.fi()); + codeToSv.put(koodistoEntry.koodiArvo(), koodistoEntry.sv()); + }); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Transactional + public Municipality getOrCreateByCode(final String code) { + Optional existingMunicipality = municipalityRepository.findByCode(code); + if (existingMunicipality.isPresent()) { + return existingMunicipality.get(); + } else { + Municipality municipality = new Municipality(); + municipality.setCode(code); + municipality.setNameFI(codeToFi.get(code)); + municipality.setNameSV(codeToSv.get(code)); + municipalityRepository.saveAndFlush(municipality); + return municipality; + } + } + + private List deserializeJson(final InputStream is) throws IOException { + return OBJECT_MAPPER.readValue(is, new TypeReference<>() {}); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record KoodistoEntry(@NonNull String koodiArvo, @NonNull String fi, @NonNull String sv) {} +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java index d6ff6faef..dcfe9430f 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java @@ -5,7 +5,7 @@ import fi.oph.vkt.api.dto.PublicMunicipalityDTO; import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.Examiner; -import fi.oph.vkt.model.ExaminerMunicipality; +import fi.oph.vkt.model.Municipality; import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.repository.ExaminerRepository; import java.util.ArrayList; @@ -21,8 +21,8 @@ public class PublicExaminerService { private final ExaminerRepository examinerRepository; - private static PublicMunicipalityDTO toPublicMunicipalityDTO(ExaminerMunicipality municipality) { - return PublicMunicipalityDTO.builder().fi(municipality.getNameFi()).sv(municipality.getNameSv()).build(); + private static PublicMunicipalityDTO toPublicMunicipalityDTO(Municipality municipality) { + return PublicMunicipalityDTO.builder().fi(municipality.getNameFI()).sv(municipality.getNameSV()).build(); } private static PublicExaminerExamDateDTO toPublicExaminerExamDateDTO(ExamEvent examEvent) { diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index b95e579a5..db75575dc 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -859,7 +859,7 @@ ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; - + @@ -881,81 +881,125 @@ - + + + + - + - + - + - + - + + + + - + - + + + + + + + + + + + + + + + + + - + - + + + + + + + + + - + + + referencedColumnNames="examiner_id"/> + - + - + + referencedColumnNames="examiner_id"/> - + - + - + - + diff --git a/frontend/packages/vkt/src/pages/examiner/ExaminerDetailsPage.tsx b/frontend/packages/vkt/src/pages/examiner/ExaminerDetailsPage.tsx index df514cc3c..bd14cb604 100644 --- a/frontend/packages/vkt/src/pages/examiner/ExaminerDetailsPage.tsx +++ b/frontend/packages/vkt/src/pages/examiner/ExaminerDetailsPage.tsx @@ -507,7 +507,7 @@ export const ExaminerDetailsPage = () => { }); const examinerDetails = useExaminerDetails(); - // TODO Navigate away from page if details are saved successfully + // TODO Navigate away from page & show success toast if details are saved successfully // TODO Navigate away from page if cancel is pressed // TODO Perhaps navigation protection if dirty fields? return ( diff --git a/frontend/packages/vkt/src/redux/reducers/examinerDetailsUpsert.ts b/frontend/packages/vkt/src/redux/reducers/examinerDetailsUpsert.ts index 905adbcfc..8be9646c0 100644 --- a/frontend/packages/vkt/src/redux/reducers/examinerDetailsUpsert.ts +++ b/frontend/packages/vkt/src/redux/reducers/examinerDetailsUpsert.ts @@ -10,6 +10,9 @@ const initialState: ExaminerDetailsUpsertState = { status: APIResponseStatus.NotStarted, examinerDetails: { isPublic: true, + examLanguageFinnish: false, + examLanguageSwedish: false, + municipalities: [], }, };