/** * Callback function for comparing sequence blocks. * The applied, ranked criteria for comparison are: * 1. "academic level" * Numeric sort, ascending. * 2. "start date" * Numeric sort on timestamps, ascending. NULL values will be treated as unix timestamp 0. * 3. "title" * Alphabetical sort. * 4. "sequence block id" * A last resort. Numeric sort, ascending. * * @param CurriculumInventorySequenceBlockInterface $a * @param CurriculumInventorySequenceBlockInterface $b * @return int One of -1, 0, 1. */ public static function compareSequenceBlocksWithDefaultStrategy(CurriculumInventorySequenceBlockInterface $a, CurriculumInventorySequenceBlockInterface $b) { // 1. academic level id if ($a->getAcademicLevel()->getLevel() > $b->getAcademicLevel()->getLevel()) { return 1; } elseif ($a->getAcademicLevel()->getLevel() < $b->getAcademicLevel()->getLevel()) { return -1; } // 2. start date $startDateA = $a->getStartDate() ? $a->getStartDate()->getTimestamp() : 0; $startDateB = $b->getStartDate() ? $b->getStartDate()->getTimestamp() : 0; if ($startDateA > $startDateB) { return 1; } elseif ($startDateA < $startDateB) { return -1; } // 3. title comparison $n = strcasecmp($a->getTitle(), $b->getTitle()); if ($n) { return $n > 0 ? 1 : -1; } // 4. sequence block id comparison if ($a->getId() > $b->getId()) { return 1; } elseif ($a->getId() < $b->getId()) { return -1; } return 0; }
/** * Recursively creates and appends sequence block nodes to the XML document. * * @param \DomDocument $dom the document object * @param \DomElement $sequenceNode the sequence DOM node to append to * @param CurriculumInventorySequenceBlockInterface $block the current sequence block * @param array $eventReferences A reference map of sequence blocks to events. * @param array $competencyObjectReferences A reference map of sequence blocks to competency objects. * @param \DomElement|null $parentSequenceBlockNode the DOM node representing the parent sequence block. * @param int $order of this sequence block in relation to other nested sequence blocks. '0' if n/a. */ protected function createSequenceBlockNode(\DomDocument $dom, \DomElement $sequenceNode, CurriculumInventorySequenceBlockInterface $block, array $eventReferences, array $competencyObjectReferences, \DomElement $parentSequenceBlockNode = null, $order = 0) { $sequenceBlockNode = $dom->createElement('SequenceBlock'); $sequenceNode->appendChild($sequenceBlockNode); // append a reference to _this_ sequence block to the parent sequence block if (isset($parentSequenceBlockNode)) { $ref = "/CurriculumInventory/Sequence/SequenceBlock[@id='{$block->getId()}']"; $sequenceBlockReferenceNode = $dom->createElement('SequenceBlockReference', $ref); $parentSequenceBlockNode->appendChild($sequenceBlockReferenceNode); if ($order) { $sequenceBlockReferenceNode->setAttribute('order', $order); } } $sequenceBlockNode->setAttribute('id', $block->getId()); switch ($block->getRequired()) { case CurriculumInventorySequenceBlockInterface::OPTIONAL: $sequenceBlockNode->setAttribute('required', 'Optional'); break; case CurriculumInventorySequenceBlockInterface::REQUIRED: $sequenceBlockNode->setAttribute('required', 'Required'); break; case CurriculumInventorySequenceBlockInterface::REQUIRED_IN_TRACK: $sequenceBlockNode->setAttribute('required', 'Required In Track'); break; } switch ($block->getChildSequenceOrder()) { case CurriculumInventorySequenceBlockInterface::ORDERED: $sequenceBlockNode->setAttribute('order', 'Ordered'); break; case CurriculumInventorySequenceBlockInterface::UNORDERED: $sequenceBlockNode->setAttribute('order', 'Unordered'); break; case CurriculumInventorySequenceBlockInterface::PARALLEL: $sequenceBlockNode->setAttribute('order', 'Parallel'); break; } // // min/max are currently not supported. // //$sequenceBlockNode->setAttribute('minimum', $block->getMinimum()); //$sequenceBlockNode->setAttribute('maximum', $block->getMaximum()); if ($block->hasTrack()) { $sequenceBlockNode->setAttribute('track', 'true'); } else { $sequenceBlockNode->setAttribute('track', 'false'); } $titleNode = $dom->createElement('Title'); $sequenceBlockNode->appendChild($titleNode); $titleNode->appendChild($dom->createTextNode($block->getTitle())); if ('' !== trim($block->getDescription())) { $descriptionNode = $dom->createElement('Description'); $sequenceBlockNode->appendChild($descriptionNode); $descriptionNode->appendChild($dom->createTextNode($block->getDescription())); } // add duration and/or start+end date $timingNode = $dom->createElement('Timing'); $sequenceBlockNode->appendChild($timingNode); if ($block->getDuration()) { $durationNode = $dom->createElement('Duration'); $timingNode->appendChild($durationNode); $durationNode->appendChild($dom->createTextNode('P' . $block->getDuration() . 'D')); // duration in days. } if ($block->getStartDate()) { $datesNode = $dom->createElement('Dates'); $timingNode->appendChild($datesNode); $startDateNode = $dom->createElement('StartDate', $block->getStartDate()->format('Y-m-d')); $datesNode->appendChild($startDateNode); $endDateNode = $dom->createElement('EndDate', $block->getEndDate()->format('Y-m-d')); $datesNode->appendChild($endDateNode); } // academic level $levelNode = $dom->createElement('Level', "/CurriculumInventory/AcademicLevels/Level[@number='{$block->getAcademicLevel()->getLevel()}']"); $sequenceBlockNode->appendChild($levelNode); // clerkship type // map course clerkship type to "Clerkship Model" // @todo Refactor this out into utility method. [ST 2015/09/14] $course = $block->getCourse(); $clerkshipModel = false; if ($course) { $clerkshipType = $course->getClerkshipType() ? $course->getClerkshipType()->getId() : null; switch ($clerkshipType) { case CourseClerkshipTypeInterface::INTEGRATED: $clerkshipModel = 'integrated'; break; case CourseClerkshipTypeInterface::BLOCK: case CourseClerkshipTypeInterface::LONGITUDINAL: $clerkshipModel = 'rotation'; break; } } if ($clerkshipModel) { $clerkshipModelNode = $dom->createElement('ClerkshipModel', $clerkshipModel); $sequenceBlockNode->appendChild($clerkshipModelNode); } // link to competency objects if (array_key_exists($block->getId(), $competencyObjectReferences)) { $refs = $competencyObjectReferences[$block->getId()]; foreach ($refs['program_objectives'] as $id) { $uri = $this->createCompetencyObjectUri($id, 'program_objective'); $this->createCompetencyObjectReferenceNode($dom, $sequenceBlockNode, $uri); } foreach ($refs['course_objectives'] as $id) { $uri = $this->createCompetencyObjectUri($id, 'course_objective'); $this->createCompetencyObjectReferenceNode($dom, $sequenceBlockNode, $uri); } } // pre-conditions and post-conditions are n/a // link to events if (array_key_exists($block->getId(), $eventReferences)) { $refs = $eventReferences[$block->getId()]; foreach ($refs as $reference) { $sequenceBlockEventNode = $dom->createElement('SequenceBlockEvent'); $sequenceBlockNode->appendChild($sequenceBlockEventNode); if ($reference['required']) { $sequenceBlockEventNode->setAttribute('required', 'true'); } else { $sequenceBlockEventNode->setAttribute('required', 'false'); } $refUri = "/CurriculumInventory/Events/Event[@id='E{$reference['event_id']}']"; $eventReferenceNode = $dom->createElement('EventReference', $refUri); $sequenceBlockEventNode->appendChild($eventReferenceNode); // start/end-date // Not implemented at this point. // // Some food for thought: // This information may be retrieved from the date range values of offerings or independent learning // sessions associated with Ilios sessions. // E.g. // For a start date of this sequence block event reference, the earliest start date of any offerings // within a session may be assumed. // Likewise, the latest end date of any offerings within a session could be used for the end date of // event reference. // How accurate this will match the expected start/end date values here remains to be seen and will // require further discussion. // [ST 2013/08/08] } } // recursively generate XML for nested sequence blocks $children = $block->getChildrenAsSortedList(); if (!empty($children)) { $order = 0; $isOrdered = CurriculumInventorySequenceBlockInterface::ORDERED === $block->getChildSequenceOrder(); foreach ($children as $child) { // apply an incremental sort order for "ordered" sequence blocks if ($isOrdered) { $order++; } $this->createSequenceBlockNode($dom, $sequenceNode, $child, $eventReferences, $competencyObjectReferences, $sequenceBlockNode, $order); } } }
/** * Reorder the entire sequence if on of the blocks changes position. * @param int $oldValue * @param CurriculumInventorySequenceBlockInterface $block * @param ManagerInterface $manager * @throws \OutOfRangeException */ protected function reorderBlocksInSequenceOnOrderChange($oldValue, CurriculumInventorySequenceBlockInterface $block, ManagerInterface $manager) { $parent = $block->getParent(); if (!$parent) { return; } if ($parent->getChildSequenceOrder() !== CurriculumInventorySequenceBlockInterface::ORDERED) { return; } $newValue = $block->getOrderInSequence(); $blocks = $parent->getChildrenAsSortedList(); $blocks = array_filter($blocks, function ($sibling) use($block) { return $sibling->getId() !== $block->getId(); }); $blocks = array_values($blocks); $minRange = 1; $maxRange = count($blocks) + 1; if ($newValue < $minRange || $newValue > $maxRange) { throw new \OutOfRangeException("The given order-in-sequence value {$newValue} falls outside the range {$minRange} - {$maxRange}."); } if ($oldValue === $newValue) { return; } array_splice($blocks, $block->getOrderInSequence() - 1, 0, [$block]); for ($i = 0, $n = count($blocks); $i < $n; $i++) { /* @var CurriculumInventorySequenceBlockInterface $current */ $current = $blocks[$i]; $j = $i + 1; if ($current->getId() !== $block && $current->getOrderInSequence() !== $j) { $current->setOrderInSequence($j); $manager->update($current, false, false); } } }