diff --git a/.github/workflows/moodle-plugin-ci.yml b/.github/workflows/moodle-plugin-ci.yml index 11680c6..4247956 100644 --- a/.github/workflows/moodle-plugin-ci.yml +++ b/.github/workflows/moodle-plugin-ci.yml @@ -59,13 +59,6 @@ jobs: moodle-branch: MOODLE_401_STABLE database: mariadb - - php: 8.0 - moodle-branch: MOODLE_400_STABLE - database: pgsql - - php: 8.0 - moodle-branch: MOODLE_400_STABLE - database: mariadb - - php: 7.4 moodle-branch: MOODLE_401_STABLE database: pgsql @@ -73,20 +66,6 @@ jobs: moodle-branch: MOODLE_401_STABLE database: mariadb - - php: 7.4 - moodle-branch: MOODLE_400_STABLE - database: pgsql - - php: 7.4 - moodle-branch: MOODLE_400_STABLE - database: mariadb - - - php: 7.4 - moodle-branch: MOODLE_311_STABLE - database: pgsql - - php: 7.4 - moodle-branch: MOODLE_311_STABLE - database: mariadb - steps: - name: Check out repository code uses: actions/checkout@v4 diff --git a/backup/moodle2/backup_pdfannotator_stepslib.php b/backup/moodle2/backup_pdfannotator_stepslib.php index bd0cdec..441022b 100644 --- a/backup/moodle2/backup_pdfannotator_stepslib.php +++ b/backup/moodle2/backup_pdfannotator_stepslib.php @@ -55,7 +55,7 @@ protected function define_structure() { // 2. Define each element separately. $pdfannotator = new backup_nested_element('pdfannotator', array('id'), array( 'name', 'intro', 'introformat', 'usevotes', 'useprint', 'useprintcomments', 'use_studenttextbox', 'use_studentdrawing', - 'useprivatecomments', 'useprotectedcomments', 'timecreated', 'timemodified')); + 'useprivatecomments', 'useprotectedcomments', 'forcesubscribe', 'timecreated', 'timemodified')); $annotations = new backup_nested_element('annotations'); $annotation = new backup_nested_element('annotation', array('id'), array('page', 'userid', 'annotationtypeid', diff --git a/classes/output/comment.php b/classes/output/comment.php index e37ca65..5161f85 100644 --- a/classes/output/comment.php +++ b/classes/output/comment.php @@ -83,7 +83,7 @@ public function __construct($data, $cm, $context) { $this->addeditbutton($comment, $editanypost); $this->addhidebutton($comment, $seehiddencomments, $hidecomments); $this->adddeletebutton($comment, $deleteown, $deleteany); - $this->addsubscribebutton($comment, $subscribe); + $this->addsubscribebutton($comment, $subscribe, $cm); $this->addforwardbutton($comment, $forwardquestions, $cm); $this->addmarksolvedbutton($comment, $solve); @@ -270,15 +270,26 @@ private function adddeletebutton($comment, $deleteown, $deleteany) { } } - private function addsubscribebutton($comment, $subscribe) { + /** + * Add a subscribe button + * + * @param object $comment + * @param bool $subscribe + * @param stdClass $cm course module object + * @throws \coding_exception + */ + private function addsubscribebutton($comment, $subscribe, $cm) { if (!isset($comment->type) && $comment->isquestion && $subscribe && $comment->visibility != 'private') { - // Only set for textbox and drawing. - if (!empty($comment->issubscribed)) { - $comment->buttons[] = ["classes" => "comment-subscribe-a", "faicon" => ["class" => "fa-bell-slash"], - "text" => get_string('unsubscribeQuestion', 'pdfannotator')]; - } else { - $comment->buttons[] = ["classes" => "comment-subscribe-a", "faicon" => ["class" => "fa-bell"], - "text" => get_string('subscribeQuestion', 'pdfannotator')]; + // Only set for textbox and drawing, and only if subscription mode is not disabled or forced. + if ((pdfannotator_get_subscriptionmode($cm->instance) == PDFANNOTATOR_CHOOSESUBSCRIBE) || + (pdfannotator_get_subscriptionmode($cm->instance) == PDFANNOTATOR_INITIALSUBSCRIBE)) { + if (!empty($comment->issubscribed)) { + $comment->buttons[] = ["classes" => "comment-subscribe-a", "faicon" => ["class" => "fa-bell-slash"], + "text" => get_string('unsubscribeQuestion', 'pdfannotator')]; + } else { + $comment->buttons[] = ["classes" => "comment-subscribe-a", "faicon" => ["class" => "fa-bell"], + "text" => get_string('subscribeQuestion', 'pdfannotator')]; + } } } } diff --git a/classes/subscriptions.php b/classes/subscriptions.php new file mode 100644 index 0000000..20e7fde --- /dev/null +++ b/classes/subscriptions.php @@ -0,0 +1,885 @@ +. + +/** + * Pdfannotator subscription manager. + * + * @package mod_pdfannotator + * @copyright 2021 Luca Bösch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_pdfannotator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Pdfannotator subscription manager. + * + * @copyright 2021 Luca Bösch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class subscriptions { + + /** + * The status value for an unsubscribed discussion. + * + * @var int + */ + const PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED = -1; + + /** + * The subscription cache for pdfannotators. + * + * The first level key is the user ID + * The second level is the pdfannotator ID + * The Value then is bool for subscribed of not. + * + * @var array[] An array of arrays. + */ + protected static $pdfannotatorcache = array(); + + /** + * The list of pdfannotators which have been wholly retrieved for the pdfannotator subscription cache. + * + * This allows for prior caching of an entire pdfannotator to reduce the + * number of DB queries in a subscription check loop. + * + * @var bool[] + */ + protected static $fetchedpdfannotators = array(); + + /** + * The subscription cache for pdfannotator discussions. + * + * The first level key is the user ID + * The second level is the pdfannotator ID + * The third level key is the discussion ID + * The value is then the users preference (int) + * + * @var array[] + */ + protected static $pdfannotatordiscussioncache = array(); + + /** + * The list of pdfannotators which have been wholly retrieved for the pdfannotator discussion subscription cache. + * + * This allows for prior caching of an entire pdfannotator to reduce the + * number of DB queries in a subscription check loop. + * + * @var bool[] + */ + protected static $discussionfetchedpdfannotators = array(); + + /** + * Whether a user is subscribed to this pdfannotator, or a discussion within + * the pdfannotator. + * + * If a discussion is specified, then report whether the user is + * subscribed to posts to this particular discussion, taking into + * account the pdfannotator preference. + * + * If it is not specified then only the pdfannotator preference is considered. + * + * @param int $userid The user ID + * @param \stdClass $pdfannotator The record of the pdfannotator to test + * @param int $discussionid The ID of the discussion to check + * @param object $cm The coursemodule record. If not supplied, this will be calculated using get_fast_modinfo instead. + * @return bool + * @throws \coding_exception + * @throws \moodle_exception + */ + public static function is_subscribed($userid, $pdfannotator, $discussionid = null, $cm = null) { + // If pdfannotator is force subscribed and has allowforcesubscribe, then user is subscribed. + if (self::is_forcesubscribed($pdfannotator)) { + if (!$cm) { + $cm = get_fast_modinfo($pdfannotator->course)->instances['pdfannotator'][$pdfannotator->id]; + } + if (has_capability('mod/pdfannotator:allowforcesubscribe', \context_module::instance($cm->id), $userid)) { + return true; + } + } + + if ($discussionid === null) { + return self::is_subscribed_to_pdfannotator($userid, $pdfannotator); + } + + $subscriptions = self::fetch_discussion_subscription($pdfannotator->id, $userid); + + // Check whether there is a record for this discussion subscription. + if (isset($subscriptions[$discussionid])) { + return ($subscriptions[$discussionid] != self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED); + } + + return self::is_subscribed_to_pdfannotator($userid, $pdfannotator); + } + + /** + * Whether a user is subscribed to this pdfannotator. + * + * @param int $userid The user ID + * @param \stdClass $pdfannotator The record of the pdfannotator to test + * @return boolean + */ + protected static function is_subscribed_to_pdfannotator($userid, $pdfannotator) { + return self::fetch_subscription_cache($pdfannotator->id, $userid); + } + + /** + * Helper to determine whether a pdfannotator has it's subscription mode set + * to forced subscription. + * + * @param \stdClass $pdfannotator The record of the pdfannotator to test + * @return bool + */ + public static function is_forcesubscribed($pdfannotator) { + return ($pdfannotator->forcesubscribe == pdfannotator_FORCESUBSCRIBE); + } + + /** + * Helper to determine whether a pdfannotator has it's subscription mode set to disabled. + * + * @param \stdClass $pdfannotator The record of the pdfannotator to test + * @return bool + */ + public static function subscription_disabled($pdfannotator) { + return ($pdfannotator->forcesubscribe == pdfannotator_DISALLOWSUBSCRIBE); + } + + /** + * Helper to determine whether the specified pdfannotator can be subscribed to. + * + * @param \stdClass $pdfannotator The record of the pdfannotator to test + * @return bool + */ + public static function is_subscribable($pdfannotator) { + return (isloggedin() && !isguestuser() && + !self::is_forcesubscribed($pdfannotator) && + !self::subscription_disabled($pdfannotator)); + } + + /** + * Set the pdfannotator subscription mode. + * + * By default when called without options, this is set to PDFANNOTATOR_FORCESUBSCRIBE. + * + * @param \stdClass $pdfannotatorid The id of the pdfannotator to set the state + * @param int $status The new subscription state + * @return bool + * @throws \dml_exception + */ + public static function set_subscription_mode($pdfannotatorid, $status = 1) { + global $DB; + return $DB->set_field("pdfannotator", "forcesubscribe", $status, array("id" => $pdfannotatorid)); + } + + /** + * Returns the current subscription mode for the pdfannotator. + * + * @param \stdClass $pdfannotator The record of the pdfannotator to set + * @return int The pdfannotator subscription mode + */ + public static function get_subscription_mode($pdfannotator) { + return $pdfannotator->forcesubscribe; + } + + /** + * Returns an array of pdfannotators that the current user is subscribed to and is allowed to unsubscribe from + * + * @return array An array of unsubscribable pdfannotators + */ + public static function get_unsubscribable_pdfannotators() { + global $USER, $DB; + + // Get courses that $USER is enrolled in and can see. + $courses = enrol_get_my_courses(); + if (empty($courses)) { + return array(); + } + + $courseids = array(); + foreach ($courses as $course) { + $courseids[] = $course->id; + } + list($coursesql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'c'); + + // Get all pdfannotators from the user's courses that they are subscribed to and which are not set to forced. + // It is possible for users to be subscribed to a pdfannotator in subscription disallowed mode so they must be listed + // here so that that can be unsubscribed from. + $sql = "SELECT f.id, cm.id as cm, cm.visible, f.course + FROM {pdfannotator} f + JOIN {course_modules} cm ON cm.instance = f.id + JOIN {modules} m ON m.name = :modulename AND m.id = cm.module + LEFT JOIN {pdfannotator_subscriptions} fs ON (fs.pdfannotator = f.id AND fs.userid = :userid) + WHERE f.forcesubscribe <> :forcesubscribe + AND fs.id IS NOT NULL + AND cm.course + $coursesql"; + $params = array_merge($courseparams, array( + 'modulename' => 'pdfannotator', + 'userid' => $USER->id, + 'forcesubscribe' => pdfannotator_FORCESUBSCRIBE, + )); + $pdfannotators = $DB->get_recordset_sql($sql, $params); + + $unsubscribablepdfannotators = array(); + foreach ($pdfannotators as $pdfannotator) { + if (empty($pdfannotator->visible)) { + // The pdfannotator is hidden - check if the user can view the pdfannotator. + $context = \context_module::instance($pdfannotator->cm); + if (!has_capability('moodle/course:viewhiddenactivities', $context)) { + // The user can't see the hidden pdfannotator to cannot unsubscribe. + continue; + } + } + + $unsubscribablepdfannotators[] = $pdfannotator; + } + $pdfannotators->close(); + + return $unsubscribablepdfannotators; + } + + /** + * Get the list of potential subscribers to a pdfannotator. + * + * @param context_module $context the pdfannotator context. + * @param integer $groupid the id of a group, or 0 for all groups. + * @param string $fields the list of fields to return for each user. As for get_users_by_capability. + * @param string $sort sort order. As for get_users_by_capability. + * @return array list of users. + */ + public static function get_potential_subscribers($context, $groupid, $fields, $sort = '') { + global $DB; + + // Only active enrolled users or everybody on the frontpage. + list($esql, $params) = get_enrolled_sql($context, 'mod/pdfannotator:allowforcesubscribe', $groupid, true); + if (!$sort) { + list($sort, $sortparams) = users_order_by_sql('u'); + $params = array_merge($params, $sortparams); + } + + $sql = "SELECT $fields + FROM {user} u + JOIN ($esql) je ON je.id = u.id + WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 + ORDER BY $sort"; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Fetch the pdfannotator subscription data for the specified userid and pdfannotator. + * + * @param int $pdfannotatorid The pdfannotator to retrieve a cache for + * @param int $userid The user ID + * @return boolean + */ + public static function fetch_subscription_cache($pdfannotatorid, $userid) { + if (isset(self::$pdfannotatorcache[$userid]) && isset(self::$pdfannotatorcache[$userid][$pdfannotatorid])) { + return self::$pdfannotatorcache[$userid][$pdfannotatorid]; + } + self::fill_subscription_cache($pdfannotatorid, $userid); + + if (!isset(self::$pdfannotatorcache[$userid]) || !isset(self::$pdfannotatorcache[$userid][$pdfannotatorid])) { + return false; + } + + return self::$pdfannotatorcache[$userid][$pdfannotatorid]; + } + + /** + * Fill the pdfannotator subscription data for the specified userid and pdfannotator. + * + * If the userid is not specified, then all subscription data for that pdfannotator is fetched in a single query and used + * for subsequent lookups without requiring further database queries. + * + * @param int $pdfannotatorid The pdfannotator to retrieve a cache for + * @param int $userid The user ID + * @return void + */ + public static function fill_subscription_cache($pdfannotatorid, $userid = null) { + global $DB; + + if (!isset(self::$fetchedpdfannotators[$pdfannotatorid])) { + // This pdfannotator has not been fetched as a whole. + if (isset($userid)) { + if (!isset(self::$pdfannotatorcache[$userid])) { + self::$pdfannotatorcache[$userid] = array(); + } + + if (!isset(self::$pdfannotatorcache[$userid][$pdfannotatorid])) { + if ($DB->record_exists('pdfannotator_subscriptions', array( + 'userid' => $userid, + 'pdfannotator' => $pdfannotatorid, + ))) { + self::$pdfannotatorcache[$userid][$pdfannotatorid] = true; + } else { + self::$pdfannotatorcache[$userid][$pdfannotatorid] = false; + } + } + } else { + $subscriptions = $DB->get_recordset('pdfannotator_subscriptions', array( + 'pdfannotator' => $pdfannotatorid, + ), '', 'id, userid'); + foreach ($subscriptions as $id => $data) { + if (!isset(self::$pdfannotatorcache[$data->userid])) { + self::$pdfannotatorcache[$data->userid] = array(); + } + self::$pdfannotatorcache[$data->userid][$pdfannotatorid] = true; + } + self::$fetchedpdfannotators[$pdfannotatorid] = true; + $subscriptions->close(); + } + } + } + + /** + * Fill the pdfannotator subscription data for all pdfannotators that the specified userid can subscribe to in the specified + * course. + * + * @param int $courseid The course to retrieve a cache for + * @param int $userid The user ID + * @return void + */ + public static function fill_subscription_cache_for_course($courseid, $userid) { + global $DB; + + if (!isset(self::$pdfannotatorcache[$userid])) { + self::$pdfannotatorcache[$userid] = array(); + } + + $sql = "SELECT + f.id AS pdfannotatorid, + s.id AS subscriptionid + FROM {pdfannotator} f + LEFT JOIN {pdfannotator_subscriptions} s ON (s.pdfannotator = f.id AND s.userid = :userid) + WHERE f.course = :course + AND f.forcesubscribe <> :subscriptionforced"; + + $subscriptions = $DB->get_recordset_sql($sql, array( + 'course' => $courseid, + 'userid' => $userid, + 'subscriptionforced' => pdfannotator_FORCESUBSCRIBE, + )); + + foreach ($subscriptions as $id => $data) { + self::$pdfannotatorcache[$userid][$id] = !empty($data->subscriptionid); + } + $subscriptions->close(); + } + + /** + * Returns a list of user objects who are subscribed to this pdfannotator. + * + * @param stdClass $pdfannotator The pdfannotator record. + * @param int $groupid The group id if restricting subscriptions to a group of users, or 0 for all. + * @param context_module $context the pdfannotator context, to save re-fetching it where possible. + * @param string $fields requested user fields (with "u." table prefix). + * @param boolean $includediscussionsubscriptions Whether to take discussion subscriptions and unsubscriptions into + * consideration. + * @return array list of users. + */ + public static function fetch_subscribed_users($pdfannotator, $groupid = 0, $context = null, $fields = null, + $includediscussionsubscriptions = false) { + global $CFG, $DB; + + if (empty($fields)) { + $userfieldsapi = \core_user\fields::for_name(); + $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; + $fields = "u.id, + u.username, + $allnames, + u.maildisplay, + u.mailformat, + u.maildigest, + u.imagealt, + u.email, + u.emailstop, + u.city, + u.country, + u.lastaccess, + u.lastlogin, + u.picture, + u.timezone, + u.theme, + u.lang, + u.trackpdfannotators, + u.mnethostid"; + } + + // Retrieve the pdfannotator context if it wasn't specified. + $context = pdfannotator_get_context($pdfannotator->id, $context); + + if (self::is_forcesubscribed($pdfannotator)) { + $results = self::get_potential_subscribers($context, $groupid, $fields, "u.email ASC"); + + } else { + // Only active enrolled users or everybody on the frontpage. + list($esql, $params) = get_enrolled_sql($context, '', $groupid, true); + $params['pdfannotatorid'] = $pdfannotator->id; + + if ($includediscussionsubscriptions) { + $params['spdfannotatorid'] = $pdfannotator->id; + $params['dspdfannotatorid'] = $pdfannotator->id; + $params['unsubscribed'] = self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED; + + $sql = "SELECT $fields + FROM ( + SELECT userid FROM {pdfannotator_subscriptions} s + WHERE + s.pdfannotator = :spdfannotatorid + UNION + SELECT userid FROM {pdfannotator_discussion_subs} ds + WHERE + ds.pdfannotator = :dspdfannotatorid AND ds.preference <> :unsubscribed + ) subscriptions + JOIN {user} u ON u.id = subscriptions.userid + JOIN ($esql) je ON je.id = u.id + WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 + ORDER BY u.email ASC"; + + } else { + $sql = "SELECT $fields + FROM {user} u + JOIN ($esql) je ON je.id = u.id + JOIN {pdfannotator_subscriptions} s ON s.userid = u.id + WHERE + s.pdfannotator = :pdfannotatorid AND u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1 + ORDER BY u.email ASC"; + } + $results = $DB->get_records_sql($sql, $params); + } + + // Guest user should never be subscribed to a pdfannotator. + unset($results[$CFG->siteguest]); + + // Apply the activity module availability resetrictions. + $cm = get_coursemodule_from_instance('pdfannotator', $pdfannotator->id, $pdfannotator->course); + $modinfo = get_fast_modinfo($pdfannotator->course); + $info = new \core_availability\info_module($modinfo->get_cm($cm->id)); + $results = $info->filter_user_list($results); + + return $results; + } + + /** + * Retrieve the discussion subscription data for the specified userid and pdfannotator. + * + * This is returned as an array of discussions for that pdfannotator which contain the preference in a stdClass. + * + * @param int $pdfannotatorid The pdfannotator to retrieve a cache for + * @param int $userid The user ID + * @return array of stdClass objects with one per discussion in the pdfannotator. + */ + public static function fetch_discussion_subscription($pdfannotatorid, $userid = null) { + self::fill_discussion_subscription_cache($pdfannotatorid, $userid); + + if (!isset(self::$pdfannotatordiscussioncache[$userid]) || + !isset(self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid])) { + return array(); + } + + return self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid]; + } + + /** + * Fill the discussion subscription data for the specified userid and pdfannotator. + * + * If the userid is not specified, then all discussion subscription data for that pdfannotator is fetched in a single query + * and used for subsequent lookups without requiring further database queries. + * + * @param int $pdfannotatorid The pdfannotator to retrieve a cache for + * @param int $userid The user ID + * @return void + */ + public static function fill_discussion_subscription_cache($pdfannotatorid, $userid = null) { + global $DB; + + if (!isset(self::$discussionfetchedpdfannotators[$pdfannotatorid])) { + // This pdfannotator hasn't been fetched as a whole yet. + if (isset($userid)) { + if (!isset(self::$pdfannotatordiscussioncache[$userid])) { + self::$pdfannotatordiscussioncache[$userid] = array(); + } + + if (!isset(self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid])) { + $subscriptions = $DB->get_recordset('pdfannotator_discussion_subs', array( + 'userid' => $userid, + 'pdfannotator' => $pdfannotatorid, + ), null, 'id, discussion, preference'); + + self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid] = array(); + foreach ($subscriptions as $id => $data) { + self::add_to_discussion_cache($pdfannotatorid, $userid, $data->discussion, $data->preference); + } + + $subscriptions->close(); + } + } else { + $subscriptions = $DB->get_recordset('pdfannotator_discussion_subs', array( + 'pdfannotator' => $pdfannotatorid, + ), null, 'id, userid, discussion, preference'); + foreach ($subscriptions as $id => $data) { + self::add_to_discussion_cache($pdfannotatorid, $data->userid, $data->discussion, $data->preference); + } + self::$discussionfetchedpdfannotators[$pdfannotatorid] = true; + $subscriptions->close(); + } + } + } + + /** + * Add the specified discussion and user preference to the discussion + * subscription cache. + * + * @param int $pdfannotatorid The ID of the pdfannotator that this preference belongs to + * @param int $userid The ID of the user that this preference belongs to + * @param int $discussion The ID of the discussion that this preference relates to + * @param int $preference The preference to store + */ + protected static function add_to_discussion_cache($pdfannotatorid, $userid, $discussion, $preference) { + if (!isset(self::$pdfannotatordiscussioncache[$userid])) { + self::$pdfannotatordiscussioncache[$userid] = array(); + } + + if (!isset(self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid])) { + self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid] = array(); + } + + self::$pdfannotatordiscussioncache[$userid][$pdfannotatorid][$discussion] = $preference; + } + + /** + * Reset the discussion cache. + * + * This cache is used to reduce the number of database queries when + * checking pdfannotator discussion subscription states. + */ + public static function reset_discussion_cache() { + self::$pdfannotatordiscussioncache = array(); + self::$discussionfetchedpdfannotators = array(); + } + + /** + * Reset the pdfannotator cache. + * + * This cache is used to reduce the number of database queries when + * checking pdfannotator subscription states. + */ + public static function reset_pdfannotator_cache() { + self::$pdfannotatorcache = array(); + self::$fetchedpdfannotators = array(); + } + + /** + * Adds user to the subscriber list. + * + * @param int $userid The ID of the user to subscribe + * @param \stdClass $pdfannotator The pdfannotator record for this pdfannotator. + * @param \context_module|null $context Module context, may be omitted if not known or if called for the current + * module set in page. + * @param boolean $userrequest Whether the user requested this change themselves. This has an effect on whether + * discussion subscriptions are removed too. + * @return bool|int Returns true if the user is already subscribed, or the pdfannotator_subscriptions ID if the user was + * successfully subscribed. + */ + public static function subscribe_user($userid, $pdfannotator, $context = null, $userrequest = false) { + global $DB; + + if (self::is_subscribed($userid, $pdfannotator)) { + return true; + } + + $sub = new \stdClass(); + $sub->userid = $userid; + $sub->pdfannotator = $pdfannotator->id; + + $result = $DB->insert_record("pdfannotator_subscriptions", $sub); + + if ($userrequest) { + $discussionsubscriptions = $DB->get_recordset('pdfannotator_discussion_subs', array('userid' => $userid, + 'pdfannotator' => $pdfannotator->id)); + $DB->delete_records_select('pdfannotator_discussion_subs', + 'userid = :userid AND pdfannotator = :pdfannotatorid AND preference <> :preference', array( + 'userid' => $userid, + 'pdfannotatorid' => $pdfannotator->id, + 'preference' => self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED, + )); + + // Reset the subscription caches for this pdfannotator. + // We know that the there were previously entries and there aren't any more. + if (isset(self::$pdfannotatordiscussioncache[$userid]) && + isset(self::$pdfannotatordiscussioncache[$userid][$pdfannotator->id])) { + foreach (self::$pdfannotatordiscussioncache[$userid][$pdfannotator->id] as $discussionid => $preference) { + if ($preference != self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED) { + unset(self::$pdfannotatordiscussioncache[$userid][$pdfannotator->id][$discussionid]); + } + } + } + } + + // Reset the cache for this pdfannotator. + self::$pdfannotatorcache[$userid][$pdfannotator->id] = true; + + $context = pdfannotator_get_context($pdfannotator->id, $context); + $params = array( + 'context' => $context, + 'objectid' => $result, + 'relateduserid' => $userid, + 'other' => array('pdfannotatorid' => $pdfannotator->id), + + ); + $event = event\subscription_created::create($params); + if ($userrequest && $discussionsubscriptions) { + foreach ($discussionsubscriptions as $subscription) { + $event->add_record_snapshot('pdfannotator_discussion_subs', $subscription); + } + $discussionsubscriptions->close(); + } + $event->trigger(); + + return $result; + } + + /** + * Removes user from the subscriber list + * + * @param int $userid The ID of the user to unsubscribe + * @param \stdClass $pdfannotator The pdfannotator record for this pdfannotator. + * @param \context_module|null $context Module context, may be omitted if not known or if called for the current + * module set in page. + * @param boolean $userrequest Whether the user requested this change themselves. This has an effect on whether + * discussion subscriptions are removed too. + * @return boolean Always returns true. + */ + public static function unsubscribe_user($userid, $pdfannotator, $context = null, $userrequest = false) { + global $DB; + + $sqlparams = array( + 'userid' => $userid, + 'pdfannotator' => $pdfannotator->id, + ); + $DB->delete_records('pdfannotator_digests', $sqlparams); + + if ($pdfannotatorsubscription = $DB->get_record('pdfannotator_subscriptions', $sqlparams)) { + $DB->delete_records('pdfannotator_subscriptions', array('id' => $pdfannotatorsubscription->id)); + + if ($userrequest) { + $discussionsubscriptions = $DB->get_recordset('pdfannotator_discussion_subs', $sqlparams); + $DB->delete_records('pdfannotator_discussion_subs', + array('userid' => $userid, 'pdfannotator' => $pdfannotator->id, + 'preference' => self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED)); + + // We know that the there were previously entries and there aren't any more. + if (isset(self::$pdfannotatordiscussioncache[$userid]) && + isset(self::$pdfannotatordiscussioncache[$userid][$pdfannotator->id])) { + self::$pdfannotatordiscussioncache[$userid][$pdfannotator->id] = array(); + } + } + + // Reset the cache for this pdfannotator. + self::$pdfannotatorcache[$userid][$pdfannotator->id] = false; + + $context = pdfannotator_get_context($pdfannotator->id, $context); + $params = array( + 'context' => $context, + 'objectid' => $pdfannotatorsubscription->id, + 'relateduserid' => $userid, + 'other' => array('pdfannotatorid' => $pdfannotator->id), + + ); + $event = event\subscription_deleted::create($params); + $event->add_record_snapshot('pdfannotator_subscriptions', $pdfannotatorsubscription); + if ($userrequest && $discussionsubscriptions) { + foreach ($discussionsubscriptions as $subscription) { + $event->add_record_snapshot('pdfannotator_discussion_subs', $subscription); + } + $discussionsubscriptions->close(); + } + $event->trigger(); + } + + return true; + } + + /** + * Subscribes the user to the specified discussion. + * + * @param int $userid The userid of the user being subscribed + * @param \stdClass $discussion The discussion to subscribe to + * @param \context_module|null $context Module context, may be omitted if not known or if called for the current + * module set in page. + * @return boolean Whether a change was made + */ + public static function subscribe_user_to_discussion($userid, $discussion, $context = null) { + global $DB; + + // First check whether the user is subscribed to the discussion already. + $subscription = $DB->get_record('pdfannotator_discussion_subs', array('userid' => $userid, + 'discussion' => $discussion->id)); + if ($subscription) { + if ($subscription->preference != self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED) { + // The user is already subscribed to the discussion. Ignore. + return false; + } + } + // No discussion-level subscription. Check for a pdfannotator level subscription. + if ($DB->record_exists('pdfannotator_subscriptions', array('userid' => $userid, + 'pdfannotator' => $discussion->pdfannotator))) { + if ($subscription && $subscription->preference == self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED) { + // The user is subscribed to the pdfannotator, but unsubscribed from the discussion, delete the discussion + // preference. + $DB->delete_records('pdfannotator_discussion_subs', array('id' => $subscription->id)); + unset(self::$pdfannotatordiscussioncache[$userid][$discussion->pdfannotator][$discussion->id]); + } else { + // The user is already subscribed to the pdfannotator. Ignore. + return false; + } + } else { + if ($subscription) { + $subscription->preference = time(); + $DB->update_record('pdfannotator_discussion_subs', $subscription); + } else { + $subscription = new \stdClass(); + $subscription->userid = $userid; + $subscription->pdfannotator = $discussion->pdfannotator; + $subscription->discussion = $discussion->id; + $subscription->preference = time(); + + $subscription->id = $DB->insert_record('pdfannotator_discussion_subs', $subscription); + self::$pdfannotatordiscussioncache[$userid][$discussion->pdfannotator][$discussion->id] = $subscription->preference; + } + } + + $context = pdfannotator_get_context($discussion->pdfannotator, $context); + $params = array( + 'context' => $context, + 'objectid' => $subscription->id, + 'relateduserid' => $userid, + 'other' => array( + 'pdfannotatorid' => $discussion->pdfannotator, + 'discussion' => $discussion->id, + ), + + ); + $event = event\discussion_subscription_created::create($params); + $event->trigger(); + + return true; + } + /** + * Unsubscribes the user from the specified discussion. + * + * @param int $userid The userid of the user being unsubscribed + * @param \stdClass $discussion The discussion to unsubscribe from + * @param \context_module|null $context Module context, may be omitted if not known or if called for the current + * module set in page. + * @return boolean Whether a change was made + */ + public static function unsubscribe_user_from_discussion($userid, $discussion, $context = null) { + global $DB; + + // First check whether the user's subscription preference for this discussion. + $subscription = $DB->get_record('pdfannotator_discussion_subs', array('userid' => $userid, + 'discussion' => $discussion->id)); + if ($subscription) { + if ($subscription->preference == self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED) { + // The user is already unsubscribed from the discussion. Ignore. + return false; + } + } + // No discussion-level preference. Check for a pdfannotator level subscription. + if (!$DB->record_exists('pdfannotator_subscriptions', array('userid' => $userid, + 'pdfannotator' => $discussion->pdfannotator))) { + if ($subscription && $subscription->preference != self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED) { + // The user is not subscribed to the pdfannotator, but subscribed from the discussion, delete the discussion + // subscription. + $DB->delete_records('pdfannotator_discussion_subs', array('id' => $subscription->id)); + unset(self::$pdfannotatordiscussioncache[$userid][$discussion->pdfannotator][$discussion->id]); + } else { + // The user is not subscribed from the pdfannotator. Ignore. + return false; + } + } else { + if ($subscription) { + $subscription->preference = self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED; + $DB->update_record('pdfannotator_discussion_subs', $subscription); + } else { + $subscription = new \stdClass(); + $subscription->userid = $userid; + $subscription->pdfannotator = $discussion->pdfannotator; + $subscription->discussion = $discussion->id; + $subscription->preference = self::PDFANNOTATOR_DISCUSSION_UNSUBSCRIBED; + + $subscription->id = $DB->insert_record('pdfannotator_discussion_subs', $subscription); + } + self::$pdfannotatordiscussioncache[$userid][$discussion->pdfannotator][$discussion->id] = $subscription->preference; + } + + $context = pdfannotator_get_context($discussion->pdfannotator, $context); + $params = array( + 'context' => $context, + 'objectid' => $subscription->id, + 'relateduserid' => $userid, + 'other' => array( + 'pdfannotatorid' => $discussion->pdfannotator, + 'discussion' => $discussion->id, + ), + + ); + $event = event\discussion_subscription_deleted::create($params); + $event->trigger(); + + return true; + } + + /** + * Gets the default subscription value for the logged in user. + * + * @param \stdClass $pdfannotator The pdfannotator record + * @param \context $context The course context + * @param \cm_info $cm cm_info + * @param int|null $discussionid The discussion we are checking against + * @return bool Default subscription + * @throws coding_exception + */ + public static function get_user_default_subscription($pdfannotator, $context, $cm, ?int $discussionid) { + global $USER; + $manageactivities = has_capability('moodle/course:manageactivities', $context); + if (self::subscription_disabled($pdfannotator) && !$manageactivities) { + // User does not have permission to subscribe to this discussion at all. + $discussionsubscribe = false; + } else if (self::is_forcesubscribed($pdfannotator)) { + // User does not have permission to unsubscribe from this discussion at all. + $discussionsubscribe = true; + } else { + if (isset($discussionid) && self::is_subscribed($USER->id, $pdfannotator, $discussionid, $cm)) { + // User is subscribed to the discussion - continue the subscription. + $discussionsubscribe = true; + } else if (!isset($discussionid) && self::is_subscribed($USER->id, $pdfannotator, null, $cm)) { + // Starting a new discussion, and the user is subscribed to the pdfannotator - subscribe to the discussion. + $discussionsubscribe = true; + } else { + // User is not subscribed to either pdfannotator or discussion. Follow user preference. + $discussionsubscribe = $USER->autosubscribe ?? false; + } + } + + return $discussionsubscribe; + } +} + diff --git a/db/install.xml b/db/install.xml index 102667d..e8dac02 100755 --- a/db/install.xml +++ b/db/install.xml @@ -18,6 +18,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index d831d7a..75e68a2 100755 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -644,5 +644,19 @@ function xmldb_pdfannotator_upgrade($oldversion) { upgrade_mod_savepoint(true, 2022110200, 'pdfannotator'); } + if ($oldversion < 2023112901) { + + // Define field forcesubscribe to be added to pdfannotator. + $table = new xmldb_table('pdfannotator'); + $field = new xmldb_field('forcesubscribe', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'useprotectedcomments'); + + // Conditionally launch add field forcesubscribe. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Pdfannotator savepoint reached. + upgrade_mod_savepoint(true, 2023112901, 'pdfannotator'); + } return true; } diff --git a/lang/en/pdfannotator.php b/lang/en/pdfannotator.php index f8d65b5..bc7f915 100644 --- a/lang/en/pdfannotator.php +++ b/lang/en/pdfannotator.php @@ -453,6 +453,18 @@ $string['subscribed'] = 'Subscribed'; $string['subscribedanswers'] = 'to my subscribed questions'; $string['subscribeQuestion'] = 'Subscribe'; +$string['subscription'] = 'Subscription'; +$string['subscriptionauto'] = 'Auto subscription'; +$string['subscriptiondisabled'] = 'Subscription disabled'; +$string['subscriptionforced'] = 'Forced subscription'; +$string['subscriptionmode'] = 'Subscription mode'; +$string['subscriptionmode_help'] = 'When a participant is subscribed to a question it means they will receive notifications for questions. There are 4 subscription +mode options: +* Auto subscription - Everyone is subscribed initially to notifications for questions but can choose to unsubscribe at any time +* Optional subscription - Participants can choose whether notifications for questions are subscribed +* Forced subscription - Everyone is subscribed to notifications for questions and cannot unsubscribe +* Subscription disabled - Subscriptions to notifications for questions are not allowed'; +$string['subscriptionoptional'] = 'Optional subscription'; $string['subtitleforreportcommentform'] = 'Your message for the course manager'; $string['successfullyEdited'] = 'Changes saved'; $string['successfullyHidden'] = 'Participants now see this comment as hidden.'; diff --git a/lib.php b/lib.php index 1353c40..7f12ba2 100644 --- a/lib.php +++ b/lib.php @@ -21,6 +21,11 @@ */ defined('MOODLE_INTERNAL') || die; +define('PDFANNOTATOR_CHOOSESUBSCRIBE', 0); +define('PDFANNOTATOR_FORCESUBSCRIBE', 1); +define('PDFANNOTATOR_INITIALSUBSCRIBE', 2); +define('PDFANNOTATOR_DISALLOWSUBSCRIBE',3); + require_once($CFG->dirroot . '/mod/pdfannotator/locallib.php'); // Ugly hack to make 3.11 and 4.0 work seamlessly. @@ -788,7 +793,7 @@ function mod_pdfannotator_output_fragment_open_edit_comment_editor($args) { $out .= html_writer::empty_tag('input', ['type' => 'hidden', 'class' => 'pdfannotator_' . $args['action'] . 'comment' . '_editoritemid', 'name' => 'input_value_editor', 'value' => $data['draftItemId']]); $out .= html_writer::empty_tag('input', ['type' => 'hidden', 'class' => 'pdfannotator_' . $args['action'] . 'comment' . '_editorformat', 'name' => 'input_value_editor', 'value' => $data['editorFormat']]); $out .= 'displaycontent:' . $displaycontent; - + return $out; } @@ -802,10 +807,24 @@ function mod_pdfannotator_output_fragment_open_add_comment_editor($args) { $data = pdfannotator_data_preprocessing($context, 'id_pdfannotator_content', 0); $text = file_prepare_draft_area($data['draftItemId'], $context->id, 'mod_pdfannotator', 'post', 0, pdfannotator_get_editor_options($context)); - + $out = ''; $out = html_writer::empty_tag('input', ['type' => 'hidden', 'class' => 'pdfannotator_' . $args['action'] . 'comment' . '_editoritemid', 'name' => 'input_value_editor', 'value' => $data['draftItemId']]); $out .= html_writer::empty_tag('input', ['type' => 'hidden', 'class' => 'pdfannotator_' . $args['action'] . 'comment' . '_editorformat', 'name' => 'input_value_editor', 'value' => $data['editorFormat']]); return $out; } +/** + * List the options for pdfannotator subscription modes. + * This is used by the settings page and by the mod_form page. + * + * @return array + */ +function pdfannotator_get_subscriptionmode_options() { + $options = []; + $options[PDFANNOTATOR_INITIALSUBSCRIBE] = get_string('subscriptionauto', 'pdfannotator'); + $options[PDFANNOTATOR_CHOOSESUBSCRIBE] = get_string('subscriptionoptional', 'pdfannotator'); + $options[PDFANNOTATOR_FORCESUBSCRIBE] = get_string('subscriptionforced', 'pdfannotator'); + $options[PDFANNOTATOR_DISALLOWSUBSCRIBE] = get_string('subscriptiondisabled', 'pdfannotator'); + return $options; +} diff --git a/locallib.php b/locallib.php index 4e245dd..f9f9f73 100644 --- a/locallib.php +++ b/locallib.php @@ -90,7 +90,7 @@ function pdfannotator_display_embed($pdfannotator, $cm, $course, $file, $page = $capabilities->useprintcomments = has_capability('mod/pdfannotator:printcomments', $context); // 3. Comment editor setting. $editorsettings = new stdClass(); - $editorsettings->active_editor = get_class(editors_get_preferred_editor(FORMAT_HTML)); + $editorsettings->active_editor = explode(',', get_config('core', 'texteditors'))[0]; $params = [$cm, $documentobject, $context->id, $USER->id, $capabilities, $toolbarsettings, $page, $annoid, $commid, $editorsettings]; $PAGE->requires->js_init_call('adjustPdfannotatorNavbar', null, true); @@ -126,10 +126,10 @@ function pdfannotator_get_editor_options($context) { 'maxbytes' => get_config('mod_pdfannotator', 'maxbytes'), 'maxfiles' => PDFANNOTATOR_EDITOR_UNLIMITED_FILES, 'return_types' => 15, - 'enable_filemanagement' => true, - 'removeorphaneddrafts' => false, + 'enable_filemanagement' => true, + 'removeorphaneddrafts' => false, 'autosave' => false, - 'noclean' => false, + 'noclean' => false, 'trusttext' => 0, 'subdirs' => true, 'forcehttps' => false, @@ -191,8 +191,8 @@ function pdfannotator_split_content_image($content, $res, $itemid, $context=null $data = []; while (preg_match_all('/', $imgpos_start); $firststr = substr($content, 0, $imgpos_start); @@ -204,7 +204,7 @@ function pdfannotator_split_content_image($content, $res, $itemid, $context=null if (!$format) { throw new \moodle_exception('error:unsupportedextension', 'pdfannotator'); } - if (in_array('jpg', $format) || in_array('jpeg', $format) || in_array('jpe', $format) + if (in_array('jpg', $format) || in_array('jpeg', $format) || in_array('jpe', $format) || in_array('JPG', $format) || in_array('JPEG', $format) || in_array('JPE', $format)) { $format[0] = 'jpeg'; } @@ -241,7 +241,7 @@ function pdfannotator_split_content_image($content, $res, $itemid, $context=null } else { throw new Exception(get_string('error:findimage', 'pdfannotator', $encodedurl)); } - + preg_match('/height=[0-9]+/', $imgstr, $height); if ($height) { $data['imageheight'] = str_replace("\"", "", explode('=', $height[0])[1]); @@ -266,7 +266,7 @@ function pdfannotator_split_content_image($content, $res, $itemid, $context=null } finally { $res[] = $firststr; $res[] = $data; - $content = $laststr; + $content = $laststr; } } @@ -299,8 +299,8 @@ function pdfannotator_data_preprocessing($context, $textarea, $draftitemid = 0) $editor->use_editor($textarea, $options); } else { // initialize Filepicker if image button is active. - $args = new \stdClass(); - // need these three to filter repositories list. + $args = new \stdClass(); + // need these three to filter repositories list. $args->accepted_types = ['web_image']; $args->return_types = 15; $args->context = $context; @@ -324,7 +324,7 @@ function pdfannotator_data_preprocessing($context, $textarea, $draftitemid = 0) $editorformat = editors_get_preferred_format(FORMAT_HTML); //$PAGE->requires->js_init_call('inputDraftItemID', [$draftitemid, (int)$editorformat, $classname]); - + return ['draftItemId' => $draftitemid, 'editorFormat' => $editorformat]; } @@ -1963,7 +1963,7 @@ function pdfannotator_userspoststable_add_row($table, $post) { */ function pdfannotator_reportstable_add_row($thiscourse, $table, $report, $cmid, $itemsperpage, $reportfilter, $currentpage, $context) { global $CFG, $PAGE, $DB; - + $questionid = $DB->get_record('pdfannotator_comments', ['annotationid' => $report->annotationid, 'isquestion' => 1], 'id'); $report->report = pdfannotator_get_relativelink($report->report, $questionid, $context); $report->reportedcomment = pdfannotator_get_relativelink($report->reportedcomment, $report->commentid, $context); @@ -2117,4 +2117,17 @@ function pdfannotator_count_answers($annotationid, $context) { $count++; } return $count; -} \ No newline at end of file +} + +/** + * Returns the subscription mode for a given pdfannotator + * + * @param $id The pdfannotator id + * @return false|int + * @throws dml_exception + */ +function pdfannotator_get_subscriptionmode($id) { + global $DB; + $subscriptionmode = $DB->get_field('pdfannotator', 'forcesubscribe', array('id' => $id), $strictness = MUST_EXIST); + return $subscriptionmode; +} diff --git a/mod_form.php b/mod_form.php index cf4f7fd..070dfef 100644 --- a/mod_form.php +++ b/mod_form.php @@ -119,6 +119,19 @@ public function definition() { $mform->addElement('select', 'legacyfiles', get_string('legacyfiles', 'pdfannotator'), $options); } + // Subscription and tracking. + $mform->addElement('header', 'subscriptionandtrackinghdr', get_string('subscription', 'pdfannotator')); + + $options = pdfannotator_get_subscriptionmode_options(); + $mform->addElement('select', 'forcesubscribe', get_string('subscriptionmode', 'pdfannotator'), $options); + $mform->addHelpButton('forcesubscribe', 'subscriptionmode', 'pdfannotator'); + if (isset($CFG->pdfannotator_subscription)) { + $defaultpdfannotatorsubscription = $CFG->pdfannotator_subscription; + } else { + $defaultpdfannotatorsubscription = PDFANNOTATOR_INITIALSUBSCRIBE; + } + $mform->setDefault('forcesubscribe', $defaultpdfannotatorsubscription); + $this->standard_coursemodule_elements(); $this->add_action_buttons(); diff --git a/model/comment.class.php b/model/comment.class.php index 3b5e2c7..500c0a5 100644 --- a/model/comment.class.php +++ b/model/comment.class.php @@ -59,7 +59,7 @@ public static function create($documentid, $annotationid, $content, $visibility, // Create a new record in the table named 'comments' and return its id, which is created by autoincrement. $commentuuid = $DB->insert_record('pdfannotator_comments', $datarecord, true); $datarecord->id = $commentuuid; - + // Get the draftitemid and prepare the draft area. $draftitemid = required_param('pdfannotator_addcomment_editoritemid', PARAM_INT); $options = pdfannotator_get_editor_options($context); @@ -71,7 +71,7 @@ public static function create($documentid, $annotationid, $content, $visibility, $datarecord->uuid = $commentuuid; self::set_username($datarecord); - + $datarecord->displaycontent = pdfannotator_get_relativelink($datarecord->content, $datarecord->id, $context); $datarecord->displaycontent = format_text($datarecord->displaycontent, FORMAT_MOODLE, ['para' => false, 'filter' => true]); $datarecord->timecreated = pdfannotator_optional_timeago($datarecord->timecreated); @@ -108,7 +108,11 @@ public static function create($documentid, $annotationid, $content, $visibility, } } } else if ($visibility != 'private') { - self::insert_subscription($annotationid, $context); + if (!((pdfannotator_get_subscriptionmode($cm->instance) == PDFANNOTATOR_CHOOSESUBSCRIBE) || + (pdfannotator_get_subscriptionmode($cm->instance) == PDFANNOTATOR_DISALLOWSUBSCRIBE))) { + // Don't insert if subscription mode is Optional subscription or Subscription disabled + self::insert_subscription($annotationid, $context); + } // Notify all users, that there is a new question. $recipients = get_enrolled_users($context, 'mod/pdfannotator:recievenewquestionnotifications'); diff --git a/tests/behat/add_pdfannotator.feature b/tests/behat/add_pdfannotator.feature new file mode 100644 index 0000000..24f7ff4 --- /dev/null +++ b/tests/behat/add_pdfannotator.feature @@ -0,0 +1,35 @@ +@mod @mod_pdfannotator @_file_upload +Feature: Add a pdfannotator activity + In order to let the users use the pdfannotator in a course + As a teacher + I need to add a pdfannotator to a moodle course + + @javascript + Scenario: Add a pdfannotator to a course + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "user preferences" exist: + | user | preference | value | + | teacher1 | htmleditor | atto | + | student1 | htmleditor | atto | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a pdfannotator activity to course "Course 1" section "1" and I fill the form with: + | Name | Test pdf annotation | + | Description | Test pdf annotation description | + | Select a pdf-file | mod/pdfannotator/tests/fixtures/submission.pdf | + And I am on "Course 1" course homepage with editing mode on + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test pdf annotation" + Then I should see "Test pdf annotation" diff --git a/tests/behat/annotate_pdfannotator.feature b/tests/behat/annotate_pdfannotator.feature new file mode 100644 index 0000000..d9573e7 --- /dev/null +++ b/tests/behat/annotate_pdfannotator.feature @@ -0,0 +1,110 @@ +@mod @mod_pdfannotator @_file_upload @javascript +Feature: Annotate in a pdfannotator activity + In order to annotate in the pdfannotator in a course + As a student + I need to note questions and subscribe or unsubscribe to notificatoins + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "user preferences" exist: + | user | preference | value | + | teacher1 | htmleditor | atto | + | student1 | htmleditor | atto | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a pdfannotator activity to course "Course 1" section "1" and I fill the form with: + | Name | Test PDF annotation | + | Description | Test pdf annotation description | + | Subscription mode | Optional subscription | + | Select a pdf-file | mod/pdfannotator/tests/fixtures/submission.pdf | + And I am on "Course 1" course homepage with editing mode on + And I log out + + Scenario: Add a question to a pdfannotator with optional subscription + Given I am on the "Test PDF annotation" "mod_pdfannotator > View" page logged in as "student1" + And I click on "comment" "button" + And I wait "1" seconds + And I point at the pdfannotator canvas + And I wait "1" seconds + And I set the field with xpath "//div[@id='id_pdfannotator_contenteditable']" to "This is a smurfing smurf" + And I click on "Create Annotation" "button" + And I wait until the page is ready + And I should see "This is a smurfing smurf" + And I click the pdfannotator public comment dropdown menu button + Then I should not see "Unsubscribe" + And I should see "Subscribe" + And I log out + + Scenario: Add a question to a pdfannotator with auto subscription + Given I am on the "Test PDF annotation" "mod_pdfannotator > Edit" page logged in as "teacher1" + And I set the following fields to these values: + | Subscription mode | Auto subscription | + And I press "Save" + And I log out + And I am on the "Test PDF annotation" "mod_pdfannotator > View" page logged in as "student1" + And I click on "comment" "button" + And I wait "1" seconds + And I point at the pdfannotator canvas + And I wait "1" seconds + And I set the field with xpath "//div[@id='id_pdfannotator_contenteditable']" to "This is a smurfing smurf" + And I click on "Create Annotation" "button" + And I wait until the page is ready + And I should see "This is a smurfing smurf" + And I click the pdfannotator public comment dropdown menu button + Then I should not see "Subscribe" + And I should see "Unsubscribe" + And I log out + + Scenario: Add a question to a pdfannotator with subscription disabled + Given I am on the "Test PDF annotation" "mod_pdfannotator > Edit" page logged in as "teacher1" + And I set the following fields to these values: + | Subscription mode | Subscription disabled | + And I press "Save" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test PDF annotation" + And I click on "comment" "button" + And I wait "1" seconds + And I point at the pdfannotator canvas + And I wait "1" seconds + And I set the field with xpath "//div[@id='id_pdfannotator_contenteditable']" to "This is a smurfing smurf" + And I click on "Create Annotation" "button" + And I wait until the page is ready + And I should see "This is a smurfing smurf" + And I click the pdfannotator public comment dropdown menu button + Then I should not see "Subscribe" + And I should not see "Unsubscribe" + And I log out + + Scenario: Add a question to a pdfannotator with forced subscription + Given I am on the "Test PDF annotation" "mod_pdfannotator > Edit" page logged in as "teacher1" + And I set the following fields to these values: + | Subscription mode | Forced subscription | + And I press "Save" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test PDF annotation" + And I click on "comment" "button" + And I wait "1" seconds + And I point at the pdfannotator canvas + And I wait "1" seconds + And I set the field with xpath "//div[@id='id_pdfannotator_contenteditable']" to "This is a smurfing smurf" + And I click on "Create Annotation" "button" + And I wait until the page is ready + And I should see "This is a smurfing smurf" + And I click the pdfannotator public comment dropdown menu button + Then I should not see "Subscribe" + And I should not see "Unsubscribe" + And I log out diff --git a/tests/behat/behat_mod_pdfannotator.php b/tests/behat/behat_mod_pdfannotator.php new file mode 100644 index 0000000..cebb6b8 --- /dev/null +++ b/tests/behat/behat_mod_pdfannotator.php @@ -0,0 +1,120 @@ +. + +/** + * Steps definitions related to mod_pdfannotator. + * + * @package mod_pdfannotator + * @category test + * @copyright 2019 HSR (http://www.hsr.ch) + * @author 2019 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use mod_pdfannotator\utils; +use Behat\Mink\Exception\ExpectationException as ExpectationException; +use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; +use Behat\Gherkin\Node\TableNode as TableNode; + +/** + * Steps definitions related to mod_pdfannotator. + * + * @package pdfannotator + * @category test + * @copyright 2024 Luca Bösch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_pdfannotator extends behat_base { + + /** + * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. + * + * Recognised page names are: + * | None so far! | | + * + * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_url(string $page): moodle_url { + switch ($page) { + default: + throw new Exception('Unrecognised pdfannotator page type "' . $page . '."'); + } + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | pagetype | name meaning | description | + * | View | Student Quiz name | The student quiz info page (view.php) | + * | Edit | Student Quiz name | The edit quiz page (edit.php) | + * | Statistics | Student Quiz name | The Statistics report page | + * | Ranking | Student Quiz name | The Ranking page | + * + * @param string $type identifies which type of page this is, e.g. 'View'. + * @param string $identifier identifies the particular page, e.g. 'Test student quiz'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { + switch ($type) { + case 'View': + return new moodle_url('/mod/pdfannotator/view.php', + ['id' => $this->get_cm_by_pdfannotator_name($identifier)->id]); + + case 'Edit': + return new moodle_url('/course/modedit.php', + ['update' => $this->get_cm_by_pdfannotator_name($identifier)->id]); + + case 'Statistics': + return new moodle_url('/mod/pdfannotator/reportstat.php', + ['id' => $this->get_cm_by_pdfannotator_name($identifier)->id]); + + case 'Ranking': + return new moodle_url('/mod/pdfannotator/reportrank.php', + ['id' => $this->get_cm_by_pdfannotator_name($identifier)->id]); + + default: + throw new Exception('Unrecognised pdfannotator page type "' . $type . '."'); + } + } + + /** + * Get a pdfannotator by name. + * + * @param string $name pdfannotator name. + * @return stdClass the corresponding DB row. + */ + protected function get_pdfannotator_by_name(string $name): stdClass { + global $DB; + return $DB->get_record('pdfannotator', array('name' => $name), '*', MUST_EXIST); + } + + /** + * Get cmid from the pdfannotator name. + * + * @param string $name pdfannotator name. + * @return stdClass cm from get_coursemodule_from_instance. + */ + protected function get_cm_by_pdfannotator_name(string $name): stdClass { + $pdfannotator = $this->get_pdfannotator_by_name($name); + return get_coursemodule_from_instance('pdfannotator', $pdfannotator->id, $pdfannotator->course); + } +} diff --git a/tests/behat/behat_pdfannotator_editpdf.php b/tests/behat/behat_pdfannotator_editpdf.php new file mode 100644 index 0000000..9e7a250 --- /dev/null +++ b/tests/behat/behat_pdfannotator_editpdf.php @@ -0,0 +1,59 @@ +. + +/** + * Behat pdfannotator-related steps definitions. + * + * @package pdfannotator + * @category test + * @copyright 2021 Luca Bösch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +/** + * Steps definitions related with the pdfannotator. + * + * @package pdfannotator + * @category test + * @copyright 2021 Luca Bösch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_pdfannotator_editpdf extends behat_base { + + /** + * Point at the pdfannotator pdf. + * + * @When /^I point at the pdfannotator canvas$/ + */ + public function i_point_at_the_pdfannotator_canvas() { + $node = $this->find('xpath', '//div[@id=\'pageContainer1\']'); + $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); + } + + /** + * Point at the pdfannotator pdf. + * + * @When /^I click the pdfannotator public comment dropdown menu button$/ + */ + public function i_click_the_pdfannotator_public_comment_dropdown_menu_button() { + $node = $this->find('xpath', '//a[@id=\'dropdownMenuButton\']'); + $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); + } +} diff --git a/tests/fixtures/submission.pdf b/tests/fixtures/submission.pdf new file mode 100644 index 0000000..576d378 Binary files /dev/null and b/tests/fixtures/submission.pdf differ