343 lines
12 KiB
PHP
343 lines
12 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
|
||
|
*/
|
||
|
|
||
|
namespace oat\taoProctoring\model\implementation;
|
||
|
|
||
|
use Exception;
|
||
|
use oat\taoDelivery\model\execution\ServiceProxy;
|
||
|
use oat\taoProctoring\model\TestSessionHistoryService as TestSessionHistoryServiceInterface;
|
||
|
use \oat\oatbox\service\ConfigurableService;
|
||
|
use DateTime;
|
||
|
use tao_helpers_Date as DateHelper;
|
||
|
use oat\taoProctoring\model\deliveryLog\DeliveryLog;
|
||
|
use oat\tao\helpers\UserHelper;
|
||
|
|
||
|
/**
|
||
|
* Service is used to retrieve test session history
|
||
|
*
|
||
|
* @author Aleh Hutnikau <hutnikau@1pt.com>
|
||
|
* @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()];
|
||
|
}
|
||
|
}
|