From 730d5140a98f26ac87d9a9f1eae30af4d281bf60 Mon Sep 17 00:00:00 2001 From: uhensler <urs.hensler@frentix.com> Date: Fri, 5 Jun 2020 15:14:56 +0200 Subject: [PATCH] OO-4630: GUI to create recurring appointments --- .../java/org/olat/core/util/DateUtils.java | 65 +++++++ .../ui/TopicCreateController.java | 172 ++++++++++++++---- ...s_create.html => appointments_single.html} | 0 .../ui/_i18n/LocalStrings_de.properties | 5 + .../ui/_i18n/LocalStrings_en.properties | 5 + .../org/olat/core/util/DateUtilsTest.java | 74 ++++++++ 6 files changed, 290 insertions(+), 31 deletions(-) rename src/main/java/org/olat/course/nodes/appointments/ui/_content/{appointments_create.html => appointments_single.html} (100%) diff --git a/src/main/java/org/olat/core/util/DateUtils.java b/src/main/java/org/olat/core/util/DateUtils.java index 269e2799641..06d3c682d6a 100644 --- a/src/main/java/org/olat/core/util/DateUtils.java +++ b/src/main/java/org/olat/core/util/DateUtils.java @@ -19,12 +19,17 @@ */ package org.olat.core.util; +import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; /** * @@ -58,6 +63,35 @@ public class DateUtils { return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(); } + public static Date setTime(Date date, int hour, int minutes, int seconds) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + calendar.set(Calendar.HOUR, hour); + calendar.set(Calendar.MINUTE, minutes); + calendar.set(Calendar.SECOND, seconds); + return calendar.getTime(); + } + + /** + * Keeps the day of the date but copies the date from the from date. + * + * @param to + * @param from + * @return + */ + public static Date copyTime(Date date, Date from) { + Calendar fromCalendar = new GregorianCalendar(); + fromCalendar.setTime(from); + + Calendar toCalendar = new GregorianCalendar(); + toCalendar.setTime(date); + toCalendar.set(Calendar.HOUR, fromCalendar.get(Calendar.HOUR)); + toCalendar.set(Calendar.MINUTE, fromCalendar.get(Calendar.MINUTE)); + toCalendar.set(Calendar.SECOND, fromCalendar.get(Calendar.SECOND)); + + return toCalendar.getTime(); + } + public static Date addDays(Date date, int days) { Calendar c = Calendar.getInstance(); c.setTime(date); @@ -71,5 +105,36 @@ public class DateUtils { return date1.after(date2)? date1: date2; } + + public static List<Date> getDaysInRange(Date start, Date end) { + List<Date> dates = new ArrayList<>(); + Calendar calendar = new GregorianCalendar(); + calendar.setTime(start); + + while (calendar.getTime().before(end)) { + Date result = calendar.getTime(); + dates.add(result); + calendar.add(Calendar.DATE, 1); + } + + return dates; + } + + public static List<Date> getDaysInRange(Date start, Date end, Collection<DayOfWeek> daysOfWeek) { + List<Date> dates = new ArrayList<>(); + Calendar calendar = new GregorianCalendar(); + calendar.setTime(start); + + while (calendar.getTime().before(end)) { + DayOfWeek dayOfWeek = toLocalDate(calendar.getTime()).getDayOfWeek(); + if (daysOfWeek.contains(dayOfWeek)) { + Date result = calendar.getTime(); + dates.add(result); + } + calendar.add(Calendar.DATE, 1); + } + + return dates; + } } diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java index fbb6e8a7ea7..06422bf1954 100644 --- a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java +++ b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java @@ -22,6 +22,8 @@ package org.olat.course.nodes.appointments.ui; import static org.olat.core.gui.components.util.KeyValues.VALUE_ASC; import static org.olat.core.gui.components.util.KeyValues.entry; +import java.time.DayOfWeek; +import java.time.format.TextStyle; import java.util.ArrayList; import java.util.Collection; import java.util.Date; @@ -46,6 +48,7 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.id.Identity; +import org.olat.core.util.DateUtils; import org.olat.core.util.StringHelper; import org.olat.course.nodes.appointments.Appointment; import org.olat.course.nodes.appointments.AppointmentsSecurityCallback; @@ -74,7 +77,11 @@ public class TopicCreateController extends FormBasicController { private MultipleSelectionElement organizerEl; private TextElement locationEl; private TextElement maxParticipationsEl; - private FormLayoutContainer appointmentsCont; + private MultipleSelectionElement recurringEl; + private FormLayoutContainer singleCont; + private DateChooser recurringFirstEl; + private MultipleSelectionElement recurringDaysOfWeekEl; + private DateChooser recurringLastEl; private RepositoryEntry entry; private String subIdent; @@ -112,16 +119,11 @@ public class TopicCreateController extends FormBasicController { @Override protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { // Topic - String title = topic == null ? "" : topic.getTitle(); - titleEl = uifactory.addTextElement("topic.title", "topic.title", 128, title, formLayout); + titleEl = uifactory.addTextElement("topic.title", "topic.title", 128, null, formLayout); titleEl.setMandatory(true); - if(!StringHelper.containsNonWhitespace(title)) { - titleEl.setFocus(true); - } - String description = topic == null ? "" : topic.getDescription(); descriptionEl = uifactory.addTextAreaElement("topic.description", "topic.description", 2000, 4, 72, false, - false, description, formLayout); + false, null, formLayout); // Organizer KeyValues coachesKV = new KeyValues(); @@ -143,25 +145,62 @@ public class TopicCreateController extends FormBasicController { maxParticipationsEl = uifactory.addTextElement("appointment.max.participations", 5, null, formLayout); - appointmentsCont = FormLayoutContainer.createCustomFormLayout("appointmentsCont", getTranslator(), velocity_root + "/appointments_create.html"); - formLayout.add(appointmentsCont); - appointmentsCont.setRootForm(mainForm); - appointmentsCont.setLabel("appointments", null); + recurringEl = uifactory.addCheckboxesHorizontal("appointments.recurring", formLayout, + new String[] { "xx" }, new String[] { null }); + recurringEl.addActionListener(FormEvent.ONCHANGE); + + // Single appointments + singleCont = FormLayoutContainer.createCustomFormLayout("singleCont", getTranslator(), velocity_root + "/appointments_single.html"); + formLayout.add(singleCont); + singleCont.setRootForm(mainForm); + singleCont.setLabel("appointments", null); appointmentWrappers = new ArrayList<>(); doCreateAppointmentWrapper(null); - appointmentsCont.contextPut("appointments", appointmentWrappers); + singleCont.contextPut("appointments", appointmentWrappers); + + // Reccuring appointments + recurringFirstEl = uifactory.addDateChooser("appointments.recurring.first", null, formLayout); + recurringFirstEl.setDateChooserTimeEnabled(true); + recurringFirstEl.setSecondDate(true); + recurringFirstEl.setSameDay(true); + recurringFirstEl.setMandatory(true); + + DayOfWeek[] dayOfWeeks = DayOfWeek.values(); + KeyValues dayOfWeekKV = new KeyValues(); + for (int i = 0; i < dayOfWeeks.length; i++) { + dayOfWeekKV.add(entry(dayOfWeeks[i].name(), dayOfWeeks[i].getDisplayName(TextStyle.FULL_STANDALONE, getLocale()))); + } + recurringDaysOfWeekEl = uifactory.addCheckboxesHorizontal("appointments.recurring.days.of.week", formLayout, + dayOfWeekKV.keys(), dayOfWeekKV.values()); + recurringDaysOfWeekEl.setMandatory(true); + + recurringLastEl = uifactory.addDateChooser("appointments.recurring.last", null, formLayout); + recurringLastEl.setMandatory(true); + // Buttons FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); formLayout.add(buttonsCont); buttonsCont.setRootForm(mainForm); uifactory.addFormSubmitButton("save", buttonsCont); uifactory.addFormCancelButton("cancel", buttonsCont, ureq, getWindowControl()); + + updateUI(); + } + + private void updateUI() { + boolean recurring = recurringEl.isAtLeastSelected(1); + singleCont.setVisible(!recurring); + recurringFirstEl.setVisible(recurring); + recurringDaysOfWeekEl.setVisible(recurring); + recurringLastEl.setVisible(recurring); } @Override protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { - if (source instanceof DateChooser) { + if (source == recurringEl) { + updateUI(); + } else if (source instanceof DateChooser) { DateChooser dateChooser = (DateChooser)source; AppointmentWrapper wrapper = (AppointmentWrapper)dateChooser.getUserObject(); doInitEndDate(wrapper); @@ -204,26 +243,59 @@ public class TopicCreateController extends FormBasicController { } } + boolean recurring = recurringEl.isAtLeastSelected(1); + for (AppointmentWrapper wrapper : appointmentWrappers) { DateChooser startEl = wrapper.getStartEl(); DateChooser endEl = wrapper.getEndEl(); startEl.clearError(); endEl.clearError(); - if (startEl.getDate() == null && endEl.getDate() != null) { - startEl.setErrorKey("form.legende.mandatory", null); + if (!recurring) { + if (startEl.getDate() == null && endEl.getDate() != null) { + startEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } + if (endEl.getDate() == null && startEl.getDate() != null) { + endEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } + if (startEl.getDate() != null && endEl.getDate() != null) { + Date start = startEl.getDate(); + Date end = endEl.getDate(); + if(end.before(start)) { + endEl.setErrorKey("error.start.after.end", null); + allOk &= false; + } + } + } + } + + recurringFirstEl.clearError(); + recurringDaysOfWeekEl.clearError(); + recurringLastEl.clearError(); + if (recurring) { + if (recurringFirstEl.getDate() == null || recurringFirstEl.getSecondDate() == null) { + recurringFirstEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } else if (recurringFirstEl.getDate().after(recurringFirstEl.getSecondDate())) { + recurringFirstEl.setErrorKey("error.start.after.end", null); allOk &= false; } - if (endEl.getDate() == null && startEl.getDate() != null) { - endEl.setErrorKey("form.legende.mandatory", null); + + if (!recurringDaysOfWeekEl.isAtLeastSelected(1)) { + recurringDaysOfWeekEl.setErrorKey("form.legende.mandatory", null); allOk &= false; } - if (startEl.getDate() != null && endEl.getDate() != null) { - Date start = startEl.getDate(); - Date end = endEl.getDate(); - if(end.before(start)) { - endEl.setErrorKey("error.start.after.end", null); - allOk &= false; - } + + if (recurringLastEl.getDate() == null) { + recurringLastEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } + + if (recurringFirstEl.getDate() != null && recurringLastEl.getDate() != null + && recurringFirstEl.getDate().after(recurringLastEl.getDate())) { + recurringLastEl.setErrorKey("error.first.after.start", null); + allOk &= false; } } @@ -244,7 +316,13 @@ public class TopicCreateController extends FormBasicController { private void doSave() { doSaveTopic(); doSaveOrganizers(); - doSaveAppointments(); + + boolean reccuring = recurringEl.isAtLeastSelected(1); + if (reccuring) { + doSaveReccuringAppointments(); + } else { + doSaveSingleAppointments(); + } } private void doSaveTopic() { @@ -282,7 +360,7 @@ public class TopicCreateController extends FormBasicController { .forEach(coach -> appointmentsService.createOrganizer(topic, coach)); } - private void doSaveAppointments() { + private void doSaveSingleAppointments() { for (AppointmentWrapper wrapper : appointmentWrappers) { DateChooser startEl = wrapper.getStartEl(); DateChooser endEl = wrapper.getEndEl(); @@ -308,26 +386,58 @@ public class TopicCreateController extends FormBasicController { } } } + + private void doSaveReccuringAppointments() { + Date firstStart = recurringFirstEl.getDate(); + Date firstEnd = recurringFirstEl.getSecondDate(); + Date last = recurringLastEl.getDate(); + last = DateUtils.setTime(last, 23, 59, 59); + + Collection<DayOfWeek> daysOfWeek = recurringDaysOfWeekEl.getSelectedKeys().stream() + .map(DayOfWeek::valueOf) + .collect(Collectors.toList()); + + List<Date> starts = DateUtils.getDaysInRange(firstStart, last, daysOfWeek); + for (Date start : starts) { + Appointment appointment = appointmentsService.createUnsavedAppointment(topic); + + appointment.setStart(start); + + Date end = DateUtils.copyTime(start, firstEnd); + appointment.setEnd(end); + + String location = locationEl.getValue(); + appointment.setLocation(location); + + String maxParticipationsValue = maxParticipationsEl.getValue(); + Integer maxParticipations = StringHelper.containsNonWhitespace(maxParticipationsValue) + ? Integer.valueOf(maxParticipationsValue) + : null; + appointment.setMaxParticipations(maxParticipations); + + appointmentsService.saveAppointment(appointment); + } + } private void doCreateAppointmentWrapper(AppointmentWrapper after) { AppointmentWrapper wrapper = new AppointmentWrapper(); - DateChooser startEl = uifactory.addDateChooser("start_" + counter++, null, appointmentsCont); + DateChooser startEl = uifactory.addDateChooser("start_" + counter++, null, singleCont); startEl.setDateChooserTimeEnabled(true); startEl.setUserObject(wrapper); startEl.addActionListener(FormEvent.ONCHANGE); wrapper.setStartEl(startEl); - DateChooser endEl = uifactory.addDateChooser("end_" + counter++, null, appointmentsCont); + DateChooser endEl = uifactory.addDateChooser("end_" + counter++, null, singleCont); endEl.setDateChooserTimeEnabled(true); wrapper.setEndEl(endEl); - FormLink addEl = uifactory.addFormLink("add_" + counter++, CMD_ADD, "", null, appointmentsCont, Link.NONTRANSLATED + Link.BUTTON); + FormLink addEl = uifactory.addFormLink("add_" + counter++, CMD_ADD, "", null, singleCont, Link.NONTRANSLATED + Link.BUTTON); addEl.setIconLeftCSS("o_icon o_icon-lg o_icon_add"); addEl.setUserObject(wrapper); wrapper.setAddEl(addEl); - FormLink removeEl = uifactory.addFormLink("remove_" + counter++, CMD_REMOVE, "", null, appointmentsCont, Link.NONTRANSLATED + Link.BUTTON); + FormLink removeEl = uifactory.addFormLink("remove_" + counter++, CMD_REMOVE, "", null, singleCont, Link.NONTRANSLATED + Link.BUTTON); removeEl.setIconLeftCSS("o_icon o_icon-lg o_icon_delete"); removeEl.setUserObject(wrapper); wrapper.setRemoveEl(removeEl); diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_create.html b/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_single.html similarity index 100% rename from src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_create.html rename to src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_single.html 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 b46259a5b64..b2c8c42f543 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 @@ -28,6 +28,10 @@ appointments.confirmable.one=Ein Termin muss noch best\u00E4tigt werden. appointments.free=Es sind noch {0} Termine frei. appointments.free.one=Es ist noch ein Termin frei. appointments.open=Termine anzeigen +appointments.recurring=Wiederkehrende Termine +appointments.recurring.days.of.week=Wochentage +appointments.recurring.first=Erster Termin +appointments.recurring.last=Letzter Termin appointments.select=Termin ausw\u00E4hlen appointments.selected=Sie haben {0} Termine ausgew\u00E4hlt. appointments.selected.one=Sie haben einen Termin ausgew\u00E4hlt. @@ -53,6 +57,7 @@ edit.topic=Thema bearbeiten email.title=Neue Nachricht email.organizer.recipients=Organisatoren email.organizer.subject=Termin "{0}" +error.first.after.start=Der letzte Termin darf nicht vor dem ersten Termin liegen. 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. 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 d4faf593289..b07d2a38c89 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 @@ -28,6 +28,10 @@ appointments.confirmable.one=One appointment has to be confirmed. appointments.free=There are {0} appointments left. appointments.free.one=There is one appointment left. appointments.open=Show appointments +appointments.recurring=Recurring appointments +appointments.recurring.days.of.week=Days of week +appointments.recurring.first=First appointment +appointments.recurring.last=Last appointment appointments.select=Select appointment appointments.selected=You have {0} appointments selected. appointments.selected.one=You have one appointment selected. @@ -53,6 +57,7 @@ edit.topic=Edit topic email.title=New message email.organizer.recipients=Organizers email.organizer.subject=Appointment "{0}" +error.first.after.start=The last appointment must not be before the first appointment. 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. diff --git a/src/test/java/org/olat/core/util/DateUtilsTest.java b/src/test/java/org/olat/core/util/DateUtilsTest.java index 34a47320935..f0472676ae3 100644 --- a/src/test/java/org/olat/core/util/DateUtilsTest.java +++ b/src/test/java/org/olat/core/util/DateUtilsTest.java @@ -22,8 +22,12 @@ package org.olat.core.util; import static org.assertj.core.api.Assertions.assertThat; import static org.olat.core.util.DateUtils.toDate; +import java.time.DayOfWeek; import java.time.LocalDate; import java.util.Date; +import java.util.EnumSet; +import java.util.GregorianCalendar; +import java.util.List; import org.junit.Test; @@ -35,6 +39,25 @@ import org.junit.Test; */ public class DateUtilsTest { + @Test + public void shouldSetTime() { + Date date = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(); + + date = DateUtils.setTime(date, 20, 10, 5); + + assertThat(date).isEqualTo(new GregorianCalendar(2020, 5, 1, 20, 10, 5).getTime()); + } + + @Test + public void shouldCopyTime() { + Date date = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(); + Date from = new GregorianCalendar(2020, 8, 20, 8, 3, 2).getTime(); + + date = DateUtils.copyTime(date, from); + + assertThat(date).isEqualTo(new GregorianCalendar(2020, 5, 1, 8, 3, 2).getTime()); + } + @Test public void shouldGetLaterIfFirstIsLater() { Date date1 = toDate(LocalDate.of(2011, 10, 12)); @@ -68,6 +91,57 @@ public class DateUtilsTest { assertThat(later).isEqualTo(expected); } + + @Test + public void shouldGetDaysInRange() { + Date start = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(); + Date end = new GregorianCalendar(2020, 5, 10, 0, 0, 0).getTime(); + + List<Date> days = DateUtils.getDaysInRange(start, end); + + assertThat(days) + .containsExactly( + new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 2, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 3, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 4, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 5, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 6, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 7, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 8, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 9, 10, 0, 0).getTime()) + .doesNotContain( + // because of the time + end + ); + } + + @Test + public void shouldGetDaysOfWeekInRange() { + Date start = new GregorianCalendar(2020, 4, 1, 10, 0, 0).getTime(); + Date end = new GregorianCalendar(2020, 5, 10, 0, 0, 0).getTime(); + + EnumSet<DayOfWeek> daysOfWeek = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); + List<Date> days = DateUtils.getDaysInRange(start, end, daysOfWeek); + + assertThat(days) + .containsExactly( + new GregorianCalendar(2020, 4, 4, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 6, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 11, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 13, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 18, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 20, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 25, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 4, 27, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 3, 10, 0, 0).getTime(), + new GregorianCalendar(2020, 5, 8, 10, 0, 0).getTime()) + .doesNotContain( + end + ); + + } } -- GitLab