From 039e9a19b3752dbcbc6ce02dac28597dfa5d3bce Mon Sep 17 00:00:00 2001 From: uhensler <urs.hensler@frentix.com> Date: Mon, 15 Jun 2020 17:37:20 +0200 Subject: [PATCH] OO-4630: Show only really selected appointment in topics list And some performance optimizations. --- .../appointments/AppointmentsService.java | 14 +++- .../appointments/manager/AppointmentDAO.java | 61 ++++++++++++-- .../manager/AppointmentsServiceImpl.java | 11 +++ .../manager/ParticipationDAO.java | 2 +- .../appointments/ui/TopicsRunController.java | 79 ++++++------------- .../manager/AppointmentDAOTest.java | 77 ++++++++++++++++++ 6 files changed, 180 insertions(+), 64 deletions(-) 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 c1073373210..333132c6c40 100644 --- a/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java +++ b/src/main/java/org/olat/course/nodes/appointments/AppointmentsService.java @@ -21,6 +21,7 @@ package org.olat.course.nodes.appointments; import java.util.Collection; import java.util.List; +import java.util.Map; import org.olat.core.commons.services.notifications.PublisherData; import org.olat.core.commons.services.notifications.SubscriptionContext; @@ -65,7 +66,18 @@ public interface AppointmentsService { public void unconfirmAppointment(Appointment appointment); public void deleteAppointment(Appointment appointment); - + + /** + * Gets the key of the topic and the according count of appointments. + * + * @param params + * @param freeOnly counts only appointments with free participations + * @return the count may be null (instead of 0) if no appointments available. + */ + public Map<Long, Long> getTopicKeyToAppointmentCount(AppointmentSearchParams params, boolean freeOnly); + + public Long getAppointmentCount(AppointmentSearchParams params); + public List<Appointment> getAppointments(AppointmentSearchParams params); public ParticipationResult createParticipation(Appointment appointment, Identity identity, diff --git a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentDAO.java b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentDAO.java index 94a4f2c0e38..f322a28fd33 100644 --- a/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentDAO.java +++ b/src/main/java/org/olat/course/nodes/appointments/manager/AppointmentDAO.java @@ -22,8 +22,13 @@ package org.olat.course.nodes.appointments.manager; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.persistence.Query; +import javax.persistence.Tuple; import javax.persistence.TypedQuery; import org.olat.core.commons.persistence.DB; @@ -159,10 +164,58 @@ class AppointmentDAO { .getResultList(); return appointments.isEmpty() ? null : appointments.get(0); } + + Map<Long, Long> loadTopicKeyToAppointmentCount(AppointmentSearchParams params, boolean freeOnly) { + QueryBuilder sb = new QueryBuilder(); + sb.append("select appointment.topic.key as topicKey"); + sb.append(" , appointment.key as key"); + sb.append(" , appointment.maxParticipations as maxParticipations"); + sb.append(" , ( select count(*)"); + sb.append(" from appointmentparticipation participation"); + sb.append(" where participation.appointment.key = appointment.key"); + sb.append(" ) as count"); + appendQuery(sb, params); + + TypedQuery<Tuple> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), Tuple.class); + addParameters(query, params); + + List<Tuple> results = query.getResultList(); + return results.stream() + .filter(filterFreeOnly(freeOnly)) + .map(t -> (Long)t.get(0)) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + } + + private Predicate<? super Tuple> filterFreeOnly(boolean freeOnly) { + return t -> freeOnly? t.get(2) == null || ((Integer)t.get(2)).intValue() < ((Long)t.get(3)).intValue(): true; + } + + Long loadAppointmentCount(AppointmentSearchParams params) { + QueryBuilder sb = new QueryBuilder(); + sb.append("select count(appointment)"); + appendQuery(sb, params); + + TypedQuery<Long> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), Long.class); + addParameters(query, params); + + return query.getSingleResult(); + } List<Appointment> loadAppointments(AppointmentSearchParams params) { QueryBuilder sb = new QueryBuilder(); sb.append("select appointment"); + appendQuery(sb, params); + + TypedQuery<Appointment> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), Appointment.class); + addParameters(query, params); + + return query.getResultList(); + } + + private void appendQuery(QueryBuilder sb, AppointmentSearchParams params) { sb.append(" from appointment appointment"); sb.append(" join").append(" fetch", params.isFetchTopic()).append(" appointment.topic topic"); if (params.getAppointmentKey() != null) { @@ -183,9 +236,9 @@ class AppointmentDAO { if (params.getStatus() != null) { sb.and().append("appointment.status = :status"); } - - TypedQuery<Appointment> query = dbInstance.getCurrentEntityManager() - .createQuery(sb.toString(), Appointment.class); + } + + private void addParameters(TypedQuery<?> query, AppointmentSearchParams params) { if (params.getAppointmentKey() != null) { query.setParameter("appointmentKey", params.getAppointmentKey()); } @@ -204,8 +257,6 @@ class AppointmentDAO { if (params.getStatus() != null) { query.setParameter("status", params.getStatus()); } - - return query.getResultList(); } } 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 06469e48a77..1319f2e1f5d 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 @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.olat.basesecurity.BaseSecurityManager; @@ -250,6 +251,16 @@ public class AppointmentsServiceImpl implements AppointmentsService { appointmentsMailing.sendAppointmentDeleted(singletonList(appointment)); appointmentDao.delete(appointment); } + + @Override + public Map<Long, Long> getTopicKeyToAppointmentCount(AppointmentSearchParams params, boolean freeOnly) { + return appointmentDao.loadTopicKeyToAppointmentCount(params, freeOnly); + } + + @Override + public Long getAppointmentCount(AppointmentSearchParams params) { + return appointmentDao.loadAppointmentCount(params); + } @Override public List<Appointment> getAppointments(AppointmentSearchParams params) { 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 9d4b83c8e30..c1425b5614f 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 @@ -145,7 +145,7 @@ class ParticipationDAO { return query.getSingleResult(); } - + List<Participation> loadParticipations(ParticipationSearchParams params) { QueryBuilder sb = new QueryBuilder(); sb.append("select participation"); 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 ebef86eebdc..d8889a38afc 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 @@ -23,13 +23,11 @@ import static java.util.Collections.emptyList; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import org.olat.core.gui.UserRequest; @@ -117,23 +115,11 @@ public class TopicsRunController extends BasicController implements Activateable } private void refresh() { - removeButtons(); + mainVC.clear(); topics = loadTopicWrappers(); mainVC.contextPut("topics", topics); } - private void removeButtons() { - List<String> componentNames = new ArrayList<>(); - for (Component component : mainVC.getComponents()) { - if (!"infoSubscription".equals(component.getComponentName())) { - componentNames.add(component.getComponentName()); - } - } - for (String componentName : componentNames) { - mainVC.remove(componentName); - } - } - private List<TopicWrapper> loadTopicWrappers() { List<Topic> topics = appointmentsService.getTopics(entry, subIdent); Map<Long, List<Organizer>> topicKeyToOrganizer = appointmentsService @@ -143,37 +129,24 @@ public class TopicsRunController extends BasicController implements Activateable AppointmentSearchParams aParams = new AppointmentSearchParams(); aParams.setEntry(entry); aParams.setSubIdent(subIdent); - Map<Long, List<Appointment>> topicKeyToAppointments = appointmentsService - .getAppointments(aParams).stream() - .collect(Collectors.groupingBy(a -> a.getTopic().getKey())); + Map<Long, Long> topicKeyToAppointmentCount = appointmentsService.getTopicKeyToAppointmentCount(aParams, true); ParticipationSearchParams myParticipationsParams = new ParticipationSearchParams(); myParticipationsParams.setEntry(entry); myParticipationsParams.setSubIdent(subIdent); myParticipationsParams.setIdentity(getIdentity()); myParticipationsParams.setFetchAppointments(true); - List<Participation> myParticipations = appointmentsService.getParticipations(myParticipationsParams); - Map<Long, List<Participation>> topicKeyToMyParticipation = myParticipations.stream() + Map<Long, List<Participation>> topicKeyToMyParticipation = appointmentsService + .getParticipations(myParticipationsParams).stream() .collect(Collectors.groupingBy(p -> p.getAppointment().getTopic().getKey())); - Map<Long, List<Participation>> appointmentsToMyParticipation = myParticipations.stream() - .collect(Collectors.groupingBy(p -> p.getAppointment().getKey())); - - Set<Long> myAppointmentKeys = appointmentsToMyParticipation.keySet(); - ParticipationSearchParams allParticipationParams = new ParticipationSearchParams(); - allParticipationParams.setAppointmentKeys(myAppointmentKeys); - Map<Long, List<Participation>> appointmentKeyToAllParticipations = appointmentsService - .getParticipations(allParticipationParams).stream() - .collect(Collectors.groupingBy(p -> p.getAppointment().getKey())); List<TopicWrapper> wrappers = new ArrayList<>(topics.size()); for (Topic topic : topics) { TopicWrapper wrapper = new TopicWrapper(topic); List<Organizer> organizers = topicKeyToOrganizer.getOrDefault(topic.getKey(), emptyList()); wrapOrganizers(wrapper, organizers); - List<Appointment> appointments = topicKeyToAppointments.getOrDefault(topic.getKey(), emptyList()); - List<Participation> topicParticipations = topicKeyToMyParticipation.getOrDefault(topic.getKey(), emptyList()); - wrapParticpations(wrapper, topic, topicParticipations, appointments, appointmentsToMyParticipation, - appointmentKeyToAllParticipations); + List<Participation> myTopicParticipations = topicKeyToMyParticipation.getOrDefault(topic.getKey(), emptyList()); + wrapParticpations(wrapper, topic, myTopicParticipations, topicKeyToAppointmentCount); wrappers.add(wrapper); } return wrappers; @@ -196,18 +169,19 @@ public class TopicsRunController extends BasicController implements Activateable } } - private void wrapParticpations(TopicWrapper wrapper, Topic topic, List<Participation> participations, - List<Appointment> appointments, Map<Long, List<Participation>> appointmentKeyToParticipation, - Map<Long, List<Participation>> appointmentKeyToAllParticipations) { - if (!participations.isEmpty()) { + private void wrapParticpations(TopicWrapper wrapper, Topic topic, List<Participation> myTopicParticipations, + Map<Long, Long> topicKeyToAppointmentCount) { + if (!myTopicParticipations.isEmpty()) { Date now = new Date(); - Optional<Appointment> nextAppointment = appointments.stream() + Optional<Appointment> nextAppointment = myTopicParticipations.stream() + .map(Participation::getAppointment) .filter(a1 -> now.before(a1.getEnd())) .sorted((a1, a2) -> a1.getStart().compareTo(a2.getStart())) .findFirst(); Appointment appointment = nextAppointment.isPresent() ? nextAppointment.get() // Next appointment ... - : appointments.stream() + : myTopicParticipations.stream() + .map(Participation::getAppointment) .sorted((a1, a2) -> a2.getStart().compareTo(a1.getStart())) .findFirst().get(); // ... or the most recent one. wrapper.setFuture(Boolean.valueOf(appointment.getStart().after(now))); @@ -217,16 +191,17 @@ public class TopicsRunController extends BasicController implements Activateable wrapper.setTranslatedStatus(translate("appointment.status." + appointment.getStatus().name())); wrapper.setStatusCSS("o_ap_status_" + appointment.getStatus().name()); - List<String> participants = appointmentKeyToAllParticipations - .getOrDefault(appointment.getKey(), Collections.emptyList()).stream() + ParticipationSearchParams allParticipationParams = new ParticipationSearchParams(); + allParticipationParams.setAppointment(appointment); + List<Participation> appointmentParticipations = appointmentsService.getParticipations(allParticipationParams); + + List<String> participants = appointmentParticipations.stream() .map(p -> userManager.getUserDisplayName(p.getIdentity().getKey())) .sorted(String.CASE_INSENSITIVE_ORDER) .collect(Collectors.toList()); wrapper.setParticipants(participants); - if (participations.size() >= 2) { - wrapper.setSelectedAppointments(Integer.valueOf(participations.size())); - } + wrapper.setSelectedAppointments(Integer.valueOf(myTopicParticipations.size())); boolean canChange = config.isMultiParticipations() ? true @@ -235,12 +210,9 @@ public class TopicsRunController extends BasicController implements Activateable wrapOpenLink(wrapper, topic, "appointments.change"); } } else { - long freeAppointments = appointments.stream() - .filter(a -> Appointment.Status.planned == a.getStatus()) - .filter(a -> hasFreeParticipations(a, appointmentKeyToParticipation)) - .count(); - wrapper.setFreeAppointments(Long.valueOf(freeAppointments)); - if (freeAppointments > 0) { + Long freeAppointments = topicKeyToAppointmentCount.getOrDefault(topic.getKey(), Long.valueOf(0)); + wrapper.setFreeAppointments(freeAppointments); + if (freeAppointments.longValue() > 0) { wrapOpenLink(wrapper, topic, "appointments.select"); } } @@ -301,13 +273,6 @@ public class TopicsRunController extends BasicController implements Activateable openLink.setUserObject(topic); wrapper.setOpenLinkName(openLink.getComponentName()); } - - private boolean hasFreeParticipations(Appointment appointment, Map<Long, List<Participation>> appointmentKeyToParticipation) { - if (appointment.getMaxParticipations() == null) return true; - - List<Participation> participations = appointmentKeyToParticipation.getOrDefault(appointment.getKey(), emptyList()); - return appointment.getMaxParticipations().intValue() > participations.size(); - } @Override protected void event(UserRequest ureq, Controller source, Event event) { 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 eb034200fa2..6ce8cbfad43 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 @@ -27,10 +27,12 @@ import java.time.LocalDateTime; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; +import java.util.Map; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.olat.core.commons.persistence.DB; +import org.olat.core.id.Identity; import org.olat.core.util.DateUtils; import org.olat.course.nodes.appointments.Appointment; import org.olat.course.nodes.appointments.Appointment.Status; @@ -53,6 +55,8 @@ public class AppointmentDAOTest extends OlatTestCase { private DB dbInstance; @Autowired private TopicDAO topicDao; + @Autowired + private ParticipationDAO participationDao; @Autowired private AppointmentDAO sut; @@ -239,6 +243,79 @@ public class AppointmentDAOTest extends OlatTestCase { assertThat(reloadedAppointment).isEqualTo(appointment); } + @Test + public void shouldLoadTopicKeyToAppointmentCount() { + RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry(); + Identity identity = JunitTestHelper.createAndPersistIdentityAsUser(random()); + String subIdent = JunitTestHelper.random(); + Topic topic1 = topicDao.createTopic(entry, subIdent); + Topic topic2 = topicDao.createTopic(entry, subIdent); + Appointment appointment11 = sut.createAppointment(topic1); + sut.createAppointment(topic1); + Appointment appointment21 = sut.createAppointment(topic2); + appointment21.setMaxParticipations(1); + sut.saveAppointment(appointment21); + participationDao.createParticipation(appointment11, identity); + participationDao.createParticipation(appointment11, identity); + participationDao.createParticipation(appointment21, identity); + dbInstance.commitAndCloseSession(); + + AppointmentSearchParams params = new AppointmentSearchParams(); + params.setEntry(entry); + params.setSubIdent(subIdent); + Map<Long, Long> appointmentCountByTopic = sut.loadTopicKeyToAppointmentCount(params, false); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(appointmentCountByTopic.get(topic1.getKey())).isEqualTo(2); + softly.assertThat(appointmentCountByTopic.get(topic2.getKey())).isEqualTo(1); + softly.assertAll(); + } + + @Test + public void shouldLoadTopicKeyToAppointmentCountOnlyFree() { + RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry(); + Identity identity = JunitTestHelper.createAndPersistIdentityAsUser(random()); + String subIdent = JunitTestHelper.random(); + Topic topic1 = topicDao.createTopic(entry, subIdent); + Topic topic2 = topicDao.createTopic(entry, subIdent); + Appointment appointment11 = sut.createAppointment(topic1); + sut.createAppointment(topic1); + Appointment appointment21 = sut.createAppointment(topic2); + appointment21.setMaxParticipations(1); + sut.saveAppointment(appointment21); + participationDao.createParticipation(appointment11, identity); + participationDao.createParticipation(appointment11, identity); + participationDao.createParticipation(appointment21, identity); + dbInstance.commitAndCloseSession(); + + AppointmentSearchParams params = new AppointmentSearchParams(); + params.setEntry(entry); + params.setSubIdent(subIdent); + Map<Long, Long> appointmentCountByTopic = sut.loadTopicKeyToAppointmentCount(params, true); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(appointmentCountByTopic.get(topic1.getKey())).isEqualTo(2); + softly.assertThat(appointmentCountByTopic.get(topic2.getKey())).isNull(); + softly.assertAll(); + } + + @Test + public void shouldLoadCount() { + RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry(); + Topic topic = topicDao.createTopic(entry, random()); + Topic topicOther = topicDao.createTopic(entry, random()); + sut.createAppointment(topic); + sut.createAppointment(topic); + sut.createAppointment(topicOther); + dbInstance.commitAndCloseSession(); + + AppointmentSearchParams params = new AppointmentSearchParams(); + params.setTopic(topic); + Long count = sut.loadAppointmentCount(params); + + assertThat(count).isEqualTo(2); + } + @Test public void shouldLoadByAppointmentKey() { RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry(); -- GitLab