Improve supervisor change integration ()

Previously when we wanted to change supervisor we had to make two calls against the daisy api.
A DELETE and a POST, this was brittle because if one of the calls failed we didn't have a good way of handling that.
This could leave the application in a state where a project could end up with 2 different supervisors.
This caused side effects and forced us to manually go into the databases and clean up the errors.

Now the daisy api is updated and we only need to do a POST to change the supervisor. See further documentation here
[POST /thesis/{id}/contributor](https://apitest.dsv.su.se/resource_Theses.html#resource_Theses_postContributor_id_projectParticipant_POST)

### IMPORTANT: Release needs to be synced with Daisy API

Reviewed-on: 
Reviewed-by: Andreas Svanberg <andreass@dsv.su.se>
Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Co-committed-by: Nico Athanassiadis <nico@dsv.su.se>
This commit is contained in:
Nico Athanassiadis 2025-02-21 00:27:22 +01:00 committed by Andreas Svanberg
parent b7cf87d6d3
commit 03570fc6db
7 changed files with 79 additions and 63 deletions
core/src/main
java/se/su/dsv/scipro/reusable
xjb
xsd
daisy-integration/src
main/java/se/su/dsv/scipro
test/java/se/su/dsv/scipro

@ -1,7 +1,10 @@
package se.su.dsv.scipro.reusable;
import java.time.LocalDate;
import java.util.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
public final class SciProUtilities {
@ -75,15 +78,4 @@ public final class SciProUtilities {
}
return copy;
}
public static Calendar toCalendar(final LocalDate localDate) {
if (localDate == null) {
return null;
} else {
final Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(localDate.getYear(), localDate.getMonthValue() - 1, localDate.getDayOfMonth());
return calendar;
}
}
}

@ -9,6 +9,10 @@
parseMethod="jakarta.xml.bind.DatatypeConverter.parseDateTime"
printMethod="jakarta.xml.bind.DatatypeConverter.printDateTime" />
<jaxb:javaType name="java.time.LocalDate" xmlType="xs:date"
parseMethod="java.time.LocalDate.parse"
printMethod="java.time.format.DateTimeFormatter.ISO_LOCAL_DATE.format" />
<!-- Force all classes implements Serializable -->
<xjc:simple />
<xjc:serializable uid="1" />

@ -375,14 +375,16 @@
</xs:element>
<xs:element name="title_en" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="startDate" type="xs:dateTime" minOccurs="0">
<xs:element name="startDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="endDate" type="xs:dateTime" minOccurs="0">
<xs:element name="endDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="unit" type="serializableUnit" minOccurs="0">
</xs:element>
<xs:element name="researchAreas" type="researchAreas" minOccurs="0">
</xs:element>
<xs:element name="externalID" type="xs:string" minOccurs="0">
</xs:element>
</xs:sequence>
</xs:complexType>
@ -390,13 +392,15 @@
<xs:sequence>
<xs:element name="title_en" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="externalID" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="aborted" type="xs:boolean" minOccurs="1">
</xs:element>
<xs:element name="level" type="educationalLevel" minOccurs="0">
</xs:element>
<xs:element name="title" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="endDate" type="xs:dateTime" minOccurs="0">
<xs:element name="endDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="status" type="STATUS" minOccurs="0">
</xs:element>
@ -404,7 +408,7 @@
</xs:element>
<xs:element name="finished" type="xs:boolean" minOccurs="1">
</xs:element>
<xs:element name="startDate" type="xs:dateTime" minOccurs="0">
<xs:element name="startDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="id" type="xs:int" minOccurs="1">
</xs:element>
@ -475,9 +479,11 @@
</xs:element>
<xs:element name="title_en" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="startDate" type="xs:dateTime" minOccurs="0">
<xs:element name="externalID" type="xs:string" minOccurs="0">
</xs:element>
<xs:element name="endDate" type="xs:dateTime" minOccurs="0">
<xs:element name="startDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="endDate" type="xs:date" minOccurs="0">
</xs:element>
<xs:element name="aborted" type="xs:boolean" minOccurs="1">
</xs:element>

@ -4,10 +4,9 @@ import static com.querydsl.core.types.dsl.Expressions.anyOf;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.ws.rs.core.Response;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Period;
import java.util.Calendar;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
@ -38,7 +37,6 @@ import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectRepo;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.project.QProject;
import se.su.dsv.scipro.reusable.SciProUtilities;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.workerthreads.AbstractWorker;
@ -198,7 +196,7 @@ public class ProjectExporter extends AbstractWorker {
UnitWithID unit = new UnitWithID();
unit.setId(project.getHeadSupervisor().getUnit().getIdentifier());
Calendar startDate = SciProUtilities.toCalendar(project.getStartDate());
LocalDate startDate = project.getStartDate();
String title = finalThesis
.map(FinalThesis::getSwedishTitle)
@ -211,7 +209,7 @@ public class ProjectExporter extends AbstractWorker {
thesis.setUnit(unit);
thesis.setAborted(project.getProjectStatus() == ProjectStatus.INACTIVE);
thesis.setStartDate(startDate);
thesis.setEndDate(SciProUtilities.toCalendar(project.getExpectedEndDate()));
thesis.setEndDate(project.getExpectedEndDate());
final ResearchAreas researchAreas = new ResearchAreas();
if (project.getResearchArea() != null && project.getResearchArea().getIdentifier() != null) {
final ResearchAreaWithID researchArea2 = new ResearchAreaWithID();
@ -260,36 +258,14 @@ public class ProjectExporter extends AbstractWorker {
}
private void updateHeadSupervisor(Project project) throws ExternalExportException {
boolean toAdd = true;
boolean shouldChange = true;
Set<ProjectParticipant> contributors = daisyAPI.getContributors(project.getIdentifier());
for (ProjectParticipant contributor : contributors) {
if (contributor.getRole() == Role.SUPERVISOR) {
if (contributor.getPerson().getId().equals(project.getHeadSupervisor().getIdentifier())) {
toAdd = false;
} else {
Response response = daisyAPI.deleteThesisPerson(
project.getIdentifier(),
contributor.getPerson().getId()
);
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
LOG.warn(
"Failed to delete supervisor from project: {} (id: {} / {})",
project.getTitle(),
project.getId(),
project.getIdentifier()
);
// Card 3413
// Do not attempt to add the new supervisor if the old one is still there to prevent
// the project from having multiple supervisors.
// Hopefully Daisy API will soon have support for the function "change supervisor"
// rather than just INSERT and DELETE rows in a table.
toAdd = false;
}
}
shouldChange = !contributor.getPerson().getId().equals(project.getHeadSupervisor().getIdentifier());
}
}
if (toAdd) {
if (shouldChange) {
externalExporter.addContributorToProject(project, project.getHeadSupervisor(), Role.SUPERVISOR);
}
}

@ -3,7 +3,7 @@ package se.su.dsv.scipro.io.impl;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import java.math.BigDecimal;
import java.util.Calendar;
import java.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.su.dsv.scipro.daisyexternal.http.DaisyAPI;
@ -21,7 +21,6 @@ import se.su.dsv.scipro.io.dto.ThesisToBeCreated;
import se.su.dsv.scipro.io.dto.UnitWithID;
import se.su.dsv.scipro.io.exceptions.ExternalExportException;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reusable.SciProUtilities;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
@ -46,9 +45,9 @@ public class ExternalExporterDaisyImpl implements ExternalExporter {
ThesisToBeCreated exportProjectDTO = new ThesisToBeCreated();
exportProjectDTO.setTitle(truncate(project.getTitle()));
Calendar startDate = SciProUtilities.toCalendar(project.getStartDate());
LocalDate startDate = project.getStartDate();
exportProjectDTO.setStartDate(startDate);
exportProjectDTO.setEndDate(SciProUtilities.toCalendar(project.getExpectedEndDate()));
exportProjectDTO.setEndDate(project.getExpectedEndDate());
exportProjectDTO.setUnit(unitDTO);
final ResearchAreas researchAreas = new ResearchAreas();
if (project.getResearchArea() != null && project.getResearchArea().getIdentifier() != null) {

@ -2,6 +2,7 @@ package se.su.dsv.scipro.integration.daisy.workers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -144,22 +145,61 @@ public class ProjectExporterTest {
}
@Test
public void deletes_wrong_head_supervisor() {
Person person = new Person();
person.setId(234234);
public void change_head_supervisor() throws Exception {
User newSupervisor = User.builder()
.firstName("New")
.lastName("Supervisor")
.emailAddress("new@supervisor.com")
.build();
Unit unit = new Unit();
unit.setIdentifier(239478);
ProjectParticipant headSupervisor = new ProjectParticipant();
headSupervisor.setRole(Role.SUPERVISOR);
headSupervisor.setPerson(person);
newSupervisor.setIdentifier(12345);
newSupervisor.setUnit(unit);
exportedProject.setHeadSupervisor(newSupervisor);
Set<ProjectParticipant> contributors = new HashSet<>();
contributors.add(headSupervisor);
ProjectParticipant oldSupervisor = new ProjectParticipant();
oldSupervisor.setRole(Role.SUPERVISOR);
Person oldPerson = new Person();
oldPerson.setId(SUPERVISOR_IDENTIFIER);
oldSupervisor.setPerson(oldPerson);
contributors.add(oldSupervisor);
when(daisyAPI.getContributors(exportedProject.getIdentifier())).thenReturn(contributors);
projectExporter.run();
verify(daisyAPI).deleteThesisPerson(exportedProject.getIdentifier(), person.getId());
verify(externalExporter).addContributorToProject(exportedProject, newSupervisor, Role.SUPERVISOR);
}
@Test
public void should_not_change_head_supervisor_if_already_exists() throws Exception {
User newSupervisor = User.builder()
.firstName("New")
.lastName("Supervisor")
.emailAddress("new@supervisor.com")
.build();
Unit unit = new Unit();
unit.setIdentifier(239478);
newSupervisor.setIdentifier(SUPERVISOR_IDENTIFIER);
newSupervisor.setUnit(unit);
exportedProject.setHeadSupervisor(newSupervisor);
Set<ProjectParticipant> contributors = new HashSet<>();
ProjectParticipant oldSupervisor = new ProjectParticipant();
oldSupervisor.setRole(Role.SUPERVISOR);
Person oldPerson = new Person();
oldPerson.setId(SUPERVISOR_IDENTIFIER);
oldSupervisor.setPerson(oldPerson);
contributors.add(oldSupervisor);
when(daisyAPI.getContributors(exportedProject.getIdentifier())).thenReturn(contributors);
projectExporter.run();
verify(externalExporter, never()).addContributorToProject(exportedProject, newSupervisor, Role.SUPERVISOR);
}
@Test

@ -19,7 +19,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import se.su.dsv.scipro.daisyexternal.http.DaisyAPI;
import se.su.dsv.scipro.io.dto.ThesisToBeCreated;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reusable.SciProUtilities;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.Unit;
@ -71,6 +70,6 @@ public class ExternalExporterDaisyImplTest {
ArgumentCaptor<ThesisToBeCreated> captor = ArgumentCaptor.forClass(ThesisToBeCreated.class);
verify(daisyAPI).createProject(captor.capture());
assertEquals(captor.getValue().getStartDate(), SciProUtilities.toCalendar(daisyStartDate));
assertEquals(captor.getValue().getStartDate(), daisyStartDate);
}
}