Skip to content

Commit

Permalink
MDL-82130 gradepenalty_duedate: Add support for exemptions subsystem
Browse files Browse the repository at this point in the history
  • Loading branch information
Fragonite committed Aug 25, 2024
1 parent be25430 commit 963f653
Show file tree
Hide file tree
Showing 12 changed files with 976 additions and 51 deletions.
3 changes: 2 additions & 1 deletion exemptions/classes/local/repository/exemption_repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,13 @@ protected function validate(exemption $exemption) {
* @throws \moodle_exception if the exemption has missing or invalid properties.
*/
public function add(exemption $exemption): exemption {
global $DB;
global $DB, $USER;
$this->validate($exemption);
$exemption = (array)$exemption;
$time = time();
$exemption['timecreated'] = $time;
$exemption['timemodified'] = $time;
$exemption['usermodified'] = $USER->id;
$id = $DB->insert_record($this->exemptiontable, $exemption);
return $this->find($id);
}
Expand Down
155 changes: 108 additions & 47 deletions exemptions/classes/local/service/component_exemption_service.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,33 +61,17 @@ public function __construct(string $component, exemption_repository_interface $r
$this->component = $component;
}


/**
* Delete a collection of exemptions by type and item, and optionally for a given context.
*
* E.g. delete all exemptions of type 'message_conversations' for the conversation '11' and in the CONTEXT_COURSE context.
*
* @param string $itemtype the type of the exempt items.
* @param int $itemid the id of the item to which the exemptions relate
* @param \context|null $context the context of the items which were exempt.
*/
public function delete_exemptions_by_type_and_item(string $itemtype, int $itemid, ?\context $context = null) {
$criteria = ['component' => $this->component, 'itemtype' => $itemtype, 'itemid' => $itemid] +
($context ? ['contextid' => $context->id] : []);
$this->repo->delete_by($criteria);
}

/**
* Exempt an item defined by itemid/context, in the area defined by component/itemtype.
*
* @param string $itemtype the type of the item being exempt.
* @param int $itemid the id of the item which is to be exempt.
* @param \context $context the context in which the item is to be exempt.
* @param int $contextid the context where the item is to be exempt.
* @param array $options optional parameters including 'reason', 'reasonformat', 'ordering', and 'usermodified'.
* @return exemption the exemption, once created.
* @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
*/
public function create_exemption(string $itemtype, int $itemid, \context $context, array $options = []): exemption {
public function create_exemption(string $itemtype, int $itemid, int $contextid, array $options = []): exemption {
// Access: Any component can ask to exempt something, we can't verify access to that 'something' here though.

// Validate the component name.
Expand All @@ -101,11 +85,113 @@ public function create_exemption(string $itemtype, int $itemid, \context $contex
$ordering = $options['ordering'] ?? null;
$usermodified = $options['usermodified'] ?? null;

$exemption = new exemption($this->component, $itemtype, $itemid, $context->id, $reason, $reasonformat, $usermodified);
$exemption = new exemption($this->component, $itemtype, $itemid, $contextid, $reason, $reasonformat, $usermodified);
$exemption->ordering = $ordering > 0 ? $ordering : null;
return $this->repo->add($exemption);
}

/**
* Update an exemption item from an area and from within a context.
*
* @param string $itemtype The name of the item type.
* @param int $itemid The item id.
* @param int $contextid The context id.
* @param array $options Optional parameters including 'reason', 'reasonformat', and 'ordering'.
* @return \core_exemptions\local\entity\exemption
*/
public function update_exemption(exemption $exemption): exemption {
// Extract optional parameters with defaults.
// $reason = $options['reason'] ?? null;
// $reasonformat = $options['reasonformat'] ?? null;
// $ordering = $options['ordering'] ?? null;

// $exemption = $this->repo->find_exemption($this->component, $itemtype, $itemid, $contextid);
// $exemption->reason = $reason;
// $exemption->reasonformat = $reasonformat;
// $exemption->ordering = $ordering > 0 ? $ordering : null;
return $this->repo->update($exemption);
}

/**
* Delete an exemption item from an area and from within a context.
*
* @param string $itemtype the type of the exempt item.
* @param int $itemid the id of the item which was exempt (not the exemption's id).
* @param int $contextid the context of the item which was exempt.
* @throws \dml_exception if any database errors are encountered.
*/
public function delete_exemption(string $itemtype, int $itemid, int $contextid) {
$params = [
'component' => $this->component,
'itemtype' => $itemtype,
'itemid' => $itemid,
'contextid' => $contextid,
];

$this->repo->delete_by($params);
}

/**
* Delete an exemption by id.
*
* @param int $id the id of the exemption to delete.
*/
public function delete_exemption_by_id(int $id) {
$this->repo->delete($id);
}

/**
* Delete a collection of exemptions by type and item, and optionally for a given context.
*
* E.g. delete all exemptions of type 'message_conversations' for the conversation '11' and in the CONTEXT_COURSE context.
*
* @param string $itemtype the type of the exempt items.
* @param int $itemid the id of the item to which the exemptions relate
* @param \context|null $context the context of the items which were exempt.
*/
public function delete_exemptions_by_type_and_item(string $itemtype, int $itemid, ?\context $context = null) {
$criteria = ['component' => $this->component, 'itemtype' => $itemtype, 'itemid' => $itemid] +
($context ? ['contextid' => $context->id] : []);
$this->repo->delete_by($criteria);
}

/**
* Find a list of exemptions, by type and item, and optionally for a given context.
*
* @param string $itemtype the type of the exempt item.
* @param int $itemid the id of the item which was exempt (not the exemption's id).
* @param \context|null $context the context of the item which was exempt.
*
* @return array the list of exemptions found.
* @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
*/
public function find_exemptions_by_type_and_item(string $itemtype, int $itemid, ?\context $context = null): array {
$criteria = [
'component' => $this->component,
'itemtype' => $itemtype,
'itemid' => $itemid,
];

if ($context) {
$criteria['contextid'] = $context->id;
}

return $this->repo->find_by($criteria);
}

/**
* Find a list of exemptions, by type and item, and optionally for a given context.
*
* @param array $options the options to filter the exemptions by, including 'itemtype', 'itemid' and 'contextid'.
*
* @return array the list of exemptions found.
* @throws \moodle_exception if the repository encounters any errors.
*/
public function find_exemptions(array $options): array {
$options['component'] = $this->component;
return $this->repo->find_by($options);
}

/**
* Find a list of exemptions, by type, where type is the component/itemtype pair.
*
Expand Down Expand Up @@ -204,31 +290,6 @@ public function get_join_sql_by_type(string $itemtype, string $tablealias, strin
return [$sql, $params];
}

/**
* Delete an exemption item from an area and from within a context.
*
* E.g. delete an exemption course from the area 'core_course', 'course' with itemid 3 and from within the CONTEXT_USER context.
*
* @param string $itemtype the type of the exempt item.
* @param int $itemid the id of the item which was exempt (not the exemption's id).
* @param \context $context the context of the item which was exempt.
* @throws \moodle_exception if the user does not control the exemption, or it doesn't exist.
*/
public function delete_exemption(string $itemtype, int $itemid, \context $context) {
if (!in_array($this->component, \core_component::get_component_names())) {
throw new \moodle_exception("Invalid component name '$this->component'");
}

// Business logic: check the user owns the exemption.
try {
$exemption = $this->repo->find_exemption($this->component, $itemtype, $itemid, $context->id);
} catch (\moodle_exception $e) {
throw new \moodle_exception("exemption does not exist for the user. Cannot delete.");
}

$this->repo->delete($exemption->id);
}

/**
* Check whether an item has been marked as an exemption in the respective area.
*
Expand All @@ -253,16 +314,16 @@ public function exemption_exists(string $itemtype, int $itemid, \context $contex
*
* @param string $itemtype the type of the exempt item.
* @param int $itemid the id of the item which was exempt (not the exemption's id).
* @param \context $context the context of the item which was exempt.
* @param int $contextid the context id of the item which was exempt.
* @return exemption|null
*/
public function get_exemption(string $itemtype, int $itemid, \context $context) {
public function get_exemption(string $itemtype, int $itemid, int $contextid) {
try {
return $this->repo->find_exemption(
$this->component,
$itemtype,
$itemid,
$context->id
$contextid
);
} catch (\dml_missing_record_exception $e) {
return null;
Expand Down
179 changes: 179 additions & 0 deletions grade/penalty/duedate/classes/exemption_helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?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 gradepenalty_duedate;
use context;
use core_exemptions\local\repository\exemption_repository;
use core_exemptions\service_factory;

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

require_once(__DIR__ . '/../lib.php');

/**
* CRUD operations for penalty exemptions for users and groups.
*
* @package gradepenalty_duedate
* @copyright 2024 Catalyst IT Australia Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exemption_helper {

/**
* Check if the user is exempt from the grade penalty.
*
* @param int $userid
* @param int $contextid
*
* @return bool
*/
public static function is_exempt(int $userid, int $contextid): bool {

$context = context::instance_by_id($contextid);
$contextids = array_filter(explode('/', $context->path ?? ''), 'is_numeric');

if (empty($contextids)) {
return false;
}

// Check if the user is exempt.
if (self::is_user_exempt($userid, $contextids)) {
return true;
}

// Check if the user is a member of any groups that are exempt.
$courseid = $context->get_course_context()->instanceid ?? null;
if ($courseid) {
$groupings = groups_get_user_groups($courseid, $userid, true);
foreach ($groupings[0] as $groupid) {
if (self::is_group_exempt($groupid, $contextids)) {
return true;
}
}
}

return false;
}

/**
* Check if the user is exempt from the grade penalty.
*
* @param int $userid The user id to check.
* @param array $contextids The context ids to check.
*
* @return bool
*/
private static function is_user_exempt(int $userid, array $contextids): bool {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$records = $service->find_exemptions_by_type_and_item('user', $userid);

foreach ($records as $record) {
if (in_array($record->contextid, $contextids)) {
return true;
}
}

return false;
}

/**
* Check if the group is exempt from the grade penalty.
*
* @param int $groupid The group id to check.
* @param array $contextids The context ids to check.
*
* @return bool
*/
private static function is_group_exempt(int $groupid, array $contextids): bool {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$records = $service->find_exemptions_by_type_and_item('group', $groupid);

foreach ($records as $record) {
if (in_array($record->contextid, $contextids)) {
return true;
}
}

return false;
}

/**
* Exempt a user from the grade penalty. This method will update the exemption if it already exists.
*
* @param int $userid The user id to exempt.
* @param int $contextid The context id to exempt the user from.
* @param string|null $reason The reason for the exemption.
* @param int|null $reasonformat The format of the reason.
*/
public static function exempt_user(int $userid, int $contextid, ?string $reason = null, ?int $reasonformat = null) {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$exem = $service->get_exemption('user', $userid, $contextid);
if (empty($exem)) {
$exem = $service->create_exemption('user', $userid, $contextid, ['reason' => $reason, 'reasonformat' => $reasonformat]);
} else {
$exem->reason = $reason;
$exem->reasonformat = $reasonformat;
$service->update_exemption($exem);
}
}

/**
* Exempt a group from the grade penalty. This method will update the exemption if it already exists.
*
* @param int $groupid The group id to exempt.
* @param int $contextid The context id to exempt the group from.
* @param string|null $reason The reason for the exemption.
* @param int|null $reasonformat The format of the reason.
*/
public static function exempt_group(int $groupid, int $contextid, ?string $reason = null, ?int $reasonformat = null) {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$exem = $service->get_exemption('group', $groupid, $contextid);
if (empty($exem)) {
$exem = $service->create_exemption('group', $groupid, $contextid, ['reason' => $reason, 'reasonformat' => $reasonformat]);
} else {
$exem->reason = $reason;
$exem->reasonformat = $reasonformat;
$service->update_exemption($exem);
}
}

/**
* Delete a user exemption.
*
* @param int $userid The user id to delete the exemption for.
* @param int $contextid The context id to delete the exemption for.
*/
public static function delete_user_exemption(int $userid, int $contextid) {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$service->delete_exemption('user', $userid, $contextid);
}

/**
* Delete a group exemption.
*
* @param int $groupid The group id to delete the exemption for.
* @param int $contextid The context id to delete the exemption for.
*/
public static function delete_group_exemption(int $groupid, int $contextid) {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$service->delete_exemption('group', $groupid, $contextid);
}

public static function delete_exemption(int $id) {
$service = service_factory::get_service_for_component('gradepenalty_duedate');
$service->delete_exemption_by_id($id);
}
}
Loading

0 comments on commit 963f653

Please sign in to comment.