This commit is contained in:
Xes
2025-08-14 22:39:38 +02:00
parent 3641e93527
commit 5403f346e3
3370 changed files with 327179 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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