From a166f6be4aab7ab6d8a54686ef21860c6fd6b7be Mon Sep 17 00:00:00 2001 From: uhensler <urs.hensler@frentix.com> Date: Wed, 20 May 2020 08:40:42 +0200 Subject: [PATCH] OO-4630: Rebook participants of an appointment --- .../nodes/appointments/Appointment.java | 4 +- .../nodes/appointments/AppointmentRef.java | 36 +++ .../appointments/AppointmentSearchParams.java | 6 +- .../appointments/AppointmentsService.java | 6 +- .../nodes/appointments/Participation.java | 4 +- .../nodes/appointments/ParticipationRef.java | 36 +++ .../appointments/ParticipationResult.java | 17 +- .../ParticipationSearchParams.java | 21 +- .../manager/AppointmentsMailing.java | 39 +++ .../manager/AppointmentsServiceImpl.java | 58 +++- .../manager/ParticipationDAO.java | 6 + .../model/ParticipationResultImpl.java | 12 +- .../ui/AbstractRebookController.java | 292 ++++++++++++++++++ .../appointments/ui/AppointmentDataModel.java | 2 + .../ui/AppointmentDeleteController.java | 104 +++++++ .../nodes/appointments/ui/AppointmentRow.java | 13 + .../appointments/ui/RebookController.java | 75 +++++ .../appointments/ui/TopicCoachController.java | 64 +++- .../appointments/ui/TopicsRunController.java | 6 +- .../ui/_content/appointment_row_coach.html | 3 + .../ui/_i18n/LocalStrings_de.properties | 18 +- .../ui/_i18n/LocalStrings_en.properties | 18 +- .../manager/AppointmentDAOTest.java | 2 +- .../manager/ParticipationDAOTest.java | 56 +++- 24 files changed, 834 insertions(+), 64 deletions(-) create mode 100644 src/main/java/org/olat/course/nodes/appointments/AppointmentRef.java create mode 100644 src/main/java/org/olat/course/nodes/appointments/ParticipationRef.java create mode 100644 src/main/java/org/olat/course/nodes/appointments/ui/AbstractRebookController.java create mode 100644 src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDeleteController.java create mode 100644 src/main/java/org/olat/course/nodes/appointments/ui/RebookController.java diff --git a/src/main/java/org/olat/course/nodes/appointments/Appointment.java b/src/main/java/org/olat/course/nodes/appointments/Appointment.java index f08814488ae..f878dfd7ce6 100644 --- a/src/main/java/org/olat/course/nodes/appointments/Appointment.java +++ b/src/main/java/org/olat/course/nodes/appointments/Appointment.java @@ -30,15 +30,13 @@ import org.olat.core.id.ModifiedInfo; * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com * */ -public interface Appointment extends ModifiedInfo, CreateInfo { +public interface Appointment extends AppointmentRef, ModifiedInfo, CreateInfo { public enum Status { planned, confirmed } - public Long getKey(); - public Status getStatus(); public Date getStatusModified(); diff --git a/src/main/java/org/olat/course/nodes/appointments/AppointmentRef.java b/src/main/java/org/olat/course/nodes/appointments/AppointmentRef.java new file mode 100644 index 00000000000..61aa8becbfc --- /dev/null +++ b/src/main/java/org/olat/course/nodes/appointments/AppointmentRef.java @@ -0,0 +1,36 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.appointments; + +/** + * + * Initial date: 19 May 2020<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public interface AppointmentRef { + + public Long getKey(); + + public static AppointmentRef of(Long key) { + return () -> key; + } + +} diff --git a/src/main/java/org/olat/course/nodes/appointments/AppointmentSearchParams.java b/src/main/java/org/olat/course/nodes/appointments/AppointmentSearchParams.java index 1bc6e5e8868..4e317da4579 100644 --- a/src/main/java/org/olat/course/nodes/appointments/AppointmentSearchParams.java +++ b/src/main/java/org/olat/course/nodes/appointments/AppointmentSearchParams.java @@ -43,10 +43,10 @@ public class AppointmentSearchParams { return appointmentKey; } - public void setAppointmentKey(Long appointmentKey) { - this.appointmentKey = appointmentKey; + public void setAppointment(AppointmentRef appointment) { + this.appointmentKey = appointment.getKey(); } - + public Topic getTopic() { return topic; } diff --git a/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java b/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java index 6614bee0034..5c11d3ad827 100644 --- a/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java +++ b/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java @@ -68,7 +68,11 @@ public interface AppointmentsService { public List<Appointment> getAppointments(AppointmentSearchParams params); - public ParticipationResult createParticipation(Appointment appointment, Identity identity, boolean autoConfirmation); + public ParticipationResult createParticipation(Appointment appointment, Identity identity, + boolean autoConfirmation); + + public ParticipationResult rebookParticipations(AppointmentRef toAppointmenRef, + Collection<? extends ParticipationRef> participationRefs, boolean autoConfirmation); public void deleteParticipation(Participation participation); diff --git a/src/main/java/org/olat/course/nodes/appointments/Participation.java b/src/main/java/org/olat/course/nodes/appointments/Participation.java index ecfa4b78a8b..71e5315f6ec 100644 --- a/src/main/java/org/olat/course/nodes/appointments/Participation.java +++ b/src/main/java/org/olat/course/nodes/appointments/Participation.java @@ -29,9 +29,7 @@ import org.olat.core.id.ModifiedInfo; * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com * */ -public interface Participation extends ModifiedInfo, CreateInfo { - - public Long getKey(); +public interface Participation extends ParticipationRef, ModifiedInfo, CreateInfo { public Appointment getAppointment(); diff --git a/src/main/java/org/olat/course/nodes/appointments/ParticipationRef.java b/src/main/java/org/olat/course/nodes/appointments/ParticipationRef.java new file mode 100644 index 00000000000..393d79a030a --- /dev/null +++ b/src/main/java/org/olat/course/nodes/appointments/ParticipationRef.java @@ -0,0 +1,36 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.appointments; + +/** + * + * Initial date: 19 May 2020<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public interface ParticipationRef { + + public Long getKey(); + + public static ParticipationRef of(Long key) { + return () -> key; + } + +} diff --git a/src/main/java/org/olat/course/nodes/appointments/ParticipationResult.java b/src/main/java/org/olat/course/nodes/appointments/ParticipationResult.java index e70fe55beea..0f52c907c62 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ParticipationResult.java +++ b/src/main/java/org/olat/course/nodes/appointments/ParticipationResult.java @@ -19,6 +19,9 @@ */ package org.olat.course.nodes.appointments; +import java.util.Collection; +import java.util.Collections; + import org.olat.course.nodes.appointments.model.ParticipationResultImpl; /** @@ -32,24 +35,30 @@ public interface ParticipationResult { static final ParticipationResult APPOINTMENT_DELETED = new ParticipationResultImpl(Status.appointmentDeleted); static final ParticipationResult APPOINTMENT_FULL = new ParticipationResultImpl(Status.appointmentFull); static final ParticipationResult APPOINTMENT_CONFIRMED = new ParticipationResultImpl(Status.appointmentConfirmed); + static final ParticipationResult NO_PARTICIPATIONS = new ParticipationResultImpl(Status.noParticipations); static ParticipationResult of(Participation participation) { - return new ParticipationResultImpl(Status.ok, participation); + return of(Collections.singletonList(participation)); + } + + static ParticipationResult of(Collection<Participation> participations) { + return new ParticipationResultImpl(Status.ok, participations); } enum Status { ok, appointmentDeleted, appointmentFull, - appointmentConfirmed + appointmentConfirmed, + noParticipations } public Status getStatus(); /** * - * @return the participation if status == ok. + * @return the participations if status == ok. */ - public Participation getParticipation(); + public Collection<Participation> getParticipations(); } diff --git a/src/main/java/org/olat/course/nodes/appointments/ParticipationSearchParams.java b/src/main/java/org/olat/course/nodes/appointments/ParticipationSearchParams.java index 74c6fc781c3..e7c8b8556bd 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ParticipationSearchParams.java +++ b/src/main/java/org/olat/course/nodes/appointments/ParticipationSearchParams.java @@ -39,6 +39,7 @@ public class ParticipationSearchParams { private String subIdent; private Identity identity; private Date createdAfter; + private Collection<Long> participationKeys; private Collection<Long> appointmentKeys; private Date startAfter; private Appointment.Status status; @@ -89,20 +90,30 @@ public class ParticipationSearchParams { this.createdAfter = createdAfter; } - public Collection<Long> getAppointmentKeys() { - return appointmentKeys; + public Collection<Long> getParticipationKeys() { + return participationKeys; } - public void setAppointments(Collection<Appointment> appointments) { - this.appointmentKeys = appointments.stream() - .map(Appointment::getKey) + public void setParticipations(Collection<? extends ParticipationRef> participations) { + this.participationKeys = participations.stream() + .map(ParticipationRef::getKey) .collect(Collectors.toSet()); } + public Collection<Long> getAppointmentKeys() { + return appointmentKeys; + } + public void setAppointmentKeys(Collection<Long> appointmentKeys) { this.appointmentKeys = appointmentKeys; } + public void setAppointments(Collection<? extends AppointmentRef> appointments) { + this.appointmentKeys = appointments.stream() + .map(AppointmentRef::getKey) + .collect(Collectors.toSet()); + } + public Date getStartAfter() { return startAfter; } diff --git a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsMailing.java b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsMailing.java index c0153c6cee9..e527f8bf9d1 100644 --- a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsMailing.java +++ b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsMailing.java @@ -161,6 +161,45 @@ public class AppointmentsMailing { } } + void sendRebook(Appointment toAppointment, List<Participation> fromParticipations) { + if (toAppointment == null || fromParticipations == null || fromParticipations.isEmpty()) return; + + ParticipationSearchParams participationParams = new ParticipationSearchParams(); + participationParams.setParticipations(fromParticipations); + participationParams.setFetchAppointments(true); + participationParams.setFetchTopics(true); + participationParams.setFetchIdentities(true); + participationParams.setFetchUser(true); + participationDao.loadParticipations(participationParams).stream() + .forEach(participation -> sendRebook(toAppointment, participation)); + } + + void sendRebook(Appointment toAppointment, Participation fromParticipation) { + Identity identity = fromParticipation.getIdentity(); + Locale locale = I18nManager.getInstance().getLocaleOrDefault(identity.getUser().getPreferences().getLanguage()); + Translator translator = Util.createPackageTranslator(AppointmentsRunController.class, locale); + + String subject = translator.translate("mail.rebooked.subject"); + String body = translator.translate("mail.rebooked.body", new String[] { + userManager.getUserDisplayName(identity.getKey()), + createFormatedAppointments(singletonList(fromParticipation.getAppointment()), translator), + createFormatedAppointments(singletonList(toAppointment), translator) + }); + + MailerResult result = new MailerResult(); + MailBundle bundle = new MailBundle(); + bundle.setToId(identity); + bundle.setContent(subject, body); + bundle.setContext(getMailContext(toAppointment.getTopic())); + + result = mailManager.sendMessage(bundle); + if (!result.isSuccessful()) { + log.warn(MessageFormat.format("Sending rebook appointment [from key={0}, to key={1}] to {2} failed: {3}", + fromParticipation.getAppointment().getKey(), toAppointment.getKey(), + fromParticipation.getIdentity(), result.getErrorMessage())); + } + } + private String createFormatedAppointments(List<Appointment> appointments, Translator translator) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < appointments.size(); i++) { diff --git a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsServiceImpl.java b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsServiceImpl.java index 0df5e249292..a00d5e65880 100644 --- a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsServiceImpl.java +++ b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentsServiceImpl.java @@ -21,6 +21,7 @@ package org.olat.course.nodes.appointments.manager; import static java.util.Collections.singletonList; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; @@ -36,10 +37,12 @@ import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.nodes.appointments.Appointment; import org.olat.course.nodes.appointments.Appointment.Status; +import org.olat.course.nodes.appointments.AppointmentRef; import org.olat.course.nodes.appointments.AppointmentSearchParams; import org.olat.course.nodes.appointments.AppointmentsService; import org.olat.course.nodes.appointments.Organizer; import org.olat.course.nodes.appointments.Participation; +import org.olat.course.nodes.appointments.ParticipationRef; import org.olat.course.nodes.appointments.ParticipationResult; import org.olat.course.nodes.appointments.ParticipationSearchParams; import org.olat.course.nodes.appointments.Topic; @@ -195,7 +198,7 @@ public class AppointmentsServiceImpl implements AppointmentsService { @Override public void confirmAppointment(Appointment appointment) { AppointmentSearchParams appointmentParams = new AppointmentSearchParams(); - appointmentParams.setAppointmentKey(appointment.getKey()); + appointmentParams.setAppointment(appointment); appointmentParams.setFetchTopic(true); List<Appointment> appointments = appointmentDao.loadAppointments(appointmentParams); if (!appointments.isEmpty()) { @@ -228,7 +231,7 @@ public class AppointmentsServiceImpl implements AppointmentsService { @Override public void unconfirmAppointment(Appointment appointment) { AppointmentSearchParams appointmentParams = new AppointmentSearchParams(); - appointmentParams.setAppointmentKey(appointment.getKey()); + appointmentParams.setAppointment(appointment); appointmentParams.setFetchTopic(true); List<Appointment> appointments = appointmentDao.loadAppointments(appointmentParams); if (!appointments.isEmpty()) { @@ -257,7 +260,7 @@ public class AppointmentsServiceImpl implements AppointmentsService { public ParticipationResult createParticipation(Appointment appointment, Identity identity, boolean autoConfirmation) { AppointmentSearchParams appointmentParams = new AppointmentSearchParams(); - appointmentParams.setAppointmentKey(appointment.getKey()); + appointmentParams.setAppointment(appointment); appointmentParams.setFetchTopic(true); List<Appointment> appointments = appointmentDao.loadAppointments(appointmentParams); if (appointments.isEmpty()) { @@ -289,6 +292,55 @@ public class AppointmentsServiceImpl implements AppointmentsService { return ParticipationResult.of(participation); } + @Override + public ParticipationResult rebookParticipations(AppointmentRef toAppointmentRef, + Collection<? extends ParticipationRef> participationRefs, boolean autoConfirmation) { + AppointmentSearchParams aParams = new AppointmentSearchParams(); + aParams.setAppointment(toAppointmentRef); + aParams.setFetchTopic(true); + List<Appointment> appointments = appointmentDao.loadAppointments(aParams); + if (appointments.isEmpty()) { + return ParticipationResult.APPOINTMENT_DELETED; + } + Appointment toAppointment = appointments.get(0); + + if (toAppointment.getMaxParticipations() != null) { + ParticipationSearchParams participationParams = new ParticipationSearchParams(); + participationParams.setAppointments(singletonList(toAppointment)); + Long count = participationDao.loadParticipationCount(participationParams); + if ((count.longValue() + participationRefs.size()) > toAppointment.getMaxParticipations().intValue()) { + return ParticipationResult.APPOINTMENT_FULL; + } + } + + ParticipationSearchParams pParams = new ParticipationSearchParams(); + pParams.setParticipations(participationRefs); + pParams.setFetchIdentities(true); + List<Participation> fromParticipations = participationDao.loadParticipations(pParams); + if (fromParticipations.isEmpty()) { + return ParticipationResult.NO_PARTICIPATIONS; + } + + List<Participation> participations = new ArrayList<>(fromParticipations.size()); + for (Participation fromParticipation : fromParticipations) { + Identity identity = fromParticipation.getIdentity(); + Participation participation = participationDao.createParticipation(toAppointment, identity); + participations.add(participation); + calendarSyncher.syncCalendar(toAppointment, identity); + } + markNews(toAppointment.getTopic()); + appointmentsMailing.sendRebook(toAppointment, fromParticipations); + + if (autoConfirmation) { + confirmReloadedAppointment(toAppointment, false); + } + + // Delete after send email to have the from participations informations in the email + fromParticipations.forEach(this::deleteParticipation); + + return ParticipationResult.of(participations); + } + @Override public void deleteParticipation(Participation participation) { calendarSyncher.unsyncCalendar(participation.getAppointment(), participation.getIdentity()); diff --git a/src/main/java/org/olat/course/nodes/appointments/manager/ParticipationDAO.java b/src/main/java/org/olat/course/nodes/appointments/manager/ParticipationDAO.java index 34379cd0c8b..9d4b83c8e30 100644 --- a/src/main/java/org/olat/course/nodes/appointments/manager/ParticipationDAO.java +++ b/src/main/java/org/olat/course/nodes/appointments/manager/ParticipationDAO.java @@ -191,6 +191,9 @@ class ParticipationDAO { if (params.getCreatedAfter() != null) { sb.and().append("participation.creationDate >= :createdAfter"); } + if (params.getParticipationKeys() != null && !params.getParticipationKeys().isEmpty()) { + sb.and().append("participation.key in (:participationKeys)"); + } if (params.getAppointmentKeys() != null && !params.getAppointmentKeys().isEmpty()) { sb.and().append("participation.appointment.key in (:appointmentKeys)"); } @@ -224,6 +227,9 @@ class ParticipationDAO { if (params.getCreatedAfter() != null) { query.setParameter("createdAfter", params.getCreatedAfter()); } + if (params.getParticipationKeys() != null && !params.getParticipationKeys().isEmpty()) { + query.setParameter("participationKeys", params.getParticipationKeys()); + } if (params.getAppointmentKeys() != null && !params.getAppointmentKeys().isEmpty()) { query.setParameter("appointmentKeys", params.getAppointmentKeys()); } diff --git a/src/main/java/org/olat/course/nodes/appointments/model/ParticipationResultImpl.java b/src/main/java/org/olat/course/nodes/appointments/model/ParticipationResultImpl.java index ad79504906f..1e351b5f40c 100644 --- a/src/main/java/org/olat/course/nodes/appointments/model/ParticipationResultImpl.java +++ b/src/main/java/org/olat/course/nodes/appointments/model/ParticipationResultImpl.java @@ -19,6 +19,8 @@ */ package org.olat.course.nodes.appointments.model; +import java.util.Collection; + import org.olat.course.nodes.appointments.Participation; import org.olat.course.nodes.appointments.ParticipationResult; @@ -31,15 +33,15 @@ import org.olat.course.nodes.appointments.ParticipationResult; public class ParticipationResultImpl implements ParticipationResult { private final Status status; - private final Participation participation; + private final Collection<Participation> participations; public ParticipationResultImpl(Status status) { this(status, null); } - public ParticipationResultImpl(Status status, Participation participation) { + public ParticipationResultImpl(Status status, Collection<Participation> participations) { this.status = status; - this.participation = participation; + this.participations = participations; } @Override @@ -48,8 +50,8 @@ public class ParticipationResultImpl implements ParticipationResult { } @Override - public Participation getParticipation() { - return participation; + public Collection<Participation> getParticipations() { + return participations; } } diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AbstractRebookController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AbstractRebookController.java new file mode 100644 index 00000000000..4253f773606 --- /dev/null +++ b/src/main/java/org/olat/course/nodes/appointments/ui/AbstractRebookController.java @@ -0,0 +1,292 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.appointments.ui; + +import static java.util.Collections.emptyList; +import static org.olat.core.gui.components.util.KeyValues.entry; + +import java.text.DateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; +import org.olat.core.gui.components.form.flexible.elements.SingleSelection; +import org.olat.core.gui.components.form.flexible.elements.StaticTextElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.components.util.KeyValues; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; +import org.olat.core.gui.control.WindowControl; +import org.olat.course.nodes.appointments.Appointment; +import org.olat.course.nodes.appointments.AppointmentRef; +import org.olat.course.nodes.appointments.AppointmentSearchParams; +import org.olat.course.nodes.appointments.AppointmentsService; +import org.olat.course.nodes.appointments.Participation; +import org.olat.course.nodes.appointments.ParticipationRef; +import org.olat.course.nodes.appointments.ParticipationSearchParams; +import org.olat.user.UserManager; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 18 May 2020<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public abstract class AbstractRebookController extends FormBasicController { + + private static final String[] EMPTY = new String[0]; + + private MultipleSelectionElement participationsEl; + private SingleSelection appointmentsEl; + private StaticTextElement noAppointmentsEl; + + private final DateFormat dateFormat; + private final Appointment currentAppointment; + private final List<Participation> participations; + private final Configuration config; + private String selectedAppointmentKey; + + @Autowired + private AppointmentsService appointmentsService; + @Autowired + private UserManager userManager; + + public AbstractRebookController(UserRequest ureq, WindowControl wControl, Appointment appointment, + Configuration config) { + super(ureq, wControl); + this.currentAppointment = appointment; + this.config = config; + + dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()); + + ParticipationSearchParams params = new ParticipationSearchParams(); + params.setAppointments(Collections.singletonList(appointment)); + participations = appointmentsService.getParticipations(params); + + initForm(ureq); + } + + abstract boolean isShowParticipations(); + + abstract boolean isParticipationsReadOnly(); + + abstract boolean isAllParticipationsSelected(); + + abstract boolean isShowAppointments(); + + abstract String getSubmitI18nKey(); + + abstract void initFormTop(FormItemContainer formLayout, Controller listener, UserRequest ureq); + + abstract void onAfterRebooking(); + + AppointmentsService getAppointmentsService() { + return appointmentsService; + } + + Appointment getCurrentAppointment() { + return currentAppointment; + } + + int getNumParticipations() { + return participations.size(); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + initFormTop(formLayout, listener, ureq); + + participationsEl = uifactory.addCheckboxesVertical("rebook.participation", formLayout, EMPTY, EMPTY, 2); + participationsEl.addActionListener(FormEvent.ONCHANGE); + + appointmentsEl = uifactory.addRadiosVertical("rebook.appointments", "rebook.appointments", formLayout, EMPTY, EMPTY); + + noAppointmentsEl = uifactory.addStaticTextElement("rebook.no.appointments", + translate("rebook.no.appointments.text"), formLayout); + + FormLayoutContainer buttonCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonCont.setRootForm(mainForm); + formLayout.add(buttonCont); + uifactory.addFormSubmitButton(getSubmitI18nKey(), buttonCont); + uifactory.addFormCancelButton("cancel", buttonCont, ureq, getWindowControl()); + + updateUI(); + } + + void updateUI(){ + if (participations.isEmpty()) { + participationsEl.setVisible(false); + appointmentsEl.setVisible(false); + noAppointmentsEl.setVisible(false); + return; + } + + if (isShowParticipations()) { + KeyValues participationsKV = new KeyValues(); + + ParticipationSearchParams params = new ParticipationSearchParams(); + params.setAppointments(Collections.singletonList(currentAppointment)); + appointmentsService.getParticipations(params) + .forEach(participation -> participationsKV.add(entry( + participation.getKey().toString(), + userManager.getUserDisplayName(participation.getIdentity().getKey())))); + + participationsKV.sort(KeyValues.VALUE_ASC); + participationsEl.setKeysAndValues(participationsKV.keys(), participationsKV.values()); + if (isAllParticipationsSelected()) { + participationsEl.selectAll(); + } + + participationsEl.setVisible(true); + } else { + participationsEl.setVisible(false); + } + participationsEl.setEnabled(!isParticipationsReadOnly()); + + updateAppointmentsUI(); + } + + private void updateAppointmentsUI() { + if (isShowAppointments()) { + selectedAppointmentKey = appointmentsEl.isOneSelected() + ? appointmentsEl.getSelectedKey() + : selectedAppointmentKey; + int numParticipants = participationsEl.isAtLeastSelected(1) + ? participationsEl.getSelectedKeys().size() + : 1; + + ParticipationSearchParams pParams = new ParticipationSearchParams(); + pParams.setTopic(currentAppointment.getTopic()); + List<Participation> participations = appointmentsService.getParticipations(pParams); + Map<Long, List<Participation>> appointmentKeyToParticipations = participations.stream() + .collect(Collectors.groupingBy(p -> p.getAppointment().getKey())); + + KeyValues freeKV = new KeyValues(); + AppointmentSearchParams aParams = new AppointmentSearchParams(); + aParams.setTopic(currentAppointment.getTopic()); + appointmentsService.getAppointments(aParams).stream() + .filter(appointment -> hasFreeParticipations(appointment, appointmentKeyToParticipations, numParticipants)) + .sorted((a1, a2) -> a1.getStart().compareTo(a2.getStart())) + .forEach(appointment -> freeKV.add(KeyValues.entry(appointment.getKey().toString(), formatDate(appointment)))); + + if (freeKV.size() > 0) { + appointmentsEl.setKeysAndValues(freeKV.keys(), freeKV.values(), null); + if (Arrays.asList(appointmentsEl.getKeys()).contains(selectedAppointmentKey)) { + appointmentsEl.select(selectedAppointmentKey, true); + } + + appointmentsEl.setVisible(true); + noAppointmentsEl.setVisible(false); + } else { + appointmentsEl.setVisible(false); + noAppointmentsEl.setVisible(true); + } + } else { + appointmentsEl.setVisible(false); + noAppointmentsEl.setVisible(false); + } + } + + private boolean hasFreeParticipations(Appointment appointment, + Map<Long, List<Participation>> appointmentKeyToParticipation, int numParticipants) { + if (appointment.equals(currentAppointment)) return false; + if (appointment.getMaxParticipations() == null) return true; + + List<Participation> participations = appointmentKeyToParticipation.getOrDefault(appointment.getKey(), emptyList()); + return appointment.getMaxParticipations().intValue() >= (participations.size() + numParticipants); + } + + private String formatDate(Appointment appointment) { + return new StringBuilder() + .append(dateFormat.format(appointment.getStart())) + .append(" - ") + .append(dateFormat.format(appointment.getEnd())) + .toString(); + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if (source == participationsEl) { + updateAppointmentsUI(); + } + super.formInnerEvent(ureq, source, event); + } + + @Override + protected boolean validateFormLogic(UserRequest ureq) { + boolean allOk = super.validateFormLogic(ureq); + + participationsEl.clearError(); + if (participationsEl.isVisible()) { + if (!participationsEl.isAtLeastSelected(1)) { + participationsEl.setErrorKey("error.select.participant", null); + allOk &= false; + } + } + + appointmentsEl.clearError(); + if (appointmentsEl.isVisible()) { + if (!appointmentsEl.isOneSelected()) { + appointmentsEl.setErrorKey("error.select.appointment", null); + allOk &= false; + } + } + + return allOk; + } + + @Override + protected void formCancelled(UserRequest ureq) { + fireEvent(ureq, Event.CANCELLED_EVENT); + } + + @Override + protected void formOK(UserRequest ureq) { + if (appointmentsEl.isVisible()) { + Collection<ParticipationRef> participationRefs = participationsEl.getSelectedKeys().stream() + .map(Long::valueOf) + .map(ParticipationRef::of) + .collect(Collectors.toList()); + Long appointmentKey = Long.valueOf(appointmentsEl.getSelectedKey()); + appointmentsService.rebookParticipations(AppointmentRef.of(appointmentKey), participationRefs, + !config.isConfirmation()); + } + + onAfterRebooking(); + + fireEvent(ureq, Event.DONE_EVENT); + } + + @Override + protected void doDispose() { + // + } + +} diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java index c26c392b984..df54523e01c 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java +++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java @@ -69,6 +69,7 @@ implements SortableFlexiTableDataModel<AppointmentRow> { case maxParticipations: return row.getAppointment().getMaxParticipations(); case freeParticipations: return row.getFreeParticipations(); case participants: return row.getParticipantsWrapper(); + case rebook: return row.getRebookLink(); case confirm: return row.getConfirmLink(); case delete: return row.getDeleteLink(); case edit: return row.getEditLink(); @@ -91,6 +92,7 @@ implements SortableFlexiTableDataModel<AppointmentRow> { maxParticipations("appointment.max.participations"), freeParticipations("appointment.free.participations"), participants("participants"), + rebook("rebook"), confirm("confirm"), edit("edit"), delete("delete"); diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDeleteController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDeleteController.java new file mode 100644 index 00000000000..1d53125fa6f --- /dev/null +++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDeleteController.java @@ -0,0 +1,104 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.appointments.ui; + +import static org.olat.core.gui.translator.TranslatorHelper.translateAll; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.SingleSelection; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.course.nodes.appointments.Appointment; + +/** + * + * Initial date: 18 May 2020<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public class AppointmentDeleteController extends AbstractRebookController { + + private static final String DELETE = "appointment.delete.no"; + private static final String CHANGE = "appointment.delete.yes"; + private static final String[] PARTICIPATIONS = { DELETE, CHANGE }; + + private SingleSelection changeEl; + + public AppointmentDeleteController(UserRequest ureq, WindowControl wControl, Appointment appointment, + Configuration config) { + super(ureq, wControl, appointment, config); + } + + @Override + boolean isShowParticipations() { + return getNumParticipations() > 0; + } + + @Override + boolean isParticipationsReadOnly() { + return true; + } + + @Override + boolean isAllParticipationsSelected() { + return true; + } + + @Override + boolean isShowAppointments() { + return changeEl != null && changeEl.isOneSelected() && CHANGE.equals(changeEl.getSelectedKey()); + } + + @Override + String getSubmitI18nKey() { + return "delete"; + } + + @Override + protected void initFormTop(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + if (getNumParticipations() > 0) { + setFormDescription("appointment.delete.participations", new String[] { String.valueOf(getNumParticipations()) } ); + + changeEl = uifactory.addRadiosHorizontal("appointment.delete.rebook", formLayout, PARTICIPATIONS, + translateAll(getTranslator(), PARTICIPATIONS)); + changeEl.addActionListener(FormEvent.ONCHANGE); + changeEl.select(DELETE, true); + } else { + setFormDescription("confirm.appointment.delete"); + } + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if (source == changeEl) { + super.updateUI(); + } + super.formInnerEvent(ureq, source, event); + } + + @Override + void onAfterRebooking() { + getAppointmentsService().deleteAppointment(getCurrentAppointment()); + } + +} diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentRow.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentRow.java index 05d1eea901a..6720bd30ed3 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentRow.java +++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentRow.java @@ -46,6 +46,7 @@ public class AppointmentRow { private String statusCSS; private Integer freeParticipations; private Integer maxParticipations; + private FormLink rebookLink; private FormLink confirmLink; private FormLink deleteLink; private FormLink editLink; @@ -162,6 +163,14 @@ public class AppointmentRow { this.maxParticipations = maxParticipations; } + public FormLink getRebookLink() { + return rebookLink; + } + + public String getRebookLinkName() { + return rebookLink != null? rebookLink.getName(): null; + } + public FormLink getConfirmLink() { return confirmLink; } @@ -174,6 +183,10 @@ public class AppointmentRow { this.confirmLink = confirmLink; } + public void setRebookLink(FormLink rebookLink) { + this.rebookLink = rebookLink; + } + public FormLink getDeleteLink() { return deleteLink; } diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/RebookController.java b/src/main/java/org/olat/course/nodes/appointments/ui/RebookController.java new file mode 100644 index 00000000000..8d6f0dd21eb --- /dev/null +++ b/src/main/java/org/olat/course/nodes/appointments/ui/RebookController.java @@ -0,0 +1,75 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.nodes.appointments.ui; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.course.nodes.appointments.Appointment; + +/** + * + * Initial date: 19 May 2020<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public class RebookController extends AbstractRebookController { + + public RebookController(UserRequest ureq, WindowControl wControl, Appointment appointment, Configuration config) { + super(ureq, wControl, appointment, config); + } + + @Override + boolean isShowParticipations() { + return true; + } + + @Override + boolean isParticipationsReadOnly() { + return false; + } + + @Override + boolean isShowAppointments() { + return true; + } + + @Override + boolean isAllParticipationsSelected() { + return false; + } + + @Override + String getSubmitI18nKey() { + return "rebook"; + } + + @Override + void initFormTop(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + // + } + + @Override + void onAfterRebooking() { + // + } + +} diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCoachController.java b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCoachController.java index cf9e590dacf..d1e4c3f928d 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCoachController.java +++ b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCoachController.java @@ -73,6 +73,7 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class TopicCoachController extends FormBasicController implements FlexiTableComponentDelegate { + private static final String CMD_REBOOK = "rebook"; private static final String CMD_CONFIRM = "confirm"; private static final String CMD_DELETE = "delete"; private static final String CMD_EDIT = "edit"; @@ -88,8 +89,9 @@ public class TopicCoachController extends FormBasicController implements FlexiTa private TopicEditController topicEditCtrl; private OrganizersEditController organizersEditCtrl; private AppointmentEditController appointmentEditCtrl; + private RebookController rebookCtrl; private DialogBoxController confirmDeleteTopicCrtl; - private DialogBoxController confirmDeletionCrtl; + private AppointmentDeleteController appointmentDeleteCtrl; private Topic topic; private final AppointmentsSecurityCallback secCallback; @@ -153,6 +155,9 @@ public class TopicCoachController extends FormBasicController implements FlexiTa participantsModel.setCellRenderer(new ParticipationsRenderer()); participantsModel.setDefaultVisible(false); columnsModel.addFlexiColumnModel(participantsModel); + DefaultFlexiColumnModel rebookModel = new DefaultFlexiColumnModel(AppointmentCols.rebook); + rebookModel.setExportable(false); + columnsModel.addFlexiColumnModel(rebookModel); DefaultFlexiColumnModel confirmModel = new DefaultFlexiColumnModel(AppointmentCols.confirm); confirmModel.setExportable(false); columnsModel.addFlexiColumnModel(confirmModel); @@ -276,6 +281,9 @@ public class TopicCoachController extends FormBasicController implements FlexiTa row.setTranslatedStatus(translate("appointment.status." + appointment.getStatus().name())); row.setStatusCSS("o_ap_status_" + appointment.getStatus().name()); + if (participations.size() > 0) { + forgeRebookLink(row); + } if (config.isConfirmation()) { boolean confirmable = Appointment.Status.planned == appointment.getStatus() && participations.size() > 0; @@ -290,6 +298,12 @@ public class TopicCoachController extends FormBasicController implements FlexiTa return row; } + private void forgeRebookLink(AppointmentRow row) { + FormLink link = uifactory.addFormLink("rebook_" + row.getKey(), CMD_REBOOK, "rebook", null, null, Link.LINK); + link.setUserObject(row); + row.setRebookLink(link); + } + private void forgeConfirmLink(AppointmentRow row, boolean confirmable) { String i18nKey = confirmable? "confirm": "unconfirm"; FormLink link = uifactory.addFormLink("confirm_" + row.getKey(), CMD_CONFIRM, i18nKey, null, null, Link.LINK); @@ -331,7 +345,10 @@ public class TopicCoachController extends FormBasicController implements FlexiTa } else if (CMD_CONFIRM.equals(cmd)) { AppointmentRow row = (AppointmentRow)link.getUserObject(); doConfirm(row.getAppointment()); - } + } else if (CMD_REBOOK.equals(cmd)) { + AppointmentRow row = (AppointmentRow)link.getUserObject(); + doRebook(ureq, row.getAppointment()); + } } super.formInnerEvent(ureq, source, event); } @@ -357,15 +374,22 @@ public class TopicCoachController extends FormBasicController implements FlexiTa } cmc.deactivate(); cleanUp(); + } else if (appointmentDeleteCtrl == source) { + if (event == Event.DONE_EVENT) { + updateModel(); + } + cmc.deactivate(); + cleanUp(); + } else if (rebookCtrl == source) { + if (event == Event.DONE_EVENT) { + updateModel(); + } + cmc.deactivate(); + cleanUp(); } else if (source == confirmDeleteTopicCrtl) { if (DialogBoxUIFactory.isYesEvent(event) || DialogBoxUIFactory.isOkEvent(event)) { doDeleteTopic(ureq); } - } else if (source == confirmDeletionCrtl) { - if (DialogBoxUIFactory.isYesEvent(event) || DialogBoxUIFactory.isOkEvent(event)) { - Appointment appointment = (Appointment)confirmDeletionCrtl.getUserObject(); - doDelete(appointment); - } } else if (cmc == source) { cleanUp(); } @@ -373,9 +397,13 @@ public class TopicCoachController extends FormBasicController implements FlexiTa } private void cleanUp() { + removeAsListenerAndDispose(appointmentDeleteCtrl); removeAsListenerAndDispose(appointmentEditCtrl); + removeAsListenerAndDispose(rebookCtrl); removeAsListenerAndDispose(cmc); + appointmentDeleteCtrl = null; appointmentEditCtrl = null; + rebookCtrl = null; cmc = null; } @@ -436,10 +464,13 @@ public class TopicCoachController extends FormBasicController implements FlexiTa } private void doConfirmDeletion(UserRequest ureq, Appointment appointment) { - String title = translate("confirm.delete.title"); - String text = translate("confirm.delete"); - confirmDeletionCrtl = activateYesNoDialog(ureq, title, text, confirmDeletionCrtl); - confirmDeletionCrtl.setUserObject(appointment); + appointmentDeleteCtrl = new AppointmentDeleteController(ureq, getWindowControl(), appointment, config); + listenTo(appointmentDeleteCtrl); + + cmc = new CloseableModalController(getWindowControl(), "close", appointmentDeleteCtrl.getInitialComponent(), + true, translate("confirm.appointment.delete.title")); + listenTo(cmc); + cmc.activate(); } private void doConfirm(Appointment appointment) { @@ -450,10 +481,15 @@ public class TopicCoachController extends FormBasicController implements FlexiTa } updateModel(); } + + private void doRebook(UserRequest ureq, Appointment appointment) { + rebookCtrl = new RebookController(ureq, getWindowControl(), appointment, config); + listenTo(rebookCtrl); - private void doDelete(Appointment appointment) { - appointmentsService.deleteAppointment(appointment); - updateModel(); + cmc = new CloseableModalController(getWindowControl(), "close", rebookCtrl.getInitialComponent(), + true, translate("rebook.title")); + listenTo(cmc); + cmc.activate(); } @Override diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/TopicsRunController.java b/src/main/java/org/olat/course/nodes/appointments/ui/TopicsRunController.java index 34a2b1f5251..365513ca424 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/TopicsRunController.java +++ b/src/main/java/org/olat/course/nodes/appointments/ui/TopicsRunController.java @@ -137,10 +137,10 @@ public class TopicsRunController extends BasicController { List<Participation> myParticipations = appointmentsService.getParticipations(myParticipationsParams); Map<Long, List<Participation>> topicKeyToMyParticipation = myParticipations.stream() .collect(Collectors.groupingBy(p -> p.getAppointment().getTopic().getKey())); - Map<Long, List<Participation>> appointmentKeyToMyParticipation = myParticipations.stream() + Map<Long, List<Participation>> appointmentsToMyParticipation = myParticipations.stream() .collect(Collectors.groupingBy(p -> p.getAppointment().getKey())); - Set<Long> myAppointmentKeys = appointmentKeyToMyParticipation.keySet(); + Set<Long> myAppointmentKeys = appointmentsToMyParticipation.keySet(); ParticipationSearchParams allParticipationParams = new ParticipationSearchParams(); allParticipationParams.setAppointmentKeys(myAppointmentKeys); Map<Long, List<Participation>> appointmentKeyToAllParticipations = appointmentsService @@ -154,7 +154,7 @@ public class TopicsRunController extends BasicController { wrapOrganizers(wrapper, organizers); List<Appointment> appointments = topicKeyToAppointments.getOrDefault(topic.getKey(), emptyList()); List<Participation> topicParticipations = topicKeyToMyParticipation.getOrDefault(topic.getKey(), emptyList()); - wrapParticpations(wrapper, topic, topicParticipations, appointments, appointmentKeyToMyParticipation, + wrapParticpations(wrapper, topic, topicParticipations, appointments, appointmentsToMyParticipation, appointmentKeyToAllParticipations); wrappers.add(wrapper); } diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointment_row_coach.html b/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointment_row_coach.html index 6f109a0d5fc..d33dd3baf38 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointment_row_coach.html +++ b/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointment_row_coach.html @@ -65,6 +65,9 @@ <div class="o_buttons"> <div class="o_button_group o_button_group_right"> + #if($row.rebookLinkName) + $r.render($row.rebookLinkName) + #end #if($row.confirmLinkName) $r.render($row.confirmLinkName) #end diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties index fb2bd214a26..3cd0abf2958 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties @@ -5,6 +5,10 @@ add.topic=Termine hinzuf\u00FCgen add.topic.title=$:\add.topic all.appointments.allocated=Es sind alle Termine belegt. appointment.details=Details +appointment.delete.rebook=Teilnehmer umbuchen? +appointment.delete.no=Nein +appointment.delete.participations=Es haben sich {0} Teilnehmer in diesen Termin eingeschrieben. Sollen diese Teilnehmer umgebucht werden? +appointment.delete.yes=Ja appointment.end=Ende appointment.id=ID appointment.location=Ort @@ -32,8 +36,8 @@ config.confirmation=Terminbest\u00E4tigung config.edit.appointment=Termine bearbeiten config.edit.topic=Thema bearbeiten confirm=Best\u00E4tigen -confirm.delete=Wollen Sie diesen Termin wirklich absagen? -confirm.delete.title=Termin absagen +confirm.appointment.delete=Wollen Sie diesen Termin wirklich absagen? +confirm.appointment.delete.title=Termin absagen confirm.participation.self=Wollen Sie sich wirklich f\u00FCr diesen Termin eintragen? Es kann anschliessend kein anderer Termin mehr gew\u00E4hlt werden. confirm.participation.self.title=Termin ausw\u00E4hlen confirm.topic.delete=Wollen Sie wirklich alle Termine absagen? @@ -49,6 +53,8 @@ email.title=Neue Nachricht email.organizer.recipients=Organisatoren email.organizer.subject=Termin "{0}" error.positiv.number=Geben Sie eine positive Ganzzahl ein. +error.select.appointment=Sie m\u00FCssen einen Termin ausw\u00E4hlen. +error.select.participant=Sie m\u00FCssen einen Teilnehmer ausw\u00E4hlen. error.start.after.end=Das Ende darf nicht vor dem Start liegen. error.too.much.participations=Es gibt bereits {0} Teilnahmen. general=Konfiguration @@ -60,6 +66,8 @@ mail.deleted.body=Liebe/r {0} <br><br>Folgender Termin wurde abgesagt.<br><br>{1 mail.deleted.subject=Termin "{0}" abgesagt mail.end=Ende: {0} mail.location=Ort: {0} +mail.rebooked.subject=Termin umgebucht +mail.rebooked.body=Liebe/r {0} <br><br>Folgender Termin wurde umbgebucht.<br><br><b>Bisheriger Termin</b><br>{1}<br><b>Neuer Termin</b><br>{2} mail.start=Start: {0} mail.unconfirmed.body=Liebe/r {0} <br><br>Folgender Termin wurde wieder ge\u00F6ffnet.<br><br>{1} mail.unconfirmed.subject=Termin "{0}" wieder ge\u00F6ffnet @@ -76,6 +84,12 @@ participants=Teilnehmer participations.free=Es sind noch {0} Pl\u00E4tze frei. participations.free.one=Es ist noch ein Platz frei. participations.selected=Es sind {0} Teilnehmer in {1} Terminen eingetragen. +rebook=Umbuchen +rebook.appointments=Termin +rebook.participation=Teilnehmer +rebook.no.appointments=$\:rebook.appointments +rebook.no.appointments.text=<i>Es gibt keine Termine mit gen\u00FCgend freien Pl\u00E4tzen.</i> +rebook.title=Teilnehmer umbuchen role.coach=Betreuer save.back=Speichern und zur\u00FCck table.empty.appointments=Es sind keine Termine vorhanden. diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties index 4facccd26ce..087b544bed8 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties @@ -5,6 +5,10 @@ add.topic=Add appointments add.topic.title=$:\add.topic all.appointments.allocated=All appointments are allocated. appointment.details=Details +appointment.delete.rebook=Rebook appointment? +appointment.delete.no=No +appointment.delete.participations={0} participants are enrolled to this appointment. Should these participants be rebooked? +appointment.delete.yes=Yes appointment.end=End appointment.id=ID appointment.location=Location @@ -32,8 +36,8 @@ config.confirmation=Confirmation of the appointment config.edit.appointment=Edit appointments config.edit.topic=Edit topic confirm=Confirm -confirm.delete=Do you really want to decline that appointment? -confirm.delete.title=Decline appointment +confirm.appointment.delete=Do you really want to decline this appointment? +confirm.appointment.delete.title=Decline appointment confirm.participation.self=Do you really want to enroll for this appointment? No other appointment can be chosen afterwards. confirm.participation.self.title=Select appointment confirm.topic.delete=Do you really want to decline all appointments? @@ -49,6 +53,8 @@ email.title=New message email.organizer.recipients=Organizers email.organizer.subject=Appointment "{0}" error.positiv.number=It has to be a positive integer. +error.select.appointment=You have to select an appointment. +error.select.participant=You have to select a participant. error.start.after.end=The end date must not be before the start date. error.too.much.participations=There are already {0} participations. general=Configuration @@ -60,6 +66,8 @@ mail.deleted.body=Dear {0} <br><br>The following appointment was declined.<br><b mail.deleted.subject=Appointment "{0}" declined mail.end=End: {0} mail.location=Location: {0} +mail.rebooked.subject=Appointment rebooked +mail.rebooked.body=Dear {0} <br><br>The following appointment was rebooked.<br><br><b>Previous appointment</b><br>{1}<br><b>New appointment</b><br>{2} mail.start=Start: {0} mail.unconfirmed.body=Dear {0} <br><br>The following appointment was reopened.<br><br>{1} mail.unconfirmed.subject=Appointment "{0}" reopened @@ -76,6 +84,12 @@ participants=Participants participations.free=There are {0} places left. participations.free.one=There is one place left. participations.selected={0} participants in {1} appointments are enrolled. +rebook=Rebook +rebook.appointments=Appointment +rebook.participation=Participants +rebook.no.appointments=$\:rebook.appointments +rebook.no.appointments.text=<i>There are no appointments with enough free places.</i> +rebook.title=Rebook participants role.coach=Coach save.back=Save and back table.empty.appointments=There are no appointments available. diff --git a/src/test/java/org/olat/course/nodes/appointments/manager/AppointmentDAOTest.java b/src/test/java/org/olat/course/nodes/appointments/manager/AppointmentDAOTest.java index 05007ce09aa..eb034200fa2 100644 --- a/src/test/java/org/olat/course/nodes/appointments/manager/AppointmentDAOTest.java +++ b/src/test/java/org/olat/course/nodes/appointments/manager/AppointmentDAOTest.java @@ -247,7 +247,7 @@ public class AppointmentDAOTest extends OlatTestCase { dbInstance.commitAndCloseSession(); AppointmentSearchParams params = new AppointmentSearchParams(); - params.setAppointmentKey(appointment.getKey()); + params.setAppointment(appointment); List<Appointment> appointments = sut.loadAppointments(params); assertThat(appointments).containsExactlyInAnyOrder(appointment); diff --git a/src/test/java/org/olat/course/nodes/appointments/manager/ParticipationDAOTest.java b/src/test/java/org/olat/course/nodes/appointments/manager/ParticipationDAOTest.java index 68337f98899..532ae0a2c33 100644 --- a/src/test/java/org/olat/course/nodes/appointments/manager/ParticipationDAOTest.java +++ b/src/test/java/org/olat/course/nodes/appointments/manager/ParticipationDAOTest.java @@ -35,6 +35,7 @@ import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.course.nodes.appointments.Appointment; import org.olat.course.nodes.appointments.Appointment.Status; +import org.olat.course.nodes.appointments.AppointmentRef; import org.olat.course.nodes.appointments.Participation; import org.olat.course.nodes.appointments.ParticipationSearchParams; import org.olat.course.nodes.appointments.Topic; @@ -213,10 +214,10 @@ public class ParticipationDAOTest extends OlatTestCase { sut.createParticipation(appointmentD1 , identity1); dbInstance.commitAndCloseSession(); - Collection<Long> appointmentKeys = Arrays.asList(appointmentA1.getKey(), appointmentA2.getKey(), - appointmentB1.getKey(), appointmentD1.getKey()); + Collection<AppointmentRef> appointments = Arrays.asList(appointmentA1, appointmentA2, appointmentB1, + appointmentD1); ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(appointmentKeys); + params.setAppointments(appointments); params.setStatus(Status.confirmed); Long count = sut.loadParticipationCount(params); @@ -303,13 +304,39 @@ public class ParticipationDAOTest extends OlatTestCase { //Is just a syntax check. The appointment key is the limiting clause. ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(asList(appointment.getKey())); + params.setAppointments(asList(appointment)); params.setCreatedAfter(new GregorianCalendar(2000, 1, 1).getTime()); List<Participation> participations = sut.loadParticipations(params); assertThat(participations).containsExactlyInAnyOrder(participation); } + @Test + public void shouldLoadKeys() { + RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry(); + Identity identity1 = JunitTestHelper.createAndPersistIdentityAsUser(random()); + Identity identity2 = JunitTestHelper.createAndPersistIdentityAsUser(random()); + Topic topic1 = topicDao.createTopic(entry, JunitTestHelper.random()); + Topic topic2 = topicDao.createTopic(entry, JunitTestHelper.random()); + Appointment appointment11 = appointmentDao.createAppointment(topic1); + Appointment appointment12 = appointmentDao.createAppointment(topic1); + Appointment appointment21 = appointmentDao.createAppointment(topic2); + Participation participation111 = sut.createParticipation(appointment11, identity1); + Participation participation121 = sut.createParticipation(appointment12, identity1); + Participation participation112 = sut.createParticipation(appointment11, identity2); + Participation participation211 = sut.createParticipation(appointment21, identity1); + dbInstance.commitAndCloseSession(); + + ParticipationSearchParams params = new ParticipationSearchParams(); + Collection<Participation> asList = Arrays.asList(participation112, participation211); + params.setParticipations(asList); + List<Participation> participations = sut.loadParticipations(params); + + assertThat(participations) + .containsExactlyInAnyOrder(participation112, participation211) + .doesNotContain(participation111, participation121); + } + @Test public void shouldLoadByAppointments() { RepositoryEntry entry1 = JunitTestHelper.createAndPersistRepositoryEntry(); @@ -337,9 +364,9 @@ public class ParticipationDAOTest extends OlatTestCase { Participation participationD11 = sut.createParticipation(appointmentD1 , identity1); dbInstance.commitAndCloseSession(); - Collection<Long> appointmentKeys = Arrays.asList(appointmentA1.getKey(), appointmentA2.getKey(), appointmentC1.getKey()); + Collection<Appointment> appointments = Arrays.asList(appointmentA1, appointmentA2, appointmentC1); ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(appointmentKeys); + params.setAppointments(appointments); List<Participation> participations = sut.loadParticipations(params); assertThat(participations) @@ -386,10 +413,10 @@ public class ParticipationDAOTest extends OlatTestCase { Participation participationD11 = sut.createParticipation(appointmentD1 , identity1); dbInstance.commitAndCloseSession(); - Collection<Long> appointmentKeys = Arrays.asList(appointmentA1.getKey(), appointmentA2.getKey(), - appointmentB1.getKey(), appointmentD1.getKey()); + Collection<Appointment> appointments = Arrays.asList(appointmentA1, appointmentA2, appointmentB1, + appointmentD1); ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(appointmentKeys); + params.setAppointments(appointments); params.setStartAfter(new GregorianCalendar(2020, 1, 1).getTime()); List<Participation> participations = sut.loadParticipations(params); @@ -432,10 +459,10 @@ public class ParticipationDAOTest extends OlatTestCase { Participation participationD11 = sut.createParticipation(appointmentD1 , identity1); dbInstance.commitAndCloseSession(); - Collection<Long> appointmentKeys = Arrays.asList(appointmentA1.getKey(), appointmentA2.getKey(), - appointmentB1.getKey(), appointmentD1.getKey()); + Collection<Appointment> appointments = Arrays.asList(appointmentA1, appointmentA2, appointmentB1, + appointmentD1); ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(appointmentKeys); + params.setAppointments(appointments); params.setStatus(Status.confirmed); List<Participation> participations = sut.loadParticipations(params); @@ -474,10 +501,9 @@ public class ParticipationDAOTest extends OlatTestCase { Participation participationD11 = sut.createParticipation(appointmentD1 , identity); dbInstance.commitAndCloseSession(); - Collection<Long> appointmentKeys = Arrays.asList(appointmentA1.getKey(), appointmentA2.getKey(), - appointmentB1.getKey(), appointmentD1.getKey()); + Collection<Appointment> appointments = Arrays.asList(appointmentA1, appointmentA2, appointmentB1, appointmentD1); ParticipationSearchParams params = new ParticipationSearchParams(); - params.setAppointmentKeys(appointmentKeys); + params.setAppointments(appointments); params.setStatusModifiedAfter(new GregorianCalendar(2020, 3, 1, 12, 30, 0).getTime()); List<Participation> participations = sut.loadParticipations(params); -- GitLab