Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Wayne Westmoreland 2023-11-23 15:44:48 +01:00
commit da53f15bb1
24 changed files with 369 additions and 43 deletions

@ -75,4 +75,6 @@ public interface DaisyAPI {
PublishingConsent getPublishingConsent(int projectId, int personId);
boolean setPublishingConsent(int projectId, int personId, PublishingConsentLevel publishingConsentLevel);
List<ResearchSubject> getNationalResearchSubjects(int organisationId);
}

@ -434,6 +434,15 @@ public class DaisyAPIImpl implements DaisyAPI {
}
}
@Override
public List<ResearchSubject> getNationalResearchSubjects(int organisationId) {
return units()
.path(Integer.toString(organisationId))
.path("nationalSubjectCategories")
.request(MediaType.APPLICATION_XML_TYPE)
.get(new GenericType<>() {});
}
private WebTarget program() {
return target()
.path(PROGRAM);

@ -25,5 +25,9 @@ public class GradingModule extends PrivateModule {
expose(ThesisApprovedHistoryService.class);
bind(ThesisSubmissionHistoryService.class).to(GradingHistory.class);
expose(ThesisSubmissionHistoryService.class);
bind(NationalSubjectCategoryRepository.class).to(NationalSubjectCategoryRepositoryImpl.class);
bind(NationalSubjectCategoryService.class).to(NationalSubjectCategoryServiceImpl.class);
expose(NationalSubjectCategoryService.class);
}
}

@ -0,0 +1,116 @@
package se.su.dsv.scipro.grading;
import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "national_subject_category")
public class NationalSubjectCategory {
@Id
@GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Basic
@Column(name = "external_id")
private Integer externalId;
@Basic
@Column(name = "swedish_name")
private String swedishName;
@Basic
@Column(name = "english_name")
private String englishName;
@Basic
@Column(name = "active")
private boolean active;
@Basic
@Column(name = "preselected")
private boolean preselected;
public NationalSubjectCategory() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getExternalId() {
return externalId;
}
public void setExternalId(Integer externalId) {
this.externalId = externalId;
}
public String getSwedishName() {
return swedishName;
}
public void setSwedishName(String swedishName) {
this.swedishName = swedishName;
}
public String getEnglishName() {
return englishName;
}
public void setEnglishName(String englishName) {
this.englishName = englishName;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isPreselected() {
return preselected;
}
public void setPreselected(boolean preselected) {
this.preselected = preselected;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof NationalSubjectCategory that)) {
return false;
}
if (this.id != null) {
return Objects.equals(this.id, that.id);
}
return Objects.equals(this.externalId, that.externalId)
&& Objects.equals(this.swedishName, that.swedishName)
&& Objects.equals(this.englishName, that.englishName)
&& Objects.equals(this.active, that.active)
&& Objects.equals(this.preselected, that.preselected);
}
@Override
public int hashCode() {
if (this.id != null) {
return Objects.hashCode(id);
}
return Objects.hash(externalId, swedishName, englishName, active, preselected);
}
}

@ -0,0 +1,9 @@
package se.su.dsv.scipro.grading;
import java.util.Optional;
public interface NationalSubjectCategoryRepository {
void save(NationalSubjectCategory nationalSubjectCategory);
Optional<NationalSubjectCategory> findByExternalId(Integer externalId);
}

@ -0,0 +1,40 @@
package se.su.dsv.scipro.grading;
import com.google.inject.persist.Transactional;
import jakarta.persistence.EntityManager;
import se.su.dsv.scipro.system.AbstractRepository;
import javax.inject.Inject;
import javax.inject.Provider;
import java.util.Optional;
public class NationalSubjectCategoryRepositoryImpl
extends AbstractRepository
implements NationalSubjectCategoryRepository
{
@Inject
public NationalSubjectCategoryRepositoryImpl(Provider<EntityManager> em) {
super(em);
}
@Override
@Transactional
public void save(NationalSubjectCategory nationalSubjectCategory) {
EntityManager entityManager = em();
if (entityManager.contains(nationalSubjectCategory)) {
entityManager.merge(nationalSubjectCategory);
}
else {
entityManager.persist(nationalSubjectCategory);
}
}
@Override
public Optional<NationalSubjectCategory> findByExternalId(Integer externalId) {
NationalSubjectCategory nationalSubjectCategory = from(QNationalSubjectCategory.nationalSubjectCategory)
.where(QNationalSubjectCategory.nationalSubjectCategory.externalId.eq(externalId))
.select(QNationalSubjectCategory.nationalSubjectCategory)
.fetchOne();
return Optional.ofNullable(nationalSubjectCategory);
}
}

@ -0,0 +1,9 @@
package se.su.dsv.scipro.grading;
import java.util.Optional;
public interface NationalSubjectCategoryService {
Optional<NationalSubjectCategory> findByExternalId(Integer externalId);
void save(NationalSubjectCategory nationalSubjectCategory);
}

@ -0,0 +1,25 @@
package se.su.dsv.scipro.grading;
import javax.inject.Inject;
import java.util.Optional;
public class NationalSubjectCategoryServiceImpl
implements NationalSubjectCategoryService
{
private final NationalSubjectCategoryRepository nationalSubjectCategoryRepository;
@Inject
public NationalSubjectCategoryServiceImpl(NationalSubjectCategoryRepository nationalSubjectCategoryRepository) {
this.nationalSubjectCategoryRepository = nationalSubjectCategoryRepository;
}
@Override
public Optional<NationalSubjectCategory> findByExternalId(Integer externalId) {
return nationalSubjectCategoryRepository.findByExternalId(externalId);
}
@Override
public void save(NationalSubjectCategory nationalSubjectCategory) {
nationalSubjectCategoryRepository.save(nationalSubjectCategory);
}
}

@ -0,0 +1,8 @@
package se.su.dsv.scipro.match;
class AllowAllIdeaCreationJudge implements IdeaCreationJudge {
@Override
public Decision ruling(Idea idea) {
return Decision.allowed();
}
}

@ -3,19 +3,25 @@ package se.su.dsv.scipro.match;
public final class Decision {
private final boolean allowed;
private final String reason;
private final Integer identifier;
private Decision(final boolean allowed, final String reason) {
private Decision(final boolean allowed, final String reason, Integer identifier) {
this.allowed = allowed;
this.reason = reason;
this.identifier = identifier;
}
public static Decision allowed() {
return new Decision(true, "");
return allowed(null);
}
public static Decision allowed(Integer identifier) {
return new Decision(true, "", identifier);
}
public static Decision denied(String reason) {
return new Decision(false, reason);
return new Decision(false, reason, null);
}
public boolean isAllowed() {
@ -25,4 +31,8 @@ public final class Decision {
public String getReason() {
return reason;
}
public Integer getIdentifier() {
return identifier;
}
}

@ -1,12 +1,13 @@
package se.su.dsv.scipro.match;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.multibindings.OptionalBinder;
public class MatchModule extends AbstractModule {
@Override
protected void configure() {
Multibinder.newSetBinder(binder(), IdeaCreationJudge.class);
OptionalBinder.newOptionalBinder(binder(), IdeaCreationJudge.class)
.setDefault().to(AllowAllIdeaCreationJudge.class);
bind(ProjectStartNotifier.class).asEagerSingleton();
bind(AddActivityPlanOnProjectStart.class).asEagerSingleton();
bind(ApplicationPeriodService.class).to(ApplicationPeriodServiceImpl.class);

@ -109,4 +109,8 @@ public class SupervisorGradingReport extends GradingReport {
public void setMotivation(String motivation) {
this.motivation = motivation;
}
public boolean hasProvidedOverallMotivation() {
return getMotivation() != null && !getMotivation().isBlank();
}
}

@ -27,7 +27,7 @@ public class IdeaExportWorker extends AbstractWorker {
private final IdeaService ideaService;
private final MailEventService mailService;
private final ProjectService projectService;
private final Set<IdeaCreationJudge> ideaCreationJudges;
private final IdeaCreationJudge ideaCreationJudge;
private final EventBus eventBus;
private final FirstMeetingService firstMeetingService;
@ -35,14 +35,14 @@ public class IdeaExportWorker extends AbstractWorker {
public IdeaExportWorker(final IdeaService ideaService,
final MailEventService mailService,
final ProjectService projectService,
final Set<IdeaCreationJudge> ideaCreationJudges,
final IdeaCreationJudge ideaCreationJudge,
final EventBus eventBus,
final FirstMeetingService firstMeetingService)
{
this.ideaService = ideaService;
this.mailService = mailService;
this.projectService = projectService;
this.ideaCreationJudges = ideaCreationJudges;
this.ideaCreationJudge = ideaCreationJudge;
this.eventBus = eventBus;
this.firstMeetingService = firstMeetingService;
}
@ -55,7 +55,7 @@ public class IdeaExportWorker extends AbstractWorker {
Decision decision = isAllowedToStart(idea);
if (decision.isAllowed()) {
allow(idea);
createProject(idea);
createProject(idea, decision.getIdentifier());
eventBus.post(new ProjectStartedEvent(idea));
} else {
deny(idea, decision.getReason());
@ -74,13 +74,7 @@ public class IdeaExportWorker extends AbstractWorker {
}
private Decision isAllowedToStart(final Idea idea) {
for (IdeaCreationJudge ideaCreationJudge : ideaCreationJudges) {
Decision decision = ideaCreationJudge.ruling(idea);
if (!decision.isAllowed()) {
return decision;
}
}
return Decision.allowed();
return ideaCreationJudge.ruling(idea);
}
@ -101,7 +95,7 @@ public class IdeaExportWorker extends AbstractWorker {
ideaService.save(idea);
}
private void createProject(Idea idea) {
private void createProject(Idea idea, Integer identifier) {
beginTransaction();
LOGGER.info("Exporting idea: {}", idea);
Project project = Project.builder()
@ -110,6 +104,7 @@ public class IdeaExportWorker extends AbstractWorker {
.startDate(getCourseStartDate(idea))
.headSupervisor(idea.getMatch().getSupervisor())
.projectParticipants(getAuthors(idea))
.identifier(identifier)
.build();
project.setExpectedEndDate(idea.getApplicationPeriod().getCourseEndDate());
project.setResearchArea(idea.getResearchArea());

@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS `national_subject_category` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`external_id` INT NOT NULL,
`swedish_name` VARCHAR(255) NOT NULL,
`english_name` VARCHAR(255) NOT NULL,
`active` BOOLEAN NOT NULL,
`preselected` BOOLEAN NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `U_national_subject_category_external_id` (`external_id`)
);

@ -37,9 +37,7 @@ public class Daisy implements IdeaCreationJudge {
try {
exporterFacade.exportProject(project, project.getHeadSupervisor().getUnit());
externalExporter.deleteProject(project);
project.setIdentifier(null);
return Decision.allowed();
return Decision.allowed(project.getIdentifier());
} catch (ExternalExportException e) {
return Decision.denied(e.getMessage());
}

@ -22,8 +22,8 @@ public class DaisyModule extends ServletModule {
bind(ExternalImporter.class).to(ExternalImporterDaisyImpl.class);
bind(ImporterTransactions.class).to(ImporterTransactionsImpl.class);
Multibinder<IdeaCreationJudge> judges = Multibinder.newSetBinder(binder(), IdeaCreationJudge.class);
judges.addBinding().to(Daisy.class);
OptionalBinder.newOptionalBinder(binder(), IdeaCreationJudge.class)
.setBinding().to(Daisy.class);
bind(ExternalExporter.class).to(ExternalExporterDaisyImpl.class);
bind(UserImportWorker.class);

@ -15,11 +15,13 @@ public class DaisyWorkerInitialization {
Provider<UserImportWorker> userImportWorker,
Provider<ProjectFinalizer> projectFinalizer,
Provider<RejectedThesisWorker> rejectedThesisWorkerProvider,
Provider<GradingCompletedMilestoneActivator> gradingFinalizer) {
Provider<GradingCompletedMilestoneActivator> gradingFinalizer,
Provider<ImportNationalCategories> importNationalCategories) {
scheduler.schedule("Export projects to daisy").runBy(projectExporter).dailyAt(1, 0);
scheduler.schedule("Remote supervisor (and projects) bulk import").runBy(userImportWorker).dailyAt(1, 30);
scheduler.schedule("Mark projects as completed").runBy(projectFinalizer).dailyAt(2, 0);
scheduler.schedule("Mark the 'grading completed' milestone as completed").runBy(gradingFinalizer).dailyAt(2, 30);
scheduler.schedule("Reject thesis based on examiner rejection in Daisy").runBy(rejectedThesisWorkerProvider).every(5, TimeUnit.MINUTES);
scheduler.schedule("Import national subject categories").runBy(importNationalCategories).dailyAt(3, 0);
}
}

@ -0,0 +1,50 @@
package se.su.dsv.scipro.integration.daisy.workers;
import se.su.dsv.scipro.daisyExternal.http.DaisyAPI;
import se.su.dsv.scipro.grading.NationalSubjectCategory;
import se.su.dsv.scipro.grading.NationalSubjectCategoryService;
import se.su.dsv.scipro.io.dto.ResearchSubject;
import se.su.dsv.scipro.workerthreads.AbstractWorker;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
public class ImportNationalCategories extends AbstractWorker {
private static final int DSV = 4;
private final DaisyAPI daisyAPI;
private final NationalSubjectCategoryService nationalSubjectCategoryService;
@Inject
public ImportNationalCategories(DaisyAPI daisyAPI, NationalSubjectCategoryService nationalSubjectCategoryService) {
this.daisyAPI = daisyAPI;
this.nationalSubjectCategoryService = nationalSubjectCategoryService;
}
@Override
protected void doWork() {
List<ResearchSubject> nationalResearchSubjects = daisyAPI.getNationalResearchSubjects(DSV);
for (ResearchSubject researchSubject : nationalResearchSubjects) {
Optional<NationalSubjectCategory> nationalSubjectCategory =
nationalSubjectCategoryService.findByExternalId(researchSubject.getExternalID());
if (nationalSubjectCategory.isEmpty()) {
NationalSubjectCategory newSubjectCategory = new NationalSubjectCategory();
newSubjectCategory.setExternalId(researchSubject.getExternalID());
newSubjectCategory.setSwedishName(researchSubject.getName());
newSubjectCategory.setEnglishName(researchSubject.getNameEn());
newSubjectCategory.setActive(researchSubject.isActive());
newSubjectCategory.setPreselected(researchSubject.isPreselected());
nationalSubjectCategoryService.save(newSubjectCategory);
}
else {
NationalSubjectCategory existingSubjectCategory = nationalSubjectCategory.get();
existingSubjectCategory.setSwedishName(researchSubject.getName());
existingSubjectCategory.setEnglishName(researchSubject.getNameEn());
existingSubjectCategory.setActive(researchSubject.isActive());
existingSubjectCategory.setPreselected(researchSubject.isPreselected());
nationalSubjectCategoryService.save(existingSubjectCategory);
}
}
}
}

@ -51,7 +51,7 @@
</p>
<textarea class="form-control" rows="10" wicket:id="overall_motivation"></textarea>
</div>
<wicket:enclosure child="grading_basis_requirements_not_met">
<wicket:container wicket:id="grading_basis_missing">
<h4 class="mt-3">Criteria not met</h4>
<ul>
<li wicket:id="grading_basis_requirements_not_met">
@ -61,8 +61,11 @@
<span wicket:id="required_points"></span> required to pass
</wicket:message>
</li>
<li wicket:id="overall_motivation_missing">
Overall motivation not filled in
</li>
</ul>
</wicket:enclosure>
</wicket:container>
<wicket:enclosure child="save">
<div class="position-sticky bottom-0 bg-white py-3 d-flex">
<button class="btn btn-success me-3" wicket:id="save">

@ -98,7 +98,16 @@ public class GradingBasisPanel extends GenericPanel<Project> {
assessment.getPoints(),
0) < assessment.criterion().minimumPoints())
.toList());
add(new AutoHidingListView<>("grading_basis_requirements_not_met", criteriaNotMet) {
WebMarkupContainer gradingBasisMissing = new WebMarkupContainer("grading_basis_missing") {
@Override
protected void onConfigure() {
super.onConfigure();
String overallMotivation = gradingBasis.getObject().getOverallMotivation();
setVisible(!criteriaNotMet.getObject().isEmpty() || overallMotivation == null || overallMotivation.isBlank());
}
};
add(gradingBasisMissing);
gradingBasisMissing.add(new AutoHidingListView<>("grading_basis_requirements_not_met", criteriaNotMet) {
@Override
protected void populateItem(ListItem<Assessment> item) {
IModel<Criterion> criterion = item.getModel().map(Assessment::criterion);
@ -108,12 +117,19 @@ public class GradingBasisPanel extends GenericPanel<Project> {
item.add(new Label("required_points", criterion.map(Criterion::minimumPoints)));
}
});
gradingBasisMissing.add(new WebMarkupContainer("overall_motivation_missing") {
@Override
protected void onConfigure() {
super.onConfigure();
String overallMotivation = gradingBasis.getObject().getOverallMotivation();
setVisible(overallMotivation == null || overallMotivation.isBlank());
}
});
IModel<String> overallMotivation = LambdaModel.of(gradingBasis,
GradingBasis::getOverallMotivation,
GradingBasis::setOverallMotivation);
TextArea<String> overallMotivationField = new TextArea<>("overall_motivation", overallMotivation);
overallMotivationField.setRequired(true);
add(overallMotivationField);
add(new Label("rejection_comment", gradingBasis.map(GradingBasis::rejectionComment)) {

@ -38,17 +38,18 @@
<li wicket:id="status_plagiarism"></li>
<li>
<div wicket:id="status_grading_basis">></div>
<wicket:enclosure child="grading_basis_requirements_not_met">
<ul>
<li wicket:id="grading_basis_requirements_not_met">
<wicket:message key="criteria_not_met">
<span wicket:id="title">[U1 title]</span>
<span wicket:id="given_points"></span>/<span wicket:id="maximum_points"></span>
<span wicket:id="required_points"></span> required to pass
</wicket:message>
</li>
</ul>
</wicket:enclosure>
<ul wicket:id="grading_basis_missing">
<li wicket:id="grading_basis_requirements_not_met">
<wicket:message key="criteria_not_met">
<span wicket:id="title">[U1 title]</span>
<span wicket:id="given_points"></span>/<span wicket:id="maximum_points"></span>
<span wicket:id="required_points"></span> required to pass
</wicket:message>
</li>
<li wicket:id="grading_basis_overall_motivation_missing">
Overall motivation not filled in
</li>
</ul>
</li>
<li>
<div wicket:id="status_individual_assessment"></div>

@ -84,7 +84,15 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
add(new UserLabel("author_name", authorModel));
add(new AutoHidingListView<>("grading_basis_requirements_not_met", gradingBasisCriterionNotMet) {
WebMarkupContainer gradingBasisMissing = new WebMarkupContainer("grading_basis_missing") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!hasFilledInGradingBasis.getObject());
}
};
add(gradingBasisMissing);
gradingBasisMissing.add(new AutoHidingListView<>("grading_basis_requirements_not_met", gradingBasisCriterionNotMet) {
@Override
protected void populateItem(ListItem<GradingCriterion> item) {
item.add(new Label("title", item.getModel().map(GradingCriterion::getTitle)));
@ -93,6 +101,13 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
item.add(new Label("required_points", item.getModel().map(GradingCriterion::getPointsRequiredToPass)));
}
});
gradingBasisMissing.add(new WebMarkupContainer("grading_basis_overall_motivation_missing") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!gradingReport.getObject().hasProvidedOverallMotivation());
}
});
add(new AutoHidingListView<>("individual_assessment_requirements_not_met", individualCriterionNotMet) {
@Override
protected void populateItem(ListItem<GradingCriterion> item) {
@ -164,7 +179,7 @@ public class IndividualAuthorAssessment extends GenericPanel<User> {
boolean criteriaMet = supervisorGradingReport.getProjectCriteria()
.stream()
.allMatch(GradingCriterion::meetsMinimumPointRequirement);
return criteriaMet && supervisorGradingReport.hasProvidedRejectionFeedback();
return criteriaMet && supervisorGradingReport.hasProvidedRejectionFeedback() && supervisorGradingReport.hasProvidedOverallMotivation();
}
private void redGreen(String id, IModel<Boolean> finished, String redTextKey, String greenTextKey) {

@ -163,7 +163,7 @@ public class SupervisorGradingReportPage extends AbstractSupervisorProjectDetail
boolean criteriaMet = supervisorGradingReport.getProjectCriteria()
.stream()
.allMatch(GradingCriterion::meetsMinimumPointRequirement);
return criteriaMet && supervisorGradingReport.hasProvidedRejectionFeedback();
return criteriaMet && supervisorGradingReport.hasProvidedRejectionFeedback() && supervisorGradingReport.hasProvidedOverallMotivation();
}
private boolean individualCriteriaDone(SupervisorGradingReport supervisorGradingReport) {

@ -19,7 +19,6 @@ import se.su.dsv.scipro.test.DomainObjects;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
@ -46,7 +45,7 @@ public class IdeaExportWorkerTest {
@BeforeEach
public void setUp() throws Exception {
worker = new IdeaExportWorker(ideaService, mailService, projectService, Collections.singleton(ideaCreationJudge), eventBus, firstMeetingService) {
worker = new IdeaExportWorker(ideaService, mailService, projectService, ideaCreationJudge, eventBus, firstMeetingService) {
@Override
protected void beginTransaction() {
}