Plugin: Zoom: allow attendance signatures - refs BT#19534

pull/4151/head
Angel Fernando Quiroz Campos 4 years ago
parent 246612dfaf
commit 0aae15ffd3
  1. 74
      plugin/zoom/Entity/Meeting.php
  2. 23
      plugin/zoom/Entity/Registrant.php
  3. 84
      plugin/zoom/Entity/Signature.php
  4. 17
      plugin/zoom/README.md
  5. 181
      plugin/zoom/attendance.php
  6. 47
      plugin/zoom/join_meeting.php
  7. 5
      plugin/zoom/lang/english.php
  8. 5
      plugin/zoom/lang/spanish.php
  9. 16
      plugin/zoom/lib/RegistrantRepository.php
  10. 174
      plugin/zoom/lib/ZoomPlugin.php
  11. 50
      plugin/zoom/meeting.ajax.php
  12. 192
      plugin/zoom/view/join.tpl
  13. 4
      plugin/zoom/view/meeting.tpl

@ -18,6 +18,7 @@ use DateInterval;
use DateTime;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Exception;
@ -57,7 +58,7 @@ class Meeting
/**
* @var int
* @ORM\Column(type="integer")
* @ORM\Column(type="integer", name="id")
* @ORM\Id
* @ORM\GeneratedValue()
*/
@ -109,6 +110,20 @@ class Meeting
*/
protected $meetingInfoGetJson;
/**
* @var bool
*
* @ORM\Column(type="boolean", name="sign_attendance")
*/
protected $signAttendance;
/**
* @var string|null
*
* @ORM\Column(type="text", name="reason_to_sign_attendance", nullable=true)
*/
protected $reasonToSignAttendance;
/** @var MeetingListItem */
protected $meetingListItem;
@ -141,6 +156,7 @@ class Meeting
$this->registrants = new ArrayCollection();
$this->recordings = new ArrayCollection();
$this->activities = new ArrayCollection();
$this->signAttendance = false;
}
/**
@ -508,26 +524,21 @@ class Meeting
public function hasRegisteredUser($user)
{
return $this->getRegistrants()->exists(
function (Registrant $registrantEntity) use (&$user) {
function (int $key, Registrant $registrantEntity) use (&$user) {
return $registrantEntity->getUser() === $user;
}
);
}
/**
* @param User $user
*
* @return Registrant|null
*/
public function getRegistrant($user)
public function getRegistrant(User $user): ?Registrant
{
foreach ($this->getRegistrants() as $registrant) {
if ($registrant->getUser() === $user) {
return $registrant;
}
}
$criteria = Criteria::create()
->where(
Criteria::expr()->eq('user', $user)
)
;
return null;
return $this->registrants->matching($criteria)->first() ?? null;
}
/**
@ -538,31 +549,56 @@ class Meeting
*/
public function getIntroduction()
{
$introduction = sprintf('<h1>%s</h1>', $this->meetingInfoGet->topic);
$introduction = sprintf('<h1>%s</h1>', $this->meetingInfoGet->topic).PHP_EOL;
if (!$this->isGlobalMeeting()) {
if (!empty($this->formattedStartTime)) {
$introduction .= $this->formattedStartTime;
if (!empty($this->formattedDuration)) {
$introduction .= ' ('.$this->formattedDuration.')';
}
$introduction .= PHP_EOL;
}
}
if ($this->user) {
$introduction .= sprintf('<p>%s</p>', $this->user->getFullname());
$introduction .= sprintf('<p>%s</p>', $this->user->getFullname()).PHP_EOL;
} elseif ($this->isCourseMeeting()) {
if (null === $this->session) {
$introduction .= sprintf('<p class="main">%s</p>', $this->course);
$introduction .= sprintf('<p class="main">%s</p>', $this->course).PHP_EOL;
} else {
$introduction .= sprintf('<p class="main">%s (%s)</p>', $this->course, $this->session);
$introduction .= sprintf('<p class="main">%s (%s)</p>', $this->course, $this->session).PHP_EOL;
}
}
if (!empty($this->meetingInfoGet->agenda)) {
$introduction .= sprintf('<p>%s</p>', $this->meetingInfoGet->agenda);
$introduction .= sprintf('<p>%s</p>', $this->meetingInfoGet->agenda).PHP_EOL;
}
return $introduction;
}
public function isSignAttendance(): bool
{
return $this->signAttendance;
}
public function setSignAttendance(bool $signAttendance): Meeting
{
$this->signAttendance = $signAttendance;
return $this;
}
public function getReasonToSignAttendance(): ?string
{
return $this->reasonToSignAttendance;
}
public function setReasonToSignAttendance(string $reasonToSignAttendance): Meeting
{
$this->reasonToSignAttendance = $reasonToSignAttendance;
return $this;
}
/**
* @throws Exception on unexpected start_time or duration
*/

@ -30,8 +30,8 @@ class Registrant
public $fullName;
/**
* @var string
* @ORM\Column(type="integer")
* @var int
* @ORM\Column(type="integer", name="id")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
@ -69,6 +69,13 @@ class Registrant
*/
protected $meetingRegistrantJson;
/**
* @var Signature|null
*
* @ORM\OneToOne(targetEntity="Chamilo\PluginBundle\Zoom\Signature", mappedBy="registrant", orphanRemoval=true)
*/
protected $signature;
/** @var CreatedRegistration */
protected $createdRegistration;
@ -262,4 +269,16 @@ class Registrant
$this->meetingRegistrantListItemJson = json_encode($this->meetingRegistrantListItem);
}
}
public function setSignature(Signature $signature): void
{
$this->signature = $signature;
$signature->setRegistrant($this);
}
public function getSignature(): ?Signature
{
return $this->signature;
}
}

@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Zoom;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
* @ORM\Table(name="plugin_zoom_signature")
*/
class Signature
{
/**
* @var int
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var Registrant
*
* @ORM\OneToOne(targetEntity="Chamilo\PluginBundle\Zoom\Registrant", inversedBy="signature")
* @ORM\JoinColumn(name="registrant_id", referencedColumnName="id")
*/
private $registrant;
/**
* @var string
*
* @ORM\Column(name="signature", type="text")
*/
private $file;
/**
* @var DateTime
*
* @ORM\Column(name="registered_at", type="datetime")
*/
private $registeredAt;
public function getId(): int
{
return $this->id;
}
public function getRegistrant(): Registrant
{
return $this->registrant;
}
public function setRegistrant(Registrant $registrant): Signature
{
$this->registrant = $registrant;
return $this;
}
public function getFile(): string
{
return $this->file;
}
public function setFile(string $file): Signature
{
$this->file = $file;
return $this;
}
public function getRegisteredAt(): DateTime
{
return $this->registeredAt;
}
public function setRegisteredAt(DateTime $registeredAt): Signature
{
$this->registeredAt = $registeredAt;
return $this;
}
}

@ -29,7 +29,14 @@ required to authenticate with JWT. To get them, create a JWT App:
- Recording transcript files have completed
Then click on Done then on Save and copy your Verification Token to the field below.
11. click on Continue
10. click on Continue
## Changelog
**v0.4**
Added signed attendance to allow you to configure an attendance sheet where participants register their signature. The
signed attendance functionality is similar to that found in the Exercise Signature plugin but does not reuse it.
## Meetings
@ -63,6 +70,14 @@ For a non-paying Zoom user, this plugin still works but participants will join a
The user that starts the meeting will be identified as the Zoom account that is defined in the plugin. Socreate a generic account that works for all the users that start meetings.
# Upgrade database to v0.4
```sql
CREATE TABLE plugin_zoom_signature (id INT AUTO_INCREMENT NOT NULL, registrant_id INT DEFAULT NULL, signature LONGTEXT NOT NULL, registered_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_D41895893304A716 (registrant_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE plugin_zoom_signature ADD CONSTRAINT FK_D41895893304A716 FOREIGN KEY (registrant_id) REFERENCES plugin_zoom_registrant (id);
ALTER TABLE plugin_zoom_meeting ADD sign_attendance TINYINT(1) NOT NULL, ADD reason_to_sign_attendance LONGTEXT DEFAULT NULL;
```
# Contributing
Read README.code.md for an introduction to the plugin's code.

@ -0,0 +1,181 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Chamilo\PluginBundle\Zoom\Registrant;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/config.php';
api_block_anonymous_users();
$httpRequest = HttpRequest::createFromGlobals();
$meetingId = $httpRequest->get('meetingId', 0);
if (empty($meetingId)) {
api_not_allowed(true);
}
$plugin = ZoomPlugin::create();
$em = Database::getManager();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
$registrantsRepo = $em->getRepository(Registrant::class);
if (null === $meeting) {
api_not_allowed(
true,
Display::return_message($plugin->get_lang('MeetingNotFound'), 'error')
);
}
if (!$plugin->userIsConferenceManager($meeting)
|| !$meeting->isSignAttendance()
) {
api_not_allowed(
true,
Display::return_message(get_lang('NotAvailable'), 'warning')
);
}
$getNumberOfSignatures = function () use ($meeting) {
return $meeting->getRegistrants()->count();
};
$getSignaturesData = function (
$from,
$limit,
$column,
$direction
) use ($registrantsRepo, $meeting) {
if (0 === $column) {
$columnField = 'u.lastname';
} elseif (1 === $column) {
$columnField = 'u.firstname';
} else {
$columnField = 's.registeredAt';
}
$result = $registrantsRepo->findByMeetingPaginated($meeting, $from, $limit, $columnField, $direction);
return array_map(
function (Registrant $registrant) {
$signature = $registrant->getSignature();
return [
$registrant->getUser()->getLastname(),
$registrant->getUser()->getFirstname(),
$signature ? $signature->getRegisteredAt() : null,
$signature ? $signature->getFile() : null,
];
},
$result
);
};
if ($httpRequest->query->has('export')) {
$plugin->exportSignatures(
$meeting,
$httpRequest->query->getAlnum('export')
);
}
$table = new SortableTable('zoom_signatures', $getNumberOfSignatures, $getSignaturesData, 2);
$table->set_header(0, get_lang('LastName'));
$table->set_header(1, get_lang('FirstName'));
$table->set_header(2, get_lang('DateTime'), true, ['class' => 'text-center'], ['class' => 'text-center']);
$table->set_header(3, $plugin->get_lang('Signature'), false, ['style' => 'width: 200px', 'class' => 'text-center']);
$table->set_additional_parameters(
array_filter(
$httpRequest->query->all(),
function ($key): bool {
return strpos($key, 'zoom_signatures_') === false;
},
ARRAY_FILTER_USE_KEY
)
);
$table->set_column_filter(
2,
function ($dateTime) {
return $dateTime ? api_convert_and_format_date($dateTime, DATE_TIME_FORMAT_LONG) : null;
}
);
$table->set_column_filter(
3,
function ($imgData) use ($plugin) {
if (empty($imgData)) {
return null;
}
return Display::img(
$imgData,
$plugin->get_lang('SignatureDone'),
['class' => 'img-thumbnail'],
false
);
}
);
$cidReq = api_get_cidreq();
$queryParams = 'meetingId='.$meeting->getMeetingId().'&'.$cidReq;
$returnURL = 'meetings.php';
if ($meeting->isCourseMeeting()) {
api_protect_course_script(true);
$this_section = SECTION_COURSES;
$returnURL = 'start.php?'.$cidReq;
if (api_is_in_group()) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.$cidReq,
'name' => get_lang('Groups'),
];
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.$cidReq,
'name' => get_lang('GroupSpace').' '.$meeting->getGroup()->getName(),
];
}
}
$interbreadcrumb[] = [
'url' => $returnURL,
'name' => $plugin->get_lang('ZoomVideoConferences'),
];
$interbreadcrumb[] = [
'url' => 'meeting.php?'.$queryParams,
'name' => $meeting->getMeetingInfoGet()->topic,
];
$exportPdfLink = Display::url(
Display::return_icon('pdf.png', get_lang('ExportToPDF'), [], ICON_SIZE_MEDIUM),
api_get_self().'?'.$queryParams.'&export=pdf'
);
$exportXlsLink = Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
api_get_self().'?'.$queryParams.'&export=xls'
);
$pageTitle = $plugin->get_lang('Attendance');
$content = '
<dl>
<dt>'.$plugin->get_lang('ReasonToSign').'</dt>
<dd>'.$meeting->getReasonToSignAttendance().'</dd>
</dl>
'.$table->return_table();
$tpl = new Template($pageTitle);
$tpl->assign(
'actions',
Display::toolbarAction(
'attendance-actions',
[$exportPdfLink.PHP_EOL.$exportXlsLink]
)
);
$tpl->assign('header', $pageTitle);
$tpl->assign('content', $content);
$tpl->display_one_col_template();

@ -15,7 +15,6 @@ if (empty($meetingId)) {
}
$plugin = ZoomPlugin::create();
$content = '';
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
if (null === $meeting) {
@ -36,22 +35,30 @@ if ($meeting->isCourseMeeting()) {
}
}
$startJoinURL = '';
$detailsURL = '';
$signature = '';
$currentUser = api_get_user_entity(api_get_user_id());
$isConferenceManager = $plugin->userIsConferenceManager($meeting);
try {
$startJoinURL = $plugin->getStartOrJoinMeetingURL($meeting);
$content .= $meeting->getIntroduction();
if (!empty($startJoinURL)) {
$content .= Display::url($plugin->get_lang('EnterMeeting'), $startJoinURL, ['class' => 'btn btn-primary']);
} else {
$content .= Display::return_message($plugin->get_lang('ConferenceNotAvailable'), 'warning');
if (empty($startJoinURL)) {
Display::addFlash(
Display::return_message($plugin->get_lang('ConferenceNotAvailable'), 'warning')
);
}
if ($plugin->userIsConferenceManager($meeting)) {
$content .= '&nbsp;'.Display::url(
get_lang('Details'),
api_get_path(WEB_PLUGIN_PATH).'zoom/meeting.php?meetingId='.$meeting->getMeetingId(),
['class' => 'btn btn-default']
);
if ($meeting->isSignAttendance() && !$isConferenceManager) {
$signature = $meeting->getRegistrant($currentUser)->getSignature();
Security::get_token('zoom_signature');
}
if ($isConferenceManager) {
$detailsURL = api_get_path(WEB_PLUGIN_PATH).'zoom/meeting.php?meetingId='.$meeting->getMeetingId();
}
} catch (Exception $exception) {
Display::addFlash(
@ -59,7 +66,15 @@ try {
);
}
Display::display_header($plugin->get_title());
echo $plugin->getToolbar();
echo $content;
Display::display_footer();
$htmlHeadXtra[] = api_get_asset('signature_pad/signature_pad.umd.js');
$tpl = new Template($meeting->getMeetingId());
$tpl->assign('meeting', $meeting);
$tpl->assign('start_url', $startJoinURL);
$tpl->assign('details_url', $detailsURL);
$tpl->assign('is_conference_manager', $isConferenceManager);
$tpl->assign('signature', $signature);
$content = $tpl->fetch('zoom/view/join.tpl');
$tpl->assign('actions', $plugin->getToolbar());
$tpl->assign('content', $content);
$tpl->display_one_col_template();

@ -142,3 +142,8 @@ $strings['ForEveryone'] = "Everyone";
$strings['SomeUsers'] = "Some users (Select later)";
$strings['Activity'] = "Activity";
$strings['ConferenceNotAvailable'] = "Conference not available";
$strings['SignAttendance'] = "Sign attendance";
$strings['ReasonToSign'] = 'Reason to sign attendance';
$strings['ConferenceWithAttendance'] = "Conference with attendance sign";
$strings['Sign'] = "Sign";
$strings['Signature'] = "Signature";

@ -136,3 +136,8 @@ $strings['JoinURLNotAvailable'] = "URL no disponible";
$strings['Meetings'] = "Conferencias";
$strings['Activity'] = "Actividad";
$strings['ConferenceNotAvailable'] = "Conferencia no disponible";
$strings['SignAttendance'] = "Firmar asistencia";
$strings['ReasonToSign'] = 'Razón para firmar asistencia';
$strings['ConferenceWithAttendance'] = "Conferencia con registro de asistencia";
$strings['Sign'] = "Firmar";
$strings['Signature'] = "Firma";

@ -30,4 +30,20 @@ class RegistrantRepository extends EntityRepository
return $this->findBy(['meeting' => $meetings, 'user' => $user]);
}
public function findByMeetingPaginated(Meeting $meeting, int $from, int $limit, string $column, string $direction)
{
$queryBuilder = $this->createQueryBuilder('r')
->join('r.user', 'u')
->leftJoin('r.signature', 's')
->where('r.meeting = :meeting')
->setFirstResult($from)
->setMaxResults($limit)
->orderBy($column, $direction)
;
$queryBuilder->setParameter('meeting', $meeting);
return $queryBuilder->getQuery()->getResult();
}
}

@ -18,6 +18,7 @@ use Chamilo\PluginBundle\Zoom\Recording;
use Chamilo\PluginBundle\Zoom\RecordingRepository;
use Chamilo\PluginBundle\Zoom\Registrant;
use Chamilo\PluginBundle\Zoom\RegistrantRepository;
use Chamilo\PluginBundle\Zoom\Signature;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\OptimisticLockException;
@ -47,8 +48,8 @@ class ZoomPlugin extends Plugin
public function __construct()
{
parent::__construct(
'0.3',
'Sébastien Ducoulombier, Julio Montoya',
'0.4',
'Sébastien Ducoulombier, Julio Montoya, Angel Fernando Quiroz Campos',
[
'tool_enable' => 'boolean',
'apiKey' => 'text',
@ -155,11 +156,12 @@ class ZoomPlugin extends Plugin
$items = [];
foreach ($meetings as $registrant) {
$meeting = $registrant->getMeeting();
$meetingInfoGet = $meeting->getMeetingInfoGet();
$items[sprintf(
$this->get_lang('DateMeetingTitle'),
$meeting->formattedStartTime,
$meeting->getMeetingInfoGet()->topic
)] = sprintf($linkTemplate, $meeting->getId());
$meetingInfoGet->topic
)] = sprintf($linkTemplate, $meetingInfoGet->id);
}
return $items;
@ -189,6 +191,7 @@ class ZoomPlugin extends Plugin
'plugin_zoom_meeting_activity',
'plugin_zoom_recording',
'plugin_zoom_registrant',
'plugin_zoom_signature',
]
);
@ -196,12 +199,15 @@ class ZoomPlugin extends Plugin
return;
}
(new SchemaTool(Database::getManager()))->createSchema(
$em = Database::getManager();
(new SchemaTool($em))->createSchema(
[
Database::getManager()->getClassMetadata(Meeting::class),
Database::getManager()->getClassMetadata(MeetingActivity::class),
Database::getManager()->getClassMetadata(Recording::class),
Database::getManager()->getClassMetadata(Registrant::class),
$em->getClassMetadata(Meeting::class),
$em->getClassMetadata(MeetingActivity::class),
$em->getClassMetadata(Recording::class),
$em->getClassMetadata(Registrant::class),
$em->getClassMetadata(Signature::class),
]
);
@ -232,12 +238,15 @@ class ZoomPlugin extends Plugin
*/
public function uninstall()
{
(new SchemaTool(Database::getManager()))->dropSchema(
$em = Database::getManager();
(new SchemaTool($em))->dropSchema(
[
Database::getManager()->getClassMetadata(Meeting::class),
Database::getManager()->getClassMetadata(MeetingActivity::class),
Database::getManager()->getClassMetadata(Recording::class),
Database::getManager()->getClassMetadata(Registrant::class),
$em->getClassMetadata(Meeting::class),
$em->getClassMetadata(MeetingActivity::class),
$em->getClassMetadata(Recording::class),
$em->getClassMetadata(Registrant::class),
$em->getClassMetadata(Signature::class),
]
);
$this->uninstall_course_fields_in_all_courses();
@ -332,17 +341,25 @@ class ZoomPlugin extends Plugin
$form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
//$form->addLabel(get_lang('Password'), $meeting->getMeetingInfoGet()->password);
// $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
$form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
$form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
$form->addButtonUpdate(get_lang('Update'));
if ($form->validate()) {
$values = $form->exportValues();
if ($meeting->requiresDateAndDuration()) {
$meetingInfoGet->start_time = (new DateTime($form->getSubmitValue('startTime')))->format(
$meetingInfoGet->start_time = (new DateTime($values['startTime']))->format(
DATE_ATOM
);
$meetingInfoGet->timezone = date_default_timezone_get();
$meetingInfoGet->duration = (int) $form->getSubmitValue('duration');
$meetingInfoGet->duration = (int) $values['duration'];
}
$meetingInfoGet->topic = $form->getSubmitValue('topic');
$meetingInfoGet->agenda = $form->getSubmitValue('agenda');
$meetingInfoGet->topic = $values['topic'];
$meetingInfoGet->agenda = $values['agenda'];
$meeting
->setSignAttendance(isset($values['sign_attendance']))
->setReasonToSignAttendance($values['reason_to_sign']);
try {
$meetingInfoGet->update();
$meeting->setMeetingInfoGet($meetingInfoGet);
@ -365,6 +382,8 @@ class ZoomPlugin extends Plugin
$defaults['startTime'] = $meeting->startDateTime->format('Y-m-d H:i');
$defaults['duration'] = $meetingInfoGet->duration;
}
$defaults['sign_attendance'] = $meeting->isSignAttendance();
$defaults['reason_to_sign'] = $meeting->getReasonToSignAttendance();
$form->setDefaults($defaults);
return $form;
@ -829,9 +848,13 @@ class ZoomPlugin extends Plugin
}
}*/
$form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
$form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
$form->addButtonCreate(get_lang('Save'));
if ($form->validate()) {
$formValues = $form->exportValues();
$type = $form->getSubmitValue('type');
switch ($type) {
@ -866,7 +889,9 @@ class ZoomPlugin extends Plugin
$form->getSubmitValue('duration'),
$form->getSubmitValue('topic'),
$form->getSubmitValue('agenda'),
substr(uniqid('z', true), 0, 10)
substr(uniqid('z', true), 0, 10),
isset($formValues['sign_attendance']),
$formValues['reason_to_sign']
);
Display::addFlash(
@ -1193,6 +1218,111 @@ class ZoomPlugin extends Plugin
return self::RECORDING_TYPE_NONE !== $recording;
}
/**
* @throws OptimisticLockException
* @throws \Doctrine\ORM\ORMException
*/
public function saveSignature(Registrant $registrant, string $file): bool
{
if (empty($file)) {
return false;
}
$signature = $registrant->getSignature();
if (null !== $signature) {
return false;
}
$signature = new Signature();
$signature
->setFile($file)
->setRegisteredAt(api_get_utc_datetime(null, false, true))
;
$registrant->setSignature($signature);
$em = Database::getManager();
$em->persist($signature);
$em->flush();
return true;
}
public function getSignature(int $userId, Meeting $meeting): ?Signature
{
$signatureRepo = Database::getManager()
->getRepository(Signature::class)
;
return $signatureRepo->findOneBy(['user' => $userId, 'meeting' => $meeting]);
}
public function exportSignatures(Meeting $meeting, $formatToExport)
{
$signatures = array_map(
function (Registrant $registrant) use ($formatToExport) {
$signature = $registrant->getSignature();
$item = [
$registrant->getUser()->getLastname(),
$registrant->getUser()->getFirstname(),
$signature
? api_convert_and_format_date($signature->getRegisteredAt(), DATE_TIME_FORMAT_LONG)
: '-',
];
if ('pdf' === $formatToExport) {
$item[] = $signature
? Display::img($signature->getFile(), '', ['style' => 'width: 150px;'], false)
: '-';
}
return $item;
},
$meeting->getRegistrants()->toArray()
);
$data = array_merge(
[
[
get_lang('LastName'),
get_lang('FirstName'),
get_lang('DateTime'),
'pdf' === $formatToExport ? get_lang('File') : null,
],
],
$signatures
);
if ('pdf' === $formatToExport) {
$params = [
'filename' => get_lang('Attendance'),
'pdf_title' => get_lang('Attendance'),
'pdf_description' => $meeting->getIntroduction(),
'show_teacher_as_myself' => false,
];
Export::export_table_pdf($data, $params);
}
if ('xls' === $formatToExport) {
$introduction = array_map(
function ($line) {
return [
strip_tags(trim($line)),
];
},
explode(PHP_EOL, $meeting->getIntroduction())
);
Export::arrayToXls(
array_merge($introduction, $data),
get_lang('Attendance')
);
}
}
/**
* Updates meeting registrants list. Adds the missing registrants and removes the extra.
*
@ -1416,7 +1546,9 @@ class ZoomPlugin extends Plugin
$duration,
$topic,
$agenda,
$password
$password,
bool $signAttendance = false,
string $reasonToSignAttendance = ''
) {
$meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
$meetingInfoGet->duration = $duration;
@ -1435,6 +1567,8 @@ class ZoomPlugin extends Plugin
->setCourse($course)
->setGroup($group)
->setSession($session)
->setSignAttendance($signAttendance)
->setReasonToSignAttendance($reasonToSignAttendance)
);
}

@ -0,0 +1,50 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\Meeting;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
require_once __DIR__.'/config.php';
api_block_anonymous_users(false);
$httpRequest = HttpRequest::createFromGlobals();
$meetingId = $httpRequest->get('meetingId', 0);
if (empty($meetingId)) {
api_not_allowed();
}
$plugin = ZoomPlugin::create();
/** @var Meeting $meeting */
$meeting = $plugin->getMeetingRepository()->findOneBy(['meetingId' => $meetingId]);
$currentUserId = api_get_user_id();
$currentUser = api_get_user_entity($currentUserId);
if (null === $meeting) {
api_not_allowed(false, $plugin->get_lang('MeetingNotFound'));
}
switch ($httpRequest->get('a')) {
case 'sign_attempt':
if (!$meeting->isSignAttendance() ||
!$meeting->hasRegisteredUser($currentUser)
) {
api_not_allowed();
}
$registrant = $meeting->getRegistrant($currentUser);
$file = $httpRequest->request->get('file', '');
$secToken = Security::get_token('zoom_signature');
if (!Security::check_token($secToken, null, 'zoom_signature')) {
api_not_allowed();
}
echo (int) $plugin->saveSignature($registrant, $file);
exit;
}

@ -0,0 +1,192 @@
{{ meeting.introduction }}
{% if is_conference_manager and meeting.isSignAttendance %}
<p class="text-info">
<span class="fa fa-list-alt"></span>
{{ 'ConferenceWithAttendance'|get_plugin_lang('ZoomPlugin') }}
</p>
{% endif %}
<hr>
{% set btn_start = '' %}
{% if start_url %}
{% set btn_start %}
<a href="{{ start_url }}" class="btn btn-primary">
{{ 'EnterMeeting'|get_plugin_lang('ZoomPlugin') }}
</a>
{% endset %}
{% endif %}
{% if not is_conference_manager %}
{% if meeting.isSignAttendance %}
<div class="row">
<div class="col-md-offset-3 col-md-6">
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">
<span class="fa fa-pencil-square-o fa-fw" aria-hidden="true"></span>
{{ 'Attendance'|get_lang }}
</h3>
</div>
<div class="panel-body">
<p>{{ meeting.reasonToSignAttendance }}</p>
{% if signature %}
<div class="thumbnail">
<img src="{{ signature.file }}"
alt="{{ 'SignatureDone'|get_plugin_lang('ZoomPlugin') }}">
<div class="caption text-center">
{{ signature.registeredAt|api_convert_and_format_date(constant('DATE_TIME_FORMAT_LONG')) }}
</div>
</div>
{% else %}
{% set btn_start = '' %}
{% if 'started' == meeting.meetingInfoGet.status %}
<button class="btn btn-info" id="btn-sign" data-toggle="modal"
data-target="#signature-modal">
<i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
{{ 'Sign'|get_plugin_lang('ZoomPlugin') }}
</button>
<div class="modal fade" tabindex="-1" role="dialog" id="signature-modal"
data-backdrop="static">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{ 'SignAttendance'|get_plugin_lang('ZoomPlugin') }}</h4>
</div>
<div class="modal-body">
<div id="signature-modal--signature-area" class="well">
<canvas></canvas>
</div>
</div>
<div class="modal-footer">
<span id="signature-modal--loader" aria-hidden="true"
class="fa fa-refresh fa-spin"
aria-label="{{ 'Loading'|get_lang }}" style="display: none;">
</span>
<span id="signature-modal--save-controls">
<button id="signature-modal--btn-save" class="btn btn-primary">
<em class="fa fa-save" aria-hidden="true"></em>
{{ 'Save'|get_lang }}
</button>
<button id="signature-modal--btn-clean" class="btn btn-default">
<em class="fa fa-eraser" aria-hidden="true"></em>
{{ 'Clean'|get_lang }}
</button>
</span>
<div id="signature-modal--close-controls" style="display: none;">
<span id="signature-modal--results"></span>
<button class="btn btn-default"
data-dismiss="modal">{{ 'Close'|get_lang }}</button>
</div>
</div>
</div>
</div>
</div>
<script>
$(function () {
var $signatureArea = $('#signature-modal--signature-area')
var $loader = $('#signature-modal--loader')
var $saveControls = $('#signature-modal--save-controls')
var $btnSave = $('#signature-modal--btn-save')
var $btnClean = $('#signature-modal--btn-clean')
var $closeControls = $('#signature-modal--close-controls')
var $txtResults = $('#signature-modal--results')
var imageFormat = 'image/png'
var canvas = document.querySelector('#signature-modal--signature-area canvas')
var signaturePad = new SignaturePad(canvas)
$('#signature-modal')
.on('shown.bs.modal', function (e) {
var parentWidth = $signatureArea.width()
var parentHeight = $signatureArea.height()
canvas.setAttribute('width', parentWidth + 'px')
canvas.setAttribute('height', parentHeight + 'px')
signaturePad = new SignaturePad(canvas)
})
.on('hide.bs.modal', function (e) {
$loader.hide()
$saveControls.show()
$closeControls.hide()
$signatureArea.show()
$btnSave.prop('disabled', false)
$btnClean.prop('disabled', false)
})
$btnClean.on('click', function () {
signaturePad.clear()
})
$btnSave.on('click', function () {
if (signaturePad.isEmpty()) {
alert('{{ 'ProvideASignatureFirst'|get_plugin_lang('ZoomPlugin')|e('js') }}')
return false
}
var dataURL = signaturePad.toDataURL(imageFormat)
$.ajax({
beforeSend: function () {
$loader.show()
$btnSave.prop('disabled', true)
$btnClean.prop('disabled', true)
},
type: 'POST',
url: 'meeting.ajax.php?{{ _p.web_cid_query }}',
data: {
a: 'sign_attempt',
meetingId: {{ meeting.meetingId }},
file: dataURL
},
success: function (data) {
$btnSave.prop('disabled', false)
$btnClean.prop('disabled', false)
$loader.hide()
$saveControls.hide()
$signatureArea.hide()
signaturePad.clear()
if ('1' === data) {
$txtResults.html('{{ 'Saved'|get_lang }}')
window.location.reload()
} else {
$txtResults.html('{{ 'Error'|get_lang }}')
}
$closeControls.show()
},
})
})
})
</script>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
{{ btn_start }}
{% if details_url %}
<a href="{{ details_url }}" class="btn btn-default">
{{ 'Details'|get_lang }}
</a>
{% endif %}

@ -20,6 +20,10 @@
<a class="btn btn-default" href="activity.php?meetingId={{ meeting.meetingId }}&{{ url_extra }}">
{{ 'Activity'|get_plugin_lang('ZoomPlugin') }}
</a>
<a href="attendance.php?meetingId={{ meeting.meetingId ~ '&' ~ url_extra }}" class="btn btn-info">
{{ 'Attendance'|get_lang }}
</a>
{% endif %}
</div>

Loading…
Cancel
Save