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,3 @@
<?php
/* For licensing terms, see /license.txt */

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,97 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Symfony\Component\Filesystem\Filesystem;
require_once __DIR__.'/../../../main/inc/global.inc.php';
if ('cli' !== PHP_SAPI) {
exit('For security reasons, this script can only be launched from cron or from the command line');
}
exit;
$plugin = ExerciseMonitoringPlugin::create();
$em = Database::getManager();
$repo = $em->getRepository(Log::class);
$trackExeRepo = $em->getRepository(TrackEExercises::class);
$lifetimeDays = (int) $plugin->get(ExerciseMonitoringPlugin::SETTING_SNAPSHOTS_LIFETIME);
if (empty($lifetimeDays)) {
logging("There is no set time limit");
exit;
}
$timeLimit = api_get_utc_datetime(null, false, true);
$timeLimit->modify("-$lifetimeDays day");
logging(
sprintf("Deleting snapshots taken before than %s", $timeLimit->format('Y-m-d H:i:s'))
);
$fs = new Filesystem();
$logs = findLogsBeforeThan($timeLimit);
foreach ($logs as $log) {
$sysPath = ExerciseMonitoringPlugin::generateSnapshotUrl(
$log['exe_user_id'],
$log['image_filename'],
SYS_UPLOAD_PATH
);
if (!file_exists($sysPath)) {
logging(
sprintf("File %s not exists", $sysPath)
);
continue;
}
$fs->remove($sysPath);
Database::update(
'plugin_exercisemonitoring_log',
['removed' => true],
['id = ?' => $log['log_id']]
);
logging(
sprintf(
"From exe_id %s; deleting filename %s created at %s",
$log['exe_id'],
$sysPath,
$log['created_at']
)
);
}
function findLogsBeforeThan(DateTime $timeLimit): array
{
$sql = "SELECT tee.exe_id, l.id AS log_id, l.image_filename, tee.exe_user_id
FROM plugin_exercisemonitoring_log l
INNER JOIN chamilo.track_e_exercises tee on l.exe_id = tee.exe_id
WHERE l.created_at <= '".$timeLimit->format('Y-m-d H:i:s')."'
AND l.removed IS FALSE";
$result = Database::query($sql);
$rows = [];
while ($row = Database::fetch_assoc($result)) {
$rows[] = $row;
}
return $rows;
}
function logging(string $message)
{
$time = time();
printf("[%s] %s \n", $time, $message);
}

View File

@@ -0,0 +1,61 @@
<?php
/* For licensing terms, see /license.txt */
$plugin = ExerciseMonitoringPlugin::create();
$em = Database::getManager();
$isEnabled = $plugin->isEnabled(true);
$showOverviewRegion = $isEnabled && strpos($_SERVER['SCRIPT_NAME'], '/main/exercise/overview.php') !== false;
$showSubmitRegion = $isEnabled && strpos($_SERVER['SCRIPT_NAME'], '/main/exercise/exercise_submit.php') !== false;
$_template['enabled'] = false;
$_template['show_overview_region'] = $showOverviewRegion;
$_template['show_submit_region'] = $showSubmitRegion;
if ($showOverviewRegion || $showSubmitRegion) {
$exerciseId = (int) $_GET['exerciseId'];
$objFieldValue = new ExtraFieldValue('exercise');
$values = $objFieldValue->get_values_by_handler_and_field_variable(
$exerciseId,
ExerciseMonitoringPlugin::FIELD_SELECTED
);
$_template['enabled'] = $values && (bool) $values['value'];
$_template['exercise_id'] = $exerciseId;
}
$_template['enable_snapshots'] = true;
$isAdult = $plugin->isAdult();
if ($showOverviewRegion && $_template['enabled']) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS);
if ('true' === $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE)) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS_MINORS);
if ($isAdult) {
$_template['instructions'] = $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTIONS_ADULTS);
} else {
$_template['enable_snapshots'] = false;
}
}
$_template['instructions'] = Security::remove_XSS($_template['instructions']);
}
if ($showSubmitRegion && $_template['enabled']) {
$exercise = new Exercise(api_get_course_int_id());
if ($exercise->read($_template['exercise_id'])) {
$_template['exercise_type'] = (int) $exercise->selectType();
if ('true' === $plugin->get(ExerciseMonitoringPlugin::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE)
&& !$isAdult
) {
$_template['enable_snapshots'] = false;
}
}
}

View File

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

View File

@@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Exercise monitoring";
$strings['plugin_comment'] = "Random photo taking mechanism during an exercise";
$strings['tool_enable'] = "Enable tool";
$strings['intructions'] = 'Instructions';
$strings['age_distinction_enable'] = 'Enable age distinction';
$strings['legal_age'] = 'Legal age';
$strings['extrafield_birtdate'] = 'Extra field for birthdate';
$strings['extrafield_birtdate_help'] = 'The name of the field with which age will be calculated, e.g. <code>birthdate</code>';
$strings['instructions_adults'] = 'Intructions for adults students';
$strings['instructions_minors'] = 'Intrucctions for minors students';
$strings['snapshots_lifetime'] = 'Life time of photos taken';
$strings['snapshots_lifetime_help'] = 'Number of days that taken photos can remain stored on the server.<br>The cleanup script is located in <code>plugin/exercisemonitoring/cron/cleanup.php</code>';
$strings['ExerciseMonitored'] = "Exercise monitored";
$strings['Retry'] = "Retry";
$strings['IdDocumentSnapshot'] = "Validated photo of the ID document";
$strings['StudentSnapshot'] = "Validated photo of the student";
$strings['ImageIdDocumentCameraInstructions'] = "Place your ID document in front of the camera and place it in the marked box. Click the <i>Capture</i> button or press the space bar on your keyboard.";
$strings['ImageStudentCameraInstructions'] = "Place your face in front of the camera and place it within the marked circle. Click the <i>Capture</i> button or press the space bar on your keyboard";
$strings['Snapshots'] = "Snapshots";
$strings['ExerciseUnmonitored'] = "Exercise unmonitored";
$strings['Birthdate'] = "Birthdate";
$strings['AdultStudent'] = "Adult student";
$strings['MinorStudent'] = "Minor student";

View File

@@ -0,0 +1,32 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = "Monitoreo de Ejercicios";
$strings['plugin_comment'] = "Mecanismo de toma de fotos aleatorias durante un ejercicio";
$strings['tool_enable'] = "Enable tool";
$strings['intructions'] = 'Intrucciones';
$strings['age_distinction_enable'] = 'Habilitar distinción de edad';
$strings['legal_age'] = 'Mayoría de edad';
$strings['extrafield_birtdate'] = 'Campo extra para fecha de nacimiento';
$strings['extrafield_birtdate_help'] = 'El nombre del campo con el cual se calculará la edad, por ejemplo <code>birthdate</code>';
$strings['instructions_adults'] = 'Intrucciones para estudiantes adultos';
$strings['instructions_minors'] = 'Intrucciones para estudiantes menores de edad';
$strings['snapshots_lifetime'] = 'Tiempo de vida de las fotos tomadas';
$strings['snapshots_lifetime_help'] = 'Cantidad de días que las fotos tomadas pueden permanecer almacenadas en el servidor.<br>El script de limpieza está ubicado en <code>plugin/exercisemonitoring/cron/cleanup.php</code>';
$strings['ExerciseMonitored'] = "Ejercicio monitoreado";
$strings['Retry'] = "Reintentar";
$strings['IdDocumentSnapshot'] = "Foto validada del documento de identidad";
$strings['StudentSnapshot'] = "Foto validada del estudiante";
$strings['ImageIdDocumentCameraInstructions'] = "Coloca tu DNI o documento de identidad frente a la cámara y ubícalo en el recuadro marcado. Dale clic al botón <i>Capturar</i> o presiona la barra de espacio de tu teclado.";
$strings['ImageStudentCameraInstructions'] = "Coloca tu rostro frente a la cámara y ubícalo dentro del círculo marcado. Dale click al botón <i>Capturar</i> o presiona la barra de espacio de tu teclado";
$strings['Snapshots'] = "Fotos tomadas";
$strings['ExerciseUnmonitored'] = "Ejercicio no monitoreado";
$strings['Birthdate'] = "Fecha de nacimiento";
$strings['AdultStudent'] = "Estudiante adulto";
$strings['MinorStudent'] = "Estudiante menor de edad";

View File

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

View File

@@ -0,0 +1,18 @@
<?php
/* For license terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script();
$plugin = ExerciseMonitoringPlugin::create();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$exerciseSubmitController = new ExerciseSubmitController($plugin, $request, $em);
$response = $exerciseSubmitController();
$response->send();

View File

@@ -0,0 +1,18 @@
<?php
/* For license terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script();
$plugin = ExerciseMonitoringPlugin::create();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$startController = new StartController($plugin, $request, $em);
$response = $startController();
$response->send();

View File

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

View File

@@ -0,0 +1,111 @@
<?php
/* For license terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Controller;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseFocused\Traits\DetailControllerTrait;
use Display;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Exception;
use Exercise;
use ExerciseMonitoringPlugin;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class DetailController
{
use DetailControllerTrait;
/**
* @var ExerciseMonitoringPlugin
*/
private $plugin;
/**
* @var HttpRequest
*/
private $request;
/**
* @var EntityManager
*/
private $em;
/**
* @var EntityRepository
*/
private $logRepository;
public function __construct(
ExerciseMonitoringPlugin $plugin,
HttpRequest $request,
EntityManager $em,
EntityRepository $logRepository
) {
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
$this->logRepository = $logRepository;
}
/**
* @throws Exception
*/
public function __invoke(): HttpResponse
{
if (!$this->plugin->isEnabled(true)) {
throw new Exception();
}
$trackExe = $this->em->find(
TrackEExercises::class,
$this->request->query->getInt('id')
);
if (!$trackExe) {
throw new Exception();
}
$exercise = $this->em->find(CQuiz::class, $trackExe->getExeExoId());
$user = api_get_user_entity($trackExe->getExeUserId());
$objExercise = new Exercise($trackExe->getCId());
$objExercise->read($trackExe->getExeExoId());
$logs = $this->logRepository->findSnapshots($objExercise, $trackExe);
$content = $this->generateHeader($objExercise, $user, $trackExe)
.'<hr>'
.$this->generateSnapshotList($logs, $trackExe->getExeUserId());
return HttpResponse::create($content);
}
private function generateSnapshotList(array $logs, int $userId): string
{
$html = '';
foreach ($logs as $i => $log) {
$date = api_get_local_time($log['createdAt'], null, null, true, true, true);
$html .= '<div class="col-xs-12 col-sm-6 col-md-3" style="clear: '.($i % 4 === 0 ? 'both' : 'none').';">';
$html .= '<div class="thumbnail">';
$html .= Display::img(
ExerciseMonitoringPlugin::generateSnapshotUrl($userId, $log['imageFilename']),
$date
);
$html .= '<div class="caption">';
$html .= Display::tag('p', $date, ['class' => 'text-center']);
$html .= Display::tag('div', $log['log_level'], ['class' => 'text-center']);
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
}
return '<div class="row">'.$html.'</div>';
}
}

View File

@@ -0,0 +1,119 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class ExerciseSubmitController
{
private $plugin;
private $request;
private $em;
public function __construct(ExerciseMonitoringPlugin $plugin, HttpRequest $request, EntityManager $em)
{
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
}
/**
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
public function __invoke(): HttpResponse
{
$userDirName = $this->createDirectory();
$existingExeId = (int) ChamiloSession::read('exe_id');
$levelId = $this->request->request->getInt('level_id');
$exerciseId = $this->request->request->getInt('exercise_id');
$exercise = $this->em->find(CQuiz::class, $exerciseId);
$objExercise = new Exercise();
$objExercise->read($exerciseId);
$trackingExercise = $this->em->find(TrackEExercises::class, $existingExeId);
$newFilename = '';
$level = 0;
/** @var UploadedFile $imgSubmit */
if ($imgSubmit = $this->request->files->get('snapshot')) {
$newFilename = uniqid().'_submit.jpg';
$imgSubmit->move($userDirName, $newFilename);
}
if (ONE_PER_PAGE == $objExercise->selectType()) {
$question = $this->em->find(CQuizQuestion::class, $levelId);
$level = $question->getIid();
}
$log = new Log();
$log
->setExercise($exercise)
->setExe($trackingExercise)
->setLevel($level)
->setImageFilename($newFilename)
;
$this->em->persist($log);
$this->updateOrphanSnapshots($exercise, $trackingExercise);
$this->em->flush();
return HttpResponse::create();
}
private function createDirectory(): string
{
$user = api_get_user_entity(api_get_user_id());
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$userDirName = $pluginDirName.'/'.$user->getId();
$fs = new Filesystem();
$fs->mkdir(
[$pluginDirName, $userDirName],
api_get_permissions_for_new_directories()
);
return $userDirName;
}
private function updateOrphanSnapshots(CQuiz $exercise, TrackEExercises $trackingExe)
{
$repo = $this->em->getRepository(Log::class);
$fileNamesToUpdate = ChamiloSession::read($this->plugin->get_name().'_orphan_snapshots', []);
if (empty($fileNamesToUpdate)) {
return;
}
foreach ($fileNamesToUpdate as $filename) {
$log = $repo->findOneBy(['imageFilename' => $filename, 'exercise' => $exercise, 'exe' => null]);
if (!$log) {
continue;
}
$log->setExe($trackingExe);
}
ChamiloSession::erase($this->plugin->get_name().'_orphan_snapshots');
}
}

View File

@@ -0,0 +1,93 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class StartController
{
private $plugin;
private $request;
private $em;
public function __construct(ExerciseMonitoringPlugin $plugin, HttpRequest $request, EntityManager $em)
{
$this->plugin = $plugin;
$this->request = $request;
$this->em = $em;
}
public function __invoke(): HttpResponse
{
$userDirName = $this->createDirectory();
/** @var UploadedFile $imgIddoc */
$imgIddoc = $this->request->files->get('iddoc');
/** @var UploadedFile $imgLearner */
$imgLearner = $this->request->files->get('learner');
$exercise = $this->em->find(CQuiz::class, $this->request->request->getInt('exercise_id'));
$fileNamesToUpdate = [];
if ($imgIddoc) {
$newFilename = uniqid().'_iddoc.jpg';
$fileNamesToUpdate[] = $newFilename;
$imgIddoc->move($userDirName, $newFilename);
$log = new Log();
$log
->setExercise($exercise)
->setLevel(-1)
->setImageFilename($newFilename)
;
$this->em->persist($log);
}
if ($imgLearner) {
$newFilename = uniqid().'_learner.jpg';
$fileNamesToUpdate[] = $newFilename;
$imgLearner->move($userDirName, $newFilename);
$log = new Log();
$log
->setExercise($exercise)
->setLevel(-1)
->setImageFilename($newFilename)
;
$this->em->persist($log);
}
$this->em->flush();
ChamiloSession::write($this->plugin->get_name().'_orphan_snapshots', $fileNamesToUpdate);
return HttpResponse::create();
}
private function createDirectory(): string
{
$user = api_get_user_entity(api_get_user_id());
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$userDirName = $pluginDirName.'/'.$user->getId();
$fs = new Filesystem();
$fs->mkdir(
[$pluginDirName, $userDirName],
api_get_permissions_for_new_directories()
);
return $userDirName;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Entity;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Chamilo\CourseBundle\Entity\CQuiz;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\ExerciseMonitoring\Repository\LogRepository")
* @ORM\Table(name="plugin_exercisemonitoring_log")
*/
class Log
{
use TimestampableTypedEntity;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
protected $id;
/**
* @var CQuiz
*
* @ORM\ManyToOne(targetEntity="Chamilo\CourseBundle\Entity\CQuiz")
* @ORM\JoinColumn(name="exercise_id", referencedColumnName="iid")
*/
protected $exercise;
/**
* @var TrackEExercises
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\TrackEExercises")
* @ORM\JoinColumn(name="exe_id", referencedColumnName="exe_id")
*/
private $exe;
/**
* @var int
*
* @ORM\Column(name="level", type="integer")
*/
private $level;
/**
* @var string
*
* @ORM\Column(name="image_filename", type="string")
*/
private $imageFilename;
/**
* @var bool
*
* @ORM\Column(name="removed", type="boolean", nullable=false, options={"default": false})
*/
private $removed;
public function __construct()
{
$this->removed = false;
}
public function getId(): int
{
return $this->id;
}
public function getExercise(): CQuiz
{
return $this->exercise;
}
public function setExercise(CQuiz $exercise): Log
{
$this->exercise = $exercise;
return $this;
}
public function getExe(): ?TrackEExercises
{
return $this->exe;
}
public function setExe(?TrackEExercises $exe): Log
{
$this->exe = $exe;
return $this;
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel(int $level): Log
{
$this->level = $level;
return $this;
}
public function getImageFilename(): string
{
return $this->imageFilename;
}
public function setImageFilename(string $imageFilename): Log
{
$this->imageFilename = $imageFilename;
return $this;
}
public function isRemoved(): bool
{
return $this->removed;
}
public function setRemoved(bool $removed): void
{
$this->removed = $removed;
}
}

View File

@@ -0,0 +1,185 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\ExerciseMonitoring\Entity\Log;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
use Symfony\Component\Filesystem\Filesystem;
class ExerciseMonitoringPlugin extends Plugin
{
public const SETTING_TOOL_ENABLE = 'tool_enable';
public const SETTING_INSTRUCTIONS = 'intructions';
public const SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE = 'age_distinction_enable';
public const SETTING_INSTRUCTION_LEGAL_AGE = 'legal_age';
public const SETTING_EXTRAFIELD_BIRTHDATE = 'extrafield_birtdate';
public const SETTING_INSTRUCTIONS_ADULTS = 'instructions_adults';
public const SETTING_INSTRUCTIONS_MINORS = 'instructions_minors';
public const SETTING_SNAPSHOTS_LIFETIME = 'snapshots_lifetime';
public const FIELD_SELECTED = 'exercisemonitoring_selected';
private const TABLE_LOG = 'plugin_exercisemonitoring_log';
protected function __construct()
{
$version = '0.0.1';
$settings = [
self::SETTING_TOOL_ENABLE => 'boolean',
self::SETTING_INSTRUCTIONS => 'wysiwyg',
self::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE => 'boolean',
self::SETTING_INSTRUCTION_LEGAL_AGE => 'text',
self::SETTING_EXTRAFIELD_BIRTHDATE => 'text',
self::SETTING_INSTRUCTIONS_ADULTS => 'wysiwyg',
self::SETTING_INSTRUCTIONS_MINORS => 'wysiwyg',
self::SETTING_SNAPSHOTS_LIFETIME => 'text',
];
parent::__construct(
$version,
"Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>",
$settings
);
}
public static function create(): self
{
static $result = null;
return $result ?: $result = new self();
}
/**
* @throws ToolsException
*/
public function install()
{
$em = Database::getManager();
if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->createSchema(
[
$em->getClassMetadata(Log::class),
]
);
$pluginDirName = api_get_path(SYS_UPLOAD_PATH).'plugins/exercisemonitoring';
$fs = new Filesystem();
$fs->mkdir(
$pluginDirName,
api_get_permissions_for_new_directories()
);
$objField = new ExtraField('exercise');
$objField->save([
'variable' => self::FIELD_SELECTED,
'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
'display_text' => $this->get_title(),
'visible_to_self' => true,
'changeable' => true,
'filter' => false,
]);
}
public function uninstall()
{
$em = Database::getManager();
if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_LOG])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
$em->getClassMetadata(Log::class),
]
);
$objField = new ExtraField('exercise');
$extraFieldInfo = $objField->get_handler_field_info_by_field_variable(self::FIELD_SELECTED);
if ($extraFieldInfo) {
$objField->delete($extraFieldInfo['id']);
}
}
public function getAdminUrl(): string
{
$name = $this->get_name();
$webPath = api_get_path(WEB_PLUGIN_PATH).$name;
return "$webPath/admin.php";
}
public function generateDetailLink(int $exeId, int $userId): string
{
$title = $this->get_lang('ExerciseMonitored');
$webcamIcon = Display::return_icon('webcam.png', $title);
$webcamNaIcon = Display::return_icon('webcam_na.png', $this->get_lang('ExerciseUnmonitored'));
$monitoringDetailUrl = api_get_path(WEB_PLUGIN_PATH).'exercisemonitoring/pages/detail.php?'.api_get_cidreq()
.'&'.http_build_query(['id' => $exeId]);
$url = Display::url(
$webcamIcon,
$monitoringDetailUrl,
[
'class' => 'ajax',
'data-title' => $title,
'data-size' => 'lg',
]
);
$showLink = true;
if ('true' === $this->get(self::SETTING_INSTRUCTION_AGE_DISTINCTION_ENABLE) && !$this->isAdult($userId)) {
$showLink = false;
}
return $showLink ? $url : $webcamNaIcon;
}
public static function generateSnapshotUrl(
int $userId,
string $imageFileName,
string $path = WEB_UPLOAD_PATH
): string {
$pluginDirName = api_get_path($path).'plugins/exercisemonitoring';
return $pluginDirName.'/'.$userId.'/'.$imageFileName;
}
/**
* @throws Exception
*/
public function isAdult(int $userId = 0): bool
{
$userId = $userId ?: api_get_user_id();
$fieldVariable = $this->get(self::SETTING_EXTRAFIELD_BIRTHDATE);
$legalAge = (int) $this->get(self::SETTING_INSTRUCTION_LEGAL_AGE);
$value = UserManager::get_extra_user_data_by_field($userId, $fieldVariable);
if (empty($value)) {
return false;
}
if (empty($value[$fieldVariable])) {
return false;
}
$birthdate = new DateTime($value[$fieldVariable]);
$now = new DateTime();
$diff = $birthdate->diff($now);
return !$diff->invert && $diff->y >= $legalAge;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\ExerciseMonitoring\Repository;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Exercise;
class LogRepository extends EntityRepository
{
public function findByLevelAndExe(int $level, TrackEExercises $exe): array
{
return $this->findBy(
[
'level' => $level,
'exe' => $exe,
],
['createdAt' => 'ASC']
);
}
public function findSnapshots(Exercise $objExercise, TrackEExercises $trackExe)
{
$qb = $this->createQueryBuilder('l');
$qb->select(['l.imageFilename', 'l.createdAt']);
if (ONE_PER_PAGE == $objExercise->selectType()) {
$qb
->addSelect(['qq.question AS log_level'])
->leftJoin(CQuizQuestion::class, 'qq', Join::WITH, 'l.level = qq.iid');
}
$query = $qb
->andWhere(
$qb->expr()->eq('l.exe', $trackExe->getExeId())
)
->addOrderBy('l.createdAt')
->getQuery();
return $query->getResult();
}
}

View File

@@ -0,0 +1,84 @@
{% if exercisemonitoring.show_submit_region and exercisemonitoring.enabled and exercisemonitoring.enable_snapshots %}
{% set ALL_ON_ONE_PAGE = exercisemonitoring.exercise_type == 1 %}
{% set ONE_PER_PAGE = exercisemonitoring.exercise_type == 2 %}
<div id="monitoring-camera"></div>
<script src="{{ _p.web }}web/assets/webcamjs/webcam.js"></script>
<script>
$(function () {
var $btnSaveNow = $('button[name="save_now"]');
var $btnEndTest = $('button[name="validate_all"]');
Webcam.set({
height: 480,
width: 640,
});
Webcam.attach('#monitoring-camera');
Webcam.on('live', function () {
{% if ALL_ON_ONE_PAGE %}
snapAndSendData(0);
{% elseif ONE_PER_PAGE %}
snapByQuestion();
{% endif %}
});
{% if ALL_ON_ONE_PAGE %}
$btnEndTest.on('click', function () {
snapAndSendData(0);
});
{% elseif ONE_PER_PAGE %}
$btnSaveNow.on('click', function () {
snapByQuestion();
});
{% endif %}
function snapAndSendData(levelId) {
Webcam.snap(function (dataUri) {
sendData(levelId, dataUri);
});
}
function snapByQuestion() {
var questionId = $btnSaveNow.data('question') || 0;
snapAndSendData(questionId);
}
function sendData(questionId, imageUri) {
var rawImgIdDoc = imageUri.replace(/^data:image\/\w+;base64,/, '');
var blobImgIdDoc = new Blob( [ Webcam.base64DecToArr(rawImgIdDoc) ], {type: 'image/jpeg'} );
var formData = new FormData();
formData.append('exercise_id', '{{ exercisemonitoring.exercise_id }}');
formData.append('level_id', questionId);
formData.append('snapshot', blobImgIdDoc, 'snapshot.jpg');
return $.ajax({
url: '{{ _p.web_plugin }}exercisemonitoring/pages/exercise_submit.ajax.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
});
}
});
</script>
<style>
#plugin_pre_footer {
position: relative;
}
#monitoring-camera {
bottom: 15px;
right: 15px;
max-width: 108px;
max-height: 81px;
position: fixed;
z-index: 1015;
}
#monitoring-camera video {
max-width: 108px;
max-height: 81px;
}
</style>
{% endif %}

View File

@@ -0,0 +1,314 @@
{% if exercisemonitoring.show_overview_region and exercisemonitoring.enabled %}
{% if exercisemonitoring.enable_snapshots %}
<div id="em-modal-start" class="modal fade in" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'ExerciseMonitored'|get_plugin_lang('ExerciseMonitoringPlugin') }}</h4>
</div>
<div class="modal-body" id="em-terms-body">
{{ exercisemonitoring.instructions }}
</div>
<div class="modal-body text-center" id="em-camera-body" style="display: none;">
<p id="txt-iddoc-img-instructions" class="lead" style="display: block;">
{{ 'ImageIdDocumentCameraInstructions'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</p>
<p id="txt-learner-img-instructions" class="lead" style="display: none;">
{{ 'ImageStudentCameraInstructions'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</p>
<div style="position: relative">
<div id="monitoring-camera" class="embed-responsive embed-responsive-4by3"></div>
<img id="img-iddoc-placeholder" style="display: block;" src="{{ _p.web_plugin }}exercisemonitoring/assets/images/idcard.png">
<img id="img-learner-placeholder" style="display: none" src="{{ _p.web_plugin }}exercisemonitoring/assets/images/user.png">
</div>
<br>
<button class="btn btn-default" type="button" id="btn-snap" disabled>
<span class="fa fa-camera" aria-hidden="true"></span>
{{ 'Snapshot'|get_lang }}
</button>
</div>
<div class="modal-body text-center" id="em-iddoc-body" style="display: none;">
<p class="lead">{{ 'IdDocumentSnapshot'|get_plugin_lang('ExerciseMonitoringPlugin') }}</p>
<div id="img-iddoc"></div>
</div>
<div class="modal-body text-center" id="em-student-body" style="display: none;">
<p class="lead">{{ 'StudentSnapshot'|get_plugin_lang('ExerciseMonitoringPlugin') }}</p>
<div id="img-learner"></div>
</div>
<div id="em-terms-footer" class="modal-footer">
<button type="button" class="btn btn-primary" id="btn-accept">
<span class="fa fa-check" aria-hidden="true"></span> {{ 'Accept'|get_lang }}
</button>
</div>
<div id="em-camera-footer" class="modal-footer" style="display: none;">
<button class="btn btn-default" type="button" id="btn-retry" disabled>
<span class="fa fa-refresh" aria-hidden="true"></span>
{{ 'Retry'|get_plugin_lang('ExerciseMonitoringPlugin') }}
</button>
<button class="btn btn-primary" type="button" id="btn-next" disabled>
<span class="fa fa-forward" aria-hidden="true"></span> {{ 'Next'|get_lang }}
</button>
</div>
</div>
</div>
</div>
<script src="{{ _p.web }}web/assets/webcamjs/webcam.js"></script>
<script>
$(function () {
var $btnStartExercise = $('.exercise_overview_options a');
var $bodyTerms = $('#em-terms-body');
var $bodyCamera = $('#em-camera-body');
var $bodyIdDoc = $('#em-iddoc-body');
var $bodyStudent = $('#em-student-body');
var $footTerms = $('#em-terms-footer');
var $footCamera = $('#em-camera-footer');
var $btnSnap = $('#btn-snap');
var $btnRetry = $('#btn-retry');
var $btnNext = $('#btn-next');
var $imgIdDoc = $('#img-iddoc');
var $imgLearner = $('#img-learner');
var $txtIdDocInstructions = $('#txt-iddoc-img-instructions');
var $txtLearnerInstructions = $('#txt-learner-img-instructions');
var $imgIdDocPlaceholder = $('#img-iddoc-placeholder');
var $imgLearnerPlaceholder = $('#img-learner-placeholder');
var hasIdDoc = false;
var hasLearner = false;
var imgIdDoc = null;
var imgLearner = null;
if ($btnStartExercise.length > 0) {
$("#em-modal-start").modal("show");
}
$btnStartExercise.addClass('disabled').attr('aria-disabled', 'true');
$("#btn-accept").on('click', function (e) {
e.preventDefault();
$bodyTerms.hide();
$footTerms.hide();
$bodyCamera.show();
$footCamera.show();
Webcam.set({
height: 480,
width: 640,
});
Webcam.attach('#monitoring-camera');
Webcam.on('live', function () {
$txtIdDocInstructions.show();
$imgIdDocPlaceholder.show();
$txtLearnerInstructions.hide();
$imgLearnerPlaceholder.hide();
$btnSnap.prop({disabled: false}).focus();
$('#monitoring-camera video').addClass('embed-responsive-item');
});
});
$btnSnap.on('click', function (e) {
e.preventDefault();
$btnSnap.prop({disabled: true});
$btnRetry.prop({disabled: true});
$btnNext.prop({disabled: true});
snap()
.done(function () {
$btnRetry.prop({disabled: false});
$btnNext.prop({disabled: false});
});
});
$btnRetry.on('click', function (e) {
e.preventDefault();
$btnSnap.prop({disabled: false}).focus();
$btnRetry.prop({disabled: true});
$btnNext.prop({disabled: true});
if (hasIdDoc && !hasLearner) {
$bodyCamera.show();
$bodyIdDoc.hide();
hasIdDoc = false;
hasLearner = false;
} else if (hasIdDoc && hasLearner) {
$bodyCamera.show();
$bodyStudent.hide();
hasIdDoc = true;
hasLearner = false;
}
});
$btnNext.on('click', function (e) {
e.preventDefault();
$btnRetry.prop({disabled: true});
if (hasIdDoc && !hasLearner) {
$bodyIdDoc.hide();
$bodyCamera.show();
$txtIdDocInstructions.hide();
$imgIdDocPlaceholder.hide();
$txtLearnerInstructions.show();
$imgLearnerPlaceholder.show();
$btnSnap.prop({disabled: false}).focus();
} else if (hasIdDoc && hasLearner) {
$btnNext.prop({disabled: true});
$btnSnap.prop({disabled: true});
Webcam.reset();
sendData().done(function () {
$btnStartExercise.removeClass('disabled').removeAttr('aria-disabled');
window.location = $btnStartExercise.prop('href');
$("#em-modal-start").modal('hide');
});
}
});
$(window).on('keyup', function (e) {
if (32 === event.which && !$btnSnap.prop('disabled')) {
e.preventDefault();
$btnSnap.trigger('click');
}
});
function snap() {
var deferred = $.Deferred();
Webcam.snap(function (dataUri) {
var $imgSnapshot = $('<img>')
.prop({src: dataUri, id: 'img-snapshot'})
.addClass('img-responsive');
if (!hasIdDoc && !hasLearner) {
$imgIdDoc.html($imgSnapshot);
$bodyCamera.hide();
$bodyIdDoc.show();
hasIdDoc = true;
hasLearner = false;
imgIdDoc = dataUri;
} else if (hasIdDoc && !hasLearner) {
$imgLearner.html($imgSnapshot);
$bodyCamera.hide();
$bodyStudent.show();
hasIdDoc = true;
hasLearner = true;
imgLearner = dataUri;
}
deferred.resolve();
});
return deferred.promise();
}
function sendData() {
var rawImgIdDoc = imgIdDoc.replace(/^data:image\/\w+;base64,/, '');
var blobImgIdDoc = new Blob( [ Webcam.base64DecToArr(rawImgIdDoc) ], {type: 'image/jpeg'} );
var rawImgLearner = imgLearner.replace(/^data:image\/\w+;base64,/, '');
var blobImgLearner = new Blob( [ Webcam.base64DecToArr(rawImgLearner) ], {type: 'image/jpeg'} );
var formData = new FormData();
formData.append('iddoc', blobImgIdDoc, 'iddoc.jpg');
formData.append('learner', blobImgLearner, 'learner.jpg');
formData.append('exercise_id', '{{ exercisemonitoring.exercise_id }}');
return $.ajax({
url: '{{ _p.web_plugin }}exercisemonitoring/pages/start.ajax.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
});
}
});
</script>
<style>
#monitoring-camera {
height: auto !important;
max-width: 100% !important;
margin: 0 auto;
}
#em-camera-body img#img-iddoc-placeholder,
#em-camera-body img#img-learner-placeholder {
height: auto;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
#monitoring-camera video {
height: auto !important;
max-width: 100%;
}
</style>
{% else %}
<div id="em-modal-start" class="modal fade in" tabindex="1" role="dialog" data-backdrop="static" data-keyboard="false" data-show="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'ExerciseMonitored'|get_plugin_lang('ExerciseMonitoringPlugin') }}</h4>
</div>
<div class="modal-body" id="em-terms-body">
{{ exercisemonitoring.instructions }}
</div>
<div id="em-terms-footer" class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">
<span class="fa fa-check" aria-hidden="true"></span> {{ 'Accept'|get_lang }}
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
var $modal = $("#em-modal-start");
var $btnStartExercise = $('.exercise_overview_options a');
if ($btnStartExercise.length > 0) {
$modal.modal("show");
}
$modal.on('hidden.bs.modal', function (e) {
$btnStartExercise.removeClass('disabled').removeAttr('aria-disabled');
window.location = $btnStartExercise.prop('href');
});
});
</script>
{% endif %}
{% endif %}

View File

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