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