diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java index 536d717640..ab83676b5c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java @@ -16,9 +16,7 @@ package uk.ac.cam.cl.dtg.isaac.api; import com.google.api.client.util.Lists; -import com.google.api.client.util.Maps; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Sets; import com.google.inject.Inject; import com.opencsv.CSVWriter; import io.swagger.v3.oas.annotations.Operation; @@ -34,6 +32,7 @@ import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsCancelledException; import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsFullException; import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsNotFullException; +import uk.ac.cam.cl.dtg.isaac.api.managers.EventsManager; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; import uk.ac.cam.cl.dtg.isaac.dos.eventbookings.BookingStatus; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; @@ -47,11 +46,9 @@ import uk.ac.cam.cl.dtg.isaac.dto.eventbookings.EventBookingDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.UserSummaryDTO; -import uk.ac.cam.cl.dtg.segue.api.Constants; import uk.ac.cam.cl.dtg.segue.api.managers.GroupManager; import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager; import uk.ac.cam.cl.dtg.segue.api.managers.UserAssociationManager; -import uk.ac.cam.cl.dtg.segue.api.services.ContentService; import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException; import uk.ac.cam.cl.dtg.segue.comm.EmailMustBeVerifiedException; @@ -59,11 +56,8 @@ import uk.ac.cam.cl.dtg.segue.dao.ResourceNotFoundException; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; -import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; import uk.ac.cam.cl.dtg.segue.dao.schools.SchoolListReader; import uk.ac.cam.cl.dtg.segue.dao.schools.UnableToIndexSchoolsException; -import uk.ac.cam.cl.dtg.segue.search.AbstractFilterInstruction; -import uk.ac.cam.cl.dtg.segue.search.DateRangeFilterInstruction; import uk.ac.cam.cl.dtg.util.AbstractConfigLoader; import uk.ac.cam.cl.dtg.util.mappers.MainMapper; @@ -85,7 +79,6 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -94,8 +87,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static uk.ac.cam.cl.dtg.isaac.api.Constants.DATE_FIELDNAME; -import static uk.ac.cam.cl.dtg.isaac.api.Constants.*; import static uk.ac.cam.cl.dtg.segue.api.Constants.*; /** @@ -106,13 +97,11 @@ public class EventsFacade extends AbstractIsaacFacade { private static final Logger log = LoggerFactory.getLogger(EventsFacade.class); + private final EventsManager eventsManager; private final EventBookingManager bookingManager; - private final UserAccountManager userManager; - private final GroupManager groupManager; - private final GitContentManager contentManager; private final UserAssociationManager userAssociationManager; private final UserAccountManager userAccountManager; private final SchoolListReader schoolListReader; @@ -126,26 +115,23 @@ public class EventsFacade extends AbstractIsaacFacade { * @param logManager - for managing logs. * @param bookingManager - Instance of Booking Manager * @param userManager - * @param contentManager - for retrieving event content. * @param mapper */ @Inject public EventsFacade(final AbstractConfigLoader properties, final ILogManager logManager, - final EventBookingManager bookingManager, - final UserAccountManager userManager, final GitContentManager contentManager, - final UserAssociationManager userAssociationManager, - final GroupManager groupManager, - final UserAccountManager userAccountManager, final SchoolListReader schoolListReader, - final MainMapper mapper) { + final EventsManager eventsManager, final EventBookingManager bookingManager, + final UserAccountManager userManager, final UserAssociationManager userAssociationManager, + final GroupManager groupManager, final UserAccountManager userAccountManager, + final SchoolListReader schoolListReader, final MainMapper mapper) { super(properties, logManager); this.bookingManager = bookingManager; this.userManager = userManager; - this.contentManager = contentManager; this.userAssociationManager = userAssociationManager; this.groupManager = groupManager; this.userAccountManager = userAccountManager; this.schoolListReader = schoolListReader; this.mapper = mapper; + this.eventsManager = eventsManager; } /** @@ -157,7 +143,6 @@ public EventsFacade(final AbstractConfigLoader properties, final ILogManager log * @param limit - the maximums number of results to return * @param sortOrder - flag to indicate preferred sort order. * @param showActiveOnly - true will impose filtering on the results. False will not. Defaults to false. - * @param showInactiveOnly - true will impose filtering on the results. False will not. Defaults to false. * @param showStageOnly - if present, only events with an audience matching this string will be shown * @return a Response containing a list of events objects or containing a SegueErrorResponse. */ @@ -172,172 +157,62 @@ public final Response getEvents(@Context final HttpServletRequest request, @DefaultValue(DEFAULT_RESULTS_LIMIT_AS_STRING) @QueryParam("limit") final Integer limit, @QueryParam("sort_by") final String sortOrder, @QueryParam("show_active_only") final Boolean showActiveOnly, - @QueryParam("show_inactive_only") final Boolean showInactiveOnly, @QueryParam("show_booked_only") final Boolean showMyBookingsOnly, @QueryParam("show_reservations_only") final Boolean showReservationsOnly, @QueryParam("show_stage_only") final String showStageOnly) { - Map> fieldsToMatch = Maps.newHashMap(); - - Integer newLimit = null; - Integer newStartIndex = null; - if (limit != null) { - newLimit = limit; - } - - if (startIndex != null) { - newStartIndex = startIndex; - } - - if (tags != null) { - fieldsToMatch.put(TAGS_FIELDNAME, Arrays.asList(tags.split(","))); - } - - if (showStageOnly != null) { - fieldsToMatch.put(STAGE_FIELDNAME, Arrays.asList(showStageOnly.split(","))); - } - - final Map sortInstructions = Maps.newHashMap(); - if (sortOrder != null && sortOrder.equals("title")) { - sortInstructions.put(Constants.TITLE_FIELDNAME + "." + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, - SortOrder.ASC); - } else { - sortInstructions.put(DATE_FIELDNAME, SortOrder.DESC); - } - fieldsToMatch.put(TYPE_FIELDNAME, List.of(EVENT_TYPE)); - - Map filterInstructions = null; - if (null != showActiveOnly && showActiveOnly) { - filterInstructions = Maps.newHashMap(); - DateRangeFilterInstruction anyEventsFromNow = new DateRangeFilterInstruction(new Date(), null); - filterInstructions.put(ENDDATE_FIELDNAME, anyEventsFromNow); - sortInstructions.put(DATE_FIELDNAME, SortOrder.ASC); - } - - if (null != showInactiveOnly && showInactiveOnly) { - if (null != showActiveOnly && showActiveOnly) { - return new SegueErrorResponse(Status.BAD_REQUEST, - "You cannot request both show active and inactive only.").toResponse(); + try { + RegisteredUserDTO currentUser = null; + try { + currentUser = this.userManager.getCurrentRegisteredUser(request); + } catch (final NoUserLoggedInException e) { + // Safe to ignore at this point; only a problem if showMyBookings/ReservationsOnly set } - filterInstructions = Maps.newHashMap(); - DateRangeFilterInstruction anyEventsToNow = new DateRangeFilterInstruction(null, new Date()); - filterInstructions.put(ENDDATE_FIELDNAME, anyEventsToNow); - sortInstructions.put(DATE_FIELDNAME, SortOrder.DESC); - } - - try { - ResultsWrapper findByFieldNames = null; + ResultsWrapper results = null; if (null != showMyBookingsOnly && showMyBookingsOnly) { - RegisteredUserDTO currentUser = null; - try { - currentUser = this.userManager.getCurrentRegisteredUser(request); - } catch (NoUserLoggedInException e) { - /* Safe to ignore; will just leave currentUser null. */ - } if (null != currentUser) { - findByFieldNames = getEventsBookedByUser(request, fieldsToMatch.get(TAGS_FIELDNAME), currentUser); + List tagList = null != tags ? List.of(tags.split(",")) : null; + results = this.eventsManager.getEventsBookedByUser(tagList, currentUser); } else { - SegueErrorResponse.getNotLoggedInResponse(); + return SegueErrorResponse.getNotLoggedInResponse(); } } else if (null != showReservationsOnly && showReservationsOnly) { - RegisteredUserDTO currentUser = null; - try { - currentUser = this.userManager.getCurrentRegisteredUser(request); - } catch (NoUserLoggedInException e) { - /* Safe to ignore; will just leave currentUser null. */ - } if (null != currentUser) { - findByFieldNames = getEventsReservedByUser(request, currentUser); + results = this.eventsManager.getEventsReservedByUser(currentUser); } else { - SegueErrorResponse.getNotLoggedInResponse(); + return SegueErrorResponse.getNotLoggedInResponse(); } } else { - findByFieldNames = this.contentManager.findByFieldNames( - ContentService.generateDefaultFieldToMatch(fieldsToMatch), newStartIndex, newLimit, - sortInstructions, filterInstructions); + boolean includeHiddenContent = false; + try { + includeHiddenContent = isUserStaff(userManager, request); + } catch (final NoUserLoggedInException e) { + // Safe to ignore; leave includeHiddenContent as false + } + results = this.eventsManager.getEvents(tags, startIndex, limit, sortOrder, showActiveOnly, showStageOnly, + includeHiddenContent); + } + if (null != currentUser) { // augment (maybe slow for large numbers of bookings) - for (ContentDTO c : findByFieldNames.getResults()) { - this.augmentEventWithBookingInformation(request, c); + for (ContentDTO c : results.getResults()) { + this.eventsManager.augmentEventWithBookingInformation(currentUser, c); } } - return Response.ok(findByFieldNames).build(); - } catch (ContentManagerException e) { + return Response.ok(results).build(); + } catch (final ContentManagerException e) { log.error("Error during event request", e); return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error locating the content you requested.") .toResponse(); - } catch (SegueDatabaseException e) { + } catch (final SegueDatabaseException e) { return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error accessing your bookings.") .toResponse(); } } - /** - * Get Events Booked by user. - * - * @param request - the http request so we can resolve booking information - * @param tags - the tags we want to filter on - * @param currentUser - the currently logged on user. - * @return a list of event pages that the user has been booked - * @throws SegueDatabaseException - * @throws ContentManagerException - */ - private ResultsWrapper getEventsBookedByUser(final HttpServletRequest request, final List tags, - final RegisteredUserDTO currentUser) - throws SegueDatabaseException, ContentManagerException { - List filteredResults = Lists.newArrayList(); - - Map userBookingMap = this.bookingManager.getAllEventStatesForUser(currentUser.getId()); - - for (String eventId : userBookingMap.keySet()) { - if (BookingStatus.CANCELLED.equals(userBookingMap.get(eventId))) { - continue; - } - - final IsaacEventPageDTO eventDTOById = this.getAugmentedEventDTOById(request, eventId); - - if (tags != null) { - Set tagsList = Sets.newHashSet(tags); - tagsList.retainAll(eventDTOById.getTags()); // get intersection - if (tagsList.size() == 0) { - // if the intersection is empty then we can continue - continue; - } - } - - filteredResults.add(eventDTOById); - } - return new ResultsWrapper<>(filteredResults, (long) filteredResults.size()); - } - - /** - * Get Events Reserved by user. - * - * @param request - the http request so we can resolve booking information - * @param currentUser - the currently logged on user. - * @return a list of event pages that the user has been booked - * @throws SegueDatabaseException - * @throws ContentManagerException - */ - private ResultsWrapper getEventsReservedByUser(final HttpServletRequest request, - final RegisteredUserDTO currentUser) - throws SegueDatabaseException, ContentManagerException { - List filteredResults = Lists.newArrayList(); - - List userReservationList = this.mapper.mapToListOfEventBookingDTO(bookingManager.getAllEventReservationsForUser(currentUser.getId())); - - for (EventBookingDTO booking : userReservationList) { - - final IsaacEventPageDTO eventDTOById = this.getAugmentedEventDTOById(request, booking.getEventId()); - - filteredResults.add(eventDTOById); - } - return new ResultsWrapper<>(filteredResults, (long) filteredResults.size()); - } - /** * REST end point to retrieve an event by id.. * @@ -352,7 +227,13 @@ private ResultsWrapper getEventsReservedByUser(final HttpServletRequ public final Response getEvent(@Context final HttpServletRequest request, @PathParam("event_id") final String eventId) { try { - IsaacEventPageDTO page = getAugmentedEventDTOById(request, eventId); + RegisteredUserDTO currentUser = null; + try { + currentUser = this.userManager.getCurrentRegisteredUser(request); + } catch (final NoUserLoggedInException e) { + // Safe to ignore; we can still return results without user information. + } + IsaacEventPageDTO page = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); return Response.ok(page) .cacheControl(getCacheControl(NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build(); } catch (ResourceNotFoundException e) { @@ -448,7 +329,7 @@ public final Response promoteBooking(@Context final HttpServletRequest request, try { RegisteredUserDTO currentUser = this.userManager.getCurrentRegisteredUser(request); RegisteredUserDTO userOfInterest = this.userManager.getUserDTOById(userId); - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -503,7 +384,7 @@ public final Response adminGetEventBookingByEventId(@Context final HttpServletRe @PathParam("event_id") final String eventId) { try { RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(request); - IsaacEventPageDTO event = getRawEventDTOById(eventId); + IsaacEventPageDTO event = this.eventsManager.getRawEventDTOById(eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -544,7 +425,7 @@ public final Response getEventBookingForGivenGroup(@Context final HttpServletReq return new SegueErrorResponse(Status.FORBIDDEN, "You are not the owner or manager of this group.").toResponse(); } - IsaacEventPageDTO eventPageDTO = getRawEventDTOById(eventId); + IsaacEventPageDTO eventPageDTO = this.eventsManager.getRawEventDTOById(eventId); if (null == eventPageDTO) { return new SegueErrorResponse(Status.BAD_REQUEST, "No event found with this ID.").toResponse(); } @@ -631,7 +512,7 @@ public Response getEventBookingCSV(@Context final HttpServletRequest request, @PathParam("event_id") final String eventId) { try { RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(request); - IsaacEventPageDTO event = this.getRawEventDTOById(eventId); + IsaacEventPageDTO event = this.eventsManager.getRawEventDTOById(eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -765,7 +646,7 @@ public final Response createBookingForGivenUser(@Context final HttpServletReques try { RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(request); RegisteredUserDTO bookedUser = userManager.getUserDTOById(userId); - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -826,7 +707,7 @@ public final Response createReservationsForGivenUsers(@Context final HttpServlet RegisteredUserDTO reservingUser; IsaacEventPageDTO event; try { - event = this.getRawEventDTOById(eventId); + event = this.eventsManager.getRawEventDTOById(eventId); } catch (SegueDatabaseException | ContentManagerException e) { event = null; } @@ -913,7 +794,7 @@ public final Response cancelReservations(@Context final HttpServletRequest reque @PathParam("event_id") final String eventId, final List userIds) { try { - IsaacEventPageDTO event = getRawEventDTOById(eventId); + IsaacEventPageDTO event = this.eventsManager.getRawEventDTOById(eventId); RegisteredUserDTO userLoggedIn = this.userManager.getCurrentRegisteredUser(request); if (event.getDate() != null && new Date().after(event.getDate())) { @@ -987,7 +868,7 @@ public final Response createBookingForMe(@Context final HttpServletRequest reque final Map additionalInformation) { try { RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(user, eventId); if (EventStatus.CLOSED.equals(event.getEventStatus())) { return new SegueErrorResponse(Status.BAD_REQUEST, "Sorry booking for this event is closed. Please try again later.") @@ -1074,7 +955,7 @@ public final Response addMeToWaitingList(@Context final HttpServletRequest reque try { RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(user, eventId); EventBookingDTO eventBookingDTO = bookingManager.requestWaitingListBooking(event, user, additionalInformation); this.getLogManager().logEvent(userManager.getCurrentUser(request), request, @@ -1144,7 +1025,7 @@ public final Response cancelBooking(@Context final HttpServletRequest request, @PathParam("event_id") final String eventId, @PathParam("user_id") final Long userId) { try { - IsaacEventPageDTO event = getRawEventDTOById(eventId); + IsaacEventPageDTO event = this.eventsManager.getRawEventDTOById(eventId); RegisteredUserDTO userLoggedIn = this.userManager.getCurrentRegisteredUser(request); RegisteredUserDTO userOwningBooking; @@ -1216,9 +1097,9 @@ public final Response resendEventEmail(@Context final HttpServletRequest request @PathParam("event_id") final String eventId, @PathParam("user_id") final Long userId) { try { - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); RegisteredUserDTO bookedUser = this.userManager.getUserDTOById(userId); RegisteredUserDTO currentUser = this.userManager.getCurrentRegisteredUser(request); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -1277,7 +1158,7 @@ public final Response deleteBooking(@Context final HttpServletRequest request, return new SegueErrorResponse(Status.BAD_REQUEST, "User is not booked on this event.").toResponse(); } - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); RegisteredUserDTO user = this.userManager.getUserDTOById(userId); bookingManager.deleteBooking(event, user); @@ -1321,7 +1202,7 @@ public final Response recordEventAttendance(@Context final HttpServletRequest re try { RegisteredUserDTO currentUser = this.userManager.getCurrentRegisteredUser(request); RegisteredUserDTO userOfInterest = this.userManager.getUserDTOById(userId); - IsaacEventPageDTO event = this.getAugmentedEventDTOById(request, eventId); + IsaacEventPageDTO event = this.eventsManager.getAugmentedEventDTOById(currentUser, eventId); if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { return SegueErrorResponse.getIncorrectRoleResponse(); @@ -1381,109 +1262,26 @@ public final Response getEventOverviews(@Context final HttpServletRequest reques @DefaultValue(DEFAULT_START_INDEX_AS_STRING) @QueryParam("start_index") final Integer startIndex, @DefaultValue(DEFAULT_RESULTS_LIMIT_AS_STRING) @QueryParam("limit") final Integer limit, @QueryParam("filter") final String filter) { - Map> fieldsToMatch = Maps.newHashMap(); - - Integer newLimit = null; - Integer newStartIndex = null; - if (limit != null) { - newLimit = limit; - } - - if (startIndex != null) { - newStartIndex = startIndex; - } - - final Map sortInstructions = Maps.newHashMap(); - sortInstructions.put(DATE_FIELDNAME, SortOrder.DESC); - - fieldsToMatch.put(TYPE_FIELDNAME, Collections.singletonList(EVENT_TYPE)); try { - RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(request); + RegisteredUserDTO currentUser = this.userManager.getCurrentRegisteredUser(request); + if (!Arrays.asList(Role.EVENT_LEADER, Role.EVENT_MANAGER, Role.ADMIN).contains(currentUser.getRole())) { return SegueErrorResponse.getIncorrectRoleResponse(); } - Map filterInstructions = null; - if (filter != null) { - EventFilterOption filterOption = EventFilterOption.valueOf(filter); - filterInstructions = Maps.newHashMap(); - if (filterOption.equals(EventFilterOption.FUTURE)) { - DateRangeFilterInstruction anyEventsFromNow = new DateRangeFilterInstruction(new Date(), null); - filterInstructions.put(ENDDATE_FIELDNAME, anyEventsFromNow); - sortInstructions.put(DATE_FIELDNAME, SortOrder.ASC); - } else if (filterOption.equals(EventFilterOption.RECENT)) { - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.MONTH, -1); - DateRangeFilterInstruction eventsOverPreviousMonth = - new DateRangeFilterInstruction(calendar.getTime(), new Date()); - filterInstructions.put(ENDDATE_FIELDNAME, eventsOverPreviousMonth); - } else if (filterOption.equals(EventFilterOption.PAST)) { - DateRangeFilterInstruction anyEventsToNow = new DateRangeFilterInstruction(null, new Date()); - filterInstructions.put(ENDDATE_FIELDNAME, anyEventsToNow); - } - } - - ResultsWrapper findByFieldNames = null; - - findByFieldNames = this.contentManager.findByFieldNames( - ContentService.generateDefaultFieldToMatch(fieldsToMatch), - newStartIndex, newLimit, sortInstructions, filterInstructions); - - List> resultList = Lists.newArrayList(); - - for (ContentDTO c : findByFieldNames.getResults()) { - if (!(c instanceof IsaacEventPageDTO)) { - continue; - } - IsaacEventPageDTO event = (IsaacEventPageDTO) c; - - if (!bookingManager.isUserAbleToManageEvent(currentUser, event)) { - continue; - } - - ImmutableMap.Builder eventOverviewBuilder = new ImmutableMap.Builder<>(); - eventOverviewBuilder.put("id", event.getId()); - eventOverviewBuilder.put("title", event.getTitle()); - eventOverviewBuilder.put("subtitle", event.getSubtitle()); - eventOverviewBuilder.put("date", event.getDate()); - eventOverviewBuilder.put("bookingDeadline", - event.getBookingDeadline() == null ? event.getDate() : event.getBookingDeadline()); - eventOverviewBuilder.put("eventStatus", event.getEventStatus()); - - if (null != event.getLocation()) { - eventOverviewBuilder.put("location", event.getLocation()); - } - - Map bookingCounts = this.bookingManager.getBookingStatusCountsByEventId(event.getId()); - eventOverviewBuilder.put("numberOfConfirmedBookings", - bookingCounts.getOrDefault(BookingStatus.CONFIRMED, 0L)); - eventOverviewBuilder.put("numberOfWaitingListBookings", - bookingCounts.getOrDefault(BookingStatus.WAITING_LIST, 0L)); - eventOverviewBuilder.put("numberAttended", - bookingCounts.getOrDefault(BookingStatus.ATTENDED, 0L)); - eventOverviewBuilder.put("numberAbsent", - bookingCounts.getOrDefault(BookingStatus.ABSENT, 0L)); - - if (null != event.getNumberOfPlaces()) { - eventOverviewBuilder.put("numberOfPlaces", event.getNumberOfPlaces()); - } - - resultList.add(eventOverviewBuilder.build()); - } - - return Response.ok(new ResultsWrapper<>(resultList, findByFieldNames.getTotalResults())).build(); - } catch (ContentManagerException e) { + return Response.ok(this.eventsManager.getEventOverviews(startIndex, limit, filter, currentUser)).build(); + } catch (final ContentManagerException e) { log.error("Error during event request", e); return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error locating the content you requested.") .toResponse(); - } catch (NoUserLoggedInException e) { + } catch (final NoUserLoggedInException e) { return SegueErrorResponse.getNotLoggedInResponse(); - } catch (SegueDatabaseException e) { + } catch (final SegueDatabaseException e) { log.error("Error occurred during event overview look up", e); return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error locating the database content you requested.") .toResponse(); - } catch (IllegalArgumentException e) { + } catch (final IllegalArgumentException e) { log.error("Error occurred during event overview look up", e); return new SegueErrorResponse(Status.BAD_REQUEST, "Invalid request format.").toResponse(); } @@ -1509,160 +1307,13 @@ public final Response getEventMapData(@Context final HttpServletRequest request, @DefaultValue(DEFAULT_RESULTS_LIMIT_AS_STRING) @QueryParam("limit") final Integer limit, @QueryParam("show_active_only") final Boolean showActiveOnly, @QueryParam("show_stage_only") final String showStageOnly) { - Map> fieldsToMatch = Maps.newHashMap(); - - Integer newLimit = null; - Integer newStartIndex = null; - if (limit != null) { - newLimit = limit; - } - - if (startIndex != null) { - newStartIndex = startIndex; - } - - if (tags != null) { - fieldsToMatch.put(TAGS_FIELDNAME, Arrays.asList(tags.split(","))); - } - - if (showStageOnly != null) { - fieldsToMatch.put(STAGE_FIELDNAME, Arrays.asList(showStageOnly.split(","))); - } - - final Map sortInstructions = Maps.newHashMap(); - sortInstructions.put(DATE_FIELDNAME, SortOrder.DESC); - - fieldsToMatch.put(TYPE_FIELDNAME, Collections.singletonList(EVENT_TYPE)); - - Map filterInstructions = null; - if (null == showActiveOnly || showActiveOnly) { - filterInstructions = Maps.newHashMap(); - DateRangeFilterInstruction anyEventsFromNow = new DateRangeFilterInstruction(new Date(), null); - filterInstructions.put(ENDDATE_FIELDNAME, anyEventsFromNow); - sortInstructions.put(DATE_FIELDNAME, SortOrder.ASC); - } - try { - ResultsWrapper findByFieldNames = null; - - findByFieldNames = this.contentManager.findByFieldNames( - ContentService.generateDefaultFieldToMatch(fieldsToMatch), - newStartIndex, newLimit, sortInstructions, filterInstructions); - - List> resultList = Lists.newArrayList(); - - for (ContentDTO c : findByFieldNames.getResults()) { - if (!(c instanceof IsaacEventPageDTO)) { - continue; - } - - IsaacEventPageDTO e = (IsaacEventPageDTO) c; - if (null == e.getLocation() || (null == e.getLocation().getLatitude() && null == e.getLocation().getLongitude())) { - // Ignore events without locations. - continue; - } - if (e.getLocation().getLatitude().equals(0.0) && e.getLocation().getLongitude().equals(0.0)) { - // Ignore events with locations that haven't been set properly. - log.info("Event with 0.0 lat/long: " + e.getId()); - continue; - } - - ImmutableMap.Builder eventOverviewBuilder = new ImmutableMap.Builder<>(); - eventOverviewBuilder.put("id", e.getId()); - eventOverviewBuilder.put("title", e.getTitle()); - eventOverviewBuilder.put("date", e.getDate()); - eventOverviewBuilder.put("subtitle", e.getSubtitle()); - if (e.getEventStatus() != null) { - eventOverviewBuilder.put("status", e.getEventStatus()); - } - // The schema required needs lat and long at top-level, so add address at top-level too. - eventOverviewBuilder.put("address", e.getLocation().getAddress()); - eventOverviewBuilder.put("latitude", e.getLocation().getLatitude()); - eventOverviewBuilder.put("longitude", e.getLocation().getLongitude()); - - if (null != e.getBookingDeadline()) { - eventOverviewBuilder.put("deadline", e.getBookingDeadline()); - } - - resultList.add(eventOverviewBuilder.build()); - } - - return Response.ok(new ResultsWrapper<>(resultList, findByFieldNames.getTotalResults())).build(); - } catch (ContentManagerException e) { + return Response.ok(this.eventsManager.getEventMapData(tags, startIndex, limit, showActiveOnly, + showStageOnly)).build(); + } catch (final ContentManagerException e) { log.error("Error during event request", e); return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error locating the content you requested.") .toResponse(); } } - - /** - * A helper method for retrieving an event object without augmented information - * - * @param eventId the id of the event of interest - * @return the fully populated event dto with user context information. - * @throws ContentManagerException - if there is a problem finding the event information - * @throws SegueDatabaseException if there is a database error. - */ - private IsaacEventPageDTO getRawEventDTOById(final String eventId) - throws ContentManagerException, SegueDatabaseException { - - ContentDTO possibleEvent = this.contentManager.getContentById(eventId); - - if (null == possibleEvent) { - throw new ResourceNotFoundException(String.format("Unable to locate the event with id; %s", eventId)); - } - - if (possibleEvent instanceof IsaacEventPageDTO) { - // The Events Facade *mutates* the EventDTO returned by this method; we must return a copy of - // the original object else we will poison the contentManager's cache! - // TODO: might it be better to get the DO from the cache and map it to DTO here to reduce overhead? - IsaacEventPageDTO eventPageDTO = (IsaacEventPageDTO) possibleEvent; - return mapper.copy(eventPageDTO); - } - return null; - } - - /** - * A helper method for retrieving an event and the number of places available and if the user is booked or not. - * - * @param request so we can determine if the user is logged in - * @param eventId the id of the event of interest - * @return the fully populated event dto with user context information. - * @throws ContentManagerException - if there is a problem finding the event information - * @throws SegueDatabaseException if there is a database error. - */ - private IsaacEventPageDTO getAugmentedEventDTOById(final HttpServletRequest request, final String eventId) - throws ContentManagerException, SegueDatabaseException { - IsaacEventPageDTO event = getRawEventDTOById(eventId); - return augmentEventWithBookingInformation(request, event); - } - - /** - * Augment a single event with booking information before we send it out. - * - * @param request - for user look up - * @param possibleEvent - a ContentDTO that should hopefully be an IsaacEventPageDTO. - * @return an augmented IsaacEventPageDTO. - * @throws SegueDatabaseException - */ - private IsaacEventPageDTO augmentEventWithBookingInformation(final HttpServletRequest request, - final ContentDTO possibleEvent) - throws SegueDatabaseException { - if (possibleEvent instanceof IsaacEventPageDTO) { - IsaacEventPageDTO page = (IsaacEventPageDTO) possibleEvent; - - try { - RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - page.setUserBookingStatus(this.bookingManager.getBookingStatus(page.getId(), user.getId())); - } catch (NoUserLoggedInException e) { - // no action as we don't require the user to be logged in. - page.setUserBookingStatus(null); - } - - page.setPlacesAvailable(this.bookingManager.getPlacesAvailable(page)); - return page; - } else { - throw new ClassCastException("The object provided was not an event."); - } - } } \ No newline at end of file diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventsManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventsManager.java new file mode 100644 index 0000000000..0c99219379 --- /dev/null +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventsManager.java @@ -0,0 +1,409 @@ +package uk.ac.cam.cl.dtg.isaac.api.managers; + +import com.google.api.client.util.Lists; +import com.google.api.client.util.Maps; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.dos.eventbookings.BookingStatus; +import uk.ac.cam.cl.dtg.isaac.dto.IsaacEventPageDTO; +import uk.ac.cam.cl.dtg.isaac.dto.ResultsWrapper; +import uk.ac.cam.cl.dtg.isaac.dto.content.ContentDTO; +import uk.ac.cam.cl.dtg.isaac.dto.eventbookings.EventBookingDTO; +import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; +import uk.ac.cam.cl.dtg.segue.api.Constants; +import uk.ac.cam.cl.dtg.segue.dao.ResourceNotFoundException; +import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; +import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; +import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; +import uk.ac.cam.cl.dtg.segue.search.BooleanInstruction; +import uk.ac.cam.cl.dtg.segue.search.IsaacSearchInstructionBuilder; +import uk.ac.cam.cl.dtg.segue.search.SearchInField; +import uk.ac.cam.cl.dtg.util.mappers.MainMapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static uk.ac.cam.cl.dtg.isaac.api.Constants.*; +import static uk.ac.cam.cl.dtg.segue.api.Constants.*; + +/** + * EventsManager. + */ +public class EventsManager { + private static final Logger log = LoggerFactory.getLogger(EventsManager.class); + + private final EventBookingManager bookingManager; + private final GitContentManager contentManager; + + private final MainMapper mapper; + + /** + * EventsManager. + * + * @param bookingManager - event booking manager + * @param contentManager - git content manager + * @param mapper - mapper for mapping between DOs and DTOs + */ + @Inject + public EventsManager(final EventBookingManager bookingManager, final GitContentManager contentManager, + final MainMapper mapper) { + this.bookingManager = bookingManager; + this.contentManager = contentManager; + this.mapper = mapper; + } + + /** + * Logic for the /events endpoint to provide a list of events. + * + * @param tags - a comma separated list of tags to include in the search. + * @param startIndex - the initial index for the first result. + * @param limit - the maximums number of results to return + * @param sortOrder - flag to indicate preferred sort order. + * @param showActiveOnly - true will impose filtering on the results. False will not. Defaults to false. + * @param showStageOnly - if present, only events with an audience matching this string will be shown. + * @param includeHiddenContent - if true, include hidden (nofilter) events. + * @return a ResultsWrapper containing a list of filtered events as ContentDTOs. + */ + public ResultsWrapper getEvents(final String tags, final Integer startIndex, final Integer limit, + final String sortOrder, final Boolean showActiveOnly, + final String showStageOnly, final Boolean includeHiddenContent) + throws ContentManagerException, SegueDatabaseException { + + IsaacSearchInstructionBuilder searchInstructionBuilder = this.contentManager.getBaseSearchInstructionBuilder() + .includeContentTypes(Collections.singleton(EVENT_TYPE)); + + if (tags != null) { + searchInstructionBuilder.searchFor(new SearchInField(TAGS_FIELDNAME, + Arrays.stream(tags.split(",")).collect(Collectors.toSet()))); + } + + if (showStageOnly != null) { + searchInstructionBuilder.searchFor(new SearchInField(STAGE_FIELDNAME, + Arrays.stream(showStageOnly.split(",")).collect(Collectors.toSet()))); + } + + if (null != includeHiddenContent && includeHiddenContent) { + searchInstructionBuilder.includeHiddenContent(true); + } + + final Map sortInstructions = Maps.newHashMap(); + if (sortOrder != null && sortOrder.equals("title")) { + sortInstructions.put(TITLE_FIELDNAME + "." + UNPROCESSED_SEARCH_FIELD_SUFFIX, + Constants.SortOrder.ASC); + } + + if (null != showActiveOnly && showActiveOnly) { + // Should default to future events only, but set this explicitly anyway + searchInstructionBuilder.setEventFilterOption(Constants.EventFilterOption.FUTURE); + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.ASC); + } else { + searchInstructionBuilder.setEventFilterOption(Constants.EventFilterOption.ALL); + } + + if (sortInstructions.isEmpty()) { + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.DESC); + } + + BooleanInstruction instruction = searchInstructionBuilder.build(); + return this.contentManager.nestedMatchSearch(instruction, startIndex, limit, null, sortInstructions); + } + + /** + * Logic for the /events/overview endpoint to provide a list of events with summary information. + * + * @param startIndex - the initial index for the first result. + * @param limit - the maximums number of results to return + * @param filter - in which way should the results be filtered from a choice defined in the EventFilterOption enum. + * @param currentUser - the currently logged-in user (must be event leader or above). + * @return a ResultsWrapper containing a list of filtered events with summary information. + */ + public ResultsWrapper> getEventOverviews(final Integer startIndex, final Integer limit, + final String filter, final RegisteredUserDTO currentUser) + throws ContentManagerException, SegueDatabaseException { + + IsaacSearchInstructionBuilder searchInstructionBuilder = this.contentManager.getBaseSearchInstructionBuilder() + .includeContentTypes(Collections.singleton(EVENT_TYPE)); + + final Map sortInstructions = Maps.newHashMap(); + + if (filter != null) { + Constants.EventFilterOption filterOption = Constants.EventFilterOption.valueOf(filter); + searchInstructionBuilder.setEventFilterOption(filterOption); + if (filterOption.equals(Constants.EventFilterOption.FUTURE)) { + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.ASC); + } + } + + if (sortInstructions.isEmpty()) { + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.DESC); + } + + BooleanInstruction instruction = searchInstructionBuilder.build(); + ResultsWrapper findByFieldNames = this.contentManager.nestedMatchSearch(instruction, startIndex, + limit, null, sortInstructions); + + List> resultList = Lists.newArrayList(); + + for (ContentDTO c : findByFieldNames.getResults()) { + if (!(c instanceof IsaacEventPageDTO)) { + continue; + } + IsaacEventPageDTO event = (IsaacEventPageDTO) c; + + if (null == currentUser || !bookingManager.isUserAbleToManageEvent(currentUser, event)) { + continue; + } + + ImmutableMap.Builder eventOverviewBuilder = new ImmutableMap.Builder<>(); + eventOverviewBuilder.put("id", event.getId()); + eventOverviewBuilder.put("title", event.getTitle()); + eventOverviewBuilder.put("subtitle", event.getSubtitle()); + eventOverviewBuilder.put("date", event.getDate()); + eventOverviewBuilder.put("bookingDeadline", + event.getBookingDeadline() == null ? event.getDate() : event.getBookingDeadline()); + eventOverviewBuilder.put("eventStatus", event.getEventStatus()); + + if (null != event.getLocation()) { + eventOverviewBuilder.put("location", event.getLocation()); + } + + Map bookingCounts = this.bookingManager.getBookingStatusCountsByEventId(event.getId()); + eventOverviewBuilder.put("numberOfConfirmedBookings", + bookingCounts.getOrDefault(BookingStatus.CONFIRMED, 0L)); + eventOverviewBuilder.put("numberOfWaitingListBookings", + bookingCounts.getOrDefault(BookingStatus.WAITING_LIST, 0L)); + eventOverviewBuilder.put("numberAttended", + bookingCounts.getOrDefault(BookingStatus.ATTENDED, 0L)); + eventOverviewBuilder.put("numberAbsent", + bookingCounts.getOrDefault(BookingStatus.ABSENT, 0L)); + + if (null != event.getNumberOfPlaces()) { + eventOverviewBuilder.put("numberOfPlaces", event.getNumberOfPlaces()); + } + + resultList.add(eventOverviewBuilder.build()); + } + return new ResultsWrapper<>(resultList, findByFieldNames.getTotalResults()); + } + + /** + * Logic for the /events/map_data endpoint to provide a list of events suitable for mapping. + * + * @param tags - a comma separated list of tags to include in the search. + * @param startIndex - the initial index for the first result. + * @param limit - the maximum number of results to return. + * @param showActiveOnly - true will impose filtering on the results. False will not. Defaults to false. + * @param showStageOnly - if present, only events with an audience matching this string will be shown. + * @return a ResultsWrapper containing a list of event map summaries. + */ + public ResultsWrapper> getEventMapData(final String tags, final Integer startIndex, + final Integer limit, final Boolean showActiveOnly, + final String showStageOnly) + throws ContentManagerException { + + IsaacSearchInstructionBuilder searchInstructionBuilder = this.contentManager.getBaseSearchInstructionBuilder() + .includeContentTypes(Collections.singleton(EVENT_TYPE)); + + if (tags != null) { + searchInstructionBuilder.searchFor(new SearchInField(TAGS_FIELDNAME, + Arrays.stream(tags.split(",")).collect(Collectors.toSet()))); + } + + if (showStageOnly != null) { + searchInstructionBuilder.searchFor(new SearchInField(STAGE_FIELDNAME, + Arrays.stream(showStageOnly.split(",")).collect(Collectors.toSet()))); + } + + final Map sortInstructions = Maps.newHashMap(); + + if (null == showActiveOnly || showActiveOnly) { + // Should default to future events only, but set this explicitly anyway + searchInstructionBuilder.setEventFilterOption(Constants.EventFilterOption.FUTURE); + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.ASC); + } else { + searchInstructionBuilder.setEventFilterOption(Constants.EventFilterOption.ALL); + } + + if (sortInstructions.isEmpty()) { + sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.DESC); + } + + BooleanInstruction instruction = searchInstructionBuilder.build(); + ResultsWrapper findByFieldNames = this.contentManager.nestedMatchSearch(instruction, startIndex, + limit, null, sortInstructions); + + List> resultList = Lists.newArrayList(); + + for (ContentDTO c : findByFieldNames.getResults()) { + if (!(c instanceof IsaacEventPageDTO)) { + continue; + } + + IsaacEventPageDTO e = (IsaacEventPageDTO) c; + if (null == e.getLocation() || (null == e.getLocation().getLatitude() && null == e.getLocation().getLongitude())) { + // Ignore events without locations. + continue; + } + if (e.getLocation().getLatitude().equals(0.0) && e.getLocation().getLongitude().equals(0.0)) { + // Ignore events with locations that haven't been set properly. + log.info("Event with 0.0 lat/long: " + e.getId()); + continue; + } + + ImmutableMap.Builder eventOverviewBuilder = new ImmutableMap.Builder<>(); + eventOverviewBuilder.put("id", e.getId()); + eventOverviewBuilder.put("title", e.getTitle()); + eventOverviewBuilder.put("date", e.getDate()); + eventOverviewBuilder.put("subtitle", e.getSubtitle()); + if (e.getEventStatus() != null) { + eventOverviewBuilder.put("status", e.getEventStatus()); + } + // The schema required needs lat and long at top-level, so add address at top-level too. + eventOverviewBuilder.put("address", e.getLocation().getAddress()); + eventOverviewBuilder.put("latitude", e.getLocation().getLatitude()); + eventOverviewBuilder.put("longitude", e.getLocation().getLongitude()); + + if (null != e.getBookingDeadline()) { + eventOverviewBuilder.put("deadline", e.getBookingDeadline()); + } + + resultList.add(eventOverviewBuilder.build()); + } + + return new ResultsWrapper<>(resultList, findByFieldNames.getTotalResults()); + } + + /** + * Get Events Booked by user. + * + * @param tags - the tags we want to filter on + * @param currentUser - the currently logged on user. + * @return a list of event pages that the user has been booked + * @throws SegueDatabaseException + * @throws ContentManagerException + */ + public ResultsWrapper getEventsBookedByUser(final List tags, + final RegisteredUserDTO currentUser) + throws SegueDatabaseException, ContentManagerException { + List filteredResults = Lists.newArrayList(); + + Map userBookingMap = this.bookingManager.getAllEventStatesForUser(currentUser.getId()); + + for (String eventId : userBookingMap.keySet()) { + if (BookingStatus.CANCELLED.equals(userBookingMap.get(eventId))) { + continue; + } + + final IsaacEventPageDTO eventDTOById = this.getAugmentedEventDTOById(currentUser, eventId); + + if (tags != null) { + Set tagsList = Sets.newHashSet(tags); + tagsList.retainAll(eventDTOById.getTags()); // get intersection + if (tagsList.isEmpty()) { + // if the intersection is empty then we can continue + continue; + } + } + + filteredResults.add(eventDTOById); + } + return new ResultsWrapper<>(filteredResults, (long) filteredResults.size()); + } + + /** + * Get Events Reserved by user. + * + * @param currentUser - the currently logged on user. + * @return a list of event pages that the user has been booked + * @throws SegueDatabaseException + * @throws ContentManagerException + */ + public ResultsWrapper getEventsReservedByUser(final RegisteredUserDTO currentUser) + throws SegueDatabaseException, ContentManagerException { + List filteredResults = Lists.newArrayList(); + + List userReservationList = this.mapper.mapToListOfEventBookingDTO(bookingManager.getAllEventReservationsForUser(currentUser.getId())); + + for (EventBookingDTO booking : userReservationList) { + + final IsaacEventPageDTO eventDTOById = this.getAugmentedEventDTOById(currentUser, booking.getEventId()); + + filteredResults.add(eventDTOById); + } + return new ResultsWrapper<>(filteredResults, (long) filteredResults.size()); + } + + /** + * A helper method for retrieving an event and the number of places available and if the user is booked or not. + * + * @param eventId the id of the event of interest + * @return the fully populated event dto with user context information. + * @throws ContentManagerException - if there is a problem finding the event information + * @throws SegueDatabaseException if there is a database error. + */ + public IsaacEventPageDTO getAugmentedEventDTOById(final RegisteredUserDTO currentUser, final String eventId) + throws ContentManagerException, SegueDatabaseException { + IsaacEventPageDTO event = getRawEventDTOById(eventId); + return augmentEventWithBookingInformation(currentUser, event); + } + + /** + * Augment a single event with booking information before we send it out. + * + * @param possibleEvent - a ContentDTO that should hopefully be an IsaacEventPageDTO. + * @return an augmented IsaacEventPageDTO. + * @throws SegueDatabaseException + */ + public IsaacEventPageDTO augmentEventWithBookingInformation(final RegisteredUserDTO currentUser, + final ContentDTO possibleEvent) + throws SegueDatabaseException { + if (possibleEvent instanceof IsaacEventPageDTO) { + IsaacEventPageDTO page = (IsaacEventPageDTO) possibleEvent; + + if (null != currentUser) { + page.setUserBookingStatus(this.bookingManager.getBookingStatus(page.getId(), currentUser.getId())); + } else { + page.setUserBookingStatus(null); + } + + page.setPlacesAvailable(this.bookingManager.getPlacesAvailable(page)); + return page; + } else { + throw new ClassCastException("The object provided was not an event."); + } + } + + /** + * A helper method for retrieving an event object without augmented information + * + * @param eventId the id of the event of interest + * @return the fully populated event dto with user context information. + * @throws ContentManagerException - if there is a problem finding the event information + * @throws SegueDatabaseException if there is a database error. + */ + public IsaacEventPageDTO getRawEventDTOById(final String eventId) + throws ContentManagerException, SegueDatabaseException { + + ContentDTO possibleEvent = this.contentManager.getContentById(eventId); + + if (null == possibleEvent) { + throw new ResourceNotFoundException(String.format("Unable to locate the event with id; %s", eventId)); + } + + if (possibleEvent instanceof IsaacEventPageDTO) { + // The Events Facade *mutates* the EventDTO returned by this method; we must return a copy of + // the original object else we will poison the contentManager's cache! + // TODO: might it be better to get the DO from the cache and map it to DTO here to reduce overhead? + IsaacEventPageDTO eventPageDTO = (IsaacEventPageDTO) possibleEvent; + return mapper.copy(eventPageDTO); + } + return null; + } +} diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java index 8ce6336581..62e6b54455 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java @@ -460,7 +460,6 @@ public enum SegueServerLogType implements LogType { public static final String CATEGORIES_FIELDNAME = "categories"; public static final String LEVEL_FIELDNAME = "level"; public static final String SUMMARY_FIELDNAME = "summary"; - public static final String DATE_FIELDNAME = "date"; public static final String ADDRESS_PSEUDO_FIELDNAME = "address"; public static final String[] ADDRESS_PATH_FIELDNAME = {"location", "address"}; public static final String[] ADDRESS_FIELDNAMES = {"addressLine1", "addressLine2", "town", "county", "postalCode"}; @@ -503,7 +502,7 @@ public enum SegueServerLogType implements LogType { * Enum to represent filter values for event management. */ public enum EventFilterOption { - FUTURE, RECENT, PAST + FUTURE, RECENT, PAST, ALL } public static final String ID_SEPARATOR = "|"; diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java index 550f465eb4..d3b7799ffd 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java @@ -42,6 +42,7 @@ import uk.ac.cam.cl.dtg.segue.api.Constants; import uk.ac.cam.cl.dtg.segue.database.GitDb; import uk.ac.cam.cl.dtg.segue.search.AbstractFilterInstruction; +import uk.ac.cam.cl.dtg.segue.search.BooleanInstruction; import uk.ac.cam.cl.dtg.segue.search.ISearchProvider; import uk.ac.cam.cl.dtg.segue.search.IsaacSearchInstructionBuilder; import uk.ac.cam.cl.dtg.segue.search.IsaacSearchInstructionBuilder.Priority; @@ -81,8 +82,6 @@ public class GitContentManager { private static final Logger log = LoggerFactory.getLogger(GitContentManager.class); - private static final String CONTENT_TYPE = "content"; - private final GitDb database; private final ContentMapper mapper; private final ContentSubclassMapper contentSubclassMapper; @@ -255,7 +254,8 @@ public final Content getContentDOById(final String id, final boolean failQuietly ResultsWrapper rawResults = searchProvider.termSearch( contentIndex, - CONTENT_TYPE, id, + CONTENT_INDEX_TYPE.CONTENT.toString(), + id, Constants.ID_FIELDNAME + "." + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, 0, 1, getBaseFilters()); List searchResults = contentSubclassMapper @@ -312,7 +312,7 @@ public ResultsWrapper getUnsafeCachedContentDTOsMatchingIds(final Co ResultsWrapper searchHits = this.searchProvider.termSearch( contentIndex, - CONTENT_TYPE, + CONTENT_INDEX_TYPE.CONTENT.toString(), null, null, startIndex, @@ -386,7 +386,7 @@ public final ResultsWrapper siteWideSearch( // Event specific queries .searchFor(new SearchInField(Constants.ADDRESS_PSEUDO_FIELDNAME, searchTerms)) - .includePastEvents(false); + .setEventFilterOption(EventFilterOption.FUTURE); // If no search terms were provided, sort by ascending alphabetical order of title. Map sortOrder = null; @@ -400,7 +400,7 @@ public final ResultsWrapper siteWideSearch( ResultsWrapper searchHits = searchProvider.nestedMatchSearch( contentIndex, - CONTENT_TYPE, + CONTENT_INDEX_TYPE.CONTENT.toString(), startIndex, limit, searchInstructionBuilder.build(), @@ -518,7 +518,7 @@ public final ResultsWrapper questionSearch( ResultsWrapper searchHits = searchProvider.nestedMatchSearch( contentIndex, - CONTENT_TYPE, + CONTENT_INDEX_TYPE.CONTENT.toString(), startIndex, limit, searchInstructionBuilder.build(), @@ -531,12 +531,14 @@ public final ResultsWrapper questionSearch( return new ResultsWrapper<>(contentSubclassMapper.getDTOByDOList(searchResults), searchHits.getTotalResults()); } + @Deprecated public final ResultsWrapper findByFieldNames( final List fieldsToMatch, final Integer startIndex, final Integer limit ) throws ContentManagerException { return this.findByFieldNames(fieldsToMatch, startIndex, limit, null); } + @Deprecated public final ResultsWrapper findByFieldNames( final List fieldsToMatch, final Integer startIndex, final Integer limit, @Nullable final Map sortInstructions @@ -544,6 +546,7 @@ public final ResultsWrapper findByFieldNames( return this.findByFieldNames(fieldsToMatch, startIndex, limit, sortInstructions, null); } + @Deprecated public final ResultsWrapper findByFieldNames( final List fieldsToMatch, final Integer startIndex, final Integer limit, @Nullable final Map sortInstructions, @@ -569,8 +572,9 @@ public final ResultsWrapper findByFieldNames( newFilterInstructions.putAll(this.getBaseFilters()); } - ResultsWrapper searchHits = searchProvider.matchSearch(contentIndex, CONTENT_TYPE, fieldsToMatch, - startIndex, limit, newSortInstructions, newFilterInstructions); + ResultsWrapper searchHits = searchProvider.matchSearch(contentIndex, + CONTENT_INDEX_TYPE.CONTENT.toString(), fieldsToMatch, startIndex, limit, + newSortInstructions, newFilterInstructions); // setup object mapper to use pre-configured deserializer module. // Required to deal with type polymorphism @@ -600,7 +604,8 @@ public final ResultsWrapper findByFieldNamesRandomOrder( ResultsWrapper searchHits; searchHits = searchProvider.randomisedMatchSearch( - contentIndex, CONTENT_TYPE, fieldsToMatch, startIndex, limit, randomSeed, this.getBaseFilters()); + contentIndex, CONTENT_INDEX_TYPE.CONTENT.toString(), fieldsToMatch, startIndex, limit, + randomSeed, this.getBaseFilters()); // setup object mapper to use pre-configured deserializer module. // Required to deal with type polymorphism @@ -799,6 +804,38 @@ public String getCurrentContentSHA() { } } + /** + * Get a search instruction builder initialised with the base configuration for this content manager. + * + * @return a base search instruction builder. + */ + public IsaacSearchInstructionBuilder getBaseSearchInstructionBuilder() { + return new IsaacSearchInstructionBuilder( + searchProvider, this.showOnlyPublishedContent, this.hideRegressionTestContent, true); + } + + /** + * Search for content that matches a given instruction and map the hits to DTOs. + * + * @param instruction - the {@link BooleanInstruction} to search with. + * @param startIndex - the initial index for the first result. + * @param limit - the maximum number of results to return. + * @param randomSeed - the random seed to use for the search. + * @param sortInstructions - map of sorting functions to use in ElasticSearch query. + * @return a ResultsWrapper containing the matching content as DTOs and the total number of results. + * @throws ContentManagerException + */ + public ResultsWrapper nestedMatchSearch(final BooleanInstruction instruction, final Integer startIndex, + final Integer limit, final Long randomSeed, + final Map sortInstructions) + throws ContentManagerException { + ResultsWrapper searchHits = this.searchProvider.nestedMatchSearch(contentIndex, + CONTENT_INDEX_TYPE.CONTENT.toString(), startIndex, limit, instruction, randomSeed, sortInstructions); + List searchResults = this.contentSubclassMapper.mapFromStringListToContentList(searchHits.getResults()); + List dtoResults = this.contentSubclassMapper.getDTOByDOList(searchResults); + return new ResultsWrapper<>(dtoResults, searchHits.getTotalResults()); + } + /** * Returns the basic filter configuration. * diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/search/ISearchProvider.java b/src/main/java/uk/ac/cam/cl/dtg/segue/search/ISearchProvider.java index 95586b4400..68c13a9d28 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/search/ISearchProvider.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/search/ISearchProvider.java @@ -69,6 +69,7 @@ public interface ISearchProvider { * - the map of how to sort each field of interest. * @return Results */ + @Deprecated ResultsWrapper matchSearch( final String indexBase, final String indexType, final List fieldsToMatch, final int startIndex, final int limit, @@ -160,6 +161,7 @@ ResultsWrapper termSearch( * - post search filter instructions e.g. remove content of a certain type. * @return results in a random order for a given match search. */ + @Deprecated ResultsWrapper randomisedMatchSearch( String indexBase, String indexType, List fieldsToMatch, int startIndex, int limit, Long randomSeed, Map filterInstructions diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java b/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java index 571a26b3ee..6e5d39fa18 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java @@ -41,9 +41,9 @@ public class IsaacSearchInstructionBuilder { private final boolean includeOnlyPublishedContent; private final boolean excludeRegressionTestContent; - private final boolean excludeNofilterContent; + private boolean excludeNofilterContent; private boolean excludeSupersededContent; - private boolean includePastEvents; + private Constants.EventFilterOption eventFilterOption; private Set includedContentTypes; private Set priorityIncludedContentTypes; @@ -119,7 +119,7 @@ public IsaacSearchInstructionBuilder(final ISearchProvider searchProvider, this.excludeRegressionTestContent = excludeRegressionTestContent; this.excludeNofilterContent = excludeNofilterContent; - this.includePastEvents = false; + this.eventFilterOption = Constants.EventFilterOption.FUTURE; this.excludeSupersededContent = false; } @@ -201,14 +201,14 @@ public IsaacSearchInstructionBuilder searchFor(final SearchInField searchInField } /** - * Sets whether to return events where the date field contains a date in the past. Defaults to false, and has no - * effect if the event content type is excluded. + * Sets a filter for events in a given date range (future/recent/past/all). Defaults to future, and has no effect if + * the event content type is excluded. * - * @param includePastEvents Whether to include past events. + * @param eventFilterOption The date option to filter events by. * @return This {@link IsaacSearchInstructionBuilder}, to allow chained operations. */ - public IsaacSearchInstructionBuilder includePastEvents(final boolean includePastEvents) { - this.includePastEvents = includePastEvents; + public IsaacSearchInstructionBuilder setEventFilterOption(final Constants.EventFilterOption eventFilterOption) { + this.eventFilterOption = eventFilterOption; return this; } @@ -217,6 +217,17 @@ public IsaacSearchInstructionBuilder excludeSupersededContent(final boolean excl return this; } + /** + * Sets whether to include content tagged with "nofilter" in the results. Defaults to excluding such content. + * + * @param includeHiddenContent Whether to include nofilter content in the results. + * @return This IsaacSearchInstructionBuilder, to allow chained operations. + */ + public IsaacSearchInstructionBuilder includeHiddenContent(final boolean includeHiddenContent) { + this.excludeNofilterContent = !includeHiddenContent; + return this; + } + /** * Builds and returns the final BooleanInstruction reflecting the builder's settings. * @@ -248,12 +259,23 @@ public BooleanInstruction build() { contentInstruction.must(new MatchInstruction(Constants.TAGS_FIELDNAME, SEARCHABLE_TAG)); } - // Optionally add instruction to match only events that have not yet taken place - if (contentType.equals(EVENT_TYPE) && !includePastEvents) { + // Optionally add instructions to match only events in a particular date range + if (contentType.equals(EVENT_TYPE)) { LocalDate today = LocalDate.now(); long now = today.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * Constants.EVENT_DATE_EPOCH_MULTIPLIER; - contentInstruction.must(new RangeInstruction(Constants.DATE_FIELDNAME).greaterThanOrEqual(now)); + // Default to showing only future events + if (null == this.eventFilterOption || this.eventFilterOption == Constants.EventFilterOption.FUTURE) { + contentInstruction.must(new RangeInstruction(ENDDATE_FIELDNAME).greaterThanOrEqual(now)); + } else if (this.eventFilterOption == Constants.EventFilterOption.RECENT) { + long oneMonthAgo = today.minusMonths(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond() + * Constants.EVENT_DATE_EPOCH_MULTIPLIER; + contentInstruction.must(new RangeInstruction(ENDDATE_FIELDNAME).greaterThanOrEqual(oneMonthAgo)); + contentInstruction.must(new RangeInstruction(ENDDATE_FIELDNAME).lessThanOrEqual(now)); + } else if (this.eventFilterOption == Constants.EventFilterOption.PAST) { + contentInstruction.must(new RangeInstruction(ENDDATE_FIELDNAME).lessThanOrEqual(now)); + } + // else eventFilterOption == ALL, so don't filter } // Apply instructions to search for specific terms in specific fields @@ -276,7 +298,7 @@ public BooleanInstruction build() { this.searchesInFields = new ArrayList<>(); this.includedContentTypes = Sets.newHashSet(); this.priorityIncludedContentTypes = Sets.newHashSet(); - this.includePastEvents = false; + this.eventFilterOption = Constants.EventFilterOption.FUTURE; return masterInstruction; } diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java index a5da974966..f863965d2e 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java @@ -16,6 +16,7 @@ import org.testcontainers.utility.MountableFile; import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; +import uk.ac.cam.cl.dtg.isaac.api.managers.EventsManager; import uk.ac.cam.cl.dtg.isaac.api.managers.FastTrackManger; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizAssignmentManager; @@ -137,6 +138,7 @@ public class AbstractIsaacIntegrationTest { protected static GameManager gameManager; protected static GroupManager groupManager; protected static EventBookingManager eventBookingManager; + protected static EventsManager eventsManager; protected static ILogManager logManager; protected static GitContentManager contentManager; protected static UserAssociationManager userAssociationManager; @@ -304,6 +306,7 @@ public static void setUpClass() throws Exception { userAssociationManager = new UserAssociationManager(pgAssociationDataManager, userAccountManager, groupManager); PgTransactionManager pgTransactionManager = new PgTransactionManager(postgresSqlDb); eventBookingManager = new EventBookingManager(bookingPersistanceManager, emailManager, userAssociationManager, properties, groupManager, userAccountManager, pgTransactionManager); + eventsManager = new EventsManager(eventBookingManager, contentManager, mainMapper); assignmentManager = new AssignmentManager(assignmentPersistenceManager, groupManager, new EmailService(properties, emailManager, groupManager, userAccountManager, mailGunEmailManager), gameManager, properties); schoolListReader = createNiceMock(SchoolListReader.class); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeIT.java index b1cb25db7d..218c977d98 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeIT.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeIT.java @@ -35,19 +35,19 @@ public class EventsFacadeIT extends IsaacIntegrationTest { @BeforeEach public void setUp() { // Get an instance of the facade to test - eventsFacade = new EventsFacade(properties, logManager, eventBookingManager, userAccountManager, contentManager, + eventsFacade = new EventsFacade(properties, logManager, eventsManager, eventBookingManager, userAccountManager, userAssociationManager, groupManager, userAccountManager, schoolListReader, mainMapper); } @Test - // GET /events -> EventFacade::getEvents(request, tags, startIndex, limit, sortOrder, showActiveOnly, showInactiveOnly, showMyBookingsOnly, showReservationsOnly, showStageOnly) + // GET /events -> EventFacade::getEvents(request, tags, startIndex, limit, sortOrder, showActiveOnly, showMyBookingsOnly, showReservationsOnly, showStageOnly) public void getEventsTest() { // Create an anonymous request (this is a mocked object) HttpServletRequest request = createRequestWithCookies(new Cookie[]{}); replay(request); // Execute the method (endpoint) to be tested - Response response = eventsFacade.getEvents(request, null, 0, 10, null, null, null, null, null, null); + Response response = eventsFacade.getEvents(request, null, 0, 10, null, null, null, null, null); // Check that the request succeeded assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); // Fetch the entity object. This can be anything, so we declare it first as Object