diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java index 522fbcdb06..d3b1c35773 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumService.java @@ -25,4 +25,5 @@ public interface ProjectForumService { // TODO: Get these away from here List<Pair<ProjectThread, ForumPost>> latestPost(Project a, int amount); + boolean hasUnreadThreads(Project project, User user); } diff --git a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java index 4c4cc0b2c2..1825a16197 100644 --- a/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/forum/ProjectForumServiceImpl.java @@ -95,6 +95,17 @@ public class ProjectForumServiceImpl implements ProjectForumService { return postRepository.latestPost(project, amount); } + @Override + public boolean hasUnreadThreads(Project project, User user) { + List<ProjectThread> threads = getThreads(project); + for (ProjectThread thread : threads) { + if (!basicForumService.isThreadRead(user, thread.getForumThread())) { + return true; + } + } + return false; + } + @Override public ProjectThread findOne(long threadId) { return projectThreadRepository.findOne(threadId); diff --git a/core/src/main/java/se/su/dsv/scipro/project/Project.java b/core/src/main/java/se/su/dsv/scipro/project/Project.java index 92c82a9014..9429d2cbfc 100755 --- a/core/src/main/java/se/su/dsv/scipro/project/Project.java +++ b/core/src/main/java/se/su/dsv/scipro/project/Project.java @@ -85,6 +85,13 @@ public class Project extends DomainObject { @Enumerated(EnumType.STRING) private Language language; + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "project_user_note", joinColumns = @JoinColumn(name = "project_id")) + @Column(name = "note") + @SuppressWarnings("JpaDataSourceORMInspection") // false warning from IntelliJ for the @MapKeyJoinColumn + @MapKeyJoinColumn(name = "user_id") + private Map<User, String> userNotes = new HashMap<>(); + @PrePersist @PreUpdate void cleanTitle() { @@ -94,6 +101,14 @@ public class Project extends DomainObject { title = title.trim(); } + public Map<User, String> getUserNotes() { + return userNotes; + } + + public void setUserNotes(Map<User, String> userNotes) { + this.userNotes = userNotes; + } + public boolean isFinalSeminarRuleExempted() { return finalSeminarRuleExempted; } diff --git a/core/src/main/java/se/su/dsv/scipro/project/ProjectNoteService.java b/core/src/main/java/se/su/dsv/scipro/project/ProjectNoteService.java new file mode 100644 index 0000000000..7ff1657d63 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/project/ProjectNoteService.java @@ -0,0 +1,9 @@ +package se.su.dsv.scipro.project; + +import se.su.dsv.scipro.system.User; + +public interface ProjectNoteService { + String getUserNote(Project project, User user); + + void setUserNote(Project project, User user, String note); +} diff --git a/core/src/main/java/se/su/dsv/scipro/project/ProjectRepo.java b/core/src/main/java/se/su/dsv/scipro/project/ProjectRepo.java index 0121567847..a1c985044f 100755 --- a/core/src/main/java/se/su/dsv/scipro/project/ProjectRepo.java +++ b/core/src/main/java/se/su/dsv/scipro/project/ProjectRepo.java @@ -11,4 +11,8 @@ import java.util.List; @Transactional public interface ProjectRepo extends JpaRepository<Project, Long>, QueryDslPredicateExecutor<Project> { List<User> findMultipleAuthors(Collection<Project> projects); + + String getUserNoteForProject(Project project, User user); + + void setUserNoteForProject(Project project, User user, String note); } \ No newline at end of file diff --git a/core/src/main/java/se/su/dsv/scipro/project/ProjectRepoImpl.java b/core/src/main/java/se/su/dsv/scipro/project/ProjectRepoImpl.java index 627af554ec..8211412568 100644 --- a/core/src/main/java/se/su/dsv/scipro/project/ProjectRepoImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/project/ProjectRepoImpl.java @@ -1,5 +1,6 @@ package se.su.dsv.scipro.project; +import jakarta.transaction.Transactional; import se.su.dsv.scipro.system.GenericRepo; import se.su.dsv.scipro.system.User; @@ -30,4 +31,16 @@ public class ProjectRepoImpl extends GenericRepo<Project, Long> implements Proje query.setParameter("projects", projects); return query.getResultList(); } + + @Override + public String getUserNoteForProject(Project project, User user) { + return project.getUserNotes().get(user); + } + + @Override + @Transactional + public void setUserNoteForProject(Project project, User user, String note) { + project.getUserNotes().put(user, note); + save(project); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/project/ProjectServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/project/ProjectServiceImpl.java index ed71696686..e52cae2397 100755 --- a/core/src/main/java/se/su/dsv/scipro/project/ProjectServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/project/ProjectServiceImpl.java @@ -22,7 +22,7 @@ import java.time.Duration; import java.time.Instant; import java.util.*; -public class ProjectServiceImpl extends AbstractServiceImpl<Project, Long> implements ProjectService { +public class ProjectServiceImpl extends AbstractServiceImpl<Project, Long> implements ProjectService, ProjectNoteService { public static final int MIN_TITLE_LENGTH = 3; private final ProjectRepo projectRepo; @@ -175,6 +175,16 @@ public class ProjectServiceImpl extends AbstractServiceImpl<Project, Long> imple return completed; } + @Override + public String getUserNote(Project project, User user) { + return projectRepo.getUserNoteForProject(project, user); + } + + @Override + public void setUserNote(Project project, User user, String note) { + projectRepo.setUserNoteForProject(project, user, note); + } + @Override public List<Project> findAll(Filter filter, Pageable pageable) { return findAll(toPredicate(filter), pageable); diff --git a/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/SupervisorProjectNoteDisplay.java b/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/SupervisorProjectNoteDisplay.java new file mode 100644 index 0000000000..1a6c45d452 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/SupervisorProjectNoteDisplay.java @@ -0,0 +1,5 @@ +package se.su.dsv.scipro.settings.dataobjects; + +public enum SupervisorProjectNoteDisplay { + COMPACT, FULL +} diff --git a/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/UserProfile.java b/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/UserProfile.java index 8cf7d44e31..ac863a2afa 100644 --- a/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/UserProfile.java +++ b/core/src/main/java/se/su/dsv/scipro/settings/dataobjects/UserProfile.java @@ -58,6 +58,11 @@ public class UserProfile extends DomainObject { @Enumerated(EnumType.STRING) private Roles selectedRole; + @Basic + @Enumerated(EnumType.STRING) + @Column(name = "supervisor_project_note_display") + private SupervisorProjectNoteDisplay supervisorProjectNoteDisplay = SupervisorProjectNoteDisplay.COMPACT; + @Override public Long getId() { return this.id; @@ -147,6 +152,14 @@ public class UserProfile extends DomainObject { this.selectedRole = selectedRole; } + public SupervisorProjectNoteDisplay getSupervisorProjectNoteDisplay() { + return supervisorProjectNoteDisplay; + } + + public void setSupervisorProjectNoteDisplay(SupervisorProjectNoteDisplay supervisorProjectNoteDisplay) { + this.supervisorProjectNoteDisplay = supervisorProjectNoteDisplay; + } + @Override public String toString() { return "UserProfile(id=" + this.getId() + ", user=" + this.getUser() + ", skypeId=" + this.getSkypeId() + ", phoneNumber=" + this.getPhoneNumber() + ", otherInfo=" + this.getOtherInfo() + ", mailCompilation=" + this.isMailCompilation() + ", defaultProjectStatusFilter=" + this.getDefaultProjectStatusFilter() + ", defaultProjectTeamMemberRolesFilter=" + this.getDefaultProjectTeamMemberRolesFilter() + ", defaultSupervisorFilter=" + this.isDefaultSupervisorFilter() + ", defaultProjectTypeFilter=" + this.getDefaultProjectTypeFilter() + ", selectedRole=" + this.getSelectedRole() + ")"; diff --git a/core/src/main/resources/db/migration/V388__user_notes_for_projects.sql b/core/src/main/resources/db/migration/V388__user_notes_for_projects.sql new file mode 100644 index 0000000000..334caad59b --- /dev/null +++ b/core/src/main/resources/db/migration/V388__user_notes_for_projects.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `project_user_note` ( + `project_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `note` text NULL, + PRIMARY KEY (`project_id`, `user_id`), + CONSTRAINT `FK_project_user_note_project` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_project_user_note_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +); + +ALTER TABLE `user_profile` + ADD COLUMN `supervisor_project_note_display` VARCHAR(15) NOT NULL DEFAULT 'COMPACT'; diff --git a/view/src/main/java/se/su/dsv/scipro/components/MaxLengthLabel.java b/view/src/main/java/se/su/dsv/scipro/components/MaxLengthLabel.java new file mode 100644 index 0000000000..edfdd48ab9 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/components/MaxLengthLabel.java @@ -0,0 +1,37 @@ +package se.su.dsv.scipro.components; + +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.model.IModel; +import org.apache.wicket.util.convert.IConverter; + +import java.util.Locale; + +public class MaxLengthLabel extends Label { + private final IModel<Integer> maxLengthModel; + + public MaxLengthLabel(String id, IModel<String> dataModel, IModel<Integer> maxLength) { + super(id, dataModel); + this.maxLengthModel = maxLength; + } + + @Override + protected IConverter<?> createConverter(Class<?> type) { + return new MaxLengthConverter(); + } + + private class MaxLengthConverter implements IConverter<String> { + @Override + public String convertToObject(String s, Locale locale) { + return s; + } + + @Override + public String convertToString(String o, Locale locale) { + Integer maxLength = maxLengthModel.getObject(); + if (o.length() > maxLength) { + return o.substring(0, maxLength) + "..."; + } + return o; + } + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/components/ModalWindowPlus.java b/view/src/main/java/se/su/dsv/scipro/components/ModalWindowPlus.java index 9faa92158d..0eca546462 100644 --- a/view/src/main/java/se/su/dsv/scipro/components/ModalWindowPlus.java +++ b/view/src/main/java/se/su/dsv/scipro/components/ModalWindowPlus.java @@ -1,6 +1,7 @@ package se.su.dsv.scipro.components; import org.apache.wicket.Component; +import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.ComponentTag; @@ -9,6 +10,7 @@ import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; +import org.danekja.java.util.function.serializable.SerializableConsumer; import java.util.function.Function; @@ -83,4 +85,13 @@ public class ModalWindowPlus extends Panel { component.setOutputMarkupPlaceholderTag(true); replace(component); } + + public void onClose(SerializableConsumer<AjaxRequestTarget> onClose) { + add(new AjaxEventBehavior("hidden.bs.modal") { + @Override + protected void onEvent(AjaxRequestTarget target) { + onClose.accept(target); + } + }); + } } diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/AbstractReadStatePanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/AbstractReadStatePanel.java index 4f57d632d0..25eea67afa 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/AbstractReadStatePanel.java +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/AbstractReadStatePanel.java @@ -20,7 +20,7 @@ public abstract class AbstractReadStatePanel extends Panel { @Override public void onClick(final Optional<AjaxRequestTarget> target) { target.ifPresent(t -> { - toggleReadState(t); + onFlagClick(t); t.add(icon); }); } @@ -33,7 +33,7 @@ public abstract class AbstractReadStatePanel extends Panel { } protected abstract boolean isRead(); - protected abstract void toggleReadState(final AjaxRequestTarget target); + protected abstract void onFlagClick(final AjaxRequestTarget target); public static final String TOGGLE = "toggle"; static final String ICON = "icon"; diff --git a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java index 2e16cb4953..908c096695 100644 --- a/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java +++ b/view/src/main/java/se/su/dsv/scipro/forum/panels/threaded/ThreadReadStatePanel.java @@ -31,7 +31,7 @@ public class ThreadReadStatePanel extends AbstractReadStatePanel { } @Override - protected void toggleReadState(final AjaxRequestTarget target) { + protected void onFlagClick(final AjaxRequestTarget target) { boolean read = isRead(); basicForumService.setThreadRead(SciProSession.get().getUser(), model.getObject(), !read); diff --git a/view/src/main/java/se/su/dsv/scipro/notifications/panels/NotificationDataPanel.java b/view/src/main/java/se/su/dsv/scipro/notifications/panels/NotificationDataPanel.java index 8718550924..d5e132eb9f 100755 --- a/view/src/main/java/se/su/dsv/scipro/notifications/panels/NotificationDataPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/notifications/panels/NotificationDataPanel.java @@ -117,7 +117,7 @@ public class NotificationDataPanel extends Panel { } @Override - protected void toggleReadState(final AjaxRequestTarget target) { + protected void onFlagClick(final AjaxRequestTarget target) { Notification notification = rowModel.getObject(); notification.setUnread(!notification.isUnread()); notificationService.save(notification); diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.html b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.html new file mode 100644 index 0000000000..7a5e924096 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.html @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org"> +<body> +<wicket:panel> + <div wicket:id="edit_note_modal"></div> + <span wicket:id="shortened_note"></span> + <wicket:container wicket:id="full_note"></wicket:container> + <a wicket:id="view_note"><wicket:message key="note.view.edit"/></a> +</wicket:panel> +</body> +</html> \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.utf8.properties b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.utf8.properties new file mode 100644 index 0000000000..5f31ea7d1c --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteCellPanel.utf8.properties @@ -0,0 +1,2 @@ +note.modal.title=View/edit note for ${title} +note.view.edit=View/edit note diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteForm.html b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteForm.html new file mode 100644 index 0000000000..bf546f6a76 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn$ViewAndEditNoteForm.html @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org"> +<body> +<wicket:panel> + <form wicket:id="form"> + <div wicket:id="feedback"></div> + <div class="mb-3"> + <label wicket:for="note" class="sr-only">Note</label> + <textarea wicket:id="note" rows="20" class="form-control"></textarea> + </div> + <button wicket:id="save" type="submit" class="btn btn-primary">Save</button> + </form> +</wicket:panel> +</body> +</html> \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn.java b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn.java new file mode 100644 index 0000000000..b35962f891 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/ProjectNoteColumn.java @@ -0,0 +1,129 @@ +package se.su.dsv.scipro.supervisor.panels; + +import jakarta.inject.Inject; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.AjaxLink; +import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink; +import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; +import org.apache.wicket.extensions.markup.html.repeater.data.table.export.AbstractExportableColumn; +import org.apache.wicket.feedback.FencedFeedbackPanel; +import org.apache.wicket.injection.Injector; +import org.apache.wicket.markup.html.basic.MultiLineLabel; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.TextArea; +import org.apache.wicket.markup.html.panel.GenericPanel; +import org.apache.wicket.markup.repeater.Item; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.model.StringResourceModel; +import se.su.dsv.scipro.components.LargeModalWindow; +import se.su.dsv.scipro.components.MaxLengthLabel; +import se.su.dsv.scipro.components.ModalWindowPlus; +import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.project.ProjectNoteService; +import se.su.dsv.scipro.settings.dataobjects.SupervisorProjectNoteDisplay; +import se.su.dsv.scipro.system.User; + +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; + +public class ProjectNoteColumn extends AbstractExportableColumn<Project, String> { + @Inject + private ProjectNoteService projectNoteService; + + private final IModel<User> user; + private final IModel<SupervisorProjectNoteDisplay> supervisorProjectNoteDisplayModel; + + public ProjectNoteColumn(IModel<String> displayModel, IModel<User> user, + IModel<SupervisorProjectNoteDisplay> supervisorProjectNoteDisplayModel) { + super(displayModel); + Injector.get().inject(this); + this.supervisorProjectNoteDisplayModel = supervisorProjectNoteDisplayModel; + this.user = user; + } + + @Override + public IModel<String> getDataModel(IModel<Project> rowModel) { + return LoadableDetachableModel.of(() -> projectNoteService.getUserNote( + rowModel.getObject(), + user.getObject())); + } + + @Override + public void populateItem( + Item<ICellPopulator<Project>> cellItem, + String componentId, + IModel<Project> rowModel) + { + cellItem.add(new ViewAndEditNoteCellPanel(componentId, rowModel)); + } + + private class ViewAndEditNoteCellPanel extends GenericPanel<Project> { + public ViewAndEditNoteCellPanel(String id, IModel<Project> model) { + super(id, model); + + ModalWindowPlus modal = new LargeModalWindow("edit_note_modal"); + modal.setTitle(new StringResourceModel("note.modal.title", this, model)); + modal.setContent(componentId -> new ViewAndEditNoteForm(componentId, model)); + add(modal); + + setOutputMarkupId(true); + modal.onClose(target -> target.add(this)); + + add(new MaxLengthLabel("shortened_note", getDataModel(model), Model.of(100)) { + @Override + protected void onConfigure() { + super.onConfigure(); + setVisibilityAllowed(supervisorProjectNoteDisplayModel.getObject() == SupervisorProjectNoteDisplay.COMPACT); + } + }); + add(new MultiLineLabel("full_note", getDataModel(model)) { + @Override + protected void onConfigure() { + super.onConfigure(); + setVisibilityAllowed(supervisorProjectNoteDisplayModel.getObject() == SupervisorProjectNoteDisplay.FULL); + } + }); + + AjaxLink<Object> noteLink = new AjaxLink<>("view_note") { + @Override + public void onClick(AjaxRequestTarget target) { + modal.show(target); + } + }; + add(noteLink); + } + } + + private class ViewAndEditNoteForm extends GenericPanel<Project> { + public ViewAndEditNoteForm(String id, IModel<Project> project) { + super(id, project); + + IModel<String> note = getDataModel(project); + + Form<Project> form = new Form<>("form", project) { + @Override + protected void onSubmit() { + projectNoteService.setUserNote( + project.getObject(), + user.getObject(), + note.getObject() + ); + success("Note saved at " + LocalTime.now().truncatedTo(ChronoUnit.SECONDS)); + } + }; + add(form); + + form.add(new FencedFeedbackPanel("feedback", form)); + + form.add(new TextArea<>("note", note)); + form.add(new AjaxSubmitLink("save") { + @Override + protected void onAfterSubmit(AjaxRequestTarget target) { + target.add(form); + } + }); + } + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.html b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.html index 9b68bf8d79..73ab571f7f 100755 --- a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.html @@ -31,6 +31,10 @@ </strong> <div wicket:id="projectTypes"></div> </div> + <fieldset class="col-6 col-md-3 col-lg-2"> + <legend>Note</legend> + <div wicket:id="note_display"></div> + </fieldset> </div> </form> </div> diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.java b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.java index 00498cbd6b..e3cab053eb 100755 --- a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.java @@ -1,8 +1,11 @@ package se.su.dsv.scipro.supervisor.panels; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior; import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox; +import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder; +import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.LambdaColumn; import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider; @@ -10,8 +13,10 @@ import org.apache.wicket.markup.html.form.EnumChoiceRenderer; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.LambdaChoiceRenderer; import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LambdaModel; +import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import se.su.dsv.scipro.components.*; import se.su.dsv.scipro.components.datatables.MultipleUsersColumn; @@ -19,11 +24,15 @@ import se.su.dsv.scipro.components.datatables.UserColumn; import se.su.dsv.scipro.dataproviders.FilteredDataProvider; import se.su.dsv.scipro.datatables.project.ProjectStateColumn; import se.su.dsv.scipro.datatables.project.ProjectTitleColumn; +import se.su.dsv.scipro.forum.ProjectForumService; +import se.su.dsv.scipro.forum.pages.threaded.SupervisorThreadedForumPage; +import se.su.dsv.scipro.forum.panels.AbstractReadStatePanel; import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.ProjectService; import se.su.dsv.scipro.project.ProjectStatus; import se.su.dsv.scipro.project.ProjectTeamMemberRoles; import se.su.dsv.scipro.session.SciProSession; +import se.su.dsv.scipro.settings.dataobjects.SupervisorProjectNoteDisplay; import se.su.dsv.scipro.settings.dataobjects.UserProfile; import se.su.dsv.scipro.springdata.services.UserProfileService; import se.su.dsv.scipro.system.ProjectType; @@ -51,9 +60,12 @@ public class SupervisorMyProjectsPanel extends Panel { private ProjectService projectService; @Inject private UserProfileService profileService; + @Inject + private ProjectForumService projectForumService; private ExportableDataPanel dataPanel; private ProjectService.Filter filter = new ProjectService.Filter(); + private IModel<SupervisorProjectNoteDisplay> supervisorProjectNoteDisplayModel = new Model<>(); public SupervisorMyProjectsPanel(String id) { super(id); @@ -72,6 +84,7 @@ public class SupervisorMyProjectsPanel extends Panel { private List<IColumn<Project, String>> createColumns() { List<IColumn<Project, String>> columns = new ArrayList<>(); columns.add(new ProjectStateColumn(Model.of("State"), "stateOfMind")); + columns.add(new ProjectForumStateColumn(Model.of("Forum"))); columns.add(new TemporalColumn<>(Model.of("Started"), "startDate", Project::getStartDate)); columns.add(new LambdaColumn<>(Model.of("Type"), "projectType.name", Project::getProjectTypeName)); columns.add(new ProjectTitleColumn(Model.of("Title"), "title")); @@ -81,6 +94,7 @@ public class SupervisorMyProjectsPanel extends Panel { return new ListAdapterModel<>(rowModel.map(Project::getProjectParticipants)); } }); + columns.add(new ProjectNoteColumn(Model.of("Note"), LoadableDetachableModel.of(this::currentUser), supervisorProjectNoteDisplayModel)); columns.add(new UserColumn<>(Model.of("Head supervisor"), "headSupervisor.fullName", Project::getHeadSupervisor)); return columns; } @@ -103,6 +117,7 @@ public class SupervisorMyProjectsPanel extends Panel { filter.setRoles(userProfile.getDefaultProjectTeamMemberRolesFilter()); filter.setFilterSupervisor(userProfile.isDefaultSupervisorFilter()); filter.setProjectTypes(userProfile.getDefaultProjectTypeFilter()); + supervisorProjectNoteDisplayModel.setObject(userProfile.getSupervisorProjectNoteDisplay()); } private User currentUser() { @@ -146,6 +161,19 @@ public class SupervisorMyProjectsPanel extends Panel { updateProfileWithCurrentFilter(); } }); + BootstrapRadioChoice<SupervisorProjectNoteDisplay> noteDisplay = new BootstrapRadioChoice<>( + "note_display", + supervisorProjectNoteDisplayModel, + List.of(SupervisorProjectNoteDisplay.values()), + new EnumChoiceRenderer<>(this)); + noteDisplay.add(new AjaxFormChoiceComponentUpdatingBehavior() { + @Override + public void onUpdate(AjaxRequestTarget target) { + target.add(dataPanel); + updateProfileWithCurrentFilter(); + } + }); + add(noteDisplay); } private void updateProfileWithCurrentFilter() { @@ -154,7 +182,33 @@ public class SupervisorMyProjectsPanel extends Panel { userProfile.setDefaultProjectTeamMemberRolesFilter(filter.getRoles()); userProfile.setDefaultSupervisorFilter(filter.isFilterSupervisor()); userProfile.setDefaultProjectTypeFilter(filter.getProjectTypes()); + userProfile.setSupervisorProjectNoteDisplay(supervisorProjectNoteDisplayModel.getObject()); profileService.save(userProfile); } } + + private class ProjectForumStateColumn extends AbstractColumn<Project, String> { + public ProjectForumStateColumn(IModel<String> label) { + super(label); + } + + @Override + public void populateItem(Item<ICellPopulator<Project>> item, String id, IModel<Project> projectModel) { + item.add(new AbstractReadStatePanel(id) { + @Override + protected boolean isRead() { + return !projectForumService.hasUnreadThreads( + projectModel.getObject(), + SciProSession.get().getUser()); + } + + @Override + protected void onFlagClick(AjaxRequestTarget target) { + setResponsePage( + SupervisorThreadedForumPage.class, + SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())); + } + }); + } + } } diff --git a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.utf8.properties b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.utf8.properties index 8c2d03baf2..17f454ad8c 100644 --- a/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.utf8.properties +++ b/view/src/main/java/se/su/dsv/scipro/supervisor/panels/SupervisorMyProjectsPanel.utf8.properties @@ -9,3 +9,6 @@ ProjectTeamMemberRoles.CO_SUPERVISOR= Co-Supervisor ProjectStatus.ACTIVE= Active ProjectStatus.INACTIVE= Inactive ProjectStatus.COMPLETED= Completed + +SupervisorProjectNoteDisplay.COMPACT=Compact +SupervisorProjectNoteDisplay.FULL=Full diff --git a/view/src/test/java/se/su/dsv/scipro/SciProTest.java b/view/src/test/java/se/su/dsv/scipro/SciProTest.java index af6c912281..e712c36790 100755 --- a/view/src/test/java/se/su/dsv/scipro/SciProTest.java +++ b/view/src/test/java/se/su/dsv/scipro/SciProTest.java @@ -83,6 +83,7 @@ import se.su.dsv.scipro.peer.PerformReviewService; import se.su.dsv.scipro.plagiarism.PlagiarismControl; import se.su.dsv.scipro.plagiarism.urkund.UrkundService; import se.su.dsv.scipro.profiles.CurrentProfile; +import se.su.dsv.scipro.project.ProjectNoteService; import se.su.dsv.scipro.project.ProjectPeopleStatisticsService; import se.su.dsv.scipro.project.ProjectService; import se.su.dsv.scipro.project.pages.ProjectStartPage; @@ -351,6 +352,8 @@ public abstract class SciProTest { protected ExaminerTimelineService examinerTimelineService; @Mock protected NationalSubjectCategoryService nationalSubjectCategoryService; + @Mock + protected ProjectNoteService projectNoteService; protected WicketTester tester; diff --git a/war/src/test/java/se/su/dsv/scipro/forum/ProjectForumServiceImplTest.java b/war/src/test/java/se/su/dsv/scipro/forum/ProjectForumServiceImplTest.java index 199d5e8520..c25abbd10f 100644 --- a/war/src/test/java/se/su/dsv/scipro/forum/ProjectForumServiceImplTest.java +++ b/war/src/test/java/se/su/dsv/scipro/forum/ProjectForumServiceImplTest.java @@ -20,6 +20,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ProjectForumServiceImplTest extends ForumModuleTest { @@ -29,23 +30,24 @@ public class ProjectForumServiceImplTest extends ForumModuleTest { ProjectForumService service; private Project project; - private User user; + private User author; + private User supervisor; @BeforeEach public void setUp() throws Exception { ProjectType projectType = new ProjectType(DegreeType.BACHELOR, "Some project type", "Some description"); projectType.addModule(ProjectModule.FORUM); save(projectType); - final User supervisor = save(User.builder().firstName("Bob").lastName("The Builder").emailAddress("bob@example.com").build()); + supervisor = save(User.builder().firstName("Bob").lastName("The Builder").emailAddress("bob@example.com").build()); project = Project.builder().title("Some title").projectType(projectType).startDate(LocalDate.now()).headSupervisor(supervisor).build(); save(project); - user = User.builder().firstName("Stina").lastName("Student").emailAddress("stina@example.com").build(); - save(user); + author = User.builder().firstName("Stina").lastName("Student").emailAddress("stina@example.com").build(); + save(author); } @Test public void testLoadThread() { - final ProjectThread thread = service.createThread(project, user, "subject", "content", Set.of()); + final ProjectThread thread = service.createThread(project, author, "subject", "content", Set.of()); ProjectThread serviceThread = service.findOne(thread.getId()); assertEquals(thread, serviceThread); @@ -55,17 +57,17 @@ public class ProjectForumServiceImplTest extends ForumModuleTest { public void testCreateThreadWithPostAttachment() throws Exception { final String file = "attachment.txt"; try (var is = ProjectForumServiceImplTest.class.getResourceAsStream(file)) { - final StreamingUpload upload = new StreamingUpload(file, "text/plain", user, 2, is); - final ProjectThread thread = service.createThread(project, user, SUBJECT, CONTENT, Set.of(Attachment.newUpload(upload))); + final StreamingUpload upload = new StreamingUpload(file, "text/plain", author, 2, is); + final ProjectThread thread = service.createThread(project, author, SUBJECT, CONTENT, Set.of(Attachment.newUpload(upload))); - assertNewForumThread(thread, project, user, SUBJECT, CONTENT, file); + assertNewForumThread(thread, project, author, SUBJECT, CONTENT, file); } } @Test public void testGetPostPageByForumThread() { - final ProjectThread thread = service.createThread(project, user, "subject", "content", Set.of()); - final ForumPost reply = service.createReply(thread, user, "reply", Set.of()); + final ProjectThread thread = service.createThread(project, author, "subject", "content", Set.of()); + final ForumPost reply = service.createReply(thread, author, "reply", Set.of()); List<ForumPost> servicePage = service.getPosts(thread); @@ -76,8 +78,8 @@ public class ProjectForumServiceImplTest extends ForumModuleTest { @Test public void testGetThreadsByProject() { - final ProjectThread thread1 = service.createThread(project, user, "subject 1", "content", Set.of()); - final ProjectThread thread2 = service.createThread(project, user, "subject 2", "content", Set.of()); + final ProjectThread thread1 = service.createThread(project, author, "subject 1", "content", Set.of()); + final ProjectThread thread2 = service.createThread(project, author, "subject 2", "content", Set.of()); List<ProjectThread> serviceThreads = service.getThreads(project); @@ -86,6 +88,16 @@ public class ProjectForumServiceImplTest extends ForumModuleTest { assertThat(serviceThreads, hasSize(2)); } + @Test + public void supervisor_has_unread_forum_threads_after_student_replies() { + final ProjectThread thread = service.createThread(project, supervisor, "subject", "content", Set.of()); + service.createReply(thread, author, "reply", Set.of()); + + boolean hasUnreadThreads = service.hasUnreadThreads(project, supervisor); + + assertTrue(hasUnreadThreads); + } + private void assertNewForumThread( ProjectThread thread, Project project, User user, String subject, String content, String attachmentFileName) { assertEquals(project, thread.getProject(), "Thread created for the wrong project");