* @package oat\taoProctoring */ class TestSessionHistoryService extends ConfigurableService implements TestSessionHistoryServiceInterface { /** * List of event ids which should be excluded from history (in lower case) */ protected static $eventsToExclude = ['heartbeat']; /** * List of event ids which should be represented in the brief history report */ protected static $briefEvents = [ 'section_exit_code', 'test_exit_code', 'test_pause', 'test_run', 'test_run', 'test_authorise', 'test_terminate', 'test_irregularity', 'pause', 'unsecured-launch-prohibited', 'focus-loss-prohibited', 'leave-fullscreen-prohibited', 'pause-on-disconnect', 'test_adjust_time', ]; /** * @var \core_kernel_classes_Resource[] list of user instances */ private $authors = []; /** * @var \core_kernel_classes_Resource[] */ private $authorRoles = []; /** * @var \core_kernel_classes_Resource[] */ private $proctorRoles = []; /** * TestSessionHistoryService constructor. * @param array $options */ public function __construct(array $options = []) { parent::__construct($options); $roles = $this->getOption(self::PROCTOR_ROLES); if(is_null($roles)){ $roles = []; } $this->proctorRoles = array_merge([new \core_kernel_classes_Resource('http://www.tao.lu/Ontologies/TAOProctor.rdf#ProctorRole')], $roles); } /** * @param array $sessions List of session ids * @param array $options The following option is handled: * - periodStart: a date/time string. * - periodEnd: a date/time string. * - detailed: whether to retrieve detailed or brief report. Defaults to false (brief). * - sortBy: column name string. * - sortOrder: order direction (asc|desc) string. * @return array */ public function getSessionsHistory(array $sessions, $options) { $history = []; $periodStart = $this->getPeriodStart($options); $periodEnd = $this->getPeriodEnd($options); /** @var DeliveryLog $deliveryLog */ $deliveryLog = $this->getServiceManager()->get(DeliveryLog::SERVICE_ID); //empty array means that all events (except listed in self::$eventsToExclude) will be represented in the report $eventsToInclude = $options['detailed'] ? [] : self::$briefEvents; foreach ($sessions as $sessionUri) { $deliveryExecution = ServiceProxy::singleton()->getDeliveryExecution($sessionUri); $logs = $deliveryLog->get($deliveryExecution->getIdentifier()); $exportable = []; foreach ($logs as $data) { $eventId = $data['data']['type'] ?? $data[DeliveryLog::EVENT_ID]; $eventName = strtolower(explode('.', $eventId)[0]); if ( (!empty($eventsToInclude) && !in_array($eventName, $eventsToInclude)) || //event should not be included in_array($eventName, self::$eventsToExclude) //event must be excluded ) { continue; } $author = $this->getAuthor($data); $details = $this->getEventDetails($data); $context = $this->getEventContext($data); $role = $this->getUserRole($author); $exportable['timestamp'] = (isset($data['data']['timestamp']))?$data['data']['timestamp']:$data['created_at']; if (($periodStart && $exportable['timestamp'] < $periodStart) || ($periodEnd && $exportable['timestamp'] > $periodEnd)) { continue; } $exportable['date'] = DateHelper::displayeDate($exportable['timestamp'], DateHelper::FORMAT_LONG_MICROSECONDS); $exportable['role'] = $role; $exportable['actor'] = _dh($this->getActorName($author->getUri())); $exportable['event'] = $eventId; $exportable['details'] = $details; $exportable['context'] = $context; $history[] = $exportable; } } $this->sortHistory($history, $options); return $history; } /** * Gets the url that leads to the page listing the history * @param $delivery * @return string */ public function getHistoryUrl($delivery = null) { $params = []; if ($delivery) { if ($delivery instanceof \core_kernel_classes_Resource) { $delivery = $delivery->getUri(); } $params['delivery'] = $delivery . ''; } return _url('index', 'Reporting', 'taoProctoring', $params); } /** * Gets the back url that returns to the page listing the sessions * @param $delivery * @return string */ public function getBackUrl($delivery = null) { $params = []; if ($delivery) { if ($delivery instanceof \core_kernel_classes_Resource) { $delivery = $delivery->getUri(); } $params['delivery'] = $delivery . ''; } return _url('index', 'Monitor', 'taoProctoring', $params); } /** * @param array $data event data from delivery log * @return string|array */ private function getEventDetails($data) { $details = ''; if(isset($data['data']['type'])) { $details = $data['data']['context']['shortcut'] ?? ''; } elseif (isset($data['data']['reason'], $data['data']['reason']['reasons'])) { $details = is_array($data['data']['reason']['reasons']) ? array_merge(array_values($data['data']['reason']['reasons']), [__($data['data']['reason']['comment'])]) : array_merge([$data['data']['reason']['reasons']], [__($data['data']['reason']['comment'])]); } elseif (isset($data['data']['exitCode'])) { $details = $data['data']['exitCode']; } elseif (isset($data['data']['itemId'])) { $details = $data['data']['itemId']; } elseif (isset($data['data']['web_browser_name'])) { $details = ($data['data']['web_browser_name'] . ' ') . (isset($data['data']['web_browser_version']) ? $data['data']['web_browser_version'] . '; ' : '') . (isset($data['data']['os_name']) ? $data['data']['os_name'] . ' ' : '') . (isset($data['data']['os_version']) ? $data['data']['os_version'] . ' ' : ''); } elseif (is_string($data['data'])) { $details = $data['data']; } if (isset($data['data']['increment']) && is_array($details)) { $details[] = $data['data']['increment'] . __(' sec'); } return $details; } /** * @param array $data event data from delivery log * @return string */ private function getEventContext($data): string { if (isset($data['data']['type'])) { $context = $data['data']['context']['readable'] ?? ''; } else { $context = (isset($data['data']['context']) && !is_null($data['data']['context'])) ? $data['data']['context'] : ''; } return $context; } /** * @param $options * @return null|number timestamp * @throws Exception */ private function getPeriodStart(array $options) { $periodStart = null; if (!empty($options['periodStart'])) { $periodStart = new DateTime($options['periodStart']); $periodStart->setTime(0, 0, 0); $periodStart = DateHelper::getTimeStamp($periodStart->getTimestamp()); } return $periodStart; } /** * @param $options * @return null|number timestamp */ private function getPeriodEnd(array $options) { $periodEnd = null; if (!empty($options['periodEnd'])) { $periodEnd = new DateTime($options['periodEnd']); $periodEnd->setTime(23, 59, 59); $periodEnd = DateHelper::getTimeStamp($periodEnd->getTimestamp()); } return $periodEnd; } /** * Sort events * @param array $history * @param array $options */ private function sortHistory(array &$history, array $options) { $sortBy = isset($options['sortBy']) ? $options['sortBy'] : 'timestamp'; $sortOrder = isset($options['sortOrder']) ? $options['sortOrder'] : 'desc'; if ($sortOrder == 'asc') { $sortOrder = 1; } else { $sortOrder = -1; } if ($sortBy == 'timestamp' || $sortBy == 'id') { usort($history, function($a, $b) use($sortOrder) { $result = $sortOrder * (floatval($a['timestamp']) - floatval($b['timestamp'])); if ($result === 0) { return $result; } return $result > 0 ? 1 : -1; }); } else { usort($history, function($a, $b) use($sortBy, $sortOrder) { return $sortOrder * strnatcasecmp($a[$sortBy], $b[$sortBy]); }); } } /** * @param array $data event data from delivery log * @return \core_kernel_classes_Resource */ protected function getAuthor(array $data) { if (!isset($this->authors[$data['created_by']])) { $this->authors[$data['created_by']] = new \core_kernel_classes_Resource($data['created_by']); } return $this->authors[$data['created_by']]; } /** * @param $userId * @return string */ protected function getActorName($userId) { $user = UserHelper::getUser($userId); return UserHelper::getUserName($user, true); } /** * @param \core_kernel_classes_Resource $user * @return string */ private function getUserRole(\core_kernel_classes_Resource $user) { $userService = \tao_models_classes_UserService::singleton(); if (!isset($this->authorRoles[$user->getUri()])) { $userRole = ''; $allUserRoles = $userService->getUserRoles($user); if (!empty($allUserRoles)) { $userRole = $userService->userHasRoles($user, $this->proctorRoles) ? __('Proctor') : __('Test-Taker'); } $this->authorRoles[$user->getUri()] = $userRole; } return $this->authorRoles[$user->getUri()]; } }