diff --git a/public/plugin/justification/Justification.php b/public/plugin/justification/Justification.php
new file mode 100644
index 0000000000..d9e648b26e
--- /dev/null
+++ b/public/plugin/justification/Justification.php
@@ -0,0 +1,99 @@
+ 'boolean',
+ 'default_course_id' => 'text',
+ ]
+ );
+ }
+
+ /**
+ * @return $this
+ */
+ public static function create()
+ {
+ static $result = null;
+
+ return $result ? $result : $result = new self();
+ }
+
+ public function getJustification($id)
+ {
+ $id = (int) $id;
+
+ $sql = 'SELECT * FROM justification_document WHERE id = '.$id;
+ $query = Database::query($sql);
+
+ return Database::fetch_array($query, 'ASSOC');
+ }
+
+ public function getUserJustificationList($userId)
+ {
+ $userId = (int) $userId;
+
+ $sql = "SELECT * FROM justification_document_rel_users WHERE user_id = $userId ";
+ $query = Database::query($sql);
+
+ return Database::store_result($query, 'ASSOC');
+ }
+
+ public function getUserJustification($id)
+ {
+ $id = (int) $id;
+
+ $sql = "SELECT * FROM justification_document_rel_users WHERE id = $id ";
+ $query = Database::query($sql);
+
+ return Database::fetch_array($query, 'ASSOC');
+ }
+
+ public function getList()
+ {
+ $sql = 'SELECT * FROM justification_document ORDER BY name ';
+ $query = Database::query($sql);
+
+ return Database::store_result($query, 'ASSOC');
+ }
+
+ /**
+ * Install.
+ */
+ public function install()
+ {
+ $sql = "CREATE TABLE IF NOT EXISTS justification_document (
+ id INT unsigned NOT NULL auto_increment PRIMARY KEY,
+ code TEXT NULL,
+ name TEXT NULL,
+ validity_duration INT,
+ comment TEXT NULL,
+ date_manual_on INT
+ )";
+ Database::query($sql);
+
+ $sql = "CREATE TABLE IF NOT EXISTS justification_document_rel_users (
+ id INT unsigned NOT NULL auto_increment PRIMARY KEY,
+ justification_document_id INT NOT NULL,
+ file_path VARCHAR(255),
+ user_id INT,
+ date_validity DATE
+ )";
+ Database::query($sql);
+ }
+
+ public function uninstall()
+ {
+ $sql = 'DROP TABLE IF EXISTS justification_document';
+ Database::query($sql);
+
+ $sql = 'DROP TABLE IF EXISTS justification_document_rel_users';
+ Database::query($sql);
+ }
+}
diff --git a/public/plugin/justification/README.md b/public/plugin/justification/README.md
new file mode 100644
index 0000000000..8eac4b4faf
--- /dev/null
+++ b/public/plugin/justification/README.md
@@ -0,0 +1,5 @@
+Justification
+==============
+
+1. Enable the plugin.
+2. Create the justification files in plugin/justification/list.php
\ No newline at end of file
diff --git a/public/plugin/justification/add.php b/public/plugin/justification/add.php
new file mode 100644
index 0000000000..ed5629b018
--- /dev/null
+++ b/public/plugin/justification/add.php
@@ -0,0 +1,68 @@
+addText('name', get_lang('Name'));
+$form->addText('code', $plugin->get_lang('JustificationCode'));
+$form->addNumeric('validity_duration', $plugin->get_lang('ValidityDuration'));
+$form->addCheckBox('date_manual_on', $plugin->get_lang('DateManualOn'));
+$form->addTextarea('comment', get_lang('Comment'));
+$form->addButtonSave(get_lang('Save'));
+
+if ($form->validate()) {
+ $values = $form->getSubmitValues();
+ $dateManual = isset($values['date_manual_on']) ? 1 : 0;
+
+ $cleanedCode = api_replace_dangerous_char($values['code']);
+ $code = Database::escape_string($cleanedCode);
+
+ $sql = "SELECT * FROM justification_document WHERE code = '$code' ";
+ $result = Database::query($sql);
+ $data = Database::fetch_array($result);
+ $message = Display::return_message(get_lang('ThisCodeAlradyExists'), 'warning');
+
+ if (empty($data)) {
+ $params = [
+ 'name' => $values['name'],
+ 'code' => $cleanedCode,
+ 'validity_duration' => $values['validity_duration'],
+ 'date_manual_on' => $dateManual,
+ 'comment' => $values['comment'],
+ ];
+ Database::insert('justification_document', $params);
+ $message = Display::return_message(get_lang('Saved'));
+ }
+
+ Display::addFlash($message);
+
+ $url = api_get_path(WEB_PLUGIN_PATH).'justification/list.php?';
+ header('Location: '.$url);
+ exit;
+}
+
+$actionLinks = Display::toolbarButton(
+ $plugin->get_lang('Back'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/list.php',
+ 'arrow-left',
+ 'primary'
+);
+
+$tpl->assign(
+ 'actions',
+ Display::toolbarAction('toolbar', [$actionLinks])
+);
+
+$content = $form->returnForm();
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/public/plugin/justification/cron.php b/public/plugin/justification/cron.php
new file mode 100644
index 0000000000..d03992f3d9
--- /dev/null
+++ b/public/plugin/justification/cron.php
@@ -0,0 +1,88 @@
+getList();
+$totalFields = count($fieldList);
+
+if (empty($fieldList)) {
+ echo 'No fields to check. Please add them in the justification plugin';
+ exit;
+}
+
+$userList = UserManager::get_user_list();
+$count = count($userList);
+
+echo "#$count users found".PHP_EOL;
+$currentDate = api_get_utc_datetime();
+
+foreach ($userList as $user) {
+ $userId = $user['id'];
+
+ echo "Checking user id #$userId".PHP_EOL;
+
+ $userJustificationList = $plugin->getUserJustificationList($userId);
+ $userJustificationDocumentList = array_column($userJustificationList, 'date_validity', 'justification_document_id');
+
+ if (count($userJustificationList) < $totalFields) {
+ unsubscribeUser($userId, $courseInfo);
+ continue;
+ }
+
+ if (count($userJustificationList) >= $totalFields) {
+ $successList = [];
+ foreach ($fieldList as $field) {
+ if (isset($userJustificationDocumentList[$field['id']])) {
+ $dateValidity = $userJustificationDocumentList[$field['id']];
+ if ($dateValidity > $currentDate) {
+ $successList[] = true;
+ }
+ }
+ }
+ $countSuccess = count($successList);
+ if ($countSuccess === $totalFields) {
+ subscribeUser($userId, $courseInfo);
+ continue;
+ } else {
+ echo "User #$userId only got $countSuccess justification(s) out of $totalFields.".PHP_EOL;
+ }
+ }
+
+ unsubscribeUser($userId, $courseInfo);
+}
+
+function unsubscribeUser($userId, $courseInfo)
+{
+ $courseId = $courseInfo['real_id'];
+ CourseManager::unsubscribe_user($userId, $courseInfo['code']);
+ echo "Unsubscribe user id #$userId to course #$courseId".PHP_EOL;
+}
+
+function subscribeUser($userId, $courseInfo)
+{
+ $courseId = $courseInfo['real_id'];
+ $isUserSubscribed = CourseManager::is_user_subscribed_in_course($userId, $courseInfo['code']);
+ if ($isUserSubscribed === false) {
+ CourseManager::subscribeUser($userId, $courseInfo['code'], STUDENT);
+ echo "Subscribe user id #$userId to course #$courseId".PHP_EOL;
+ } else {
+ echo "Nothing to do user id #$userId is already subscribed to #$courseId".PHP_EOL;
+ }
+}
diff --git a/public/plugin/justification/edit.php b/public/plugin/justification/edit.php
new file mode 100644
index 0000000000..2852732eac
--- /dev/null
+++ b/public/plugin/justification/edit.php
@@ -0,0 +1,76 @@
+getJustification($id);
+
+$tpl = new Template($tool);
+$fields = [];
+
+$form = new FormValidator('add', 'post', api_get_self().'?id='.$id);
+$form->addText('name', get_lang('Name'));
+$form->addText('code', $plugin->get_lang('JustificationCode'));
+$form->addNumeric('validity_duration', $plugin->get_lang('ValidityDuration'));
+$form->addCheckBox('date_manual_on', $plugin->get_lang('DateManualOn'));
+$form->addTextarea('comment', get_lang('Comment'));
+$form->addButtonSave(get_lang('Update'));
+
+$form->setDefaults($justification);
+
+if ($form->validate()) {
+ $values = $form->getSubmitValues();
+ $cleanedCode = api_replace_dangerous_char($values['code']);
+ $code = Database::escape_string($cleanedCode);
+
+ $sql = "SELECT * FROM justification_document WHERE code = '$code' AND id <> $id";
+ $result = Database::query($sql);
+ $data = Database::fetch_array($result);
+ $message = Display::return_message(get_lang('ThisCodeAlradyExists'), 'warning');
+ if (empty($data)) {
+ $params = [
+ 'name' => $values['name'],
+ 'code' => $cleanedCode,
+ 'validity_duration' => $values['validity_duration'],
+ 'date_manual_on' => (int) $values['date_manual_on'],
+ 'comment' => $values['comment'],
+ ];
+
+ Database::update('justification_document', $params, ['id = ?' => $id]);
+ $message = Display::return_message(get_lang('Saved'));
+ }
+
+ Display::addFlash($message);
+
+ $url = api_get_path(WEB_PLUGIN_PATH).'justification/list.php?';
+ header('Location: '.$url);
+ exit;
+}
+
+$actionLinks = Display::toolbarButton(
+ $plugin->get_lang('Back'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/list.php',
+ 'arrow-left',
+ 'primary'
+);
+
+$tpl->assign(
+ 'actions',
+ Display::toolbarAction('toolbar', [$actionLinks])
+);
+
+$content = $form->returnForm();
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/public/plugin/justification/index.php b/public/plugin/justification/index.php
new file mode 100644
index 0000000000..e1533d2fe8
--- /dev/null
+++ b/public/plugin/justification/index.php
@@ -0,0 +1,4 @@
+install();
diff --git a/public/plugin/justification/justification_by_user.php b/public/plugin/justification/justification_by_user.php
new file mode 100644
index 0000000000..ac900fec7c
--- /dev/null
+++ b/public/plugin/justification/justification_by_user.php
@@ -0,0 +1,113 @@
+addHeader('Search');
+$form->addSelectAjax(
+ 'user_id',
+ get_lang('User'),
+ [],
+ [
+ 'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
+ ]
+);
+$form->addButtonSearch(get_lang('Search'));
+$tpl->assign('form', $form->returnForm());
+
+$userId = isset($_REQUEST['user_id']) ? (int) $_REQUEST['user_id'] : 0;
+
+if ($form->validate()) {
+ $userId = $form->getSubmitValue('user_id');
+}
+
+if ($userId) {
+ $tpl->assign('user_info', api_get_user_info($userId));
+ $list = $plugin->getUserJustificationList($userId);
+ if ($list) {
+ foreach ($list as &$item) {
+ if ($item['date_validity'] < api_get_local_time()) {
+ $item['date_validity'] = Display::label($item['date_validity'], 'warning');
+ }
+ $item['justification'] = $plugin->getJustification($item['justification_document_id']);
+ $item['file_path'] = Display::url(
+ $item['file_path'],
+ api_get_uploaded_web_url('justification', $item['id'], $item['file_path']),
+ ['target' => '_blank']
+ );
+ }
+ }
+ if (empty($list)) {
+ Display::addFlash(Display::return_message($plugin->get_lang('NoJustificationFound')));
+ }
+ $tpl->assign('list', $list);
+}
+
+$tpl->assign('user_id', $userId);
+$content = $tpl->fetch('justification/view/justification_user_list.tpl');
+
+$actionLinks = '';
+
+$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : '';
+$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
+
+switch ($action) {
+ case 'edit':
+ $userJustification = $plugin->getUserJustification($id);
+ $userInfo = api_get_user_info($userJustification['user_id']);
+ $form = new FormValidator('edit', 'post', api_get_self().'?a=edit&id='.$id.'&user_id='.$userId);
+ $form->addHeader($userInfo['complete_name']);
+ $element = $form->addDatePicker('date_validity', $plugin->get_lang('ValidityDate'));
+ $element->setValue($userJustification['date_validity']);
+ $form->addButtonUpdate(get_lang('Update'));
+ $form->setDefaults($userJustification);
+ $content = $form->returnForm();
+
+ if ($form->validate()) {
+ $values = $form->getSubmitValues();
+ $date = Database::escape_string($values['date_validity']);
+ $sql = "UPDATE justification_document_rel_users SET date_validity = '$date' WHERE id = $id";
+ Database::query($sql);
+ Display::addFlash(Display::return_message(get_lang('Updated')));
+ header('Location: '.api_get_self().'?user_id='.$userId);
+ exit;
+ }
+ break;
+ case 'delete':
+ $userJustification = $plugin->getUserJustification($id);
+ if ($userJustification) {
+ api_remove_uploaded_file_by_id('justification', $id, $userJustification['file_path']);
+
+ $sql = "DELETE FROM justification_document_rel_users WHERE id = $id";
+ Database::query($sql);
+
+ Display::addFlash(Display::return_message(get_lang('Deleted')));
+ }
+ header('Location: '.api_get_self().'?user_id='.$userId);
+ exit;
+ break;
+}
+
+$actionLinks .= Display::toolbarButton(
+ $plugin->get_lang('Back'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/list.php',
+ 'arrow-left',
+ 'primary'
+);
+
+$tpl->assign(
+ 'actions',
+ Display::toolbarAction('toolbar', [$actionLinks])
+);
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/public/plugin/justification/lang/english.php b/public/plugin/justification/lang/english.php
new file mode 100644
index 0000000000..8542a97dfe
--- /dev/null
+++ b/public/plugin/justification/lang/english.php
@@ -0,0 +1,21 @@
+getList();
+
+$tpl->assign('list', $list);
+
+$content = $tpl->fetch('justification/view/list.tpl');
+$actionLinks = '';
+$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : '';
+$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
+
+switch ($action) {
+ case 'delete':
+ $sql = "DELETE FROM justification_document WHERE id = $id";
+ Database::query($sql);
+
+ Display::addFlash(Display::return_message(get_lang('Deleted')));
+ header('Location: '.api_get_self());
+ exit;
+ break;
+}
+
+$actionLinks .= Display::toolbarButton(
+ $plugin->get_lang('Add'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/add.php',
+ 'plus',
+ 'primary'
+);
+$actionLinks .= Display::toolbarButton(
+ $plugin->get_lang('Users'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/justification_by_user.php',
+ 'user',
+ 'primary'
+);
+
+$actionLinks .= Display::toolbarButton(
+ $plugin->get_lang('SetNewCourse'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/set_course.php',
+ 'book',
+ 'primary'
+);
+
+$tpl->assign(
+ 'actions',
+ Display::toolbarAction('toolbar', [$actionLinks])
+);
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/public/plugin/justification/plugin.php b/public/plugin/justification/plugin.php
new file mode 100644
index 0000000000..02126ea19e
--- /dev/null
+++ b/public/plugin/justification/plugin.php
@@ -0,0 +1,6 @@
+get_info();
diff --git a/public/plugin/justification/set_course.php b/public/plugin/justification/set_course.php
new file mode 100644
index 0000000000..1fdecfb6d6
--- /dev/null
+++ b/public/plugin/justification/set_course.php
@@ -0,0 +1,57 @@
+addHeader($plugin->get_lang('SetNewCourse'));
+$currentCourse = api_get_setting('justification_default_course_id', 'justification');
+
+if (!empty($currentCourse)) {
+ $courseInfo = api_get_course_info_by_id($currentCourse);
+ Display::addFlash(Display::return_message(get_lang('Course').': '.$courseInfo['title']));
+}
+
+$form->addSelectAjax(
+ 'course_id',
+ get_lang('Course'),
+ null,
+ [
+ 'url' => api_get_path(WEB_AJAX_PATH).'course.ajax.php?a=search_course',
+ ]
+);
+$form->addButtonSave(get_lang('Save'));
+
+if ($form->validate()) {
+ $values = $form->getSubmitValues();
+ api_set_setting('justification_default_course_id', $values['course_id']);
+ Display::addFlash(Display::return_message(get_lang('Saved')));
+ $url = api_get_path(WEB_PLUGIN_PATH).'justification/list.php?';
+ header('Location: '.$url);
+ exit;
+}
+
+$actionLinks = Display::toolbarButton(
+ $plugin->get_lang('Back'),
+ api_get_path(WEB_PLUGIN_PATH).'justification/list.php',
+ 'arrow-left',
+ 'primary'
+);
+
+$tpl->assign(
+ 'actions',
+ Display::toolbarAction('toolbar', [$actionLinks])
+);
+
+$content = $form->returnForm();
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/public/plugin/justification/uninstall.php b/public/plugin/justification/uninstall.php
new file mode 100644
index 0000000000..a1e3b57a12
--- /dev/null
+++ b/public/plugin/justification/uninstall.php
@@ -0,0 +1,8 @@
+uninstall();
diff --git a/public/plugin/justification/view/add.tpl b/public/plugin/justification/view/add.tpl
new file mode 100644
index 0000000000..078f032f1f
--- /dev/null
+++ b/public/plugin/justification/view/add.tpl
@@ -0,0 +1,48 @@
+{{ search_form }}
+
+
+
+ | {{ 'CreatedAt'|get_lang }} |
+ {{ 'Status'|get_lang }} |
+ {{ 'Records'|get_plugin_lang('BBBPlugin') }} |
+ {{ 'Course'|get_lang }} |
+ {{ 'Session'|get_lang }} |
+ {{ 'Participants'|get_lang }} |
+ {{ 'Actions'|get_lang }} |
+
+
+
+ {% for meeting in meetings %}
+
+ {% if meeting.visibility == 0 %}
+ | {{ meeting.created_at }} |
+ {% else %}
+ {{ meeting.created_at }} |
+ {% endif %}
+
+ {% if meeting.status == 1 %}
+ {{ 'MeetingOpened'|get_plugin_lang('BBBPlugin') }}
+ {% else %}
+ {{ 'MeetingClosed'|get_plugin_lang('BBBPlugin') }}
+ {% endif %}
+ |
+
+ {% if meeting.record == 1 %}
+ {# Record list #}
+ {{ meeting.show_links }}
+ {% else %}
+ {{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}
+ {% endif %}
+ |
+ {{ meeting.course ?: '-' }} |
+ {{ meeting.session ?: '-' }} |
+
+ {{ meeting.participants ? meeting.participants|join(' ') : '-' }}
+ |
+
+ {{ meeting.action_links }}
+ |
+
+ {% endfor %}
+
+
diff --git a/public/plugin/justification/view/justification_user_list.tpl b/public/plugin/justification/view/justification_user_list.tpl
new file mode 100644
index 0000000000..35b88d286c
--- /dev/null
+++ b/public/plugin/justification/view/justification_user_list.tpl
@@ -0,0 +1,38 @@
+{{ form }}
+
+{% if list %}
+
+
+
+
+
+ | {{ 'Justification'| get_plugin_lang('Justification') }} |
+ {{ 'File'| get_lang }} |
+ {{ 'Date'| get_lang('Date') }} |
+ {{ 'Actions'| get_lang }} |
+
+ {% for item in list %}
+
+ | {{ item.justification.name }} |
+ {{ item.file_path }} |
+
+ {{ item.date_validity }}
+ |
+
+
+ {{'Edit' | get_lang}}
+
+
+ {{'Delete' | get_lang}}
+
+ |
+
+ {% endfor %}
+
+
+
+{% endif %}
diff --git a/public/plugin/justification/view/list.tpl b/public/plugin/justification/view/list.tpl
new file mode 100644
index 0000000000..2147efa944
--- /dev/null
+++ b/public/plugin/justification/view/list.tpl
@@ -0,0 +1,33 @@
+
+
+
+
+
+ | {{ 'Name'| get_lang }} |
+ {{ 'ValidityDuration'| get_plugin_lang('Justification') }} |
+ {{ 'DateManualOn'| get_plugin_lang('Justification') }} |
+ {{ 'Actions'| get_lang }} |
+
+
+ {% for item in list %}
+
+ | {{ item.name }} ({{ item.code }}) |
+ {{ item.validity_duration }} |
+
+ {{ item.date_manual_on }} |
+
+
+ {{'Edit' | get_lang}}
+
+
+
+ {{'Delete' | get_lang}}
+
+ |
+
+ {% endfor %}
+
+
+