Java tutorial
/* Educational Online Test Delivery System Copyright (c) 2013 American Institutes for Research Distributed under the AIR Open Source License, Version 1.0 See accompanying file AIR-License-1_0.txt or at http://www.smarterapp.org/documents/American_Institutes_for_Research_Open_Source_Software_License.pdf */ package org.opentestsystem.delivery.testadmin.scheduling; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.joda.time.DateTime; import org.joda.time.Interval; import org.opentestsystem.delivery.testadmin.domain.AccessibilityEquipment; import org.opentestsystem.delivery.testadmin.domain.Affinity; import org.opentestsystem.delivery.testadmin.domain.Affinity.AffinityType; import org.opentestsystem.delivery.testadmin.domain.Facility; import org.opentestsystem.delivery.testadmin.domain.FacilityAvailability; import org.opentestsystem.delivery.testadmin.domain.Proctor; import org.opentestsystem.delivery.testadmin.domain.ProctorRole; import org.opentestsystem.delivery.testadmin.domain.schedule.Schedule; import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduleCreationInfo; import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduleTestStatus; import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledSeat; import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledStudent; import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledTimeSlot; import org.opentestsystem.delivery.testadmin.domain.schedule.SchedulerValidationError; import org.opentestsystem.delivery.testadmin.domain.schedule.SchedulerValidationError.ErrorType; import org.opentestsystem.delivery.testadmin.domain.search.AccessibilityEquipmentSearchRequest; import org.opentestsystem.delivery.testadmin.persistence.ProctorRepository; import org.opentestsystem.delivery.testadmin.persistence.ProctorRoleRepository; import org.opentestsystem.delivery.testadmin.service.AccessibilityEquipmentService; import org.opentestsystem.delivery.testadmin.service.FacilityAvailabilityService; import org.opentestsystem.delivery.testadmin.service.FacilityService; import org.opentestsystem.delivery.testreg.domain.Assessment; import org.opentestsystem.delivery.testreg.domain.Assessment.TestWindow; import org.opentestsystem.delivery.testreg.domain.EligibleStudent; import org.opentestsystem.delivery.testreg.domain.FormatType; import org.opentestsystem.delivery.testreg.domain.Sb11Entity; import org.opentestsystem.delivery.testreg.domain.Student.GradeLevel; import org.opentestsystem.delivery.testreg.domain.StudentGroup; import org.opentestsystem.delivery.testreg.persistence.EligibleStudentRepository; import org.opentestsystem.delivery.testreg.service.StudentGroupService; import org.opentestsystem.delivery.testreg.service.TestRegPersister; import org.opentestsystem.shared.search.domain.SearchResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * Helper class that contains most of the scheduling logic */ @Component public class SchedulerHelper { private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerHelper.class); @Autowired private FacilityService facilityService; @Autowired private FacilityAvailabilityService facilityAvailabilityService; @Autowired private EligibleStudentRepository eligStudentRepository; @Autowired private ProctorRoleRepository proctorRoleRepository; @Autowired private StudentGroupService studentGroupService; @Autowired private TestRegPersister testRegPersister; @Autowired private ProctorRepository proctorRepository; @Autowired private ScheduleTestStatusCreator scheduleTestStatusCreator; @Autowired private AccessibilityEquipmentService accessibilityEquipmentService; private static final String ACCESS_EQUIP_ERROR_TEMPLATE = "Student %1$s was not allocated to required equipment %2$s for assessment %3$s"; private static final String NOT_SCHEDULED_FOR_ASSESSMENT_ERROR_TEMPLATE = "Student %1$s was not scheduled for assessment %2$s"; private static final String NO_PROCTOR_ERROR_TEMPLATE = "No proctor found for the test slot that starts on %1$tD %1$tR and ends on %2$tD %2$tR"; /** * Gets Facility and FacilityAvailability data for the given institution and merges them together using the * FacilityData class * * @param institutionId * @return */ public Map<String, FacilityData> findFacilityData(final String institutionId) { final Map<String, FacilityData> facilityDatas = new HashMap<String, FacilityData>(); // get Facilities for Institution final List<Facility> facilities = this.facilityService.getFacilities(institutionId); for (final Facility facility : facilities) { final FacilityData facilityData = new FacilityData(); facilityData.setFacilityId(facility.getId()); facilityData.setFacility(facility); // get availabilities for facility and institution final List<FacilityAvailability> availabilities = this.facilityAvailabilityService .getAvailabilities(facility.getId(), facility.getInstitutionIdentifier()); facilityData.setAvailabilities(availabilities); facilityDatas.put(facility.getId(), facilityData); } return facilityDatas; } /** * Finds all Students associated to the given institution * * @param institutionId * @return */ public List<EligibleStudent> findStudents(final String institutionId) { // find all Students for institution return this.eligStudentRepository.findByStudentInstitutionEntityMongoId(institutionId); } /** * Interrogates a List of EligibleStudents to find a unique set of Assessments for all of the Students eligible. * * @param eligibleStudents * @param startDate * @param endDate * @return */ public List<Assessment> findAssessments(final List<EligibleStudent> eligibleStudents, final DateTime startDate, final DateTime endDate, final List<String> tenantIdsForAssessment) { // the assessments to schedule are the assessments with a test window at least partially within the schedule // dates // taken from the EligibleStudents // use a set to avoid duplicate Assessments // IMPORTANT!!!!!!!!! // This code will add one day to the end date of the schedule and each of the test window end dates // because the Joda Interval makes the end instant EXCLUSIVE // We need to ensure that the interval takes into account that the end dates are part of the interval. // Since all of our schedule dates and test windows use midnight as the time, an end date of 06-02-2014 00:00:00 // will turn into 06-03-2014 00:00:00 and thus the entire day of 06-02 is now part of the interval // !!!!!!!!!!!!!!!!!! final Interval scheduleInterval = new Interval(startDate, endDate.plusDays(1)); Interval testWindowInterval = null; final Set<Assessment> schedulableAssessments = new HashSet<Assessment>(); for (final EligibleStudent eligStudent : eligibleStudents) { for (final Assessment assess : eligStudent.getAssessments()) { for (final TestWindow testWindow : assess.getTestWindow()) { testWindowInterval = new Interval(testWindow.getBeginWindow(), testWindow.getEndWindow().plusDays(1)); if (scheduleInterval.overlaps(testWindowInterval) && tenantIdsForAssessment.contains(assess.getTenantId())) { schedulableAssessments.add(assess); break; } } } } return new ArrayList<Assessment>(schedulableAssessments); } /** * Finds Proctors that are able to Proctor the List of Assessments using the Proctor Role Associations * * @param assessments * @return */ public List<Proctor> findProctors(final List<ProctorRole> proctorRoles, final String associatedEntityId) { // Create new query on ProctorRepository to find Proctors that have a user/role association with the roles // determined from previous step return this.proctorRepository.findByAssociatedRolesAndEntity(proctorRoles, associatedEntityId); } private List<ProctorRole> findProctorRoles(final List<Assessment> assessments) { // find all Proctors that can Proctor assessment types found // get unique assessment types from assessments List above final Set<String> uniqueTypes = new HashSet<String>(); for (final Assessment assess : assessments) { uniqueTypes.add(assess.getType().toUpperCase()); } // Create new query on ProctorRoleRepository to get the Roles that can administer the assessment types final List<ProctorRole> proctorRoles = this.proctorRoleRepository .findByAssessmentTypesIn(new ArrayList<String>(uniqueTypes)); return proctorRoles; } /** * Iterate through the timeslots for this schedule to see if any of them have any affinities. If they have * affinities, these time slots must be scheduled first. * Be aware that the actual scheduling modifies the Schedule argument passed into this function * * @param scheduled */ public void allocateTimeslotAffinities(final Schedule scheduled, final List<Assessment> assessments, final Map<String, ScheduledStudent> studentsScheduled, final HashMultimap<String, Assessment> gradesToAssessments, final boolean reschedule) { for (final ScheduledTimeSlot timeslot : scheduled.getOrderedTimeSlots(reschedule)) { // for each timeslot, if there is an affinity, // check if affinity with strict rule is scheduled // if yes then skip this timeslot // else check if timeslot has any unscheduled seat // if yes then allocate timeslot for that affinity // else skip timeslot if (timeslot.hasAffinities()) { for (final Affinity affinity : timeslot.getAffinities()) { if (!timeslot.isStrictAffinityScheduled() && timeslot.hasUnscheduledSeat()) { allocateTimeslotAffinity(assessments, timeslot, studentsScheduled, scheduled.getId(), affinity, gradesToAssessments); } } } } } /** * Allocate students who are eligible for the assessment to the timeslot * * @param scheduledTimeSlot * @param assessment * @param studentsScheduled */ public boolean allocateToTimeslot(final ScheduledTimeSlot scheduledTimeSlot, final Assessment assessment, final Map<String, ScheduledStudent> studentsScheduled, final boolean priorityAllocation, final String scheduleId, final HashMultimap<String, Assessment> gradesToAssessments) { boolean studentAssigned = false; if (scheduledTimeSlot.hasUnscheduledSeat()) { // must check to see if this time slot has a strict affinity scheduled to it // if so, then we double check to see if the assessment passed in satisfies that strict affinity // if it does, then go ahead with scheduling, if it doesn't then we cannot schedule if (!scheduledTimeSlot.isStrictAffinityScheduled() || scheduledTimeSlot.isStrictAffinityScheduled() && doesAssessmentFulfillAffinity(scheduledTimeSlot, assessment, gradesToAssessments)) { // get unscheduled seats with equipment final Set<ScheduledSeat> seatsWithEquipment = scheduledTimeSlot .getUnscheduledSeatsWithAccessbilityEquip(); // for each seat with equipment for (final ScheduledSeat nextSeatWithEquip : seatsWithEquipment) { // does the seat have accessiblity equipment objects loaded yet? // if not, load them // if so, skip the load and continue loadAccessibilityEquipmentObj(nextSeatWithEquip); final List<ScheduledStudent> eligibleStudents = findStudentsEligibleForAssessmentAndEquipment( studentsScheduled, assessment, nextSeatWithEquip.getAccessibilityEquipmentObjs()); // for each student for (final ScheduledStudent studentToSchedule : eligibleStudents) { // if not scheduled for assessment && able to use seat equipment? if (!studentToSchedule.isStudentScheduled(assessment) && studentToSchedule.canUseAccessibilityEquipment( nextSeatWithEquip.getAccessibilityEquipmentObjs(), assessment)) { // then schedule for seat studentAssigned = assignStudentToSeat(scheduledTimeSlot, assessment, nextSeatWithEquip, Lists.newArrayList(studentToSchedule), scheduleId); if (studentAssigned) { studentToSchedule.scheduledToEquipment(assessment, nextSeatWithEquip.getAccessibilityEquipmentObjs()); } // break break; } } } // end iteration of seats with equipment // we have scheduled any students that qualify for seats with equipment // now add any student to any seat // get all unscheduled seats final Set<ScheduledSeat> unscheduledSeats = scheduledTimeSlot.getAllUnscheduledSeats(); // for each seat for (final ScheduledSeat nextSeat : unscheduledSeats) { final List<ScheduledStudent> eligibleStudents = findStudentsEligibleForAssessment( studentsScheduled, assessment); // for each student for (final ScheduledStudent studentToSchedule : eligibleStudents) { // if student not scheduled for assessment if (!studentToSchedule.isStudentScheduled(assessment)) { // then schedule for seat studentAssigned = assignStudentToSeat(scheduledTimeSlot, assessment, nextSeat, com.google.common.collect.Lists.newArrayList(studentToSchedule), scheduleId); // break break; } } } } } return studentAssigned; } /** * Allocate students to seats based on schedule-wide priority rules. * Be aware that the actual scheduling modifies the Schedule argument passed into this function * * @param scheduled */ public void allocateWithPriorities(final Schedule scheduled, final List<Assessment> assessments, final Map<String, ScheduledStudent> studentsScheduled, final HashMultimap<String, Assessment> gradesToAssessments, final HashMultimap<String, Assessment> studentGroupToAssessment, final boolean reschedule) { // iterate through affinities in the order // find assessments, but only schedule a single assessment at once if (scheduled.hasPriorityAllocationRules()) { for (final Affinity affinity : scheduled.getAffinities()) { final Collection<Assessment> affinityAssessments = filterAssessmentByAffinity(assessments, affinity.getType(), affinity.getValue(), gradesToAssessments, studentGroupToAssessment); for (final Assessment assessmentToSchedule : affinityAssessments) { allocateToScheduleAffinity(scheduled, studentsScheduled, assessmentToSchedule, affinity, gradesToAssessments, reschedule); } } } } /** * Generic allocation algorithm used when no other affinities or priorities are in effect. * Be aware that the actual scheduling modifies the Schedule argument passed into this function * * @param scheduled */ public void allocateAll(final Schedule scheduled, final List<Assessment> assessments, final Map<String, ScheduledStudent> studentsScheduled, final HashMultimap<String, Assessment> gradesToAssessments, final boolean reschedule) { // look at the students that still need to be scheduled, do any of them require accommodations? // -- if so then determine the distinct assessments those students need to be scheduled for // ---- get next assessment // ------ get next time slot with empty seats // -------- are there still students with accommodations to be allocated? // ---------- if yes then, are there any empty seats with accessibility equipment? // ------------ if yes then run accessibility rules to see if equipment matches up with students that need // accommodations // -------------- does equipment match up? // ---------------- if yes, then allocate the student to the seat, get next seat and repeat // ---------------- if no, then get next seat and repeat // ------------ if no then get next time slot and repeat // ---------- if no, then are there other students for this assessment that need to be scheduled? // ------------ if yes then allocate students to any empty seats, loop back to students with accommodations // check // ------------ if no, then loop back to get next assessment // -- when no students are left to schedule, then done Map<String, ScheduledStudent> studentsToBeScheduled = null; // for assessment in assessments for (final Assessment curAssessment : assessments) { // guava filter to get ScheduledStudents that need to be scheduled for assessment studentsToBeScheduled = ImmutableMap .copyOf(Maps.filterValues(studentsScheduled, new Predicate<ScheduledStudent>() { @Override public boolean apply(final ScheduledStudent schedStudent) { return schedStudent.isEligibleForAssessment(curAssessment) && !schedStudent.isStudentScheduled(curAssessment); } })); // get ordered time slot itr final TreeSet<ScheduledTimeSlot> timeSlots = scheduled.getOrderedTimeSlots(reschedule); // for each time slot for (final ScheduledTimeSlot nextTimeSlot : timeSlots) { // if there are no students to schedule for assessment anymore, then break here if (allStudentsScheduled(studentsToBeScheduled)) { break; } // if time slot has no affinities we can schedule // if time slot has affinities and no strict affinity was scheduled, then we can schedule // if time slot has affinities and a strict affinity was scheduled, then if the assessment matches the // affinity, we can schedule final boolean canSchedule = nextTimeSlot.isStrictAffinityScheduled() && doesAssessmentFulfillAffinity(nextTimeSlot, curAssessment, gradesToAssessments) || !nextTimeSlot.isStrictAffinityScheduled(); if (canSchedule) { // get unscheduled seats with equipment final Set<ScheduledSeat> seatsWithEquipment = nextTimeSlot .getUnscheduledSeatsWithAccessbilityEquip(); // for each seat with equipment for (final ScheduledSeat nextSeatWithEquip : seatsWithEquipment) { // for each student for (final ScheduledStudent studentToSchedule : studentsToBeScheduled.values()) { // does the seat have accessiblity equipment objects loaded yet? // if not, load them // if so, skip the load and continue loadAccessibilityEquipmentObj(nextSeatWithEquip); // if not scheduled for assessment && able to use seat equipment? if (!studentToSchedule.isStudentScheduled(curAssessment) && studentToSchedule.canUseAccessibilityEquipment( nextSeatWithEquip.getAccessibilityEquipmentObjs(), curAssessment)) { // then schedule for seat final boolean assigned = assignStudentToSeat(nextTimeSlot, curAssessment, nextSeatWithEquip, Lists.newArrayList(studentToSchedule), scheduled.getId()); if (assigned) { studentToSchedule.scheduledToEquipment(curAssessment, nextSeatWithEquip.getAccessibilityEquipmentObjs()); } // break break; } } } // end iteration of seats with equipment // we have scheduled any students that qualify for seats with equipment // now add any student to any seat // get all unscheduled seats final Set<ScheduledSeat> unscheduledSeats = nextTimeSlot.getAllUnscheduledSeats(); // for each seat for (final ScheduledSeat nextSeat : unscheduledSeats) { // for each student for (final ScheduledStudent studentToSchedule : studentsToBeScheduled.values()) { // if student not scheduled for assessment if (!studentToSchedule.isStudentScheduled(curAssessment)) { // then schedule for seat assignStudentToSeat(nextTimeSlot, curAssessment, nextSeat, Lists.newArrayList(studentToSchedule), scheduled.getId()); // break break; } } } } } } } /** * Allocate proctors to all time slots that need a proctor * Be aware that the actual scheduling modifies the Schedule argument passed into this function * * @param scheduled * @param proctors */ public void allocateProctors(final Schedule scheduled, final HashMultimap<String, Assessment> gradesToAssessments, final boolean reschedule) { // iterate through time slots // -- next time slot, does it have a proctor associated? // ---- if yes, then loop to next time slot // ---- if no, then find proctors from list that can proctor the type/types of assessments allocated to this // time slot // ------ iterate through proctors // -------- get proctor availability, is proctor available for entire time slot? // ---------- if yes, does proctor have affinities? // ------------ if yes, then what is the affinity strictness? // -------------- if none or non-exclusive then add proctor to list of possible proctors for this time slot, // loop and get next proctor // -------------- if strict then what affinity type does the proctor have? // ---------------- if assessment, then do(es) the affinity assessment(s) match the assessment(s) for the time // slot? // ------------------ if yes then add proctor to list of possible proctors for this time slot, loop and get next // proctor // ------------------ if no then, loop and get next proctor // ---------------- if subject, then do(es) the affinity subject(s) match the subject(s) on the assessments for // the time slot? // ------------------ if yes then add proctor to list of possible proctors for this time slot, loop and get next // proctor // ------------------ if no then loop and get next proctor // ---------------- if grade, then do(es) the affinity grade(s) match the grade(s) on the students assigned to // the time slot? // ------------------ if yes then add proctor to list of possible proctors for this time slot, loop and get next // proctor // ------------------ if no then loop and get next proctor // ------------ if no, then add proctor to list of possible proctors for this time slot, loop and get next // proctor // ---------- if no, loop and get next proctor // ------ How many proctors are in the list of possible proctors for this time slot? // -------- if 0, Cannot allocate any proctors, rescheduling must occur to move things around (FUTURE, not // defined yet) // -------- if 1, Allocate proctor to this time slot, loop to next time slot // -------- if >1, Check to see how many time slots each proctor is assigned to currently, choose proctor with // least assignments and allocate to this tie slot, loop to next time slot // // all time slots with scheduled students have proctors assigned, done final Map<String, Integer> proctorAllocationCount = new HashMap<String, Integer>(); List<Proctor> possibleProctorsForTimeSlot; Set<Assessment> timeSlotAssessments; final Set<ScheduledTimeSlot> timeSlots = scheduled.getOrderedTimeSlots(reschedule); for (final ScheduledTimeSlot timeSlot : timeSlots) { if (timeSlot.getProctor() == null) { possibleProctorsForTimeSlot = new ArrayList<Proctor>(); timeSlotAssessments = findAssessmentsForTimeSlot(timeSlot); final List<ProctorRole> proctorRoles = findProctorRoles( new ArrayList<Assessment>(timeSlotAssessments)); final List<Proctor> proctorsForTimeSlotAssessments = findProctors(proctorRoles, scheduled.getInstitutionId()); // build proctors for a particular institution buildAvailableProctors(gradesToAssessments, possibleProctorsForTimeSlot, timeSlotAssessments, timeSlot, proctorsForTimeSlotAssessments); // build proctors up the entity hierarchy buildParentProctors(scheduled, gradesToAssessments, possibleProctorsForTimeSlot, timeSlotAssessments, timeSlot, proctorRoles); if (possibleProctorsForTimeSlot.size() == 0) { LOGGER.debug("No Proctors found for scheduling"); } else if (possibleProctorsForTimeSlot.size() == 1) { timeSlot.setProctor(possibleProctorsForTimeSlot.get(0)); if (proctorAllocationCount.containsKey(possibleProctorsForTimeSlot.get(0).getId())) { proctorAllocationCount.put(possibleProctorsForTimeSlot.get(0).getId(), new Integer( proctorAllocationCount.get(possibleProctorsForTimeSlot.get(0).getId()).intValue() + 1)); } else { proctorAllocationCount.put(possibleProctorsForTimeSlot.get(0).getId(), 1); } } else { Proctor lowestProctor = null; for (final Proctor possProctor : possibleProctorsForTimeSlot) { if (lowestProctor == null) { lowestProctor = possProctor; } if (!proctorAllocationCount.containsKey(possProctor.getId())) { proctorAllocationCount.put(possProctor.getId(), 0); } if (!proctorAllocationCount.containsKey(lowestProctor.getId())) { proctorAllocationCount.put(lowestProctor.getId(), 0); } if (proctorAllocationCount.get(possProctor.getId()) .compareTo(proctorAllocationCount.get(lowestProctor.getId())) <= -1) { lowestProctor = possProctor; } } timeSlot.setProctor(lowestProctor); proctorAllocationCount.put(lowestProctor.getId(), new Integer(proctorAllocationCount.get(lowestProctor.getId()).intValue() + 1)); } } } } private void buildParentProctors(final Schedule scheduled, final HashMultimap<String, Assessment> gradesToAssessments, final List<Proctor> possibleProctorsForTimeSlot, final Set<Assessment> timeSlotAssessments, final ScheduledTimeSlot timeSlot, final List<ProctorRole> proctorRoles) { String entityId = scheduled.getInstitutionId(); FormatType entityType = FormatType.INSTITUTION; while (possibleProctorsForTimeSlot.size() == 0 && entityType != null) { final Sb11Entity entity = this.testRegPersister.findById(entityId, entityType); final List<Proctor> proctorsForTimeSlotAssessments = findProctors(proctorRoles, entity.getParentId()); buildAvailableProctors(gradesToAssessments, possibleProctorsForTimeSlot, timeSlotAssessments, timeSlot, proctorsForTimeSlotAssessments); entityId = entity.getParentId(); entityType = entity.getParentEntityType() == null ? null : FormatType.valueOf(entity.getParentEntityType().toString()); } } private void buildAvailableProctors(final HashMultimap<String, Assessment> gradesToAssessments, final List<Proctor> possibleProctorsForTimeSlot, final Set<Assessment> timeSlotAssessments, final ScheduledTimeSlot timeSlot, final List<Proctor> proctorsForTimeSlotAssessments) { for (final Proctor tmpProc : proctorsForTimeSlotAssessments) { if (tmpProc.isAvailableForTimeSlot(timeSlot)) { if (tmpProc.hasAffinities()) { // each of the affinity can specify level of affinity. if (!tmpProc.hasStrictAffinity()) { if (doAllProctorAffinitiesMatchAssessments(tmpProc, timeSlotAssessments, timeSlot, gradesToAssessments, false)) { possibleProctorsForTimeSlot.add(tmpProc); } } else { if (doAllProctorAffinitiesMatchAssessments(tmpProc, timeSlotAssessments, timeSlot, gradesToAssessments, true)) { possibleProctorsForTimeSlot.add(tmpProc); } } } else { possibleProctorsForTimeSlot.add(tmpProc); } } } } /** * Look through the studentsScheduled Map to find any students eligible for the assessment given who have not yet * been scheduled to take that assessment * * @param studentsScheduled * @param assessment * @return */ public List<ScheduledStudent> findStudentsEligibleForAssessment( final Map<String, ScheduledStudent> studentsScheduled, final Assessment assessment) { final List<ScheduledStudent> students = new ArrayList<ScheduledStudent>(); for (final Map.Entry<String, ScheduledStudent> entry : studentsScheduled.entrySet()) { final ScheduledStudent scheduledStudent = entry.getValue(); if (scheduledStudent.isEligibleForAssessment(assessment) && !scheduledStudent.isStudentScheduled(assessment)) { students.add(scheduledStudent); } } return students; } /** * Look through the studentsScheduled Map to find any students eligible for the assessment given who have not yet * been scheduled to take that assessment and who are eligible to use the accessbility equipment given * * @param studentsScheduled * @param assessment * @param accessibilityEquipment * @return */ public List<ScheduledStudent> findStudentsEligibleForAssessmentAndEquipment( final Map<String, ScheduledStudent> studentsScheduled, final Assessment assessment, final List<AccessibilityEquipment> accessibilityEquipment) { final List<ScheduledStudent> students = new ArrayList<ScheduledStudent>(); for (final Map.Entry<String, ScheduledStudent> entry : studentsScheduled.entrySet()) { final ScheduledStudent scheduledStudent = entry.getValue(); if (scheduledStudent.isEligibleForAssessment(assessment) && !scheduledStudent.isStudentScheduled(assessment) && scheduledStudent.canUseAccessibilityEquipment(accessibilityEquipment, assessment)) { students.add(scheduledStudent); } } return students; } /** * Validate the schedule to ensure that all students that should be scheduled are scheduled and that each time slot * with scheduled students has a proctor. * TBD whether we should validate everything from scratch or assume that the collections we're passing into this * method are good enough that we don't have to re-load all the data. * * @param scheduled * @param eligibleStudents * @param studentsScheduled * @return */ public ScheduleCreationInfo validateSchedule(final Schedule scheduled, final List<EligibleStudent> eligibleStudents, final Map<String, ScheduledStudent> studentsScheduled, final boolean reschedule) { // the "easy" way to do validation // -- iterate through all the time slots in the Schedule // ---- for each timeslot, if any seats are scheduled there MUST be a Proctor // ------ if there is no proctor, add an error to the list // -- iterate through all the ScheduledStudents // ---- for each ScheduledStudent ensure that each assessment is marked as scheduled // ------ if one is not marked as scheduled, add an error to the list // questions that we can answer here to make it easier to get data to the user through the UI: // how many test slots do not have a proctor? // which student-assessment combinations were unable to be scheduled? /* * more questions % capacity: # of testing slots, students scheduled, how many unscheduled students and * assessments what kind of excess capacity is there if everyone scheduled? * * questions of accessibility equipment: did everyone who needs accessibility equipment get allocated? */ int numTimeslots = 0; int numEmptyTimeslots = 0; int numSeats = 0; int numStudentsScheduled = 0; int numAssessmentsScheduled = 0; int numStudentsNotScheduled = 0; int numAssessmentsNotScheduled = 0; int numEmptySeats = 0; int numSeatsWithAccessEquip = 0; int numStudentsNeedingAccessEquipForAssessment = 0; int numStudentsCorrectlyAllocatedToEquipForAssessment = 0; int numStudentsNotCorrectlyAllocatedToEquipForAssessment = 0; boolean hasAccessEquip = false; final Set<String> uniqueStudents = new HashSet<String>(); final ScheduleCreationInfo info = new ScheduleCreationInfo(); final List<SchedulerValidationError> errors = new ArrayList<SchedulerValidationError>(); int numNoProctor = 0; for (final ScheduledTimeSlot timeSlot : scheduled.getOrderedTimeSlots(reschedule)) { numTimeslots++; if (timeSlot.getProctor() == null && !timeSlot.isTimeslotCompletelyEmpty()) { numNoProctor++; errors.add(new SchedulerValidationError(ErrorType.NO_PROCTOR, "No proctor assigned to test slot", String.format(NO_PROCTOR_ERROR_TEMPLATE, timeSlot.getStartTime().toDate(), timeSlot.getEndTime().toDate()))); } if (timeSlot.isTimeslotCompletelyEmpty()) { numEmptyTimeslots++; } for (final ScheduledSeat seat : timeSlot.getSeats()) { numSeats++; if (seat.getAccessibilityEquipments() != null && !seat.getAccessibilityEquipments().isEmpty()) { numSeatsWithAccessEquip++; hasAccessEquip = true; } if (seat.getStudent() != null) { if (uniqueStudents.add(seat.getStudent().getEntityId())) { numStudentsScheduled++; } numAssessmentsScheduled++; // if this seat has accessibility equipment // then we look at this student's ScheduledStudent object // and see if there is an entry for this assessment and the equipment // if the value is TRUE, then the student was correctly allocated to the equipment if (seat.getStudent().hasAccommodations(seat.getAssessment())) { if (hasAccessEquip) { final ScheduledStudent thisStudent = studentsScheduled .get(seat.getStudent().getEntityId()); if (thisStudent != null) { for (final AccessibilityEquipment equip : seat.getAccessibilityEquipmentObjs()) { if (thisStudent.isScheduledForEquipmentInAssessment(seat.getAssessment(), equip)) { numStudentsCorrectlyAllocatedToEquipForAssessment++; break; // only want to count the equipment allocation once per // student/assessment } } } } } } else { numEmptySeats++; } hasAccessEquip = false; } } info.setNumTimeslotsWithNoProctor(numNoProctor); info.setNumEmptyTestSlots(numEmptyTimeslots); final Map<String, ScheduledStudent> studentsScheduledCopy = Maps.newHashMap(studentsScheduled); final Map<String, ScheduledStudent> studentsNotScheduled = ImmutableMap .copyOf(Maps.filterValues(studentsScheduledCopy, new Predicate<ScheduledStudent>() { @Override public boolean apply(final ScheduledStudent scheduledStudent) { final List<Assessment> toRemove = new ArrayList<Assessment>(); for (final Map.Entry<Assessment, Boolean> entry : scheduledStudent.getScheduledAssessments() .entrySet()) { if (entry.getValue()) { toRemove.add(entry.getKey()); } } if (!toRemove.isEmpty()) { for (final Assessment assess : toRemove) { scheduledStudent.getScheduledAssessments().remove(assess); } } if (scheduledStudent.getScheduledAssessments().size() > 0) { for (final Assessment assess : scheduledStudent.getScheduledAssessments().keySet()) { errors.add(new SchedulerValidationError(ErrorType.STUDENT_NOT_SCHEDULED, "Student not scheduled for an assessment he/she is eligible for", String.format(NOT_SCHEDULED_FOR_ASSESSMENT_ERROR_TEMPLATE, scheduledStudent.getStudent().getEntityId(), assess.getTestName()))); } return true; } else { return false; } } })); final Map<String, List<String>> studentsNotScheduledMap = new HashMap<String, List<String>>(); for (final Map.Entry<String, ScheduledStudent> entry : studentsNotScheduled.entrySet()) { numStudentsNotScheduled++; final List<String> notScheduledAssessments = new ArrayList<String>(); for (final Assessment assess : entry.getValue().getScheduledAssessments().keySet()) { numAssessmentsNotScheduled++; notScheduledAssessments.add(assess.getTestName()); } studentsNotScheduledMap.put(entry.getKey(), notScheduledAssessments); } // iterate through Students Scheduled // get scheduled assessments // if TRUE (assessment scheduled for the student) // then check assessment equipment association // if there is one and the value is false // this means that the student was scheduled for the assessment, but was NOT scheduled into a seat // that had the proper equipment the student needs for (final ScheduledStudent schedStudent : studentsScheduled.values()) { for (final Map.Entry<Assessment, Boolean> entry : schedStudent.getScheduledAssessments().entrySet()) { boolean scheduledForAllEquip = true; if (entry.getValue()) { final Map<AccessibilityEquipment, Boolean> equipmentScheduled = schedStudent .getAllScheduledForEquipmentInAssessment(entry.getKey()); for (final Map.Entry<AccessibilityEquipment, Boolean> accessEquipEntry : equipmentScheduled .entrySet()) { if (!accessEquipEntry.getValue()) { scheduledForAllEquip = false; errors.add(new SchedulerValidationError(ErrorType.NOT_ALLOCATED_TO_REQUIRED_EQUIPMENT, "Student not allocated to necessary accessibility equipment", String.format(ACCESS_EQUIP_ERROR_TEMPLATE, schedStudent.getStudent().getEntityId(), accessEquipEntry.getKey().getName(), entry.getKey().getTestName()))); } } if (!scheduledForAllEquip) { numStudentsNotCorrectlyAllocatedToEquipForAssessment++; } } } } // now just iterate through all the scheduled students // and see if any of them is eligible for accessibility equipment // in order to generate the count of all students who require equipment for (final ScheduledStudent schedStudent : studentsScheduled.values()) { numStudentsNeedingAccessEquipForAssessment += schedStudent.numAssessmentsAccessEquipRequiredFor(); } info.setStudentsNotScheduled(studentsNotScheduledMap); info.setErrors(errors); info.setNumEmptySeats(numEmptySeats); info.setNumSeatsWithAccessibilityEquipment(numSeatsWithAccessEquip); info.setNumStudentAssessmentsReqAccessEquipScheduled(numStudentsCorrectlyAllocatedToEquipForAssessment); info.setNumStudentAssessmentsReqAccessEquipScheduledIncorrect( numStudentsNotCorrectlyAllocatedToEquipForAssessment); info.setNumStudentAssessmentsRequiringAccessibilityEquipment(numStudentsNeedingAccessEquipForAssessment); info.setNumStudentsScheduled(numStudentsScheduled); info.setNumUnscheduledAssessments(numAssessmentsNotScheduled); info.setNumUnscheduledStudents(numStudentsNotScheduled); info.setPercentCapacityUsed((float) numAssessmentsScheduled / (float) numSeats * 100); info.setTotalNumSeats(numSeats); info.setTotalNumTestSlots(numTimeslots); return info; } /** * Returns a list of EligibleStudents that only contain those students that match the assessments * * @param eligibleStudents * @param assessments * @return */ public List<EligibleStudent> filterStudents(final List<EligibleStudent> eligibleStudents, final List<Assessment> assessments) { final List<EligibleStudent> filtered = new ArrayList<EligibleStudent>(); final com.google.common.collect.ListMultimap<Assessment, EligibleStudent> multimap = ArrayListMultimap .create(); for (final EligibleStudent eligStudent : eligibleStudents) { // create multimap so we know which eligible students go with which assessments for (final Assessment assess : eligStudent.getAssessments()) { multimap.put(assess, eligStudent); } } // iterate through assessments and get the eligible students for each, putting them into filtered and then // return filtered for (final Assessment assess : assessments) { if (multimap.containsKey(assess)) { filtered.addAll(multimap.get(assess)); } } return filtered; } /** * Figures out what accessibility equipment each student is eligible to use * * @param studentsScheduled * @param schedule * @param reschedule */ public void determineStudentAccessEquip(final Map<String, ScheduledStudent> studentsScheduled, final Schedule schedule, final boolean reschedule) { final TreeSet<ScheduledTimeSlot> timeSlots = schedule.getOrderedTimeSlots(reschedule); final List<ScheduledSeat> seatsWithEquipment = new ArrayList<ScheduledSeat>(); final Set<AccessibilityEquipment> uniqueEquipment = new HashSet<AccessibilityEquipment>(); for (final ScheduledTimeSlot timeSlot : timeSlots) { seatsWithEquipment.addAll(timeSlot.getSeatsWithAccessbilityEquip()); } // ensure the seats have the access equip objects loaded from the db // load all equipment into a Set so we have all the unique equipment from all seats and no duplicates for (final ScheduledSeat seat : seatsWithEquipment) { loadAccessibilityEquipmentObj(seat); uniqueEquipment.addAll(seat.getAccessibilityEquipmentObjs()); } // iterate through each assessment the student is eligible for // and each accessibility equipment for (final ScheduledStudent schedStudent : studentsScheduled.values()) { for (final Assessment assessment : schedStudent.getScheduledAssessments().keySet()) { for (final AccessibilityEquipment equip : uniqueEquipment) { if (schedStudent.canUseAccessibilityEquipment(Lists.newArrayList(equip), assessment)) { schedStudent.addAccessEquipEligibility(assessment, equip); } } } } } /** * Returns the filtered list of assessments by affinity type */ private Collection<Assessment> filterAssessmentByAffinity(final List<Assessment> assessments, final AffinityType affinityType, final String affinity, final HashMultimap<String, Assessment> gradesToAssessments, final HashMultimap<String, Assessment> studentGroupToAssessments) { switch (affinityType) { case ASSESSMENT: return Collections2.filter(assessments, new Predicate<Assessment>() { @Override public boolean apply(final Assessment assessment) { return assessment.getId().equals(affinity); } }); case SUBJECT: return Collections2.filter(assessments, new Predicate<Assessment>() { @Override public boolean apply(final Assessment assessment) { return assessment.getSubjectCode().equals(affinity); } }); case GRADE: return gradesToAssessments.get(affinity); case STUDENTGROUP: return studentGroupToAssessments.get(affinity); default: return null; } } /** * Gets assessments by affinity type and allocates it to the timeslot having this affinity */ private void allocateTimeslotAffinity(final List<Assessment> assessments, final ScheduledTimeSlot timeslot, final Map<String, ScheduledStudent> studentsScheduled, final String scheduleId, final Affinity affinity, final HashMultimap<String, Assessment> gradesToAssessments) { boolean successfulAllocation = false; final Collection<Assessment> affinityAssessments = filterAssessmentByAffinity(assessments, affinity.getType(), affinity.getValue(), gradesToAssessments, null); for (final Assessment affinityAssessment : affinityAssessments) { successfulAllocation = allocateToTimeslot(timeslot, affinityAssessment, studentsScheduled, false, scheduleId, gradesToAssessments); if (successfulAllocation) { timeslot.addScheduledAffinity(affinity); } } } /** * Allocates all the affinities defined in the schedule to all the available seats in each timeslot */ private void allocateToScheduleAffinity(final Schedule scheduled, final Map<String, ScheduledStudent> studentsScheduled, final Assessment affinityAssessment, final Affinity affinity, final HashMultimap<String, Assessment> gradesToAssessments, final boolean reschedule) { // this will be one assessment at a time // if the affinity is strict then we must find a timeslot that either already has this affinity scheduled for // this assessment // or a time slot that has NO scheduled seats // if the affinity is not strict, then we can find any time slot with empty seats and fill it up // be sure to mark the timeslot as having the affinity // each loop for a timeslot we need to check to see if all students have been scheduled for the current // assessment we're scheduling so that // we make sure that we schedule ALL students for any particular assessment in contiguous time slots (or as // close to contiguous as possible) // in order to avoid missing scheduling students for a priority in the correct order for (final ScheduledTimeSlot timeslot : scheduled.getOrderedTimeSlots(reschedule)) { if (studentsStillToBeScheduled(studentsScheduled, affinityAssessment)) { if (affinity.isStrict()) { if (timeslot.getNumberOfAssignedSeats() == 0 || !timeslot.getAffinitiesScheduled().isEmpty() && timeslot.getAffinitiesScheduled().contains(affinity)) { final boolean affinityScheduled = allocateToTimeslot(timeslot, affinityAssessment, studentsScheduled, true, scheduled.getId(), gradesToAssessments); if (affinityScheduled) { timeslot.addScheduledAffinity(affinity); } } } else { final boolean affinityScheduled = allocateToTimeslot(timeslot, affinityAssessment, studentsScheduled, true, scheduled.getId(), gradesToAssessments); if (affinityScheduled) { timeslot.addScheduledAffinity(affinity); } } } } } /** * Allocates eligible student for an assessment to an available seat. Once assigned marks the seat and student as * scheduled */ private boolean assignStudentToSeat(final ScheduledTimeSlot scheduledTimeSlot, final Assessment assessment, final ScheduledSeat seat, final List<ScheduledStudent> eligibleStudents, final String scheduleId) { for (final ScheduledStudent eligibleStudent : eligibleStudents) { if (scheduledTimeSlot.getAssignedStudents().add(eligibleStudent.getStudent().getEntityId())) { seat.setStudent(eligibleStudent.getStudent()); seat.setAssessment(assessment); seat.setSeatScheduled(true); eligibleStudent.getScheduledAssessments().put(assessment, Boolean.TRUE); this.scheduleTestStatusCreator.addStatus(new ScheduleTestStatus(scheduleId, assessment.getId(), eligibleStudent.getStudent().getEntityId(), eligibleStudent.getStudent().getStateAbbreviation(), scheduledTimeSlot.getStartTime())); return true; } } return false; } private boolean allStudentsScheduled(final Map<String, ScheduledStudent> studentsToBeScheduled) { for (final ScheduledStudent student : studentsToBeScheduled.values()) { if (!student.isStudentFullyScheduled()) { return false; } } return true; } private Set<Assessment> findAssessmentsForTimeSlot(final ScheduledTimeSlot timeSlot) { final Set<Assessment> assessmentsForTimeSlot = new HashSet<Assessment>(); final Set<ScheduledSeat> seats = timeSlot.getSeats(); for (final ScheduledSeat seat : seats) { if (seat.getAssessment() != null) { assessmentsForTimeSlot.add(seat.getAssessment()); } } return assessmentsForTimeSlot; } private boolean doAllProctorAffinitiesMatchAssessments(final Proctor proctor, final Set<Assessment> assessments, final ScheduledTimeSlot timeSlot, final HashMultimap<String, Assessment> gradesToAssessments, final boolean isStrict) { // if strict find the first strict affinity // else get all the affinities from the proctor // for each affinity // if affinity is grade then the match should be based on the students grades in who are scheduled to the // timeslot // --if there are student grades matching affinity grade return true else continue // else match assessments for the affinity value // --if there are assessments matching the affinity return true else continue List<Affinity> affinities = new ArrayList<Affinity>(); if (isStrict) { final Affinity affinity = proctor.findFirstStrictAffinity(); affinities = affinity != null ? com.google.common.collect.Lists.newArrayList(affinity) : affinities; } else { affinities = proctor.getAffinities(); } for (final Affinity affinity : affinities) { if (affinity.getType() == AffinityType.GRADE) { final Set<GradeLevel> studentGrades = getGradesFromScheduledStudents(timeSlot); for (final GradeLevel grade : studentGrades) { if (grade.getGrade().equalsIgnoreCase(affinity.getValue())) { return true; } } } else { final Collection<Assessment> affinityAssessments = filterAssessmentByAffinity( com.google.common.collect.Lists.newArrayList(assessments), affinity.getType(), affinity.getValue(), gradesToAssessments, null); if (!CollectionUtils.isEmpty(affinityAssessments)) { return true; } } } return false; } private Set<GradeLevel> getGradesFromScheduledStudents(final ScheduledTimeSlot timeSlot) { final Set<GradeLevel> grades = new HashSet<GradeLevel>(); final Set<ScheduledSeat> seats = timeSlot.getSeats(); for (final ScheduledSeat seat : seats) { grades.add(seat.getStudent().getGradeLevelWhenAssessed()); } return grades; } // specifically, the strict affinity that was previously scheduled here private boolean doesAssessmentFulfillAffinity(final ScheduledTimeSlot timeSlot, final Assessment assessment, final HashMultimap<String, Assessment> gradesToAssessments) { boolean returnVal = false; final Set<Affinity> affSet = timeSlot.getAffinitiesScheduled(); Affinity aff = null; for (final Affinity tmpAff : affSet) { if (tmpAff.isStrict()) { aff = tmpAff; break; } } if (aff == null) { // immediately return true because if this is called with a non-strict affinity scheduled // to the time slot, then we can schedule anything else here return true; } switch (aff.getType()) { case ASSESSMENT: if (aff.getValue().equals(assessment.getTestName())) { returnVal = true; } break; case GRADE: final Set<Assessment> assessmentsForGrade = gradesToAssessments.get(aff.getValue()); for (final Assessment tmpAssess : assessmentsForGrade) { if (assessment.equals(tmpAssess)) { returnVal = true; break; } } break; case SUBJECT: if (aff.getValue().equals(assessment.getSubjectCode())) { returnVal = true; } break; default: returnVal = false; break; } return returnVal; } private boolean studentsStillToBeScheduled(final Map<String, ScheduledStudent> studentsToBeScheduled, final Assessment assessment) { final Map<String, ScheduledStudent> filtered = Maps.filterEntries(studentsToBeScheduled, new Predicate<Map.Entry<String, ScheduledStudent>>() { @Override public boolean apply(final Map.Entry<String, ScheduledStudent> entry) { if (entry.getValue().isEligibleForAssessment(assessment) && !entry.getValue().isStudentScheduled(assessment)) { return true; } else { return false; } } }); return !filtered.isEmpty(); } private void loadAccessibilityEquipmentObj(final ScheduledSeat seat) { // does the seat have accessiblity equipment objects loaded yet? // if not, load them // if so, skip the load and continue if (seat.getAccessibilityEquipmentObjs() == null || seat.getAccessibilityEquipmentObjs().isEmpty()) { final List<AccessibilityEquipment> accessEquipList = new ArrayList<AccessibilityEquipment>(); if (seat.getAccessibilityEquipments() != null && !seat.getAccessibilityEquipments().isEmpty()) { for (final String equipName : seat.getAccessibilityEquipments()) { final Map<String, String[]> reqMap = new HashMap<String, String[]>(); reqMap.put("name", new String[] { equipName }); final AccessibilityEquipmentSearchRequest searchReq = new AccessibilityEquipmentSearchRequest( reqMap); final SearchResponse<AccessibilityEquipment> equipSearchResp = this.accessibilityEquipmentService .searchAccessibilityEquipments(searchReq); if (equipSearchResp.getTotalCount() > 0) { accessEquipList.add(equipSearchResp.getSearchResults().get(0)); } } } seat.setAccessibilityEquipmentObjs(accessEquipList); } } public List<StudentGroup> findStudentGroups(final String institutionMongoId) { return this.studentGroupService.findStudentGroups(institutionMongoId); } }