From 775a5452f10c8084e3f4c0e4302b3895a5a06050 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos Date: Mon, 28 Oct 2024 17:33:56 -0500 Subject: [PATCH] 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 code --- main/exercise/exercise_report.php | 2 + main/img/icons/22/webcam_na.png | Bin 0 -> 5243 bytes plugin/exercisefocused/admin.php | 32 ++ plugin/exercisefocused/index.php | 48 +++ plugin/exercisefocused/install.php | 5 + plugin/exercisefocused/lang/english.php | 39 ++ plugin/exercisefocused/lang/spanish.php | 39 ++ plugin/exercisefocused/pages/detail.php | 32 ++ plugin/exercisefocused/pages/export.php | 298 +++++++++++++ plugin/exercisefocused/pages/log.php | 30 ++ plugin/exercisefocused/pages/reporting.php | 34 ++ plugin/exercisefocused/plugin.php | 10 + .../src/Controller/AdminController.php | 52 +++ .../src/Controller/BaseController.php | 86 ++++ .../src/Controller/DetailController.php | 98 +++++ .../src/Controller/LogController.php | 97 +++++ .../src/Controller/ReportingController.php | 138 ++++++ plugin/exercisefocused/src/Entity/Log.php | 99 +++++ .../src/ExerciseFocusedPlugin.php | 213 ++++++++++ .../src/Repository/LogRepository.php | 28 ++ .../src/Traits/DetailControllerTrait.php | 28 ++ .../src/Traits/ReportingFilterTrait.php | 393 ++++++++++++++++++ .../exercisefocused/templates/block.html.twig | 105 +++++ .../templates/script.html.twig | 163 ++++++++ plugin/exercisefocused/uninstall.php | 5 + plugin/exercisemonitoring/admin.php | 3 + .../assets/images/idcard.png | Bin 0 -> 7398 bytes .../exercisemonitoring/assets/images/user.png | Bin 0 -> 9957 bytes plugin/exercisemonitoring/cron/cleanup.php | 97 +++++ plugin/exercisemonitoring/index.php | 61 +++ plugin/exercisemonitoring/install.php | 5 + plugin/exercisemonitoring/lang/english.php | 32 ++ plugin/exercisemonitoring/lang/spanish.php | 32 ++ plugin/exercisemonitoring/pages/detail.php | 32 ++ .../pages/exercise_submit.ajax.php | 18 + .../exercisemonitoring/pages/start.ajax.php | 18 + plugin/exercisemonitoring/plugin.php | 10 + .../src/Controller/DetailController.php | 111 +++++ .../Controller/ExerciseSubmitController.php | 119 ++++++ .../src/Controller/StartController.php | 93 +++++ plugin/exercisemonitoring/src/Entity/Log.php | 133 ++++++ .../src/ExerciseMonitoringPlugin.php | 185 +++++++++ .../src/Repository/LogRepository.php | 47 +++ .../templates/exercise_submit.html.twig | 84 ++++ .../templates/modal.html.twig | 314 ++++++++++++++ plugin/exercisemonitoring/uninstall.php | 5 + 46 files changed, 3473 insertions(+) create mode 100644 main/img/icons/22/webcam_na.png create mode 100644 plugin/exercisefocused/admin.php create mode 100644 plugin/exercisefocused/index.php create mode 100644 plugin/exercisefocused/install.php create mode 100644 plugin/exercisefocused/lang/english.php create mode 100644 plugin/exercisefocused/lang/spanish.php create mode 100644 plugin/exercisefocused/pages/detail.php create mode 100644 plugin/exercisefocused/pages/export.php create mode 100644 plugin/exercisefocused/pages/log.php create mode 100644 plugin/exercisefocused/pages/reporting.php create mode 100644 plugin/exercisefocused/plugin.php create mode 100644 plugin/exercisefocused/src/Controller/AdminController.php create mode 100644 plugin/exercisefocused/src/Controller/BaseController.php create mode 100644 plugin/exercisefocused/src/Controller/DetailController.php create mode 100644 plugin/exercisefocused/src/Controller/LogController.php create mode 100644 plugin/exercisefocused/src/Controller/ReportingController.php create mode 100644 plugin/exercisefocused/src/Entity/Log.php create mode 100644 plugin/exercisefocused/src/ExerciseFocusedPlugin.php create mode 100644 plugin/exercisefocused/src/Repository/LogRepository.php create mode 100644 plugin/exercisefocused/src/Traits/DetailControllerTrait.php create mode 100644 plugin/exercisefocused/src/Traits/ReportingFilterTrait.php create mode 100644 plugin/exercisefocused/templates/block.html.twig create mode 100644 plugin/exercisefocused/templates/script.html.twig create mode 100644 plugin/exercisefocused/uninstall.php create mode 100644 plugin/exercisemonitoring/admin.php create mode 100644 plugin/exercisemonitoring/assets/images/idcard.png create mode 100644 plugin/exercisemonitoring/assets/images/user.png create mode 100644 plugin/exercisemonitoring/cron/cleanup.php create mode 100644 plugin/exercisemonitoring/index.php create mode 100644 plugin/exercisemonitoring/install.php create mode 100644 plugin/exercisemonitoring/lang/english.php create mode 100644 plugin/exercisemonitoring/lang/spanish.php create mode 100644 plugin/exercisemonitoring/pages/detail.php create mode 100644 plugin/exercisemonitoring/pages/exercise_submit.ajax.php create mode 100644 plugin/exercisemonitoring/pages/start.ajax.php create mode 100644 plugin/exercisemonitoring/plugin.php create mode 100644 plugin/exercisemonitoring/src/Controller/DetailController.php create mode 100644 plugin/exercisemonitoring/src/Controller/ExerciseSubmitController.php create mode 100644 plugin/exercisemonitoring/src/Controller/StartController.php create mode 100644 plugin/exercisemonitoring/src/Entity/Log.php create mode 100644 plugin/exercisemonitoring/src/ExerciseMonitoringPlugin.php create mode 100644 plugin/exercisemonitoring/src/Repository/LogRepository.php create mode 100644 plugin/exercisemonitoring/templates/exercise_submit.html.twig create mode 100644 plugin/exercisemonitoring/templates/modal.html.twig create mode 100644 plugin/exercisemonitoring/uninstall.php diff --git a/main/exercise/exercise_report.php b/main/exercise/exercise_report.php index 40eab064bb..9e1241f4ad 100755 --- a/main/exercise/exercise_report.php +++ b/main/exercise/exercise_report.php @@ -500,6 +500,8 @@ if ($is_allowedToEdit && $origin !== 'learnpath') { 'comparative_group_report.php?'.api_get_cidreq().'&id='.$exercise_id, ['class' => 'btn btn-default'] ); + + $actions .= ExerciseFocusedPlugin::create()->getLinkReporting($exercise_id); } } else { $actions .= ''. diff --git a/main/img/icons/22/webcam_na.png b/main/img/icons/22/webcam_na.png new file mode 100644 index 0000000000000000000000000000000000000000..d562a106445e80e728be93b9157f18d2b2414fb4 GIT binary patch literal 5243 zcmeHKYg7~07LJb!1(8-!k%EMX7A<)PG8qYPgj9kEQBXl0W+q`Od6*0k5Pb1a1gT&j zLd6#f76k#cu#^Nu6C`vTg69+bH*6Hlc<({$;z_+|e z;nKos{()`jdvNcT9i@r~B9pB}Pe{YG%+@IjmR%!?>yg$VZ;9W$hgLtwP4=y+YEIji zk!H=_I5W*YEirTttHXJ5(T%)5!@lg|@U>T|yUOHqw)~k-7(|WL))+;n_n1U zmZGaRpO}9g|#&fZBWUt!sfjyHTb`eJ!oob_D4zOjbRV}fFvR>&#b-TNDwmfkfq z|9u>zs42U`Af%g8vmhD!vdC=z`N>A7Urvh;oM2w@QEWrSt)(xacnas*qWj|@XzaPv zZz{X{x_A9{YfxB!zsG>1QunzSR5^gw+e?5J3l{srs8Y^AFl89d(8^Vy_b3!67p)3G zSK%Zz43`lKA+6_R6^%+@LRyf6m?c(u;NgT%tQrrD^^>5nt55+(b8$9u(!zj1j*|#g zE00iUV6Bj*=Y_$u&dj7y^$>EEkQOZVr+O&WIF-lXF<5jjEfK|`IU7-()R+_w@SOLW z0=x-n;UuYonat?uXht-bp;XJ5Y=J<)WO0}r4jmxqnivI%Xz2>g3?0P~hbOK<)r5*9 zlnSbj6A4pBl0q5{j8oslCs&EZ!}JQxYZZVVOf8~fvKcI#ei&1t1P1FL z`G`Qk79fsvp1?ss=Q&`Gbcn-d(OC|TY!)jFaS&k82r7|6Lm~IHHi@V4Hc}*Kh2^-}DTRhhYK^ z%BABFB&G9sP#8T7!7)07b2woTPr!9x3G^xqvul)6G8$3iZZaS^&f4Ng^M zo&_(!iELchd~Z+6E8TZbW&U$%L<5d-#ky;hdsX`!U^E~_VlRVs4KlBN#pi2tllvozZMKy6x=oo>;(l;^4iS`-l**tbZ0nv= zZl89YbI=DjVtD2odtY$P%Gl6&{qkA+aih!Z=4_@JO>;20GAs9lg|+CH$`j9QUnxS4 z)|bGkS6_8ktZfmrw8foXXWI$?AeG5n5fb7qRb@s4avx5*7)x3ZuM7Ref6h2{o%uulhHkETP|N-ALsnF@2rHYR|D)B zj5{|9m-W6J_$j()dU|^L9y_D)HLmgT!t(O+Q%_>AQFF(xenm#`kp#4c$T; z1dfj9va>Dxe0_UwmdPG;bnI#Ak-<56h`AF4t#q2+o?u{l>Xez2lhc1v^N4G_t+vx9 zY+4c&R697hHakz0xic$kZ*AM{noXvn%tzXt*?Es1wbiccJ2p_9(Ad~GC3BnY$4Ty^ z_WBztj(=8HR~N2OB-@G(9zN`eqBCyXxUtfFoNs;NC!@FQlY|;Miu+9KO5-U0c4q#e zp(b7nTh%94E}3HHV}^=begso(b|wt8M#LTDEid?)JX+OIhbJ2!>zO9NThtaNs4HpPz5$u3EQl zcZN@5Cz+k)o1C4;{`O0@#n#eb`#-PbdfR+ocO7ze9;`mrDijJ=B)i+x^Tb9rc8M!X zg0Yg|{Y72Awi7Lvow`I_p)2VrueE%FgEI=IxF@h!qOO zJWf`kt?0&B{F>sDEK@b~S zdNvW+2M--esJVUm-o06t6AVgk+6Vk7i9(Ru`AJDhcI&qv3PHyBca!k-l`M0~qGN&I z{jqdY&BB0%3vH~dt&MFwW0gv!49EXy2c?Llz?81aW%WsGET_&BQ)K;Bk3TPua?3qd TZ0-lP1&YYa&$GyV<;MR4pgetRepository(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(); diff --git a/plugin/exercisefocused/index.php b/plugin/exercisefocused/index.php new file mode 100644 index 0000000000..cfe8e1c389 --- /dev/null +++ b/plugin/exercisefocused/index.php @@ -0,0 +1,48 @@ +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(); + } + } +} diff --git a/plugin/exercisefocused/install.php b/plugin/exercisefocused/install.php new file mode 100644 index 0000000000..f66dfb8ec6 --- /dev/null +++ b/plugin/exercisefocused/install.php @@ -0,0 +1,5 @@ +install(); diff --git a/plugin/exercisefocused/lang/english.php b/plugin/exercisefocused/lang/english.php new file mode 100644 index 0000000000..96433f1656 --- /dev/null +++ b/plugin/exercisefocused/lang/english.php @@ -0,0 +1,39 @@ +
You must return and complete it."; +$strings['YouHaveXTimeToReturn'] = "You have %s seconds to return"; +$strings['YouAreAllowedXOutfocused'] = "You are allowed %d 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"; diff --git a/plugin/exercisefocused/lang/spanish.php b/plugin/exercisefocused/lang/spanish.php new file mode 100644 index 0000000000..d2348a6644 --- /dev/null +++ b/plugin/exercisefocused/lang/spanish.php @@ -0,0 +1,39 @@ +
Debes retornar y culminarlo."; +$strings['YouHaveXTimeToReturn'] = "Tienes %s segundos para regresar"; +$strings['YouAreAllowedXOutfocused'] = "Se te permite %d 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"; diff --git a/plugin/exercisefocused/pages/detail.php b/plugin/exercisefocused/pages/detail.php new file mode 100644 index 0000000000..b258392b75 --- /dev/null +++ b/plugin/exercisefocused/pages/detail.php @@ -0,0 +1,32 @@ +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(); diff --git a/plugin/exercisefocused/pages/export.php b/plugin/exercisefocused/pages/export.php new file mode 100644 index 0000000000..2b0b271445 --- /dev/null +++ b/plugin/exercisefocused/pages/export.php @@ -0,0 +1,298 @@ +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 $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); +} diff --git a/plugin/exercisefocused/pages/log.php b/plugin/exercisefocused/pages/log.php new file mode 100644 index 0000000000..b6b98e94a5 --- /dev/null +++ b/plugin/exercisefocused/pages/log.php @@ -0,0 +1,30 @@ +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(); diff --git a/plugin/exercisefocused/pages/reporting.php b/plugin/exercisefocused/pages/reporting.php new file mode 100644 index 0000000000..06c962518d --- /dev/null +++ b/plugin/exercisefocused/pages/reporting.php @@ -0,0 +1,34 @@ +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(); diff --git a/plugin/exercisefocused/plugin.php b/plugin/exercisefocused/plugin.php new file mode 100644 index 0000000000..905029d995 --- /dev/null +++ b/plugin/exercisefocused/plugin.php @@ -0,0 +1,10 @@ +get_info(); + +$plugin_info['templates'] = [ + 'templates/script.html.twig', + 'templates/block.html.twig', +]; diff --git a/plugin/exercisefocused/src/Controller/AdminController.php b/plugin/exercisefocused/src/Controller/AdminController.php new file mode 100644 index 0000000000..c28d3ec187 --- /dev/null +++ b/plugin/exercisefocused/src/Controller/AdminController.php @@ -0,0 +1,52 @@ +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'), + ]; + } +} diff --git a/plugin/exercisefocused/src/Controller/BaseController.php b/plugin/exercisefocused/src/Controller/BaseController.php new file mode 100644 index 0000000000..61e26e9ffe --- /dev/null +++ b/plugin/exercisefocused/src/Controller/BaseController.php @@ -0,0 +1,86 @@ +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); + } +} diff --git a/plugin/exercisefocused/src/Controller/DetailController.php b/plugin/exercisefocused/src/Controller/DetailController.php new file mode 100644 index 0000000000..9d2f2c60ea --- /dev/null +++ b/plugin/exercisefocused/src/Controller/DetailController.php @@ -0,0 +1,98 @@ +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) + .'
' + .$table->toHtml(); + + return HttpResponse::create($content); + } + + /** + * @param array $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; + } +} diff --git a/plugin/exercisefocused/src/Controller/LogController.php b/plugin/exercisefocused/src/Controller/LogController.php new file mode 100644 index 0000000000..380c276ef3 --- /dev/null +++ b/plugin/exercisefocused/src/Controller/LogController.php @@ -0,0 +1,97 @@ +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); + } +} diff --git a/plugin/exercisefocused/src/Controller/ReportingController.php b/plugin/exercisefocused/src/Controller/ReportingController.php new file mode 100644 index 0000000000..1c29ad1cd6 --- /dev/null +++ b/plugin/exercisefocused/src/Controller/ReportingController.php @@ -0,0 +1,138 @@ +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 + */ + 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'), + ]; + } +} diff --git a/plugin/exercisefocused/src/Entity/Log.php b/plugin/exercisefocused/src/Entity/Log.php new file mode 100644 index 0000000000..8fa8ed650e --- /dev/null +++ b/plugin/exercisefocused/src/Entity/Log.php @@ -0,0 +1,99 @@ +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; + } +} diff --git a/plugin/exercisefocused/src/ExerciseFocusedPlugin.php b/plugin/exercisefocused/src/ExerciseFocusedPlugin.php new file mode 100644 index 0000000000..62feda2d1e --- /dev/null +++ b/plugin/exercisefocused/src/ExerciseFocusedPlugin.php @@ -0,0 +1,213 @@ + '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 ", + $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; + } +} diff --git a/plugin/exercisefocused/src/Repository/LogRepository.php b/plugin/exercisefocused/src/Repository/LogRepository.php new file mode 100644 index 0000000000..01fb4d190d --- /dev/null +++ b/plugin/exercisefocused/src/Repository/LogRepository.php @@ -0,0 +1,28 @@ +count([ + 'exe' => $exe, + 'action' => $action, + ]); + } + + public function countByActionAndLevel(TrackEExercises $exe, string $action, int $level): int + { + return $this->count([ + 'exe' => $exe, + 'action' => $action, + 'level' => $level, + ]); + } +} diff --git a/plugin/exercisefocused/src/Traits/DetailControllerTrait.php b/plugin/exercisefocused/src/Traits/DetailControllerTrait.php new file mode 100644 index 0000000000..95a5166d9f --- /dev/null +++ b/plugin/exercisefocused/src/Traits/DetailControllerTrait.php @@ -0,0 +1,28 @@ +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())) + ); + } +} diff --git a/plugin/exercisefocused/src/Traits/ReportingFilterTrait.php b/plugin/exercisefocused/src/Traits/ReportingFilterTrait.php new file mode 100644 index 0000000000..ad8126d73a --- /dev/null +++ b/plugin/exercisefocused/src/Traits/ReportingFilterTrait.php @@ -0,0 +1,393 @@ +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; + } +} diff --git a/plugin/exercisefocused/templates/block.html.twig b/plugin/exercisefocused/templates/block.html.twig new file mode 100644 index 0000000000..f86f72bd0a --- /dev/null +++ b/plugin/exercisefocused/templates/block.html.twig @@ -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') %} + + + +{% endif %} \ No newline at end of file diff --git a/plugin/exercisefocused/templates/script.html.twig b/plugin/exercisefocused/templates/script.html.twig new file mode 100644 index 0000000000..d849c6cc51 --- /dev/null +++ b/plugin/exercisefocused/templates/script.html.twig @@ -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 %} + + +{% endif %} \ No newline at end of file diff --git a/plugin/exercisefocused/uninstall.php b/plugin/exercisefocused/uninstall.php new file mode 100644 index 0000000000..7a0396b9f3 --- /dev/null +++ b/plugin/exercisefocused/uninstall.php @@ -0,0 +1,5 @@ +uninstall(); diff --git a/plugin/exercisemonitoring/admin.php b/plugin/exercisemonitoring/admin.php new file mode 100644 index 0000000000..4210e607a6 --- /dev/null +++ b/plugin/exercisemonitoring/admin.php @@ -0,0 +1,3 @@ +p%kqmik6^MZP5TOqk;?KiglZt;DR&S8Ru(f{_#P+@4d79&N;t(?#a8! z3=17+Z8g{m0AL*)6fhA0Vj2L1w!Rin!a8~WGJF`6Iyn-bi0TlPS}BprFa)2e!VoM) zDghv+d42e~8D=j19&cH+60DxN>eB4
5!xC)1A^S}wcNDk4Ym<2ku;dD*vTn3tRV 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 Mz|eplW8%~Q0RppA!2kdN literal 0 HcmV?d00001 diff --git a/plugin/exercisemonitoring/assets/images/user.png b/plugin/exercisemonitoring/assets/images/user.png new file mode 100644 index 0000000000000000000000000000000000000000..eed8a246f79d590be8d0f3b8ead3fa1a832f8c8c GIT binary patch literal 9957 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::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); +} diff --git a/plugin/exercisemonitoring/index.php b/plugin/exercisemonitoring/index.php new file mode 100644 index 0000000000..2e7af8ad1a --- /dev/null +++ b/plugin/exercisemonitoring/index.php @@ -0,0 +1,61 @@ +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; + } + } +} diff --git a/plugin/exercisemonitoring/install.php b/plugin/exercisemonitoring/install.php new file mode 100644 index 0000000000..a9af675a75 --- /dev/null +++ b/plugin/exercisemonitoring/install.php @@ -0,0 +1,5 @@ +install(); diff --git a/plugin/exercisemonitoring/lang/english.php b/plugin/exercisemonitoring/lang/english.php new file mode 100644 index 0000000000..25a1ee89e3 --- /dev/null +++ b/plugin/exercisemonitoring/lang/english.php @@ -0,0 +1,32 @@ +birthdate'; +$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.
The cleanup script is located in plugin/exercisemonitoring/cron/cleanup.php'; + +$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 Capture 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 Capture 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"; diff --git a/plugin/exercisemonitoring/lang/spanish.php b/plugin/exercisemonitoring/lang/spanish.php new file mode 100644 index 0000000000..0f5ff8c2ce --- /dev/null +++ b/plugin/exercisemonitoring/lang/spanish.php @@ -0,0 +1,32 @@ +birthdate'; +$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.
El script de limpieza está ubicado en plugin/exercisemonitoring/cron/cleanup.php'; + +$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 Capturar 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 Capturar 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"; diff --git a/plugin/exercisemonitoring/pages/detail.php b/plugin/exercisemonitoring/pages/detail.php new file mode 100644 index 0000000000..33b471f94c --- /dev/null +++ b/plugin/exercisemonitoring/pages/detail.php @@ -0,0 +1,32 @@ +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(); diff --git a/plugin/exercisemonitoring/pages/exercise_submit.ajax.php b/plugin/exercisemonitoring/pages/exercise_submit.ajax.php new file mode 100644 index 0000000000..471f908684 --- /dev/null +++ b/plugin/exercisemonitoring/pages/exercise_submit.ajax.php @@ -0,0 +1,18 @@ +send(); diff --git a/plugin/exercisemonitoring/pages/start.ajax.php b/plugin/exercisemonitoring/pages/start.ajax.php new file mode 100644 index 0000000000..3832106e8d --- /dev/null +++ b/plugin/exercisemonitoring/pages/start.ajax.php @@ -0,0 +1,18 @@ +send(); diff --git a/plugin/exercisemonitoring/plugin.php b/plugin/exercisemonitoring/plugin.php new file mode 100644 index 0000000000..391c44e506 --- /dev/null +++ b/plugin/exercisemonitoring/plugin.php @@ -0,0 +1,10 @@ +get_info(); + +$plugin_info['templates'] = [ + 'templates/modal.html.twig', + 'templates/exercise_submit.html.twig', +]; diff --git a/plugin/exercisemonitoring/src/Controller/DetailController.php b/plugin/exercisemonitoring/src/Controller/DetailController.php new file mode 100644 index 0000000000..e9c5b05e7a --- /dev/null +++ b/plugin/exercisemonitoring/src/Controller/DetailController.php @@ -0,0 +1,111 @@ +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) + .'
' + .$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 .= '
'; + $html .= '
'; + $html .= Display::img( + ExerciseMonitoringPlugin::generateSnapshotUrl($userId, $log['imageFilename']), + $date + ); + $html .= '
'; + $html .= Display::tag('p', $date, ['class' => 'text-center']); + $html .= Display::tag('div', $log['log_level'], ['class' => 'text-center']); + $html .= '
'; + $html .= '
'; + $html .= '
'; + } + + return '
'.$html.'
'; + } +} diff --git a/plugin/exercisemonitoring/src/Controller/ExerciseSubmitController.php b/plugin/exercisemonitoring/src/Controller/ExerciseSubmitController.php new file mode 100644 index 0000000000..94e54aa439 --- /dev/null +++ b/plugin/exercisemonitoring/src/Controller/ExerciseSubmitController.php @@ -0,0 +1,119 @@ +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'); + } +} diff --git a/plugin/exercisemonitoring/src/Controller/StartController.php b/plugin/exercisemonitoring/src/Controller/StartController.php new file mode 100644 index 0000000000..408a551d4c --- /dev/null +++ b/plugin/exercisemonitoring/src/Controller/StartController.php @@ -0,0 +1,93 @@ +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; + } +} diff --git a/plugin/exercisemonitoring/src/Entity/Log.php b/plugin/exercisemonitoring/src/Entity/Log.php new file mode 100644 index 0000000000..81a6291681 --- /dev/null +++ b/plugin/exercisemonitoring/src/Entity/Log.php @@ -0,0 +1,133 @@ +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; + } +} diff --git a/plugin/exercisemonitoring/src/ExerciseMonitoringPlugin.php b/plugin/exercisemonitoring/src/ExerciseMonitoringPlugin.php new file mode 100644 index 0000000000..997885e966 --- /dev/null +++ b/plugin/exercisemonitoring/src/ExerciseMonitoringPlugin.php @@ -0,0 +1,185 @@ + '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 ", + $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; + } +} diff --git a/plugin/exercisemonitoring/src/Repository/LogRepository.php b/plugin/exercisemonitoring/src/Repository/LogRepository.php new file mode 100644 index 0000000000..f307f97d06 --- /dev/null +++ b/plugin/exercisemonitoring/src/Repository/LogRepository.php @@ -0,0 +1,47 @@ +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(); + } +} diff --git a/plugin/exercisemonitoring/templates/exercise_submit.html.twig b/plugin/exercisemonitoring/templates/exercise_submit.html.twig new file mode 100644 index 0000000000..6430d8a454 --- /dev/null +++ b/plugin/exercisemonitoring/templates/exercise_submit.html.twig @@ -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 %} + +
+ + + + +{% endif %} diff --git a/plugin/exercisemonitoring/templates/modal.html.twig b/plugin/exercisemonitoring/templates/modal.html.twig new file mode 100644 index 0000000000..18a14c831a --- /dev/null +++ b/plugin/exercisemonitoring/templates/modal.html.twig @@ -0,0 +1,314 @@ +{% if exercisemonitoring.show_overview_region and exercisemonitoring.enabled %} + {% if exercisemonitoring.enable_snapshots %} + + + + + + {% else %} + + + {% endif %} +{% endif %} \ No newline at end of file diff --git a/plugin/exercisemonitoring/uninstall.php b/plugin/exercisemonitoring/uninstall.php new file mode 100644 index 0000000000..4201e82f99 --- /dev/null +++ b/plugin/exercisemonitoring/uninstall.php @@ -0,0 +1,5 @@ +uninstall();