diff --git a/main/exercise/exercise_report.php b/main/exercise/exercise_report.php index 40eab064bb7..9e1241f4ad0 100755 --- a/main/exercise/exercise_report.php +++ b/main/exercise/exercise_report.php @@ -500,6 +500,8 @@ '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/glossary/index.php b/main/glossary/index.php index 3dee76f303a..60723212d51 100755 --- a/main/glossary/index.php +++ b/main/glossary/index.php @@ -90,12 +90,12 @@ function sorter($item1, $item2) $form->addHtmlEditor( 'name', get_lang('TermName'), - false, + true, false, ['ToolbarSet' => 'TitleAsHtml'] ); } else { - $form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']); + $form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']); } $form->addHtmlEditor( @@ -107,7 +107,6 @@ function sorter($item1, $item2) ); $form->addButtonCreate(get_lang('TermAddButton'), 'SubmitGlossary'); // setting the rules - $form->addRule('name', get_lang('ThisFieldIsRequired'), 'required'); // The validation or display if ($form->validate()) { $check = Security::check_token('post'); @@ -154,12 +153,12 @@ function sorter($item1, $item2) $form->addHtmlEditor( 'name', get_lang('TermName'), - false, + true, false, ['ToolbarSet' => 'TitleAsHtml'] ); } else { - $form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']); + $form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']); } $form->addHtmlEditor( @@ -192,9 +191,6 @@ function sorter($item1, $item2) $form->addButtonUpdate(get_lang('TermUpdateButton'), 'SubmitGlossary'); $form->setDefaults($glossary_data); - // setting the rules - $form->addRule('name', get_lang('ThisFieldIsRequired'), 'required'); - // The validation or display if ($form->validate()) { $check = Security::check_token('post'); diff --git a/main/img/icons/22/webcam_na.png b/main/img/icons/22/webcam_na.png new file mode 100644 index 00000000000..d562a106445 Binary files /dev/null and b/main/img/icons/22/webcam_na.png differ diff --git a/main/inc/lib/TrackingCourseLog.php b/main/inc/lib/TrackingCourseLog.php index 8db8e3b67cc..4c1b462b113 100644 --- a/main/inc/lib/TrackingCourseLog.php +++ b/main/inc/lib/TrackingCourseLog.php @@ -284,6 +284,8 @@ public static function getItemResourcesData($from, $numberOfItems, $column, $dir $row[4] = $ip; } + $row[5] = Security::remove_XSS($row[5]); + $resources[] = $row; } } diff --git a/main/inc/lib/formvalidator/FormValidator.class.php b/main/inc/lib/formvalidator/FormValidator.class.php index be21eeddafe..0dcfcb442c9 100755 --- a/main/inc/lib/formvalidator/FormValidator.class.php +++ b/main/inc/lib/formvalidator/FormValidator.class.php @@ -2102,14 +2102,10 @@ function plain_url_filter($html, $mode = NO_HTML) /** * Prevent execution of event handlers in HTML elements. - * - * @param string $html - * - * @return string */ -function attr_on_filter($html) +function attr_on_filter(string $html): string { - $prefix = uniqid('data-cke-').'-'; + $pattern = '/\s*on\w+=(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i'; - return preg_replace('/\b(on[a-z]+)\b\s*=/i', '$1'.$prefix.'$2', $html); + return preg_replace($pattern, '', $html); } diff --git a/plugin/exercisefocused/admin.php b/plugin/exercisefocused/admin.php new file mode 100644 index 00000000000..e2078618b1e --- /dev/null +++ b/plugin/exercisefocused/admin.php @@ -0,0 +1,32 @@ +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(); diff --git a/plugin/exercisefocused/index.php b/plugin/exercisefocused/index.php new file mode 100644 index 00000000000..cfe8e1c389c --- /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 00000000000..f66dfb8ec68 --- /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 00000000000..96433f1656f --- /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 00000000000..d2348a66444 --- /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 00000000000..b258392b755 --- /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 00000000000..2b0b271445a --- /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 00000000000..b6b98e94a51 --- /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 00000000000..06c962518d8 --- /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 00000000000..905029d995f --- /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 00000000000..c28d3ec187a --- /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 00000000000..61e26e9ffe9 --- /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 00000000000..9d2f2c60ea6 --- /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 00000000000..380c276ef3b --- /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 00000000000..1c29ad1cd69 --- /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 00000000000..8fa8ed650e3 --- /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 00000000000..62feda2d1e6 --- /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 00000000000..01fb4d190d1 --- /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 00000000000..95a5166d9f4 --- /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 00000000000..ad8126d73a6 --- /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 00000000000..f86f72bd0a6 --- /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 00000000000..d849c6cc514 --- /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 00000000000..7a0396b9f35 --- /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 00000000000..4210e607a60 --- /dev/null +++ b/plugin/exercisemonitoring/admin.php @@ -0,0 +1,3 @@ +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); +} diff --git a/plugin/exercisemonitoring/index.php b/plugin/exercisemonitoring/index.php new file mode 100644 index 00000000000..2e7af8ad1a0 --- /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 00000000000..a9af675a757 --- /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 00000000000..25a1ee89e38 --- /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 00000000000..0f5ff8c2ce6 --- /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 00000000000..33b471f94c7 --- /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 00000000000..471f9086845 --- /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 00000000000..3832106e8d0 --- /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 00000000000..391c44e506d --- /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 00000000000..e9c5b05e7a4 --- /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 00000000000..94e54aa4390 --- /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 00000000000..408a551d4c0 --- /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 00000000000..81a62916811 --- /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 00000000000..997885e966b --- /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 00000000000..f307f97d060 --- /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 00000000000..6430d8a4544 --- /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 00000000000..18a14c831af --- /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 00000000000..4201e82f999 --- /dev/null +++ b/plugin/exercisemonitoring/uninstall.php @@ -0,0 +1,5 @@ +uninstall();