Plugin: Exercise monitoring and mouse focus tracking - refs BT#20900 BT#20901 (#4900)
* Plugin: ExerciseFocused: Add plugin - refs BT#20900 * Plugin: ExerciseFocused: Refactoring variable and setting names - refs BT#20900 * Plugin: ExerciseMonitoring: Add plugin - refs BT#20901 * Plugin: ExerciseMonitoring: Terms popup is not callable - refs BT#20901 * Plugin: ExerciseMonitoring: Remove countdown to snap - refs BT#20901 * Plugin: ExerciseMonitoring: Snap when pressing spacer - refs BT#20901 * Plugin: ExerciseMonitoring: Redirect when ending initial photos - refs BT#20901 * Plugin: ExerciseMonitoring: Differ between exercise one per page or all question per page - refs BT#20901 * Plugin: ExerciseMonitoring: Set snapshot dimension to 640x480 - refs BT#20901 * Plugin: ExerciseFocused: Show warning to alert user before leaving exercise - refs BT#20900 * Plugin: ExerciseMonitoring: Don't show modal when there isn't Start button - refs BT#20901 * Plugin: ExerciseFocused: Block click event - refs BT#20900 * Plugin: ExerciseMonitoring: Fix video responsive - refs BT#20901 * Plugin: ExerciseMonitoring: Add instructions to take snapshots - refs BT#20901 * Plugin: ExerciseFocused: Refactor query for results - refs BT#20901 * Plugin: ExerciseFocused: Add setting to generate random sampling - refs BT#21074 * Plugin: ExerciseFocused: Display motive in report with contextual style - refs BT#21074 * Plugin: ExerciseMonitoring: Show snapshot logs in ExerciseFocused plugin report - refs BT#21074 * Minor: Format code - refs BT#21074 * Plugin: ExerciseFocused: Language variable - refs BT#20900 * Plugin: ExerciseMonitoring: Fix extrafield name - refs BT#20900 * Plugin: ExerciseFocused: Allow to enable time limit by setting - refs BT#20901 * Plugin: ExerciseFocused: Add spanish language - refs BT#20901 * Plugin: ExerciseMonitoring: Add spanish language - refs BT#20900 * Plugin: ExerciseMonitoring: Add placeholders to camera - refs BT#20901 * Plugin: ExerciseMonitoring: Refactor to show link in plugin Exercise Focused - refs BT#20901 * Plugin: ExerciseMonitoring: Fix lang var - refs BT#20901 * Plugin: ExerciseMonitoring: Fix irregular grid - refs BT#20901 * Plugin: ExerciseFocused: Allow save level in log - refs BT#20900 * Plugin: ExerciseFocused: Allow export for exercise with one question per page - refs BT#21074 * Plugin: ExerciseFocused: ExerciseMonitoring: Fix lang variables - refs BT#20900 BT#20901 * Plugin: ExerciseFocused: Fix report when there is no exercise attempts in course - refs BT#21074 * Plugin: ExerciseFocused: Include snapshots column from ExerciseMonitoring plugin in report - refs BT#21074 * Minor: Format code - refs BT#21074 * Plugin: ExerciseFocused: Simplify conditions with exercise type - refs BT#20900 * Plugin: ExerciseFocused: ExerciseMonitoring: Use new term to level + improve warning message - refs BT#21074 * Plugin: ExerciseMonitoring: set genera column to level in report - refs BT#21074 * Plugin: ExerciseMonitoring: ExerciseFocused: Change language vars - refs BT#21074 * Plugin: ExerciseMonitoring: ExerciseFocused: unify header in modals - BT#21074 * Plugin: ExerciseMonitoring: ExerciseFocused: use Student term instead of Learner - BT#21074 * Plugin: ExerciseFocused: Display level reached in detail - BT#21074 * Plugin: ExerciseMonitoring: Move code to function - BT#21074 * Plugin: ExerciseFocused: Change language variable - refs BT#21074 * Plugin: ExerciseFocused: Change language variables - refs BT#21074 * Plugin: ExerciseFocused: Add columns about session/course in admin report - refs BT#21074 * Plugin: ExerciseFocused: Fix language vars in report - refs BT#21074 * Plugin: ExerciseFocused: Add IP report exported + fix lang var - refs BT#21074 * Plugin: ExerciseMonitoring: Add option to set instructions with age distinction - refs BT#21179 * Plugin: ExerciseMonitoring: Move code to function - refs BT#20901 * Plugin: ExerciseMonitoring: Fix ID and user snapshots without track_e_exercise.id - refs BT#20901 * Plugin: ExerciseFocused: Separate the column full name in two columns + separate username row in report - refs BT#21074 * Plugin: ExerciseFocused: Search form has optional fields - refs BT#21074 * Plugin: ExerciseMonitoring: Show the birthdate and legal age in report - refs BT#21074 * Minor: Add missing webcam.png icon with size small - refs BT#21074 * Plugin: ExerciseFocused: Fix detail for admin report - refs BT#21074 * Plugin: ExerciseMonitoring: Add setting and cron job to delete snapshots taken - refs BT#21074 * Minor: Plugin: ExerciseFocused: Delay backdrop - refs BT#21074 * Minor: Plugin: ExerciseFocused: change message for window/tab title - refs BT#21074 * Plugin: ExerciseFocused: Keep message visibility after refocusing - refs BT#20901 * Plugin: ExerciseFocused: Fix filter by session extra fields - refs BT#21074 * Plugin: ExerciseFocused: Fix report by session extra fields - refs BT#21074 * Plugin: ExerciseMonitoring: Make the live camera floating - refs BT#20901 * Plugin: ExerciseFocused: Make the alert message floating - refs BT#20901 * Plugin: ExerciseFocused: Change lang var for motive - refs BT#21074 * Plugin: ExerciseFocused: Increase delay time to hide messages - refs BT#20900 * Plugin: ExerciseMonitoring: Improve image placeholders for id card and student - refs BT#20901 * Plugin: ExerciseMonitoring: Improve image placeholders for id card - refs BT#20901 * Minor: Plugin: ExerciseFocused: Fix lang var - refs BT#20901 * Plugin: ExerciseFocused: Fix random results - refs BT#21074 * Plugin: ExerciseFocused: Fix session filter - refs BT#21074 * Plugin: ExerciseFocused: Allow multiple match in firstname and lastname filters - refs BT#21074 * Plugin: ExerciseFocused: Round number of random results - refs BT#21074 * Plugin: ExerciseFocused: Add button to reset search - refs BT#21074 * Minor: Format codepull/5903/head
parent
3e2582f64f
commit
775a5452f1
After Width: | Height: | Size: 5.1 KiB |
@ -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 */ |
After Width: | Height: | Size: 7.2 KiB |
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…
Reference in new issue