public static function update_purpose(stdClass $record) {
self::check_can_manage_data_registry();
+ if (!isset($record->sensitivedatareasons)) {
+ $record->sensitivedatareasons = '';
+ }
+
$purpose = new purpose($record->id);
$purpose->from_record($record);
<table class="generaltable fullwidth">
<caption class="accesshide">{{#str}}purposeslist, tool_dataprivacy{{/str}}</caption>
<thead>
- <tr style="display: flex;">
+ <tr>
<th scope="col" class="col-md-2">{{#str}}name{{/str}}</th>
<th scope="col" class="col-md-3">{{#str}}description{{/str}}</th>
<th scope="col" class="col-md-2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th>
FROM {enrol_paypal} ep
JOIN {enrol} e ON ep.instanceid = e.id
JOIN {context} ctx ON e.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
- LEFT JOIN {user} u1 ON LOWER(u1.email) = ep.receiver_email
- LEFT JOIN {user} u2 ON LOWER(u2.email) = ep.business
+ LEFT JOIN {user} u ON u.id = :emailuserid AND (
+ LOWER(u.email) = ep.receiver_email
+ OR
+ LOWER(u.email) = ep.business
+ )
WHERE ep.userid = :userid
- OR u1.id = :receiverid
- OR u2.id = :businessid";
+ OR u.id IS NOT NULL";
$params = [
'contextcourse' => CONTEXT_COURSE,
'userid' => $userid,
- 'receiverid' => $userid,
- 'businessid' => $userid,
+ 'emailuserid' => $userid,
];
$contextlist->add_from_sql($sql, $params);
FROM {enrol_paypal} ep
JOIN {enrol} e ON ep.instanceid = e.id
JOIN {context} ctx ON e.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
- LEFT JOIN {user} u1 ON LOWER(u1.email) = ep.receiver_email
- LEFT JOIN {user} u2 ON LOWER(u2.email) = ep.business
+ LEFT JOIN {user} u ON u.id = :emailuserid AND (
+ LOWER(u.email) = ep.receiver_email
+ OR
+ LOWER(u.email) = ep.business
+ )
WHERE ctx.id {$contextsql}
AND (ep.userid = :userid
- OR u1.id = :receiverid
- OR u2.id = :businessid)
+ OR u.id IS NOT NULL)
ORDER BY e.courseid";
$params = [
'contextcourse' => CONTEXT_COURSE,
'userid' => $user->id,
- 'receiverid' => $user->id,
- 'businessid' => $user->id,
+ 'emailuserid' => $user->id,
];
$params += $contextparams;
/** @var stdClass A user whose email address matches the business field in some of the PayPal transactions. */
protected $businessuser2;
+ /** @var stdClass A user whose email address matches the business field in some of the PayPal transactions. */
+ protected $businessuser3;
+
/** @var stdClass A user whose email address matches the receiver_email field in some of the PayPal transactions. */
protected $receiveruser1;
/** @var stdClass A user whose email address matches the receiver_email field in some of the PayPal transactions. */
protected $receiveruser2;
+ /** @var stdClass A user whose email address matches the receiver_email field in some of the PayPal transactions. */
+ protected $receiveruser3;
+
/** @var stdClass A user who is not enrolled in any course. */
protected $student0;
/** @var stdClass A student who is only enrolled in course2 with 2 transaction histories in the course. */
protected $student2;
+ /** @var stdClass A student who is only enrolled in course3 with 1 transaction histories in the course. */
+ protected $student3;
+
/** @var stdClass A student who is enrolled in both course1 and course2. */
protected $student12;
/** @var stdClass A test course with 2 enrolments for student2 and student12. */
protected $course2;
+ /** @var stdClass A test course with 2 enrolments for student2 and student12. */
+ protected $course3;
+
protected function setUp() {
global $DB;
$generator = $this->getDataGenerator();
// Create seller accounts.
- $this->businessuser1 = $generator->create_user(['email' => 'busines1@domain.invalid']);
- $this->businessuser2 = $generator->create_user(['email' => 'busines2@domain.invalid']);
+ $this->businessuser1 = $generator->create_user(['email' => 'business1@domain.invalid']);
+ $this->businessuser2 = $generator->create_user(['email' => 'business2@domain.invalid']);
+ $this->businessuser3 = $generator->create_user(['email' => 'business3@domain.invalid']);
$this->receiveruser1 = $generator->create_user(['email' => 'receiver1@domain.invalid']);
$this->receiveruser2 = $generator->create_user(['email' => 'receiver2@domain.invalid']);
+ $this->receiveruser3 = $generator->create_user(['email' => 'receiver3@domain.invalid']);
// Create courses.
$this->course1 = $generator->create_course();
$this->course2 = $generator->create_course();
+ $this->course3 = $generator->create_course();
// Create enrolment instances.
$paypalplugin = enrol_get_plugin('paypal');
['roleid' => $studentrole->id, 'courseid' => $this->course2->id]);
$enrolinstance2 = $DB->get_record('enrol', array('id' => $enrolinstanceid));
+ $enrolinstanceid = $paypalplugin->add_instance($this->course3,
+ ['roleid' => $studentrole->id, 'courseid' => $this->course3->id]);
+ $enrolinstance3 = $DB->get_record('enrol', array('id' => $enrolinstanceid));
+
// Create students.
$this->student0 = $generator->create_user(); // This user will not be enrolled in any course.
$this->student1 = $generator->create_user();
$this->student2 = $generator->create_user();
+ $this->student3 = $generator->create_user();
$this->student12 = $generator->create_user();
// Enrol student1 in course1.
$paypalplugin->enrol_user($enrolinstance1, $this->student1->id, $studentrole->id);
- $paypaldata = [
- 'business' => $this->businessuser1->email,
- 'receiver_email' => $this->receiveruser1->email,
- 'receiver_id' => 'SELLERSID',
- 'item_name' => $this->course1->fullname,
- 'courseid' => $this->course1->id,
- 'userid' => $this->student1->id,
- 'instanceid' => $enrolinstance1->id,
- 'payment_status' => 'Completed',
- 'txn_id' => 'STUDENT1-IN-COURSE1-00',
- 'payment_type' => 'instant',
- 'timeupdated' => time(),
- ];
- $DB->insert_record('enrol_paypal', $paypaldata);
+ $this->create_enrol_paypal_record(
+ $this->businessuser1,
+ $this->receiveruser1,
+ $this->course1,
+ $this->student1,
+ $enrolinstance1,
+ 'STUDENT1-IN-COURSE1-00',
+ time()
+ );
// Enrol student2 in course2.
$paypalplugin->enrol_user($enrolinstance2, $this->student2->id, $studentrole->id);
// This user has 2 transaction histories.
// Here is the first one.
- $paypaldata = [
- 'business' => $this->businessuser1->email,
- 'receiver_email' => $this->receiveruser2->email,
- 'receiver_id' => 'SELLERSID',
- 'item_name' => $this->course2->fullname,
- 'courseid' => $this->course2->id,
- 'userid' => $this->student2->id,
- 'instanceid' => $enrolinstance2->id,
- 'payment_status' => 'Completed',
- 'txn_id' => 'STUDENT2-IN-COURSE2-00',
- 'payment_type' => 'instant',
- 'timeupdated' => time() - 86400, // Yesterday.
- ];
- $DB->insert_record('enrol_paypal', $paypaldata);
+ $this->create_enrol_paypal_record(
+ $this->businessuser1,
+ $this->receiveruser2,
+ $this->course2,
+ $this->student2,
+ $enrolinstance2,
+ 'STUDENT2-IN-COURSE2-00',
+ // Yesterday.
+ time() - DAYSECS
+ );
// And now, the second one.
- $paypaldata = [
- 'business' => $this->businessuser1->email,
- 'receiver_email' => $this->receiveruser2->email,
- 'receiver_id' => 'SELLERSID',
- 'item_name' => $this->course2->fullname,
- 'courseid' => $this->course2->id,
- 'userid' => $this->student2->id,
- 'instanceid' => $enrolinstance2->id,
- 'payment_status' => 'Completed',
- 'txn_id' => 'STUDENT2-IN-COURSE2-01',
- 'payment_type' => 'instant',
- 'timeupdated' => time(),
- ];
- $DB->insert_record('enrol_paypal', $paypaldata);
+ $this->create_enrol_paypal_record(
+ $this->businessuser1,
+ $this->receiveruser2,
+ $this->course2,
+ $this->student2,
+ $enrolinstance2,
+ 'STUDENT2-IN-COURSE2-01',
+ time()
+ );
// Enrol student12 in course1 and course2.
// First in course1.
$paypalplugin->enrol_user($enrolinstance1, $this->student12->id, $studentrole->id);
- $paypaldata = [
- 'business' => $this->businessuser2->email,
- 'receiver_email' => $this->receiveruser1->email,
- 'receiver_id' => 'SELLERSID',
- 'item_name' => $this->course1->fullname,
- 'courseid' => $this->course1->id,
- 'userid' => $this->student12->id,
- 'instanceid' => $enrolinstance1->id,
- 'payment_status' => 'Completed',
- 'txn_id' => 'STUDENT12-IN-COURSE1-00',
- 'payment_type' => 'instant',
- 'timeupdated' => time(),
- ];
- $DB->insert_record('enrol_paypal', $paypaldata);
+ $this->create_enrol_paypal_record(
+ $this->businessuser2,
+ $this->receiveruser1,
+ $this->course1,
+ $this->student12,
+ $enrolinstance1,
+ 'STUDENT12-IN-COURSE1-00',
+ time()
+ );
// Then in course2.
$paypalplugin->enrol_user($enrolinstance2, $this->student12->id, $studentrole->id);
- $paypaldata = [
- 'business' => $this->businessuser2->email,
- 'receiver_email' => $this->receiveruser2->email,
- 'receiver_id' => 'SELLERSID',
- 'item_name' => $this->course2->fullname,
- 'courseid' => $this->course2->id,
- 'userid' => $this->student12->id,
- 'instanceid' => $enrolinstance2->id,
- 'payment_status' => 'Completed',
- 'txn_id' => 'STUDENT12-IN-COURSE2-00',
- 'payment_type' => 'instant',
- 'timeupdated' => time(),
- ];
- $DB->insert_record('enrol_paypal', $paypaldata);
+ $this->create_enrol_paypal_record(
+ $this->businessuser2,
+ $this->receiveruser2,
+ $this->course2,
+ $this->student12,
+ $enrolinstance2,
+ 'STUDENT12-IN-COURSE2-00',
+ time()
+ );
+
+ // Enrol student3 in course3 with businessuser3 as the receiver.
+ $paypalplugin->enrol_user($enrolinstance1, $this->student1->id, $studentrole->id);
+ $this->create_enrol_paypal_record(
+ $this->businessuser3,
+ $this->receiveruser3,
+ $this->course3,
+ $this->student3,
+ $enrolinstance3,
+ 'STUDENT3-IN-COURSE3-00',
+ time()
+ );
}
/**
$this->assertContains($coursecontext2->id, $contextids);
}
+ /**
+ * Test for provider::get_contexts_for_userid with a user who is a receiver.
+ */
+ public function test_get_contexts_for_userid_receiver() {
+ $coursecontext1 = context_course::instance($this->course1->id);
+ $coursecontext2 = context_course::instance($this->course2->id);
+
+ // Receiver User 1 is the Receiver of one course.
+ $contextlist = provider::get_contexts_for_userid($this->receiveruser1->id);
+ $this->assertCount(1, $contextlist);
+
+ $contextids = $contextlist->get_contextids();
+ $this->assertEquals([$coursecontext1->id], $contextids);
+
+ // Receiver User 2 is the Receiver of course.
+ $contextlist = provider::get_contexts_for_userid($this->receiveruser2->id);
+ $this->assertCount(1, $contextlist);
+
+ $contextids = $contextlist->get_contextids();
+ $this->assertEquals([$coursecontext2->id], $contextids);
+ }
+
+ /**
+ * Test for provider::get_contexts_for_userid with a user who is a business.
+ */
+ public function test_get_contexts_for_userid_business() {
+ $coursecontext1 = context_course::instance($this->course1->id);
+ $coursecontext2 = context_course::instance($this->course2->id);
+ $coursecontext3 = context_course::instance($this->course3->id);
+
+ // Business User 1 is the Receiver of course 1 and course 2.
+ $contextlist = provider::get_contexts_for_userid($this->businessuser1->id);
+ $this->assertCount(2, $contextlist);
+
+ $contextids = $contextlist->get_contextids();
+ $this->assertEquals([$coursecontext1->id, $coursecontext2->id], $contextids, '', 0.0, 1, true);
+
+ // Business User 3 is the Receiver of course 3 only.
+ $contextlist = provider::get_contexts_for_userid($this->businessuser3->id);
+ $this->assertCount(1, $contextlist);
+
+ $contextids = $contextlist->get_contextids();
+ $this->assertEquals([$coursecontext3->id], $contextids);
+ }
+
/**
* Test for provider::export_user_data().
*/
$DB->count_records('enrol_paypal', ['receiver_email' => $this->receiveruser2->email])
);
}
+
+ /**
+ * Helper function to create an enrol_paypal record.
+ *
+ * @param \stdClass $business The user associated with the business
+ * @param \stdClass $receiver The user associated with the receiver
+ * @param \stdClass $course The course to associate with
+ * @param \stdClass $user The user associated with the student
+ * @param \stdClass $enrol The enrolment instance
+ * @param String $txnid The Paypal txnid to use
+ * @param int $time The txn time
+ */
+ protected function create_enrol_paypal_record($business, $receiver, $course, $user, $enrol, $txnid, $time) {
+ global $DB;
+
+ $paypaldata = [
+ 'business' => $business->email,
+ 'receiver_email' => $receiver->email,
+ 'receiver_id' => 'SELLERSID',
+ 'item_name' => $course->fullname,
+ 'courseid' => $course->id,
+ 'userid' => $user->id,
+ 'instanceid' => $enrol->id,
+ 'payment_status' => 'Completed',
+ 'txn_id' => $txnid,
+ 'payment_type' => 'instant',
+ 'timeupdated' => $time,
+ ];
+ $DB->insert_record('enrol_paypal', $paypaldata);
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
+ *
+ * @package core_grading
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_grading\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\request\approved_contextlist;
+
+/**
+ * The trait used to provide backwards compatability for third-party plugins.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait gradingform_legacy_polyfill {
+
+ /**
+ * This method is used to export any user data this sub-plugin has using the object to get the context and userid.
+ *
+ * @param context $context Context owner of the data.
+ * @param stdClass $definition Grading definition entry to export.
+ * @param int $userid The user whose information is to be exported.
+ *
+ * @return stdClass The data to export.
+ */
+ public static function get_gradingform_export_data(\context $context, $definition, int $userid) {
+ return static::_get_gradingform_export_data($context, $definition, $userid);
+ }
+
+ /**
+ * Any call to this method should delete all user data for the context defined.
+ *
+ * @param context $context Context owner of the data.
+ */
+ public static function delete_gradingform_for_context(\context $context) {
+ static::_delete_gradingform_for_context($context);
+ }
+
+ /**
+ * A call to this method should delete user data (where practicle) from the userid and context.
+ *
+ * @param int $userid The user whose information is to be deleted.
+ * @param context $context Context owner of the data.
+ */
+ public static function delete_gradingform_for_userid(int $userid, \context $context) {
+ static::_delete_gradingform_for_userid($userid, $context);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * This file contains the grading method interface.
+ *
+ * Grading method plugins should implement this if they store personal information.
+ *
+ * @package core_grading
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_grading\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\request\approved_contextlist;
+
+interface gradingform_provider extends
+ \core_privacy\local\request\plugin\subsystem_provider {
+
+ /**
+ * This method is used to export any user data this sub-plugin has using the object to get the context and userid.
+ *
+ * @param \context $context Context owner of the data.
+ * @param \stdClass $definition Grading definition entry to export.
+ * @param int $userid The user whose information is to be exported.
+ *
+ * @return \stdClass The data to export.
+ */
+ public static function get_gradingform_export_data(\context $context, $definition, int $userid);
+
+ /**
+ * Any call to this method should delete all user data for the context defined.
+ *
+ * @param \context $context Context owner of the data.
+ */
+ public static function delete_gradingform_for_context(\context $context);
+
+ /**
+ * A call to this method should delete user data (where practicle) from the userid and context.
+ *
+ * @param int $userid The user to delete.
+ * @param \context $context the context to refine the deletion.
+ */
+ public static function delete_gradingform_for_userid(int $userid, \context $context);
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package core_grading
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_grading\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\writer;
+use \core_privacy\manager;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+ \core_privacy\local\metadata\provider,
+ \core_privacy\local\request\subsystem\provider {
+
+ /**
+ * Returns meta data about this system.
+ *
+ * @param collection $collection The initialised collection to add items to.
+ * @return collection A listing of user data stored through this system.
+ */
+ public static function get_metadata(collection $collection) : collection {
+ $collection->add_database_table('grading_definitions', [
+ 'method' => 'privacy:metadata:grading_definitions:method',
+ 'areaid' => 'privacy:metadata:grading_definitions:areaid',
+ 'name' => 'privacy:metadata:grading_definitions:name',
+ 'description' => 'privacy:metadata:grading_definitions:description',
+ 'status' => 'privacy:metadata:grading_definitions:status',
+ 'copiedfromid' => 'privacy:metadata:grading_definitions:copiedfromid',
+ 'timecopied' => 'privacy:metadata:grading_definitions:timecopied',
+ 'timecreated' => 'privacy:metadata:grading_definitions:timecreated',
+ 'usercreated' => 'privacy:metadata:grading_definitions:usercreated',
+ 'timemodified' => 'privacy:metadata:grading_definitions:timemodified',
+ 'usermodified' => 'privacy:metadata:grading_definitions:usermodified',
+ 'options' => 'privacy:metadata:grading_definitions:options',
+ ], 'privacy:metadata:grading_definitions');
+
+ $collection->add_database_table('grading_instances', [
+ 'raterid' => 'privacy:metadata:grading_instances:raterid',
+ 'rawgrade' => 'privacy:metadata:grading_instances:rawgrade',
+ 'status' => 'privacy:metadata:grading_instances:status',
+ 'feedback' => 'privacy:metadata:grading_instances:feedback',
+ 'feedbackformat' => 'privacy:metadata:grading_instances:feedbackformat',
+ 'timemodified' => 'privacy:metadata:grading_instances:timemodified',
+ ], 'privacy:metadata:grading_instances');
+
+ // Link to subplugin.
+ $collection->add_plugintype_link('gradingform', [], 'privacy:metadata:gradingformpluginsummary');
+
+ return $collection;
+ }
+
+ /**
+ * Get the list of contexts that contain user information for the specified user.
+ *
+ * @param int $userid The user to search.
+ * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+ */
+ public static function get_contexts_for_userid(int $userid) : contextlist {
+ $contextlist = new contextlist();
+
+ $sql = "SELECT c.id
+ FROM {context} c
+ JOIN {grading_areas} a ON a.contextid = c.id
+ JOIN {grading_definitions} d ON d.areaid = a.id
+ LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid
+ WHERE c.contextlevel = :contextlevel
+ AND (d.usercreated = :usercreated OR d.usermodified = :usermodified OR i.id IS NOT NULL)";
+ $params = [
+ 'usercreated' => $userid,
+ 'usermodified' => $userid,
+ 'raterid' => $userid,
+ 'contextlevel' => CONTEXT_MODULE
+ ];
+ $contextlist->add_from_sql($sql, $params);
+
+ return $contextlist;
+ }
+
+ /**
+ * Export all user data for the specified user, in the specified contexts.
+ *
+ * @param approved_contextlist $contextlist The approved contexts to export information for.
+ */
+ public static function export_user_data(approved_contextlist $contextlist) {
+ // Remove contexts different from MODULE.
+ $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+ if ($context->contextlevel == CONTEXT_MODULE) {
+ $carry[] = $context;
+ }
+ return $carry;
+ }, []);
+
+ if (empty($contexts)) {
+ return;
+ }
+
+ $userid = $contextlist->get_user()->id;
+ $subcontext = [get_string('gradingmethod', 'grading')];
+ foreach ($contexts as $context) {
+ // Export grading definitions created or modified on this context.
+ self::export_definitions($context, $subcontext, $userid);
+ }
+ }
+
+ /**
+ * Exports the data related to grading definitions within the specified context/subcontext.
+ *
+ * @param \context $context Context owner of the data.
+ * @param array $subcontext Subcontext owner of the data.
+ * @param int $userid The user whose information is to be exported.
+ */
+ protected static function export_definitions(\context $context, array $subcontext, int $userid = 0) {
+ global $DB;
+
+ $join = "JOIN {grading_areas} a ON a.id = d.areaid
+ JOIN {context} c ON a.contextid = c.id AND c.contextlevel = :contextlevel";
+ $select = 'a.contextid = :contextid';
+ $params = [
+ 'contextlevel' => CONTEXT_MODULE,
+ 'contextid' => $context->id
+ ];
+
+ if (!empty($userid)) {
+ $join .= ' LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid';
+ $select .= ' AND (usercreated = :usercreated
+ OR usermodified = :usermodified OR i.id IS NOT NULL)';
+ $params['usercreated'] = $userid;
+ $params['usermodified'] = $userid;
+ $params['raterid'] = $userid;
+ }
+
+ $sql = "SELECT DISTINCT d.id,
+ d.method,
+ d.name,
+ d.description,
+ d.timecopied,
+ d.timecreated,
+ d.usercreated,
+ d.timemodified,
+ d.usermodified
+ FROM {grading_definitions} d
+ $join
+ WHERE $select";
+ $definitions = $DB->get_recordset_sql($sql, $params);
+ $defdata = [];
+ foreach ($definitions as $definition) {
+ $tmpdata = [
+ 'method' => $definition->method,
+ 'name' => $definition->name,
+ 'description' => $definition->description,
+ 'timecreated' => transform::datetime($definition->timecreated),
+ 'usercreated' => transform::user($definition->usercreated),
+ 'timemodified' => transform::datetime($definition->timemodified),
+ 'usermodified' => transform::user($definition->usermodified),
+ ];
+ if (!empty($definition->timecopied)) {
+ $tmpdata['timecopied'] = transform::datetime($definition->timecopied);
+ }
+ // Export gradingform information (if needed).
+ $instancedata = manager::component_class_callback(
+ "gradingform_{$definition->method}",
+ gradingform_provider::class,
+ 'get_gradingform_export_data',
+ [$context, $definition, $userid]
+ );
+ if (null !== $instancedata) {
+ $tmpdata = array_merge($tmpdata, $instancedata);
+ }
+
+ $defdata[] = (object) $tmpdata;
+
+ // Export grading_instances information.
+ self::export_grading_instances($context, $subcontext, $definition->id, $userid);
+ }
+ $definitions->close();
+
+ if (!empty($defdata)) {
+ $data = (object) [
+ 'definitions' => $defdata,
+ ];
+
+ writer::with_context($context)->export_data($subcontext, $data);
+ }
+ }
+
+ /**
+ * Exports the data related to grading instances within the specified definition.
+ *
+ * @param \context $context Context owner of the data.
+ * @param array $subcontext Subcontext owner of the data.
+ * @param int $definitionid The definition ID whose grading instance information is to be exported.
+ * @param int $userid The user whose information is to be exported.
+ */
+ protected static function export_grading_instances(\context $context, array $subcontext, int $definitionid, int $userid = 0) {
+ global $DB;
+
+ $params = ['definitionid' => $definitionid];
+ if (!empty($userid)) {
+ $params['raterid'] = $userid;
+ }
+ $instances = $DB->get_recordset('grading_instances', $params);
+ $instancedata = [];
+ foreach ($instances as $instance) {
+ // TODO: Get the status name (instead of the ID).
+ $tmpdata = [
+ 'rawgrade' => $instance->rawgrade,
+ 'status' => $instance->status,
+ 'feedback' => $instance->feedback,
+ 'feedbackformat' => $instance->feedbackformat,
+ 'timemodified' => transform::datetime($instance->timemodified),
+ ];
+ $instancedata[] = (object) $tmpdata;
+ }
+ $instances->close();
+
+ if (!empty($instancedata)) {
+ $data = (object) [
+ 'instances' => $instancedata,
+ ];
+
+ writer::with_context($context)->export_related_data($subcontext, 'gradinginstances', $data);
+ }
+ }
+
+ /**
+ * Delete all use data which matches the specified $context.
+ *
+ * We never delete grading content.
+ *
+ * @param context $context A user context.
+ */
+ public static function delete_data_for_all_users_in_context(\context $context) {
+ manager::plugintype_class_callback(
+ 'gradingform',
+ gradingform_provider::class,
+ 'delete_gradingform_for_context',
+ [$context]
+ );
+ }
+
+ /**
+ * Delete all user data for the specified user, in the specified contexts.
+ *
+ * We never delete grading content.
+ *
+ * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+ */
+ public static function delete_data_for_user(approved_contextlist $contextlist) {
+ manager::plugintype_class_callback(
+ 'gradingform',
+ gradingform_provider::class,
+ 'delete_gradingform_for_userid',
+ [$contextlist]
+ );
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package gradingform_guide
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradingform_guide\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\writer;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+ \core_privacy\local\request\user_preference_provider {
+
+ /**
+ * Return the fields which contain personal data.
+ *
+ * @param collection $collection The initialised collection to add items to.
+ * @return collection A listing of user data stored through this system.
+ */
+ public static function get_metadata(collection $collection) : collection {
+ $collection->add_user_preference(
+ 'gradingform_guide-showmarkerdesc',
+ 'privacy:metadata:preference:showmarkerdesc'
+ );
+ $collection->add_user_preference(
+ 'gradingform_guide-showstudentdesc',
+ 'privacy:metadata:preference:showstudentdesc'
+ );
+
+ return $collection;
+ }
+
+ /**
+ * Store all user preferences for the plugin.
+ *
+ * @param int $userid The userid of the user whose data is to be exported.
+ */
+ public static function export_user_preferences(int $userid) {
+ $prefvalue = get_user_preferences('gradingform_guide-showmarkerdesc', null, $userid);
+ if ($prefvalue !== null) {
+ $transformedvalue = transform::yesno($prefvalue);
+ writer::export_user_preference(
+ 'gradingform_guide',
+ 'gradingform_guide-showmarkerdesc',
+ $transformedvalue,
+ get_string('privacy:metadata:preference:showmarkerdesc', 'gradingform_guide')
+ );
+ }
+
+ $prefvalue = get_user_preferences('gradingform_guide-showstudentdesc', null, $userid);
+ if ($prefvalue !== null) {
+ $transformedvalue = transform::yesno($prefvalue);
+ writer::export_user_preference(
+ 'gradingform_guide',
+ 'gradingform_guide-showstudentdesc',
+ $transformedvalue,
+ get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide')
+ );
+ }
+ }
+}
$string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
$string['pluginname'] = 'Marking guide';
$string['previewmarkingguide'] = 'Preview marking guide';
+$string['privacy:metadata:preference:showmarkerdesc'] = 'Whether to show marker criterion descriptions';
+$string['privacy:metadata:preference:showstudentdesc'] = 'Whether to show student criterion descriptions';
$string['regrademessage1'] = 'You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
--- /dev/null
+<?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/>.
+
+/**
+ * Privacy tests for gradingform_guide.
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+use \core_privacy\local\request\writer;
+use \gradingform_guide\privacy\provider;
+
+/**
+ * Privacy tests for gradingform_guide.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradingform_guide_privacy_testcase extends provider_testcase {
+
+ /**
+ * Ensure that export_user_preferences returns no data if the user has no data.
+ */
+ public function test_export_user_preferences_not_defined() {
+ $user = \core_user::get_user_by_username('admin');
+ provider::export_user_preferences($user->id);
+
+ $writer = writer::with_context(\context_system::instance());
+ $this->assertFalse($writer->has_any_data());
+ }
+
+ /**
+ * Ensure that export_user_preferences returns single preferences.
+ */
+ public function test_export_user_preferences() {
+ $this->resetAfterTest();
+
+ // Define a user preference.
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+ set_user_preference('gradingform_guide-showmarkerdesc', 0, $user);
+ set_user_preference('gradingform_guide-showstudentdesc', 1, $user);
+
+ // Validate exported data.
+ provider::export_user_preferences($user->id);
+ $context = context_user::instance($user->id);
+ $writer = writer::with_context($context);
+ $this->assertTrue($writer->has_any_data());
+ $prefs = $writer->get_user_preferences('gradingform_guide');
+ $this->assertCount(2, (array) $prefs);
+ $this->assertEquals(
+ get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide'),
+ $prefs->{'gradingform_guide-showstudentdesc'}->description
+ );
+ $this->assertEquals(get_string('no'), $prefs->{'gradingform_guide-showmarkerdesc'}->value);
+ $this->assertEquals(get_string('yes'), $prefs->{'gradingform_guide-showstudentdesc'}->value);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package gradingform_rubric
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradingform_rubric\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+ \core_privacy\local\metadata\null_provider {
+
+ /**
+ * Get the language string identifier with the component's language
+ * file to explain why this plugin stores no data.
+ *
+ * @return string
+ */
+ public static function get_reason() : string {
+ return 'privacy:metadata';
+ }
+}
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
$string['previewrubric'] = 'Preview rubric';
+$string['privacy:metadata'] = 'The rubric grading form plugin does not store any personal data.';
$string['regrademessage1'] = 'You are about to save changes to a rubric that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the rubric will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for the privacy legacy polyfill for gradingform.
+ *
+ * @package core_grading
+ * @category test
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for the Grading API's privacy legacy_polyfill.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeform_privacy_legacy_polyfill_test extends advanced_testcase {
+ /**
+ * Test that the core_grading\privacy\legacy_polyfill works and that the static _get_grading_export_data can be called.
+ */
+ public function test_get_gradingform_export_data() {
+ $userid = 476;
+ $context = context_system::instance();
+
+ $mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class);
+ $mock->expects($this->once())
+ ->method('get_return_value')
+ ->with('_get_gradingform_export_data', [$context, (object)[], $userid]);
+
+ test_legacy_polyfill_gradingform_provider::$mock = $mock;
+ test_legacy_polyfill_gradingform_provider::get_gradingform_export_data($context, (object)[], $userid);
+ }
+
+ /**
+ * Test for _get_metadata shim.
+ */
+ public function test_get_metadata() {
+ $collection = new \core_privacy\local\metadata\collection('core_gradingform');
+ $this->assertSame($collection, test_legacy_polyfill_gradingform_provider::get_metadata($collection));
+ }
+
+ /**
+ * Test the _delete_gradingform_for_context shim.
+ */
+ public function test_delete_gradingform_for_context() {
+ $context = context_system::instance();
+
+ $mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class);
+ $mock->expects($this->once())
+ ->method('get_return_value')
+ ->with('_delete_gradingform_for_context', [$context]);
+
+ test_legacy_polyfill_gradingform_provider::$mock = $mock;
+ test_legacy_polyfill_gradingform_provider::delete_gradingform_for_context($context);
+ }
+
+ /**
+ * Test the _delete_gradingform_for_context shim.
+ */
+ public function test_delete_gradingform_for_user() {
+ $userid = 696;
+ $context = \context_system::instance();
+
+ $mock = $this->createMock(test_gradingform_legacy_polyfill_mock_wrapper::class);
+ $mock->expects($this->once())
+ ->method('get_return_value')
+ ->with('_delete_gradingform_for_userid', [$userid, $context]);
+
+ test_legacy_polyfill_gradingform_provider::$mock = $mock;
+ test_legacy_polyfill_gradingform_provider::delete_gradingform_for_userid($userid, $context);
+ }
+}
+
+/**
+ * Legacy polyfill test class for the gradingform_provider.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_legacy_polyfill_gradingform_provider implements
+ \core_privacy\local\metadata\provider,
+ \core_grading\privacy\gradingform_provider {
+
+ use \core_grading\privacy\gradingform_legacy_polyfill;
+ use \core_privacy\local\legacy_polyfill;
+
+ /**
+ * @var test_legacy_polyfill_gradingform_provider $mock.
+ */
+ public static $mock = null;
+
+ /**
+ * Export all user data for the gradingform plugin.
+ *
+ * @param context $context
+ * @param stdClass $definition
+ * @param int $userid
+ */
+ protected static function _get_gradingform_export_data(\context $context, $definition, $userid) {
+ static::$mock->get_return_value(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * Deletes all user data for the given context.
+ *
+ * @param context $context
+ */
+ protected static function _delete_gradingform_for_context(\context $context) {
+ static::$mock->get_return_value(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * Delete personal data for the given user and context.
+ *
+ * @param int $userid
+ * @param context $context
+ */
+ protected static function _delete_gradingform_for_userid($userid, \context $context) {
+ static::$mock->get_return_value(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * Returns metadata about this plugin.
+ *
+ * @param \core_privacy\local\metadata\collection $collection The initialised collection to add items to.
+ * @return \core_privacy\local\metadata\collection A listing of user data stored through this system.
+ */
+ protected static function _get_metadata(\core_privacy\local\metadata\collection $collection) {
+ return $collection;
+ }
+}
+
+/**
+ * Called inside the polyfill methods in the test polyfill provider, allowing us to ensure these are called with correct params.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_gradingform_legacy_polyfill_mock_wrapper {
+ /**
+ * Get the return value for the specified item.
+ */
+ public function get_return_value() {
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Privacy tests for core_grading.
+ *
+ * @package core_grading
+ * @category test
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\writer;
+use \core_grading\privacy\provider;
+
+/**
+ * Privacy tests for core_grading.
+ *
+ * @copyright 2018 Sara Arjona <sara@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_grading_privacy_testcase extends provider_testcase {
+
+ /** @var stdClass User without data. */
+ protected $user0;
+
+ /** @var stdClass User with data. */
+ protected $user1;
+
+ /** @var stdClass User with data. */
+ protected $user2;
+
+ /** @var context context_module of an activity without grading definitions. */
+ protected $instancecontext0;
+
+ /** @var context context_module of the activity where the grading definitions are. */
+ protected $instancecontext1;
+
+ /** @var context context_module of the activity where the grading definitions are. */
+ protected $instancecontext2;
+
+ /**
+ * Test getting the context for the user ID related to this plugin.
+ */
+ public function test_get_contexts_for_userid() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->grading_setup_test_scenario_data();
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+
+ // User1 has created grading definitions for instance1 and instance2.
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $this->assertCount(2, $contextlist);
+ $this->assertContains($this->instancecontext1->id, $contextlist->get_contextids());
+ $this->assertContains($this->instancecontext2->id, $contextlist->get_contextids());
+ $this->assertNotContains($this->instancecontext0->id, $contextlist->get_contextids());
+
+ // User2 has only modified grading definitions for instance2.
+ $contextlist = provider::get_contexts_for_userid($this->user2->id);
+ $this->assertCount(1, $contextlist);
+ $this->assertContains($this->instancecontext2->id, $contextlist->get_contextids());
+
+ // User0 hasn't created or modified any grading definition.
+ $contextlist = provider::get_contexts_for_userid($this->user0->id);
+ $this->assertCount(0, $contextlist);
+ }
+
+ /**
+ * Export for a user with no grading definitions created or modified will not have any data exported.
+ */
+ public function test_export_user_data_no_content() {
+ $this->resetAfterTest();
+
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+ $context = \context_system::instance();
+
+ $writer = writer::with_context($context);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($user->id, $context, 'core_grading');
+ $this->assertFalse(writer::with_context($context)->has_any_data());
+ }
+
+ /**
+ * Test that data is exported correctly for this plugin.
+ */
+ public function test_export_user_data() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $now = time();
+ $defnameprefix = 'fakename';
+ $this->grading_setup_test_scenario_data($defnameprefix, $now);
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+
+ // Validate exported data: instance1 - user0 has NO data.
+ $this->setUser($this->user0);
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext1);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user0->id, $this->instancecontext1, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertEmpty($data);
+
+ // Validate exported data: instance0 - user1 has NO data.
+ $this->setUser($this->user1);
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext0);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user1->id, $this->instancecontext0, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertEmpty($data);
+
+ // Validate exported data: instance1 - user1 has data (user has created and modified it).
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext1);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user1->id, $this->instancecontext1, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertCount(1, $data->definitions);
+
+ $firstkey = reset($data->definitions);
+ $this->assertNotEmpty($firstkey->name);
+ $this->assertEquals('test_method', $firstkey->method);
+ $this->assertEquals(transform::datetime($now), $firstkey->timecreated);
+ $this->assertEquals($this->user1->id, $firstkey->usercreated);
+ $this->assertEquals($defnameprefix.'1', $firstkey->name);
+
+ // Validate exported data: instance2 - user1 has data (user has created it).
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext2);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user1->id, $this->instancecontext2, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertCount(1, $data->definitions);
+
+ $firstkey = reset($data->definitions);
+ $this->assertNotEmpty($firstkey->name);
+ $this->assertEquals('test_method', $firstkey->method);
+ $this->assertEquals(transform::datetime($now), $firstkey->timecreated);
+ $this->assertEquals($this->user1->id, $firstkey->usercreated);
+ $this->assertEquals($defnameprefix.'2', $firstkey->name);
+
+ // Validate exported data: instance1 - user2 has NO data.
+ $this->setUser($this->user2);
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext1);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user2->id, $this->instancecontext1, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertEmpty($data);
+
+ // Validate exported data: instance2 - user2 has data (user has modified it).
+ $this->setUser($this->user2);
+ writer::reset();
+ $writer = writer::with_context($this->instancecontext2);
+ $this->assertFalse($writer->has_any_data());
+ $this->export_context_data_for_user($this->user2->id, $this->instancecontext2, 'core_grading');
+ $data = $writer->get_data([get_string('gradingmethod', 'grading')]);
+ $this->assertCount(1, $data->definitions);
+ }
+
+ /**
+ * Test for provider::delete_data_for_all_users_in_context().
+ */
+ public function test_delete_data_for_all_users_in_context() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->grading_setup_test_scenario_data();
+
+ // Before deletion, we should have 2 grading_definitions.
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+
+ // Delete data.
+ provider::delete_data_for_all_users_in_context($this->instancecontext0);
+ provider::delete_data_for_all_users_in_context($this->instancecontext1);
+ provider::delete_data_for_all_users_in_context($this->instancecontext2);
+
+ // Before deletion, we should have same grading_definitions (nothing was deleted).
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+ }
+
+ /**
+ * Test for provider::delete_data_for_user().
+ */
+ public function test_delete_data_for_user() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->grading_setup_test_scenario_data();
+
+ // Before deletion, we should have 2 grading_definitions.
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+
+ // Delete data for $user0.
+ $contextlist = provider::get_contexts_for_userid($this->user0->id);
+ $approvedcontextlist = new approved_contextlist(
+ $this->user0,
+ 'core_grading',
+ $contextlist->get_contextids()
+ );
+ provider::delete_data_for_user($approvedcontextlist);
+
+ // Delete data for $user1.
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $approvedcontextlist = new approved_contextlist(
+ $this->user1,
+ 'core_grading',
+ $contextlist->get_contextids()
+ );
+ provider::delete_data_for_user($approvedcontextlist);
+
+ // Delete data for $user2.
+ $contextlist = provider::get_contexts_for_userid($this->user2->id);
+ $approvedcontextlist = new approved_contextlist(
+ $this->user2,
+ 'core_grading',
+ $contextlist->get_contextids()
+ );
+ provider::delete_data_for_user($approvedcontextlist);
+
+ // Before deletion, we should have same grading_definitions (nothing was deleted).
+ $this->assertCount(2, $DB->get_records('grading_definitions'));
+ }
+
+ /**
+ * Helper function to setup the environment.
+ *
+ * course
+ * |
+ * +--instance0 (assignment)
+ * | |
+ * +--instance1 (assignment)
+ * | |
+ * | +--grading_definition1 (created and modified by user1)
+ * | |
+ * +--instance2 (assignment)
+ * | |
+ * | +--grading_definition2 (created by user1 and modified by user2)
+ *
+ *
+ * user0 hasn't any data.
+ *
+ * @param string $defnameprefix
+ * @param timestamp $now
+ */
+ protected function grading_setup_test_scenario_data($defnameprefix = null, $now = null) {
+ global $DB;
+
+ $this->user0 = $this->getDataGenerator()->create_user();
+ $this->user1 = $this->getDataGenerator()->create_user();
+ $this->user2 = $this->getDataGenerator()->create_user();
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course();
+ $coursecontext = context_course::instance($course->id);
+
+ // Create some assignment instances.
+ $params = (object)array(
+ 'course' => $course->id,
+ 'name' => 'Testing instance'
+ );
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $instance0 = $generator->create_instance($params);
+ $cm0 = get_coursemodule_from_instance('assign', $instance0->id);
+ $this->instancecontext0 = context_module::instance($cm0->id);
+ $instance1 = $generator->create_instance($params);
+ $cm1 = get_coursemodule_from_instance('assign', $instance1->id);
+ $this->instancecontext1 = context_module::instance($cm1->id);
+ $instance2 = $generator->create_instance($params);
+ $cm2 = get_coursemodule_from_instance('assign', $instance2->id);
+ $this->instancecontext2 = context_module::instance($cm2->id);
+
+ // Create fake grading areas.
+ $fakearea1 = (object)array(
+ 'contextid' => $this->instancecontext1->id,
+ 'component' => 'mod_assign',
+ 'areaname' => 'submissions',
+ 'activemethod' => 'test_method'
+ );
+ $fakeareaid1 = $DB->insert_record('grading_areas', $fakearea1);
+ $fakearea2 = clone($fakearea1);
+ $fakearea2->contextid = $this->instancecontext2->id;
+ $fakeareaid2 = $DB->insert_record('grading_areas', $fakearea2);
+
+ // Create fake grading definitions.
+ if (empty($now)) {
+ $now = time();
+ }
+ if (empty($defnameprefix)) {
+ $defnameprefix = 'fakename';
+ }
+ $fakedefinition1 = (object)array(
+ 'areaid' => $fakeareaid1,
+ 'method' => 'test_method',
+ 'name' => $defnameprefix.'1',
+ 'status' => 0,
+ 'timecreated' => $now,
+ 'usercreated' => $this->user1->id,
+ 'timemodified' => $now + 1,
+ 'usermodified' => $this->user1->id,
+ );
+ $fakedefid1 = $DB->insert_record('grading_definitions', $fakedefinition1);
+ $fakedefinition2 = clone($fakedefinition1);
+ $fakedefinition2->areaid = $fakeareaid2;
+ $fakedefinition2->name = $defnameprefix.'2';
+ $fakedefinition2->usermodified = $this->user2->id;
+ $fakedefid2 = $DB->insert_record('grading_definitions', $fakedefinition2);
+ }
+}
$string['manageactionsharedone'] = 'The form was successfully saved as a template';
$string['noitemid'] = 'Grading not possible. The graded item does not exist.';
$string['nosharedformfound'] = 'No template found';
+$string['privacy:metadata:gradingformpluginsummary'] = 'Data for the grading method.';
+$string['privacy:metadata:grading_definitions'] = 'Basic information about an advanced grading form defined in a gradable area.';
+$string['privacy:metadata:grading_definitions:areaid'] = 'The area ID where the advanced grading form is defined.';
+$string['privacy:metadata:grading_definitions:copiedfromid'] = 'The grading definition ID from where this was copied.';
+$string['privacy:metadata:grading_definitions:description'] = 'The description of the advanced grading method.';
+$string['privacy:metadata:grading_definitions:method'] = 'The grading method which is responsible for the definition.';
+$string['privacy:metadata:grading_definitions:name'] = 'The name of the advanced grading definition.';
+$string['privacy:metadata:grading_definitions:options'] = 'Some settings of this grading definition.';
+$string['privacy:metadata:grading_definitions:status'] = 'The status of this advanced grading definition.';
+$string['privacy:metadata:grading_definitions:timecopied'] = 'The time when the grading definition was copied.';
+$string['privacy:metadata:grading_definitions:timecreated'] = 'The time when the grading definition was created.';
+$string['privacy:metadata:grading_definitions:timemodified'] = 'The time when the grading definition was last modified.';
+$string['privacy:metadata:grading_definitions:usercreated'] = 'The ID of the user who created the grading definition.';
+$string['privacy:metadata:grading_definitions:usermodified'] = 'The ID of the user who last modified the grading definition.';
+$string['privacy:metadata:grading_instances'] = 'Assessment record for one gradable item assessed by one rater.';
+$string['privacy:metadata:grading_instances:feedback'] = 'The feedback given by the user.';
+$string['privacy:metadata:grading_instances:feedbackformat'] = 'The text format of the feedback given by the user.';
+$string['privacy:metadata:grading_instances:raterid'] = 'The ID of the user who rated the grading instance.';
+$string['privacy:metadata:grading_instances:rawgrade'] = 'The grade for the grading instance.';
+$string['privacy:metadata:grading_instances:status'] = 'The status of this grading instance.';
+$string['privacy:metadata:grading_instances:timemodified'] = 'The time when the grading instance was last modified.';
$string['searchtemplate'] = 'Grading forms search';
$string['searchtemplate_help'] = 'You can search for a grading form and use it as a template for the new grading form here. Simply type words that should appear somewhere in the form name, its description or the form body itself. To search for a phrase, wrap the whole query in double quotes.
// Add the javascript required to enhance this mform.
$PAGE->requires->js_call_amd('mod_quiz/add_random_form', 'init', [
$mform->getAttribute('id'),
- $contexts->lowest()->id
+ $contexts->lowest()->id,
+ $tops
]);
}
TAG_IDS_FORM_ELEMENT: '[name="fromtags[]"]'
};
+ /**
+ * Get the selected category value from the form.
+ *
+ * @param {jquery} form The form element.
+ * @return {string} The category value.
+ */
+ var getCategorySelectValue = function(form) {
+ return form.find(SELECTORS.CATEGORY_FORM_ELEMENT).val();
+ };
+
/**
* Get the category id from the form.
*
var getCategoryId = function(form) {
// The value string is the category id and category context id joined
// by a comma.
- var valueString = form.find(SELECTORS.CATEGORY_FORM_ELEMENT).val();
+ var valueString = getCategorySelectValue(form);
// Split the two ids.
var values = valueString.split(',');
// Return just the category id.
return values[0];
};
+ /**
+ * Check if a top level category is selected in the form.
+ *
+ * @param {jquery} form The form element.
+ * @param {string[]} topCategories List of top category values (matching the select box values)
+ * @return {bool}
+ */
+ var isTopLevelCategorySelected = function(form, topCategories) {
+ var selectedValue = getCategorySelectValue(form);
+ return (topCategories.indexOf(selectedValue) > -1);
+ };
+
/**
* Check if the form indicates we should include include subcategories in
* the filter.
*
* @param {jquery} form The form element.
+ * @param {string[]} topCategories List of top category values (matching the select box values)
* @return {bool}
*/
- var shouldIncludeSubcategories = function(form) {
- return form.find(SELECTORS.SUBCATEGORY_FORM_ELEMENT).is(':checked');
+ var shouldIncludeSubcategories = function(form, topCategories) {
+ if (isTopLevelCategorySelected(form, topCategories)) {
+ return true;
+ } else {
+ return form.find(SELECTORS.SUBCATEGORY_FORM_ELEMENT).is(':checked');
+ }
};
/**
*
* @param {jquery} form The form element.
* @param {int} contextId The current context id.
+ * @param {string[]} topCategories List of top category values (matching the select box values)
*/
- var reloadQuestionPreview = function(form, contextId) {
+ var reloadQuestionPreview = function(form, contextId, topCategories) {
var previewContainer = form.find(SELECTORS.PREVIEW_CONTAINER);
RandomQuestionFormPreview.reload(
previewContainer,
getCategoryId(form),
- shouldIncludeSubcategories(form),
+ shouldIncludeSubcategories(form, topCategories),
getTagIds(form),
contextId
);
*
* @param {jquery} form The form element.
* @param {int} contextId The current context id.
+ * @param {string[]} topCategories List of top category values (matching the select box values)
*/
- var addEventListeners = function(form, contextId) {
+ var addEventListeners = function(form, contextId, topCategories) {
var reloadTimerId = null;
form.on('change', function(e) {
// in case the user is still modifying the form. We don't want to
// spam reload requests.
reloadTimerId = setTimeout(function() {
- reloadQuestionPreview(form, contextId);
+ reloadQuestionPreview(form, contextId, topCategories);
}, RELOAD_DELAY);
});
};
*
* @param {jquery} formId The form element id.
* @param {int} contextId The current context id.
+ * @param {string[]} topCategories List of top category values (matching the select box values)
*/
- var init = function(formId, contextId) {
+ var init = function(formId, contextId, topCategories) {
var form = $('#' + formId);
- reloadQuestionPreview(form, contextId);
- addEventListeners(form, contextId);
+ reloadQuestionPreview(form, contextId, topCategories);
+ addEventListeners(form, contextId, topCategories);
};
return {
public function export_data(array $subcontext, \stdClass $data) : content_writer {
$path = $this->get_path($subcontext, 'data.json');
- $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE));
+ $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
return $this;
}
];
$path = $this->get_path($subcontext, 'metadata.json');
- $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE));
+ $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
return $this;
}
public function export_related_data(array $subcontext, $name, $data) : content_writer {
$path = $this->get_path($subcontext, "{$name}.json");
- $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE));
+ $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
return $this;
}
'value' => $value,
'description' => $description,
];
- $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE));
+ $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
return $this;
}
$contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
$json = $fileroot->getChild($contextpath)->getContent();
- $this->assertRegExp("/$text.*$text.*$text/", $json);
+ $this->assertRegExp("/$text.*$text.*$text/is", $json);
$expanded = json_decode($json);
$this->assertTrue(isset($expanded->$text));
$contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
$json = $fileroot->getChild($contextpath)->getContent();
- $this->assertRegExp("/$text.*$text.*$text/", $json);
+ $this->assertRegExp("/$text.*$text.*$text/is", $json);
$expanded = json_decode($json);
$this->assertTrue(isset($expanded->$text));
$this->assertEquals($expectedpath, $contextpath);
$json = $fileroot->getChild($contextpath)->getContent();
- $this->assertRegExp("/$text.*$text.*$text/", $json);
+ $this->assertRegExp("/$text.*$text.*$text/is", $json);
$expanded = json_decode($json);
$this->assertTrue(isset($expanded->$text));
$this->assertEquals($expectedpath, $contextpath);
$json = $fileroot->getChild($contextpath)->getContent();
- $this->assertRegExp("/$text.*$text.*$text/", $json);
+ $this->assertRegExp("/$text.*$text.*$text/is", $json);
$expanded = json_decode($json);
$this->assertTrue(isset($expanded->$text));
// Decode equations in question text.
$qtext = $this->qtypeobj->substitute_variables(
$this->question->questiontext, $data);
- $textequations = $this->qtypeobj->find_math_equations($qtext);
+ $textequations = $this->qtypeobj->find_formulas($qtext);
if ($textequations != '' && count($textequations) > 0 ) {
$mform->addElement('static', "divider1[{$j}]", '',
'Formulas {=..} in question text');
if (isset($this->question->id)) {
// Remove prefix #{..}# if exists.
$this->initialname = $question->name;
- $regs= array();
- if (preg_match('~#\{([^[:space:]]*)#~', $question->name , $regs)) {
- $question->name = str_replace($regs[0], '', $question->name);
- };
+ $question->name = question_bank::get_qtype($this->qtype())
+ ->clean_technical_prefix_from_question_name($question->name);
}
}
parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
require_once($CFG->dirroot . '/question/type/questionbase.php');
require_once($CFG->dirroot . '/question/type/numerical/question.php');
+require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
/**
* Represents a calculated question.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated extends question_type {
- /** Regular expression that finds the formulas in content. */
- const FORMULAS_IN_TEXT_REGEX = '~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)\}~';
+ /**
+ * @const string a placeholder is a letter, followed by almost any characters. (This should probably be restricted more.)
+ */
+ const PLACEHOLDER_REGEX_PART = '[[:alpha:]][^>} <`{"\']*';
+
+ /**
+ * @const string REGEXP for a placeholder, wrapped in its {...} delimiters, with capturing brackets around the name.
+ */
+ const PLACEHODLER_REGEX = '~\{(' . self::PLACEHOLDER_REGEX_PART . ')\}~';
+
+ /**
+ * @const string Regular expression that finds the formulas in content, with capturing brackets to get the forumlas.
+ */
+ const FORMULAS_IN_TEXT_REGEX = '~\{=([^{}]*(?:\{' . self::PLACEHOLDER_REGEX_PART . '\}[^{}]*)*)\}~';
const MAX_DATASET_ITEMS = 100;
}
}
+ /**
+ * Remove prefix #{..}# if exists.
+ * @param $name a question name,
+ * @return string the cleaned up question name.
+ */
+ public function clean_technical_prefix_from_question_name($name) {
+ return preg_replace('~#\{([^[:space:]]*)#~', '', $name);
+ }
+
/**
* This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
* so that they can be saved
AND a.category != 0
AND b.question = ?
ORDER BY a.name ", array($question->id));
- $questionname = $question->name;
- $regs= array();
- if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) {
- $questionname = str_replace($regs[0], '', $questionname);
- };
+ $questionname = $this->clean_technical_prefix_from_question_name($question->name);
if (!empty($categorydatasetdefs)) {
// There is at least one with the same name.
: '');
}
+ /**
+ * Find the names of all datasets mentioned in a piece of question content like the question text.
+ * @param $text the text to analyse.
+ * @return array with dataset name for both key and value.
+ */
public function find_dataset_names($text) {
- // Returns the possible dataset names found in the text as an array.
- // The array has the dataset name for both key and value.
- $datasetnames = array();
- while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
- $datasetnames[$regs[1]] = $regs[1];
- $text = str_replace($regs[0], '', $text);
- }
- return $datasetnames;
+ preg_match_all(self::PLACEHODLER_REGEX, $text, $matches);
+ return array_combine($matches[1], $matches[1]);
+ }
+
+ /**
+ * Find all the formulas in a bit of text.
+ *
+ * For example, called with "What is {a} plus {b}? (Hint, it is not {={a}*{b}}.)" this
+ * returns ['{a}*{b}'].
+ *
+ * @param $text text to analyse.
+ * @return array where they keys an values are the formulas.
+ */
+ public function find_formulas($text) {
+ preg_match_all(self::FORMULAS_IN_TEXT_REGEX, $text, $matches);
+ return array_combine($matches[1], $matches[1]);
}
/**
return $text;
}
- public function find_math_equations($text) {
- // Returns the possible dataset names found in the text as an array.
- // The array has the dataset name for both key and value.
- $equations = array();
- while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
- $equations[] = $regs[1];
- $text = str_replace($regs[0], '', $text);
- }
- return $equations;
- }
-
public function get_virtual_qtype() {
return question_bank::get_qtype('numerical');
}
* @return string|boolean false if there are no problems. Otherwise a string error message.
*/
function qtype_calculated_find_formula_errors($formula) {
+ foreach (['//', '/*', '#', '<?', '?>'] as $commentstart) {
+ if (strpos($formula, $commentstart) !== false) {
+ return get_string('illegalformulasyntax', 'qtype_calculated', $commentstart);
+ }
+ }
+
// Validates the formula submitted from the question edit page.
// Returns false if everything is alright
// otherwise it constructs an error message.
- // Strip away dataset names.
- while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
- $formula = str_replace($regs[0], '1', $formula);
- }
+ // Strip away dataset names. Use 1.0 to catch illegal concatenation like {a}{b}.
+ $formula = preg_replace(qtype_calculated::PLACEHODLER_REGEX, '1.0', $formula);
// Strip away empty space and lowercase it.
$formula = strtolower(str_replace(' ', '', $formula));
return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
}
- // Exchange the function call with '1' and then check for
+ // Exchange the function call with '1.0' and then check for
// another function call...
if ($regs[1]) {
// The function call is proceeded by an operator.
- $formula = str_replace($regs[0], $regs[1] . '1', $formula);
+ $formula = str_replace($regs[0], $regs[1] . '1.0', $formula);
} else {
// The function call starts the formula.
- $formula = preg_replace("~^{$regs[2]}\\([^)]*\\)~", '1', $formula);
+ $formula = preg_replace('~^' . preg_quote($regs[2], '~') . '\([^)]*\)~', '1.0', $formula);
}
}
* @return string|boolean false if there are no problems. Otherwise a string error message.
*/
function qtype_calculated_find_formula_errors_in_text($text) {
- preg_match_all(qtype_calculated::FORMULAS_IN_TEXT_REGEX, $text, $matches);
+ $formulas = question_bank::get_qtype('calculated')->find_formulas($text);
$errors = array();
- foreach ($matches[1] as $match) {
+ foreach ($formulas as $match) {
$error = qtype_calculated_find_formula_errors($match);
if ($error) {
$errors[] = $error;
$this->assertFalse(qtype_calculated_find_formula_errors('1 + 1'));
$this->assertFalse(qtype_calculated_find_formula_errors('{x} + {y}'));
$this->assertFalse(qtype_calculated_find_formula_errors('{x}*{y}'));
+ $this->assertFalse(qtype_calculated_find_formula_errors('{x}*({y}+1)'));
+ }
+
+ public function test_simple_equations_errors() {
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('{a{b}}'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('{a{b}}'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('{a}({b})'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('2({b})'));
}
public function test_safe_functions_ok() {
$this->assertFalse(qtype_calculated_find_formula_errors('max(1.0, 1.0, 2, 3)'));
}
+ public function test_php_comments_blocked() {
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('# No need for this.'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('/* Also blocked. */'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('1 + 1 /* Blocked too. */'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('// As is this.'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('1/*2'));
+ $this->assert_nonempty_string(qtype_calculated_find_formula_errors('/*{a*///{x}}'));
+ }
+
public function test_dangerous_functions_blocked() {
$this->assert_nonempty_string(qtype_calculated_find_formula_errors('eval(1)'));
$this->assert_nonempty_string(qtype_calculated_find_formula_errors('system(1)'));
$this->assertEquals("Lorem ipsum dolor...", $this->qtype->get_short_question_name($longquestionname, 20));
$this->assertEquals("Lorem ipsum", $this->qtype->get_short_question_name($shortquestionname, 20));
}
+
+ public function test_placehodler_regex() {
+ preg_match_all(qtype_calculated::PLACEHODLER_REGEX, '= {={a} + {b}}', $matches);
+ $this->assertEquals([['{a}', '{b}'], ['a', 'b']], $matches);
+ }
+
+ public function test_formulas_in_text_regex() {
+ preg_match_all(qtype_calculated::FORMULAS_IN_TEXT_REGEX, '= {={a} + {b}}', $matches);
+ $this->assertEquals([['{={a} + {b}}'], ['{a} + {b}']], $matches);
+ }
+
+ public function test_find_dataset_names() {
+ $this->assertEquals([], $this->qtype->find_dataset_names('Frog.'));
+
+ $this->assertEquals(['a' => 'a', 'b' => 'b'],
+ $this->qtype->find_dataset_names('= {={a} + {b}}'));
+
+ $this->assertEquals(['a' => 'a', 'b' => 'b'],
+ $this->qtype->find_dataset_names('What is {a} plus {b}? (Hint, it is not {={a}*{b}}.)'));
+
+ $this->assertEquals(['a' => 'a', 'b' => 'b', 'c' => 'c'],
+ $this->qtype->find_dataset_names('
+ <p>If called with $a = {a} and $b = {b}, what does this PHP function return?</p>
+ <pre>
+ /**
+ * What does this do?
+ */
+ function mystery($a, $b) {
+ return {c}*$a + $b;
+ }
+ </pre>
+ '));
+ }
}
*/
public function replace_expressions_in_text($text, $length = null, $format = null) {
$vs = $this; // Can't see to use $this in a PHP closure.
- $text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~',
+ $text = preg_replace_callback(qtype_calculated::FORMULAS_IN_TEXT_REGEX,
function ($matches) use ($vs, $format, $length) {
return $vs->format_float($vs->calculate($matches[1]), $length, $format);
}, $text);
if (isset($this->question->id)) {
// Remove prefix #{..}# if exists.
$this->initialname = $question->name;
- $regs= array();
- if (preg_match('~#\{([^[:space:]]*)#~', $question->name , $regs)) {
- $question->name = str_replace($regs[0], '', $question->name);
- };
+ $question->name = question_bank::get_qtype('calculated')
+ ->clean_technical_prefix_from_question_name($question->name);
}
}
parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
$answers, $data, $number) {
- global $DB;
+
$comment = new stdClass();
$comment->stranswers = array();
$comment->outsidelimit = false;
$comment->answers = array();
$answers = fullclone($answers);
- $errors = '';
- $delimiter = ': ';
foreach ($answers as $key => $answer) {
- $anssubstituted = $this->substitute_variables($answer->answer, $data);
// Evaluate the equations i.e {=5+4).
- $anstext = '';
- $anstextremaining = $anssubstituted;
- while (preg_match('~\{=([^[:space:]}]*)}~', $anstextremaining, $regs1)) {
- $anstextsplits = explode($regs1[0], $anstextremaining, 2);
- $anstext =$anstext.$anstextsplits[0];
- $anstextremaining = $anstextsplits[1];
- if (empty($regs1[1])) {
- $str = '';
+ $anssubstituted = $this->substitute_variables($answer->answer, $data);
+ $formulas = $this->find_formulas($anssubstituted);
+ $replaces = [];
+ foreach ($formulas as $formula) {
+ if ($formulaerrors = qtype_calculated_find_formula_errors($formula)) {
+ $str = $formulaerrors;
} else {
- if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
- $str=$formulaerrors;
- } else {
- eval('$str = '.$regs1[1].';');
- }
+ eval('$str = ' . $formula . ';');
}
- $anstext = $anstext.$str;
+ $replaces[$formula] = $str;
}
- $anstext .= $anstextremaining;
+ $anstext = str_replace(arary_keys($replaces), arary_values($replaces), $anssubstituted);
$comment->stranswers[$key] = $anssubstituted.'<br/>'.$anstext;
}
return fullclone($comment);
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
- $params = ['dailyuser' => $userid, 'weeklyuser' => $userid, 'monthlyuser' => $userid, 'contextcourse' => CONTEXT_COURSE];
+ $params = ['userid' => $userid, 'contextcourse' => CONTEXT_COURSE];
$sql = "SELECT ctx.id
FROM {context} ctx
- LEFT JOIN {stats_user_daily} sud ON sud.courseid = ctx.instanceid
- LEFT JOIN {stats_user_weekly} suw ON suw.courseid = ctx.instanceid
- LEFT JOIN {stats_user_monthly} sum ON sum.courseid = ctx.instanceid
- WHERE ctx.contextlevel = :contextcourse
- AND (sud.userid = :dailyuser OR suw.userid = :weeklyuser OR sum.userid = :monthlyuser)";
+ JOIN {stats_user_daily} sud ON sud.courseid = ctx.instanceid AND sud.userid = :userid
+ WHERE ctx.contextlevel = :contextcourse";
+
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
+
+ $sql = "SELECT ctx.id
+ FROM {context} ctx
+ JOIN {stats_user_weekly} suw ON suw.courseid = ctx.instanceid AND suw.userid = :userid
+ WHERE ctx.contextlevel = :contextcourse";
+ $contextlist->add_from_sql($sql, $params);
+
+ $sql = "SELECT ctx.id
+ FROM {context} ctx
+ JOIN {stats_user_monthly} sum ON sum.courseid = ctx.instanceid AND sum.userid = :userid
+ WHERE ctx.contextlevel = :contextcourse";
+ $contextlist->add_from_sql($sql, $params);
+
return $contextlist;
}
"homelink": "/"
}
}}
-<div class="container-fluid">
- <div class="row">
+<div class="container-fluid mt-1 mt-md-5">
+ <div class="row justify-content-md-center">
<div class="col-md-8 push-md-2 col-xl-6 push-xl-3">
<div class="card">
- <div class="card-block">
+ <div class="card-body">
<div class="card-title text-xs-center">
{{#logourl}}
<h2><img src="{{logourl}}" title="{{sitename}}" alt="{{sitename}}"/></h2>
"formhtml": "(Form html would go here)"
}
}}
-<div class="container-fluid">
- <div class="row">
+<div class="container-fluid mt-1 mt-md-5">
+ <div class="row justify-content-md-center">
<div class="col-md-8 push-md-2 col-xl-6 push-xl-3">
<div class="card">
- <div class="card-block">
+ <div class="card-body">
<div class="card-title text-xs-center">
{{#logourl}}
<h2><img src="{{logourl}}" title="{{sitename}}" alt="{{sitename}}"/></h2>