Skip to content

Commit

Permalink
CTP-3261 Address testing comments
Browse files Browse the repository at this point in the history
  • Loading branch information
aydevworks committed Apr 12, 2024
1 parent fa3d0b1 commit 4564a61
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 47 deletions.
21 changes: 20 additions & 1 deletion classes/examactivity/examactivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,27 @@
*/
abstract class examactivity {
/** @var \stdClass Activity instance */
protected $activityinstance;
public \stdClass $activityinstance;

/** @var int Time buffer before/after exam period */
public int $timebuffer;

/** @var int Exam duration */
public int $examduration;

/**
* Constructor.
*
* @param \stdClass $activityinstance The activity instance.
* @throws \dml_exception
*/
public function __construct(\stdClass $activityinstance) {
// Set activity instance.
$this->activityinstance = $activityinstance;

// Get time buffer and exam duration settings in seconds.
$this->timebuffer = get_config('local_examguard', 'timebuffer') * 60;
$this->examduration = get_config('local_examguard', 'examduration') * 60;
}

/**
Expand All @@ -50,4 +62,11 @@ abstract public function is_exam_activity(): bool;
* @return bool
*/
abstract public function is_active_exam_activity(): bool;

/**
* Get the exam end time including buffer time.
*
* @return int
*/
abstract public function get_exam_end_time(): int;
}
23 changes: 13 additions & 10 deletions classes/examactivity/quiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@
* @author Alex Yeung <[email protected]>
*/
class quiz extends examactivity {
/** @var int Time buffer (20 minutes) before/after exam period. */
const TIME_BUFFER = 60 * 20;

/** @var int Exam duration less than this value (5 hours) will be counted as exam */
const EXAM_DURATION = 3600 * 5;

/**
* Check if the quiz is an exam activity.
*
Expand All @@ -39,8 +33,8 @@ class quiz extends examactivity {
public function is_exam_activity(): bool {
// Timeopen and timeclose are set.
if ($this->activityinstance->timeopen != 0 && $this->activityinstance->timeclose != 0) {
// Quiz is considered an exam activity if the time between open and close is less than 5 hours.
return ($this->activityinstance->timeclose - $this->activityinstance->timeopen) < self::EXAM_DURATION;
// Quiz is considered an exam activity if the time between open and close is less than configured exam duration.
return ($this->activityinstance->timeclose - $this->activityinstance->timeopen) < $this->examduration;
}

return false;
Expand All @@ -54,10 +48,19 @@ public function is_exam_activity(): bool {
public function is_active_exam_activity(): bool {
if ($this->is_exam_activity()) {
$now = time();
return ($this->activityinstance->timeopen - self::TIME_BUFFER < $now &&
$this->activityinstance->timeclose + self::TIME_BUFFER > $now);
return ($this->activityinstance->timeopen - $this->timebuffer < $now &&
$this->activityinstance->timeclose + $this->timebuffer > $now);
}

return false;
}

/**
* Get the exam end time including buffer time (in unix timestamp).
*
* @return int
*/
public function get_exam_end_time(): int {
return $this->activityinstance->timeclose + $this->timebuffer;
}
}
43 changes: 37 additions & 6 deletions classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use cache;
use context_course;
use core\notification;

/**
* Manager class for local_examguard.
Expand Down Expand Up @@ -70,6 +71,13 @@ public static function get_active_exam_activities(int $courseid): array {
}
}

if (!empty($activities)) {
// Sort the activities by end time in descending order.
usort($activities, function ($a, $b) {
return $b->get_exam_end_time() <=> $a->get_exam_end_time();
});
}

return $activities;
}

Expand Down Expand Up @@ -162,26 +170,49 @@ public static function user_has_an_editing_role(int $courseid, int $userid): boo
* Depends on course's exam status, add/remove the exam guard role.
*
* @param int $courseid
* @return bool
* @return array
* @throws \dml_exception
* @throws \moodle_exception
*/
public static function check_course_exam_status(int $courseid) {
public static function check_course_exam_status(int $courseid): array {
global $DB;

$editbanned = $DB->get_record('local_examguard', ['courseid' => $courseid]);
$preventediting = self::should_prevent_course_editing($courseid);
$activeexamactivities = self::get_active_exam_activities($courseid);
$preventediting = !empty($activeexamactivities);

// Added exam guard role to all users having editing roles in the course already.
if ($editbanned && $preventediting) {
// Do nothing and show the notification message.
return true;
return [true, $activeexamactivities];
} else if (!$editbanned && !$preventediting) {
// Do nothing and do not show the notification message.
return false;
return [false, $activeexamactivities];
}

return self::handle_users_roles($courseid, $preventediting);
return [self::handle_users_roles($courseid, $preventediting), $activeexamactivities];
}

/**
* Show notification banner.
*
* @param array $activeexamactivities
* @return void
*/
public static function show_notfication_banner(array $activeexamactivities): void {
global $PAGE;

if (!empty($activeexamactivities)) {
// Get the active exam activity with the latest end time.
$latestexamactivity = reset($activeexamactivities);
$renderer = $PAGE->get_renderer('local_examguard');

// Add notification banner.
notification::add(
$renderer->render_notification_banner($latestexamactivity->get_exam_end_time()),
notification::ERROR
);
}
}

/**
Expand Down
44 changes: 44 additions & 0 deletions classes/output/renderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace local_examguard\output;

use plugin_renderer_base;

/**
* Output renderer for local_examguard.
*
* @package local_examguard
* @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Alex Yeung <[email protected]>
*/
class renderer extends plugin_renderer_base {

/**
* Render notification banner.
*
* @param int $examendtime The exam end time.
* @return string
* @throws \moodle_exception
*/
public function render_notification_banner(int $examendtime): string {
return $this->output->render_from_template(
'local_examguard/notification_banner',
['examendtime' => date('h:i a', $examendtime)]
);
}
}
8 changes: 7 additions & 1 deletion lang/en/local_examguard.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@

$string['cachedef_editingroles'] = 'Stores editing roles in the system.';
$string['cachedef_examguardcourses'] = 'Stores courses that are in exam mode.';
$string['errorcourseeditingbanned'] = 'Editing is not allowed during exam.';
$string['errorcreaterole'] = 'Exam Guard role could not be created.';
$string['errorrolenotexists'] = 'Exam Guard role not exists.';
$string['pluginname'] = 'Exam Guard';
$string['privacy:metadata'] = 'This plugin does not store any personal data.';
$string['settings:enable'] = 'Enable Exam Guard';
$string['settings:enable:desc'] = 'Enable Exam Guard to prevent course editing during exams.';
$string['settings:examduration'] = 'Exam duration';
$string['settings:examduration_desc'] = 'The duration of an exam activity will be considered an exam activity (in minutes).';
$string['settings:generalsettingsheader'] = 'General settings';
$string['warningmessage'] = 'Exam in progress! Course editing is not allowed.';
$string['settings:manage'] = 'Manage Exam Guard';
$string['settings:timebuffer'] = 'Time buffer';
$string['settings:timebuffer_desc'] = 'The time buffer before and after an exam activity (in minutes).';
$string['warningmessage'] = 'Exam in progress! Course editing will be available after {$a}';
68 changes: 55 additions & 13 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@
function local_examguard_before_standard_top_of_body_html(): string {
global $PAGE, $USER;

try {
// Check if the current page is a course view page.
if (str_contains($PAGE->pagetype, 'course-view') && $PAGE->course->id != SITEID) {
// Check exam guard is enabled.
// Check if the current page is a course view page or a module view page.
if ((str_contains($PAGE->pagetype, 'course-view') || preg_match('/mod-(.+?)-view/', $PAGE->pagetype))
&& $PAGE->course->id != SITEID) {
try {
// Exam guard is enabled.
if (get_config('local_examguard', 'enabled')) {

// Do nothing if the current user is not an editing role, e.g. student.
Expand All @@ -46,23 +47,64 @@ function local_examguard_before_standard_top_of_body_html(): string {
}

// Ban course editing if the exam is in progress / release course editing if the exam is finished.
$editingbanned = manager::check_course_exam_status($PAGE->course->id);
list($editingbanned, $activeexamactivities) = manager::check_course_exam_status($PAGE->course->id);

// Add notification if course editing is banned.
if ($editingbanned) {
notification::add(
'<h4>
<i class="fa-solid fa-user-clock fa-lg text-danger ml-2 mr-3"></i>
' . get_string('warningmessage', 'local_examguard') . '
</h4>',
notification::ERROR
);
manager::show_notfication_banner($activeexamactivities);
}
}
} catch (Exception $e) {
notification::add($e->getMessage(), notification::ERROR);
}
}

return '';
}

/**
* Validate the data in the new field when the form is submitted
*
* @param moodleform_mod $fromform
* @param array $fields
* @return string[]|void
*/
function local_examguard_coursemodule_validation($fromform, $fields) {
try {
// Exam Guard is enabled.
if (get_config('local_examguard', 'enabled')) {
// Prevent change to activity if the course is in exam mode and the user is not a site admin.
if (!has_capability('moodle/site:config', context_system::instance()) &&
manager::should_prevent_course_editing($fields['course'])) {
// Return an error message to prevent saving changes, but the user will not see it.
return ['examguard' => get_string('errorcourseeditingbanned', 'local_examguard')];
}
}
} catch (Exception $e) {
// Show error message when exception is caught.
notification::add($e->getMessage(), notification::ERROR);
}
}

return '';
/**
* Process data from submitted form.
*
* @param stdClass $data
* @param stdClass $course
*/
function local_examguard_coursemodule_edit_post_actions($data, $course) {
try {
// Exam Guard is enabled.
if (get_config('local_examguard', 'enabled')) {
// Execute Exam Guard actions if the module is an exam activity.
if (in_array($data->modulename, manager::EXAM_ACTIVITIES)) {
manager::check_course_exam_status($course->id);
}
}
} catch (Exception $e) {
// Show error message when exception is caught.
notification::add($e->getMessage(), notification::ERROR);
}

return $data;
}
52 changes: 37 additions & 15 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,42 @@

defined('MOODLE_INTERNAL') || die();

$settings = new admin_settingpage('local_examguard_settings', new lang_string('pluginname', 'local_examguard'));
$ADMIN->add('localplugins', $settings);
if ($hassiteconfig) {
$ADMIN->add('localplugins', new admin_category('local_examguard_settings', new lang_string('pluginname', 'local_examguard')));
$settingspage = new admin_settingpage('managelocalexamguard', new lang_string('settings:manage', 'local_examguard'));

if ($ADMIN->fulltree) {
// General settings.
$settings->add(new admin_setting_heading('local_examguard_general_settings',
get_string('settings:generalsettingsheader', 'local_examguard'),
''
));
// Setting to enable/disable the plugin.
$settings->add(new admin_setting_configcheckbox(
'local_examguard/enabled',
get_string('settings:enable', 'local_examguard'),
get_string('settings:enable:desc', 'local_examguard'),
'1'
));
if ($ADMIN->fulltree) {
// General settings.
$settingspage->add(new admin_setting_heading('local_examguard_general_settings',
get_string('settings:generalsettingsheader', 'local_examguard'),
''
));
// Setting to enable/disable the plugin.
$settingspage->add(new admin_setting_configcheckbox(
'local_examguard/enabled',
get_string('settings:enable', 'local_examguard'),
get_string('settings:enable:desc', 'local_examguard'),
'1'
));

// Exam duration setting, default to 300 minutes, i.e. 5 hours.
$settingspage->add(new admin_setting_configtext(
'local_examguard/examduration',
get_string('settings:examduration', 'local_examguard'),
get_string('settings:examduration_desc', 'local_examguard'),
300,
PARAM_INT
));

// Time buffer setting, default to 10 minutes.
$settingspage->add(new admin_setting_configtext(
'local_examguard/timebuffer',
get_string('settings:timebuffer', 'local_examguard'),
get_string('settings:timebuffer_desc', 'local_examguard'),
10,
PARAM_INT
));
}

$ADMIN->add('localplugins', $settingspage);
}
Loading

0 comments on commit 4564a61

Please sign in to comment.