Merge branch '1.11.x' of github.com:chamilo/chamilo-lms into 1.11.x

pull/5903/head
Yannick Warnier 11 months ago
commit 896b085694
  1. 2
      main/exercise/exercise_report.php
  2. 12
      main/glossary/index.php
  3. BIN
      main/img/icons/22/webcam_na.png
  4. 2
      main/inc/lib/TrackingCourseLog.php
  5. 10
      main/inc/lib/formvalidator/FormValidator.class.php
  6. 32
      plugin/exercisefocused/admin.php
  7. 48
      plugin/exercisefocused/index.php
  8. 5
      plugin/exercisefocused/install.php
  9. 39
      plugin/exercisefocused/lang/english.php
  10. 39
      plugin/exercisefocused/lang/spanish.php
  11. 32
      plugin/exercisefocused/pages/detail.php
  12. 298
      plugin/exercisefocused/pages/export.php
  13. 30
      plugin/exercisefocused/pages/log.php
  14. 34
      plugin/exercisefocused/pages/reporting.php
  15. 10
      plugin/exercisefocused/plugin.php
  16. 52
      plugin/exercisefocused/src/Controller/AdminController.php
  17. 86
      plugin/exercisefocused/src/Controller/BaseController.php
  18. 98
      plugin/exercisefocused/src/Controller/DetailController.php
  19. 97
      plugin/exercisefocused/src/Controller/LogController.php
  20. 138
      plugin/exercisefocused/src/Controller/ReportingController.php
  21. 99
      plugin/exercisefocused/src/Entity/Log.php
  22. 213
      plugin/exercisefocused/src/ExerciseFocusedPlugin.php
  23. 28
      plugin/exercisefocused/src/Repository/LogRepository.php
  24. 28
      plugin/exercisefocused/src/Traits/DetailControllerTrait.php
  25. 393
      plugin/exercisefocused/src/Traits/ReportingFilterTrait.php
  26. 105
      plugin/exercisefocused/templates/block.html.twig
  27. 163
      plugin/exercisefocused/templates/script.html.twig
  28. 5
      plugin/exercisefocused/uninstall.php
  29. 3
      plugin/exercisemonitoring/admin.php
  30. BIN
      plugin/exercisemonitoring/assets/images/idcard.png
  31. BIN
      plugin/exercisemonitoring/assets/images/user.png
  32. 97
      plugin/exercisemonitoring/cron/cleanup.php
  33. 61
      plugin/exercisemonitoring/index.php
  34. 5
      plugin/exercisemonitoring/install.php
  35. 32
      plugin/exercisemonitoring/lang/english.php
  36. 32
      plugin/exercisemonitoring/lang/spanish.php
  37. 32
      plugin/exercisemonitoring/pages/detail.php
  38. 18
      plugin/exercisemonitoring/pages/exercise_submit.ajax.php
  39. 18
      plugin/exercisemonitoring/pages/start.ajax.php
  40. 10
      plugin/exercisemonitoring/plugin.php
  41. 111
      plugin/exercisemonitoring/src/Controller/DetailController.php
  42. 119
      plugin/exercisemonitoring/src/Controller/ExerciseSubmitController.php
  43. 93
      plugin/exercisemonitoring/src/Controller/StartController.php
  44. 133
      plugin/exercisemonitoring/src/Entity/Log.php
  45. 185
      plugin/exercisemonitoring/src/ExerciseMonitoringPlugin.php
  46. 47
      plugin/exercisemonitoring/src/Repository/LogRepository.php
  47. 84
      plugin/exercisemonitoring/templates/exercise_submit.html.twig
  48. 314
      plugin/exercisemonitoring/templates/modal.html.twig
  49. 5
      plugin/exercisemonitoring/uninstall.php

@ -500,6 +500,8 @@ if ($is_allowedToEdit && $origin !== 'learnpath') {
'comparative_group_report.php?'.api_get_cidreq().'&id='.$exercise_id,
['class' => 'btn btn-default']
);
$actions .= ExerciseFocusedPlugin::create()->getLinkReporting($exercise_id);
}
} else {
$actions .= '<a href="exercise.php">'.

@ -90,12 +90,12 @@ switch ($action) {
$form->addHtmlEditor(
'name',
get_lang('TermName'),
false,
true,
false,
['ToolbarSet' => 'TitleAsHtml']
);
} else {
$form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']);
$form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']);
}
$form->addHtmlEditor(
@ -107,7 +107,6 @@ switch ($action) {
);
$form->addButtonCreate(get_lang('TermAddButton'), 'SubmitGlossary');
// setting the rules
$form->addRule('name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');
@ -154,12 +153,12 @@ switch ($action) {
$form->addHtmlEditor(
'name',
get_lang('TermName'),
false,
true,
false,
['ToolbarSet' => 'TitleAsHtml']
);
} else {
$form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']);
$form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']);
}
$form->addHtmlEditor(
@ -192,9 +191,6 @@ switch ($action) {
$form->addButtonUpdate(get_lang('TermUpdateButton'), 'SubmitGlossary');
$form->setDefaults($glossary_data);
// setting the rules
$form->addRule('name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@ -284,6 +284,8 @@ class TrackingCourseLog
$row[4] = $ip;
}
$row[5] = Security::remove_XSS($row[5]);
$resources[] = $row;
}
}

@ -2102,14 +2102,10 @@ function plain_url_filter($html, $mode = NO_HTML)
/**
* Prevent execution of event handlers in HTML elements.
*
* @param string $html
*
* @return string
*/
function attr_on_filter($html)
function attr_on_filter(string $html): string
{
$prefix = uniqid('data-cke-').'-';
$pattern = '/\s*on\w+=(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i';
return preg_replace('/\b(on[a-z]+)\b\s*=/i', '$1'.$prefix.'$2', $html);
return preg_replace($pattern, '', $html);
}

@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseFocused\Controller\AdminController;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_admin_script();
$em = Database::getManager();
$logRepository = $em->getRepository(Log::class);
$reportingController = new AdminController(
ExerciseFocusedPlugin::create(),
HttpRequest::createFromGlobals(),
$em,
$logRepository
);
try {
$response = $reportingController();
} catch (Exception $e) {
$response = HttpResponse::create('', HttpResponse::HTTP_FORBIDDEN);
}
$response->send();

@ -0,0 +1,48 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
$plugin = ExerciseFocusedPlugin::create();
$exerciseId = (int) ($_GET['exerciseId'] ?? 0);
$renderRegion = $plugin->isEnableForExercise($exerciseId);
if ($renderRegion) {
$_template['show_region'] = true;
$em = Database::getManager();
$existingExeId = (int) ChamiloSession::read('exe_id');
$trackingExercise = null;
if ($existingExeId) {
$trackingExercise = $em->find(TrackEExercises::class, $existingExeId);
}
$_template['sec_token'] = Security::get_token('exercisefocused');
if ('true' === $plugin->get(ExerciseFocusedPlugin::SETTING_ENABLE_OUTFOCUSED_LIMIT)) {
$logRepository = $em->getRepository(Log::class);
if ($trackingExercise) {
$countOutfocused = $logRepository->countByActionInExe($trackingExercise, Log::TYPE_OUTFOCUSED);
} else {
$countOutfocused = 0;
}
$_template['count_outfocused'] = $countOutfocused;
$_template['remaining_outfocused'] = (int) $plugin->get(ExerciseFocusedPlugin::SETTING_OUTFOCUSED_LIMIT) - $countOutfocused;
}
if ($trackingExercise) {
$exercise = new Exercise($trackingExercise->getCId());
if ($exercise->read($trackingExercise->getExeExoId())) {
$_template['exercise_type'] = (int) $exercise->selectType();
}
}
}

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
ExerciseFocusedPlugin::create()->install();

@ -0,0 +1,39 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Exercise Focused";
$strings['plugin_comment'] = "Show a message to return to the exercise when the user exits the Chamilo window/tab.";
$strings['tool_enable'] = "Enable tool";
$strings['enable_time_limit'] = 'Enable time limit';
$strings['time_limit'] = "Limit time";
$strings['time_limit_help'] = "Limit time (in seconds) to return to the exercise. After this time the exercise will be closed.";
$strings['enable_outfocused_limit'] = "Enable maximum of outfocused";
$strings['outfocused_limit'] = "Maximum number of outfocused allowed";
$strings['outfocused_limit_help'] = "Number of outfocused allowed. After this limit the exercise will be closed.";
$strings['session_field_filters'] = "Session field as filter";
$strings['session_field_filters_help'] = "Extra field names separeted by a comma.";
$strings['percentage_sampling'] = "Percentage of sampling attempts";
$strings['percentage_sampling_help'] = "A percentage of attempts will be selected for random review";
$strings['ReportByAttempts'] = "Exercise focused: Report by attempts";
$strings['YouHaveLeftTheExercise'] = "Careful! We detect that you have left the exam window.<br><br>You must return and complete it.";
$strings['YouHaveXTimeToReturn'] = "You have <span class=\"h3 text-danger\" id=\"time-limit-target\">%s</span> seconds to return";
$strings['YouAreAllowedXOutfocused'] = "You are allowed <span class=\"h3 text-danger\" id=\"outfocused-limit-target\">%d</span> outfocused";
$strings['OutfocusedLimitExceeded'] = "You have exceeded the allowed limit of outfocused";
$strings['SelectExercise'] = "Select exercise";
$strings['UnselectExercise'] = "Unselect exercise";
$strings['Returns'] = "Returns";
$strings['MaxOutfocusedReached'] = "Max outfocused reached";
$strings['TimeLimitReached'] = "Time limit reached";
$strings['Outfocused'] = "Outfocused";
$strings['Return'] = "Return";
$strings['Motive'] = "Motive";
$strings['AlertBeforeLeaving'] = "Please stay within the exam";
$strings['RandomSampling'] = "Random sampling";
$strings['WindowTitleOutfocused'] = '🚨 Stay within the exam!';
$strings['LevelReached'] = 'Level reached';
$strings['ExerciseStartDateAndTime'] = "Exercise start date and time";
$strings['ExerciseEndDateAndTime'] = "Exercise end date and time";
$strings['MotiveExerciseFinished'] = "Successfully completed the exam";

@ -0,0 +1,39 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Enfoque en el Ejercicio";
$strings['plugin_comment'] = "Mostrar un mensaje para regresar al ejercicio cuando el usuario sale de la ventana/pestaña de Chamilo.";
$strings['tool_enable'] = "Habilitar herramienta";
$strings['enable_time_limit'] = 'Habilitar límite de tiempo';
$strings['time_limit'] = "Límite de tiempo";
$strings['time_limit_help'] = "Límite el tiempo (en segundos) para regresar al ejercicio. Pasado este tiempo, el ejercicio se cerrará.";
$strings['enable_outfocused_limit'] = "Habilitar el máximo de desenfoque";
$strings['outfocused_limit'] = "Número máximo de desenfoques permitidos";
$strings['outfocused_limit_help'] = "Número de desenfoques permitidos. Después de este límite, el ejercicio se cerrará.";
$strings['session_field_filters'] = "Campo de sesión como filtro";
$strings['session_field_filters_help'] = "Nombres de campos adicionales separados por comas.";
$strings['percentage_sampling'] = "Porcentaje de intentos de muestreo";
$strings['percentage_sampling_help'] = "Se seleccionará un porcentaje de intentos para una revisión aleatoria";
$strings['ReportByAttempts'] = "Enfoque en el Ejercicio: Informe por intentos";
$strings['YouHaveLeftTheExercise'] = "¡Cuidado! Detectamos que has abandonado la ventana del examen.<br><br>Debes retornar y culminarlo.";
$strings['YouHaveXTimeToReturn'] = "Tienes <span class=\"h3 text-danger\" id=\"time-limit-target\">%s</span> segundos para regresar";
$strings['YouAreAllowedXOutfocused'] = "Se te permite <span class=\"h3 text-danger\" id=\"outfocused-limit-target\">%d</span> desenfoques";
$strings['OutfocusedLimitExceeded'] = "Has excedido el límite permitido de desenfoques";
$strings['SelectExercise'] = "Seleccionar ejercicio";
$strings['UnselectExercise'] = "Deseleccionar ejercicio";
$strings['Returns'] = "Regresos";
$strings['MaxOutfocusedReached'] = "Se ha alcanzado el máximo de desenfoques";
$strings['TimeLimitReached'] = "Se ha alcanzado el límite de tiempo";
$strings['Outfocused'] = "Desenfoques";
$strings['Return'] = "Regresos";
$strings['Motive'] = "Motivo";
$strings['AlertBeforeLeaving'] = "Por favor, mantente dentro del examen.";
$strings['RandomSampling'] = "Muestreo Aleatorio";
$strings['WindowTitleOutfocused'] = '🚨 Retorna y culmina tu examen';
$strings['LevelReached'] = 'Nivel alcanzado';
$strings['ExerciseStartDateAndTime'] = "Fecha y hora de inicio del ejercicio";
$strings['ExerciseEndDateAndTime'] = "Fecha y hora de finalización del ejercicio";
$strings['MotiveExerciseFinished'] = "Culminó exitosamente el examen";

@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseFocused\Controller\DetailController;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
require_once __DIR__.'/../../../main/inc/global.inc.php';
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$em = Database::getManager();
$logRepository = $em->getRepository(Log::class);
$detailController = new DetailController(
ExerciseFocusedPlugin::create(),
HttpRequest::createFromGlobals(),
$em,
$logRepository
);
try {
$response = $detailController();
} catch (Exception $e) {
$response = HttpResponse::create('', HttpResponse::HTTP_FORBIDDEN);
}
$response->send();

@ -0,0 +1,298 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEAttempt;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log as FocusedLog;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log as MonitoringLog;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr\Join;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$plugin = ExerciseFocusedPlugin::create();
$monitoringPlugin = ExerciseMonitoringPlugin::create();
$monitoringPluginIsEnabled = $monitoringPlugin->isEnabled(true);
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$focusedLogRepository = $em->getRepository(FocusedLog::class);
$attempsRepository = $em->getRepository(TrackEAttempt::class);
if (!$plugin->isEnabled(true)) {
api_not_allowed(true);
}
$params = $request->query->all();
$results = findResults($params, $em, $plugin);
$data = [];
/** @var array<string, mixed> $result */
foreach ($results as $result) {
/** @var TrackEExercises $trackExe */
$trackExe = $result['exe'];
$user = api_get_user_entity($trackExe->getExeUserId());
$outfocusedLimitCount = $focusedLogRepository->countByActionInExe($trackExe, FocusedLog::TYPE_OUTFOCUSED_LIMIT);
$timeLimitCount = $focusedLogRepository->countByActionInExe($trackExe, FocusedLog::TYPE_TIME_LIMIT);
$exercise = new Exercise($trackExe->getCId());
$exercise->read($trackExe->getExeExoId());
$quizType = (int) $exercise->selectType();
$data[] = [
get_lang('LoginName'),
$user->getUsername(),
];
$data[] = [
get_lang('Student'),
$user->getFirstname(),
$user->getLastname(),
];
if ($monitoringPluginIsEnabled
&& 'true' === $monitoringPlugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE)
) {
$fieldVariable = $monitoringPlugin->get(ExerciseMonitoringPlugin::SETTING_EXTRAFIELD_BIRTHDATE);
$birthdateValue = UserManager::get_extra_user_data_by_field($user->getId(), $fieldVariable);
$data[] = [
$monitoringPlugin->get_lang('Birthdate'),
$birthdateValue ? $birthdateValue[$fieldVariable] : '----',
$monitoringPlugin->isAdult($user->getId())
? $monitoringPlugin->get_lang('AdultStudent')
: $monitoringPlugin->get_lang('MinorStudent'),
];
}
if ($trackExe->getSessionId()) {
$data[] = [
get_lang('SessionName'),
api_get_session_entity($trackExe->getSessionId())->getName(),
];
}
$data[] = [
get_lang('CourseTitle'),
api_get_course_entity($trackExe->getCId())->getTitle(),
];
$data[] = [
get_lang('ExerciseName'),
$exercise->getUnformattedTitle(),
];
$data[] = [
$plugin->get_lang('ExerciseStartDateAndTime'),
api_get_local_time($result['exe']->getStartDate(), null, null, true, true, true),
];
$data[] = [
$plugin->get_lang('ExerciseEndDateAndTime'),
api_get_local_time($result['exe']->getExeDate(), null, null, true, true, true),
];
$data[] = [
get_lang('IP'),
$result['exe']->getUserIp(),
];
$data[] = [
$plugin->get_lang('Motive'),
$plugin->calculateMotive($outfocusedLimitCount, $timeLimitCount),
];
$data[] = [];
$data[] = [
$plugin->get_lang('LevelReached'),
get_lang('DateExo'),
get_lang('Score'),
$plugin->get_lang('Outfocused'),
$plugin->get_lang('Returns'),
$monitoringPluginIsEnabled ? $monitoringPlugin->get_lang('Snapshots') : '',
];
if (ONE_PER_PAGE === $quizType) {
$questionList = explode(',', $trackExe->getDataTracking());
foreach ($questionList as $idx => $questionId) {
$attempt = $attempsRepository->findOneBy(
['exeId' => $trackExe->getExeId(), 'questionId' => $questionId],
['tms' => 'DESC']
);
if (!$attempt) {
continue;
}
$result = $exercise->manage_answer(
$trackExe->getExeId(),
$questionId,
null,
'exercise_result',
false,
false,
true,
false,
$exercise->selectPropagateNeg()
);
$row = [
get_lang('QuestionNumber').' '.($idx + 1),
api_get_local_time($attempt->getTms()),
$result['score'].' / '.$result['weight'],
$focusedLogRepository->countByActionAndLevel($trackExe, FocusedLog::TYPE_OUTFOCUSED, $questionId),
$focusedLogRepository->countByActionAndLevel($trackExe, FocusedLog::TYPE_RETURN, $questionId),
getSnapshotListForLevel($questionId, $trackExe),
];
$data[] = $row;
}
} elseif (ALL_ON_ONE_PAGE === $quizType) {
}
$data[] = [];
$data[] = [];
$data[] = [];
}
Export::arrayToXls($data);
function getSessionIdFromFormValues(array $formValues, array $fieldVariableList): array
{
$fieldItemIdList = [];
$objFieldValue = new ExtraFieldValue('session');
foreach ($fieldVariableList as $fieldVariable) {
if (!isset($formValues["extra_$fieldVariable"])) {
continue;
}
$itemValues = $objFieldValue->get_item_id_from_field_variable_and_field_value(
$fieldVariable,
$formValues["extra_$fieldVariable"],
false,
false,
true
);
foreach ($itemValues as $itemValue) {
$fieldItemIdList[] = (int) $itemValue['item_id'];
}
}
return array_unique($fieldItemIdList);
}
function findResults(array $formValues, EntityManagerInterface $em, ExerciseFocusedPlugin $plugin)
{
$cId = api_get_course_int_id();
$sId = api_get_session_id();
$qb = $em->createQueryBuilder();
$qb
->select('te AS exe, q.title, te.startDate , u.firstname, u.lastname, u.username')
->from(TrackEExercises::class, 'te')
->innerJoin(CQuiz::class, 'q', Join::WITH, 'te.exeExoId = q.iid')
->innerJoin(User::class, 'u', Join::WITH, 'te.exeUserId = u.id');
$params = [];
if ($cId) {
$qb->andWhere($qb->expr()->eq('te.cId', ':cId'));
$params['cId'] = $cId;
$sessionItemIdList = $sId ? [$sId] : [];
} else {
$sessionItemIdList = getSessionIdFromFormValues(
$formValues,
$plugin->getSessionFieldList()
);
}
if ($sessionItemIdList) {
$qb->andWhere($qb->expr()->in('te.sessionId', ':sessionItemIdList'));
$params['sessionItemIdList'] = $sessionItemIdList;
}
if (!empty($formValues['username'])) {
$qb->andWhere($qb->expr()->eq('u.username', ':username'));
$params['username'] = $formValues['username'];
}
if (!empty($formValues['firstname'])) {
$qb->andWhere($qb->expr()->eq('u.firstname', ':firstname'));
$params['firstname'] = $formValues['firstname'];
}
if (!empty($formValues['lastname'])) {
$qb->andWhere($qb->expr()->eq('u.lastname', ':lastname'));
$params['lastname'] = $formValues['lastname'];
}
if (!empty($formValues['start_date'])) {
$qb->andWhere(
$qb->expr()->andX(
$qb->expr()->gte('te.startDate', ':start_date'),
$qb->expr()->lte('te.exeDate', ':end_date')
)
);
$params['start_date'] = api_get_utc_datetime($formValues['start_date'].' 00:00:00', false, true);
$params['end_date'] = api_get_utc_datetime($formValues['start_date'].' 23:59:59', false, true);
}
if (empty($params)) {
return [];
}
if ($cId && !empty($formValues['id'])) {
$qb->andWhere($qb->expr()->eq('q.iid', ':q_id'));
$params['q_id'] = $formValues['id'];
}
$qb->setParameters($params);
$query = $qb->getQuery();
return $query->getResult();
}
function getSnapshotListForLevel(int $level, TrackEExercises $trackExe): string
{
$monitoringPluginIsEnabled = ExerciseMonitoringPlugin::create()->isEnabled(true);
if (!$monitoringPluginIsEnabled) {
return '';
}
$user = api_get_user_entity($trackExe->getExeUserId());
$monitoringLogRepository = Database::getManager()->getRepository(MonitoringLog::class);
$monitoringLogsByQuestion = $monitoringLogRepository->findByLevelAndExe($level, $trackExe);
$snapshotList = [];
/** @var MonitoringLog $logByQuestion */
foreach ($monitoringLogsByQuestion as $logByQuestion) {
$snapshotUrl = ExerciseMonitoringPlugin::generateSnapshotUrl(
$user->getId(),
$logByQuestion->getImageFilename()
);
$snapshotList[] = api_get_local_time($logByQuestion->getCreatedAt()).' '.$snapshotUrl;
}
return implode(PHP_EOL, $snapshotList);
}

@ -0,0 +1,30 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseFocused\Controller\LogController;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
$em = Database::getManager();
$logRepository = $em->getRepository(Log::class);
$logController = new LogController(
ExerciseFocusedPlugin::create(),
HttpRequest::createFromGlobals(),
$em,
$logRepository
);
try {
$response = $logController();
} catch (Exception $e) {
$response = HttpResponse::create('', HttpResponse::HTTP_FORBIDDEN);
}
$response->send();

@ -0,0 +1,34 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseFocused\Controller\ReportingController;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$em = Database::getManager();
$logRepository = $em->getRepository(Log::class);
$startController = new ReportingController(
ExerciseFocusedPlugin::create(),
HttpRequest::createFromGlobals(),
$em,
$logRepository
);
//try {
$response = $startController();
//} catch (Exception $e) {
//$response = HttpResponse::create('', HttpResponse::HTTP_FORBIDDEN);
//}
$response->send();

@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
$plugin_info = ExerciseFocusedPlugin::create()->get_info();
$plugin_info['templates'] = [
'templates/script.html.twig',
'templates/block.html.twig',
];

@ -0,0 +1,52 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Controller;
use Chamilo\PluginBundle\ExerciseFocused\Traits\ReportingFilterTrait;
use Display;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class AdminController extends BaseController
{
use ReportingFilterTrait;
public function __invoke(): HttpResponse
{
parent::__invoke();
$form = $this->createForm();
$results = [];
if ($form->validate()) {
$results = $this->findResults(
$form->exportValues()
);
}
$table = $this->createTable($results);
$content = $form->returnForm()
.Display::page_subheader2($this->plugin->get_lang('ReportByAttempts'))
.$table->toHtml();
$this->setBreadcrumb();
return $this->renderView(
$this->plugin->get_title(),
$content
);
}
private function setBreadcrumb()
{
$codePath = api_get_path(WEB_CODE_PATH);
$GLOBALS['interbreadcrumb'][] = [
'url' => $codePath.'admin/index.php',
'name' => get_lang('Administration'),
];
}
}

@ -0,0 +1,86 @@
<?php
namespace Chamilo\PluginBundle\ExerciseFocused\Controller;
use Chamilo\PluginBundle\ExerciseFocused\Repository\LogRepository;
use Doctrine\ORM\EntityManager;
use Exception;
use ExerciseFocusedPlugin;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
use Template;
abstract class BaseController
{
/**
* @var ExerciseFocusedPlugin
*/
protected $plugin;
/**
* @var HttpRequest
*/
protected $request;
/**
* @var EntityManager
*/
protected $em;
/**
* @var LogRepository
*/
protected $logRepository;
/**
* @var Template
*/
protected $template;
public function __construct(
ExerciseFocusedPlugin $plugin,
HttpRequest $request,
EntityManager $em,
LogRepository $logRepository
) {
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
$this->logRepository = $logRepository;
}
/**
* @throws Exception
*/
public function __invoke(): HttpResponse
{
if (!$this->plugin->isEnabled(true)) {
throw new Exception();
}
return HttpResponse::create();
}
protected function renderView(
string $title,
string $content,
?string $header = null,
array $actions = []
): HttpResponse {
if (!$header) {
$header = $title;
}
$this->template = new Template($title);
$this->template->assign('header', $header);
$this->template->assign('actions', implode(PHP_EOL, $actions));
$this->template->assign('content', $content);
ob_start();
$this->template->display_one_col_template();
$html = ob_get_contents();
ob_end_clean();
return HttpResponse::create($html);
}
}

@ -0,0 +1,98 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Controller;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Chamilo\PluginBundle\ExerciseFocused\Traits\DetailControllerTrait;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
use Exception;
use Exercise;
use HTML_Table;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class DetailController extends BaseController
{
use DetailControllerTrait;
/**
* @throws OptimisticLockException
* @throws TransactionRequiredException
* @throws ORMException
* @throws Exception
*/
public function __invoke(): HttpResponse
{
parent::__invoke();
$exeId = $this->request->query->getInt('id');
$exe = $this->em->find(TrackEExercises::class, $exeId);
if (!$exe) {
throw new Exception();
}
$user = api_get_user_entity($exe->getExeUserId());
$objExercise = new Exercise($exe->getCId());
$objExercise->read($exe->getExeExoId());
$logs = $this->logRepository->findBy(['exe' => $exe], ['updatedAt' => 'ASC']);
$table = $this->getTable($objExercise, $logs);
$content = $this->generateHeader($objExercise, $user, $exe)
.'<hr>'
.$table->toHtml();
return HttpResponse::create($content);
}
/**
* @param array<int, Log> $logs
*
* @return void
*/
private function getTable(Exercise $objExercise, array $logs): HTML_Table
{
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$table->setHeaderContents(0, 0, get_lang('Action'));
$table->setHeaderContents(0, 1, get_lang('DateTime'));
$table->setHeaderContents(0, 2, $this->plugin->get_lang('LevelReached'));
$row = 1;
foreach ($logs as $log) {
$strLevel = '';
if (ONE_PER_PAGE == $objExercise->selectType()) {
try {
$question = $this->em->find(CQuizQuestion::class, $log->getLevel());
$strLevel = $question->getQuestion();
} catch (Exception $exception) {
}
}
$table->setCellContents(
$row,
0,
$this->plugin->getActionTitle($log->getAction())
);
$table->setCellContents(
$row,
1,
api_get_local_time($log->getCreatedAt(), null, null, true, true, true)
);
$table->setCellContents($row, 2, $strLevel);
$row++;
}
return $table;
}
}

@ -0,0 +1,97 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Controller;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use ChamiloSession;
use Exception;
use Exercise;
use ExerciseFocusedPlugin;
use Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class LogController extends BaseController
{
public const VALID_ACTIONS = [
Log::TYPE_OUTFOCUSED,
Log::TYPE_RETURN,
Log::TYPE_OUTFOCUSED_LIMIT,
Log::TYPE_TIME_LIMIT,
];
/**
* @throws Exception
*/
public function __invoke(): Response
{
parent::__invoke();
$tokenIsValid = Security::check_token('get', null, 'exercisefocused');
if (!$tokenIsValid) {
throw new Exception('token invalid');
}
$action = $this->request->query->get('action');
$levelId = $this->request->query->getInt('level_id');
$exeId = (int) ChamiloSession::read('exe_id');
if (!in_array($action, self::VALID_ACTIONS)) {
throw new Exception('action invalid');
}
$trackingExercise = $this->em->find(TrackEExercises::class, $exeId);
if (!$trackingExercise) {
throw new Exception('no exercise attempt');
}
$objExercise = new Exercise($trackingExercise->getCId());
$objExercise->read($trackingExercise->getExeExoId());
$level = 0;
if (ONE_PER_PAGE == $objExercise->selectType()) {
$question = $this->em->find(CQuizQuestion::class, $levelId);
if (!$question) {
throw new Exception('Invalid level');
}
$level = $question->getIid();
}
$log = new Log();
$log
->setAction($action)
->setExe($trackingExercise)
->setLevel($level);
$this->em->persist($log);
$this->em->flush();
$remainingOutfocused = -1;
if ('true' === $this->plugin->get(ExerciseFocusedPlugin::SETTING_ENABLE_OUTFOCUSED_LIMIT)) {
$countOutfocused = $this->logRepository->countByActionInExe($trackingExercise, Log::TYPE_OUTFOCUSED);
$remainingOutfocused = (int) $this->plugin->get(ExerciseFocusedPlugin::SETTING_OUTFOCUSED_LIMIT) - $countOutfocused;
}
$exercise = new Exercise(api_get_course_int_id());
$exercise->read($trackingExercise->getExeExoId());
$json = [
'sec_token' => Security::get_token('exercisefocused'),
'remainingOutfocused' => $remainingOutfocused,
];
return JsonResponse::create($json);
}
}

@ -0,0 +1,138 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Controller;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseFocused\Traits\ReportingFilterTrait;
use Display;
use Exception;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class ReportingController extends BaseController
{
use ReportingFilterTrait;
public function __invoke(): HttpResponse
{
parent::__invoke();
$exercise = $this->em->find(
CQuiz::class,
$this->request->query->getInt('id')
);
if (!$exercise) {
throw new Exception();
}
$courseCode = api_get_course_id();
$sessionId = api_get_session_id();
$tab1 = $this->generateTabResume($exercise);
$tab2 = $this->generateTabSearch($exercise, $courseCode, $sessionId);
$tab3 = $this->generateTabSampling($exercise);
$content = Display::tabs(
[
$this->plugin->get_lang('ReportByAttempts'),
get_lang('Search'),
$this->plugin->get_lang('RandomSampling'),
],
[$tab1, $tab2, $tab3],
'exercise-focused-tabs',
[],
[],
isset($_GET['submit']) ? 2 : 1
);
$this->setBreadcrumb($exercise->getId());
return $this->renderView(
$this->plugin->get_lang('ReportByAttempts'),
$content,
$exercise->getTitle()
);
}
private function generateTabResume(CQuiz $exercise): string
{
$results = $this->findResultsInCourse($exercise->getId());
return $this->createTable($results)->toHtml();
}
/**
* @throws Exception
*/
private function generateTabSearch(CQuiz $exercise, string $courseCode, int $sessionId): string
{
$form = $this->createForm();
$form->updateAttributes(['action' => api_get_self().'?'.api_get_cidreq().'&id='.$exercise->getId()]);
$form->addHidden('cidReq', $courseCode);
$form->addHidden('id_session', $sessionId);
$form->addHidden('gidReq', 0);
$form->addHidden('gradebook', 0);
$form->addHidden('origin', api_get_origin());
$form->addHidden('id', $exercise->getId());
$tableHtml = '';
$actions = '';
if ($form->validate()) {
$formValues = $form->exportValues();
$actionLeft = Display::url(
Display::return_icon('export_excel.png', get_lang('ExportExcel'), [], ICON_SIZE_MEDIUM),
api_get_path(WEB_PLUGIN_PATH).'exercisefocused/pages/export.php?'.http_build_query($formValues)
);
$actionRight = Display::toolbarButton(
get_lang('Clean'),
api_get_path(WEB_PLUGIN_PATH)
.'exercisefocused/pages/reporting.php?'
.api_get_cidreq().'&'.http_build_query(['id' => $exercise->getId(), 'submit' => '']),
'search'
);
$actions = Display::toolbarAction(
'em-actions',
[$actionLeft, $actionRight]
);
$results = $this->findResults($formValues);
$tableHtml = $this->createTable($results)->toHtml();
}
return $form->returnForm().$actions.$tableHtml;
}
private function generateTabSampling(CQuiz $exercise): string
{
$results = $this->findRandomResults($exercise->getId());
return $this->createTable($results)->toHtml();
}
/**
* @return array<int, TrackEExercises>
*/
private function setBreadcrumb($exerciseId): void
{
$codePath = api_get_path('WEB_CODE_PATH');
$cidReq = api_get_cidreq();
$GLOBALS['interbreadcrumb'][] = [
'url' => $codePath."exercise/exercise.php?$cidReq",
'name' => get_lang('Exercises'),
];
$GLOBALS['interbreadcrumb'][] = [
'url' => $codePath."exercise/exercise_report.php?$cidReq&".http_build_query(['exerciseId' => $exerciseId]),
'name' => get_lang('StudentScore'),
];
}
}

@ -0,0 +1,99 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Entity;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class EmbedRegistry.
*
* @package Chamilo\PluginBundle\Entity\EmbedRegistry
*
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\ExerciseFocused\Repository\LogRepository")
* @ORM\Table(name="plugin_exercisefocused_log")
*/
class Log
{
use TimestampableTypedEntity;
public const TYPE_RETURN = 'return';
public const TYPE_OUTFOCUSED = 'outfocused';
public const TYPE_OUTFOCUSED_LIMIT = 'outfocused_limit';
public const TYPE_TIME_LIMIT = 'time_limit';
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @var TrackEExercises
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\TrackEExercises")
* @ORM\JoinColumn(name="exe_id", referencedColumnName="exe_id", onDelete="SET NULL")
*/
private $exe;
/**
* @var int
*
* @ORM\Column(name="level", type="integer")
*/
private $level;
/**
* @var string
*
* @ORM\Column(name="action", type="string", nullable=false)
*/
private $action;
public function getId(): int
{
return $this->id;
}
public function getExe(): TrackEExercises
{
return $this->exe;
}
public function setExe(TrackEExercises $exe): Log
{
$this->exe = $exe;
return $this;
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel(int $level): self
{
$this->level = $level;
return $this;
}
public function getAction(): string
{
return $this->action;
}
public function setAction(string $action): Log
{
$this->action = $action;
return $this;
}
}

@ -0,0 +1,213 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CTool;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
class ExerciseFocusedPlugin extends Plugin
{
public const SETTING_TOOL_ENABLE = 'tool_enable';
public const SETTING_ENABLE_TIME_LIMIT = 'enable_time_limit';
public const SETTING_TIME_LIMIT = 'time_limit';
public const SETTING_ENABLE_OUTFOCUSED_LIMIT = 'enable_outfocused_limit';
public const SETTING_OUTFOCUSED_LIMIT = 'outfocused_limit';
public const SETTING_SESSION_FIELD_FILTERS = 'session_field_filters';
public const SETTING_PERCENTAGE_SAMPLING = 'percentage_sampling';
public const FIELD_SELECTED = 'exercisefocused_selected';
private const TABLE_LOG = 'plugin_exercisefocused_log';
protected function __construct()
{
$settings = [
self::SETTING_TOOL_ENABLE => 'boolean',
self::SETTING_ENABLE_TIME_LIMIT => 'boolean',
self::SETTING_TIME_LIMIT => 'text',
self::SETTING_ENABLE_OUTFOCUSED_LIMIT => 'boolean',
self::SETTING_OUTFOCUSED_LIMIT => 'text',
self::SETTING_SESSION_FIELD_FILTERS => 'text',
self::SETTING_PERCENTAGE_SAMPLING => 'text',
];
parent::__construct(
"0.0.1",
"Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>",
$settings
);
}
public static function create(): ?ExerciseFocusedPlugin
{
static $result = null;
return $result ?: $result = new self();
}
/**
* @throws ToolsException
*/
public function install()
{
$em = Database::getManager();
if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->createSchema(
[
$em->getClassMetadata(Log::class),
]
);
$objField = new ExtraField('exercise');
$objField->save([
'variable' => self::FIELD_SELECTED,
'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
'display_text' => $this->get_title(),
'visible_to_self' => true,
'changeable' => true,
'filter' => false,
]);
}
public function uninstall()
{
$em = Database::getManager();
if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
$em->getClassMetadata(Log::class),
]
);
$objField = new ExtraField('exercise');
$extraFieldInfo = $objField->get_handler_field_info_by_field_variable(self::FIELD_SELECTED);
if ($extraFieldInfo) {
$objField->delete($extraFieldInfo['id']);
}
}
public function getAdminUrl(): string
{
$name = $this->get_name();
$webPath = api_get_path(WEB_PLUGIN_PATH).$name;
return "$webPath/admin.php";
}
public function getActionTitle($action): string
{
switch ($action) {
case Log::TYPE_OUTFOCUSED:
return $this->get_lang('Outfocused');
case Log::TYPE_RETURN:
return $this->get_lang('Return');
case Log::TYPE_OUTFOCUSED_LIMIT:
return $this->get_lang('MaxOutfocusedReached');
case Log::TYPE_TIME_LIMIT:
return $this->get_lang('TimeLimitReached');
}
return '';
}
public function getLinkReporting(int $exerciseId): string
{
if (!$this->isEnabled(true)) {
return '';
}
$values = (new ExtraFieldValue('exercise'))
->get_values_by_handler_and_field_variable($exerciseId, self::FIELD_SELECTED);
if (!$values || !$values['value']) {
return '';
}
$icon = Display::return_icon(
'window_list_slide.png',
$this->get_lang('ReportByAttempts'),
[],
ICON_SIZE_MEDIUM
);
$url = api_get_path(WEB_PLUGIN_PATH)
.'exercisefocused/pages/reporting.php?'
.api_get_cidreq().'&'.http_build_query(['id' => $exerciseId]);
return Display::url($icon, $url);
}
public function getSessionFieldList(): array
{
$settingField = $this->get(self::SETTING_SESSION_FIELD_FILTERS);
$fields = explode(',', $settingField);
return array_map('trim', $fields);
}
public function isEnableForExercise(int $exerciseId): bool
{
$renderRegion = $this->isEnabled(true)
&& strpos($_SERVER['SCRIPT_NAME'], '/main/exercise/exercise_submit.php') !== false;
if (!$renderRegion) {
return false;
}
$objFieldValue = new ExtraFieldValue('exercise');
$values = $objFieldValue->get_values_by_handler_and_field_variable(
$exerciseId,
self::FIELD_SELECTED
);
return $values && (bool) $values['value'];
}
public function calculateMotive(int $outfocusedLimitCount, int $timeLimitCount)
{
$motive = $this->get_lang('MotiveExerciseFinished');
if ($outfocusedLimitCount > 0) {
$motive = $this->get_lang('MaxOutfocusedReached');
}
if ($timeLimitCount > 0) {
$motive = $this->get_lang('TimeLimitReached');
}
return $motive;
}
protected function createLinkToCourseTool($name, $courseId, $iconName = null, $link = null, $sessionId = 0, $category = 'plugin'): ?CTool
{
$tool = parent::createLinkToCourseTool($name, $courseId, $iconName, $link, $sessionId, $category);
if (!$tool) {
return null;
}
$tool->setName(
$tool->getName().':teacher'
);
$em = Database::getManager();
$em->persist($tool);
$em->flush();
return $tool;
}
}

@ -0,0 +1,28 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Repository;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Doctrine\ORM\EntityRepository;
class LogRepository extends EntityRepository
{
public function countByActionInExe(TrackEExercises $exe, string $action): int
{
return $this->count([
'exe' => $exe,
'action' => $action,
]);
}
public function countByActionAndLevel(TrackEExercises $exe, string $action, int $level): int
{
return $this->count([
'exe' => $exe,
'action' => $action,
'level' => $level,
]);
}
}

@ -0,0 +1,28 @@
<?php
/* For license terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Traits;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\UserBundle\Entity\User;
use Display;
use Exercise;
trait DetailControllerTrait
{
private function generateHeader(Exercise $objExercise, User $student, TrackEExercises $trackExe): string
{
$startDate = api_get_local_time($trackExe->getStartDate(), null, null, true, true, true);
$endDate = api_get_local_time($trackExe->getExeDate(), null, null, true, true, true);
return Display::page_subheader2($objExercise->selectTitle())
.Display::tag('p', $student->getCompleteNameWithUsername(), ['class' => 'lead'])
.Display::tag(
'p',
sprintf(get_lang('QuizRemindStartDate'), $startDate)
.sprintf(get_lang('QuizRemindEndDate'), $endDate)
.sprintf(get_lang('QuizRemindDuration'), api_format_time($trackExe->getExeDuration()))
);
}
}

@ -0,0 +1,393 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseFocused\Traits;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseFocused\Entity\Log;
use Chamilo\UserBundle\Entity\User;
use Database;
use Display;
use Doctrine\ORM\Query\Expr\Join;
use Exception;
use ExerciseFocusedPlugin;
use ExerciseMonitoringPlugin;
use ExtraField;
use ExtraFieldValue;
use FormValidator;
use HTML_Table;
trait ReportingFilterTrait
{
/**
* @throws Exception
*/
protected function createForm(): FormValidator
{
$extraFieldNameList = $this->plugin->getSessionFieldList();
$cId = api_get_course_int_id();
$sessionId = api_get_session_id();
$form = new FormValidator('exercisefocused', 'get');
$form->addText('username', get_lang('LoginName'), false);
$form->addText('firstname', get_lang('FirstName'), false);
$form->addText('lastname', get_lang('LastName'), false);
if ($extraFieldNameList && ($sessionId || !$cId)) {
(new ExtraField('session'))
->addElements(
$form,
$sessionId,
[],
false,
false,
$extraFieldNameList
);
$extraNames = [];
foreach ($extraFieldNameList as $key => $value) {
$extraNames[$key] = "extra_$value";
}
if ($sessionId) {
$form->freeze($extraNames);
}
}
$form->addDatePicker('start_date', get_lang('StartDate'));
$form->addButtonSearch(get_lang('Search'));
//$form->protect();
return $form;
}
/**
* @throws Exception
*/
protected function findResults(array $formValues = []): array
{
$cId = api_get_course_int_id();
$sId = api_get_session_id();
$qb = $this->em->createQueryBuilder();
$qb
->select('te AS exe, q.title, te.startDate, u.id AS user_id, u.firstname, u.lastname, u.username, te.sessionId, te.cId')
->from(TrackEExercises::class, 'te')
->innerJoin(CQuiz::class, 'q', Join::WITH, 'te.exeExoId = q.iid')
->innerJoin(User::class, 'u', Join::WITH, 'te.exeUserId = u.id');
$params = [];
if ($cId) {
$qb->andWhere($qb->expr()->eq('te.cId', ':cId'));
$params['cId'] = $cId;
$sessionItemIdList = $sId ? [$sId] : [];
} else {
$sessionItemIdList = $this->getSessionIdFromFormValues(
$formValues,
$this->plugin->getSessionFieldList()
);
}
if ($sessionItemIdList) {
$qb->andWhere($qb->expr()->in('te.sessionId', ':sessionItemIdList'));
$params['sessionItemIdList'] = $sessionItemIdList;
}
if (!empty($formValues['username'])) {
$qb->andWhere($qb->expr()->eq('u.username', ':username'));
$params['username'] = $formValues['username'];
}
if (!empty($formValues['firstname'])) {
$qb->andWhere($qb->expr()->like('u.firstname', ':firstname'));
$params['firstname'] = $formValues['firstname'].'%';
}
if (!empty($formValues['lastname'])) {
$qb->andWhere($qb->expr()->like('u.lastname', ':lastname'));
$params['lastname'] = $formValues['lastname'].'%';
}
if (!empty($formValues['start_date'])) {
$qb->andWhere(
$qb->expr()->andX(
$qb->expr()->gte('te.startDate', ':start_date'),
$qb->expr()->lte('te.exeDate', ':end_date')
)
);
$params['start_date'] = api_get_utc_datetime($formValues['start_date'].' 00:00:00', false, true);
$params['end_date'] = api_get_utc_datetime($formValues['start_date'].' 23:59:59', false, true);
}
if (empty($params)) {
return [];
}
if ($cId && !empty($formValues['id'])) {
$qb->andWhere($qb->expr()->eq('q.iid', ':q_id'));
$params['q_id'] = $formValues['id'];
}
$qb->setParameters($params);
return $this->formatResults(
$qb->getQuery()->getResult()
);
}
protected function formatResults(array $queryResults): array
{
$results = [];
foreach ($queryResults as $value) {
$outfocusedCount = $this->logRepository->countByActionInExe($value['exe'], Log::TYPE_OUTFOCUSED);
$returnCount = $this->logRepository->countByActionInExe($value['exe'], Log::TYPE_RETURN);
$outfocusedLimitCount = $this->logRepository->countByActionInExe($value['exe'], Log::TYPE_OUTFOCUSED_LIMIT);
$timeLimitCount = $this->logRepository->countByActionInExe($value['exe'], Log::TYPE_TIME_LIMIT);
$class = 'success';
$motive = $this->plugin->get_lang('MotiveExerciseFinished');
if ($outfocusedCount > 0 || $returnCount > 0) {
$class = 'warning';
}
if ($outfocusedLimitCount > 0 || $timeLimitCount > 0) {
$class = 'danger';
if ($outfocusedLimitCount > 0) {
$motive = $this->plugin->get_lang('MaxOutfocusedReached');
}
if ($timeLimitCount > 0) {
$motive = $this->plugin->get_lang('TimeLimitReached');
}
}
$session = api_get_session_entity($value['sessionId']);
$course = api_get_course_entity($value['cId']);
$results[] = [
'id' => $value['exe']->getExeId(),
'quiz_title' => $value['title'],
'user_id' => $value['user_id'],
'username' => $value['username'],
'firstname' => $value['firstname'],
'lastname' => $value['lastname'],
'start_date' => $value['exe']->getStartDate(),
'end_date' => $value['exe']->getExeDate(),
'count_outfocused' => $outfocusedCount,
'count_return' => $returnCount,
'motive' => Display::span($motive, ['class' => "text-$class"]),
'class' => $class,
'session_name' => $session ? $session->getName() : null,
'course_title' => $course->getTitle(),
];
}
return $results;
}
protected function createTable(array $resultData): HTML_Table
{
$courseId = api_get_course_int_id();
$pluginMonitoring = ExerciseMonitoringPlugin::create();
$isPluginMonitoringEnabled = $pluginMonitoring->isEnabled(true);
$detailIcon = Display::return_icon('forum_listview.png', get_lang('Detail'));
$urlDetail = api_get_path(WEB_PLUGIN_PATH).'exercisefocused/pages/detail.php?'.api_get_cidreq().'&';
$tableHeaders = [];
$tableHeaders[] = get_lang('LoginName');
$tableHeaders[] = get_lang('FirstName');
$tableHeaders[] = get_lang('LastName');
if (!$courseId) {
$tableHeaders[] = get_lang('SessionName');
$tableHeaders[] = get_lang('CourseTitle');
$tableHeaders[] = get_lang('ExerciseName');
}
$tableHeaders[] = $this->plugin->get_lang('ExerciseStartDateAndTime');
$tableHeaders[] = $this->plugin->get_lang('ExerciseEndDateAndTime');
$tableHeaders[] = $this->plugin->get_lang('Outfocused');
$tableHeaders[] = $this->plugin->get_lang('Returns');
$tableHeaders[] = $this->plugin->get_lang('Motive');
$tableHeaders[] = get_lang('Actions');
$tableData = [];
foreach ($resultData as $result) {
$actionLinks = Display::url(
$detailIcon,
$urlDetail.http_build_query(['id' => $result['id']]),
[
'class' => 'ajax',
'data-title' => get_lang('Detail'),
]
);
if ($isPluginMonitoringEnabled) {
$actionLinks .= $pluginMonitoring->generateDetailLink(
(int) $result['id'],
$result['user_id']
);
}
$row = [];
$row[] = $result['username'];
$row[] = $result['firstname'];
$row[] = $result['lastname'];
if (!$courseId) {
$row[] = $result['session_name'];
$row[] = $result['course_title'];
$row[] = $result['quiz_title'];
}
$row[] = api_get_local_time($result['start_date'], null, null, true, true, true);
$row[] = api_get_local_time($result['end_date'], null, null, true, true, true);
$row[] = $result['count_outfocused'];
$row[] = $result['count_return'];
$row[] = $result['motive'];
$row[] = $actionLinks;
$tableData[] = $row;
}
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$table->setHeaders($tableHeaders);
$table->setData($tableData);
$table->setColAttributes($courseId ? 3 : 6, ['class' => 'text-center']);
$table->setColAttributes($courseId ? 4 : 7, ['class' => 'text-center']);
$table->setColAttributes($courseId ? 5 : 8, ['class' => 'text-right']);
$table->setColAttributes($courseId ? 6 : 9, ['class' => 'text-right']);
$table->setColAttributes($courseId ? 7 : 10, ['class' => 'text-center']);
$table->setColAttributes($courseId ? 8 : 11, ['class' => 'text-right']);
foreach ($resultData as $idx => $result) {
$table->setRowAttributes($idx + 1, ['class' => $result['class']], true);
}
return $table;
}
protected function findResultsInCourse(int $exerciseId, bool $randomResults = false): array
{
$exeIdList = $this->getAttemptsIdForExercise($exerciseId);
if ($randomResults) {
$exeIdList = $this->pickRandomAttempts($exeIdList) ?: $exeIdList;
}
if (empty($exeIdList)) {
return [];
}
$qb = $this->em->createQueryBuilder();
$qb
->select('te AS exe, q.title, te.startDate, u.id AS user_id, u.firstname, u.lastname, u.username, te.sessionId, te.cId')
->from(TrackEExercises::class, 'te')
->innerJoin(CQuiz::class, 'q', Join::WITH, 'te.exeExoId = q.iid')
->innerJoin(User::class, 'u', Join::WITH, 'te.exeUserId = u.id')
->andWhere(
$qb->expr()->in('te.exeId', $exeIdList)
)
->addOrderBy('te.startDate');
return $this->formatResults(
$qb->getQuery()->getResult()
);
}
protected function findRandomResults(int $exerciseId): array
{
return $this->findResultsInCourse($exerciseId, true);
}
private function getSessionIdFromFormValues(array $formValues, array $fieldVariableList): array
{
$fieldItemIdList = [];
$objFieldValue = new ExtraFieldValue('session');
foreach ($fieldVariableList as $fieldVariable) {
if (!isset($formValues["extra_$fieldVariable"])) {
continue;
}
$itemValues = $objFieldValue->get_item_id_from_field_variable_and_field_value(
$fieldVariable,
$formValues["extra_$fieldVariable"],
false,
false,
true
);
foreach ($itemValues as $itemValue) {
$fieldItemIdList[] = (int) $itemValue['item_id'];
}
}
return array_unique($fieldItemIdList);
}
private function getAttemptsIdForExercise(int $exerciseId): array
{
$cId = api_get_course_int_id();
$sId = api_get_session_id();
$tblTrackExe = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$sessionCondition = api_get_session_condition($sId);
$result = Database::query(
"SELECT exe_id FROM $tblTrackExe
WHERE c_id = $cId
AND exe_exo_id = $exerciseId
$sessionCondition
ORDER BY exe_id"
);
return array_column(
Database::store_result($result),
'exe_id'
);
}
private function pickRandomAttempts(array $attemptIdList): array
{
$settingPercentage = (int) $this->plugin->get(ExerciseFocusedPlugin::SETTING_PERCENTAGE_SAMPLING);
if (!$settingPercentage) {
return [];
}
$percentage = count($attemptIdList) * ($settingPercentage / 100);
$round = round($percentage) ?: 1;
$random = (array) array_rand($attemptIdList, $round);
$selection = [];
foreach ($random as $rand) {
$selection[] = $attemptIdList[$rand];
}
return $selection;
}
}

@ -0,0 +1,105 @@
{% if exercisefocused.show_region %}
{% set enable_time_limit = 'true' == exercisefocused.plugin_info.obj.get('enable_time_limit') %}
{% set time_limit = exercisefocused.plugin_info.obj.get('time_limit') %}
{% set enable_outfocused_limit = 'true' == exercisefocused.plugin_info.obj.get('enable_outfocused_limit') %}
{% set outfocused_limit = exercisefocused.plugin_info.obj.get('outfocused_limit') %}
<div id="exercisefocused-block" style="display: none;">
<div class="exercisefocused-block__container">
<div class="exercisefocused-block__message card">
<p class="h3 text-danger">{{ 'YouHaveLeftTheExercise'|get_plugin_lang('ExerciseFocusedPlugin') }}</p>
{% if enable_time_limit %}
<div id="time-limit-block">
<p class="h4">
{{ 'YouHaveXTimeToReturn'|get_plugin_lang('ExerciseFocusedPlugin')|format(time_limit) }}
</p>
</div>
{% endif %}
{% if enable_outfocused_limit %}
<div id="outfocused-limit-block">
<p class="h4">{{ 'YouAreAllowedXOutfocused'|get_plugin_lang('ExerciseFocusedPlugin')|format(outfocused_limit) }}</p>
</div>
{% endif %}
</div>
</div>
</div>
<style>
#exercisefocused-block {
background-color: #CCCC;
border: 1px solid #DDD;
bottom: 0;
left: 0;
min-height: 100%;
min-width: 100%;
position: fixed;
right: 0;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
top: 0;
z-index: 1010;
}
.exercisefocused-block__container {
left: 50%;
position: absolute;
text-align: center;
top: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
}
.exercisefocused-block__message {
margin-bottom: 0;
padding: 20px;
}
.exercisefocused-block__message p,
.exercisefocused-block__message span {
font-weight: bold;
}
.exercisefocused-backdrop {
background-color: rgba(0, 0, 0, 0.02);
cursor: not-allowed;
height: 100%;
position: fixed;
top: 0;
user-select: none;
width: 100%;
z-index: 1000;
}
.exercisefocused-backdrop::before {
background-color: #FFF;
border-radius: 6px;
box-shadow: 0 2px 2px rgba(204, 197, 185, 0.5);
content: attr(data-alert);
display: block;
font-size: 18px;
font-weight: 700;
margin: 50px auto;
opacity: 0;
padding: 10px;
position: relative;
text-align: center;
width: 340px;
transition: opacity 0s ease-in-out;
}
.exercisefocused-backdrop:hover::before {
opacity: 1;
}
.exercisefocused-backdrop.inmediate::before {
transition-delay: 3.5s !important;
}
.exercisefocused-backdrop.out::before {
transition-delay: 0s !important;
}
form#exercise_form {
background-color: #FFF;
position: relative;
z-index: 1005;
}
</style>
{% endif %}

@ -0,0 +1,163 @@
{% if exercisefocused.show_region %}
{% set enable_time_limit = 'true' == exercisefocused.plugin_info.obj.get('enable_time_limit') %}
{% set time_limit = exercisefocused.plugin_info.obj.get('time_limit') %}
{% set enable_outfocused_limit = 'true' == exercisefocused.plugin_info.obj.get('enable_outfocused_limit') %}
{% set outfocused_limit = exercisefocused.plugin_info.obj.get('outfocused_limit') %}
{% set ALL_ON_ONE_PAGE = exercisefocused.exercise_type == 1 %}
{% set ONE_PER_PAGE = exercisefocused.exercise_type == 2 %}
<script>
$(function () {
var $exerciseFocused = $("#exercisefocused-block").appendTo('body');
var $timeLimitBlock = $exerciseFocused.find('#time-limit-block');
var $timeLimitTarget = $exerciseFocused.find('#time-limit-target');
var $outfocusedLimitBlock = $exerciseFocused.find('#outfocused-limit-block');
var $outfocusedLimitTarget = $exerciseFocused.find('#outfocused-limit-target');
var $backdrop = $('<div>')
.addClass('exercisefocused-backdrop text-danger')
.attr('data-alert', '{{ 'AlertBeforeLeaving'|get_plugin_lang('ExerciseFocusedPlugin') }}')
.hover(
function () {$backdrop.removeClass('out').addClass('inmediate'); },
function () { $backdrop.addClass('out').removeClass('inmediate'); }
);
var $btnSaveNow = $('button[name="save_now"]');
$backdrop.appendTo('body');
var secToken = "{{ exercisefocused.sec_token }}";
var initDocumentTitle = document.title;
var countdownInterval;
var remainingTime;
var enableTimeLimit = {{ enable_time_limit ? 'true' : 'false' }};
var enableOutfocusedLimit = {{ enable_outfocused_limit ? 'true' : 'false' }};
{% if enable_outfocused_limit %}
var remainingOutfocused = {{ exercisefocused.remaining_outfocused }};
{% endif %}
function finishExam() {
$(window).off("blur", onBlur)
$(window).off("focus", onFocus)
{% if ALL_ON_ONE_PAGE %}
save_now_all('validate');
{% elseif ONE_PER_PAGE %}
window.quizTimeEnding = true;
$('[name="save_now"]').trigger('click');
{% endif %}
}
{% if enable_time_limit %}
function updateCountdown() {
var seconds = remainingTime;
var strSeconds = `${seconds.toString().padStart(2, '0')}`;
$timeLimitTarget.text(strSeconds);
document.title = $timeLimitTarget.parent().text();
remainingTime--;
if (remainingTime < 0) {
clearInterval(countdownInterval);
sendAction('time_limit', function () {
finishExam()
});
}
}
{% endif %}
function sendAction(action, callback) {
{% if ALL_ON_ONE_PAGE %}
var levelId = 0;
{% elseif ONE_PER_PAGE %}
var levelId = $btnSaveNow.data('question') || -1;
{% endif %}
$.ajax({
url: "{{ _p.web_plugin }}exercisefocused/pages/log.php",
data: {
action: action,
exercisefocused_sec_token: secToken,
level_id: levelId
},
success: function (response) {
if (!response) {
return;
}
secToken = response.sec_token;
if (callback) {
callback(response)
}
},
});
}
function onBlur() {
$exerciseFocused.show();
if (enableOutfocusedLimit) {
if (remainingOutfocused <= 0) {
$outfocusedLimitBlock.find('p').text("{{ 'OutfocusedLimitExceeded'|get_plugin_lang('ExerciseFocusedPlugin')|escape('js') }}");
$timeLimitBlock.hide();
sendAction('outfocused_limit', function () {
finishExam()
});
return;
} else {
$outfocusedLimitTarget.text(remainingOutfocused);
}
remainingOutfocused--;
}
sendAction('outfocused');
{% if enable_time_limit %}
remainingTime = {{ time_limit }};
updateCountdown();
countdownInterval = window.setInterval(updateCountdown, 1000);
{% else %}
document.title = "{{ 'WindowTitleOutfocused'|get_plugin_lang('ExerciseFocusedPlugin')|escape('js') }}";
{% endif %}
}
function onFocus() {
sendAction('return');
document.title = initDocumentTitle;
window.setTimeout(
function () {
$exerciseFocused.hide();
},
3500
);
{% if enable_time_limit %}
clearInterval(countdownInterval);
{% endif %}
}
$(window).on("blur", onBlur)
$(window).on("focus", onFocus)
$('body').on('click', 'a, button', function (e) {
var $el = $(e.target);
if (0 === $el.parents('form#exercise_form').length) {
e.preventDefault();
}
});
})
</script>
{% endif %}

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
ExerciseFocusedPlugin::create()->uninstall();

@ -0,0 +1,3 @@
<?php
/* For licensing terms, see /license.txt */

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

@ -0,0 +1,97 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Symfony\Component\Filesystem\Filesystem;
require_once __DIR__.'/../../../main/inc/global.inc.php';
if ('cli' !== PHP_SAPI) {
exit('For security reasons, this script can only be launched from cron or from the command line');
}
exit;
$plugin = ExerciseMonitoringPlugin::create();
$em = Database::getManager();
$repo = $em->getRepository(Log::class);
$trackExeRepo = $em->getRepository(TrackEExercises::class);
$lifetimeDays = (int) $plugin->get(ExerciseMonitoringPlugin::SETTING_SNAPSHOTS_LIFETIME);
if (empty($lifetimeDays)) {
logging("There is no set time limit");
exit;
}
$timeLimit = api_get_utc_datetime(null, false, true);
$timeLimit->modify("-$lifetimeDays day");
logging(
sprintf("Deleting snapshots taken before than %s", $timeLimit->format('Y-m-d H:i:s'))
);
$fs = new Filesystem();
$logs = findLogsBeforeThan($timeLimit);
foreach ($logs as $log) {
$sysPath = ExerciseMonitoringPlugin::generateSnapshotUrl(
$log['exe_user_id'],
$log['image_filename'],
SYS_UPLOAD_PATH
);
if (!file_exists($sysPath)) {
logging(
sprintf("File %s not exists", $sysPath)
);
continue;
}
$fs->remove($sysPath);
Database::update(
'plugin_exercisemonitoring_log',
['removed' => true],
['id = ?' => $log['log_id']]
);
logging(
sprintf(
"From exe_id %s; deleting filename %s created at %s",
$log['exe_id'],
$sysPath,
$log['created_at']
)
);
}
function findLogsBeforeThan(DateTime $timeLimit): array
{
$sql = "SELECT tee.exe_id, l.id AS log_id, l.image_filename, tee.exe_user_id
FROM plugin_exercisemonitoring_log l
INNER JOIN chamilo.track_e_exercises tee on l.exe_id = tee.exe_id
WHERE l.created_at <= '".$timeLimit->format('Y-m-d H:i:s')."'
AND l.removed IS FALSE";
$result = Database::query($sql);
$rows = [];
while ($row = Database::fetch_assoc($result)) {
$rows[] = $row;
}
return $rows;
}
function logging(string $message)
{
$time = time();
printf("[%s] %s \n", $time, $message);
}

@ -0,0 +1,61 @@
<?php
/* For licensing terms, see /license.txt */
$plugin = ExerciseMonitoringPlugin::create();
$em = Database::getManager();
$isEnabled = $plugin->isEnabled(true);
$showOverviewRegion = $isEnabled && strpos($_SERVER['SCRIPT_NAME'], '/main/exercise/overview.php') !== false;
$showSubmitRegion = $isEnabled && strpos($_SERVER['SCRIPT_NAME'], '/main/exercise/exercise_submit.php') !== false;
$_template['enabled'] = false;
$_template['show_overview_region'] = $showOverviewRegion;
$_template['show_submit_region'] = $showSubmitRegion;
if ($showOverviewRegion || $showSubmitRegion) {
$exerciseId = (int) $_GET['exerciseId'];
$objFieldValue = new ExtraFieldValue('exercise');
$values = $objFieldValue->get_values_by_handler_and_field_variable(
$exerciseId,
ExerciseMonitoringPlugin::FIELD_SELECTED
);
$_template['enabled'] = $values && (bool) $values['value'];
$_template['exercise_id'] = $exerciseId;
}
$_template['enable_snapshots'] = true;
$isAdult = $plugin->isAdult();
if ($showOverviewRegion && $_template['enabled']) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS);
if ('true' === $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE)) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS_MINORS);
if ($isAdult) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS_ADULTS);
} else {
$_template['enable_snapshots'] = false;
}
}
$_template['instructions'] = Security::remove_XSS($_template['instructions']);
}
if ($showSubmitRegion && $_template['enabled']) {
$exercise = new Exercise(api_get_course_int_id());
if ($exercise->read($_template['exercise_id'])) {
$_template['exercise_type'] = (int) $exercise->selectType();
if ('true' === $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE)
&& !$isAdult
) {
$_template['enable_snapshots'] = false;
}
}
}

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
ExerciseMonitoringPlugin::create()->install();

@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Exercise monitoring";
$strings['plugin_comment'] = "Random photo taking mechanism during an exercise";
$strings['tool_enable'] = "Enable tool";
$strings['intructions'] = 'Instructions';
$strings['age_distinction_enable'] = 'Enable age distinction';
$strings['legal_age'] = 'Legal age';
$strings['extrafield_birtdate'] = 'Extra field for birthdate';
$strings['extrafield_birtdate_help'] = 'The name of the field with which age will be calculated, e.g. <code>birthdate</code>';
$strings['instructions_adults'] = 'Intructions for adults students';
$strings['instructions_minors'] = 'Intrucctions for minors students';
$strings['snapshots_lifetime'] = 'Life time of photos taken';
$strings['snapshots_lifetime_help'] = 'Number of days that taken photos can remain stored on the server.<br>The cleanup script is located in <code>plugin/exercisemonitoring/cron/cleanup.php</code>';
$strings['ExerciseMonitored'] = "Exercise monitored";
$strings['Retry'] = "Retry";
$strings['IdDocumentSnapshot'] = "Validated photo of the ID document";
$strings['StudentSnapshot'] = "Validated photo of the student";
$strings['ImageIdDocumentCameraInstructions'] = "Place your ID document in front of the camera and place it in the marked box. Click the <i>Capture</i> button or press the space bar on your keyboard.";
$strings['ImageStudentCameraInstructions'] = "Place your face in front of the camera and place it within the marked circle. Click the <i>Capture</i> button or press the space bar on your keyboard";
$strings['Snapshots'] = "Snapshots";
$strings['ExerciseUnmonitored'] = "Exercise unmonitored";
$strings['Birthdate'] = "Birthdate";
$strings['AdultStudent'] = "Adult student";
$strings['MinorStudent'] = "Minor student";

@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Monitoreo de Ejercicios";
$strings['plugin_comment'] = "Mecanismo de toma de fotos aleatorias durante un ejercicio";
$strings['tool_enable'] = "Enable tool";
$strings['intructions'] = 'Intrucciones';
$strings['age_distinction_enable'] = 'Habilitar distinción de edad';
$strings['legal_age'] = 'Mayoría de edad';
$strings['extrafield_birtdate'] = 'Campo extra para fecha de nacimiento';
$strings['extrafield_birtdate_help'] = 'El nombre del campo con el cual se calculará la edad, por ejemplo <code>birthdate</code>';
$strings['instructions_adults'] = 'Intrucciones para estudiantes adultos';
$strings['instructions_minors'] = 'Intrucciones para estudiantes menores de edad';
$strings['snapshots_lifetime'] = 'Tiempo de vida de las fotos tomadas';
$strings['snapshots_lifetime_help'] = 'Cantidad de días que las fotos tomadas pueden permanecer almacenadas en el servidor.<br>El script de limpieza está ubicado en <code>plugin/exercisemonitoring/cron/cleanup.php</code>';
$strings['ExerciseMonitored'] = "Ejercicio monitoreado";
$strings['Retry'] = "Reintentar";
$strings['IdDocumentSnapshot'] = "Foto validada del documento de identidad";
$strings['StudentSnapshot'] = "Foto validada del estudiante";
$strings['ImageIdDocumentCameraInstructions'] = "Coloca tu DNI o documento de identidad frente a la cámara y ubícalo en el recuadro marcado. Dale clic al botón <i>Capturar</i> o presiona la barra de espacio de tu teclado.";
$strings['ImageStudentCameraInstructions'] = "Coloca tu rostro frente a la cámara y ubícalo dentro del círculo marcado. Dale click al botón <i>Capturar</i> o presiona la barra de espacio de tu teclado";
$strings['Snapshots'] = "Fotos tomadas";
$strings['ExerciseUnmonitored'] = "Ejercicio no monitoreado";
$strings['Birthdate'] = "Fecha de nacimiento";
$strings['AdultStudent'] = "Estudiante adulto";
$strings['MinorStudent'] = "Estudiante menor de edad";

@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseMonitoring\Controller\DetailController;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
require_once __DIR__.'/../../../main/inc/global.inc.php';
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$em = Database::getManager();
$logRepository = $em->getRepository(Log::class);
$detailController = new DetailController(
ExerciseMonitoringPlugin::create(),
HttpRequest::createFromGlobals(),
$em,
$logRepository
);
try {
$response = $detailController();
} catch (Exception $e) {
$response = HttpResponse::create('', HttpResponse::HTTP_FORBIDDEN);
}
$response->send();

@ -0,0 +1,18 @@
<?php
/* For license terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script();
$plugin = ExerciseMonitoringPlugin::create();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$exerciseSubmitController = new ExerciseSubmitController($plugin, $request, $em);
$response = $exerciseSubmitController();
$response->send();

@ -0,0 +1,18 @@
<?php
/* For license terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script();
$plugin = ExerciseMonitoringPlugin::create();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$startController = new StartController($plugin, $request, $em);
$response = $startController();
$response->send();

@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
$plugin_info = ExerciseMonitoringPlugin::create()->get_info();
$plugin_info['templates'] = [
'templates/modal.html.twig',
'templates/exercise_submit.html.twig',
];

@ -0,0 +1,111 @@
<?php
/* For license terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Controller;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseFocused\Traits\DetailControllerTrait;
use Display;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Exception;
use Exercise;
use ExerciseMonitoringPlugin;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class DetailController
{
use DetailControllerTrait;
/**
* @var ExerciseMonitoringPlugin
*/
private $plugin;
/**
* @var HttpRequest
*/
private $request;
/**
* @var EntityManager
*/
private $em;
/**
* @var EntityRepository
*/
private $logRepository;
public function __construct(
ExerciseMonitoringPlugin $plugin,
HttpRequest $request,
EntityManager $em,
EntityRepository $logRepository
) {
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
$this->logRepository = $logRepository;
}
/**
* @throws Exception
*/
public function __invoke(): HttpResponse
{
if (!$this->plugin->isEnabled(true)) {
throw new Exception();
}
$trackExe = $this->em->find(
TrackEExercises::class,
$this->request->query->getInt('id')
);
if (!$trackExe) {
throw new Exception();
}
$exercise = $this->em->find(CQuiz::class, $trackExe->getExeExoId());
$user = api_get_user_entity($trackExe->getExeUserId());
$objExercise = new Exercise($trackExe->getCId());
$objExercise->read($trackExe->getExeExoId());
$logs = $this->logRepository->findSnapshots($objExercise, $trackExe);
$content = $this->generateHeader($objExercise, $user, $trackExe)
.'<hr>'
.$this->generateSnapshotList($logs, $trackExe->getExeUserId());
return HttpResponse::create($content);
}
private function generateSnapshotList(array $logs, int $userId): string
{
$html = '';
foreach ($logs as $i => $log) {
$date = api_get_local_time($log['createdAt'], null, null, true, true, true);
$html .= '<div class="col-xs-12 col-sm-6 col-md-3" style="clear: '.($i % 4 === 0 ? 'both' : 'none').';">';
$html .= '<div class="thumbnail">';
$html .= Display::img(
ExerciseMonitoringPlugin::generateSnapshotUrl($userId, $log['imageFilename']),
$date
);
$html .= '<div class="caption">';
$html .= Display::tag('p', $date, ['class' => 'text-center']);
$html .= Display::tag('div', $log['log_level'], ['class' => 'text-center']);
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
}
return '<div class="row">'.$html.'</div>';
}
}

@ -0,0 +1,119 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class ExerciseSubmitController
{
private $plugin;
private $request;
private $em;
public function __construct(ExerciseMonitoringPlugin $plugin, HttpRequest $request, EntityManager $em)
{
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
}
/**
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
public function __invoke(): HttpResponse
{
$userDirName = $this->createDirectory();
$existingExeId = (int) ChamiloSession::read('exe_id');
$levelId = $this->request->request->getInt('level_id');
$exerciseId = $this->request->request->getInt('exercise_id');
$exercise = $this->em->find(CQuiz::class, $exerciseId);
$objExercise = new Exercise();
$objExercise->read($exerciseId);
$trackingExercise = $this->em->find(TrackEExercises::class, $existingExeId);
$newFilename = '';
$level = 0;
/** @var UploadedFile $imgSubmit */
if ($imgSubmit = $this->request->files->get('snapshot')) {
$newFilename = uniqid().'_submit.jpg';
$imgSubmit->move($userDirName, $newFilename);
}
if (ONE_PER_PAGE == $objExercise->selectType()) {
$question = $this->em->find(CQuizQuestion::class, $levelId);
$level = $question->getIid();
}
$log = new Log();
$log
->setExercise($exercise)
->setExe($trackingExercise)
->setLevel($level)
->setImageFilename($newFilename)
;
$this->em->persist($log);
$this->updateOrphanSnapshots($exercise, $trackingExercise);
$this->em->flush();
return HttpResponse::create();
}
private function createDirectory(): string
{
$user = api_get_user_entity(api_get_user_id());
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$userDirName = $pluginDirName.'/'.$user->getId();
$fs = new Filesystem();
$fs->mkdir(
[$pluginDirName, $userDirName],
api_get_permissions_for_new_directories()
);
return $userDirName;
}
private function updateOrphanSnapshots(CQuiz $exercise, TrackEExercises $trackingExe)
{
$repo = $this->em->getRepository(Log::class);
$fileNamesToUpdate = ChamiloSession::read($this->plugin->get_name().'_orphan_snapshots', []);
if (empty($fileNamesToUpdate)) {
return;
}
foreach ($fileNamesToUpdate as $filename) {
$log = $repo->findOneBy(['imageFilename' => $filename, 'exercise' => $exercise, 'exe' => null]);
if (!$log) {
continue;
}
$log->setExe($trackingExe);
}
ChamiloSession::erase($this->plugin->get_name().'_orphan_snapshots');
}
}

@ -0,0 +1,93 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class StartController
{
private $plugin;
private $request;
private $em;
public function __construct(ExerciseMonitoringPlugin $plugin, HttpRequest $request, EntityManager $em)
{
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
}
public function __invoke(): HttpResponse
{
$userDirName = $this->createDirectory();
/** @var UploadedFile $imgIddoc */
$imgIddoc = $this->request->files->get('iddoc');
/** @var UploadedFile $imgLearner */
$imgLearner = $this->request->files->get('learner');
$exercise = $this->em->find(CQuiz::class, $this->request->request->getInt('exercise_id'));
$fileNamesToUpdate = [];
if ($imgIddoc) {
$newFilename = uniqid().'_iddoc.jpg';
$fileNamesToUpdate[] = $newFilename;
$imgIddoc->move($userDirName, $newFilename);
$log = new Log();
$log
->setExercise($exercise)
->setLevel(-1)
->setImageFilename($newFilename)
;
$this->em->persist($log);
}
if ($imgLearner) {
$newFilename = uniqid().'_learner.jpg';
$fileNamesToUpdate[] = $newFilename;
$imgLearner->move($userDirName, $newFilename);
$log = new Log();
$log
->setExercise($exercise)
->setLevel(-1)
->setImageFilename($newFilename)
;
$this->em->persist($log);
}
$this->em->flush();
ChamiloSession::write($this->plugin->get_name().'_orphan_snapshots', $fileNamesToUpdate);
return HttpResponse::create();
}
private function createDirectory(): string
{
$user = api_get_user_entity(api_get_user_id());
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$userDirName = $pluginDirName.'/'.$user->getId();
$fs = new Filesystem();
$fs->mkdir(
[$pluginDirName, $userDirName],
api_get_permissions_for_new_directories()
);
return $userDirName;
}
}

@ -0,0 +1,133 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Entity;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Chamilo\CourseBundle\Entity\CQuiz;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\ExerciseMonitoring\Repository\LogRepository")
* @ORM\Table(name="plugin_exercisemonitoring_log")
*/
class Log
{
use TimestampableTypedEntity;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
protected $id;
/**
* @var CQuiz
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuiz")
* @ORM\JoinColumn(name="exercise_id", referencedColumnName="iid")
*/
protected $exercise;
/**
* @var TrackEExercises
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\TrackEExercises")
* @ORM\JoinColumn(name="exe_id", referencedColumnName="exe_id")
*/
private $exe;
/**
* @var int
*
* @ORM\Column(name="level", type="integer")
*/
private $level;
/**
* @var string
*
* @ORM\Column(name="image_filename", type="string")
*/
private $imageFilename;
/**
* @var bool
*
* @ORM\Column(name="removed", type="boolean", nullable=false, options={"default": false})
*/
private $removed;
public function __construct()
{
$this->removed = false;
}
public function getId(): int
{
return $this->id;
}
public function getExercise(): CQuiz
{
return $this->exercise;
}
public function setExercise(CQuiz $exercise): Log
{
$this->exercise = $exercise;
return $this;
}
public function getExe(): ?TrackEExercises
{
return $this->exe;
}
public function setExe(?TrackEExercises $exe): Log
{
$this->exe = $exe;
return $this;
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel(int $level): Log
{
$this->level = $level;
return $this;
}
public function getImageFilename(): string
{
return $this->imageFilename;
}
public function setImageFilename(string $imageFilename): Log
{
$this->imageFilename = $imageFilename;
return $this;
}
public function isRemoved(): bool
{
return $this->removed;
}
public function setRemoved(bool $removed): void
{
$this->removed = $removed;
}
}

@ -0,0 +1,185 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
use Symfony\Component\Filesystem\Filesystem;
class ExerciseMonitoringPlugin extends Plugin
{
public const SETTING_TOOL_ENABLE = 'tool_enable';
public const SETTING_INSTRUCTIONS = 'intructions';
public const SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE = 'age_distinction_enable';
public const SETTING_INSTRUCTION_LEGAL_AGE = 'legal_age';
public const SETTING_EXTRAFIELD_BIRTHDATE = 'extrafield_birtdate';
public const SETTING_INSTRUCTIONS_ADULTS = 'instructions_adults';
public const SETTING_INSTRUCTIONS_MINORS = 'instructions_minors';
public const SETTING_SNAPSHOTS_LIFETIME = 'snapshots_lifetime';
public const FIELD_SELECTED = 'exercisemonitoring_selected';
private const TABLE_LOG = 'plugin_exercisemonitoring_log';
protected function __construct()
{
$version = '0.0.1';
$settings = [
self::SETTING_TOOL_ENABLE => 'boolean',
self::SETTING_INSTRUCTIONS => 'wysiwyg',
self::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE => 'boolean',
self::SETTING_INSTRUCTION_LEGAL_AGE => 'text',
self::SETTING_EXTRAFIELD_BIRTHDATE => 'text',
self::SETTING_INSTRUCTIONS_ADULTS => 'wysiwyg',
self::SETTING_INSTRUCTIONS_MINORS => 'wysiwyg',
self::SETTING_SNAPSHOTS_LIFETIME => 'text',
];
parent::__construct(
$version,
"Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>",
$settings
);
}
public static function create(): self
{
static $result = null;
return $result ?: $result = new self();
}
/**
* @throws ToolsException
*/
public function install()
{
$em = Database::getManager();
if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->createSchema(
[
$em->getClassMetadata(Log::class),
]
);
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$fs = new Filesystem();
$fs->mkdir(
$pluginDirName,
api_get_permissions_for_new_directories()
);
$objField = new ExtraField('exercise');
$objField->save([
'variable' => self::FIELD_SELECTED,
'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
'display_text' => $this->get_title(),
'visible_to_self' => true,
'changeable' => true,
'filter' => false,
]);
}
public function uninstall()
{
$em = Database::getManager();
if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
$em->getClassMetadata(Log::class),
]
);
$objField = new ExtraField('exercise');
$extraFieldInfo = $objField->get_handler_field_info_by_field_variable(self::FIELD_SELECTED);
if ($extraFieldInfo) {
$objField->delete($extraFieldInfo['id']);
}
}
public function getAdminUrl(): string
{
$name = $this->get_name();
$webPath = api_get_path(WEB_PLUGIN_PATH).$name;
return "$webPath/admin.php";
}
public function generateDetailLink(int $exeId, int $userId): string
{
$title = $this->get_lang('ExerciseMonitored');
$webcamIcon = Display::return_icon('webcam.png', $title);
$webcamNaIcon = Display::return_icon('webcam_na.png', $this->get_lang('ExerciseUnmonitored'));
$monitoringDetailUrl = api_get_path(WEB_PLUGIN_PATH).'exercisemonitoring/pages/detail.php?'.api_get_cidreq()
.'&'.http_build_query(['id' => $exeId]);
$url = Display::url(
$webcamIcon,
$monitoringDetailUrl,
[
'class' => 'ajax',
'data-title' => $title,
'data-size' => 'lg',
]
);
$showLink = true;
if ('true' === $this->get(self::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE) && !$this->isAdult($userId)) {
$showLink = false;
}
return $showLink ? $url : $webcamNaIcon;
}
public static function generateSnapshotUrl(
int $userId,
string $imageFileName,
string $path = WEB_UPLOAD_PATH
): string {
$pluginDirName = api_get_path($path).'plugins/exercisemonitoring';
return $pluginDirName.'/'.$userId.'/'.$imageFileName;
}
/**
* @throws Exception
*/
public function isAdult(int $userId = 0): bool
{
$userId = $userId ?: api_get_user_id();
$fieldVariable = $this->get(self::SETTING_EXTRAFIELD_BIRTHDATE);
$legalAge = (int) $this->get(self::SETTING_INSTRUCTION_LEGAL_AGE);
$value = UserManager::get_extra_user_data_by_field($userId, $fieldVariable);
if (empty($value)) {
return false;
}
if (empty($value[$fieldVariable])) {
return false;
}
$birthdate = new DateTime($value[$fieldVariable]);
$now = new DateTime();
$diff = $birthdate->diff($now);
return !$diff->invert && $diff->y >= $legalAge;
}
}

@ -0,0 +1,47 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Repository;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Exercise;
class LogRepository extends EntityRepository
{
public function findByLevelAndExe(int $level, TrackEExercises $exe): array
{
return $this->findBy(
[
'level' => $level,
'exe' => $exe,
],
['createdAt' => 'ASC']
);
}
public function findSnapshots(Exercise $objExercise, TrackEExercises $trackExe)
{
$qb = $this->createQueryBuilder('l');
$qb->select(['l.imageFilename', 'l.createdAt']);
if (ONE_PER_PAGE == $objExercise->selectType()) {
$qb
->addSelect(['qq.question AS log_level'])
->leftJoin(CQuizQuestion::class, 'qq', Join::WITH, 'l.level = qq.iid');
}
$query = $qb
->andWhere(
$qb->expr()->eq('l.exe', $trackExe->getExeId())
)
->addOrderBy('l.createdAt')
->getQuery();
return $query->getResult();
}
}

@ -0,0 +1,84 @@
{% if exercisemonitoring.show_submit_region and exercisemonitoring.enabled and exercisemonitoring.enable_snapshots %}
{% set ALL_ON_ONE_PAGE = exercisemonitoring.exercise_type == 1 %}
{% set ONE_PER_PAGE = exercisemonitoring.exercise_type == 2 %}
<div id="monitoring-camera"></div>
<script src="{{ _p.web }}web/assets/webcamjs/webcam.js"></script>
<script>
$(function () {
var $btnSaveNow = $('button[name="save_now"]');
var $btnEndTest = $('button[name="validate_all"]');
Webcam.set({
height: 480,
width: 640,
});
Webcam.attach('#monitoring-camera');
Webcam.on('live', function () {
{% if ALL_ON_ONE_PAGE %}
snapAndSendData(0);
{% elseif ONE_PER_PAGE %}
snapByQuestion();
{% endif %}
});
{% if ALL_ON_ONE_PAGE %}
$btnEndTest.on('click', function () {
snapAndSendData(0);
});
{% elseif ONE_PER_PAGE %}
$btnSaveNow.on('click', function () {
snapByQuestion();
});
{% endif %}
function snapAndSendData(levelId) {
Webcam.snap(function (dataUri) {
sendData(levelId, dataUri);
});
}
function snapByQuestion() {
var questionId = $btnSaveNow.data('question') || 0;
snapAndSendData(questionId);
}
function sendData(questionId, imageUri) {
var rawImgIdDoc = imageUri.replace(/^data:image\/\w+;base64,/, '');
var blobImgIdDoc = new Blob( [ Webcam.base64DecToArr(rawImgIdDoc) ], {type: 'image/jpeg'} );
var formData = new FormData();
formData.append('exercise_id', '{{ exercisemonitoring.exercise_id }}');
formData.append('level_id', questionId);
formData.append('snapshot', blobImgIdDoc, 'snapshot.jpg');
return $.ajax({
url: '{{ _p.web_plugin }}exercisemonitoring/pages/exercise_submit.ajax.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
});
}
});
</script>
<style>
#plugin_pre_footer {
position: relative;
}
#monitoring-camera {
bottom: 15px;
right: 15px;
max-width: 108px;
max-height: 81px;
position: fixed;
z-index: 1015;
}
#monitoring-camera video {
max-width: 108px;
max-height: 81px;
}
</style>
{% endif %}

@ -0,0 +1,314 @@
{% if exercisemonitoring.show_overview_region and exercisemonitoring.enabled %}
{% if exercisemonitoring.enable_snapshots %}
<div id="em-modal-start" class="modal fade in" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'ExerciseMonitored'|get_plugin_lang('ExerciseMonitoringPlugin') }}</h4>
</div>
<div class="modal-body" id="em-terms-body">
{{ exercisemonitoring.instructions }}
</div>
<div class="modal-body text-center" id="em-camera-body" style="display: none;">
<p id="txt-iddoc-img-instructions" class="lead" style="display: block;">
{{ 'ImageIdDocumentCameraInstructions'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</p>
<p id="txt-learner-img-instructions" class="lead" style="display: none;">
{{ 'ImageStudentCameraInstructions'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</p>
<div style="position: relative">
<div id="monitoring-camera" class="embed-responsive embed-responsive-4by3"></div>
<img id="img-iddoc-placeholder" style="display: block;" src="{{ _p.web_plugin }}exercisemonitoring/assets/images/idcard.png">
<img id="img-learner-placeholder" style="display: none" src="{{ _p.web_plugin }}exercisemonitoring/assets/images/user.png">
</div>
<br>
<button class="btn btn-default" type="button" id="btn-snap" disabled>
<span class="fa fa-camera" aria-hidden="true"></span>
{{ 'Snapshot'|get_lang }}
</button>
</div>
<div class="modal-body text-center" id="em-iddoc-body" style="display: none;">
<p class="lead">{{ 'IdDocumentSnapshot'|get_plugin_lang('ExerciseMonitoringPlugin') }}</p>
<div id="img-iddoc"></div>
</div>
<div class="modal-body text-center" id="em-student-body" style="display: none;">
<p class="lead">{{ 'StudentSnapshot'|get_plugin_lang('ExerciseMonitoringPlugin') }}</p>
<div id="img-learner"></div>
</div>
<div id="em-terms-footer" class="modal-footer">
<button type="button" class="btn btn-primary" id="btn-accept">
<span class="fa fa-check" aria-hidden="true"></span> {{ 'Accept'|get_lang }}
</button>
</div>
<div id="em-camera-footer" class="modal-footer" style="display: none;">
<button class="btn btn-default" type="button" id="btn-retry" disabled>
<span class="fa fa-refresh" aria-hidden="true"></span>
{{ 'Retry'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</button>
<button class="btn btn-primary" type="button" id="btn-next" disabled>
<span class="fa fa-forward" aria-hidden="true"></span> {{ 'Next'|get_lang }}
</button>
</div>
</div>
</div>
</div>
<script src="{{ _p.web }}web/assets/webcamjs/webcam.js"></script>
<script>
$(function () {
var $btnStartExercise = $('.exercise_overview_options a');
var $bodyTerms = $('#em-terms-body');
var $bodyCamera = $('#em-camera-body');
var $bodyIdDoc = $('#em-iddoc-body');
var $bodyStudent = $('#em-student-body');
var $footTerms = $('#em-terms-footer');
var $footCamera = $('#em-camera-footer');
var $btnSnap = $('#btn-snap');
var $btnRetry = $('#btn-retry');
var $btnNext = $('#btn-next');
var $imgIdDoc = $('#img-iddoc');
var $imgLearner = $('#img-learner');
var $txtIdDocInstructions = $('#txt-iddoc-img-instructions');
var $txtLearnerInstructions = $('#txt-learner-img-instructions');
var $imgIdDocPlaceholder = $('#img-iddoc-placeholder');
var $imgLearnerPlaceholder = $('#img-learner-placeholder');
var hasIdDoc = false;
var hasLearner = false;
var imgIdDoc = null;
var imgLearner = null;
if ($btnStartExercise.length > 0) {
$("#em-modal-start").modal("show");
}
$btnStartExercise.addClass('disabled').attr('aria-disabled', 'true');
$("#btn-accept").on('click', function (e) {
e.preventDefault();
$bodyTerms.hide();
$footTerms.hide();
$bodyCamera.show();
$footCamera.show();
Webcam.set({
height: 480,
width: 640,
});
Webcam.attach('#monitoring-camera');
Webcam.on('live', function () {
$txtIdDocInstructions.show();
$imgIdDocPlaceholder.show();
$txtLearnerInstructions.hide();
$imgLearnerPlaceholder.hide();
$btnSnap.prop({disabled: false}).focus();
$('#monitoring-camera video').addClass('embed-responsive-item');
});
});
$btnSnap.on('click', function (e) {
e.preventDefault();
$btnSnap.prop({disabled: true});
$btnRetry.prop({disabled: true});
$btnNext.prop({disabled: true});
snap()
.done(function () {
$btnRetry.prop({disabled: false});
$btnNext.prop({disabled: false});
});
});
$btnRetry.on('click', function (e) {
e.preventDefault();
$btnSnap.prop({disabled: false}).focus();
$btnRetry.prop({disabled: true});
$btnNext.prop({disabled: true});
if (hasIdDoc && !hasLearner) {
$bodyCamera.show();
$bodyIdDoc.hide();
hasIdDoc = false;
hasLearner = false;
} else if (hasIdDoc && hasLearner) {
$bodyCamera.show();
$bodyStudent.hide();
hasIdDoc = true;
hasLearner = false;
}
});
$btnNext.on('click', function (e) {
e.preventDefault();
$btnRetry.prop({disabled: true});
if (hasIdDoc && !hasLearner) {
$bodyIdDoc.hide();
$bodyCamera.show();
$txtIdDocInstructions.hide();
$imgIdDocPlaceholder.hide();
$txtLearnerInstructions.show();
$imgLearnerPlaceholder.show();
$btnSnap.prop({disabled: false}).focus();
} else if (hasIdDoc && hasLearner) {
$btnNext.prop({disabled: true});
$btnSnap.prop({disabled: true});
Webcam.reset();
sendData().done(function () {
$btnStartExercise.removeClass('disabled').removeAttr('aria-disabled');
window.location = $btnStartExercise.prop('href');
$("#em-modal-start").modal('hide');
});
}
});
$(window).on('keyup', function (e) {
if (32 === event.which && !$btnSnap.prop('disabled')) {
e.preventDefault();
$btnSnap.trigger('click');
}
});
function snap() {
var deferred = $.Deferred();
Webcam.snap(function (dataUri) {
var $imgSnapshot = $('<img>')
.prop({src: dataUri, id: 'img-snapshot'})
.addClass('img-responsive');
if (!hasIdDoc && !hasLearner) {
$imgIdDoc.html($imgSnapshot);
$bodyCamera.hide();
$bodyIdDoc.show();
hasIdDoc = true;
hasLearner = false;
imgIdDoc = dataUri;
} else if (hasIdDoc && !hasLearner) {
$imgLearner.html($imgSnapshot);
$bodyCamera.hide();
$bodyStudent.show();
hasIdDoc = true;
hasLearner = true;
imgLearner = dataUri;
}
deferred.resolve();
});
return deferred.promise();
}
function sendData() {
var rawImgIdDoc = imgIdDoc.replace(/^data:image\/\w+;base64,/, '');
var blobImgIdDoc = new Blob( [ Webcam.base64DecToArr(rawImgIdDoc) ], {type: 'image/jpeg'} );
var rawImgLearner = imgLearner.replace(/^data:image\/\w+;base64,/, '');
var blobImgLearner = new Blob( [ Webcam.base64DecToArr(rawImgLearner) ], {type: 'image/jpeg'} );
var formData = new FormData();
formData.append('iddoc', blobImgIdDoc, 'iddoc.jpg');
formData.append('learner', blobImgLearner, 'learner.jpg');
formData.append('exercise_id', '{{ exercisemonitoring.exercise_id }}');
return $.ajax({
url: '{{ _p.web_plugin }}exercisemonitoring/pages/start.ajax.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
});
}
});
</script>
<style>
#monitoring-camera {
height: auto !important;
max-width: 100% !important;
margin: 0 auto;
}
#em-camera-body img#img-iddoc-placeholder,
#em-camera-body img#img-learner-placeholder {
height: auto;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
#monitoring-camera video {
height: auto !important;
max-width: 100%;
}
</style>
{% else %}
<div id="em-modal-start" class="modal fade in" tabindex="1" role="dialog" data-backdrop="static" data-keyboard="false" data-show="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'ExerciseMonitored'|get_plugin_lang('ExerciseMonitoringPlugin') }}</h4>
</div>
<div class="modal-body" id="em-terms-body">
{{ exercisemonitoring.instructions }}
</div>
<div id="em-terms-footer" class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">
<span class="fa fa-check" aria-hidden="true"></span> {{ 'Accept'|get_lang }}
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
var $modal = $("#em-modal-start");
var $btnStartExercise = $('.exercise_overview_options a');
if ($btnStartExercise.length > 0) {
$modal.modal("show");
}
$modal.on('hidden.bs.modal', function (e) {
$btnStartExercise.removeClass('disabled').removeAttr('aria-disabled');
window.location = $btnStartExercise.prop('href');
});
});
</script>
{% endif %}
{% endif %}

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
ExerciseMonitoringPlugin::create()->uninstall();
Loading…
Cancel
Save