From 50fc1ec44977683b10d83028e2dcc156c6ac0ca9 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 24 Aug 2023 13:35:31 +0200 Subject: [PATCH 1/9] feat(submission): Teachers can now add submissions that participants can later annotate. The submission of an AnnoPy can now be viewed on the overview page. --- CHANGES.md | 5 + classes/local/helper.php | 67 ++++++++ classes/local/submissionstats.php | 136 +++++++++++++++ classes/output/annopy_view.php | 36 +++- db/install.xml | 1 + lang/de/annopy.php | 35 +++- lang/en/annopy.php | 35 +++- lib.php | 39 +---- styles.css | 179 +++++++++++++++++++- submit.php | 253 ++++++++++++++++++++++++++++ example_form.php => submit_form.php | 36 ++-- templates/annopy_view.mustache | 107 +++++++++++- version.php | 4 +- view.php | 91 ++-------- 14 files changed, 888 insertions(+), 136 deletions(-) create mode 100644 classes/local/helper.php create mode 100644 classes/local/submissionstats.php create mode 100644 submit.php rename example_form.php => submit_form.php (58%) diff --git a/CHANGES.md b/CHANGES.md index 70e95ef..4ac3448 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ ## Changelog ## + +- [0.1]: + - Teachers can now add submissions that participants can later annotate. + - The submission of an AnnoPy can now be viewed on the overview page. + - [0.0.1]: - Added plugin template files. - Added first capabilities, events and database structure. diff --git a/classes/local/helper.php b/classes/local/helper.php new file mode 100644 index 0000000..37a7d76 --- /dev/null +++ b/classes/local/helper.php @@ -0,0 +1,67 @@ +. + +/** + * Helper utilities for the module. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_annopy\local; + +/** + * Utility class for the module. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + /** + * Return the editor and attachment options for a submission. + * @param stdClass $course The course object. + * @param stdClass $context The context object. + + * @return array $editoroptions Array containing the editor options. + * @return array $attachmentoptions Array containing the attachment options. + */ + public static function annopy_get_editor_and_attachment_options($course, $context) { + // For the editor. + $editoroptions = array( + 'trusttext' => true, + 'maxfiles' => EDITOR_UNLIMITED_FILES, + 'maxbytes' => $course->maxbytes, + 'context' => $context, + 'subdirs' => false, + ); + + // If maxfiles would be set to an int and more files are given the editor saves them all but + // saves the overcouting incorrect so that white box is displayed. + + // For a file attachments field (not really needed here). + $attachmentoptions = array( + 'subdirs' => false, + 'maxfiles' => 1, + 'maxbytes' => $course->maxbytes + ); + + return array( + $editoroptions, + $attachmentoptions + ); + } +} diff --git a/classes/local/submissionstats.php b/classes/local/submissionstats.php new file mode 100644 index 0000000..cb93ae8 --- /dev/null +++ b/classes/local/submissionstats.php @@ -0,0 +1,136 @@ +. + +/** + * Stats utilities for AnnoPy submissions. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_annopy\local; + +use stdClass; +use core_text; + +/** + * Utility class for AnnoPy submission stats. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class submissionstats { + + /** + * Get the statistics for this submission. + * + * @param string $submissiontext The text for this submission. + * @param string $submissiontimecreated The time then the submission was created. + * @return array submissionstats Array with the statistics of the submission. + */ + public static function get_submission_stats($submissiontext, $submissiontimecreated) { + + $cleantext = preg_replace('#<[^>]+>#', ' ', $submissiontext, -1, $replacementspacescount); + + $submissionstats = array(); + $submissionstats['words'] = self::get_stats_words($cleantext); + $submissionstats['chars'] = self::get_stats_chars($cleantext) - $replacementspacescount; + $submissionstats['sentences'] = self::get_stats_sentences($cleantext); + $submissionstats['paragraphs'] = self::get_stats_paragraphs($cleantext); + $submissionstats['uniquewords'] = self::get_stats_uniquewords($cleantext); + $submissionstats['spaces'] = self::get_stats_spaces($cleantext) - $replacementspacescount; + $submissionstats['charswithoutspaces'] = $submissionstats['chars'] - $submissionstats['spaces']; + + $timenow = new \DateTime(date('Y-m-d G:i:s', time())); + $timesubmissioncreated = new \DateTime(date('Y-m-d G:i:s', $submissiontimecreated)); + + if ($timenow >= $timesubmissioncreated) { + $submissionstats['datediff'] = date_diff($timenow, $timesubmissioncreated); + } else { + $submissionstats['datediff'] = false; + } + + return $submissionstats; + } + + /** + * Get the character count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @ return int The number of characters. + */ + public static function get_stats_chars($submissiontext) { + return core_text::strlen($submissiontext); + } + + /** + * Get the word count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @ return int The number of words. + */ + public static function get_stats_words($submissiontext) { + return count_words($submissiontext); + } + + /** + * Get the sentence count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @ return int The number of sentences. + */ + public static function get_stats_sentences($submissiontext) { + $sentences = preg_split('/[!?.]+(?![0-9])/', $submissiontext); + $sentences = array_filter($sentences); + return count($sentences); + } + + /** + * Get the paragraph count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @ return int The number of paragraphs. + */ + public static function get_stats_paragraphs($submissiontext) { + $paragraphs = explode("\n", $submissiontext); + $paragraphs = array_filter($paragraphs); + return count($paragraphs); + } + + /** + * Get the unique word count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @return int The number of unique words. + */ + public static function get_stats_uniquewords($submissiontext) { + $items = core_text::strtolower($submissiontext); + $items = str_word_count($items, 1); + $items = array_unique($items); + return count($items); + } + + /** + * Get the raw spaces count statistics for this annopy submission. + * + * @param string $submissiontext The text for this submission. + * @return int The number of spaces. + */ + public static function get_stats_spaces($submissiontext) { + return substr_count($submissiontext, ' '); + } +} diff --git a/classes/output/annopy_view.php b/classes/output/annopy_view.php index eccad46..e87603c 100644 --- a/classes/output/annopy_view.php +++ b/classes/output/annopy_view.php @@ -23,6 +23,7 @@ */ namespace mod_annopy\output; +use mod_annopy\local\submissionstats; use renderable; use renderer_base; use templatable; @@ -39,13 +40,25 @@ class annopy_view implements renderable, templatable { /** @var int */ protected $cmid; + /** @var object */ + protected $course; + /** @var object */ + protected $context; + /** @var object */ + protected $submission; /** * Construct this renderable. * @param int $cmid The course module id + * @param object $course The course + * @param object $context The context + * @param object $submission The submission */ - public function __construct($cmid) { + public function __construct($cmid, $course, $context, $submission) { $this->cmid = $cmid; + $this->course = $course; + $this->context = $context; + $this->submission = $submission; } /** @@ -55,8 +68,29 @@ public function __construct($cmid) { * @return stdClass */ public function export_for_template(renderer_base $output) { + global $DB, $USER, $OUTPUT; + $data = new stdClass(); $data->cmid = $this->cmid; + $data->submission = $this->submission; + + if ($data->submission) { + // If submission can be edited. + $data->submission->canbeedited = has_capability('mod/annopy:editsubmission', $this->context); + + // Set submission user. + $data->submission->user = $DB->get_record('user', array('id' => $data->submission->author)); + $data->submission->user->userpicture = $OUTPUT->user_picture($data->submission->user, + array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); + + // Submission stats. + $data->submission->stats = submissionstats::get_submission_stats($data->submission->content, + $data->submission->timecreated); + $data->submission->canviewdetails = has_capability('mod/annopy:addsubmission', $this->context); + + } + + $data->canaddsubmission = has_capability('mod/annopy:addsubmission', $this->context); return $data; } } diff --git a/db/install.xml b/db/install.xml index 6bf00bd..1f6ad2a 100644 --- a/db/install.xml +++ b/db/install.xml @@ -41,6 +41,7 @@ + diff --git a/lang/de/annopy.php b/lang/de/annopy.php index 27dab60..244384b 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -38,9 +38,34 @@ $string['modulenameplural'] = 'AnnoPys'; $string['nonewmodules'] = 'Keine neuen Instanzen'; +// Strings for submit_form.php and submit.php. +$string['editsubmissionnotpossible'] = 'Bearbeiten der Einreichung fehlgeschlagen'; +$string['addsubmission'] = 'Einreichung anlegen'; +$string['editsubmission'] = 'Einreichung bearbeiten'; +$string['title'] = 'Titel'; +$string['submissioncontent'] = 'Inhalt der Einreichung'; +$string['submissioncreated'] = 'Einreichung angelegt'; +$string['submissionnotcreated'] = 'Einreichung konnte nicht angelegt werden'; +$string['submissionmodified'] = 'Einreichung aktualisiert'; +$string['submissionnotmodified'] = 'Einreichung konnte nicht aktualisiert werden'; +$string['submissionfaileddoubled'] = 'Einreichung konnte nicht angelegt werden da sie bereits existiert.'; + // Strings for the view page. $string['viewallannopys'] = 'Alle AnnoPy-Instanzen im Kurs ansehen'; $string['overview'] = 'Übersicht'; +$string['submission'] = 'Einreichung'; +$string['author'] = 'Autor'; +$string['timecreated'] = 'Zeitpunkt der Erstellung'; +$string['lastedited'] = 'Zuletzt bearbeitet'; +$string['currentversion'] = 'Versionsnummer'; +$string['details'] = 'Details'; +$string['numwordsraw'] = '{$a->wordscount} Wörter mit {$a->charscount} Zeichen, einschließlich {$a->spacescount} Leerzeichen.'; +$string['created'] = 'vor {$a->years} Jahren, {$a->month} Monaten, {$a->days} Tagen und {$a->hours} Stunden'; +$string['annotations'] = 'Annotationen'; +$string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; +$string['annotationsarefetched'] = 'Annotationen werden geladen'; +$string['reloadannotations'] = 'Annotationen neu laden'; +$string['nosubmission'] = 'Keine Einreichung'; // Strings for lib.php. $string['deletealluserdata'] = 'Alle Benutzerdaten löschen'; @@ -78,8 +103,14 @@ // Strings for the admin settings. -// Strings for the events. -$string['eventthingcreated'] = 'AnnoPy thing angelegt'; +// Strings for events. +$string['eventsubmissioncreated'] = 'Einreichung abgegeben'; +$string['eventsubmissionupdated'] = 'Einreichung aktualisiert'; + +// Strings for error messages. +$string['errfilloutfield'] = 'Bitte Feld ausfüllen'; +$string['incorrectcourseid'] = 'Inkorrekte Kurs-ID'; +$string['incorrectmodule'] = 'Inkorrekte Kurs-Modul-ID'; // Strings for the privacy api. /* diff --git a/lang/en/annopy.php b/lang/en/annopy.php index 86b3232..572a0c8 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -38,9 +38,34 @@ $string['modulenameplural'] = 'AnnoPys'; $string['nonewmodules'] = 'No new modules'; +// Strings for submit_form.php and submit.php. +$string['editsubmissionnotpossible'] = 'Editing submission failed'; +$string['addsubmission'] = 'Add submission'; +$string['editsubmission'] = 'Edit submission'; +$string['title'] = 'Title'; +$string['submissioncontent'] = 'Content of the submission'; +$string['submissioncreated'] = 'Submission created'; +$string['submissionnotcreated'] = 'Submission could not be created'; +$string['submissionmodified'] = 'Submission updated'; +$string['submissionnotmodified'] = 'Submission could not be updated'; +$string['submissionfaileddoubled'] = 'Submission could not be created because it already exists'; + // Strings for the view page. $string['viewallannopys'] = 'View all AnnoPy instances in the course'; $string['overview'] = 'Overview'; +$string['submission'] = 'Submission'; +$string['author'] = 'Author'; +$string['timecreated'] = 'Time created'; +$string['lastedited'] = 'Last edited'; +$string['currentversion'] = 'Current version'; +$string['details'] = 'Details'; +$string['numwordsraw'] = '{$a->wordscount} text words using {$a->charscount} characters, including {$a->spacescount} spaces.'; +$string['created'] = '{$a->years} years, {$a->month} months, {$a->days} days and {$a->hours} hours ago'; +$string['annotations'] = 'Annotations'; +$string['toggleallannotations'] = 'Toggle all annotations'; +$string['annotationsarefetched'] = 'Annotations being loaded'; +$string['reloadannotations'] = 'Reload annotations'; +$string['nosubmission'] = 'No submission'; // Strings for lib.php. $string['deletealluserdata'] = 'Delete all user data'; @@ -78,8 +103,14 @@ // Strings for the admin settings. -// Strings for the events. -$string['eventthingcreated'] = 'AnnoPy thing created'; +// Strings for events. +$string['eventsubmissioncreated'] = 'Submission created'; +$string['eventsubmissionupdated'] = 'Submission updated'; + +// Strings for error messages. +$string['errfilloutfield'] = 'Please fill out this field'; +$string['incorrectcourseid'] = 'Course ID is incorrect'; +$string['incorrectmodule'] = 'Course Module ID is incorrect'; // Strings for the privacy api. /* diff --git a/lib.php b/lib.php index bec5fc9..3ca9fb0 100644 --- a/lib.php +++ b/lib.php @@ -866,55 +866,32 @@ function annopy_get_file_info($browser, $areas, $course, $cm, $context, $fileare * @return bool false if file not found, does not return if found - just sends the file. */ function annopy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options = array()) { - global $DB, $CFG; + global $DB; if ($context->contextlevel != CONTEXT_MODULE) { - send_file_not_found(); - } - - require_login($course, true, $cm); - - if (! $course->visible && ! has_capability('moodle/course:viewhiddencourses', $context)) { return false; } - $areas = annopy_get_file_areas($course, $cm, $context); + require_course_login($course, true, $cm); - // Filearea must contain a real area. - if (!isset($areas[$filearea])) { + if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $context)) { return false; } - /* // Args[0] should be the entry id. - $entryid = intval(array_shift($args)); - $entry = $DB->get_record('annopy_entries', array( - 'id' => $entryid - ), 'id, userid', MUST_EXIST); + // Args[0] should be the submission id. + $submissionid = intval(array_shift($args)); - $canmanage = has_capability('mod/annopy:manageentries', $context); - if (! $canmanage && ! has_capability('mod/annopy:addentries', $context)) { - // Even if it is your own entry. - return false; - } - - // Students can only see their own entry. - if (! $canmanage && $USER->id !== $entry->userid) { - return false; - } - - if ($filearea !== 'entry' && $filearea !== 'feedback') { + if ($filearea !== 'submission') { return false; } $fs = get_file_storage(); $relativepath = implode('/', $args); - $fullpath = "/$context->id/mod_annopy/$filearea/$entryid/$relativepath"; + $fullpath = "/$context->id/mod_annopy/$filearea/$submissionid/$relativepath"; $file = $fs->get_file_by_hash(sha1($fullpath)); // Finally send the file. - send_stored_file($file, null, 0, $forcedownload, $options); */ - - send_file_not_found(); + send_stored_file($file, null, 0, $forcedownload, $options); } /** diff --git a/styles.css b/styles.css index 4470841..045b734 100644 --- a/styles.css +++ b/styles.css @@ -23,15 +23,182 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -/* -.path-mod-annopy .div { +.path-mod-annopy .details { margin: 5px; + padding-top: 10px; text-align: center; + font-size: 0.7em; + + border-top: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.path-mod-annopy .submissionsheader { + margin-bottom: 10px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.path-mod-annopy .submissionsheader h4 { + margin-bottom: 5px; + margin-top: 5px; +} + +.path-mod-annopy .submissionfooter { + width: 100%; +} + +.path-mod-annopy .submission { + text-align: left; + font-size: 1em; + padding: 10px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.path-mod-annopy .annotationsheader { + margin-bottom: 10px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.path-mod-annopy .annotationsheader h4 { + margin-bottom: 5px; + margin-top: 5px; +} + +.path-mod-annopy .annotationarea { + text-align: left; + font-size: 1em; + padding: 10px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.path-mod-annopy .annotated, +.path-mod-annopy .annotated_temp { + background-color: yellow; + cursor: pointer; + -webkit-print-color-adjust: exact; +} + +.path-mod-annopy .hovered .annotated, +.path-mod-annopy .hovered .annotated_temp { + background: none; +} + +.path-mod-annopy .annotated:hover, +.path-mod-annopy .annotated_temp:hover, +.path-mod-annopy .hovered, +.path-mod-annopy .errortypeheader .hovered { + background-color: lightblue; +} + +.path-mod-annopy .annotation-box { + border: 1px solid #dbdbdb; + border-radius: 2px; + padding: 10px; + margin-bottom: 10px; + box-shadow: 0 1px 1px rgba(0, 0, 0, .1); +} + +.path-mod-annopy .annotation-box:hover { + box-shadow: 0 2px 3px 0 rgba(0, 0, 0, .15); +} + +.path-mod-annopy #id_submitbutton { + margin-right: 5px; +} + +.path-mod-annopy textarea { + width: 100%; +} + +.path-mod-annopy-error_summary th { + min-width: 135px; +} + +.path-mod-annopy .annotatedtextpreviewdiv { + margin-top: 5px; + margin-bottom: 5px; +} + +.path-mod-annopy .annotationauthor { + padding-top: 5px; + padding-bottom: 5px; + margin-top: 10px; +} + +.path-mod-annopy .annotatedtextpreview { + border-left: 5px solid yellow; + padding-left: 5px; + background-color: white; + display: inline-block; + width: 100%; +} + +.path-mod-annopy .annopy-btn-round-small { + width: 1.6rem; + height: 1.6rem; + border-radius: 50%; + padding: 0; + margin-right: 5px; + margin-bottom: 5px; +} + +.path-mod-annopy .annotation-form { + display: none; +} + +.path-mod-annopy #overlay { + position: fixed; /* Sit on bottom of the display */ + height: 50px; + width: 100%; + left: 0%; + bottom: 0%; + opacity: 0.9; + z-index: 2; +} + +.path-mod-annopy #overlaytext { + position: absolute; + top: 50%; + left: 50%; + padding: 5px; + font-weight: bold; + color: darkred; + background: white; + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); +} + +.path-mod-annopy .annotationareawidth, +.path-mod-annopy .submissionareawidth { + width: 50%; +} + +.path-mod-annopy .submissionbackground { + background-color: rgba(2, 49, 83, 0.07); +} + +.path-mod-annopy .textbackground { + background-color: white; } @media print { - .path-mod-annopy .div { - display: none; + .path-mod-annopy #page, + .path-mod-annopy #page.drawers, + .path-mod-annopy #page.drawers .main-inner { + margin-top: 0; } -} - */ +} \ No newline at end of file diff --git a/submit.php b/submit.php new file mode 100644 index 0000000..2d71b15 --- /dev/null +++ b/submit.php @@ -0,0 +1,253 @@ +. + +/** + * The page for submitting in mod_annopy. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\output\notification; +use mod_annopy\local\helper; + +require(__DIR__.'/../../config.php'); +require_once('./submit_form.php'); + +global $DB; + +// Course Module ID. +$id = required_param('id', PARAM_INT); + +// Module instance ID as alternative. +$a = optional_param('a', null, PARAM_INT); + +// ID of the submission to be edited (if existing). +$submissionid = optional_param('submissionid', '0', PARAM_INT); + +// Set the basic variables $course, $cm and $moduleinstance. +if ($id) { + [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); + $moduleinstance = $DB->get_record('annopy', ['id' => $cm->instance], '*', MUST_EXIST); +} else { + throw new moodle_exception('missingparameter'); +} + +if (!$cm) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} else if (!$course) { + throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); +} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); + +require_capability('mod/annopy:addsubmission', $context); + +$data = new stdClass(); +$data->id = $cm->id; + +// Get submission that should be edited. +if ($DB->record_exists('annopy_submissions', array('annopy' => $moduleinstance->id))) { + $submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); + + // Prevent editing of submissions not started by this user. + if ($submission->author != $USER->id) { + redirect(new moodle_url('/mod/annopy/view.php?id=' . $cm->id), get_string('editsubmissionnotpossible', 'mod_annopy'), + null, notification::NOTIFY_ERROR); + } + + $data->submissionid = $submission->id; + $data->timecreated = time(); + $data->submission = $submission->content; + $data->submissionformat = $submission->format; + $data->title = $submission->title; + + $title = get_string('editsubmission', 'mod_annopy'); + +} else { + $submission = false; + + $data->submissionid = null; + $data->timecreated = time(); + $data->submission = ''; + $data->submissionformat = FORMAT_HTML; + $data->title = get_string('submission', 'mod_annopy'); + + $title = get_string('addsubmission', 'mod_annopy'); +} + +list ($editoroptions, $attachmentoptions) = helper::annopy_get_editor_and_attachment_options($course, $context, $moduleinstance); + +$data = file_prepare_standard_editor($data, 'submission', $editoroptions, $context, + 'mod_annopy', 'submission', $data->submissionid); +$data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, + 'mod_annopy', 'attachment', $data->submissionid); + +// Create form. +$form = new mod_annopy_submit_form(null, array('editoroptions' => $editoroptions)); + +// Set existing data for this submission. +$form->set_data($data); + +if ($form->is_cancelled()) { + redirect($CFG->wwwroot . '/mod/annopy/view.php?id=' . $cm->id); +} else if ($fromform = $form->get_data()) { + + global $DB; + + if (isset($fromform->submissionid)) { + + if ($fromform->submissionid !== 0) { // Update existing submission. + // Get existing submission. + $submission = $DB->get_record('annopy_submissions', + array('annopy' => $moduleinstance->id, 'id' => $fromform->submissionid)); + + // Set new version and time modified. + $submission->currentversion += 1; + $submission->timemodified = time(); + + // Set editor for plugin files. + $fromform = file_postupdate_standard_editor($fromform, 'submission', $editoroptions, $editoroptions['context'], + 'mod_annopy', 'submission', $submission->id); + + $submissiontext = file_rewrite_pluginfile_urls($fromform->submission, 'pluginfile.php', $context->id, + 'mod_annopy', 'submission', $submission->id); + + // Set submission title, content and format. + $submission->title = format_text($fromform->title, 1, array('para' => false)); + + $submission->content = format_text($submissiontext, + $fromform->submission_editor['format'], array('para' => false)); + + $submission->format = (int) $fromform->submission_editor['format']; + + // Update submission. + $updated = $DB->update_record('annopy_submissions', $submission); + + if ($updated) { + // Trigger submission updated event. + $event = \mod_annopy\event\submission_updated::create(array( + 'objectid' => $submission->id, + 'context' => $context + )); + $event->trigger(); + + redirect(new moodle_url('/mod/annopy/view.php', array('id' => $id)), + get_string('submissionmodified', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + + } else { + redirect(new moodle_url('/mod/annopy/view.php', array('id' => $id)), + get_string('submissionnotmodified', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + } else if ($fromform->submissionid === 0) { // New submission. + if (!$DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id))) { // No submission made yet. + + // Create new submission object. + $submission = new stdClass(); + $submission->annopy = (int) $moduleinstance->id; + $submission->author = $USER->id; + + // Set new version, time created, and modified. + $submission->currentversion = 1; + $submission->timecreated = time(); + $submission->timemodified = null; + + // Set submission title, content and format. + $submission->title = format_text($fromform->title, 1, array('para' => false)); + $submission->content = ''; + $submission->format = 1; + + // Save submission. + $submission->id = $DB->insert_record('annopy_submissions', $submission); + + // Set editor for plugin files. + $fromform = file_postupdate_standard_editor($fromform, 'submission', $editoroptions, $editoroptions['context'], + 'mod_annopy', 'submission', $submission->id); + + $submissiontext = file_rewrite_pluginfile_urls($fromform->submission, 'pluginfile.php', + $context->id, 'mod_annopy', 'submission', $submission->id); + + // Set submission text and format. + $submission->content = format_text($submissiontext, + $fromform->submission_editor['format'], array('para' => false)); + $submission->format = (int) $fromform->submission_editor['format']; + + // Update submission with formatted content. + $updated = $DB->update_record('annopy_submissions', $submission); + + if ($updated) { + // Trigger submission created event. + $event = \mod_annopy\event\submission_created::create(array( + 'objectid' => $submission->id, + 'context' => $context + )); + $event->trigger(); + + redirect(new moodle_url('/mod/annopy/view.php', + array('id' => $id)), + get_string('submissioncreated', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + + } else { + redirect(new moodle_url('/mod/annopy/view.php', + array('id' => $id)), + get_string('submissionnotcreated', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + } else { + redirect(new moodle_url('/mod/annopy/view.php', + array('id' => $id)), + get_string('submissionfaileddoubled', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } + } + +} + +// Get the name for this activity. +$modulename = format_string($moduleinstance->name, true, array( + 'context' => $context +)); + +$PAGE->set_url('/mod/annopy/submit.php', array('id' => $id)); +$PAGE->navbar->add($title); +$PAGE->set_title($modulename . ' - ' . $title); +$PAGE->set_heading($course->fullname); +if ($CFG->branch < 400) { + $PAGE->force_settings_menu(); +} + +echo $OUTPUT->header(); + +if ($CFG->branch < 400) { + echo $OUTPUT->heading($modulename); + + if ($moduleinstance->intro) { + echo $OUTPUT->box(format_module_intro('annopy', $moduleinstance, $cm->id), 'generalbox', 'intro'); + } +} + +echo $OUTPUT->heading($title, 4); + +// Display the form for adding or editing the submission. +$form->display(); + +echo $OUTPUT->footer(); diff --git a/example_form.php b/submit_form.php similarity index 58% rename from example_form.php rename to submit_form.php index 14a283d..10d979f 100644 --- a/example_form.php +++ b/submit_form.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * File containing the class definition for the annopy example form. + * File containing the class definition for the submit form for the module. * * @package mod_annopy * @copyright 2023 coactum GmbH @@ -28,16 +28,16 @@ require_once("$CFG->libdir/formslib.php"); /** - * Form for the example to annopy. + * Form for the submissions. * * @package mod_annopy * @copyright 2023 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ -class mod_annopy_example_form extends moodleform { +class mod_annopy_submit_form extends moodleform { /** - * Define the form - called by parent constructor. + * Define the form - called by parent constructor */ public function definition() { @@ -48,17 +48,27 @@ public function definition() { $mform->addElement('hidden', 'id', null); $mform->setType('id', PARAM_INT); - $mform->addElement('hidden', 'user', null); - $mform->setType('user', PARAM_INT); + $mform->addElement('hidden', 'group', null); + $mform->setType('group', PARAM_INT); - // $mform->addElement('text', 'username', get_string('annopyusername', 'mod_annopy')); - // $mform->addHelpButton('username', 'annopyusername', 'mod_annopy'); - // $mform->setType('username', PARAM_TEXT); - // $mform->addRule('username', null, 'required', null, 'client'); + $mform->addElement('hidden', 'userid', null); + $mform->setType('userid', PARAM_INT); - // $mform->addElement('static', 'signupforannopy', get_string('noannopyyet', 'mod_annopy'), - // '' . - // get_string('registerforannopy', 'mod_annopy') . ''); + $mform->addElement('hidden', 'submissionid', null); + $mform->setType('submissionid', PARAM_INT); + + $mform->addElement('text', 'title', get_string('title', 'mod_annopy'), array('size' => '64')); + + $mform->setType('title', PARAM_TEXT); + + $mform->addRule('title', null, 'required', null, 'client'); + $mform->addRule('title', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + + $mform->addElement('editor', 'submission_editor', get_string('submissioncontent', 'mod_annopy'), + null, $this->_customdata['editoroptions']); + + $mform->setType('submission_editor', PARAM_RAW); + $mform->addRule('submission_editor', get_string('errfilloutfield', 'mod_annopy'), 'required', 'client'); $this->add_action_buttons(); } diff --git a/templates/annopy_view.mustache b/templates/annopy_view.mustache index 5da8ce9..febf23f 100644 --- a/templates/annopy_view.mustache +++ b/templates/annopy_view.mustache @@ -27,5 +27,110 @@ {{#js}} {{/js}} -
+ +
+ +

{{#str}}overview, mod_annopy{{/str}}

+ + {{#submission}}{{#submission.canbeedited}} + {{#str}}editsubmission, mod_annopy{{/str}} + {{/submission.canbeedited}}{{/submission}} + {{^submission}}{{#canaddsubmission}} + {{#str}}addsubmission, mod_annopy{{/str}} + {{/canaddsubmission}}{{/submission}} + +
+ {{#submission}} +
+
+

+ {{#str}}submission, mod_annopy{{/str}} +

+
+
+

+ {{#str}} annotations, mod_annopy {{/str}} + {{#annotations.0}}{{/annotations.0}} +

+ {{#annotations.0}}
{{#str}}annotationsarefetched, mod_annopy{{/str}}
{{/annotations.0}} +
+
+ +
+
+
+
+ {{title}} +
+ + {{#content}} +
{{{content}}}
+ {{/content}} + {{^content}}

{{#str}}nosubmission, mod_annopy{{/str}}

{{/content}} + {{#canviewdetails}}
+ {{#user}}{{#userpicture}}{{#str}}author, mod_annopy{{/str}}: {{{userpicture}}}
{{/userpicture}}{{/user}} + {{#str}}timecreated, mod_annopy{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_annopy, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}}){{/datediff}}{{/stats}} + + {{#timemodified}}
{{#str}}lastedited, mod_annopy {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/timemodified}} +
{{#str}}currentversion, mod_annopy {{/str}}: {{currentversion}} + {{#stats}} +
+ {{#str}}details, mod_annopy{{/str}}: {{#str}}numwordsraw, mod_annopy, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} + {{/stats}} +
{{/canviewdetails}} +
+ +
+ {{#annotations}} +
+
+ + {{style}} + + +
+
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#exact}}{{exact}}{{/exact}}{{^exact}}{{#str}}annotatedtextnotfound, mod_annopy {{/str}} {{/exact}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}} +
+
+ {{/annotations}} + + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_annopy {{/str}} +
+ {{{annotationform}}} +
+ {{/annotationform}} +
+
+
+
+
+ {{/submission}} + {{^submission}} + {{#str}}nosubmission, mod_annopy{{/str}} + {{/submission}} +
+
\ No newline at end of file diff --git a/version.php b/version.php index dfee5c8..3251d1d 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.0.1'; -$plugin->version = 2023082300; +$plugin->release = '0.1.0'; +$plugin->version = 2023082400; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; diff --git a/view.php b/view.php index 54a3472..9668b97 100644 --- a/view.php +++ b/view.php @@ -39,6 +39,14 @@ throw new moodle_exception('missingparameter'); } +if (!$cm) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} else if (!$course) { + throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); +} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} + require_login($course, true, $cm); $context = context_module::instance($cm->id); @@ -69,7 +77,7 @@ $PAGE->set_url('/mod/annopy/view.php', array('id' => $cm->id)); } */ -$PAGE->requires->js_call_amd('mod_annopy/view', 'init', array('cmid' => $cm->id)); +// $PAGE->requires->js_call_amd('mod_annopy/view', 'init', array('cmid' => $cm->id)); $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -85,63 +93,6 @@ echo $OUTPUT->header(); -// Check if new session should be saved. -require_once($CFG->dirroot . '/mod/annopy/example_form.php'); - -// Instantiate form. -$mform = new mod_annopy_example_form(null, array('things' => 123)); - -if ($fromform = $mform->get_data()) { - - // In this case you process validated data. $mform->get_data() returns data posted in form. - if (isset($fromform->itemid)) { // Create new item. - - $item = new stdClass(); - $session->annopy = (int) $cm->instance; - $session->userid = (int) $USER->id; - $session->timecreated = time(); - $session->property1 = $fromform->property1; - - $newitemnid = $DB->insert_record('annopy_items', $item); - - // Trigger annopy session login successfull event. - $event = \mod_annopy\event\thing_created::create(array( - 'objectid' => $newitemnid, - 'context' => $context - )); - - $event->trigger(); - - $urlparams = array('id' => $id); - $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); - - // redirect($redirecturl, get_string('creationsuccessfull', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); - - } else { // Update item. - $thing = $DB->get_record('annopy_things', array('thing' => $cm->instance, 'id' => $fromform->itemid)); - - $thing->timemodified = time(); - $thing->text = format_text($fromform->text, 2, array('para' => false)); - $thing->type = $fromform->type; - - $DB->update_record('annopy_things', $thing); - - // Trigger annopy session login failed event. - $event = \mod_annopy\event\thing_updated::create(array( - 'objectid' => (int) $USER->id, - 'context' => $context - )); - - $event->trigger(); - - $urlparams = array('id' => $id); - $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); - - redirect($redirecturl, get_string('thingupdated', 'mod_annopy'), null, notification::NOTIFY_ERROR); - - } -} - if ($CFG->branch < 400) { echo $OUTPUT->heading($modulename); @@ -150,32 +101,16 @@ } } -// Get grading of current user when annopy is rated. -/* if ($moduleinstance->assessed != 0) { - $ratingaggregationmode = helper::get_annopy_aggregation($moduleinstance->assessed) . ' ' . - get_string('forallmyentries', 'mod_annopy'); - $gradinginfo = grade_get_grades($course->id, 'mod', 'annopy', $moduleinstance->id, $USER->id); - $userfinalgrade = $gradinginfo->items[0]->grades[$USER->id]; - $currentuserrating = $userfinalgrade->str_long_grade; -} else { - $ratingaggregationmode = false; - $currentuserrating = false; -} */ - // Handle groups. echo groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/annopy/view.php?id=$id"); +// Get submission for the module. +$submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); + // Render and output page. -$page = new annopy_view($cm); +$page = new annopy_view($cm->id, $course, $context, $submission); echo $OUTPUT->render($page); -$mform = new mod_annopy_example_form(new moodle_url('/mod/annopy/view.php', array('id' => $cm->id))); - -// Set default data. -$mform->set_data(array('id' => $cm->id, 'username' => $USER->username)); - -echo $mform->render(); - // Output footer. echo $OUTPUT->footer(); From db70adac3fe0a03a25c45b8613e192b206d2eb2f Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 24 Aug 2023 18:23:11 +0200 Subject: [PATCH 2/9] feat(annotations): Users can now add, edit and delete annotations in submissions. --- CHANGES.md | 3 + amd/build/annotations.min.js | 10 + amd/build/annotations.min.js.map | 1 + amd/build/highlighting.min.js | 3 + amd/build/highlighting.min.js.map | 1 + amd/build/match-quote.min.js | 3 + amd/build/match-quote.min.js.map | 1 + amd/build/string-match.min.js | 3 + amd/build/string-match.min.js.map | 1 + amd/build/text-range.min.js | 3 + amd/build/text-range.min.js.map | 1 + amd/build/types.min.js | 3 + amd/build/types.min.js.map | 1 + amd/build/view.min.js | 0 amd/build/view.min.js.map | 0 amd/build/xpath.min.js | 3 + amd/build/xpath.min.js.map | 1 + amd/src/annotations.js | 308 ++++++++++++++++++++ amd/src/highlighting.js | 459 ++++++++++++++++++++++++++++++ amd/src/match-quote.js | 174 +++++++++++ amd/src/string-match.js | 339 ++++++++++++++++++++++ amd/src/text-range.js | 346 ++++++++++++++++++++++ amd/src/types.js | 262 +++++++++++++++++ amd/src/view.js | 35 --- amd/src/xpath.js | 195 +++++++++++++ annotation_form.php | 110 +++++++ annotations.php | 227 +++++++++++++++ classes/local/helper.php | 123 ++++++++ classes/output/annopy_view.php | 20 +- db/access.php | 14 +- db/install.xml | 30 +- lang/de/annopy.php | 33 ++- lang/en/annopy.php | 35 ++- settings.php | 12 - styles.css | 7 +- templates/annopy_view.mustache | 4 +- version.php | 4 +- view.php | 9 +- 38 files changed, 2685 insertions(+), 99 deletions(-) create mode 100644 amd/build/annotations.min.js create mode 100644 amd/build/annotations.min.js.map create mode 100644 amd/build/highlighting.min.js create mode 100644 amd/build/highlighting.min.js.map create mode 100644 amd/build/match-quote.min.js create mode 100644 amd/build/match-quote.min.js.map create mode 100644 amd/build/string-match.min.js create mode 100644 amd/build/string-match.min.js.map create mode 100644 amd/build/text-range.min.js create mode 100644 amd/build/text-range.min.js.map create mode 100644 amd/build/types.min.js create mode 100644 amd/build/types.min.js.map delete mode 100644 amd/build/view.min.js delete mode 100644 amd/build/view.min.js.map create mode 100644 amd/build/xpath.min.js create mode 100644 amd/build/xpath.min.js.map create mode 100644 amd/src/annotations.js create mode 100644 amd/src/highlighting.js create mode 100644 amd/src/match-quote.js create mode 100644 amd/src/string-match.js create mode 100644 amd/src/text-range.js create mode 100644 amd/src/types.js delete mode 100644 amd/src/view.js create mode 100644 amd/src/xpath.js create mode 100644 annotation_form.php create mode 100644 annotations.php diff --git a/CHANGES.md b/CHANGES.md index 4ac3448..951d851 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,8 @@ ## Changelog ## +- [0.2]: + - Users can now add, edit and delete annotations in submissions. + - [0.1]: - Teachers can now add submissions that participants can later annotate. - The submission of an AnnoPy can now be viewed on the overview page. diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js new file mode 100644 index 0000000..9f47ee3 --- /dev/null +++ b/amd/build/annotations.min.js @@ -0,0 +1,10 @@ +define("mod_annopy/annotations",["exports","jquery","./highlighting"],(function(_exports,_jquery,_highlighting){var obj; +/** + * Module for the annotation functions of the module. + * + * @module mod_annopy/annotations + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=(cmid,canaddannotation,myuserid,focusannotation)=>{var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canaddannotation&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var submission=annotations[annotationid].submission;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(annotations[annotationid].annotationstart),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(annotations[annotationid].annotationend),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+submission+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+submission+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+submission).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+submission+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click",".annopy_submission #id_cancel",(function(e){e.preventDefault(),(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1})),(0,_jquery.default)(".annopy_submission textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canaddannotation){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){const ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;const rangeSelectors=ranges.map((range=>(0,_highlighting.describe)(root,range))),annotation={target:rangeSelectors.map((selectors=>({selector:selectors})))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var submission=this.id.replace(/submission-/,"");(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+submission+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+submission+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1},success:function(response){annotations=JSON.parse(response),function(){for(let annotation of Object.values(annotations)){const newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.annotationstart),end:parseInt(annotation.annotationend)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((selectors=>({selector:selectors})))};(0,_highlighting.anchor)(newannotation,(0,_jquery.default)("#submission-"+annotation.submission)[0])}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))})),(0,_jquery.default)(document).on("mouseover",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),0!=focusannotation&&((0,_jquery.default)(".annotated-"+focusannotation).attr("tabindex",-1),(0,_jquery.default)(".annotated-"+focusannotation).focus())},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){}})}})); + +//# sourceMappingURL=annotations.min.js.map \ No newline at end of file diff --git a/amd/build/annotations.min.js.map b/amd/build/annotations.min.js.map new file mode 100644 index 0000000..c1edb9b --- /dev/null +++ b/amd/build/annotations.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for the annotation functions of the module.\n *\n * @module mod_annopy/annotations\n * @copyright 2023 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canaddannotation, myuserid, focusannotation) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '.annopy_submission #id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('.annopy_submission textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canaddannotation) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var submission = this.id.replace(/submission-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + submission + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\n\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotation-form-' + submission + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1},\n success: function(response) {\n annotations = JSON.parse(response);\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).addClass('hovered');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Highlight annotation if hoverannotation button is hovered\n $(document).on('mouseover', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n\n // Focus annotation if needed.\n if (focusannotation != 0) {\n $('.annotated-' + focusannotation).attr('tabindex', -1);\n $('.annotated-' + focusannotation).focus();\n }\n\n },\n complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n // For output: alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.annotationstart),\n end: parseInt(annotation.annotationend)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#submission-\" + annotation.submission)[0]);\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canaddannotation && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var submission = annotations[annotationid].submission;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(annotations[annotationid].annotationstart);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(annotations[annotationid].annotationend);\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\n\n $('.annotation-form-' + submission + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + submission + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + submission + ' select').val(annotations[annotationid].type);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\n $('#annotationpreview-temp-' + submission).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + submission + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotationarea-' + submission + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n};\n\n/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["obj","_jquery","__esModule","default","_exports","init","cmid","canaddannotation","myuserid","focusannotation","edited","annotations","Array","newannotation","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","submission","$","hide","val","startcontainer","endcontainer","startoffset","endoffset","annotationstart","annotationend","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","rangeSelectors","map","range","describe","annotation","target","selectors","selector","anchor","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","start","end","ajax","url","data","getannotations","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","attr","complete","error"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/highlighting.min.js b/amd/build/highlighting.min.js new file mode 100644 index 0000000..73e547f --- /dev/null +++ b/amd/build/highlighting.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/highlighting",["exports","jquery","./types","./text-range"],(function(_exports,_jquery,_types,_textRange){var obj;function highlightRange(range){let annotationid=arguments.length>1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00";const textNodes=function(range){if(range.collapsed)return[];let root=range.commonAncestorContainer;root.nodeType!==Node.ELEMENT_NODE&&(root=root.parentElement);if(!root)return[];const textNodes=[],nodeIter=root.ownerDocument.createNodeIterator(root,NodeFilter.SHOW_TEXT);let node;for(;node=nodeIter.nextNode();){if(!isNodeInRange(range,node))continue;let text=node;text===range.startContainer&&range.startOffset>0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset{prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));const whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((span=>span.some((node=>!whitespace.test(node.nodeValue)))));const highlights=[];return textNodeSpans.forEach((nodes=>{const highlightEl=document.createElement("annopy-highlight");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color);nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((node=>highlightEl.appendChild(node))),highlights.push(highlightEl)})),highlights}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue;const length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function querySelector(anchor){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return anchor.toRange(options)}function replaceWith(node,replacements){const parent=node.parentNode;replacements.forEach((r=>parent.insertBefore(r,node))),node.remove()}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.anchor=function(annotation,root){const highlight=anchor=>{const range=function(anchor){if(!anchor.range)return null;try{return anchor.range.toRange()}catch{return null}}(anchor);if(!range)return;let highlights=[];highlights=annotation.annotation?highlightRange(range,annotation.annotation.id,"annotated",annotation.annotation.color):highlightRange(range,!1,"annotated_temp"),highlights.forEach((h=>{h._annotation=anchor.annotation})),anchor.highlights=highlights};annotation.target||(annotation.target=[]);const anchors=annotation.target.map((target=>{if(!target.selector||!target.selector.some((s=>"TextQuoteSelector"===s.type)))return{annotation:annotation,target:target};let anchor;try{const range=function(root,selectors){let options=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},position=null,quote=null,range=null;for(let selector of selectors)switch(selector.type){case"TextPositionSelector":position=selector,options.hint=position.start;break;case"TextQuoteSelector":quote=selector;break;case"RangeSelector":range=selector}const maybeAssertQuote=range=>{var _quote;if(null!==(_quote=quote)&&void 0!==_quote&&_quote.exact&&range.toString()!==quote.exact)throw new Error("quote mismatch");return range};let queryselector=!1;try{if(range)return queryselector=querySelector(_types.RangeAnchor.fromSelector(root,range),options),queryselector||maybeAssertQuote}catch(error){try{if(position)return queryselector=querySelector(_types.TextPositionAnchor.fromSelector(root,position),options),queryselector||maybeAssertQuote}catch(error){try{if(quote)return queryselector=querySelector(_types.TextQuoteAnchor.fromSelector(root,quote),options),queryselector}catch(error){return!1}}}return!1}(root,target.selector),textRange=_textRange.TextRange.fromRange(range);anchor={annotation:annotation,target:target,range:textRange}}catch(err){anchor={annotation:annotation,target:target}}return anchor}));for(let anchor of anchors)highlight(anchor);return annotation.$orphan=anchors.length>0&&anchors.every((anchor=>anchor.target.selector&&!anchor.range)),anchors},_exports.describe=function(root,range){const types=[_types.RangeAnchor,_types.TextPositionAnchor,_types.TextQuoteAnchor],result=[];for(let type of types)try{const anchor=type.fromRange(root,range);result.push(anchor.toSelector())}catch(error){continue}return result},_exports.removeAllTempHighlights=function(){const highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i {\n\n // Only annotations with an associated quote can currently be anchored.\n // This is because the quote is used to verify anchoring with other selector\n // types.\n if (\n !target.selector ||\n !target.selector.some(s => s.type === 'TextQuoteSelector')\n ) {\n return {annotation, target};\n }\n\n /** @type {Anchor} */\n let anchor;\n try {\n const range = htmlAnchor(root, target.selector);\n // Convert the `Range` to a `TextRange` which can be converted back to\n // a `Range` later. The `TextRange` representation allows for highlights\n // to be inserted during anchoring other annotations without \"breaking\"\n // this anchor.\n\n\n const textRange = TextRange.fromRange(range);\n\n anchor = {annotation, target, range: textRange};\n\n } catch (err) {\n\n anchor = {annotation, target};\n }\n\n return anchor;\n };\n\n /**\n * Highlight the text range that `anchor` refers to.\n *\n * @param {Anchor} anchor\n */\n const highlight = anchor => {\n\n const range = resolveAnchor(anchor);\n\n if (!range) {\n return;\n }\n\n let highlights = [];\n\n if (annotation.annotation) {\n highlights = highlightRange(range, annotation.annotation.id, 'annotated', annotation.annotation.color);\n } else {\n highlights = highlightRange(range, false, 'annotated_temp');\n }\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n };\n\n // Remove existing anchors for this annotation.\n // this.detach(annotation, false /* notify */); // To be replaced by own method\n\n // Resolve selectors to ranges and insert highlights.\n if (!annotation.target) {\n annotation.target = [];\n }\n const anchors = annotation.target.map(locate);\n\n for (let anchor of anchors) {\n\n highlight(anchor);\n }\n\n // Set flag indicating whether anchoring succeeded. For each target,\n // anchoring is successful either if there are no selectors (ie. this is a\n // Page Note) or we successfully resolved the selectors to a range.\n annotation.$orphan =\n anchors.length > 0 &&\n anchors.every(anchor => anchor.target.selector && !anchor.range);\n\n return anchors;\n}\n\n/**\n * Resolve an anchor's associated document region to a concrete `Range`.\n *\n * This may fail if anchoring failed or if the document has been mutated since\n * the anchor was created in a way that invalidates the anchor.\n *\n * @param {Anchor} anchor\n * @return {Range|null}\n */\nfunction resolveAnchor(anchor) {\n\n if (!anchor.range) {\n return null;\n }\n try {\n return anchor.range.toRange();\n } catch {\n return null;\n }\n}\n\n/**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * Modified for handling annotations.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n const highlights = /** @type {HighlightElement[]} */ ([]);\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('annopy-highlight');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n const parent = /** @type {Node} */ (nodes[0].parentNode);\n parent.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n highlights.push(highlightEl);\n\n });\n\n return highlights;\n}\n\n/**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n}\n\n/**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\nfunction isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n}\n\n/**\n * @param {RangeAnchor|TextPositionAnchor|TextQuoteAnchor} anchor\n * @param {Object} [options]\n * @return {obj} - range\n */\n function querySelector(anchor, options = {}) {\n\n return anchor.toRange(options);\n}\n\n/**\n * Anchor a set of selectors.\n *\n * This function converts a set of selectors into a document range.\n * It encapsulates the core anchoring algorithm, using the selectors alone or\n * in combination to establish the best anchor within the document.\n *\n * @param {Element} root - The root element of the anchoring context.\n * @param {Selector[]} selectors - The selectors to try.\n * @param {Object} [options]\n * @return {object} the query selector\n */\n function htmlAnchor(root, selectors, options = {}) {\n let position = null;\n let quote = null;\n let range = null;\n\n // Collect all the selectors\n for (let selector of selectors) {\n switch (selector.type) {\n case 'TextPositionSelector':\n position = selector;\n options.hint = position.start; // TextQuoteAnchor hint\n break;\n case 'TextQuoteSelector':\n quote = selector;\n break;\n case 'RangeSelector':\n range = selector;\n break;\n }\n }\n\n /**\n * Assert the quote matches the stored quote, if applicable\n * @param {Range} range\n * @return {Range} range\n */\n const maybeAssertQuote = range => {\n\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n return queryselector;\n }\n } catch (error) {\n return false;\n }\n }\n }\n return false;\n}\n\n/**\n * Remove all temporary highlights under a given root element.\n */\n export function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n}\n\n/**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n }\n }\n}\n\n/**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\nfunction replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n}"],"names":["obj","highlightRange","range","annotationid","arguments","length","undefined","cssClass","color","textNodes","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","node","nextNode","isNodeInRange","text","startContainer","startOffset","splitText","endContainer","endOffset","data","push","wholeTextNodesInRange","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","highlights","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","parentNode","replaceChild","appendChild","_node$nodeValue$lengt","_node$nodeValue","childNodes","comparePoint","e","querySelector","anchor","options","toRange","replaceWith","replacements","parent","r","insertBefore","remove","annotation","highlight","resolveAnchor","h","_annotation","target","anchors","map","selector","s","type","selectors","position","quote","hint","start","maybeAssertQuote","_quote","exact","toString","Error","queryselector","RangeAnchor","fromSelector","error","TextPositionAnchor","TextQuoteAnchor","htmlAnchor","textRange","TextRange","fromRange","err","$orphan","every","types","result","toSelector","Array","from","$","default","querySelectorAll","i","children","removeHighlights","_jquery","__esModule"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/match-quote.min.js b/amd/build/match-quote.min.js new file mode 100644 index 0000000..fc1bee2 --- /dev/null +++ b/amd/build/match-quote.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/match-quote",["exports","./string-match"],(function(_exports,_stringMatch){var obj;function search(text,str,maxErrors){let matchPos=0,exactMatches=[];for(;-1!==matchPos;)matchPos=text.indexOf(str,matchPos),-1!==matchPos&&(exactMatches.push({start:matchPos,end:matchPos+str.length,errors:0}),matchPos+=1);return exactMatches.length>0?exactMatches:(0,_stringMatch.default)(text,str,maxErrors)}function textMatchScore(text,str){if(0===str.length||0===text.length)return 0;return 1-search(text,str,str.length)[0].errors/str.length}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.matchQuote=function(text,quote){let context=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===quote.length)return null;const maxErrors=Math.min(256,quote.length/2),matches=search(text,quote,maxErrors);if(0===matches.length)return null;const scoreMatch=match=>{const quoteScore=1-match.errors/quote.length,prefixScore=context.prefix?textMatchScore(text.slice(Math.max(0,match.start-context.prefix.length),match.start),context.prefix):1,suffixScore=context.suffix?textMatchScore(text.slice(match.end,match.end+context.suffix.length),context.suffix):1;let posScore=1;if("number"==typeof context.hint){posScore=1-Math.abs(match.start-context.hint)/text.length}return(50*quoteScore+20*prefixScore+20*suffixScore+2*posScore)/92},scoredMatches=matches.map((m=>({start:m.start,end:m.end,score:scoreMatch(m)})));return scoredMatches.sort(((a,b)=>b.score-a.score)),scoredMatches[0]},_stringMatch=(obj=_stringMatch)&&obj.__esModule?obj:{default:obj}})); + +//# sourceMappingURL=match-quote.min.js.map \ No newline at end of file diff --git a/amd/build/match-quote.min.js.map b/amd/build/match-quote.min.js.map new file mode 100644 index 0000000..b583708 --- /dev/null +++ b/amd/build/match-quote.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"match-quote.min.js","sources":["../src/match-quote.js"],"sourcesContent":["/**\n * Functions for quote matching for the annotations and highlighting.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport approxSearch from './string-match';\n\n/**\n * @typedef {import('approx-string-match').Match} StringMatch\n */\n\n/**\n * @typedef Match\n * @prop {number} start - Start offset of match in text\n * @prop {number} end - End offset of match in text\n * @prop {number} score -\n * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match\n * for the quote and context.\n */\n\n/**\n * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors.\n *\n * @param {string} text\n * @param {string} str\n * @param {number} maxErrors\n * @return {StringMatch[]}\n */\nfunction search(text, str, maxErrors) {\n // Do a fast search for exact matches. The `approx-string-match` library\n // doesn't currently incorporate this optimization itself.\n let matchPos = 0;\n let exactMatches = [];\n while (matchPos !== -1) {\n matchPos = text.indexOf(str, matchPos);\n if (matchPos !== -1) {\n exactMatches.push({\n start: matchPos,\n end: matchPos + str.length,\n errors: 0,\n });\n matchPos += 1;\n }\n }\n if (exactMatches.length > 0) {\n return exactMatches;\n }\n\n // If there are no exact matches, do a more expensive search for matches\n // with errors.\n return approxSearch(text, str, maxErrors);\n}\n\n/**\n * Compute a score between 0 and 1.0 for the similarity between `text` and `str`.\n *\n * @param {string} text\n * @param {string} str\n * @return {int}\n */\nfunction textMatchScore(text, str) {\n // `search` will return no matches if either the text or pattern is empty,\n // otherwise it will return at least one match if the max allowed error count\n // is at least `str.length`.\n if (str.length === 0 || text.length === 0) {\n return 0.0;\n }\n\n const matches = search(text, str, str.length);\n\n // Prettier-ignore.\n return 1 - (matches[0].errors / str.length);\n}\n\n/**\n * Find the best approximate match for `quote` in `text`.\n *\n * Returns `null` if no match exceeding the minimum quality threshold was found.\n *\n * @param {string} text - Document text to search\n * @param {string} quote - String to find within `text`\n * @param {Object} context -\n * Context in which the quote originally appeared. This is used to choose the\n * best match.\n * @param {string} [context.prefix] - Expected text before the quote\n * @param {string} [context.suffix] - Expected text after the quote\n * @param {number} [context.hint] - Expected offset of match within text\n * @return {Match|null}\n */\nexport function matchQuote(text, quote, context = {}) {\n if (quote.length === 0) {\n return null;\n }\n\n // Choose the maximum number of errors to allow for the initial search.\n // This choice involves a tradeoff between:\n //\n // - Recall (proportion of \"good\" matches found)\n // - Precision (proportion of matches found which are \"good\")\n // - Cost of the initial search and of processing the candidate matches [1]\n //\n // [1] Specifically, the expected-time complexity of the initial search is\n // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs.\n const maxErrors = Math.min(256, quote.length / 2);\n\n // Find closest matches for `quote` in `text` based on edit distance.\n const matches = search(text, quote, maxErrors);\n\n if (matches.length === 0) {\n return null;\n }\n\n /**\n * Compute a score between 0 and 1.0 for a match candidate.\n *\n * @param {StringMatch} match\n * @return {int}\n */\n const scoreMatch = match => {\n const quoteWeight = 50; // Similarity of matched text to quote.\n const prefixWeight = 20; // Similarity of text before matched text to `context.prefix`.\n const suffixWeight = 20; // Similarity of text after matched text to `context.suffix`.\n const posWeight = 2; // Proximity to expected location. Used as a tie-breaker.\n\n const quoteScore = 1 - match.errors / quote.length;\n\n const prefixScore = context.prefix\n ? textMatchScore(\n text.slice(\n Math.max(0, match.start - context.prefix.length),\n match.start\n ),\n context.prefix\n )\n : 1.0;\n const suffixScore = context.suffix\n ? textMatchScore(\n text.slice(match.end, match.end + context.suffix.length),\n context.suffix\n )\n : 1.0;\n\n let posScore = 1.0;\n if (typeof context.hint === 'number') {\n const offset = Math.abs(match.start - context.hint);\n posScore = 1.0 - offset / text.length;\n }\n\n const rawScore =\n quoteWeight * quoteScore +\n prefixWeight * prefixScore +\n suffixWeight * suffixScore +\n posWeight * posScore;\n const maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight;\n const normalizedScore = rawScore / maxScore;\n\n return normalizedScore;\n };\n\n // Rank matches based on similarity of actual and expected surrounding text\n // and actual/expected offset in the document text.\n const scoredMatches = matches.map(m => ({\n start: m.start,\n end: m.end,\n score: scoreMatch(m),\n }));\n\n // Choose match with highest score.\n scoredMatches.sort((a, b) => b.score - a.score);\n return scoredMatches[0];\n}\n"],"names":["obj","search","text","str","maxErrors","matchPos","exactMatches","indexOf","push","start","end","length","errors","approxSearch","textMatchScore","quote","context","arguments","undefined","Math","min","matches","scoreMatch","match","quoteScore","prefixScore","prefix","slice","max","suffixScore","suffix","posScore","hint","abs","quoteWeight","scoredMatches","map","m","score","sort","a","b","_stringMatch","__esModule","default"],"mappings":"8FAQ0C,IAAAA,IAuB1C,SAASC,OAAOC,KAAMC,IAAKC,WAGzB,IAAIC,SAAW,EACXC,aAAe,GACnB,MAAqB,IAAdD,UACLA,SAAWH,KAAKK,QAAQJ,IAAKE,WACX,IAAdA,WACFC,aAAaE,KAAK,CAChBC,MAAOJ,SACPK,IAAKL,SAAWF,IAAIQ,OACpBC,OAAQ,IAEVP,UAAY,GAGhB,OAAIC,aAAaK,OAAS,EACjBL,cAKF,EAAAO,sBAAaX,KAAMC,IAAKC,UACjC,CASA,SAASU,eAAeZ,KAAMC,KAI5B,GAAmB,IAAfA,IAAIQ,QAAgC,IAAhBT,KAAKS,OAC3B,OAAO,EAMT,OAAO,EAHSV,OAAOC,KAAMC,IAAKA,IAAIQ,QAGlB,GAAGC,OAAST,IAAIQ,MACtC,6EAiBO,SAAoBT,KAAMa,OAAqB,IAAdC,QAAOC,UAAAN,OAAA,QAAAO,IAAAD,UAAA,GAAAA,UAAA,GAAG,CAAA,EAChD,GAAqB,IAAjBF,MAAMJ,OACR,OAAO,KAYT,MAAMP,UAAYe,KAAKC,IAAI,IAAKL,MAAMJ,OAAS,GAGzCU,QAAUpB,OAAOC,KAAMa,MAAOX,WAEpC,GAAuB,IAAnBiB,QAAQV,OACV,OAAO,KAST,MAAMW,WAAaC,QACjB,MAKMC,WAAa,EAAID,MAAMX,OAASG,MAAMJ,OAEtCc,YAAcT,QAAQU,OACxBZ,eACEZ,KAAKyB,MACHR,KAAKS,IAAI,EAAGL,MAAMd,MAAQO,QAAQU,OAAOf,QACzCY,MAAMd,OAERO,QAAQU,QAEV,EACEG,YAAcb,QAAQc,OACxBhB,eACEZ,KAAKyB,MAAMJ,MAAMb,IAAKa,MAAMb,IAAMM,QAAQc,OAAOnB,QACjDK,QAAQc,QAEV,EAEJ,IAAIC,SAAW,EACf,GAA4B,iBAAjBf,QAAQgB,KAAmB,CAEpCD,SAAW,EADIZ,KAAKc,IAAIV,MAAMd,MAAQO,QAAQgB,MACpB9B,KAAKS,MACjC,CAUA,OArCoB,GA8BJa,WA7BK,GA8BJC,YA7BI,GA8BJI,YA7BC,EA8BJE,UACGG,EAGK,EAKlBC,cAAgBd,QAAQe,KAAIC,IAAM,CACtC5B,MAAO4B,EAAE5B,MACTC,IAAK2B,EAAE3B,IACP4B,MAAOhB,WAAWe,OAKpB,OADAF,cAAcI,MAAK,CAACC,EAAGC,IAAMA,EAAEH,MAAQE,EAAEF,QAClCH,cAAc,EACvB,EArKAO,cAA0C1C,IAA1C0C,eAA0C1C,IAAA2C,WAAA3C,IAAA4C,CAAAA,QAAA5C,IAqKzC"} \ No newline at end of file diff --git a/amd/build/string-match.min.js b/amd/build/string-match.min.js new file mode 100644 index 0000000..0c13cd3 --- /dev/null +++ b/amd/build/string-match.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/string-match",["exports"],(function(_exports){function reverse(s){return s.split("").reverse().join("")}function oneIfNotZero(n){return(n|-n)>>31&1}function advanceBlock(ctx,peq,b,hIn){let pV=ctx.P[b],mV=ctx.M[b];const hInIsNegative=hIn>>>31,eq=peq[b]|hInIsNegative,xV=eq|mV,xH=(eq&pV)+pV^pV|eq;let pH=mV|~(xH|pV),mH=pV&xH;const hOut=oneIfNotZero(pH&ctx.lastRowMask[b])-oneIfNotZero(mH&ctx.lastRowMask[b]);return pH<<=1,mH<<=1,mH|=hInIsNegative,pH|=oneIfNotZero(hIn)-hInIsNegative,pV=mH|~(xV|pH),mV=pH&xV,ctx.P[b]=pV,ctx.M[b]=mV,hOut}function findMatchEnds(text,pattern,maxErrors){if(0===pattern.length)return[];maxErrors=Math.min(maxErrors,pattern.length);const matches=[],w=32,bMax=Math.ceil(pattern.length/w)-1,ctx={P:new Uint32Array(bMax+1),M:new Uint32Array(bMax+1),lastRowMask:new Uint32Array(bMax+1)};ctx.lastRowMask.fill(1<<31),ctx.lastRowMask[bMax]=1<<(pattern.length-1)%w;const emptyPeq=new Uint32Array(bMax+1),peq=new Map,asciiPeq=[];for(let i=0;i<256;i++)asciiPeq.push(emptyPeq);for(let c=0;c=pattern.length)continue;pattern.charCodeAt(idx)===val&&(charPeq[b]|=1<0&&score[y]>=maxErrors+w;)y-=1;y===bMax&&score[y]<=maxErrors&&(score[y]{const minStart=Math.max(0,m.end-pattern.length-m.errors);return{start:findMatchEnds(reverse(text.slice(minStart,m.end)),patRev,m.errors).reduce(((min,rm)=>m.end-rm.end {\n // Find start of each match by reversing the pattern and matching segment\n // of text and searching for an approx match with the same number of\n // errors.\n const minStart = Math.max(0, m.end - pattern.length - m.errors);\n const textRev = reverse(text.slice(minStart, m.end));\n\n // If there are multiple possible start points, choose the one that\n // maximizes the length of the match.\n const start = findMatchEnds(textRev, patRev, m.errors).reduce((min, rm) => {\n if (m.end - rm.end < min) {\n return m.end - rm.end;\n }\n return min;\n }, m.end);\n\n return {\n start,\n end: m.end,\n errors: m.errors,\n };\n });\n }\n\n /**\n * Internal context used when calculating blocks of a column.\n */\n // interface Context {\n // /**\n // * Bit-arrays of positive vertical deltas.\n // *\n // * ie. `P[b][i]` is set if the vertical delta for the i'th row in the b'th\n // * block is positive.\n // */\n // P: Uint32Array;\n // /** Bit-arrays of negative vertical deltas. */\n // M: Uint32Array;\n // /** Bit masks with a single bit set indicating the last row in each block. */\n // lastRowMask: Uint32Array;\n // }\n\n /**\n * Return 1 if a number is non-zero or zero otherwise, without using\n * conditional operators.\n *\n * This should get inlined into `advanceBlock` below by the JIT.\n *\n * Adapted from https://stackoverflow.com/a/3912218/434243\n * @param {int} n\n * @return {bool}\n */\n function oneIfNotZero(n) {\n return ((n | -n) >> 31) & 1;\n }\n\n /**\n * Block calculation step of the algorithm.\n *\n * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional\n * checks with bitwise operations as per Section 4.2.3 of [2].\n *\n * @param {obj} ctx - The pattern context object\n * @param {array} peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param {int} b - The block level\n * @param {obj} hIn - Horizontal input delta ∈ {1,0,-1}\n * @return {obj} Horizontal output delta ∈ {1,0,-1}\n */\n function advanceBlock(ctx, peq, b, hIn) {\n let pV = ctx.P[b];\n let mV = ctx.M[b];\n const hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise.\n const eq = peq[b] | hInIsNegative;\n\n // Step 1: Compute horizontal deltas.\n const xV = eq | mV;\n const xH = (((eq & pV) + pV) ^ pV) | eq;\n\n let pH = mV | ~(xH | pV);\n let mH = pV & xH;\n\n // Step 2: Update score (value of last row of this block).\n const hOut =\n oneIfNotZero(pH & ctx.lastRowMask[b]) -\n oneIfNotZero(mH & ctx.lastRowMask[b]);\n\n // Step 3: Update vertical deltas for use when processing next char.\n pH <<= 1;\n mH <<= 1;\n\n mH |= hInIsNegative;\n pH |= oneIfNotZero(hIn) - hInIsNegative; // Set pH[0] if hIn > 0.\n\n pV = mH | ~(xV | pH);\n mV = pH & xV;\n\n ctx.P[b] = pV;\n ctx.M[b] = mV;\n\n return hOut;\n }\n\n /**\n * Find the ends and error counts for matches of `pattern` in `text`.\n *\n * Only the matches with the lowest error count are reported. Other matches\n * with error counts <= maxErrors are discarded.\n *\n * This is the block-based search algorithm from Fig. 9 on p.410 of [1].\n *\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n function findMatchEnds(text, pattern, maxErrors) {\n if (pattern.length === 0) {\n return [];\n }\n\n // Clamp error count so we can rely on the `maxErrors` and `pattern.length`\n // rows being in the same block below.\n maxErrors = Math.min(maxErrors, pattern.length);\n\n const matches = [];\n\n // Word size.\n const w = 32;\n\n // Index of maximum block level.\n const bMax = Math.ceil(pattern.length / w) - 1;\n\n // Context used across block calculations.\n const ctx = {\n P: new Uint32Array(bMax + 1),\n M: new Uint32Array(bMax + 1),\n lastRowMask: new Uint32Array(bMax + 1),\n };\n ctx.lastRowMask.fill(1 << 31);\n ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w;\n\n // Dummy \"peq\" array for chars in the text which do not occur in the pattern.\n const emptyPeq = new Uint32Array(bMax + 1);\n\n // Map of UTF-16 character code to bit vector indicating positions in the\n // pattern that equal that character.\n const peq = new Map();\n\n // Version of `peq` that only stores mappings for small characters. This\n // allows faster lookups when iterating through the text because a simple\n // array lookup can be done instead of a hash table lookup.\n const asciiPeq = [];\n for (let i = 0; i < 256; i++) {\n asciiPeq.push(emptyPeq);\n }\n\n // Calculate `ctx.peq` - a map of character values to bitmasks indicating\n // positions of that character within the pattern, where each bit represents\n // a position in the pattern.\n for (let c = 0; c < pattern.length; c += 1) {\n const val = pattern.charCodeAt(c);\n if (peq.has(val)) {\n // Duplicate char in pattern.\n continue;\n }\n\n const charPeq = new Uint32Array(bMax + 1);\n peq.set(val, charPeq);\n if (val < asciiPeq.length) {\n asciiPeq[val] = charPeq;\n }\n\n for (let b = 0; b <= bMax; b += 1) {\n charPeq[b] = 0;\n\n // Set all the bits where the pattern matches the current char (ch).\n // For indexes beyond the end of the pattern, always set the bit as if the\n // pattern contained a wildcard char in that position.\n for (let r = 0; r < w; r += 1) {\n const idx = b * w + r;\n if (idx >= pattern.length) {\n continue;\n }\n\n const match = pattern.charCodeAt(idx) === val;\n if (match) {\n charPeq[b] |= 1 << r;\n }\n }\n }\n }\n\n // Index of last-active block level in the column.\n let y = Math.max(0, Math.ceil(maxErrors / w) - 1);\n\n // Initialize maximum error count at bottom of each block.\n const score = new Uint32Array(bMax + 1);\n for (let b = 0; b <= y; b += 1) {\n score[b] = (b + 1) * w;\n }\n score[bMax] = pattern.length;\n\n // Initialize vertical deltas for each block.\n for (let b = 0; b <= y; b += 1) {\n ctx.P[b] = ~0;\n ctx.M[b] = 0;\n }\n\n // Process each char of the text, computing the error count for `w` chars of\n // the pattern at a time.\n for (let j = 0; j < text.length; j += 1) {\n // Lookup the bitmask representing the positions of the current char from\n // the text within the pattern.\n const charCode = text.charCodeAt(j);\n let charPeq;\n\n if (charCode < asciiPeq.length) {\n // Fast array lookup.\n charPeq = asciiPeq[charCode];\n } else {\n // Slower hash table lookup.\n charPeq = peq.get(charCode);\n if (typeof charPeq === \"undefined\") {\n charPeq = emptyPeq;\n }\n }\n\n // Calculate error count for blocks that we definitely have to process for\n // this column.\n let carry = 0;\n for (let b = 0; b <= y; b += 1) {\n carry = advanceBlock(ctx, charPeq, b, carry);\n score[b] += carry;\n }\n\n // Check if we also need to compute an additional block, or if we can reduce\n // the number of blocks processed for the next column.\n if (\n score[y] - carry <= maxErrors &&\n y < bMax &&\n (charPeq[y + 1] & 1 || carry < 0)\n ) {\n // Error count for bottom block is under threshold, increase the number of\n // blocks processed for this column & next by 1.\n y += 1;\n\n ctx.P[y] = ~0;\n ctx.M[y] = 0;\n\n let maxBlockScore;\n if (y === bMax) {\n const remainder = pattern.length % w;\n maxBlockScore = remainder === 0 ? w : remainder;\n } else {\n maxBlockScore = w;\n }\n\n score[y] =\n score[y - 1] +\n maxBlockScore -\n carry +\n advanceBlock(ctx, charPeq, y, carry);\n } else {\n // Error count for bottom block exceeds threshold, reduce the number of\n // blocks processed for the next column.\n while (y > 0 && score[y] >= maxErrors + w) {\n y -= 1;\n }\n }\n\n // If error count is under threshold, report a match.\n if (y === bMax && score[y] <= maxErrors) {\n if (score[y] < maxErrors) {\n // Discard any earlier, worse matches.\n matches.splice(0, matches.length);\n }\n\n matches.push({\n start: -1,\n end: j + 1,\n errors: score[y],\n });\n\n // Because `search` only reports the matches with the lowest error count,\n // we can \"ratchet down\" the max error threshold whenever a match is\n // encountered and thereby save a small amount of work for the remainder\n // of the text.\n maxErrors = score[y];\n }\n }\n\n return matches;\n }\n\n /**\n * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors.\n *\n * Returns the start, and end positions and error counts for each lowest-cost\n * match. Only the \"best\" matches are returned.\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n export default function search(\n text,\n pattern,\n maxErrors\n ) {\n const matches = findMatchEnds(text, pattern, maxErrors);\n return findMatchStarts(text, pattern, matches);\n }"],"names":["reverse","s","split","join","oneIfNotZero","n","advanceBlock","ctx","peq","b","hIn","pV","P","mV","M","hInIsNegative","eq","xV","xH","pH","mH","hOut","lastRowMask","findMatchEnds","text","pattern","maxErrors","length","Math","min","matches","w","bMax","ceil","Uint32Array","fill","emptyPeq","Map","asciiPeq","i","push","c","val","charCodeAt","has","charPeq","set","r","idx","y","max","score","j","charCode","get","carry","maxBlockScore","remainder","splice","start","end","errors","patRev","map","m","minStart","slice","reduce","rm","findMatchStarts","_exports","default"],"mappings":"iEAYE,SAASA,QAAQC,GACf,OAAOA,EAAEC,MAAM,IAAIF,UAAUG,KAAK,GACpC,CAiEA,SAASC,aAAaC,GACpB,OAASA,GAAKA,IAAM,GAAM,CAC5B,CAcA,SAASC,aAAaC,IAAKC,IAAKC,EAAGC,KACjC,IAAIC,GAAKJ,IAAIK,EAAEH,GACXI,GAAKN,IAAIO,EAAEL,GACf,MAAMM,cAAgBL,MAAQ,GACxBM,GAAKR,IAAIC,GAAKM,cAGdE,GAAKD,GAAKH,GACVK,IAAQF,GAAKL,IAAMA,GAAMA,GAAMK,GAErC,IAAIG,GAAKN,KAAOK,GAAKP,IACjBS,GAAKT,GAAKO,GAGd,MAAMG,KACJjB,aAAae,GAAKZ,IAAIe,YAAYb,IAClCL,aAAagB,GAAKb,IAAIe,YAAYb,IAepC,OAZAU,KAAO,EACPC,KAAO,EAEPA,IAAML,cACNI,IAAMf,aAAaM,KAAOK,cAE1BJ,GAAKS,KAAOH,GAAKE,IACjBN,GAAKM,GAAKF,GAEVV,IAAIK,EAAEH,GAAKE,GACXJ,IAAIO,EAAEL,GAAKI,GAEJQ,IACT,CAeA,SAASE,cAAcC,KAAMC,QAASC,WACpC,GAAuB,IAAnBD,QAAQE,OACV,MAAO,GAKTD,UAAYE,KAAKC,IAAIH,UAAWD,QAAQE,QAExC,MAAMG,QAAU,GAGVC,EAAI,GAGJC,KAAOJ,KAAKK,KAAKR,QAAQE,OAASI,GAAK,EAGvCxB,IAAM,CACVK,EAAG,IAAIsB,YAAYF,KAAO,GAC1BlB,EAAG,IAAIoB,YAAYF,KAAO,GAC1BV,YAAa,IAAIY,YAAYF,KAAO,IAEtCzB,IAAIe,YAAYa,KAAK,GAAK,IAC1B5B,IAAIe,YAAYU,MAAQ,IAAMP,QAAQE,OAAS,GAAKI,EAGpD,MAAMK,SAAW,IAAIF,YAAYF,KAAO,GAIlCxB,IAAM,IAAI6B,IAKVC,SAAW,GACjB,IAAK,IAAIC,EAAI,EAAGA,EAAI,IAAKA,IACvBD,SAASE,KAAKJ,UAMhB,IAAK,IAAIK,EAAI,EAAGA,EAAIhB,QAAQE,OAAQc,GAAK,EAAG,CAC1C,MAAMC,IAAMjB,QAAQkB,WAAWF,GAC/B,GAAIjC,IAAIoC,IAAIF,KAEV,SAGF,MAAMG,QAAU,IAAIX,YAAYF,KAAO,GACvCxB,IAAIsC,IAAIJ,IAAKG,SACTH,IAAMJ,SAASX,SACjBW,SAASI,KAAOG,SAGlB,IAAK,IAAIpC,EAAI,EAAGA,GAAKuB,KAAMvB,GAAK,EAAG,CACjCoC,QAAQpC,GAAK,EAKb,IAAK,IAAIsC,EAAI,EAAGA,EAAIhB,EAAGgB,GAAK,EAAG,CAC7B,MAAMC,IAAMvC,EAAIsB,EAAIgB,EACpB,GAAIC,KAAOvB,QAAQE,OACjB,SAGYF,QAAQkB,WAAWK,OAASN,MAExCG,QAAQpC,IAAM,GAAKsC,EAEvB,CACF,CACF,CAGA,IAAIE,EAAIrB,KAAKsB,IAAI,EAAGtB,KAAKK,KAAKP,UAAYK,GAAK,GAG/C,MAAMoB,MAAQ,IAAIjB,YAAYF,KAAO,GACrC,IAAK,IAAIvB,EAAI,EAAGA,GAAKwC,EAAGxC,GAAK,EAC3B0C,MAAM1C,IAAMA,EAAI,GAAKsB,EAEvBoB,MAAMnB,MAAQP,QAAQE,OAGtB,IAAK,IAAIlB,EAAI,EAAGA,GAAKwC,EAAGxC,GAAK,EAC3BF,IAAIK,EAAEH,IAAK,EACXF,IAAIO,EAAEL,GAAK,EAKb,IAAK,IAAI2C,EAAI,EAAGA,EAAI5B,KAAKG,OAAQyB,GAAK,EAAG,CAGvC,MAAMC,SAAW7B,KAAKmB,WAAWS,GACjC,IAAIP,QAEAQ,SAAWf,SAASX,OAEtBkB,QAAUP,SAASe,WAGnBR,QAAUrC,IAAI8C,IAAID,eACK,IAAZR,UACTA,QAAUT,WAMd,IAAImB,MAAQ,EACZ,IAAK,IAAI9C,EAAI,EAAGA,GAAKwC,EAAGxC,GAAK,EAC3B8C,MAAQjD,aAAaC,IAAKsC,QAASpC,EAAG8C,OACtCJ,MAAM1C,IAAM8C,MAKd,GACEJ,MAAMF,GAAKM,OAAS7B,WACpBuB,EAAIjB,OACc,EAAjBa,QAAQI,EAAI,IAAUM,MAAQ,GAC/B,CAQA,IAAIC,cACJ,GANAP,GAAK,EAEL1C,IAAIK,EAAEqC,IAAK,EACX1C,IAAIO,EAAEmC,GAAK,EAGPA,IAAMjB,KAAM,CACd,MAAMyB,UAAYhC,QAAQE,OAASI,EACnCyB,cAA8B,IAAdC,UAAkB1B,EAAI0B,SACxC,MACED,cAAgBzB,EAGlBoB,MAAMF,GACJE,MAAMF,EAAI,GACVO,cACAD,MACAjD,aAAaC,IAAKsC,QAASI,EAAGM,MAClC,MAGE,KAAON,EAAI,GAAKE,MAAMF,IAAMvB,UAAYK,GACtCkB,GAAK,EAKLA,IAAMjB,MAAQmB,MAAMF,IAAMvB,YACxByB,MAAMF,GAAKvB,WAEbI,QAAQ4B,OAAO,EAAG5B,QAAQH,QAG5BG,QAAQU,KAAK,CACXmB,OAAQ,EACRC,IAAKR,EAAI,EACTS,OAAQV,MAAMF,KAOhBvB,UAAYyB,MAAMF,GAEtB,CAEA,OAAOnB,OACT,CAmBC,gFAPc,SACbN,KACAC,QACAC,WAEA,MAAMI,QAAUP,cAAcC,KAAMC,QAASC,WAC7C,OAxTF,SAAyBF,KAAMC,QAASK,SACtC,MAAMgC,OAAS9D,QAAQyB,SAEvB,OAAOK,QAAQiC,KAAKC,IAIlB,MAAMC,SAAWrC,KAAKsB,IAAI,EAAGc,EAAEJ,IAAMnC,QAAQE,OAASqC,EAAEH,QAYxD,MAAO,CACLF,MARYpC,cAJEvB,QAAQwB,KAAK0C,MAAMD,SAAUD,EAAEJ,MAIVE,OAAQE,EAAEH,QAAQM,QAAO,CAACtC,IAAKuC,KAC9DJ,EAAEJ,IAAMQ,GAAGR,IAAM/B,IACZmC,EAAEJ,IAAMQ,GAAGR,IAEb/B,KACNmC,EAAEJ,KAIHA,IAAKI,EAAEJ,IACPC,OAAQG,EAAEH,OACX,GAEL,CA+RSQ,CAAgB7C,KAAMC,QAASK,QACxC,EAACwC,SAAAC,OAAA"} \ No newline at end of file diff --git a/amd/build/text-range.min.js b/amd/build/text-range.min.js new file mode 100644 index 0000000..213be49 --- /dev/null +++ b/amd/build/text-range.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/text-range",["exports"],(function(_exports){function nodeTextLength(node){switch(node.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return node.textContent.length;default:return 0}}function previousSiblingsTextLength(node){let sibling=node.previousSibling,length=0;for(;sibling;)length+=nodeTextLength(sibling),sibling=sibling.previousSibling;return length}function resolveOffsets(element){for(var _len=arguments.length,offsets=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)offsets[_key-1]=arguments[_key];let nextOffset=offsets.shift();const nodeIter=element.ownerDocument.createNodeIterator(element,NodeFilter.SHOW_TEXT),results=[];let textNode,currentNode=nodeIter.nextNode(),length=0;for(;void 0!==nextOffset&¤tNode;)textNode=currentNode,length+textNode.data.length>nextOffset?(results.push({node:textNode,offset:nextOffset-length}),nextOffset=offsets.shift()):(currentNode=nodeIter.nextNode(),length+=textNode.data.length);for(;void 0!==nextOffset&&length===nextOffset;)results.push({node:textNode,offset:textNode.data.length}),nextOffset=offsets.shift();if(void 0!==nextOffset)throw new RangeError("Offset exceeds text length");return results}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.TextRange=_exports.TextPosition=_exports.RESOLVE_FORWARDS=_exports.RESOLVE_BACKWARDS=void 0;_exports.RESOLVE_FORWARDS=1;_exports.RESOLVE_BACKWARDS=2;class TextPosition{constructor(element,offset){if(offset<0)throw new Error("Offset is invalid");this.element=element,this.offset=offset}relativeTo(parent){if(!parent.contains(this.element))throw new Error("Parent is not an ancestor of current element");let el=this.element,offset=this.offset;for(;el!==parent;)offset+=previousSiblingsTextLength(el),el=el.parentElement;return new TextPosition(el,offset)}resolve(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return resolveOffsets(this.element,this.offset)[0]}catch(err){if(0===this.offset&&void 0!==options.direction){const tw=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);tw.currentNode=this.element;const forwards=1===options.direction,text=forwards?tw.nextNode():tw.previousNode();if(!text)throw err;return{node:text,offset:forwards?0:text.data.length}}throw err}}static fromCharOffset(node,offset){switch(node.nodeType){case Node.TEXT_NODE:return TextPosition.fromPoint(node,offset);case Node.ELEMENT_NODE:return new TextPosition(node,offset);default:throw new Error("Node is not an element or text node")}}static fromPoint(node,offset){switch(node.nodeType){case Node.TEXT_NODE:{if(offset<0||offset>node.data.length)throw new Error("Text node offset is out of range");if(!node.parentElement)throw new Error("Text node has no parent");const textOffset=previousSiblingsTextLength(node)+offset;return new TextPosition(node.parentElement,textOffset)}case Node.ELEMENT_NODE:{if(offset<0||offset>node.childNodes.length)throw new Error("Child node offset is out of range");let textOffset=0;for(let i=0;i nextOffset) {\n results.push({node: textNode, offset: nextOffset - length});\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n }\n\n // Boundary case.\n while (nextOffset !== undefined && length === nextOffset) {\n results.push({node: textNode, offset: textNode.data.length});\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError('Offset exceeds text length');\n }\n\n return results;\n}\n\nexport let RESOLVE_FORWARDS = 1;\nexport let RESOLVE_BACKWARDS = 2;\n\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\nexport class TextPosition {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n constructor(element, offset) {\n if (offset < 0) {\n throw new Error('Offset is invalid');\n }\n\n /** Element that `offset` is relative to. */\n this.element = element;\n\n /** Character offset from the start of the element's `textContent`. */\n this.offset = offset;\n }\n\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error('Parent is not an ancestor of current element');\n }\n\n let el = this.element;\n let offset = this.offset;\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el = /** @type {Element} */ (el.parentElement);\n }\n\n return new TextPosition(el, offset);\n }\n\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element's text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n resolve(options = {}) {\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n const tw = document.createTreeWalker(\n this.element.getRootNode(),\n NodeFilter.SHOW_TEXT\n );\n tw.currentNode = this.element;\n const forwards = options.direction === RESOLVE_FORWARDS;\n const text = /** @type {Text|null} */ (\n forwards ? tw.nextNode() : tw.previousNode()\n );\n if (!text) {\n throw err;\n }\n return {node: text, offset: forwards ? 0 : text.data.length};\n } else {\n throw err;\n }\n }\n }\n\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n static fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n case Node.ELEMENT_NODE:\n return new TextPosition(/** @type {Element} */ (node), offset);\n default:\n throw new Error('Node is not an element or text node');\n }\n }\n\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n static fromPoint(node, offset) {\n\n switch (node.nodeType) {\n case Node.TEXT_NODE: {\n if (offset < 0 || offset > /** @type {Text} */ (node).data.length) {\n throw new Error('Text node offset is out of range');\n }\n\n if (!node.parentElement) {\n throw new Error('Text node has no parent');\n }\n\n // Get the offset from the start of the parent element.\n const textOffset = previousSiblingsTextLength(node) + offset;\n\n return new TextPosition(node.parentElement, textOffset);\n }\n case Node.ELEMENT_NODE: {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error('Child node offset is out of range');\n }\n\n // Get the text length before the `offset`th child of element.\n let textOffset = 0;\n for (let i = 0; i < offset; i++) {\n textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(/** @type {Element} */ (node), textOffset);\n }\n default:\n throw new Error('Point is not in an element or text node');\n }\n }\n}\n\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don't affect its text content, without affecting the text content\n * of the range itself.\n */\nexport class TextRange {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n constructor(start, end) {\n this.start = start;\n this.end = end;\n }\n\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n * @return {Range}\n */\n relativeTo(element) {\n return new TextRange(\n this.start.relativeTo(element),\n this.end.relativeTo(element)\n );\n }\n\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to \"shrink\" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n toRange() {\n let start;\n let end;\n\n if (\n this.start.element === this.end.element &&\n this.start.offset <= this.end.offset\n ) {\n // Fast path for start and end points in same element.\n [start, end] = resolveOffsets(\n this.start.element,\n this.start.offset,\n this.end.offset\n );\n } else {\n start = this.start.resolve({direction: RESOLVE_FORWARDS});\n end = this.end.resolve({direction: RESOLVE_BACKWARDS});\n }\n\n const range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n static fromRange(range) {\n const start = TextPosition.fromPoint(\n range.startContainer,\n range.startOffset\n );\n const end = TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n * @return {Range}\n */\n static fromOffsets(root, start, end) {\n return new TextRange(\n new TextPosition(root, start),\n new TextPosition(root, end)\n );\n }\n}\n"],"names":["nodeTextLength","node","nodeType","Node","ELEMENT_NODE","TEXT_NODE","textContent","length","previousSiblingsTextLength","sibling","previousSibling","resolveOffsets","element","_len","arguments","offsets","Array","_key","nextOffset","shift","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","results","textNode","currentNode","nextNode","undefined","data","push","offset","RangeError","_exports","RESOLVE_FORWARDS","RESOLVE_BACKWARDS","TextPosition","constructor","Error","this","relativeTo","parent","contains","el","parentElement","resolve","options","err","direction","tw","document","createTreeWalker","getRootNode","forwards","text","previousNode","static","fromPoint","textOffset","childNodes","i","TextRange","start","end","toRange","range","Range","setStart","setEnd","startContainer","startOffset","endContainer","endOffset","root"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/types.min.js b/amd/build/types.min.js new file mode 100644 index 0000000..e2387ed --- /dev/null +++ b/amd/build/types.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/types",["exports","./match-quote","./text-range","./xpath"],(function(_exports,_matchQuote,_textRange,_xpath){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.TextQuoteAnchor=_exports.TextPositionAnchor=_exports.RangeAnchor=void 0;class RangeAnchor{constructor(root,range){this.root=root,this.range=range}static fromRange(root,range){return new RangeAnchor(root,range)}static fromSelector(root,selector){const startContainer=(0,_xpath.nodeFromXPath)(selector.startContainer,root);if(!startContainer)throw new Error("Failed to resolve startContainer XPath");const endContainer=(0,_xpath.nodeFromXPath)(selector.endContainer,root);if(!endContainer)throw new Error("Failed to resolve endContainer XPath");const startPos=_textRange.TextPosition.fromCharOffset(startContainer,selector.startOffset),endPos=_textRange.TextPosition.fromCharOffset(endContainer,selector.endOffset),range=new _textRange.TextRange(startPos,endPos).toRange();return new RangeAnchor(root,range)}toRange(){return this.range}toSelector(){const normalizedRange=_textRange.TextRange.fromRange(this.range).toRange(),textRange=_textRange.TextRange.fromRange(normalizedRange),startContainer=(0,_xpath.xpathFromNode)(textRange.start.element,this.root),endContainer=(0,_xpath.xpathFromNode)(textRange.end.element,this.root);return{type:"RangeSelector",startContainer:startContainer,startOffset:textRange.start.offset,endContainer:endContainer,endOffset:textRange.end.offset}}}_exports.RangeAnchor=RangeAnchor;class TextPositionAnchor{constructor(root,start,end){this.root=root,this.start=start,this.end=end}static fromRange(root,range){const textRange=_textRange.TextRange.fromRange(range).relativeTo(root);return new TextPositionAnchor(root,textRange.start.offset,textRange.end.offset)}static fromSelector(root,selector){return new TextPositionAnchor(root,selector.start,selector.end)}toSelector(){return{type:"TextPositionSelector",start:this.start,end:this.end}}toRange(){return _textRange.TextRange.fromOffsets(this.root,this.start,this.end).toRange()}}_exports.TextPositionAnchor=TextPositionAnchor;class TextQuoteAnchor{constructor(root,exact){let context=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.root=root,this.exact=exact,this.context=context}static fromRange(root,range){const text=root.textContent,textRange=_textRange.TextRange.fromRange(range).relativeTo(root),start=textRange.start.offset,end=textRange.end.offset;return new TextQuoteAnchor(root,text.slice(start,end),{prefix:text.slice(Math.max(0,start-32),start),suffix:text.slice(end,Math.min(text.length,end+32))})}static fromSelector(root,selector){const{prefix:prefix,suffix:suffix}=selector;return new TextQuoteAnchor(root,selector.exact,{prefix:prefix,suffix:suffix})}toSelector(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}toRange(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(options).toRange()}toPositionAnchor(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const text=this.root.textContent,match=(0,_matchQuote.matchQuote)(text,this.exact,{...this.context,hint:options.hint});if(!match)throw new Error("Quote not found");return new TextPositionAnchor(this.root,match.start,match.end)}}_exports.TextQuoteAnchor=TextQuoteAnchor})); + +//# sourceMappingURL=types.min.js.map \ No newline at end of file diff --git a/amd/build/types.min.js.map b/amd/build/types.min.js.map new file mode 100644 index 0000000..51b6a10 --- /dev/null +++ b/amd/build/types.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.min.js","sources":["../src/types.js"],"sourcesContent":["/**\n * This module exports a set of classes for converting between DOM `Range`\n * objects and different types of selectors. It is mostly a thin wrapper around a\n * set of anchoring libraries. It serves two main purposes:\n *\n * 1. Providing a consistent interface across different types of anchors.\n * 2. Insulating the rest of the code from API changes in the underlying anchoring\n * libraries.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport {matchQuote} from './match-quote';\nimport {TextRange, TextPosition} from './text-range';\nimport {nodeFromXPath, xpathFromNode} from './xpath';\n\n/**\n * @typedef {import('../../types/api').RangeSelector} RangeSelector\n * @typedef {import('../../types/api').TextPositionSelector} TextPositionSelector\n * @typedef {import('../../types/api').TextQuoteSelector} TextQuoteSelector\n */\n\n/**\n * Converts between `RangeSelector` selectors and `Range` objects.\n */\nexport class RangeAnchor {\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n constructor(root, range) {\n this.root = root;\n this.range = range;\n }\n\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n * @return {RangeAnchor}\n */\n static fromRange(root, range) {\n return new RangeAnchor(root, range);\n }\n\n /**\n * Create an anchor from a serialized `RangeSelector` selector.\n *\n * @param {Element} root - A root element from which to anchor.\n * @param {RangeSelector} selector\n * @return {RangeAnchor}\n */\n static fromSelector(root, selector) {\n\n const startContainer = nodeFromXPath(selector.startContainer, root);\n\n if (!startContainer) {\n throw new Error('Failed to resolve startContainer XPath');\n }\n\n const endContainer = nodeFromXPath(selector.endContainer, root);\n if (!endContainer) {\n throw new Error('Failed to resolve endContainer XPath');\n }\n\n const startPos = TextPosition.fromCharOffset(\n startContainer,\n selector.startOffset\n );\n const endPos = TextPosition.fromCharOffset(\n endContainer,\n selector.endOffset\n );\n\n const range = new TextRange(startPos, endPos).toRange();\n return new RangeAnchor(root, range);\n }\n\n toRange() {\n return this.range;\n }\n\n /**\n * @return {RangeSelector}\n */\n toSelector() {\n // \"Shrink\" the range so that it tightly wraps its text. This ensures more\n // predictable output for a given text selection.\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n const textRange = TextRange.fromRange(normalizedRange);\n const startContainer = xpathFromNode(textRange.start.element, this.root);\n const endContainer = xpathFromNode(textRange.end.element, this.root);\n\n return {\n type: 'RangeSelector',\n startContainer,\n startOffset: textRange.start.offset,\n endContainer,\n endOffset: textRange.end.offset,\n };\n }\n}\n\n/**\n * Converts between `TextPositionSelector` selectors and `Range` objects.\n */\nexport class TextPositionAnchor {\n /**\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n constructor(root, start, end) {\n this.root = root;\n this.start = start;\n this.end = end;\n }\n\n /**\n * @param {Element} root\n * @param {Range} range\n * @return {TextPositionAnchor}\n */\n static fromRange(root, range) {\n const textRange = TextRange.fromRange(range).relativeTo(root);\n return new TextPositionAnchor(\n root,\n textRange.start.offset,\n textRange.end.offset\n );\n }\n /**\n * @param {Element} root\n * @param {TextPositionSelector} selector\n * @return {TextPositionAnchor}\n */\n static fromSelector(root, selector) {\n return new TextPositionAnchor(root, selector.start, selector.end);\n }\n\n /**\n * @return {TextPositionSelector}\n */\n toSelector() {\n return {\n type: 'TextPositionSelector',\n start: this.start,\n end: this.end,\n };\n }\n\n toRange() {\n return TextRange.fromOffsets(this.root, this.start, this.end).toRange();\n }\n}\n\n/**\n * @typedef QuoteMatchOptions\n * @prop {number} [hint] - Expected position of match in text. See `matchQuote`.\n */\n\n/**\n * Converts between `TextQuoteSelector` selectors and `Range` objects.\n */\nexport class TextQuoteAnchor {\n /**\n * @param {Element} root - A root element from which to anchor.\n * @param {string} exact\n * @param {Object} context\n * @param {string} [context.prefix]\n * @param {string} [context.suffix]\n */\n constructor(root, exact, context = {}) {\n this.root = root;\n this.exact = exact;\n this.context = context;\n }\n\n /**\n * Create a `TextQuoteAnchor` from a range.\n *\n * Will throw if `range` does not contain any text nodes.\n *\n * @param {Element} root\n * @param {Range} range\n * @return {TextQuoteAnchor}\n */\n static fromRange(root, range) {\n const text = /** @type {string} */ (root.textContent);\n const textRange = TextRange.fromRange(range).relativeTo(root);\n\n const start = textRange.start.offset;\n const end = textRange.end.offset;\n\n // Number of characters around the quote to capture as context. We currently\n // always use a fixed amount, but it would be better if this code was aware\n // of logical boundaries in the document (paragraph, article etc.) to avoid\n // capturing text unrelated to the quote.\n //\n // In regular prose the ideal content would often be the surrounding sentence.\n // This is a natural unit of meaning which enables displaying quotes in\n // context even when the document is not available. We could use `Intl.Segmenter`\n // for this when available.\n const contextLen = 32;\n\n return new TextQuoteAnchor(root, text.slice(start, end), {\n prefix: text.slice(Math.max(0, start - contextLen), start),\n suffix: text.slice(end, Math.min(text.length, end + contextLen)),\n });\n }\n\n /**\n * @param {Element} root\n * @param {TextQuoteSelector} selector\n * @return {TextQuoteAnchor}\n */\n static fromSelector(root, selector) {\n const {prefix, suffix} = selector;\n return new TextQuoteAnchor(root, selector.exact, {prefix, suffix});\n }\n\n /**\n * @return {TextQuoteSelector}\n */\n toSelector() {\n return {\n type: 'TextQuoteSelector',\n exact: this.exact,\n prefix: this.context.prefix,\n suffix: this.context.suffix,\n };\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextQuoteAnchor}\n */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextPositionAnchor}\n */\n toPositionAnchor(options = {}) {\n const text = /** @type {string} */ (this.root.textContent);\n const match = matchQuote(text, this.exact, {\n ...this.context,\n hint: options.hint,\n });\n\n if (!match) {\n throw new Error('Quote not found');\n }\n\n return new TextPositionAnchor(this.root, match.start, match.end);\n }\n}\n"],"names":["RangeAnchor","constructor","root","range","this","static","selector","startContainer","nodeFromXPath","Error","endContainer","startPos","TextPosition","fromCharOffset","startOffset","endPos","endOffset","TextRange","toRange","toSelector","normalizedRange","fromRange","textRange","xpathFromNode","start","element","end","type","offset","_exports","TextPositionAnchor","relativeTo","fromOffsets","TextQuoteAnchor","exact","context","arguments","length","undefined","text","textContent","slice","prefix","Math","max","suffix","min","options","toPositionAnchor","match","matchQuote","hint"],"mappings":"0QA2BO,MAAMA,YAKXC,YAAYC,KAAMC,OAChBC,KAAKF,KAAOA,KACZE,KAAKD,MAAQA,KACf,CAOAE,iBAAiBH,KAAMC,OACrB,OAAO,IAAIH,YAAYE,KAAMC,MAC/B,CASAE,oBAAoBH,KAAMI,UAExB,MAAMC,gBAAiB,EAAAC,OAAaA,eAACF,SAASC,eAAgBL,MAE9D,IAAKK,eACH,MAAM,IAAIE,MAAM,0CAGlB,MAAMC,cAAe,EAAAF,OAAaA,eAACF,SAASI,aAAcR,MAC1D,IAAKQ,aACH,MAAM,IAAID,MAAM,wCAGlB,MAAME,SAAWC,WAAAA,aAAaC,eAC5BN,eACAD,SAASQ,aAELC,OAASH,WAAAA,aAAaC,eAC1BH,aACAJ,SAASU,WAGLb,MAAQ,IAAIc,WAASA,UAACN,SAAUI,QAAQG,UAC9C,OAAO,IAAIlB,YAAYE,KAAMC,MAC/B,CAEAe,UACE,OAAOd,KAAKD,KACd,CAKAgB,aAIE,MAAMC,gBAAkBH,WAASA,UAACI,UAAUjB,KAAKD,OAAOe,UAElDI,UAAYL,WAAAA,UAAUI,UAAUD,iBAChCb,gBAAiB,EAAAgB,OAAAA,eAAcD,UAAUE,MAAMC,QAASrB,KAAKF,MAC7DQ,cAAe,EAAAa,OAAAA,eAAcD,UAAUI,IAAID,QAASrB,KAAKF,MAE/D,MAAO,CACLyB,KAAM,gBACNpB,8BACAO,YAAaQ,UAAUE,MAAMI,OAC7BlB,0BACAM,UAAWM,UAAUI,IAAIE,OAE7B,EACDC,SAAA7B,YAAAA,YAKM,MAAM8B,mBAMX7B,YAAYC,KAAMsB,MAAOE,KACvBtB,KAAKF,KAAOA,KACZE,KAAKoB,MAAQA,MACbpB,KAAKsB,IAAMA,GACb,CAOArB,iBAAiBH,KAAMC,OACrB,MAAMmB,UAAYL,WAASA,UAACI,UAAUlB,OAAO4B,WAAW7B,MACxD,OAAO,IAAI4B,mBACT5B,KACAoB,UAAUE,MAAMI,OAChBN,UAAUI,IAAIE,OAElB,CAMAvB,oBAAoBH,KAAMI,UACxB,OAAO,IAAIwB,mBAAmB5B,KAAMI,SAASkB,MAAOlB,SAASoB,IAC/D,CAKAP,aACE,MAAO,CACLQ,KAAM,uBACNH,MAAOpB,KAAKoB,MACZE,IAAKtB,KAAKsB,IAEd,CAEAR,UACE,OAAOD,qBAAUe,YAAY5B,KAAKF,KAAME,KAAKoB,MAAOpB,KAAKsB,KAAKR,SAChE,EACDW,SAAAC,mBAAAA,mBAUM,MAAMG,gBAQXhC,YAAYC,KAAMgC,OAAqB,IAAdC,QAAOC,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,CAAA,EACjChC,KAAKF,KAAOA,KACZE,KAAK8B,MAAQA,MACb9B,KAAK+B,QAAUA,OACjB,CAWA9B,iBAAiBH,KAAMC,OACrB,MAAMoC,KAA8BrC,KAAKsC,YACnClB,UAAYL,WAASA,UAACI,UAAUlB,OAAO4B,WAAW7B,MAElDsB,MAAQF,UAAUE,MAAMI,OACxBF,IAAMJ,UAAUI,IAAIE,OAa1B,OAAO,IAAIK,gBAAgB/B,KAAMqC,KAAKE,MAAMjB,MAAOE,KAAM,CACvDgB,OAAQH,KAAKE,MAAME,KAAKC,IAAI,EAAGpB,MAHd,IAGmCA,OACpDqB,OAAQN,KAAKE,MAAMf,IAAKiB,KAAKG,IAAIP,KAAKF,OAAQX,IAJ7B,MAMrB,CAOArB,oBAAoBH,KAAMI,UACxB,MAAMoC,OAACA,OAAMG,OAAEA,QAAUvC,SACzB,OAAO,IAAI2B,gBAAgB/B,KAAMI,SAAS4B,MAAO,CAACQ,cAAQG,eAC5D,CAKA1B,aACE,MAAO,CACLQ,KAAM,oBACNO,MAAO9B,KAAK8B,MACZQ,OAAQtC,KAAK+B,QAAQO,OACrBG,OAAQzC,KAAK+B,QAAQU,OAEzB,CAMA3B,UAAsB,IAAd6B,QAAOX,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,CAAA,EAChB,OAAOhC,KAAK4C,iBAAiBD,SAAS7B,SACxC,CAMA8B,mBAA+B,IAAdD,QAAOX,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,CAAA,EACzB,MAAMG,KAA8BnC,KAAKF,KAAKsC,YACxCS,OAAQ,EAAAC,YAAUA,YAACX,KAAMnC,KAAK8B,MAAO,IACtC9B,KAAK+B,QACRgB,KAAMJ,QAAQI,OAGhB,IAAKF,MACH,MAAM,IAAIxC,MAAM,mBAGlB,OAAO,IAAIqB,mBAAmB1B,KAAKF,KAAM+C,MAAMzB,MAAOyB,MAAMvB,IAC9D,EACDG,SAAAI,gBAAAA,eAAA"} \ No newline at end of file diff --git a/amd/build/view.min.js b/amd/build/view.min.js deleted file mode 100644 index e69de29..0000000 diff --git a/amd/build/view.min.js.map b/amd/build/view.min.js.map deleted file mode 100644 index e69de29..0000000 diff --git a/amd/build/xpath.min.js b/amd/build/xpath.min.js new file mode 100644 index 0000000..96f9de2 --- /dev/null +++ b/amd/build/xpath.min.js @@ -0,0 +1,3 @@ +define("mod_annopy/xpath",["exports"],(function(_exports){function getPathSegment(node){const name=function(node){const nodeName=node.nodeName.toLowerCase();let result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){let pos=0,tmp=node;for(;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();let matchIndex=-1;for(let i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return function(xpath,root){const isSimpleXPath=null!==xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/);if(!isSimpleXPath)throw new Error("Expression is not a simple XPath");const segments=xpath.split("/");let element=root;segments.shift();for(let segment of segments){let elementName,elementIndex;const separatorPos=segment.indexOf("[");if(-1!==separatorPos){elementName=segment.slice(0,separatorPos);const indexStr=segment.slice(separatorPos+1,segment.indexOf("]"));if(elementIndex=parseInt(indexStr)-1,elementIndex<0)return null}else elementName=segment,elementIndex=0;const child=nthChildOfType(element,elementName,elementIndex);if(!child)return null;element=child}return element}(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}},_exports.xpathFromNode=function(node,root){let xpath="",elem=node;for(;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath="/"+xpath,xpath=xpath.replace(/\/$/,""),xpath}})); + +//# sourceMappingURL=xpath.min.js.map \ No newline at end of file diff --git a/amd/build/xpath.min.js.map b/amd/build/xpath.min.js.map new file mode 100644 index 0000000..21ed853 --- /dev/null +++ b/amd/build/xpath.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"xpath.min.js","sources":["../src/xpath.js"],"sourcesContent":["/**\n * XPATH and DOM functions used for anchoring and highlighting.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\n/**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\nfunction getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n}\n\n/**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\nfunction getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n}\n\n/**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\nfunction getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n}\n\n/**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\nexport function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n}\n\n/**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element} - The child element or null\n */\nfunction nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\nfunction evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath =\n xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n}\n\n/**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\nexport function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // Nb. The `namespaceResolver` and `result` arguments are optional in the spec but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n}\n"],"names":["getPathSegment","node","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","concat","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","length","child","xpath","root","arguments","undefined","document","body","isSimpleXPath","match","Error","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","evaluateSimpleXPath","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","elem","parentNode","replace"],"mappings":"0DAgDA,SAASA,eAAeC,MACtB,MAAMC,KAnCR,SAAqBD,MACnB,MAAME,SAAWF,KAAKE,SAASC,cAC/B,IAAIC,OAASF,SAIb,MAHiB,UAAbA,WACFE,OAAS,UAEJA,MACT,CA4BeC,CAAYL,MACnBM,IArBR,SAAyBN,MACvB,IAAIM,IAAM,EAENC,IAAMP,KACV,KAAOO,KACDA,IAAIL,WAAaF,KAAKE,WACxBI,KAAO,GAETC,IAAMA,IAAIC,gBAEZ,OAAOF,GACT,CAUcG,CAAgBT,MAC5B,MAAA,GAAAU,OAAUT,KAAIS,KAAAA,OAAIJ,IAAG,IACvB,CAqCA,SAASK,eAAeC,QAASV,SAAUW,OACzCX,SAAWA,SAASY,cAEpB,IAAIC,YAAc,EAClB,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,QAAQK,SAASC,OAAQF,IAAK,CAChD,MAAMG,MAAQP,QAAQK,SAASD,GAC/B,GAAIG,MAAMjB,SAASY,gBAAkBZ,aACjCa,WACEA,aAAeF,OACjB,OAAOM,KAGb,CAEA,OAAO,IACT,gFA4EO,SAAuBC,OAA6B,IAAtBC,KAAIC,UAAAJ,OAAAI,QAAAC,IAAAD,UAAAC,GAAAD,UAAGE,GAAAA,SAASC,KACnD,IACE,OAvDJ,SAA6BL,MAAOC,MAClC,MAAMK,cACiD,OAArDN,MAAMO,MAAM,qCACd,IAAKD,cACH,MAAM,IAAIE,MAAM,oCAGlB,MAAMC,SAAWT,MAAMU,MAAM,KAC7B,IAAIlB,QAAUS,KAIdQ,SAASE,QAET,IAAK,IAAIC,WAAWH,SAAU,CAC5B,IAAII,YACAC,aAEJ,MAAMC,aAAeH,QAAQI,QAAQ,KACrC,IAAsB,IAAlBD,aAAqB,CACvBF,YAAcD,QAAQK,MAAM,EAAGF,cAE/B,MAAMG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,MAEjE,GADAF,aAAeK,SAASD,UAAY,EAChCJ,aAAe,EACjB,OAAO,IAEX,MACED,YAAcD,QACdE,aAAe,EAGjB,MAAMf,MAAQR,eAAeC,QAASqB,YAAaC,cACnD,IAAKf,MACH,OAAO,KAGTP,QAAUO,KACZ,CAEA,OAAOP,OACT,CAcW4B,CAAoBpB,MAAOC,KACnC,CAAC,MAAOoB,KACP,OAAOjB,SAASkB,SACd,IAAMtB,MACNC,KAGA,KACAsB,YAAYC,wBACZ,MACAC,eACJ,CACF,yBApIO,SAAuB7C,KAAMqB,MAClC,IAAID,MAAQ,GAGR0B,KAAO9C,KACX,KAAO8C,OAASzB,MAAM,CACpB,IAAKyB,KACH,MAAM,IAAIlB,MAAM,oCAElBR,MAAQrB,eAAe+C,MAAQ,IAAM1B,MACrC0B,KAAOA,KAAKC,UACd,CAIA,OAHA3B,MAAQ,IAAMA,MACdA,MAAQA,MAAM4B,QAAQ,MAAO,IAEtB5B,KACT,CAoHC"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js new file mode 100644 index 0000000..3871ca1 --- /dev/null +++ b/amd/src/annotations.js @@ -0,0 +1,308 @@ +// 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 . + +/** + * Module for the annotation functions of the module. + * + * @module mod_annopy/annotations + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import $ from 'jquery'; +import {removeAllTempHighlights, anchor, describe} from './highlighting'; + +export const init = (cmid, canaddannotation, myuserid, focusannotation) => { + + var edited = false; + var annotations = Array(); + + var newannotation = false; + + // Remove col-mds from moodle form. + $('.annotation-form div.col-md-3').removeClass('col-md-3'); + $('.annotation-form div.col-md-9').removeClass('col-md-9'); + $('.annotation-form div.form-group').removeClass('form-group'); + $('.annotation-form div.row').removeClass('row'); + + // Onclick listener if form is canceled. + $(document).on('click', '.annopy_submission #id_cancel', function(e) { + e.preventDefault(); + + removeAllTempHighlights(); // Remove other temporary highlights. + + resetForms(); // Remove old form contents. + + edited = false; + }); + + // Listen for return key pressed to submit annotation form. + $('.annopy_submission textarea').keypress(function(e) { + if (e.which == 13) { + $(this).parents(':eq(2)').submit(); + e.preventDefault(); + } + }); + + // If user selects text for new annotation + $(document).on('mouseup', '.originaltext', function() { + + var selectedrange = window.getSelection().getRangeAt(0); + + if (selectedrange.cloneContents().textContent !== '' && canaddannotation) { + + removeAllTempHighlights(); // Remove other temporary highlights. + + resetForms(); // Reset the annotation forms. + + // Create new annotation. + newannotation = createAnnotation(this); + + var submission = this.id.replace(/submission-/, ''); + + // RangeSelector. + $('.annotation-form-' + submission + ' input[name="startcontainer"]').val( + newannotation.target[0].selector[0].startContainer); + $('.annotation-form-' + submission + ' input[name="endcontainer"]').val( + newannotation.target[0].selector[0].endContainer); + $('.annotation-form-' + submission + ' input[name="startoffset"]').val( + newannotation.target[0].selector[0].startOffset); + $('.annotation-form-' + submission + ' input[name="endoffset"]').val( + newannotation.target[0].selector[0].endOffset); + + // TextPositionSelector. + $('.annotation-form-' + submission + ' input[name="annotationstart"]').val( + newannotation.target[0].selector[1].start); + $('.annotation-form-' + submission + ' input[name="annotationend"]').val( + newannotation.target[0].selector[1].end); + + // TextQuoteSelector. + $('.annotation-form-' + submission + ' input[name="exact"]').val( + newannotation.target[0].selector[2].exact); + $('.annotation-form-' + submission + ' input[name="prefix"]').val( + newannotation.target[0].selector[2].prefix); + $('.annotation-form-' + submission + ' input[name="suffix"]').val( + newannotation.target[0].selector[2].suffix); + + $('.annotation-form-' + submission + ' select').val(1); + + // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags). + $('#annotationpreview-temp-' + submission).html( + newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>')); + + $('.annotationarea-' + submission + ' .annotation-form').show(); + $('.annotation-form-' + submission + ' #id_text').focus(); + } + }); + + // Fetch and recreate annotations. + $.ajax({ + url: './annotations.php', + data: {'id': cmid, 'getannotations': 1}, + success: function(response) { + annotations = JSON.parse(response); + + recreateAnnotations(); + + // Highlight annotation and all annotated text if annotated text is hovered + $('.annotated').mouseenter(function() { + var id = this.id.replace('annotated-', ''); + $('.annotation-box-' + id).addClass('hovered'); + $('.annotated-' + id).css("background-color", 'lightblue'); + }); + + $('.annotated').mouseleave(function() { + var id = this.id.replace('annotated-', ''); + $('.annotation-box-' + id).removeClass('hovered'); + $('.annotated-' + id).css("background-color", $('.annotated-' + id).css('textDecorationColor')); + }); + + // Highlight whole temp annotation if part of temp annotation is hovered + $(document).on('mouseover', '.annotated_temp', function() { + $('.annotated_temp').addClass('hovered'); + }); + + $(document).on('mouseleave', '.annotated_temp', function() { + $('.annotated_temp').removeClass('hovered'); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.annotated', function() { + var id = this.id.replace('annotated-', ''); + editAnnotation(id); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.edit-annotation', function() { + var id = this.id.replace('edit-annotation-', ''); + editAnnotation(id); + }); + + // Highlight annotation if hoverannotation button is hovered + $(document).on('mouseover', '.hoverannotation', function() { + var id = this.id.replace('hoverannotation-', ''); + $('.annotated-' + id).css("background-color", 'lightblue'); + }); + + $(document).on('mouseleave', '.hoverannotation', function() { + var id = this.id.replace('hoverannotation-', ''); + $('.annotated-' + id).css("background-color", $('.annotated-' + id).css('textDecorationColor')); + }); + + + // Focus annotation if needed. + if (focusannotation != 0) { + $('.annotated-' + focusannotation).attr('tabindex', -1); + $('.annotated-' + focusannotation).focus(); + } + + }, + complete: function() { + $('#overlay').hide(); + }, + error: function() { + // For output: alert('Error fetching annotations'); + } + }); + + /** + * Recreate annotations. + * + */ + function recreateAnnotations() { + + for (let annotation of Object.values(annotations)) { + + const rangeSelectors = [[ + {type: "RangeSelector", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset), + endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)}, + {type: "TextPositionSelector", start: parseInt(annotation.annotationstart), + end: parseInt(annotation.annotationend)}, + {type: "TextQuoteSelector", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix} + ]]; + + const target = rangeSelectors.map(selectors => ({ + selector: selectors, + })); + + /** @type {AnnotationData} */ + const newannotation = { + annotation: annotation, + target: target, + }; + + anchor(newannotation, $("#submission-" + annotation.submission)[0]); + } + } + + /** + * Edit annotation. + * + * @param {int} annotationid + */ + function editAnnotation(annotationid) { + + if (edited == annotationid) { + removeAllTempHighlights(); // Remove other temporary highlights. + resetForms(); // Remove old form contents. + edited = false; + } else if (canaddannotation && myuserid == annotations[annotationid].userid) { + removeAllTempHighlights(); // Remove other temporary highlights. + resetForms(); // Remove old form contents. + + edited = annotationid; + + var submission = annotations[annotationid].submission; + + $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box. + + $('.annotation-form-' + submission + ' input[name="startcontainer"]').val(annotations[annotationid].startcontainer); + $('.annotation-form-' + submission + ' input[name="endcontainer"]').val(annotations[annotationid].endcontainer); + $('.annotation-form-' + submission + ' input[name="startoffset"]').val(annotations[annotationid].startoffset); + $('.annotation-form-' + submission + ' input[name="endoffset"]').val(annotations[annotationid].endoffset); + $('.annotation-form-' + submission + ' input[name="annotationstart"]').val(annotations[annotationid].annotationstart); + $('.annotation-form-' + submission + ' input[name="annotationend"]').val(annotations[annotationid].annotationend); + $('.annotation-form-' + submission + ' input[name="exact"]').val(annotations[annotationid].exact); + $('.annotation-form-' + submission + ' input[name="prefix"]').val(annotations[annotationid].prefix); + $('.annotation-form-' + submission + ' input[name="suffix"]').val(annotations[annotationid].suffix); + + $('.annotation-form-' + submission + ' input[name="annotationid"]').val(annotationid); + + $('.annotation-form-' + submission + ' textarea[name="text"]').val(annotations[annotationid].text); + + $('.annotation-form-' + submission + ' select').val(annotations[annotationid].type); + + // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags). + $('#annotationpreview-temp-' + submission).html( + annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>')); + $('#annotationpreview-temp-' + submission).css('border-color', '#' + annotations[annotationid].color); + + $('.annotationarea-' + submission + ' .annotation-form').insertBefore('.annotation-box-' + annotationid); + $('.annotationarea-' + submission + ' .annotation-form').show(); + $('.annotationarea-' + submission + ' #id_text').focus(); + } else { + $('.annotation-box-' + annotationid).focus(); + } + } + + /** + * Reset all annotation forms + */ + function resetForms() { + $('.annotation-form').hide(); + + $('.annotation-form input[name^="annotationid"]').val(null); + + $('.annotation-form input[name^="startcontainer"]').val(-1); + $('.annotation-form input[name^="endcontainer"]').val(-1); + $('.annotation-form input[name^="startoffset"]').val(-1); + $('.annotation-form input[name^="endoffset"]').val(-1); + + $('.annotation-form textarea[name^="text"]').val(''); + + $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation. + } +}; + +/** + * Create a new annotation that is associated with the selected region of + * the current document. + * + * @param {object} root - The root element + * @return {object} - The new annotation + */ +function createAnnotation(root) { + + const ranges = [window.getSelection().getRangeAt(0)]; + + if (ranges.collapsed) { + return null; + } + + const rangeSelectors = ranges.map(range => describe(root, range)); + + const target = rangeSelectors.map(selectors => ({ + selector: selectors, + })); + + /** @type {AnnotationData} */ + const annotation = { + target, + }; + + anchor(annotation, root); + + return annotation; +} \ No newline at end of file diff --git a/amd/src/highlighting.js b/amd/src/highlighting.js new file mode 100644 index 0000000..fbd4d39 --- /dev/null +++ b/amd/src/highlighting.js @@ -0,0 +1,459 @@ +/** + * Functions for the highlighting and anchoring of annotations. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + +import $ from 'jquery'; +import {RangeAnchor, TextPositionAnchor, TextQuoteAnchor} from './types'; +import {TextRange} from './text-range'; + +/** + * Get anchors for new annnotation. + * + * @param {Element} root + * @param {Range} range + * @return {object} - Array with the anchors. + */ +export function describe(root, range) { + const types = [RangeAnchor, TextPositionAnchor, TextQuoteAnchor]; + const result = []; + + for (let type of types) { + try { + const anchor = type.fromRange(root, range); + + result.push(anchor.toSelector()); + } catch (error) { + continue; + } + } + return result; +} + +/** + * Anchor an annotation's selectors in the document. + * + * _Anchoring_ resolves a set of selectors to a concrete region of the document + * which is then highlighted. + * + * Any existing anchors associated with `annotation` will be removed before + * re-anchoring the annotation. + * + * @param {AnnotationData} annotation + * @param {obj} root + * @return {obj} achor object + */ + export function anchor(annotation, root) { + /** + * Resolve an annotation's selectors to a concrete range. + * + * @param {Target} target + * @return {obj} + */ + const locate = target => { + + // Only annotations with an associated quote can currently be anchored. + // This is because the quote is used to verify anchoring with other selector + // types. + if ( + !target.selector || + !target.selector.some(s => s.type === 'TextQuoteSelector') + ) { + return {annotation, target}; + } + + /** @type {Anchor} */ + let anchor; + try { + const range = htmlAnchor(root, target.selector); + // Convert the `Range` to a `TextRange` which can be converted back to + // a `Range` later. The `TextRange` representation allows for highlights + // to be inserted during anchoring other annotations without "breaking" + // this anchor. + + + const textRange = TextRange.fromRange(range); + + anchor = {annotation, target, range: textRange}; + + } catch (err) { + + anchor = {annotation, target}; + } + + return anchor; + }; + + /** + * Highlight the text range that `anchor` refers to. + * + * @param {Anchor} anchor + */ + const highlight = anchor => { + + const range = resolveAnchor(anchor); + + if (!range) { + return; + } + + let highlights = []; + + if (annotation.annotation) { + highlights = highlightRange(range, annotation.annotation.id, 'annotated', annotation.annotation.color); + } else { + highlights = highlightRange(range, false, 'annotated_temp'); + } + + highlights.forEach(h => { + h._annotation = anchor.annotation; + }); + anchor.highlights = highlights; + + }; + + // Remove existing anchors for this annotation. + // this.detach(annotation, false /* notify */); // To be replaced by own method + + // Resolve selectors to ranges and insert highlights. + if (!annotation.target) { + annotation.target = []; + } + const anchors = annotation.target.map(locate); + + for (let anchor of anchors) { + + highlight(anchor); + } + + // Set flag indicating whether anchoring succeeded. For each target, + // anchoring is successful either if there are no selectors (ie. this is a + // Page Note) or we successfully resolved the selectors to a range. + annotation.$orphan = + anchors.length > 0 && + anchors.every(anchor => anchor.target.selector && !anchor.range); + + return anchors; +} + +/** + * Resolve an anchor's associated document region to a concrete `Range`. + * + * This may fail if anchoring failed or if the document has been mutated since + * the anchor was created in a way that invalidates the anchor. + * + * @param {Anchor} anchor + * @return {Range|null} + */ +function resolveAnchor(anchor) { + + if (!anchor.range) { + return null; + } + try { + return anchor.range.toRange(); + } catch { + return null; + } +} + +/** + * Wraps the DOM Nodes within the provided range with a highlight + * element of the specified class and returns the highlight Elements. + * + * Modified for handling annotations. + * + * @param {Range} range - Range to be highlighted + * @param {int} annotationid - ID of annotation + * @param {string} cssClass - A CSS class to use for the highlight + * @param {string} color - Color of the highlighting + * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect + */ + function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') { + + const textNodes = wholeTextNodesInRange(range); + + // Group text nodes into spans of adjacent nodes. If a group of text nodes are + // adjacent, we only need to create one highlight element for the group. + let textNodeSpans = []; + let prevNode = null; + let currentSpan = null; + + textNodes.forEach(node => { + if (prevNode && prevNode.nextSibling === node) { + currentSpan.push(node); + } else { + currentSpan = [node]; + textNodeSpans.push(currentSpan); + } + prevNode = node; + }); + + // Filter out text node spans that consist only of white space. This avoids + // inserting highlight elements in places that can only contain a restricted + // subset of nodes such as table rows and lists. + const whitespace = /^\s*$/; + textNodeSpans = textNodeSpans.filter(span => + // Check for at least one text node with non-space content. + span.some(node => !whitespace.test(node.nodeValue)) + ); + + // Wrap each text node span with a `` element. + const highlights = /** @type {HighlightElement[]} */ ([]); + + textNodeSpans.forEach(nodes => { + const highlightEl = document.createElement('annopy-highlight'); + highlightEl.className = cssClass; + + if (annotationid) { + highlightEl.className += ' ' + cssClass + '-' + annotationid; + highlightEl.style = "text-decoration:underline; text-decoration-color: #" + color; + highlightEl.id = cssClass + '-' + annotationid; + highlightEl.style.backgroundColor = '#' + color; + } + + const parent = /** @type {Node} */ (nodes[0].parentNode); + parent.replaceChild(highlightEl, nodes[0]); + nodes.forEach(node => highlightEl.appendChild(node)); + + highlights.push(highlightEl); + + }); + + return highlights; +} + +/** + * Return text nodes which are entirely inside `range`. + * + * If a range starts or ends part-way through a text node, the node is split + * and the part inside the range is returned. + * + * @param {Range} range + * @return {Text[]} + */ + function wholeTextNodesInRange(range) { + if (range.collapsed) { + // Exit early for an empty range to avoid an edge case that breaks the algorithm + // below. Splitting a text node at the start of an empty range can leave the + // range ending in the left part rather than the right part. + return []; + } + + /** @type {Node|null} */ + let root = range.commonAncestorContainer; + if (root.nodeType !== Node.ELEMENT_NODE) { + // If the common ancestor is not an element, set it to the parent element to + // ensure that the loop below visits any text nodes generated by splitting + // the common ancestor. + // + // Note that `parentElement` may be `null`. + root = root.parentElement; + } + + if (!root) { + // If there is no root element then we won't be able to insert highlights, + // so exit here. + return []; + } + + const textNodes = []; + const nodeIter = /** @type {Document} */ ( + root.ownerDocument + ).createNodeIterator( + root, + NodeFilter.SHOW_TEXT // Only return `Text` nodes. + ); + let node; + while ((node = nodeIter.nextNode())) { + if (!isNodeInRange(range, node)) { + continue; + } + let text = /** @type {Text} */ (node); + + if (text === range.startContainer && range.startOffset > 0) { + // Split `text` where the range starts. The split will create a new `Text` + // node which will be in the range and will be visited in the next loop iteration. + text.splitText(range.startOffset); + continue; + } + + if (text === range.endContainer && range.endOffset < text.data.length) { + // Split `text` where the range ends, leaving it as the part in the range. + text.splitText(range.endOffset); + } + + textNodes.push(text); + } + + return textNodes; +} + +/** + * Returns true if any part of `node` lies within `range`. + * + * @param {Range} range + * @param {Node} node + * @return {bool} - If node is in range + */ +function isNodeInRange(range, node) { + try { + const length = node.nodeValue?.length ?? node.childNodes.length; + return ( + // Check start of node is before end of range. + range.comparePoint(node, 0) <= 0 && + // Check end of node is after start of range. + range.comparePoint(node, length) >= 0 + ); + } catch (e) { + // `comparePoint` may fail if the `range` and `node` do not share a common + // ancestor or `node` is a doctype. + return false; + } +} + +/** + * @param {RangeAnchor|TextPositionAnchor|TextQuoteAnchor} anchor + * @param {Object} [options] + * @return {obj} - range + */ + function querySelector(anchor, options = {}) { + + return anchor.toRange(options); +} + +/** + * Anchor a set of selectors. + * + * This function converts a set of selectors into a document range. + * It encapsulates the core anchoring algorithm, using the selectors alone or + * in combination to establish the best anchor within the document. + * + * @param {Element} root - The root element of the anchoring context. + * @param {Selector[]} selectors - The selectors to try. + * @param {Object} [options] + * @return {object} the query selector + */ + function htmlAnchor(root, selectors, options = {}) { + let position = null; + let quote = null; + let range = null; + + // Collect all the selectors + for (let selector of selectors) { + switch (selector.type) { + case 'TextPositionSelector': + position = selector; + options.hint = position.start; // TextQuoteAnchor hint + break; + case 'TextQuoteSelector': + quote = selector; + break; + case 'RangeSelector': + range = selector; + break; + } + } + + /** + * Assert the quote matches the stored quote, if applicable + * @param {Range} range + * @return {Range} range + */ + const maybeAssertQuote = range => { + + if (quote?.exact && range.toString() !== quote.exact) { + throw new Error('quote mismatch'); + } else { + return range; + } + }; + + let queryselector = false; + + try { + if (range) { + + let anchor = RangeAnchor.fromSelector(root, range); + + queryselector = querySelector(anchor, options); + + if (queryselector) { + return queryselector; + } else { + return maybeAssertQuote; + } + } + } catch (error) { + try { + if (position) { + + let anchor = TextPositionAnchor.fromSelector(root, position); + + queryselector = querySelector(anchor, options); + if (queryselector) { + return queryselector; + } else { + return maybeAssertQuote; + } + } + } catch (error) { + try { + if (quote) { + + let anchor = TextQuoteAnchor.fromSelector(root, quote); + + queryselector = querySelector(anchor, options); + + return queryselector; + } + } catch (error) { + return false; + } + } + } + return false; +} + +/** + * Remove all temporary highlights under a given root element. + */ + export function removeAllTempHighlights() { + const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); + if (highlights !== undefined && highlights.length != 0) { + removeHighlights(highlights); + } +} + +/** + * Remove highlights from a range previously highlighted with `highlightRange`. + * + * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange` + */ + function removeHighlights(highlights) { + + for (var i = 0; i < highlights.length; i++) { + if (highlights[i].parentNode) { + const children = Array.from(highlights[i].childNodes); + replaceWith(highlights[i], children); + } + } +} + +/** + * Replace a child `node` with `replacements`. + * + * nb. This is like `ChildNode.replaceWith` but it works in older browsers. + * + * @param {ChildNode} node + * @param {Node[]} replacements + */ +function replaceWith(node, replacements) { + const parent = /** @type {Node} */ (node.parentNode); + + replacements.forEach(r => parent.insertBefore(r, node)); + node.remove(); +} \ No newline at end of file diff --git a/amd/src/match-quote.js b/amd/src/match-quote.js new file mode 100644 index 0000000..be6a5f6 --- /dev/null +++ b/amd/src/match-quote.js @@ -0,0 +1,174 @@ +/** + * Functions for quote matching for the annotations and highlighting. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + +import approxSearch from './string-match'; + +/** + * @typedef {import('approx-string-match').Match} StringMatch + */ + +/** + * @typedef Match + * @prop {number} start - Start offset of match in text + * @prop {number} end - End offset of match in text + * @prop {number} score - + * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match + * for the quote and context. + */ + +/** + * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors. + * + * @param {string} text + * @param {string} str + * @param {number} maxErrors + * @return {StringMatch[]} + */ +function search(text, str, maxErrors) { + // Do a fast search for exact matches. The `approx-string-match` library + // doesn't currently incorporate this optimization itself. + let matchPos = 0; + let exactMatches = []; + while (matchPos !== -1) { + matchPos = text.indexOf(str, matchPos); + if (matchPos !== -1) { + exactMatches.push({ + start: matchPos, + end: matchPos + str.length, + errors: 0, + }); + matchPos += 1; + } + } + if (exactMatches.length > 0) { + return exactMatches; + } + + // If there are no exact matches, do a more expensive search for matches + // with errors. + return approxSearch(text, str, maxErrors); +} + +/** + * Compute a score between 0 and 1.0 for the similarity between `text` and `str`. + * + * @param {string} text + * @param {string} str + * @return {int} + */ +function textMatchScore(text, str) { + // `search` will return no matches if either the text or pattern is empty, + // otherwise it will return at least one match if the max allowed error count + // is at least `str.length`. + if (str.length === 0 || text.length === 0) { + return 0.0; + } + + const matches = search(text, str, str.length); + + // Prettier-ignore. + return 1 - (matches[0].errors / str.length); +} + +/** + * Find the best approximate match for `quote` in `text`. + * + * Returns `null` if no match exceeding the minimum quality threshold was found. + * + * @param {string} text - Document text to search + * @param {string} quote - String to find within `text` + * @param {Object} context - + * Context in which the quote originally appeared. This is used to choose the + * best match. + * @param {string} [context.prefix] - Expected text before the quote + * @param {string} [context.suffix] - Expected text after the quote + * @param {number} [context.hint] - Expected offset of match within text + * @return {Match|null} + */ +export function matchQuote(text, quote, context = {}) { + if (quote.length === 0) { + return null; + } + + // Choose the maximum number of errors to allow for the initial search. + // This choice involves a tradeoff between: + // + // - Recall (proportion of "good" matches found) + // - Precision (proportion of matches found which are "good") + // - Cost of the initial search and of processing the candidate matches [1] + // + // [1] Specifically, the expected-time complexity of the initial search is + // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs. + const maxErrors = Math.min(256, quote.length / 2); + + // Find closest matches for `quote` in `text` based on edit distance. + const matches = search(text, quote, maxErrors); + + if (matches.length === 0) { + return null; + } + + /** + * Compute a score between 0 and 1.0 for a match candidate. + * + * @param {StringMatch} match + * @return {int} + */ + const scoreMatch = match => { + const quoteWeight = 50; // Similarity of matched text to quote. + const prefixWeight = 20; // Similarity of text before matched text to `context.prefix`. + const suffixWeight = 20; // Similarity of text after matched text to `context.suffix`. + const posWeight = 2; // Proximity to expected location. Used as a tie-breaker. + + const quoteScore = 1 - match.errors / quote.length; + + const prefixScore = context.prefix + ? textMatchScore( + text.slice( + Math.max(0, match.start - context.prefix.length), + match.start + ), + context.prefix + ) + : 1.0; + const suffixScore = context.suffix + ? textMatchScore( + text.slice(match.end, match.end + context.suffix.length), + context.suffix + ) + : 1.0; + + let posScore = 1.0; + if (typeof context.hint === 'number') { + const offset = Math.abs(match.start - context.hint); + posScore = 1.0 - offset / text.length; + } + + const rawScore = + quoteWeight * quoteScore + + prefixWeight * prefixScore + + suffixWeight * suffixScore + + posWeight * posScore; + const maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight; + const normalizedScore = rawScore / maxScore; + + return normalizedScore; + }; + + // Rank matches based on similarity of actual and expected surrounding text + // and actual/expected offset in the document text. + const scoredMatches = matches.map(m => ({ + start: m.start, + end: m.end, + score: scoreMatch(m), + })); + + // Choose match with highest score. + scoredMatches.sort((a, b) => b.score - a.score); + return scoredMatches[0]; +} diff --git a/amd/src/string-match.js b/amd/src/string-match.js new file mode 100644 index 0000000..8637003 --- /dev/null +++ b/amd/src/string-match.js @@ -0,0 +1,339 @@ +/** + * Functions for string matching used by the other methods. + * + * This code originaly is from the approx-string-match project (https://github.com/robertknight/approx-string-match-js) + * by Robert Knight wich is released under the MIT License (https://opensource.org/licenses/MIT). + */ + +/** + * Represents a match returned by a call to `search`. + * @param {string} s - Document text to search + * @return {string} + */ + function reverse(s) { + return s.split("").reverse().join(""); + } + + /** + * Given the ends of approximate matches for `pattern` in `text`, find + * the start of the matches. + * + * @param {string} text + * @param {string} pattern + * @param {array} matches + * @return {obj} Matches with the `start` property set. + */ + function findMatchStarts(text, pattern, matches) { + const patRev = reverse(pattern); + + return matches.map((m) => { + // Find start of each match by reversing the pattern and matching segment + // of text and searching for an approx match with the same number of + // errors. + const minStart = Math.max(0, m.end - pattern.length - m.errors); + const textRev = reverse(text.slice(minStart, m.end)); + + // If there are multiple possible start points, choose the one that + // maximizes the length of the match. + const start = findMatchEnds(textRev, patRev, m.errors).reduce((min, rm) => { + if (m.end - rm.end < min) { + return m.end - rm.end; + } + return min; + }, m.end); + + return { + start, + end: m.end, + errors: m.errors, + }; + }); + } + + /** + * Internal context used when calculating blocks of a column. + */ + // interface Context { + // /** + // * Bit-arrays of positive vertical deltas. + // * + // * ie. `P[b][i]` is set if the vertical delta for the i'th row in the b'th + // * block is positive. + // */ + // P: Uint32Array; + // /** Bit-arrays of negative vertical deltas. */ + // M: Uint32Array; + // /** Bit masks with a single bit set indicating the last row in each block. */ + // lastRowMask: Uint32Array; + // } + + /** + * Return 1 if a number is non-zero or zero otherwise, without using + * conditional operators. + * + * This should get inlined into `advanceBlock` below by the JIT. + * + * Adapted from https://stackoverflow.com/a/3912218/434243 + * @param {int} n + * @return {bool} + */ + function oneIfNotZero(n) { + return ((n | -n) >> 31) & 1; + } + + /** + * Block calculation step of the algorithm. + * + * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional + * checks with bitwise operations as per Section 4.2.3 of [2]. + * + * @param {obj} ctx - The pattern context object + * @param {array} peq - The `peq` array for the current character (`ctx.peq.get(ch)`) + * @param {int} b - The block level + * @param {obj} hIn - Horizontal input delta ∈ {1,0,-1} + * @return {obj} Horizontal output delta ∈ {1,0,-1} + */ + function advanceBlock(ctx, peq, b, hIn) { + let pV = ctx.P[b]; + let mV = ctx.M[b]; + const hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise. + const eq = peq[b] | hInIsNegative; + + // Step 1: Compute horizontal deltas. + const xV = eq | mV; + const xH = (((eq & pV) + pV) ^ pV) | eq; + + let pH = mV | ~(xH | pV); + let mH = pV & xH; + + // Step 2: Update score (value of last row of this block). + const hOut = + oneIfNotZero(pH & ctx.lastRowMask[b]) - + oneIfNotZero(mH & ctx.lastRowMask[b]); + + // Step 3: Update vertical deltas for use when processing next char. + pH <<= 1; + mH <<= 1; + + mH |= hInIsNegative; + pH |= oneIfNotZero(hIn) - hInIsNegative; // Set pH[0] if hIn > 0. + + pV = mH | ~(xV | pH); + mV = pH & xV; + + ctx.P[b] = pV; + ctx.M[b] = mV; + + return hOut; + } + + /** + * Find the ends and error counts for matches of `pattern` in `text`. + * + * Only the matches with the lowest error count are reported. Other matches + * with error counts <= maxErrors are discarded. + * + * This is the block-based search algorithm from Fig. 9 on p.410 of [1]. + * + * @param {string} text + * @param {string} pattern + * @param {array} maxErrors + * @return {obj} Matches with the `start` property set. + */ + function findMatchEnds(text, pattern, maxErrors) { + if (pattern.length === 0) { + return []; + } + + // Clamp error count so we can rely on the `maxErrors` and `pattern.length` + // rows being in the same block below. + maxErrors = Math.min(maxErrors, pattern.length); + + const matches = []; + + // Word size. + const w = 32; + + // Index of maximum block level. + const bMax = Math.ceil(pattern.length / w) - 1; + + // Context used across block calculations. + const ctx = { + P: new Uint32Array(bMax + 1), + M: new Uint32Array(bMax + 1), + lastRowMask: new Uint32Array(bMax + 1), + }; + ctx.lastRowMask.fill(1 << 31); + ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w; + + // Dummy "peq" array for chars in the text which do not occur in the pattern. + const emptyPeq = new Uint32Array(bMax + 1); + + // Map of UTF-16 character code to bit vector indicating positions in the + // pattern that equal that character. + const peq = new Map(); + + // Version of `peq` that only stores mappings for small characters. This + // allows faster lookups when iterating through the text because a simple + // array lookup can be done instead of a hash table lookup. + const asciiPeq = []; + for (let i = 0; i < 256; i++) { + asciiPeq.push(emptyPeq); + } + + // Calculate `ctx.peq` - a map of character values to bitmasks indicating + // positions of that character within the pattern, where each bit represents + // a position in the pattern. + for (let c = 0; c < pattern.length; c += 1) { + const val = pattern.charCodeAt(c); + if (peq.has(val)) { + // Duplicate char in pattern. + continue; + } + + const charPeq = new Uint32Array(bMax + 1); + peq.set(val, charPeq); + if (val < asciiPeq.length) { + asciiPeq[val] = charPeq; + } + + for (let b = 0; b <= bMax; b += 1) { + charPeq[b] = 0; + + // Set all the bits where the pattern matches the current char (ch). + // For indexes beyond the end of the pattern, always set the bit as if the + // pattern contained a wildcard char in that position. + for (let r = 0; r < w; r += 1) { + const idx = b * w + r; + if (idx >= pattern.length) { + continue; + } + + const match = pattern.charCodeAt(idx) === val; + if (match) { + charPeq[b] |= 1 << r; + } + } + } + } + + // Index of last-active block level in the column. + let y = Math.max(0, Math.ceil(maxErrors / w) - 1); + + // Initialize maximum error count at bottom of each block. + const score = new Uint32Array(bMax + 1); + for (let b = 0; b <= y; b += 1) { + score[b] = (b + 1) * w; + } + score[bMax] = pattern.length; + + // Initialize vertical deltas for each block. + for (let b = 0; b <= y; b += 1) { + ctx.P[b] = ~0; + ctx.M[b] = 0; + } + + // Process each char of the text, computing the error count for `w` chars of + // the pattern at a time. + for (let j = 0; j < text.length; j += 1) { + // Lookup the bitmask representing the positions of the current char from + // the text within the pattern. + const charCode = text.charCodeAt(j); + let charPeq; + + if (charCode < asciiPeq.length) { + // Fast array lookup. + charPeq = asciiPeq[charCode]; + } else { + // Slower hash table lookup. + charPeq = peq.get(charCode); + if (typeof charPeq === "undefined") { + charPeq = emptyPeq; + } + } + + // Calculate error count for blocks that we definitely have to process for + // this column. + let carry = 0; + for (let b = 0; b <= y; b += 1) { + carry = advanceBlock(ctx, charPeq, b, carry); + score[b] += carry; + } + + // Check if we also need to compute an additional block, or if we can reduce + // the number of blocks processed for the next column. + if ( + score[y] - carry <= maxErrors && + y < bMax && + (charPeq[y + 1] & 1 || carry < 0) + ) { + // Error count for bottom block is under threshold, increase the number of + // blocks processed for this column & next by 1. + y += 1; + + ctx.P[y] = ~0; + ctx.M[y] = 0; + + let maxBlockScore; + if (y === bMax) { + const remainder = pattern.length % w; + maxBlockScore = remainder === 0 ? w : remainder; + } else { + maxBlockScore = w; + } + + score[y] = + score[y - 1] + + maxBlockScore - + carry + + advanceBlock(ctx, charPeq, y, carry); + } else { + // Error count for bottom block exceeds threshold, reduce the number of + // blocks processed for the next column. + while (y > 0 && score[y] >= maxErrors + w) { + y -= 1; + } + } + + // If error count is under threshold, report a match. + if (y === bMax && score[y] <= maxErrors) { + if (score[y] < maxErrors) { + // Discard any earlier, worse matches. + matches.splice(0, matches.length); + } + + matches.push({ + start: -1, + end: j + 1, + errors: score[y], + }); + + // Because `search` only reports the matches with the lowest error count, + // we can "ratchet down" the max error threshold whenever a match is + // encountered and thereby save a small amount of work for the remainder + // of the text. + maxErrors = score[y]; + } + } + + return matches; + } + + /** + * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors. + * + * Returns the start, and end positions and error counts for each lowest-cost + * match. Only the "best" matches are returned. + * @param {string} text + * @param {string} pattern + * @param {array} maxErrors + * @return {obj} Matches with the `start` property set. + */ + export default function search( + text, + pattern, + maxErrors + ) { + const matches = findMatchEnds(text, pattern, maxErrors); + return findMatchStarts(text, pattern, matches); + } \ No newline at end of file diff --git a/amd/src/text-range.js b/amd/src/text-range.js new file mode 100644 index 0000000..3f9d6d7 --- /dev/null +++ b/amd/src/text-range.js @@ -0,0 +1,346 @@ +/** + * Functions for handling text-ranges used by the other methods. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + +/** + * Return the combined length of text nodes contained in `node`. + * + * @param {Node} node + * @return {string} + */ +function nodeTextLength(node) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: + case Node.TEXT_NODE: + // Nb. `textContent` excludes text in comments and processing instructions + // when called on a parent element, so we don't need to subtract that here. + + return /** @type {string} */ (node.textContent).length; + default: + return 0; + } +} + +/** + * Return the total length of the text of all previous siblings of `node`. + * + * @param {Node} node + * @return {int} + */ +function previousSiblingsTextLength(node) { + let sibling = node.previousSibling; + let length = 0; + + while (sibling) { + length += nodeTextLength(sibling); + sibling = sibling.previousSibling; + } + + return length; +} + +/** + * Resolve one or more character offsets within an element to (text node, position) + * pairs. + * + * @param {Element} element + * @param {number[]} offsets - Offsets, which must be sorted in ascending order + * @return {{ node: Text, offset: number }[]} + */ +function resolveOffsets(element, ...offsets) { + + let nextOffset = offsets.shift(); + const nodeIter = /** @type {Document} */ ( + element.ownerDocument + ).createNodeIterator(element, NodeFilter.SHOW_TEXT); + const results = []; + + let currentNode = nodeIter.nextNode(); + let textNode; + let length = 0; + + // Find the text node containing the `nextOffset`th character from the start + // of `element`. + while (nextOffset !== undefined && currentNode) { + textNode = /** @type {Text} */ (currentNode); + + if (length + textNode.data.length > nextOffset) { + results.push({node: textNode, offset: nextOffset - length}); + nextOffset = offsets.shift(); + } else { + currentNode = nodeIter.nextNode(); + length += textNode.data.length; + } + } + + // Boundary case. + while (nextOffset !== undefined && length === nextOffset) { + results.push({node: textNode, offset: textNode.data.length}); + nextOffset = offsets.shift(); + } + + if (nextOffset !== undefined) { + throw new RangeError('Offset exceeds text length'); + } + + return results; +} + +export let RESOLVE_FORWARDS = 1; +export let RESOLVE_BACKWARDS = 2; + +/** + * Represents an offset within the text content of an element. + * + * This position can be resolved to a specific descendant node in the current + * DOM subtree of the element using the `resolve` method. + */ +export class TextPosition { + /** + * Construct a `TextPosition` that refers to the text position `offset` within + * the text content of `element`. + * + * @param {Element} element + * @param {number} offset + */ + constructor(element, offset) { + if (offset < 0) { + throw new Error('Offset is invalid'); + } + + /** Element that `offset` is relative to. */ + this.element = element; + + /** Character offset from the start of the element's `textContent`. */ + this.offset = offset; + } + + /** + * Return a copy of this position with offset relative to a given ancestor + * element. + * + * @param {Element} parent - Ancestor of `this.element` + * @return {TextPosition} + */ + relativeTo(parent) { + if (!parent.contains(this.element)) { + throw new Error('Parent is not an ancestor of current element'); + } + + let el = this.element; + let offset = this.offset; + while (el !== parent) { + offset += previousSiblingsTextLength(el); + el = /** @type {Element} */ (el.parentElement); + } + + return new TextPosition(el, offset); + } + + /** + * Resolve the position to a specific text node and offset within that node. + * + * Throws if `this.offset` exceeds the length of the element's text. In the + * case where the element has no text and `this.offset` is 0, the `direction` + * option determines what happens. + * + * Offsets at the boundary between two nodes are resolved to the start of the + * node that begins at the boundary. + * + * @param {Object} [options] + * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] - + * Specifies in which direction to search for the nearest text node if + * `this.offset` is `0` and `this.element` has no text. If not specified + * an error is thrown. + * @return {{ node: Text, offset: number }} + * @throws {RangeError} + */ + resolve(options = {}) { + try { + return resolveOffsets(this.element, this.offset)[0]; + } catch (err) { + if (this.offset === 0 && options.direction !== undefined) { + const tw = document.createTreeWalker( + this.element.getRootNode(), + NodeFilter.SHOW_TEXT + ); + tw.currentNode = this.element; + const forwards = options.direction === RESOLVE_FORWARDS; + const text = /** @type {Text|null} */ ( + forwards ? tw.nextNode() : tw.previousNode() + ); + if (!text) { + throw err; + } + return {node: text, offset: forwards ? 0 : text.data.length}; + } else { + throw err; + } + } + } + + /** + * Construct a `TextPosition` that refers to the `offset`th character within + * `node`. + * + * @param {Node} node + * @param {number} offset + * @return {TextPosition} + */ + static fromCharOffset(node, offset) { + switch (node.nodeType) { + case Node.TEXT_NODE: + return TextPosition.fromPoint(node, offset); + case Node.ELEMENT_NODE: + return new TextPosition(/** @type {Element} */ (node), offset); + default: + throw new Error('Node is not an element or text node'); + } + } + + /** + * Construct a `TextPosition` representing the range start or end point (node, offset). + * + * @param {Node} node - Text or Element node + * @param {number} offset - Offset within the node. + * @return {TextPosition} + */ + static fromPoint(node, offset) { + + switch (node.nodeType) { + case Node.TEXT_NODE: { + if (offset < 0 || offset > /** @type {Text} */ (node).data.length) { + throw new Error('Text node offset is out of range'); + } + + if (!node.parentElement) { + throw new Error('Text node has no parent'); + } + + // Get the offset from the start of the parent element. + const textOffset = previousSiblingsTextLength(node) + offset; + + return new TextPosition(node.parentElement, textOffset); + } + case Node.ELEMENT_NODE: { + if (offset < 0 || offset > node.childNodes.length) { + throw new Error('Child node offset is out of range'); + } + + // Get the text length before the `offset`th child of element. + let textOffset = 0; + for (let i = 0; i < offset; i++) { + textOffset += nodeTextLength(node.childNodes[i]); + } + + return new TextPosition(/** @type {Element} */ (node), textOffset); + } + default: + throw new Error('Point is not in an element or text node'); + } + } +} + +/** + * Represents a region of a document as a (start, end) pair of `TextPosition` points. + * + * Representing a range in this way allows for changes in the DOM content of the + * range which don't affect its text content, without affecting the text content + * of the range itself. + */ +export class TextRange { + /** + * Construct an immutable `TextRange` from a `start` and `end` point. + * + * @param {TextPosition} start + * @param {TextPosition} end + */ + constructor(start, end) { + this.start = start; + this.end = end; + } + + /** + * Return a copy of this range with start and end positions relative to a + * given ancestor. See `TextPosition.relativeTo`. + * + * @param {Element} element + * @return {Range} + */ + relativeTo(element) { + return new TextRange( + this.start.relativeTo(element), + this.end.relativeTo(element) + ); + } + + /** + * Resolve the `TextRange` to a DOM range. + * + * The resulting DOM Range will always start and end in a `Text` node. + * Hence `TextRange.fromRange(range).toRange()` can be used to "shrink" a + * range to the text it contains. + * + * May throw if the `start` or `end` positions cannot be resolved to a range. + * + * @return {Range} + */ + toRange() { + let start; + let end; + + if ( + this.start.element === this.end.element && + this.start.offset <= this.end.offset + ) { + // Fast path for start and end points in same element. + [start, end] = resolveOffsets( + this.start.element, + this.start.offset, + this.end.offset + ); + } else { + start = this.start.resolve({direction: RESOLVE_FORWARDS}); + end = this.end.resolve({direction: RESOLVE_BACKWARDS}); + } + + const range = new Range(); + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); + return range; + } + + /** + * Convert an existing DOM `Range` to a `TextRange` + * + * @param {Range} range + * @return {TextRange} + */ + static fromRange(range) { + const start = TextPosition.fromPoint( + range.startContainer, + range.startOffset + ); + const end = TextPosition.fromPoint(range.endContainer, range.endOffset); + return new TextRange(start, end); + } + + /** + * Return a `TextRange` from the `start`th to `end`th characters in `root`. + * + * @param {Element} root + * @param {number} start + * @param {number} end + * @return {Range} + */ + static fromOffsets(root, start, end) { + return new TextRange( + new TextPosition(root, start), + new TextPosition(root, end) + ); + } +} diff --git a/amd/src/types.js b/amd/src/types.js new file mode 100644 index 0000000..d77f530 --- /dev/null +++ b/amd/src/types.js @@ -0,0 +1,262 @@ +/** + * This module exports a set of classes for converting between DOM `Range` + * objects and different types of selectors. It is mostly a thin wrapper around a + * set of anchoring libraries. It serves two main purposes: + * + * 1. Providing a consistent interface across different types of anchors. + * 2. Insulating the rest of the code from API changes in the underlying anchoring + * libraries. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + +import {matchQuote} from './match-quote'; +import {TextRange, TextPosition} from './text-range'; +import {nodeFromXPath, xpathFromNode} from './xpath'; + +/** + * @typedef {import('../../types/api').RangeSelector} RangeSelector + * @typedef {import('../../types/api').TextPositionSelector} TextPositionSelector + * @typedef {import('../../types/api').TextQuoteSelector} TextQuoteSelector + */ + +/** + * Converts between `RangeSelector` selectors and `Range` objects. + */ +export class RangeAnchor { + /** + * @param {Node} root - A root element from which to anchor. + * @param {Range} range - A range describing the anchor. + */ + constructor(root, range) { + this.root = root; + this.range = range; + } + + /** + * @param {Node} root - A root element from which to anchor. + * @param {Range} range - A range describing the anchor. + * @return {RangeAnchor} + */ + static fromRange(root, range) { + return new RangeAnchor(root, range); + } + + /** + * Create an anchor from a serialized `RangeSelector` selector. + * + * @param {Element} root - A root element from which to anchor. + * @param {RangeSelector} selector + * @return {RangeAnchor} + */ + static fromSelector(root, selector) { + + const startContainer = nodeFromXPath(selector.startContainer, root); + + if (!startContainer) { + throw new Error('Failed to resolve startContainer XPath'); + } + + const endContainer = nodeFromXPath(selector.endContainer, root); + if (!endContainer) { + throw new Error('Failed to resolve endContainer XPath'); + } + + const startPos = TextPosition.fromCharOffset( + startContainer, + selector.startOffset + ); + const endPos = TextPosition.fromCharOffset( + endContainer, + selector.endOffset + ); + + const range = new TextRange(startPos, endPos).toRange(); + return new RangeAnchor(root, range); + } + + toRange() { + return this.range; + } + + /** + * @return {RangeSelector} + */ + toSelector() { + // "Shrink" the range so that it tightly wraps its text. This ensures more + // predictable output for a given text selection. + + const normalizedRange = TextRange.fromRange(this.range).toRange(); + + const textRange = TextRange.fromRange(normalizedRange); + const startContainer = xpathFromNode(textRange.start.element, this.root); + const endContainer = xpathFromNode(textRange.end.element, this.root); + + return { + type: 'RangeSelector', + startContainer, + startOffset: textRange.start.offset, + endContainer, + endOffset: textRange.end.offset, + }; + } +} + +/** + * Converts between `TextPositionSelector` selectors and `Range` objects. + */ +export class TextPositionAnchor { + /** + * @param {Element} root + * @param {number} start + * @param {number} end + */ + constructor(root, start, end) { + this.root = root; + this.start = start; + this.end = end; + } + + /** + * @param {Element} root + * @param {Range} range + * @return {TextPositionAnchor} + */ + static fromRange(root, range) { + const textRange = TextRange.fromRange(range).relativeTo(root); + return new TextPositionAnchor( + root, + textRange.start.offset, + textRange.end.offset + ); + } + /** + * @param {Element} root + * @param {TextPositionSelector} selector + * @return {TextPositionAnchor} + */ + static fromSelector(root, selector) { + return new TextPositionAnchor(root, selector.start, selector.end); + } + + /** + * @return {TextPositionSelector} + */ + toSelector() { + return { + type: 'TextPositionSelector', + start: this.start, + end: this.end, + }; + } + + toRange() { + return TextRange.fromOffsets(this.root, this.start, this.end).toRange(); + } +} + +/** + * @typedef QuoteMatchOptions + * @prop {number} [hint] - Expected position of match in text. See `matchQuote`. + */ + +/** + * Converts between `TextQuoteSelector` selectors and `Range` objects. + */ +export class TextQuoteAnchor { + /** + * @param {Element} root - A root element from which to anchor. + * @param {string} exact + * @param {Object} context + * @param {string} [context.prefix] + * @param {string} [context.suffix] + */ + constructor(root, exact, context = {}) { + this.root = root; + this.exact = exact; + this.context = context; + } + + /** + * Create a `TextQuoteAnchor` from a range. + * + * Will throw if `range` does not contain any text nodes. + * + * @param {Element} root + * @param {Range} range + * @return {TextQuoteAnchor} + */ + static fromRange(root, range) { + const text = /** @type {string} */ (root.textContent); + const textRange = TextRange.fromRange(range).relativeTo(root); + + const start = textRange.start.offset; + const end = textRange.end.offset; + + // Number of characters around the quote to capture as context. We currently + // always use a fixed amount, but it would be better if this code was aware + // of logical boundaries in the document (paragraph, article etc.) to avoid + // capturing text unrelated to the quote. + // + // In regular prose the ideal content would often be the surrounding sentence. + // This is a natural unit of meaning which enables displaying quotes in + // context even when the document is not available. We could use `Intl.Segmenter` + // for this when available. + const contextLen = 32; + + return new TextQuoteAnchor(root, text.slice(start, end), { + prefix: text.slice(Math.max(0, start - contextLen), start), + suffix: text.slice(end, Math.min(text.length, end + contextLen)), + }); + } + + /** + * @param {Element} root + * @param {TextQuoteSelector} selector + * @return {TextQuoteAnchor} + */ + static fromSelector(root, selector) { + const {prefix, suffix} = selector; + return new TextQuoteAnchor(root, selector.exact, {prefix, suffix}); + } + + /** + * @return {TextQuoteSelector} + */ + toSelector() { + return { + type: 'TextQuoteSelector', + exact: this.exact, + prefix: this.context.prefix, + suffix: this.context.suffix, + }; + } + + /** + * @param {QuoteMatchOptions} [options] + * @return {TextQuoteAnchor} + */ + toRange(options = {}) { + return this.toPositionAnchor(options).toRange(); + } + + /** + * @param {QuoteMatchOptions} [options] + * @return {TextPositionAnchor} + */ + toPositionAnchor(options = {}) { + const text = /** @type {string} */ (this.root.textContent); + const match = matchQuote(text, this.exact, { + ...this.context, + hint: options.hint, + }); + + if (!match) { + throw new Error('Quote not found'); + } + + return new TextPositionAnchor(this.root, match.start, match.end); + } +} diff --git a/amd/src/view.js b/amd/src/view.js deleted file mode 100644 index 4019051..0000000 --- a/amd/src/view.js +++ /dev/null @@ -1,35 +0,0 @@ -// 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 . - -/** - * JavaScript for the main page of the plugin. - * - * @module mod_annopy/view - * @copyright 2023 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -import $ from 'jquery'; -import {get_string as getString} from 'core/str'; - -export const init = (cmid) => { - // getString('logintoannopy', 'mod_annopy') - // .then(buttonString => { - // $('.path-mod-annopy #id_submitbutton').attr('value', buttonString); - // }) - // .catch(); - - return cmid; -}; \ No newline at end of file diff --git a/amd/src/xpath.js b/amd/src/xpath.js new file mode 100644 index 0000000..bec362b --- /dev/null +++ b/amd/src/xpath.js @@ -0,0 +1,195 @@ +/** + * XPATH and DOM functions used for anchoring and highlighting. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + +/** + * Get the node name for use in generating an xpath expression. + * + * @param {Node} node + * @return {string} - Name of the node + */ +function getNodeName(node) { + const nodeName = node.nodeName.toLowerCase(); + let result = nodeName; + if (nodeName === '#text') { + result = 'text()'; + } + return result; +} + +/** + * Get the index of the node as it appears in its parent's child list + * + * @param {Node} node + * @return {int} - Position of the node + */ +function getNodePosition(node) { + let pos = 0; + /** @type {Node|null} */ + let tmp = node; + while (tmp) { + if (tmp.nodeName === node.nodeName) { + pos += 1; + } + tmp = tmp.previousSibling; + } + return pos; +} + +/** + * Get the path segments to the node + * + * @param {Node} node + * @return {array} - Path segments + */ +function getPathSegment(node) { + const name = getNodeName(node); + const pos = getNodePosition(node); + return `${name}[${pos}]`; +} + +/** + * A simple XPath generator which can generate XPaths of the form + * /tag[index]/tag[index]. + * + * @param {Node} node - The node to generate a path to + * @param {Node} root - Root node to which the returned path is relative + * @return {string} - The xpath of a node + */ +export function xpathFromNode(node, root) { + let xpath = ''; + + /** @type {Node|null} */ + let elem = node; + while (elem !== root) { + if (!elem) { + throw new Error('Node is not a descendant of root'); + } + xpath = getPathSegment(elem) + '/' + xpath; + elem = elem.parentNode; + } + xpath = '/' + xpath; + xpath = xpath.replace(/\/$/, ''); // Remove trailing slash + + return xpath; +} + +/** + * Return the `index`'th immediate child of `element` whose tag name is + * `nodeName` (case insensitive). + * + * @param {Element} element + * @param {string} nodeName + * @param {number} index + * @return {Element} - The child element or null + */ +function nthChildOfType(element, nodeName, index) { + nodeName = nodeName.toUpperCase(); + + let matchIndex = -1; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + if (child.nodeName.toUpperCase() === nodeName) { + ++matchIndex; + if (matchIndex === index) { + return child; + } + } + } + + return null; +} + +/** + * Evaluate a _simple XPath_ relative to a `root` element and return the + * matching element. + * + * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings. + * + * Unlike `document.evaluate` this function: + * + * - Only supports simple XPaths + * - Is not affected by the document's _type_ (HTML or XML/XHTML) + * - Ignores element namespaces when matching element names in the XPath against + * elements in the DOM tree + * - Is case insensitive for all elements, not just HTML elements + * + * The matching element is returned or `null` if no such element is found. + * An error is thrown if `xpath` is not a simple XPath. + * + * @param {string} xpath + * @param {Element} root + * @return {Element|null} + */ +function evaluateSimpleXPath(xpath, root) { + const isSimpleXPath = + xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/) !== null; + if (!isSimpleXPath) { + throw new Error('Expression is not a simple XPath'); + } + + const segments = xpath.split('/'); + let element = root; + + // Remove leading empty segment. The regex above validates that the XPath + // has at least two segments, with the first being empty and the others non-empty. + segments.shift(); + + for (let segment of segments) { + let elementName; + let elementIndex; + + const separatorPos = segment.indexOf('['); + if (separatorPos !== -1) { + elementName = segment.slice(0, separatorPos); + + const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']')); + elementIndex = parseInt(indexStr) - 1; + if (elementIndex < 0) { + return null; + } + } else { + elementName = segment; + elementIndex = 0; + } + + const child = nthChildOfType(element, elementName, elementIndex); + if (!child) { + return null; + } + + element = child; + } + + return element; +} + +/** + * Finds an element node using an XPath relative to `root` + * + * Example: + * node = nodeFromXPath('/main/article[1]/p[3]', document.body) + * + * @param {string} xpath + * @param {Element} [root] + * @return {Node|null} + */ +export function nodeFromXPath(xpath, root = document.body) { + try { + return evaluateSimpleXPath(xpath, root); + } catch (err) { + return document.evaluate( + '.' + xpath, + root, + + // Nb. The `namespaceResolver` and `result` arguments are optional in the spec but required in Edge Legacy. + null /* NamespaceResolver */, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null /* Result */ + ).singleNodeValue; + } +} diff --git a/annotation_form.php b/annotation_form.php new file mode 100644 index 0000000..177e965 --- /dev/null +++ b/annotation_form.php @@ -0,0 +1,110 @@ +. + +/** + * File containing the class definition for the annotate form for the module. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("$CFG->libdir/formslib.php"); + +/** + * Form for annotations. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later + */ +class mod_annopy_annotation_form extends moodleform { + + /** + * Define the form - called by parent constructor + */ + public function definition() { + + global $OUTPUT; + + $mform = $this->_form; // Don't forget the underscore! + + $mform->addElement('hidden', 'id', null); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'user', null); + $mform->setType('user', PARAM_INT); + + $mform->addElement('hidden', 'submission', null); + $mform->setType('submission', PARAM_INT); + + $mform->addElement('hidden', 'annotationmode', 1); + $mform->setType('annotationmode', PARAM_INT); + + $mform->addElement('hidden', 'startcontainer', -1); + $mform->setType('startcontainer', PARAM_RAW); + + $mform->addElement('hidden', 'endcontainer', -1); + $mform->setType('endcontainer', PARAM_RAW); + + $mform->addElement('hidden', 'startoffset', -1); + $mform->setType('startoffset', PARAM_INT); + + $mform->addElement('hidden', 'endoffset', -1); + $mform->setType('endoffset', PARAM_INT); + + $mform->addElement('hidden', 'annotationstart', -1); + $mform->setType('annotationstart', PARAM_INT); + + $mform->addElement('hidden', 'annotationend', -1); + $mform->setType('annotationend', PARAM_INT); + + $mform->addElement('hidden', 'annotationid', null); + $mform->setType('annotationid', PARAM_INT); + + $mform->addElement('hidden', 'exact', -1); + $mform->setType('exact', PARAM_RAW); + + $mform->addElement('hidden', 'prefix', -1); + $mform->setType('prefix', PARAM_RAW); + + $mform->addElement('hidden', 'suffix', -1); + $mform->setType('suffix', PARAM_RAW); + + $select = $mform->addElement('select', 'type', '', $this->_customdata['types']); + $mform->setType('type', PARAM_INT); + + $mform->addElement('textarea', 'text'); + $mform->setType('text', PARAM_TEXT); + + $this->add_action_buttons(); + + $mform->disable_form_change_checker(); + } + + /** + * Custom validation should be added here + * @param array $data Array with all the form data + * @param array $files Array with files submitted with form + * @return array Array with errors + */ + public function validation($data, $files) { + return array(); + } +} diff --git a/annotations.php b/annotations.php new file mode 100644 index 0000000..7b669cf --- /dev/null +++ b/annotations.php @@ -0,0 +1,227 @@ +. + +/** + * File for handling the annotation form. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\output\notification; +use mod_annopy\local\helper; + +require(__DIR__.'/../../config.php'); + +global $DB, $CFG; + +// Course Module ID. +$id = required_param('id', PARAM_INT); + +// Param if annotations should be returned via ajax. +$getannotations = optional_param('getannotations', 0, PARAM_INT); + +// Param if annotation should be deleted. +$deleteannotation = optional_param('deleteannotation', 0, PARAM_INT); // Annotation to be deleted. + +// Set the basic variables $course, $cm and $moduleinstance. +if ($id) { + [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); + $moduleinstance = $DB->get_record('annopy', ['id' => $cm->instance], '*', MUST_EXIST); +} else { + throw new moodle_exception('missingparameter'); +} + +if (!$cm) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} else if (!$course) { + throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); +} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); + +$select = "annopy = " . $moduleinstance->id; +$annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); + +// Get annotation (ajax). +if ($getannotations) { + + $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id)); + + $select = "annopy = " . $moduleinstance->id; + + foreach ($annotations as $key => $annotation) { + + if (!array_key_exists($annotation->type, $annotationtypes) && + $DB->record_exists('annopy_annotationtypes', array('id' => $annotation->type))) { + + $annotationtypes[$annotation->type] = $DB->get_record('annopy_annotationtypes', array('id' => $annotation->type)); + } + + if (isset($annotationtypes[$annotation->type])) { + $annotations[$key]->color = $annotationtypes[$annotation->type]->color; + } + + } + + if ($annotations) { + echo json_encode($annotations); + } else { + echo json_encode(array()); + } + + die; +} + +require_capability('mod/annopy:addannotation', $context); + +// Header. +$PAGE->set_url('/mod/annopy/annotations.php', array('id' => $id)); +$PAGE->set_title(format_string($moduleinstance->name)); + +$urlparams = array('id' => $id); + +$redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); + +// Delete annotation. +if (has_capability('mod/annopy:deleteannotation', $context) && $deleteannotation !== 0) { + require_sesskey(); + + global $USER; + + if ($DB->record_exists('annopy_annotations', array('id' => $deleteannotation, 'annopy' => $moduleinstance->id, + 'userid' => $USER->id))) { + + $DB->delete_records('annopy_annotations', array('id' => $deleteannotation, 'annopy' => $moduleinstance->id, + 'userid' => $USER->id)); + + // Trigger module annotation deleted event. + $event = \mod_annopy\event\annotation_deleted::create(array( + 'objectid' => $deleteannotation, + 'context' => $context + )); + + $event->trigger(); + + redirect($redirecturl, get_string('annotationdeleted', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Save annotation. +require_once($CFG->dirroot . '/mod/annopy/annotation_form.php'); + +// Instantiate form. +$mform = new mod_annopy_annotation_form(null, array('types' => helper::get_annotationtypes_for_form($annotationtypes))); + +if ($fromform = $mform->get_data()) { + + // In this case you process validated data. $mform->get_data() returns data posted in form. + if ((isset($fromform->annotationid) && $fromform->annotationid !== 0) && isset($fromform->text)) { // Update annotation. + $annotation = $DB->get_record('annopy_annotations', + array('annopy' => $moduleinstance->id, 'submission' => $fromform->submission, 'id' => $fromform->annotationid)); + + // Prevent changes by user in hidden form fields. + if (!$annotation) { + redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } else if ($annotation->userid != $USER->id) { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + if (!isset($fromform->type)) { + redirect($redirecturl, get_string('annotationtypedeleted', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + $annotation->timemodified = time(); + $annotation->text = format_text($fromform->text, 2, array('para' => false)); + $annotation->type = $fromform->type; + + $DB->update_record('annopy_annotations', $annotation); + + // Trigger module annotation updated event. + $event = \mod_annopy\event\annotation_updated::create(array( + 'objectid' => $fromform->annotationid, + 'context' => $context + )); + + $event->trigger(); + + $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $fromform->annotationid); + $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); + + redirect($redirecturl, get_string('annotationedited', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else if ((!isset($fromform->annotationid) || $fromform->annotationid === 0) && isset($fromform->text)) { // New annotation. + + if ($fromform->startcontainer != -1 && $fromform->endcontainer != -1 && + $fromform->startoffset != -1 && $fromform->endoffset != -1) { + + if (!isset($fromform->type)) { + redirect($redirecturl, get_string('annotationtypedeleted', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + if (preg_match("/[^a-zA-Z0-9()-\/[\]]/", $fromform->startcontainer) + || preg_match("/[^a-zA-Z0-9()-\/[\]]/", $fromform->endcontainer)) { + + redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + if (!$DB->record_exists('annopy_submissions', array('annopy' => $moduleinstance->id, 'id' => $fromform->submission))) { + redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + $annotation = new stdClass(); + $annotation->annopy = (int) $moduleinstance->id; + $annotation->submission = (int) $fromform->submission; + $annotation->userid = $USER->id; + $annotation->timecreated = time(); + $annotation->timemodified = 0; + $annotation->type = $fromform->type; + $annotation->startcontainer = $fromform->startcontainer; + $annotation->endcontainer = $fromform->endcontainer; + $annotation->startoffset = $fromform->startoffset; + $annotation->endoffset = $fromform->endoffset; + $annotation->annotationstart = $fromform->annotationstart; + $annotation->annotationend = $fromform->annotationend; + $annotation->exact = $fromform->exact; + $annotation->prefix = $fromform->prefix; + $annotation->suffix = $fromform->suffix; + $annotation->text = $fromform->text; + + $newid = $DB->insert_record('annopy_annotations', $annotation); + // Trigger module annotation created event. + $event = \mod_annopy\event\annotation_created::create(array( + 'objectid' => $newid, + 'context' => $context + )); + $event->trigger(); + + $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $newid); + $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); + + redirect($redirecturl, get_string('annotationadded', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } +} else { + redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); +} diff --git a/classes/local/helper.php b/classes/local/helper.php index 37a7d76..43b1480 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -23,6 +23,9 @@ */ namespace mod_annopy\local; +use mod_annopy_annotation_form; +use moodle_url; + /** * Utility class for the module. * @@ -64,4 +67,124 @@ public static function annopy_get_editor_and_attachment_options($course, $contex $attachmentoptions ); } + + /** + * Returns annotation types array for select form. + * + * @param stdClass $annotationtypes The annotation types. + * @return array action + */ + public static function get_annotationtypes_for_form($annotationtypes) { + $types = array(); + $strmanager = get_string_manager(); + foreach ($annotationtypes as $key => $type) { + if ($strmanager->string_exists($type->name, 'mod_annopy')) { + $types[$key] = get_string($type->name, 'mod_annopy'); + } else { + $types[$key] = $type->name; + } + } + + return $types; + } + + /** + * Returns all annotation type templates. + * + * @return array action + */ + public static function get_all_annotationtype_templates() { + global $USER, $DB; + + $select = "defaulttype = 1"; + $select .= " OR userid = " . $USER->id; + + $annotationtypetemplates = (array) $DB->get_records_select('annopy_annotationtype_templates', $select); + + return $annotationtypetemplates; + } + + /** + * Prepare the annotations for the submission. + * + * @param object $cm The course module. + * @param object $course The course. + * @param object $context The context. + * @param object $submission The submission to be processed. + * @param object $strmanager The moodle strmanager object needed to check annotation types in the annotation form. + * @param object $annotationtypes The annotation types for the module. + * @param object $annotationmode If annotationmode is activated. + * @return object The submission with its annotations. + */ + public static function prepare_annotations($cm, $course, $context, $submission, $strmanager, $annotationtypes, + $annotationmode) { + + global $DB, $USER, $CFG, $OUTPUT; + + // Get annotations for submission. + $submission->annotations = array_values($DB->get_records('annopy_annotations', + array('annopy' => $cm->instance, 'submission' => $submission->id))); + + foreach ($submission->annotations as $key => $annotation) { + + // If annotation type does not exist. + if (!$DB->record_exists('annopy_annotationtypes', array('id' => $annotation->type))) { + $submission->annotations[$key]->color = 'FFFF00'; + $submission->annotations[$key]->type = get_string('deletedannotationtype', 'mod_annopy'); + } else { + $submission->annotations[$key]->color = $annotationtypes[$annotation->type]->color; + + if ($strmanager->string_exists($annotationtypes[$annotation->type]->name, 'mod_annopy')) { + $submission->annotations[$key]->type = get_string($annotationtypes[$annotation->type]->name, 'mod_annopy'); + } else { + $submission->annotations[$key]->type = $annotationtypes[$annotation->type]->name; + } + } + + if (has_capability('mod/annopy:editannotation', $context) && $annotation->userid == $USER->id) { + $submission->annotations[$key]->canbeedited = true; + } else { + $submission->annotations[$key]->canbeedited = false; + } + + if ($annotationmode) { + // Add annotater images to annotations. + $annotater = $DB->get_record('user', array('id' => $annotation->userid)); + $annotaterimage = $OUTPUT->user_picture($annotater, + array('courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + $submission->annotations[$key]->userpicturestr = $annotaterimage; + + } else { + $submission->annotationform = false; + } + } + + // Sort annotations and find its position. + usort($submission->annotations, function ($a, $b) { + if ($a->annotationstart === $b->annotationstart) { + return $a->annotationend <=> $b->annotationend; + } + + return $a->annotationstart <=> $b->annotationstart; + }); + + $pos = 1; + foreach ($submission->annotations as $key => $annotation) { + $submission->annotations[$key]->position = $pos; + $pos += 1; + } + + if ($annotationmode) { + // Add annotation form. + require_once($CFG->dirroot . '/mod/annopy/annotation_form.php'); + $mform = new mod_annopy_annotation_form(new moodle_url('/mod/annopy/annotations.php', array('id' => $cm->id)), + array('types' => self::get_annotationtypes_for_form($annotationtypes))); + // Set default data. + $mform->set_data(array('id' => $cm->id, 'submission' => $submission->id)); + + $submission->annotationform = $mform->render(); + } + + return $submission; + } } diff --git a/classes/output/annopy_view.php b/classes/output/annopy_view.php index e87603c..53658fa 100644 --- a/classes/output/annopy_view.php +++ b/classes/output/annopy_view.php @@ -24,6 +24,7 @@ namespace mod_annopy\output; use mod_annopy\local\submissionstats; +use mod_annopy\local\helper; use renderable; use renderer_base; use templatable; @@ -38,8 +39,8 @@ */ class annopy_view implements renderable, templatable { - /** @var int */ - protected $cmid; + /** @var object */ + protected $cm; /** @var object */ protected $course; /** @var object */ @@ -49,13 +50,13 @@ class annopy_view implements renderable, templatable { /** * Construct this renderable. - * @param int $cmid The course module id + * @param int $cm The course module * @param object $course The course * @param object $context The context * @param object $submission The submission */ - public function __construct($cmid, $course, $context, $submission) { - $this->cmid = $cmid; + public function __construct($cm, $course, $context, $submission) { + $this->cm = $cm; $this->course = $course; $this->context = $context; $this->submission = $submission; @@ -71,7 +72,7 @@ public function export_for_template(renderer_base $output) { global $DB, $USER, $OUTPUT; $data = new stdClass(); - $data->cmid = $this->cmid; + $data->cmid = $this->cm->id; $data->submission = $this->submission; if ($data->submission) { @@ -88,9 +89,16 @@ public function export_for_template(renderer_base $output) { $data->submission->timecreated); $data->submission->canviewdetails = has_capability('mod/annopy:addsubmission', $this->context); + // Prepare annotations. + $select = "annopy = " . $this->cm->instance; + $annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); + $data->submission = helper::prepare_annotations($this->cm, $this->course, $this->context, $data->submission, + get_string_manager(), $annotationtypes, true); } $data->canaddsubmission = has_capability('mod/annopy:addsubmission', $this->context); + + $data->sesskey = sesskey(); return $data; } } diff --git a/db/access.php b/db/access.php index 2c3d4cd..40017c9 100644 --- a/db/access.php +++ b/db/access.php @@ -172,7 +172,7 @@ ) ), - 'mod/annopy:addannotationstyle' => array( + 'mod/annopy:addannotationtype' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -183,7 +183,7 @@ ) ), - 'mod/annopy:editannotationstyle' => array( + 'mod/annopy:editannotationtype' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -194,7 +194,7 @@ ) ), - 'mod/annopy:deleteannotationstyle' => array( + 'mod/annopy:deleteannotationtype' => array( 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -205,7 +205,7 @@ ) ), - 'mod/annopy:addannotationstyletemplate' => array( + 'mod/annopy:addannotationtypetemplate' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -216,7 +216,7 @@ ) ), - 'mod/annopy:editannotationstyletemplate' => array( + 'mod/annopy:editannotationtypetemplate' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -227,7 +227,7 @@ ) ), - 'mod/annopy:deleteannotationstyletemplate' => array( + 'mod/annopy:deleteannotationtypetemplate' => array( 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, @@ -238,7 +238,7 @@ ) ), - 'mod/annopy:managedefaultannotationstyletemplates' => array( + 'mod/annopy:managedefaultannotationtypetemplates' => array( 'riskbitmask' => RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, diff --git a/db/install.xml b/db/install.xml index 1f6ad2a..9302d85 100644 --- a/db/install.xml +++ b/db/install.xml @@ -86,15 +86,15 @@ - +
- - - - - + + + + + - + @@ -104,15 +104,15 @@
- +
- - - - - - - + + + + + + + diff --git a/lang/de/annopy.php b/lang/de/annopy.php index 244384b..d75a45a 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -61,11 +61,27 @@ $string['details'] = 'Details'; $string['numwordsraw'] = '{$a->wordscount} Wörter mit {$a->charscount} Zeichen, einschließlich {$a->spacescount} Leerzeichen.'; $string['created'] = 'vor {$a->years} Jahren, {$a->month} Monaten, {$a->days} Tagen und {$a->hours} Stunden'; +$string['nosubmission'] = 'Keine Einreichung'; + +// Strings for annotations. $string['annotations'] = 'Annotationen'; $string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; +$string['toggleannotation'] = 'Annotation aus- / einklappen'; +$string['hoverannotation'] = 'Annotation hervorheben'; +$string['annotationcreated'] = 'Erstellt am {$a}'; +$string['annotationmodified'] = 'Bearbeitet am {$a}'; +$string['editannotation'] = 'Bearbeiten'; +$string['deleteannotation'] = 'Löschen'; $string['annotationsarefetched'] = 'Annotationen werden geladen'; $string['reloadannotations'] = 'Annotationen neu laden'; -$string['nosubmission'] = 'Keine Einreichung'; +$string['annotationadded'] = 'Annotation hinzugefügt'; +$string['annotationedited'] = 'Annotation geändert'; +$string['annotationdeleted'] = 'Annotation gelöscht'; +$string['annotationinvalid'] = 'Annotation ungültig'; +$string['annotatedtextnotfound'] = 'Annotierter Text nicht gefunden'; +$string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist ungültig geworden. Die Markierung für diese Annotation muss deshalb neu gesetzt werden.'; +$string['deletedannotationtype'] = 'Gelöschter Stil'; +$string['annotationtypedeleted'] = 'Annotationsstil nicht vorhanden.'; // Strings for lib.php. $string['deletealluserdata'] = 'Alle Benutzerdaten löschen'; @@ -86,13 +102,13 @@ $string['annopy:viewannotations'] = 'Annotationen ansehen'; $string['annopy:viewannotationsevaluation'] = 'Annotationsauswertung ansehen'; $string['annopy:viewmyannotationsummary'] = 'Zusammenfasung meiner Annotationen ansehen'; -$string['annopy:addannotationstyle'] = 'Annotationsstil hinzufügen'; -$string['annopy:editannotationstyle'] = 'Annotationsstil bearbeiten'; -$string['annopy:deleteannotationstyle'] = 'Annotationsstil löschen'; -$string['annopy:addannotationstyletemplate'] = 'Annotationsstilvorlage hinzufügen'; -$string['annopy:editannotationstyletemplate'] = 'Annotationsstilvorlage bearbeiten'; -$string['annopy:deleteannotationstyletemplate'] = 'Annotationsstilvorlage löschen'; -$string['annopy:managedefaultannotationstyletemplates'] = 'Standard Annotationsstil-Vorlagen verwalten'; +$string['annopy:addannotationtype'] = 'Annotationsstil hinzufügen'; +$string['annopy:editannotationtype'] = 'Annotationsstil bearbeiten'; +$string['annopy:deleteannotationtype'] = 'Annotationsstil löschen'; +$string['annopy:addannotationtypetemplate'] = 'Annotationsstilvorlage hinzufügen'; +$string['annopy:editannotationtypetemplate'] = 'Annotationsstilvorlage bearbeiten'; +$string['annopy:deleteannotationtypetemplate'] = 'Annotationsstilvorlage löschen'; +$string['annopy:managedefaultannotationtypetemplates'] = 'Standard Annotationsstil-Vorlagen verwalten'; // Strings for the tasks. $string['task'] = 'Aufgabe'; @@ -111,6 +127,7 @@ $string['errfilloutfield'] = 'Bitte Feld ausfüllen'; $string['incorrectcourseid'] = 'Inkorrekte Kurs-ID'; $string['incorrectmodule'] = 'Inkorrekte Kurs-Modul-ID'; +$string['notallowedtodothis'] = 'Keine Berechtigung dies zu tun.'; // Strings for the privacy api. /* diff --git a/lang/en/annopy.php b/lang/en/annopy.php index 572a0c8..53eb648 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -67,6 +67,26 @@ $string['reloadannotations'] = 'Reload annotations'; $string['nosubmission'] = 'No submission'; +// Strings for annotations. +$string['annotations'] = 'Annotations'; +$string['toggleallannotations'] = 'Toggle all annotations'; +$string['toggleannotation'] = 'Toggle annotation'; +$string['hoverannotation'] = 'Hover annotation'; +$string['annotationcreated'] = 'Created at {$a}'; +$string['annotationmodified'] = 'Modified at {$a}'; +$string['editannotation'] = 'Edit'; +$string['deleteannotation'] = 'Delete'; +$string['annotationsarefetched'] = 'Annotations being loaded'; +$string['reloadannotations'] = 'Reload annotations'; +$string['annotationadded'] = 'Annotation added'; +$string['annotationedited'] = 'Annotation edited'; +$string['annotationdeleted'] = 'Annotation deleted'; +$string['annotationinvalid'] = 'Annotation invalid'; +$string['annotatedtextnotfound'] = 'Annotated text not found'; +$string['annotatedtextinvalid'] = 'The originally annotated text has become invalid. The marking for this annotation must therefore be redone.'; +$string['deletedannotationtype'] = 'Deleted type'; +$string['annotationtypedeleted'] = 'annotation type does not exists.'; + // Strings for lib.php. $string['deletealluserdata'] = 'Delete all user data'; @@ -86,13 +106,13 @@ $string['annopy:viewannotations'] = 'View annotations'; $string['annopy:viewannotationsevaluation'] = 'View annotations evaluation'; $string['annopy:viewmyannotationsummary'] = 'View summary of my annotations'; -$string['annopy:addannotationstyle'] = 'Add annotation style'; -$string['annopy:editannotationstyle'] = 'Edit annotation style'; -$string['annopy:deleteannotationstyle'] = 'Delete annotation style'; -$string['annopy:addannotationstyletemplate'] = 'Add annotation style template'; -$string['annopy:editannotationstyletemplate'] = 'Edit annotation style template'; -$string['annopy:deleteannotationstyletemplate'] = 'Delete annotation style template'; -$string['annopy:managedefaultannotationstyletemplates'] = 'Manage default annotation style templates'; +$string['annopy:addannotationtype'] = 'Add annotation type'; +$string['annopy:editannotationtype'] = 'Edit annotation type'; +$string['annopy:deleteannotationtype'] = 'Delete annotation type'; +$string['annopy:addannotationtypetemplate'] = 'Add annotation type template'; +$string['annopy:editannotationtypetemplate'] = 'Edit annotation type template'; +$string['annopy:deleteannotationtypetemplate'] = 'Delete annotation type template'; +$string['annopy:managedefaultannotationtypetemplates'] = 'Manage default annotation type templates'; // Strings for the tasks. $string['task'] = 'Task'; @@ -111,6 +131,7 @@ $string['errfilloutfield'] = 'Please fill out this field'; $string['incorrectcourseid'] = 'Course ID is incorrect'; $string['incorrectmodule'] = 'Course Module ID is incorrect'; +$string['notallowedtodothis'] = 'No permissions to do this.'; // Strings for the privacy api. /* diff --git a/settings.php b/settings.php index 097f0f3..4b08c73 100644 --- a/settings.php +++ b/settings.php @@ -26,16 +26,4 @@ if ($hassiteconfig) { $settings = new admin_settingpage('mod_annopy_settings', new lang_string('pluginname', 'mod_annopy')); - - /* if ($ADMIN->fulltree) { - // TODO: Define actual plugin settings page and add it to the tree - {@link https://docs.moodle.org/dev/Admin_settings}. - $settings->add(new admin_setting_heading('annopy/editability', get_string('editability', 'annopy'), '')); - $settings->add(new admin_setting_configselect('annopy/defaultannotationstyletemplateseditable', - get_string('settingsdesciption', 'annopy'), - get_string('settingsdesciption_help', 'annopy'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); - - } */ } diff --git a/styles.css b/styles.css index 045b734..11226ea 100644 --- a/styles.css +++ b/styles.css @@ -90,6 +90,7 @@ background-color: yellow; cursor: pointer; -webkit-print-color-adjust: exact; + print-color-adjust: exact; } .path-mod-annopy .hovered .annotated, @@ -99,8 +100,8 @@ .path-mod-annopy .annotated:hover, .path-mod-annopy .annotated_temp:hover, -.path-mod-annopy .hovered, -.path-mod-annopy .errortypeheader .hovered { +.path-mod-annopy .textbackground.hovered, +.path-mod-annopy .annotationtypeheader .hovered { background-color: lightblue; } @@ -124,7 +125,7 @@ width: 100%; } -.path-mod-annopy-error_summary th { +.path-mod-annopy-annotations_summary th { min-width: 135px; } diff --git a/templates/annopy_view.mustache b/templates/annopy_view.mustache index febf23f..4063bdd 100644 --- a/templates/annopy_view.mustache +++ b/templates/annopy_view.mustache @@ -84,9 +84,9 @@
{{#annotations}}
-
+
- {{style}} + {{type}}
diff --git a/version.php b/version.php index 3251d1d..b94a6e6 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.1.0'; -$plugin->version = 2023082400; +$plugin->release = '0.2.0'; +$plugin->version = 2023082401; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; diff --git a/view.php b/view.php index 9668b97..b213556 100644 --- a/view.php +++ b/view.php @@ -31,6 +31,9 @@ // Course_module ID. $id = optional_param('id', 0, PARAM_INT); +// Param with id of annotation that should be focused. +$focusannotation = optional_param('focusannotation', 0, PARAM_INT); // ID of annotation. + // Set the basic variables $course, $cm and $moduleinstance. if ($id) { [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); @@ -77,7 +80,9 @@ $PAGE->set_url('/mod/annopy/view.php', array('id' => $cm->id)); } */ -// $PAGE->requires->js_call_amd('mod_annopy/view', 'init', array('cmid' => $cm->id)); +$PAGE->requires->js_call_amd('mod_annopy/annotations', 'init', + array( 'cmid' => $cm->id, 'canaddannotation' => has_capability('mod/annopy:addannotation', $context), 'myuserid' => $USER->id, + 'focusannotation' => $focusannotation)); $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -108,7 +113,7 @@ $submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); // Render and output page. -$page = new annopy_view($cm->id, $course, $context, $submission); +$page = new annopy_view($cm, $course, $context, $submission); echo $OUTPUT->render($page); From 5d5b36989daaa5d855d25c22e27d4fb06ebcb3d5 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 30 Aug 2023 11:43:57 +0200 Subject: [PATCH 3/9] feat(annotationtypes): Teachers can now add, edit and delete annotation type templates and annotation types for the concrete module instance. Added a first version of the annotations summary. Renamed the table annopy_annotationtype_templates to annopy_atype_templates. --- CHANGES.md | 5 + amd/build/colorpicker-layout.min.js | 10 + amd/build/colorpicker-layout.min.js.map | 1 + amd/src/colorpicker-layout.js | 40 +++ annotations.php | 2 +- annotations_summary.php | 314 ++++++++++++++++++ annotationtypes.php | 268 +++++++++++++++ annotationtypes_form.php | 103 ++++++ .../mod_annopy_colorpicker_form_element.php | 165 +++++++++ classes/local/helper.php | 2 +- classes/output/annopy_annotations_summary.php | 99 ++++++ db/install.xml | 2 +- lang/de/annopy.php | 61 +++- lang/en/annopy.php | 49 ++- templates/annopy_annotations_summary.mustache | 134 ++++++++ templates/annopy_view.mustache | 1 + version.php | 4 +- view.php | 5 - 18 files changed, 1241 insertions(+), 24 deletions(-) create mode 100644 amd/build/colorpicker-layout.min.js create mode 100644 amd/build/colorpicker-layout.min.js.map create mode 100644 amd/src/colorpicker-layout.js create mode 100644 annotations_summary.php create mode 100644 annotationtypes.php create mode 100644 annotationtypes_form.php create mode 100644 classes/forms/mod_annopy_colorpicker_form_element.php create mode 100644 classes/output/annopy_annotations_summary.php create mode 100644 templates/annopy_annotations_summary.mustache diff --git a/CHANGES.md b/CHANGES.md index 951d851..73d504e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ ## Changelog ## +- [0.3]: + - Teachers can now add, edit and delete annotation type templates and annotation types for the concrete module instance. + - Added a first version of the annotations summary. + - Renamed the table annopy_annotationtype_templates to annopy_atype_templates. + - [0.2]: - Users can now add, edit and delete annotations in submissions. diff --git a/amd/build/colorpicker-layout.min.js b/amd/build/colorpicker-layout.min.js new file mode 100644 index 0000000..52c8f36 --- /dev/null +++ b/amd/build/colorpicker-layout.min.js @@ -0,0 +1,10 @@ +define("mod_annopy/colorpicker-layout",["exports","jquery"],(function(_exports,_jquery){var obj; +/** + * Module for layouting custom color picker element as default form element. + * + * @module mod_discourse/colorpicker-layout + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=colorpickerid=>{console.log("TEST!"),console.log(colorpickerid),console.log((0,_jquery.default)(".path-mod-annopy .mform fieldset #fitem"+colorpickerid)),(0,_jquery.default)(".path-mod-annopy .mform fieldset #fitem_"+colorpickerid).addClass("row"),(0,_jquery.default)(".path-mod-annopy .mform fieldset #fitem_"+colorpickerid).addClass("form-group"),(0,_jquery.default)(".path-mod-annopy .mform fieldset .fitemtitle").addClass("col-md-3"),(0,_jquery.default)(".path-mod-annopy .mform fieldset .fitemtitle").addClass("col-form-label"),(0,_jquery.default)(".path-mod-annopy .mform fieldset .fitemtitle").addClass("d-flex"),(0,_jquery.default)(".path-mod-annopy .mform fieldset .ftext").addClass("col-md-9")}})); + +//# sourceMappingURL=colorpicker-layout.min.js.map \ No newline at end of file diff --git a/amd/build/colorpicker-layout.min.js.map b/amd/build/colorpicker-layout.min.js.map new file mode 100644 index 0000000..e7b2929 --- /dev/null +++ b/amd/build/colorpicker-layout.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"colorpicker-layout.min.js","sources":["../src/colorpicker-layout.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for layouting custom color picker element as default form element.\n *\n * @module mod_discourse/colorpicker-layout\n * @copyright 2023 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (colorpickerid) => {\n console.log('TEST!');\n console.log(colorpickerid);\n console.log($('.path-mod-annopy .mform fieldset #fitem' + colorpickerid));\n\n $('.path-mod-annopy .mform fieldset #fitem_' + colorpickerid).addClass('row');\n $('.path-mod-annopy .mform fieldset #fitem_' + colorpickerid).addClass('form-group');\n\n $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('col-md-3');\n $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('col-form-label');\n $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('d-flex');\n\n $('.path-mod-annopy .mform fieldset .ftext').addClass('col-md-9');\n\n};"],"names":["obj","_jquery","__esModule","default","_exports","init","colorpickerid","console","log","$","addClass"],"mappings":"wFAuBuB,IAAAA;;;;;;;kFAAvBC,SAAuBD,IAAvBC,UAAuBD,IAAAE,WAAAF,IAAAG,CAAAA,QAAAH,KAgBrBI,SAAAC,KAdmBC,gBACjBC,QAAQC,IAAI,SACZD,QAAQC,IAAIF,eACZC,QAAQC,KAAI,EAAAC,QAAAA,SAAE,0CAA4CH,iBAE1D,EAAAG,QAAAA,SAAE,2CAA6CH,eAAeI,SAAS,QACvE,EAAAD,QAAAA,SAAE,2CAA6CH,eAAeI,SAAS,eAEvE,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,aAC3D,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,mBAC3D,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,WAE3D,EAAAD,QAAAA,SAAE,2CAA2CC,SAAS,WAAW,CAEnE"} \ No newline at end of file diff --git a/amd/src/colorpicker-layout.js b/amd/src/colorpicker-layout.js new file mode 100644 index 0000000..384844b --- /dev/null +++ b/amd/src/colorpicker-layout.js @@ -0,0 +1,40 @@ +// 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 . + +/** + * Module for layouting custom color picker element as default form element. + * + * @module mod_discourse/colorpicker-layout + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import $ from 'jquery'; + +export const init = (colorpickerid) => { + console.log('TEST!'); + console.log(colorpickerid); + console.log($('.path-mod-annopy .mform fieldset #fitem' + colorpickerid)); + + $('.path-mod-annopy .mform fieldset #fitem_' + colorpickerid).addClass('row'); + $('.path-mod-annopy .mform fieldset #fitem_' + colorpickerid).addClass('form-group'); + + $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('col-md-3'); + $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('col-form-label'); + $('.path-mod-annopy .mform fieldset .fitemtitle').addClass('d-flex'); + + $('.path-mod-annopy .mform fieldset .ftext').addClass('col-md-9'); + +}; \ No newline at end of file diff --git a/annotations.php b/annotations.php index 7b669cf..abed196 100644 --- a/annotations.php +++ b/annotations.php @@ -91,7 +91,7 @@ die; } -require_capability('mod/annopy:addannotation', $context); +require_capability('mod/annopy:viewannotations', $context); // Header. $PAGE->set_url('/mod/annopy/annotations.php', array('id' => $id)); diff --git a/annotations_summary.php b/annotations_summary.php new file mode 100644 index 0000000..988a55d --- /dev/null +++ b/annotations_summary.php @@ -0,0 +1,314 @@ +. + +/** + * Prints the annotation summary for the module. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_annopy\output\annopy_annotations_summary; +use mod_annopy\local\helper; +use core\output\notification; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); + +// Course_module ID. +$id = required_param('id', PARAM_INT); + +// ID of type that should be deleted. +$delete = optional_param('delete', 0, PARAM_INT); + +// ID of type that should be deleted. +$addtoannopy = optional_param('addtoannopy', 0, PARAM_INT); + +// ID of type where priority should be changed. +$priority = optional_param('priority', 0, PARAM_INT); +$action = optional_param('action', 0, PARAM_INT); + +// If template (1) or annopy (2) annotation type. +$mode = optional_param('mode', null, PARAM_INT); + +// Set the basic variables $course, $cm and $moduleinstance. +if ($id) { + [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); + $moduleinstance = $DB->get_record('annopy', ['id' => $cm->instance], '*', MUST_EXIST); +} else { + throw new moodle_exception('missingparameter'); +} + +if (!$cm) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} else if (!$course) { + throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); +} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); + +require_capability('mod/annopy:viewmyannotationsummary', $context); + +$canaddannotationtype = has_capability('mod/annopy:addannotationtype', $context); +$caneditannotationtype = has_capability('mod/annopy:editannotationtype', $context); +$candeleteannotationtype = has_capability('mod/annopy:deleteannotationtype', $context); + +$canaddannotationtypetemplate = has_capability('mod/annopy:addannotationtypetemplate', $context); +$caneditannotationtypetemplate = has_capability('mod/annopy:editannotationtypetemplate', $context); +$candeleteannotationtypetemplate = has_capability('mod/annopy:deleteannotationtypetemplate', $context); + +$select = "annopy = " . $moduleinstance->id; +$annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); + +global $USER; + +// Add type to annopy. +if ($addtoannopy && $canaddannotationtype) { + require_sesskey(); + + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + + if ($DB->record_exists('annopy_atype_templates', array('id' => $addtoannopy))) { + + global $USER; + + $type = $DB->get_record('annopy_atype_templates', array('id' => $addtoannopy)); + + if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { + $type->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + $type->annopy = $moduleinstance->id; + + $DB->insert_record('annopy_annotationtypes', $type); + + redirect($redirecturl, get_string('annotationtypeadded', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Change priority. +if ($caneditannotationtype && $mode == 2 && $priority && $action && + $DB->record_exists('annopy_annotationtypes', array('id' => $priority))) { + + require_sesskey(); + + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + + $type = $DB->get_record('annopy_annotationtypes', array('annopy' => $moduleinstance->id, 'id' => $priority)); + + $oldpriority = 0; + + if ($type && $action == 1 && $type->priority != 1) { // Increase priority (show more in front). + $oldpriority = $type->priority; + $type->priority -= 1; + + $typeswitched = $DB->get_record('annopy_annotationtypes', + array('annopy' => $moduleinstance->id, 'priority' => $type->priority)); + + if (!$typeswitched) { // If no type with priority+1 search for types with hihgher priority values. + $typeswitched = $DB->get_records_select('annopy_annotationtypes', + "annopy = $moduleinstance->id AND priority < $type->priority", null, 'priority ASC'); + + if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) { + $typeswitched = $typeswitched[array_key_first($typeswitched)]; + } + } + + } else if ($type && $action == 2 && $type->priority != $DB->count_records('annopy_annotationtypes', + array('annopy' => $moduleinstance->id)) + 1) { // Decrease priority (move further back). + + $oldpriority = $type->priority; + $type->priority += 1; + + $typeswitched = $DB->get_record('annopy_annotationtypes', + array('annopy' => $moduleinstance->id, 'priority' => $type->priority)); + + if (!$typeswitched) { // If no type with priority+1 search for types with higher priority values. + $typeswitched = $DB->get_records_select('annopy_annotationtypes', + "annopy = $moduleinstance->id AND priority > $type->priority", null, 'priority ASC'); + + if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) { + $typeswitched = $typeswitched[array_key_first($typeswitched)]; + } + } + } else { + redirect($redirecturl, get_string('prioritynotchanged', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + if ($typeswitched) { + // Update priority for type. + $DB->update_record('annopy_annotationtypes', $type); + + // Update priority for type that type is switched with. + $typeswitched->priority = $oldpriority; + $DB->update_record('annopy_annotationtypes', $typeswitched); + + redirect($redirecturl, get_string('prioritychanged', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('prioritynotchanged', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Delete annotation. +if ($delete !== 0 && $mode) { + + require_sesskey(); + + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + + if ($mode == 1) { // If type is template annotation type. + $table = 'annopy_atype_templates'; + } else if ($mode == 2) { // If type is annopy annotation type. + $table = 'annopy_annotationtypes'; + } + + if ($DB->record_exists($table, array('id' => $delete))) { + + $type = $DB->get_record($table, array('id' => $delete)); + + if ($mode == 2 && $candeleteannotationtype || + ($type->defaulttype == 1 && has_capability('mod/annopy:managedefaultannotationtypetemplates', $context) + && $candeleteannotationtypetemplate) + || ($type->defaulttype == 0 && $type->userid == $USER->id && $candeleteannotationtypetemplate)) { + + $DB->delete_records($table, array('id' => $delete)); + redirect($redirecturl, get_string('annotationtypedeleted', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Get the name for this module instance. +$modulename = format_string($moduleinstance->name, true, array( + 'context' => $context +)); + +$PAGE->set_url('/mod/annopy/annotations_summary.php', array('id' => $cm->id)); +$PAGE->navbar->add(get_string('annotationssummary', 'mod_annopy')); + +$PAGE->set_title(get_string('modulename', 'mod_annopy').': ' . $modulename); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($context); + +if ($CFG->branch < 400) { + $PAGE->force_settings_menu(); +} + +echo $OUTPUT->header(); + +if ($CFG->branch < 400) { + echo $OUTPUT->heading($modulename); + + if ($moduleinstance->intro) { + echo $OUTPUT->box(format_module_intro('annopy', $moduleinstance, $cm->id), 'generalbox', 'intro'); + } +} + +$participants = array_values(get_enrolled_users($context, 'mod/annopy:potentialparticipant')); +$annotationtypesforform = helper::get_annotationtypes_for_form($annotationtypes); + +foreach ($participants as $key => $participant) { + if (has_capability('mod/annopy:viewannotationsevaluation', $context) || $participant->id == $USER->id) { + $participants[$key]->annotations = array(); + + foreach ($annotationtypesforform as $i => $type) { + $sql = "SELECT COUNT(*) + FROM {annopy_annotations} a + JOIN {annopy_submissions} s ON s.id = a.submission + WHERE s.annopy = :annopy AND + s.author = :author AND + a.type = :atype"; + $params = array('annopy' => $moduleinstance->id, 'author' => $participant->id, 'atype' => $i); + $count = $DB->count_records_sql($sql, $params); + + $participants[$key]->annotations[$i] = $count; + } + + $participants[$key]->annotations = array_values($participants[$key]->annotations); + } else { + unset($participants[$key]); + } +} + +$participants = array_values($participants); + +$strmanager = get_string_manager(); + +foreach ($annotationtypes as $i => $type) { + $annotationtypes[$i]->canbeedited = $caneditannotationtype; + $annotationtypes[$i]->canbedeleted = $candeleteannotationtype; + + if ($strmanager->string_exists($type->name, 'mod_annopy')) { + $annotationtypes[$i]->name = get_string($type->name, 'mod_annopy'); + } else { + $annotationtypes[$i]->name = $type->name; + } +} + +$annotationtypes = array_values($annotationtypes); + +global $USER; + +$annotationtypetemplates = helper::get_all_annotationtype_templates(); +foreach ($annotationtypetemplates as $id => $templatetype) { + if ($templatetype->defaulttype == 1) { + $annotationtypetemplates[$id]->type = get_string('standard', 'mod_annopy'); + + if (!has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { + $annotationtypetemplates[$id]->canbeedited = false; + $annotationtypetemplates[$id]->canbedeleted = false; + } else { + $annotationtypetemplates[$id]->canbeedited = $caneditannotationtypetemplate; + $annotationtypetemplates[$id]->canbedeleted = $candeleteannotationtypetemplate; + } + } else { + $annotationtypetemplates[$id]->type = get_string('custom', 'mod_annopy'); + + if ($templatetype->userid === $USER->id) { + $annotationtypetemplates[$id]->canbeedited = $caneditannotationtypetemplate; + $annotationtypetemplates[$id]->canbedeleted = $candeleteannotationtypetemplate; + } else { + $annotationtypetemplates[$id]->canbeedited = false; + $annotationtypetemplates[$id]->canbedeleted = false; + } + } + + if ($templatetype->defaulttype == 1 && $strmanager->string_exists($templatetype->name, 'mod_annopy')) { + $annotationtypetemplates[$id]->name = get_string($templatetype->name, 'mod_annopy'); + } else { + $annotationtypetemplates[$id]->name = $templatetype->name; + } +} + +$annotationtypetemplates = array_values($annotationtypetemplates); + +// Output page. +$page = new annopy_annotations_summary($cm->id, $context, $participants, $annotationtypes, $annotationtypetemplates, sesskey()); + +echo $OUTPUT->render($page); + +echo $OUTPUT->footer(); diff --git a/annotationtypes.php b/annotationtypes.php new file mode 100644 index 0000000..77989b0 --- /dev/null +++ b/annotationtypes.php @@ -0,0 +1,268 @@ +. + +/** + * Prints the annotation type form for the module instance. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\output\notification; +use mod_annopy\output\annopy_annotations_summary; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); +require_once($CFG->dirroot . '/mod/annopy/annotationtypes_form.php'); + +// Course_module ID. +$id = required_param('id', PARAM_INT); + +// If template (1) or annopy (2) annotation type. +$mode = optional_param('mode', 1, PARAM_INT); + +// ID of type that should be edited. +$edit = optional_param('edit', 0, PARAM_INT); + +// Set the basic variables $course, $cm and $moduleinstance. +if ($id) { + [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); + $moduleinstance = $DB->get_record('annopy', ['id' => $cm->instance], '*', MUST_EXIST); +} else { + throw new moodle_exception('missingparameter'); +} + +if (!$cm) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} else if (!$course) { + throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); +} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { + throw new moodle_exception(get_string('incorrectmodule', 'annopy')); +} + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); + +$redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + +// Capabilities check. +if (!$edit) { // If type or template should be added. + if ($mode == 1 && !(has_capability('mod/annopy:addannotationtypetemplate', $context))) { // If no permission to add template. + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } else if ($mode == 2 && !(has_capability('mod/annopy:addannotationtype', $context))) { // If no permission to add type. + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} else if ($edit !== 0) { + if ($mode == 1 && !(has_capability('mod/annopy:editannotationtypetemplate', $context))) { // If no permission to edit template. + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } else if ($mode == 2 && !(has_capability('mod/annopy:editannotationtype', $context))) { // If no permission to edit type. + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Get type or template to be edited. +if ($edit !== 0) { + if ($mode == 1) { // If type is template type. + $editedtype = $DB->get_record('annopy_atype_templates', array('id' => $edit)); + + if (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 + && !has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { + + redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } else if ($mode == 2) { // If type is annopy type. + $editedtype = $DB->get_record('annopy_annotationtypes', array('id' => $edit)); + + if ($moduleinstance->id !== $editedtype->annopy) { + redirect($redirecturl, get_string('annotationtypecantbeedited', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + } + + if ($editedtype && $mode == 2 || + ((isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && + has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) + || (isset($editedtype->defaulttype) && isset($editedtype->userid) && + $editedtype->defaulttype == 0 && $editedtype->userid == $USER->id))) { + + $editedtypeid = $edit; + $editedtypename = $editedtype->name; + $editedcolor = '#' . $editedtype->color; + + if ($mode == 1) { + $editeddefaulttype = $editedtype->defaulttype; + } + } +} + +$select = "annopy = " . $moduleinstance->id; +$annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); + +// Instantiate form. +$mform = new mod_annopy_annotationtypes_form(null, + array('editdefaulttype' => has_capability('mod/annopy:managedefaultannotationtypetemplates', $context), 'mode' => $mode)); + +if (isset($editedtypeid)) { + if ($mode == 1) { // If type is template annotation type. + $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, + 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype)); + } else if ($mode == 2) { + $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, + 'color' => $editedcolor)); + } +} else { + $mform->set_data(array('id' => $id, 'mode' => $mode, 'color' => '#FFFF00')); +} + +if ($mform->is_cancelled()) { + redirect($redirecturl); +} else if ($fromform = $mform->get_data()) { + + // In this case you process validated data. $mform->get_data() returns data posted in form. + if ($fromform->typeid == 0 && isset($fromform->typename)) { // Create new annotation type. + + $annotationtype = new stdClass(); + $annotationtype->timecreated = time(); + $annotationtype->timemodified = 0; + $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); + $annotationtype->color = $fromform->color; + + if (isset($fromform->standardtype) && $fromform->standardtype === 1 && + has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { + + $annotationtype->userid = 0; + $annotationtype->defaulttype = 1; + } else { + $annotationtype->userid = $USER->id; + $annotationtype->defaulttype = 0; + } + + if ($mode == 2) { // If type is annopy annotation type. + $annotationtype->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + $annotationtype->annopy = $moduleinstance->id; + } + + if ($mode == 1) { // If type is template annotation type. + $DB->insert_record('annopy_atype_templates', $annotationtype); + + } else if ($mode == 2) { // If type is annopy annotation type. + $DB->insert_record('annopy_annotationtypes', $annotationtype); + } + + redirect($redirecturl, get_string('annotationtypeadded', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else if ($fromform->typeid !== 0 && isset($fromform->typename)) { // Update existing annotation type. + + if ($mode == 1) { // If type is template annotation type. + $annotationtype = $DB->get_record('annopy_atype_templates', array('id' => $fromform->typeid)); + } else if ($mode == 2) { // If type is annopy annotation type. + $annotationtype = $DB->get_record('annopy_annotationtypes', array('id' => $fromform->typeid)); + } + + if ($annotationtype && + ($mode == 2 || + (isset($annotationtype->defaulttype) && $annotationtype->defaulttype == 1 && + has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) + || (isset($annotationtype->defaulttype) && isset($annotationtype->userid) && $annotationtype->defaulttype == 0 + && $annotationtype->userid == $USER->id))) { + + $annotationtype->timemodified = time(); + $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); + $annotationtype->color = $fromform->color; + + if ($mode == 1 && has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { + global $USER; + if ($fromform->standardtype === 1 && $annotationtype->defaulttype !== $fromform->standardtype) { + $annotationtype->defaulttype = 1; + $annotationtype->userid = 0; + } else if ($fromform->standardtype === 0 && $annotationtype->defaulttype !== $fromform->standardtype) { + $annotationtype->defaulttype = 0; + $annotationtype->userid = $USER->id; + } + } + + if ($mode == 1) { // If type is template annotation type. + $DB->update_record('annopy_atype_templates', $annotationtype); + + } else if ($mode == 2) { // If type is annopy annotation type. + $DB->update_record('annopy_annotationtypes', $annotationtype); + } + + redirect($redirecturl, get_string('annotationtypeedited', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('annotationtypecantbeedited', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } + + } else { + redirect($redirecturl, get_string('annotationtypeinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); + } +} + +// Get the name for this module instance. +$modulename = format_string($moduleinstance->name, true, array( + 'context' => $context +)); + +$PAGE->set_url('/mod/annopy/annotationtypes.php', array('id' => $cm->id)); + +$navtitle = ''; + +if (isset($editedtypeid)) { + $navtitle = get_string('editannotationtype', 'mod_annopy'); +} else { + $navtitle = get_string('addannotationtype', 'mod_annopy'); +} + +if ($mode == 1) { // If type is template annotation type. + $navtitle .= ' (' . get_string('template', 'mod_annopy') . ')'; +} else if ($mode == 2) { // If type is annopy annotation type. + $navtitle .= ' (' . get_string('modulename', 'mod_annopy') . ')'; +} + +$PAGE->navbar->add($navtitle); + +$PAGE->set_title(get_string('modulename', 'mod_annopy').': ' . $modulename); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($context); + +if ($CFG->branch < 400) { + $PAGE->force_settings_menu(); +} + +echo $OUTPUT->header(); + +if ($CFG->branch < 400) { + echo $OUTPUT->heading($modulename); + + if ($moduleinstance->intro) { + echo $OUTPUT->box(format_module_intro('annopy', $moduleinstance, $cm->id), 'generalbox', 'intro'); + } +} + +if (isset($editedtypeid) && $mode == 1) { + if ($editeddefaulttype) { + echo $OUTPUT->notification( + get_string('warningeditdefaultannotationtypetemplate', 'mod_annopy'), notification::NOTIFY_ERROR); + } + + echo $OUTPUT->notification(get_string('changetemplate', 'mod_annopy'), notification::NOTIFY_WARNING); +} + +echo $OUTPUT->heading($navtitle, 4); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/annotationtypes_form.php b/annotationtypes_form.php new file mode 100644 index 0000000..6e66d70 --- /dev/null +++ b/annotationtypes_form.php @@ -0,0 +1,103 @@ +. + +/** + * File containing the class definition for the annotation types form for the module. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("$CFG->libdir/formslib.php"); + +/** + * Form for annotation types. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later + */ +class mod_annopy_annotationtypes_form extends moodleform { + + /** + * Define the form - called by parent constructor + */ + public function definition() { + + global $OUTPUT, $CFG; + + $mform = $this->_form; // Don't forget the underscore! + + $mform->addElement('hidden', 'id', null); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'mode', 1); + $mform->setType('mode', PARAM_INT); + + $mform->addElement('hidden', 'typeid', null); + $mform->setType('typeid', PARAM_INT); + + $mform->addElement('text', 'typename', get_string('explanationtypename', 'mod_annopy')); + $mform->setType('typename', PARAM_TEXT); + $mform->addRule('typename', null, 'required', null, 'client'); + + if ($this->_customdata['editdefaulttype']) { + $mform->addHelpButton('typename', 'explanationtypename', 'mod_annopy'); + } + + MoodleQuickForm::registerElementType('colorpicker', + "$CFG->dirroot/mod/annopy/classes/forms/mod_annopy_colorpicker_form_element.php", + 'mod_annopy_colorpicker_form_element'); + + $mform->addElement('colorpicker', 'color', get_string('explanationhexcolor', 'mod_annopy')); + $mform->setType('color', PARAM_ALPHANUM); + $mform->addRule('color', null, 'required', null, 'client'); + $mform->addHelpButton('color', 'explanationhexcolor', 'mod_annopy'); + + if ($this->_customdata['mode'] == 1) { // If template annotation type. + if ($this->_customdata['editdefaulttype']) { + $mform->addElement('advcheckbox', 'standardtype', get_string('standardtype', 'mod_annopy'), + get_string('explanationstandardtype', 'mod_annopy')); + } else { + $mform->addElement('hidden', 'standardtype', 0); + } + + $mform->setType('standardtype', PARAM_INT); + } + + $this->add_action_buttons(); + } + + /** + * Custom validation should be added here + * @param array $data Array with all the form data + * @param array $files Array with files submitted with form + * @return array Array with errors + */ + public function validation($data, $files) { + $errors = array(); + + if (strlen($data['color']) !== 6 || preg_match("/[^a-fA-F0-9]/", $data['color'])) { + $errors['color'] = get_string('errnohexcolor', 'mod_annopy'); + } + + return $errors; + } +} diff --git a/classes/forms/mod_annopy_colorpicker_form_element.php b/classes/forms/mod_annopy_colorpicker_form_element.php new file mode 100644 index 0000000..d55877f --- /dev/null +++ b/classes/forms/mod_annopy_colorpicker_form_element.php @@ -0,0 +1,165 @@ +. + +/** + * Color picker custom form element. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("HTML/QuickForm/text.php"); + +/** + * Color picker custom form element. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_annopy_colorpicker_form_element extends HTML_QuickForm_text { + // Whether to force the display of this element to flow LTR. + public $forceltr = false; + + // String html for help button, if empty then no help. + public $_helpbutton = ''; + + // If true label will be hidden. + public $_hiddenlabel = false; + + /** + * constructor + * + * @param string $elementname (optional) name of the text field + * @param string $elementlabel (optional) text field label + * @param string $attributes (optional) Either a typical HTML attribute string or an associative array + */ + public function __construct($elementname = null, $elementlabel = null, $attributes = null) { + parent::__construct($elementname, $elementlabel, $attributes); + parent::setSize(30); + parent::setMaxlength(7); + } + + /** + * Sets label to be hidden. + * + * @param bool $hiddenlabel sets if label should be hidden + */ + public function sethiddenlabel($hiddenlabel) { + $this->_hiddenlabel = $hiddenlabel; + } + + /** + * Freeze the element so that only its value is returned and set persistantfreeze to false. + * + * @return void + */ + public function freeze() { + $this->_flagFrozen = true; + // No hidden element is needed refer MDL-30845. + $this->setPersistantFreeze(false); + } + + /** + * Returns the html to be used when the element is frozen. + * + * @return string Frozen html + */ + public function getfrozenhtml() { + $attributes = array('readonly' => 'readonly'); + $this->updateAttributes($attributes); + return $this->_getTabs() . '_getAttrString($this->_attributes) . '/>' . $this->_getPersistantData(); + } + + /** + * Returns HTML for this form element. + * + * @return string + */ + public function tohtml() { + global $CFG, $PAGE; + + $PAGE->requires->js_init_call('M.util.init_colour_picker', array('id_color', null)); + $PAGE->requires->js_call_amd('mod_annopy/colorpicker-layout', 'init', array('id_color')); + + // Add the class at the last minute. + if ($this->get_force_ltr()) { + if (!isset($this->_attributes['class'])) { + $this->_attributes['class'] = 'form-control text-ltr'; + } else { + $this->_attributes['class'] .= 'form-control text-ltr'; + } + } + + $this->_generateId(); + if ($this->_flagFrozen) { + return $this->getfrozenhtml(); + } + + $html = $this->_getTabs() . + '
+
+
+
+ Loading +
+ _getAttrString($this->_attributes) . '"> + +
+
+
'; + + if ($this->_hiddenlabel) { + return '' . $html; + } else { + return $html; + } + } + + /** + * Get html for help button. + * + * @return string html for help button + */ + public function gethelpbutton() { + return $this->_helpbutton; + } + + /** + * Get force LTR option. + * + * @return bool + */ + public function get_force_ltr() { + return $this->forceltr; + } + + /** + * Force the field to flow left-to-right. + * + * This is useful for fields such as URLs, passwords, settings, etc... + * + * @param bool $value The value to set the option to. + */ + public function set_force_ltr($value) { + $this->forceltr = (bool) $value; + } +} diff --git a/classes/local/helper.php b/classes/local/helper.php index 43b1480..6f7de56 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -99,7 +99,7 @@ public static function get_all_annotationtype_templates() { $select = "defaulttype = 1"; $select .= " OR userid = " . $USER->id; - $annotationtypetemplates = (array) $DB->get_records_select('annopy_annotationtype_templates', $select); + $annotationtypetemplates = (array) $DB->get_records_select('annopy_atype_templates', $select); return $annotationtypetemplates; } diff --git a/classes/output/annopy_annotations_summary.php b/classes/output/annopy_annotations_summary.php new file mode 100644 index 0000000..ebbbd0f --- /dev/null +++ b/classes/output/annopy_annotations_summary.php @@ -0,0 +1,99 @@ +. + +/** + * Class containing data for the annotations summary. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_annopy\output; + +use renderable; +use renderer_base; +use templatable; +use stdClass; + +/** + * Class containing data for the annotations summary. + * + * @package mod_annopy + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class annopy_annotations_summary implements renderable, templatable { + + /** @var int */ + protected $cmid; + /** @var object */ + protected $context; + /** @var object */ + protected $participants; + /** @var object */ + protected $annopyannotationtypes; + /** @var object */ + protected $annotationtypetemplates; + /** @var string */ + protected $sesskey; + + /** + * Construct this renderable. + * @param int $cmid The course module id + * @param array $context The context + * @param array $participants The participants of the annopy instance + * @param array $annopyannotationtypes The annotationtypes used in the annopy instance + * @param array $annotationtypetemplates The annotationtype templates available for the current user + * @param string $sesskey The session key + */ + public function __construct($cmid, $context, $participants, $annopyannotationtypes, $annotationtypetemplates, $sesskey) { + + $this->cmid = $cmid; + $this->context = $context; + $this->participants = $participants; + $this->annopyannotationtypes = $annopyannotationtypes; + $this->annotationtypetemplates = $annotationtypetemplates; + $this->sesskey = $sesskey; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->cmid = $this->cmid; + $data->participants = $this->participants; + $data->annopyannotationtypes = $this->annopyannotationtypes; + $data->annotationtypetemplates = $this->annotationtypetemplates; + $data->sesskey = $this->sesskey; + + if (has_capability('mod/annopy:addannotationtypetemplate', $this->context) || + has_capability('mod/annopy:editannotationtypetemplate', $this->context)) { + + $data->canmanageannotationtypetemplates = true; + } else { + $data->canmanageannotationtypetemplates = false; + } + + $data->canaddannotationtype = has_capability('mod/annopy:addannotationtype', $this->context); + $data->canaddannotationtypetemplate = has_capability('mod/annopy:addannotationtypetemplate', $this->context); + + return $data; + } +} diff --git a/db/install.xml b/db/install.xml index 9302d85..be83165 100644 --- a/db/install.xml +++ b/db/install.xml @@ -86,7 +86,7 @@
- +
diff --git a/lang/de/annopy.php b/lang/de/annopy.php index d75a45a..363bd1b 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -80,8 +80,51 @@ $string['annotationinvalid'] = 'Annotation ungültig'; $string['annotatedtextnotfound'] = 'Annotierter Text nicht gefunden'; $string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist ungültig geworden. Die Markierung für diese Annotation muss deshalb neu gesetzt werden.'; -$string['deletedannotationtype'] = 'Gelöschter Stil'; -$string['annotationtypedeleted'] = 'Annotationsstil nicht vorhanden.'; +$string['deletedannotationtype'] = 'Gelöschter Typ'; +$string['annotationtypedeleted'] = 'Annotationstyp nicht vorhanden.'; + +// Strings for annotations_summary and annotationtypes_form. +$string['annotationssummary'] = 'Annotationsauswertung'; +$string['participant'] = 'TeilnehmerIn'; +$string['backtooverview'] = 'Zurück zur Übersicht'; +$string['addannotationtype'] = 'Annotationstyp anlegen'; +$string['annotationtypeadded'] = 'Annotationstyp angelegt'; +$string['editannotationtype'] = 'Annotationstyp bearbeiten'; +$string['annotationtypeedited'] = 'Annotationstyp bearbeitet'; +$string['editannotationtypetemplate'] = 'Vorlage bearbeiten'; +$string['annotationtypecantbeedited'] = 'Annotationstyp konnte nicht geändert werden'; +$string['deleteannotationtype'] = 'Annotationstyp entfernen'; +$string['annotationtypedeleted'] = 'Annotationstyp entfernt'; +$string['deleteannotationtypetemplate'] = 'Vorlage löschen'; +$string['deleteannotationtypetemplateconfirm'] = 'Soll diese Annotationstyp-Vorlage wirklich gelöscht werden? Dadurch wird die Vorlage für das gesamte System gelöscht und kann nicht mehr in neuen AnnoPys als konkreter Annotationstyp ausgewählt werden. Diese Aktion kann nicht rückgängig gemacht werden!'; +$string['annotationtypeinvalid'] = 'Annotationstyp ungültig'; +$string['annopyannotationtypes'] = 'AnnoPy Annotationstyp'; +$string['annotationtypetemplates'] = 'Annotationstyp-Vorlagen'; +$string['annotationtypes'] = 'Annotationstypen'; +$string['template'] = 'Vorlage'; +$string['addtoannopy'] = 'Zum AnnoPy hinzufügen'; +$string['switchtotemplatetypes'] = 'Zu den Annotationstyp-Vorlagen wechseln'; +$string['switchtoannopytypes'] = 'Zu den Annotationstypen des AnnoPys wechseln'; +$string['notemplatetypes'] = 'Keine Annotationstyp-Vorlagen verfügbar'; +$string['movefor'] = 'Weiter vorne anzeigen'; +$string['moveback'] = 'Weiter hinten anzeigen'; +$string['prioritychanged'] = 'Reihenfolge geändert'; +$string['prioritynotchanged'] = 'Reihenfolge konnte nicht geändert werden'; +$string['annotationcolor'] = 'Farbe des Annotationstyps'; +$string['standardtype'] = 'Standard Annotationstyp'; +$string['manualtype'] = 'Manueller Annotationstyp'; +$string['standard'] = 'Standard'; +$string['custom'] = 'Benutzerdefiniert'; +$string['type'] = 'Art'; +$string['color'] = 'Farbe'; +$string['errnohexcolor'] = 'Kein hexadezimaler Farbwert.'; +$string['warningeditdefaultannotationtypetemplate'] = 'WARNUNG: Hierdurch wird die Annotationstyp-Vorlage systemweit geändert. Bei der Erstellung neuer AnnoPys wird dann bei der Auswahl der konkreten Annotationstypen die geänderte Vorlage zur Verfügung stehen.'; +$string['changetemplate'] = 'Die Änderung des Namens oder der Farbe des Annotationstypen wirkt sich nur auf die Vorlage aus und wird daher erst bei der Erstellung neuer AnnoPys wirksam. Die Annotationstypen in bestehenden AnnoPys sind von diesen Änderungen nicht betroffen.'; +$string['explanationtypename'] = 'Name'; +$string['explanationtypename_help'] = 'Der Name des Annotationstypen. Wird nicht übersetzt.'; +$string['explanationhexcolor'] = 'Farbe'; +$string['explanationhexcolor_help'] = 'Die Farbe des Annotationstypen als Hexadezimalwert. Dieser besteht aus genau 6 Zeichen (A-F sowie 0-9) und repräsentiert eine Farbe. Wenn die Farbe hier ausgewählt wird wird der Wert automatisch eingetragen, alternativ kann der Hexwert auch eingegeben werden. Den Hexwert von beliebigen Farben kann man z. B. unter https://www.w3schools.com/colors/colors_picker.asp herausfinden.'; +$string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Annotationstyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre AnnoPys ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren AnnoPys verwendet werden.'; // Strings for lib.php. $string['deletealluserdata'] = 'Alle Benutzerdaten löschen'; @@ -102,13 +145,13 @@ $string['annopy:viewannotations'] = 'Annotationen ansehen'; $string['annopy:viewannotationsevaluation'] = 'Annotationsauswertung ansehen'; $string['annopy:viewmyannotationsummary'] = 'Zusammenfasung meiner Annotationen ansehen'; -$string['annopy:addannotationtype'] = 'Annotationsstil hinzufügen'; -$string['annopy:editannotationtype'] = 'Annotationsstil bearbeiten'; -$string['annopy:deleteannotationtype'] = 'Annotationsstil löschen'; -$string['annopy:addannotationtypetemplate'] = 'Annotationsstilvorlage hinzufügen'; -$string['annopy:editannotationtypetemplate'] = 'Annotationsstilvorlage bearbeiten'; -$string['annopy:deleteannotationtypetemplate'] = 'Annotationsstilvorlage löschen'; -$string['annopy:managedefaultannotationtypetemplates'] = 'Standard Annotationsstil-Vorlagen verwalten'; +$string['annopy:addannotationtype'] = 'Annotationstyp hinzufügen'; +$string['annopy:editannotationtype'] = 'Annotationstyp bearbeiten'; +$string['annopy:deleteannotationtype'] = 'Annotationstyp löschen'; +$string['annopy:addannotationtypetemplate'] = 'Annotationstypvorlage hinzufügen'; +$string['annopy:editannotationtypetemplate'] = 'Annotationstypvorlage bearbeiten'; +$string['annopy:deleteannotationtypetemplate'] = 'Annotationstypvorlage löschen'; +$string['annopy:managedefaultannotationtypetemplates'] = 'Standard Annotationstyp-Vorlagen verwalten'; // Strings for the tasks. $string['task'] = 'Aufgabe'; diff --git a/lang/en/annopy.php b/lang/en/annopy.php index 53eb648..a4f24fd 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -61,10 +61,6 @@ $string['details'] = 'Details'; $string['numwordsraw'] = '{$a->wordscount} text words using {$a->charscount} characters, including {$a->spacescount} spaces.'; $string['created'] = '{$a->years} years, {$a->month} months, {$a->days} days and {$a->hours} hours ago'; -$string['annotations'] = 'Annotations'; -$string['toggleallannotations'] = 'Toggle all annotations'; -$string['annotationsarefetched'] = 'Annotations being loaded'; -$string['reloadannotations'] = 'Reload annotations'; $string['nosubmission'] = 'No submission'; // Strings for annotations. @@ -85,7 +81,50 @@ $string['annotatedtextnotfound'] = 'Annotated text not found'; $string['annotatedtextinvalid'] = 'The originally annotated text has become invalid. The marking for this annotation must therefore be redone.'; $string['deletedannotationtype'] = 'Deleted type'; -$string['annotationtypedeleted'] = 'annotation type does not exists.'; +$string['annotationtypedeleted'] = 'Annotation type does not exists.'; + +// Strings for annotations_summary and annotationtypes_form. +$string['annotationssummary'] = 'Annotations summary'; +$string['participant'] = 'Participant'; +$string['backtooverview'] = 'Back to overview'; +$string['addannotationtype'] = 'Add annotation type'; +$string['annotationtypeadded'] = 'Annotation type added'; +$string['editannotationtype'] = 'Edit annotation type'; +$string['annotationtypeedited'] = 'Annotation type edited'; +$string['editannotationtypetemplate'] = 'Edit template'; +$string['annotationtypecantbeedited'] = 'Annotation type could not be changed'; +$string['deleteannotationtype'] = 'Delete annotation type'; +$string['annotationtypedeleted'] = 'Annotation type deleted'; +$string['deleteannotationtypetemplate'] = 'Delete template'; +$string['deleteannotationtypetemplateconfirm'] = 'Should this annotation type template really be deleted? This deletes the template for the entire system so that it can no longer be used as a concrete annotation type in new AnnoPys. This action cannot be undone!'; +$string['annotationtypeinvalid'] = 'Annotation type invalid'; +$string['annopyannotationtypes'] = 'AnnoPy annotation types'; +$string['annotationtypetemplates'] = 'Annotation type templates'; +$string['annotationtypes'] = 'Annotation types'; +$string['template'] = 'Template'; +$string['addtoannopy'] = 'Add to AnnoPy'; +$string['switchtotemplatetypes'] = 'Switch to the annotation type templates'; +$string['switchtoannopytypes'] = 'Switch to the annotation types for the AnnoPy'; +$string['notemplatetypes'] = 'No annotation type templates available'; +$string['movefor'] = 'Display more in front'; +$string['moveback'] = 'Display further back'; +$string['prioritychanged'] = 'Order changed'; +$string['prioritynotchanged'] = 'Order could not be changed'; +$string['annotationcolor'] = 'Color of the annotation type'; +$string['standardtype'] = 'Standard annotation type'; +$string['manualtype'] = 'Manual annotation type'; +$string['standard'] = 'Standard'; +$string['custom'] = 'Custom'; +$string['type'] = 'Type'; +$string['color'] = 'Color'; +$string['errnohexcolor'] = 'No hexadecimal value for color.'; +$string['warningeditdefaultannotationtypetemplate'] = 'WARNING: This will change the annotation type template system-wide. When creating new AnnoPys, the changed template will then be available for selecting the concrete AnnoPy annotation types.'; +$string['changetemplate'] = 'Changing the name or color of the annotation type only affects the template and therefore only takes effect when new AnnoPys are created. The annotation types in existing AnnoPys are not affected by these changes.'; +$string['explanationtypename'] = 'Name'; +$string['explanationtypename_help'] = 'The name of the annotation type. Will not be translated.'; +$string['explanationhexcolor'] = 'Color'; +$string['explanationhexcolor_help'] = 'The color of the annotation type as hexadecimal value. This consists of exactly 6 characters (A-F as well as 0-9) and represents a color. If the color is selected here the value is entered automatically, alternatively the hex value can also be entered manually. You can find out the hexadecimal value of any color, for example, at https://www.w3schools.com/colors/colors_picker.asp.'; +$string['explanationstandardtype'] = 'Here you can select whether the annotation type should be a default type. In this case teachers can select it as annotation type that can be used in their AnnoPys. Otherwise, only you can add this annotation type to your AnnoPys.'; // Strings for lib.php. $string['deletealluserdata'] = 'Delete all user data'; diff --git a/templates/annopy_annotations_summary.mustache b/templates/annopy_annotations_summary.mustache new file mode 100644 index 0000000..64dcee8 --- /dev/null +++ b/templates/annopy_annotations_summary.mustache @@ -0,0 +1,134 @@ +{{! + 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 . +}} +{{! + @copyright 2023 coactum GmbH + @template annopy/annopy_annotations_summary + + Annotations summary. + + Example context (json): + { + } +}} +
+
+
+ {{#canmanageannotationtypetemplates}} + + {{/canmanageannotationtypetemplates}} + {{#canaddannotationtype}} + {{#str}}addannotationtype, mod_annopy{{/str}} ({{#str}}modulename, mod_annopy{{/str}}) + {{/canaddannotationtype}} +
+ +

{{#str}}annopyannotationtypes, mod_annopy{{/str}}

+ +
+ + + + {{#annopyannotationtypes}} + + {{/annopyannotationtypes}} + + + + {{#participants}} + + {{#annotations}}{{/annotations}} + {{/participants}} + +
{{#str}}participant, mod_annopy{{/str}} + {{name}} +
+ {{#canbeedited}} + + {{/canbeedited}} + {{#canbedeleted}} + + {{/canbedeleted}} + {{#canbeedited}} + + + {{/canbeedited}} +
+
{{firstname}} {{lastname}}{{.}}
+
+ + {{#canmanageannotationtypetemplates}} +
+
+ + {{#canaddannotationtypetemplate}} {{#str}}addannotationtype, mod_annopy{{/str}} ({{#str}}template, mod_annopy{{/str}}){{/canaddannotationtypetemplate}} +
+ +

{{#str}}annotationtypetemplates, mod_annopy{{/str}}

+ + {{#annotationtypetemplates.0}} + + + + + + + + + + + + + {{#annotationtypetemplates}} + + + + + + + + + {{/annotationtypetemplates}} + +
+ {{#str}}name{{/str}} + + {{#str}}type, mod_annopy{{/str}} + + {{#str}}color, mod_annopy{{/str}} + + {{#str}}edit{{/str}} + + {{#str}}delete{{/str}} + + {{#str}}addtoannopy, mod_annopy{{/str}} +
{{name}} + {{type}} + + {{#canbeedited}}{{/canbeedited}} + + {{#canbedeleted}}{{/canbedeleted}} + + {{#canaddannotationtype}}{{/canaddannotationtype}} +
+ {{/annotationtypetemplates.0}} + {{^annotationtypetemplates.0}} {{#str}}notemplatetypes, mod_annopy{{/str}} {{/annotationtypetemplates.0}} +
+ {{/canmanageannotationtypetemplates}} + + + \ No newline at end of file diff --git a/templates/annopy_view.mustache b/templates/annopy_view.mustache index 4063bdd..0cd76b8 100644 --- a/templates/annopy_view.mustache +++ b/templates/annopy_view.mustache @@ -38,6 +38,7 @@ {{^submission}}{{#canaddsubmission}} {{#str}}addsubmission, mod_annopy{{/str}} {{/canaddsubmission}}{{/submission}} + {{#str}}annotationssummary, mod_annopy{{/str}}
{{#submission}} diff --git a/version.php b/version.php index b94a6e6..98a95a9 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.2.0'; -$plugin->version = 2023082401; +$plugin->release = '0.3.0'; +$plugin->version = 2023083000; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; diff --git a/view.php b/view.php index b213556..2842270 100644 --- a/view.php +++ b/view.php @@ -74,11 +74,6 @@ $PAGE->set_url('/mod/annopy/view.php', array('id' => $cm->id)); $PAGE->navbar->add(get_string("overview", "annopy")); -/* $PAGE->navbar->add(get_string("overview", "annopy"), new moodle_url('/mod/annopy/view.php', array('id' => $cm->id))); -if (true) { - $PAGE->navbar->add(get_string("overview", "annopy")); - $PAGE->set_url('/mod/annopy/view.php', array('id' => $cm->id)); -} */ $PAGE->requires->js_call_amd('mod_annopy/annotations', 'init', array( 'cmid' => $cm->id, 'canaddannotation' => has_capability('mod/annopy:addannotation', $context), 'myuserid' => $USER->id, From b9cd320251c429dd6e36f86e511bf5f22c0ddfeb Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 4 Sep 2023 11:36:24 +0200 Subject: [PATCH 4/9] feat(annotations_summary): On the annotations summary you can now view the total amount of annotations for each user and each annotation type. On the overview you can now view the annotations filtered by users. When creating a module instance you can now add annotation types. Submissions can now only be edited if they are not already annotated. --- CHANGES.md | 6 + amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 4 +- annotations.php | 9 +- annotations_summary.php | 41 +++---- backup/moodle2/backup_annopy_stepslib.php | 3 +- .../mod_annopy_colorpicker_form_element.php | 6 +- classes/local/helper.php | 111 +++++++++++++++++- classes/output/annopy_annotations_summary.php | 12 +- classes/output/annopy_view.php | 40 +++++-- lang/de/annopy.php | 3 + lang/en/annopy.php | 3 + lib.php | 81 ++++--------- mod_form.php | 41 +++++-- templates/annopy_annotations_summary.mustache | 32 ++++- templates/annopy_pagination.mustache | 46 ++++++++ templates/annopy_view.mustache | 10 +- version.php | 4 +- view.php | 12 +- 20 files changed, 343 insertions(+), 125 deletions(-) create mode 100644 templates/annopy_pagination.mustache diff --git a/CHANGES.md b/CHANGES.md index 73d504e..afb17f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ ## Changelog ## +- [0.4]: + - On the annotations summary you can now view the total amount of annotations for each user and each annotation type. + - On the overview you can now view the annotations filtered by users. + - When creating a module instance you can now add annotation types. + - Submissions can now only be edited if they are not already annotated. + - [0.3]: - Teachers can now add, edit and delete annotation type templates and annotation types for the concrete module instance. - Added a first version of the annotations summary. diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 9f47ee3..edd5e19 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -5,6 +5,6 @@ define("mod_annopy/annotations",["exports","jquery","./highlighting"],(function( * @module mod_annopy/annotations * @copyright 2023 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=(cmid,canaddannotation,myuserid,focusannotation)=>{var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canaddannotation&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var submission=annotations[annotationid].submission;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(annotations[annotationid].annotationstart),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(annotations[annotationid].annotationend),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+submission+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+submission+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+submission).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+submission+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click",".annopy_submission #id_cancel",(function(e){e.preventDefault(),(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1})),(0,_jquery.default)(".annopy_submission textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canaddannotation){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){const ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;const rangeSelectors=ranges.map((range=>(0,_highlighting.describe)(root,range))),annotation={target:rangeSelectors.map((selectors=>({selector:selectors})))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var submission=this.id.replace(/submission-/,"");(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+submission+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+submission+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1},success:function(response){annotations=JSON.parse(response),function(){for(let annotation of Object.values(annotations)){const newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.annotationstart),end:parseInt(annotation.annotationend)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((selectors=>({selector:selectors})))};(0,_highlighting.anchor)(newannotation,(0,_jquery.default)("#submission-"+annotation.submission)[0])}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))})),(0,_jquery.default)(document).on("mouseover",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),0!=focusannotation&&((0,_jquery.default)(".annotated-"+focusannotation).attr("tabindex",-1),(0,_jquery.default)(".annotated-"+focusannotation).focus())},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){}})}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=(cmid,canaddannotation,myuserid,focusannotation,userid)=>{var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canaddannotation&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var submission=annotations[annotationid].submission;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(annotations[annotationid].annotationstart),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(annotations[annotationid].annotationend),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+submission+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+submission+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+submission).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+submission+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click",".annopy_submission #id_cancel",(function(e){e.preventDefault(),(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1})),(0,_jquery.default)(".annopy_submission textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canaddannotation){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){const ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;const rangeSelectors=ranges.map((range=>(0,_highlighting.describe)(root,range))),annotation={target:rangeSelectors.map((selectors=>({selector:selectors})))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var submission=this.id.replace(/submission-/,"");(0,_jquery.default)(".annotation-form-"+submission+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+submission+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationstart"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+submission+' input[name="annotationend"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+submission+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+submission+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+submission+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+submission+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+submission).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)(".annotationarea-"+submission+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+submission+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1,userid:userid},success:function(response){annotations=JSON.parse(response),function(){for(let annotation of Object.values(annotations)){const newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.annotationstart),end:parseInt(annotation.annotationend)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((selectors=>({selector:selectors})))};(0,_highlighting.anchor)(newannotation,(0,_jquery.default)("#submission-"+annotation.submission)[0])}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))})),(0,_jquery.default)(document).on("mouseover",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color","lightblue")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),0!=focusannotation&&((0,_jquery.default)(".annotated-"+focusannotation).attr("tabindex",-1),(0,_jquery.default)(".annotated-"+focusannotation).focus())},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){}})}})); //# sourceMappingURL=annotations.min.js.map \ No newline at end of file diff --git a/amd/build/annotations.min.js.map b/amd/build/annotations.min.js.map index c1edb9b..aa8405f 100644 --- a/amd/build/annotations.min.js.map +++ b/amd/build/annotations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"annotations.min.js","sources":["../src/annotations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for the annotation functions of the module.\n *\n * @module mod_annopy/annotations\n * @copyright 2023 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canaddannotation, myuserid, focusannotation) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '.annopy_submission #id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('.annopy_submission textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canaddannotation) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var submission = this.id.replace(/submission-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + submission + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\n\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotation-form-' + submission + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1},\n success: function(response) {\n annotations = JSON.parse(response);\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).addClass('hovered');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Highlight annotation if hoverannotation button is hovered\n $(document).on('mouseover', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n\n // Focus annotation if needed.\n if (focusannotation != 0) {\n $('.annotated-' + focusannotation).attr('tabindex', -1);\n $('.annotated-' + focusannotation).focus();\n }\n\n },\n complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n // For output: alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.annotationstart),\n end: parseInt(annotation.annotationend)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#submission-\" + annotation.submission)[0]);\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canaddannotation && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var submission = annotations[annotationid].submission;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(annotations[annotationid].annotationstart);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(annotations[annotationid].annotationend);\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\n\n $('.annotation-form-' + submission + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + submission + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + submission + ' select').val(annotations[annotationid].type);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\n $('#annotationpreview-temp-' + submission).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + submission + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotationarea-' + submission + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n};\n\n/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["obj","_jquery","__esModule","default","_exports","init","cmid","canaddannotation","myuserid","focusannotation","edited","annotations","Array","newannotation","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","submission","$","hide","val","startcontainer","endcontainer","startoffset","endoffset","annotationstart","annotationend","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","rangeSelectors","map","range","describe","annotation","target","selectors","selector","anchor","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","start","end","ajax","url","data","getannotations","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","attr","complete","error"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for the annotation functions of the module.\n *\n * @module mod_annopy/annotations\n * @copyright 2023 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canaddannotation, myuserid, focusannotation, userid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '.annopy_submission #id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('.annopy_submission textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canaddannotation) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var submission = this.id.replace(/submission-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + submission + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\n\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotation-form-' + submission + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1, 'userid': userid},\n success: function(response) {\n annotations = JSON.parse(response);\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).addClass('hovered');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Highlight annotation if hoverannotation button is hovered\n $(document).on('mouseover', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", 'lightblue');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n\n // Focus annotation if needed.\n if (focusannotation != 0) {\n $('.annotated-' + focusannotation).attr('tabindex', -1);\n $('.annotated-' + focusannotation).focus();\n }\n\n },\n complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n // For output: alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.annotationstart),\n end: parseInt(annotation.annotationend)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#submission-\" + annotation.submission)[0]);\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canaddannotation && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var submission = annotations[annotationid].submission;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + submission + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + submission + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + submission + ' input[name=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + submission + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + submission + ' input[name=\"annotationstart\"]').val(annotations[annotationid].annotationstart);\n $('.annotation-form-' + submission + ' input[name=\"annotationend\"]').val(annotations[annotationid].annotationend);\n $('.annotation-form-' + submission + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + submission + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + submission + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\n\n $('.annotation-form-' + submission + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + submission + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + submission + ' select').val(annotations[annotationid].type);\n\n // Prevent JavaScript injection (if annotated text in original submission is JavaScript code in script tags).\n $('#annotationpreview-temp-' + submission).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\n $('#annotationpreview-temp-' + submission).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + submission + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + submission + ' .annotation-form').show();\n $('.annotationarea-' + submission + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n};\n\n/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["obj","_jquery","__esModule","default","_exports","init","cmid","canaddannotation","myuserid","focusannotation","userid","edited","annotations","Array","newannotation","editAnnotation","annotationid","removeAllTempHighlights","resetForms","submission","$","hide","val","startcontainer","endcontainer","startoffset","endoffset","annotationstart","annotationend","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","rangeSelectors","map","range","describe","annotation","target","selectors","selector","anchor","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","start","end","ajax","url","data","getannotations","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","attr","complete","error"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 3871ca1..eeff6fa 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -24,7 +24,7 @@ import $ from 'jquery'; import {removeAllTempHighlights, anchor, describe} from './highlighting'; -export const init = (cmid, canaddannotation, myuserid, focusannotation) => { +export const init = (cmid, canaddannotation, myuserid, focusannotation, userid) => { var edited = false; var annotations = Array(); @@ -110,7 +110,7 @@ export const init = (cmid, canaddannotation, myuserid, focusannotation) => { // Fetch and recreate annotations. $.ajax({ url: './annotations.php', - data: {'id': cmid, 'getannotations': 1}, + data: {'id': cmid, 'getannotations': 1, 'userid': userid}, success: function(response) { annotations = JSON.parse(response); diff --git a/annotations.php b/annotations.php index abed196..55b68b4 100644 --- a/annotations.php +++ b/annotations.php @@ -38,6 +38,9 @@ // Param if annotation should be deleted. $deleteannotation = optional_param('deleteannotation', 0, PARAM_INT); // Annotation to be deleted. +// The ID of the user whose annotations should be shown. +$userid = optional_param('userid', 0, PARAM_INT); + // Set the basic variables $course, $cm and $moduleinstance. if ($id) { [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); @@ -64,7 +67,11 @@ // Get annotation (ajax). if ($getannotations) { - $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id)); + if ($userid) { + $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id, 'userid' => $userid)); + } else { + $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id)); + } $select = "annopy = " . $moduleinstance->id; diff --git a/annotations_summary.php b/annotations_summary.php index 988a55d..0272664 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -228,40 +228,26 @@ } } -$participants = array_values(get_enrolled_users($context, 'mod/annopy:potentialparticipant')); $annotationtypesforform = helper::get_annotationtypes_for_form($annotationtypes); - -foreach ($participants as $key => $participant) { - if (has_capability('mod/annopy:viewannotationsevaluation', $context) || $participant->id == $USER->id) { - $participants[$key]->annotations = array(); - - foreach ($annotationtypesforform as $i => $type) { - $sql = "SELECT COUNT(*) - FROM {annopy_annotations} a - JOIN {annopy_submissions} s ON s.id = a.submission - WHERE s.annopy = :annopy AND - s.author = :author AND - a.type = :atype"; - $params = array('annopy' => $moduleinstance->id, 'author' => $participant->id, 'atype' => $i); - $count = $DB->count_records_sql($sql, $params); - - $participants[$key]->annotations[$i] = $count; - } - - $participants[$key]->annotations = array_values($participants[$key]->annotations); - } else { - unset($participants[$key]); - } -} - -$participants = array_values($participants); +$participants = helper::get_annopy_participants($context, $moduleinstance, $annotationtypesforform); $strmanager = get_string_manager(); +$annotationstotalcount = 0; foreach ($annotationtypes as $i => $type) { $annotationtypes[$i]->canbeedited = $caneditannotationtype; $annotationtypes[$i]->canbedeleted = $candeleteannotationtype; + if (has_capability('mod/annopy:viewparticipants', $context)) { + $annotationtypes[$i]->totalcount = $DB->count_records('annopy_annotations', + array('annopy' => $moduleinstance->id, 'type' => $type->id)); + } else { + $annotationtypes[$i]->totalcount = $DB->count_records('annopy_annotations', + array('annopy' => $moduleinstance->id, 'type' => $type->id, 'userid' => $USER->id)); + } + + $annotationstotalcount += $annotationtypes[$i]->totalcount; + if ($strmanager->string_exists($type->name, 'mod_annopy')) { $annotationtypes[$i]->name = get_string($type->name, 'mod_annopy'); } else { @@ -307,7 +293,8 @@ $annotationtypetemplates = array_values($annotationtypetemplates); // Output page. -$page = new annopy_annotations_summary($cm->id, $context, $participants, $annotationtypes, $annotationtypetemplates, sesskey()); +$page = new annopy_annotations_summary($cm->id, $context, $participants, $annotationtypes, $annotationtypetemplates, + sesskey(), $annotationstotalcount); echo $OUTPUT->render($page); diff --git a/backup/moodle2/backup_annopy_stepslib.php b/backup/moodle2/backup_annopy_stepslib.php index 1a0872b..ddf920c 100644 --- a/backup/moodle2/backup_annopy_stepslib.php +++ b/backup/moodle2/backup_annopy_stepslib.php @@ -68,6 +68,7 @@ protected function define_structure() { $annopy->annotate_files('mod_annopy', 'intro', null); // This file area has no itemid. $entry->annotate_files('mod_annopy', 'entry', 'id'); */ - return $this->prepare_activity_structure($annopy); + //return $this->prepare_activity_structure($annopy); + return false; } } diff --git a/classes/forms/mod_annopy_colorpicker_form_element.php b/classes/forms/mod_annopy_colorpicker_form_element.php index d55877f..56000de 100644 --- a/classes/forms/mod_annopy_colorpicker_form_element.php +++ b/classes/forms/mod_annopy_colorpicker_form_element.php @@ -34,13 +34,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_annopy_colorpicker_form_element extends HTML_QuickForm_text { - // Whether to force the display of this element to flow LTR. + /** @var forceltr Whether to force the display of this element to flow LTR. */ public $forceltr = false; - // String html for help button, if empty then no help. + /** @var _helpbutton String html for help button, if empty then no help. */ public $_helpbutton = ''; - // If true label will be hidden. + /** @var _hiddenlabel If true label will be hidden. */ public $_hiddenlabel = false; /** diff --git a/classes/local/helper.php b/classes/local/helper.php index 6f7de56..6a0c1a9 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -25,6 +25,7 @@ use mod_annopy_annotation_form; use moodle_url; +use stdClass; /** * Utility class for the module. @@ -104,6 +105,51 @@ public static function get_all_annotationtype_templates() { return $annotationtypetemplates; } + /** + * Get all participants. + * @param object $context The context. + * @param object $moduleinstance The module instance. + * @param object $annotationtypesforform The annotation types prepared for the form. + * @param bool $originalkeys If the original keys should be returned. + * @return array action + */ + public static function get_annopy_participants($context, $moduleinstance, $annotationtypesforform, $originalkeys = false) { + global $USER, $DB; + + $participants = get_enrolled_users($context, 'mod/annopy:potentialparticipant'); + + foreach ($participants as $key => $participant) { + if (has_capability('mod/annopy:viewparticipants', $context) || $participant->id == $USER->id) { + $participants[$key]->annotations = array(); + $participants[$key]->annotationscount = 0; + + foreach ($annotationtypesforform as $i => $type) { + $sql = "SELECT COUNT(*) + FROM {annopy_annotations} a + JOIN {annopy_submissions} s ON s.id = a.submission + WHERE s.annopy = :annopy AND + a.userid = :userid AND + a.type = :atype"; + $params = array('annopy' => $moduleinstance->id, 'userid' => $participant->id, 'atype' => $i); + $count = $DB->count_records_sql($sql, $params); + + $participants[$key]->annotations[$i] = $count; + $participants[$key]->annotationscount += $count; + } + + $participants[$key]->annotations = array_values($participants[$key]->annotations); + } else { + unset($participants[$key]); + } + } + + if (!$originalkeys) { + $participants = array_values($participants); + } + + return $participants; + } + /** * Prepare the annotations for the submission. * @@ -113,17 +159,25 @@ public static function get_all_annotationtype_templates() { * @param object $submission The submission to be processed. * @param object $strmanager The moodle strmanager object needed to check annotation types in the annotation form. * @param object $annotationtypes The annotation types for the module. + * @param int $userid The ID of the user whose annotations should be shown. * @param object $annotationmode If annotationmode is activated. * @return object The submission with its annotations. */ public static function prepare_annotations($cm, $course, $context, $submission, $strmanager, $annotationtypes, - $annotationmode) { + $userid, $annotationmode) { global $DB, $USER, $CFG, $OUTPUT; // Get annotations for submission. - $submission->annotations = array_values($DB->get_records('annopy_annotations', + if ($userid) { + $submission->annotations = array_values($DB->get_records('annopy_annotations', + array('annopy' => $cm->instance, 'submission' => $submission->id, 'userid' => $userid))); + } else { + $submission->annotations = array_values($DB->get_records('annopy_annotations', array('annopy' => $cm->instance, 'submission' => $submission->id))); + } + $submission->totalannotationscount = $DB->count_records('annopy_annotations', + array('annopy' => $cm->instance, 'submission' => $submission->id)); foreach ($submission->annotations as $key => $annotation) { @@ -187,4 +241,57 @@ public static function prepare_annotations($cm, $course, $context, $submission, return $submission; } + + /** + * Returns the pagebar for the module instance. + * @param object $context The context for the module. + * @param int $userid The ID of the user whose annotations should be shown. + * @param object $submission The submission. + * @param object $moduleinstance The module instance. + * @param object $annotationtypesforform The annotationtypes prepared for the form. + * + * @return array action + */ + public static function get_pagebar($context, $userid, $submission, $moduleinstance, $annotationtypesforform) { + + if (!$submission) { + return false; + } + + $participants = self::get_annopy_participants($context, $moduleinstance, $annotationtypesforform); + + $pagebar = array(); + + foreach ($participants as $user) { + $obj = new stdClass(); + if ($userid == $user->id) { + $obj->userid = $user->id; + $obj->display = '' . fullname($user) . ' (' . $user->annotationscount . ')' . ''; + } else { + $obj->userid = $user->id; + $obj->display = $user->lastname; + + if ($user->annotationscount) { + $obj->display .= ' (' . $user->annotationscount . ')'; + } + } + + array_push($pagebar, $obj); + } + + $obj = new stdClass(); + $obj->userid = 'all'; + + if (!$userid) { + $obj->display = '' . get_string('allannotations', 'mod_annopy') . + ' (' . $submission->totalannotationscount . ')' . ''; + } else { + $obj->display = get_string('allannotations', 'mod_annopy') . + ' (' . $submission->totalannotationscount . ')'; + } + + array_push($pagebar, $obj); + + return $pagebar; + } } diff --git a/classes/output/annopy_annotations_summary.php b/classes/output/annopy_annotations_summary.php index ebbbd0f..30a0653 100644 --- a/classes/output/annopy_annotations_summary.php +++ b/classes/output/annopy_annotations_summary.php @@ -49,6 +49,8 @@ class annopy_annotations_summary implements renderable, templatable { protected $annotationtypetemplates; /** @var string */ protected $sesskey; + /** @var int */ + protected $annotationstotalcount; /** * Construct this renderable. @@ -58,8 +60,10 @@ class annopy_annotations_summary implements renderable, templatable { * @param array $annopyannotationtypes The annotationtypes used in the annopy instance * @param array $annotationtypetemplates The annotationtype templates available for the current user * @param string $sesskey The session key + * @param int $annotationstotalcount The total count of annotations */ - public function __construct($cmid, $context, $participants, $annopyannotationtypes, $annotationtypetemplates, $sesskey) { + public function __construct($cmid, $context, $participants, $annopyannotationtypes, $annotationtypetemplates, + $sesskey, $annotationstotalcount) { $this->cmid = $cmid; $this->context = $context; @@ -67,6 +71,7 @@ public function __construct($cmid, $context, $participants, $annopyannotationtyp $this->annopyannotationtypes = $annopyannotationtypes; $this->annotationtypetemplates = $annotationtypetemplates; $this->sesskey = $sesskey; + $this->annotationstotalcount = $annotationstotalcount; } /** @@ -76,12 +81,16 @@ public function __construct($cmid, $context, $participants, $annopyannotationtyp * @return stdClass */ public function export_for_template(renderer_base $output) { + global $USER; + $data = new stdClass(); $data->cmid = $this->cmid; + $data->myuserid = $USER->id; $data->participants = $this->participants; $data->annopyannotationtypes = $this->annopyannotationtypes; $data->annotationtypetemplates = $this->annotationtypetemplates; $data->sesskey = $this->sesskey; + $data->annotationstotalcount = $this->annotationstotalcount; if (has_capability('mod/annopy:addannotationtypetemplate', $this->context) || has_capability('mod/annopy:editannotationtypetemplate', $this->context)) { @@ -93,6 +102,7 @@ public function export_for_template(renderer_base $output) { $data->canaddannotationtype = has_capability('mod/annopy:addannotationtype', $this->context); $data->canaddannotationtypetemplate = has_capability('mod/annopy:addannotationtypetemplate', $this->context); + $data->canviewparticipants = has_capability('mod/annopy:viewparticipants', $this->context); return $data; } diff --git a/classes/output/annopy_view.php b/classes/output/annopy_view.php index 53658fa..a3017b5 100644 --- a/classes/output/annopy_view.php +++ b/classes/output/annopy_view.php @@ -46,20 +46,28 @@ class annopy_view implements renderable, templatable { /** @var object */ protected $context; /** @var object */ + protected $moduleinstance; + /** @var object */ protected $submission; + /** @var int */ + protected $userid; /** * Construct this renderable. - * @param int $cm The course module + * @param object $cm The course module * @param object $course The course * @param object $context The context + * @param object $moduleinstance The module instance * @param object $submission The submission + * @param int $userid The ID of the user whose annotations should be shown */ - public function __construct($cm, $course, $context, $submission) { + public function __construct($cm, $course, $context, $moduleinstance, $submission, $userid) { $this->cm = $cm; $this->course = $course; $this->context = $context; + $this->moduleinstance = $moduleinstance; $this->submission = $submission; + $this->userid = $userid; } /** @@ -75,13 +83,13 @@ public function export_for_template(renderer_base $output) { $data->cmid = $this->cm->id; $data->submission = $this->submission; - if ($data->submission) { - // If submission can be edited. - $data->submission->canbeedited = has_capability('mod/annopy:editsubmission', $this->context); + $select = "annopy = " . $this->cm->instance; + $annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); - // Set submission user. - $data->submission->user = $DB->get_record('user', array('id' => $data->submission->author)); - $data->submission->user->userpicture = $OUTPUT->user_picture($data->submission->user, + if ($data->submission) { + // Set submission author. + $data->submission->author = $DB->get_record('user', array('id' => $data->submission->author)); + $data->submission->author->userpicture = $OUTPUT->user_picture($data->submission->author, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); // Submission stats. @@ -90,15 +98,25 @@ public function export_for_template(renderer_base $output) { $data->submission->canviewdetails = has_capability('mod/annopy:addsubmission', $this->context); // Prepare annotations. - $select = "annopy = " . $this->cm->instance; - $annotationtypes = (array) $DB->get_records_select('annopy_annotationtypes', $select, null, 'priority ASC'); $data->submission = helper::prepare_annotations($this->cm, $this->course, $this->context, $data->submission, - get_string_manager(), $annotationtypes, true); + get_string_manager(), $annotationtypes, $this->userid, true); + + // If submission can be edited. + if (has_capability('mod/annopy:editsubmission', $this->context) && !$data->submission->totalannotationscount) { + $data->submission->canbeedited = true; + } else { + $data->submission->canbeedited = false; + } } $data->canaddsubmission = has_capability('mod/annopy:addsubmission', $this->context); + $data->caneditsubmission = has_capability('mod/annopy:editsubmission', $this->context); + $data->canviewparticipants = has_capability('mod/annopy:viewparticipants', $this->context); $data->sesskey = sesskey(); + $data->pagebar = helper::get_pagebar($this->context, $this->userid, $this->submission, $this->moduleinstance, + helper::get_annotationtypes_for_form($annotationtypes)); + return $data; } } diff --git a/lang/de/annopy.php b/lang/de/annopy.php index 363bd1b..73aadd9 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -62,6 +62,7 @@ $string['numwordsraw'] = '{$a->wordscount} Wörter mit {$a->charscount} Zeichen, einschließlich {$a->spacescount} Leerzeichen.'; $string['created'] = 'vor {$a->years} Jahren, {$a->month} Monaten, {$a->days} Tagen und {$a->hours} Stunden'; $string['nosubmission'] = 'Keine Einreichung'; +$string['allannotations'] = 'Alle Annotationen'; // Strings for annotations. $string['annotations'] = 'Annotationen'; @@ -125,6 +126,7 @@ $string['explanationhexcolor'] = 'Farbe'; $string['explanationhexcolor_help'] = 'Die Farbe des Annotationstypen als Hexadezimalwert. Dieser besteht aus genau 6 Zeichen (A-F sowie 0-9) und repräsentiert eine Farbe. Wenn die Farbe hier ausgewählt wird wird der Wert automatisch eingetragen, alternativ kann der Hexwert auch eingegeben werden. Den Hexwert von beliebigen Farben kann man z. B. unter https://www.w3schools.com/colors/colors_picker.asp herausfinden.'; $string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Annotationstyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre AnnoPys ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren AnnoPys verwendet werden.'; +$string['viewannotationsofuser'] = 'Annotationen des Benutzers ansehen'; // Strings for lib.php. $string['deletealluserdata'] = 'Alle Benutzerdaten löschen'; @@ -171,6 +173,7 @@ $string['incorrectcourseid'] = 'Inkorrekte Kurs-ID'; $string['incorrectmodule'] = 'Inkorrekte Kurs-Modul-ID'; $string['notallowedtodothis'] = 'Keine Berechtigung dies zu tun.'; +$string['alreadyannotated'] = 'Der Text kann nicht mehr bearbeitet werden da Teilnehmende ihn bereits annotiert haben.'; // Strings for the privacy api. /* diff --git a/lang/en/annopy.php b/lang/en/annopy.php index a4f24fd..64b2aa5 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -62,6 +62,7 @@ $string['numwordsraw'] = '{$a->wordscount} text words using {$a->charscount} characters, including {$a->spacescount} spaces.'; $string['created'] = '{$a->years} years, {$a->month} months, {$a->days} days and {$a->hours} hours ago'; $string['nosubmission'] = 'No submission'; +$string['allannotations'] = 'All annotations'; // Strings for annotations. $string['annotations'] = 'Annotations'; @@ -125,6 +126,7 @@ $string['explanationhexcolor'] = 'Color'; $string['explanationhexcolor_help'] = 'The color of the annotation type as hexadecimal value. This consists of exactly 6 characters (A-F as well as 0-9) and represents a color. If the color is selected here the value is entered automatically, alternatively the hex value can also be entered manually. You can find out the hexadecimal value of any color, for example, at https://www.w3schools.com/colors/colors_picker.asp.'; $string['explanationstandardtype'] = 'Here you can select whether the annotation type should be a default type. In this case teachers can select it as annotation type that can be used in their AnnoPys. Otherwise, only you can add this annotation type to your AnnoPys.'; +$string['viewannotationsofuser'] = 'View annotations of the user'; // Strings for lib.php. $string['deletealluserdata'] = 'Delete all user data'; @@ -171,6 +173,7 @@ $string['incorrectcourseid'] = 'Course ID is incorrect'; $string['incorrectmodule'] = 'Course Module ID is incorrect'; $string['notallowedtodothis'] = 'No permissions to do this.'; +$string['alreadyannotated'] = 'The text can no longer be edited because participants have already annotated it.'; // Strings for the privacy api. /* diff --git a/lib.php b/lib.php index 3ca9fb0..1a1bb77 100644 --- a/lib.php +++ b/lib.php @@ -77,16 +77,6 @@ function annopy_supports($feature) { function annopy_add_instance($moduleinstance, $mform = null) { global $DB; - // Handle access time for grading. - /* if (empty($moduleinstance->assessed)) { - $moduleinstance->assessed = 0; - } - - if (empty($moduleinstance->ratingtime) || empty($moduleinstance->assessed)) { - $moduleinstance->assesstimestart = 0; - $moduleinstance->assesstimefinish = 0; - } */ - $moduleinstance->timecreated = time(); $moduleinstance->id = $DB->insert_record('annopy', $moduleinstance); @@ -98,9 +88,23 @@ function annopy_add_instance($moduleinstance, $mform = null) { if (! empty($moduleinstance->completionexpected)) { \core_completion\api::update_completion_date_event($moduleinstance->coursemodule, 'annopy', $moduleinstance->id, $moduleinstance->completionexpected); - } + }*/ - annopy_grade_item_update($moduleinstance); */ + if (isset($moduleinstance->annotationtypes) && !empty($moduleinstance->annotationtypes)) { + // Add annotation types for the module instance. + $priority = 1; + foreach ($moduleinstance->annotationtypes as $id => $checked) { + if ($checked) { + $type = $DB->get_record('annopy_atype_templates', array('id' => $id)); + $type->annopy = $moduleinstance->id; + $type->priority = $priority; + + $priority += 1; + + $DB->insert_record('annopy_annotationtypes', $type); + } + } + } return $moduleinstance->id; } @@ -121,41 +125,6 @@ function annopy_update_instance($moduleinstance, $mform = null) { $moduleinstance->timemodified = time(); $moduleinstance->id = $moduleinstance->instance; - /* if (empty($moduleinstance->assessed)) { - $moduleinstance->assessed = 0; - } - - if (empty($moduleinstance->ratingtime) || empty($moduleinstance->assessed)) { - $moduleinstance->assesstimestart = 0; - $moduleinstance->assesstimefinish = 0; - } - - if (empty($moduleinstance->notification)) { - $moduleinstance->notification = 0; - } - - // If the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire moduleinstance - // if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond? - // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) - // when calculating the grade. - $oldmoduleinstance = $DB->get_record('annopy', array('id' => $moduleinstance->id)); - - $updategrades = false; - - if ($oldmoduleinstance->assessed <> $moduleinstance->assessed) { - // Whether this moduleinstance is rated. - $updategrades = true; - } - - if ($oldmoduleinstance->scale <> $moduleinstance->scale) { - // The scale currently in use. - $updategrades = true; - } - - if ($updategrades) { - annopy_update_grades($moduleinstance); // Recalculate grades for the moduleinstance. - } */ - $DB->update_record('annopy', $moduleinstance); /* // Update calendar. @@ -195,22 +164,23 @@ function annopy_delete_instance($id) { return false; } - /* $context = context_module::instance($cm->id); // Delete files. $fs = get_file_storage(); $fs->delete_area_files($context->id); - // Update completion for calendar events. - \core_completion\api::update_completion_date_event($cm->id, 'annopy', $annopy->id, null); + /* // Update completion for calendar events. + \core_completion\api::update_completion_date_event($cm->id, 'annopy', $annopy->id, null); */ + + // Delete submission. + $DB->delete_records("annopy_submissions", array("annopy" => $annopy->id)); - // Delete grades. - annopy_grade_item_delete($annopy); - */ + // Delete annotations. + $DB->delete_records("annopy_annotations", array("annopy" => $annopy->id)); - // Delete other db tables. - // ... + // Delete annotation types for the module instance. + $DB->delete_records("annopy_annotationtypes", array("annopy" => $annopy->id)); // Delete annopy, else return false. if (!$DB->delete_records("annopy", array("id" => $annopy->id))) { @@ -220,7 +190,6 @@ function annopy_delete_instance($id) { return true; } - /** * Returns a small object with summary information about what a * user has done with a given particular instance of this module diff --git a/mod_form.php b/mod_form.php index b9bc3b4..81fe630 100644 --- a/mod_form.php +++ b/mod_form.php @@ -61,19 +61,40 @@ public function definition() { $this->standard_intro_elements(); - // Set default values if module instance is updated. - /* $update = optional_param('update', null, PARAM_INT); + $update = optional_param('update', null, PARAM_INT); + if (!isset($update) || $update == 0) { - // ... . - } */ + // Add the header for the error types. + $mform->addElement('header', 'annotationtypeshdr', get_string('annotationtypes', 'annopy')); + $mform->setExpanded('annotationtypeshdr'); + + $select = "defaulttype = 1"; + $select .= " OR userid = " . $USER->id; + $annotationtypetemplates = (array) $DB->get_records_select('annopy_atype_templates', $select); + + $strmanager = get_string_manager(); - // Add custom activity settings. - /* $mform->addElement('header', 'availibilityhdr', get_string('availability')); + $this->add_checkbox_controller(1); - $mform->addElement('date_time_selector', 'timeopen', get_string('annopyopentime', 'annopy'), array( - 'optional' => true - )); - $mform->addHelpButton('timeopen', 'annopyopentime', 'annopy'); */ + foreach ($annotationtypetemplates as $id => $type) { + if ($type->defaulttype == 1) { + $name = '(S)'; + } else { + $name = '(M)'; + } + + if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_annopy')) { + $name .= '' . get_string($type->name, 'mod_annopy') . ''; + } else { + $name .= '' . $type->name . ''; + } + + $mform->addElement('advcheckbox', 'annotationtypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); + } + + } // Add standard grading elements. $this->standard_grading_coursemodule_elements(); diff --git a/templates/annopy_annotations_summary.mustache b/templates/annopy_annotations_summary.mustache index 64dcee8..2eae122 100644 --- a/templates/annopy_annotations_summary.mustache +++ b/templates/annopy_annotations_summary.mustache @@ -35,7 +35,7 @@ {{/canaddannotationtype}}
-

{{#str}}annopyannotationtypes, mod_annopy{{/str}}

+

{{#str}}annopyannotationtypes, mod_annopy{{/str}}

@@ -58,13 +58,37 @@ {{/annopyannotationtypes}} + + {{#participants}} - - {{#annotations}}{{/annotations}} + + + {{#annotations}} + + {{/annotations}} + + + {{/participants}} + {{#canviewparticipants}} + + + {{#annopyannotationtypes}} + + {{/annopyannotationtypes}} + + + + {{/canviewparticipants}}
{{#str}}total{{/str}}{{#str}}view{{/str}}
{{firstname}} {{lastname}}{{.}}
{{firstname}} {{lastname}}{{.}}{{annotationscount}} + +
{{#str}}total{{/str}} + {{totalcount}} + {{annotationstotalcount}} + +
@@ -76,7 +100,7 @@ {{#canaddannotationtypetemplate}} {{#str}}addannotationtype, mod_annopy{{/str}} ({{#str}}template, mod_annopy{{/str}}){{/canaddannotationtypetemplate}} -

{{#str}}annotationtypetemplates, mod_annopy{{/str}}

+

{{#str}}annotationtypetemplates, mod_annopy{{/str}}

{{#annotationtypetemplates.0}} diff --git a/templates/annopy_pagination.mustache b/templates/annopy_pagination.mustache new file mode 100644 index 0000000..23dbf5f --- /dev/null +++ b/templates/annopy_pagination.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - https://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 . +}} +{{! + @copyright 2023 coactum GmbH + @template annopy/annopy_pagination + + Template for the pagination area. + + Example context (json): + { + } +}} + +{{#js}} +{{/js}} + +
+
+ + + +
+ +
+ +
\ No newline at end of file diff --git a/templates/annopy_view.mustache b/templates/annopy_view.mustache index 0cd76b8..2542704 100644 --- a/templates/annopy_view.mustache +++ b/templates/annopy_view.mustache @@ -32,6 +32,10 @@

{{#str}}overview, mod_annopy{{/str}}

+ {{#submission.totalannotationscount}}{{#caneditsubmission}} + + {{/caneditsubmission}}{{/submission.totalannotationscount}} + {{#submission}}{{#submission.canbeedited}} {{#str}}editsubmission, mod_annopy{{/str}} {{/submission.canbeedited}}{{/submission}} @@ -40,6 +44,10 @@ {{/canaddsubmission}}{{/submission}} {{#str}}annotationssummary, mod_annopy{{/str}} + {{#canviewparticipants}} + {{> mod_annopy/annopy_pagination }} + {{/canviewparticipants}} +
{{#submission}}
@@ -69,7 +77,7 @@ {{/content}} {{^content}}

{{#str}}nosubmission, mod_annopy{{/str}}

{{/content}} {{#canviewdetails}}
- {{#user}}{{#userpicture}}{{#str}}author, mod_annopy{{/str}}: {{{userpicture}}}
{{/userpicture}}{{/user}} + {{#author}}{{#userpicture}}{{#str}}author, mod_annopy{{/str}}: {{{userpicture}}}
{{/userpicture}}{{/author}} {{#str}}timecreated, mod_annopy{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} {{#stats}}{{#datediff}}({{#str}}created, mod_annopy, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}}){{/datediff}}{{/stats}} diff --git a/version.php b/version.php index 98a95a9..10f241e 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.3.0'; -$plugin->version = 2023083000; +$plugin->release = '0.4.0'; +$plugin->version = 2023090400; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; diff --git a/view.php b/view.php index 2842270..6962c34 100644 --- a/view.php +++ b/view.php @@ -34,6 +34,9 @@ // Param with id of annotation that should be focused. $focusannotation = optional_param('focusannotation', 0, PARAM_INT); // ID of annotation. +// The ID of the user whose annotations should be shown. +$userid = optional_param('userid', 0, PARAM_INT); + // Set the basic variables $course, $cm and $moduleinstance. if ($id) { [$course, $cm] = get_course_and_cm_from_cmid($id, 'annopy'); @@ -65,6 +68,11 @@ $event->add_record_snapshot('annopy', $moduleinstance); $event->trigger(); +// If user is participant only show his own annotations. +if (!$userid && !has_capability('mod/annopy:viewparticipants', $context)) { + $userid = $USER->id; +} + // Get the name for this activity. $modulename = format_string($moduleinstance->name, true, array( 'context' => $context @@ -77,7 +85,7 @@ $PAGE->requires->js_call_amd('mod_annopy/annotations', 'init', array( 'cmid' => $cm->id, 'canaddannotation' => has_capability('mod/annopy:addannotation', $context), 'myuserid' => $USER->id, - 'focusannotation' => $focusannotation)); + 'focusannotation' => $focusannotation, 'userid' => $userid)); $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -108,7 +116,7 @@ $submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); // Render and output page. -$page = new annopy_view($cm, $course, $context, $submission); +$page = new annopy_view($cm, $course, $context, $moduleinstance, $submission, $userid); echo $OUTPUT->render($page); From 2b8a9a382ad97c879226082a01d77a26d39b22a0 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 14 Sep 2023 19:22:16 +0200 Subject: [PATCH 5/9] feat(multiple): Added backup functionality. Added privacy functionality. Added course reset functionality. --- CHANGES.md | 5 + annotations_summary.php | 7 +- annotationtypes.php | 9 +- .../backup_annopy_activity_task.class.php | 5 +- backup/moodle2/backup_annopy_settingslib.php | 27 -- backup/moodle2/backup_annopy_stepslib.php | 48 +- .../restore_annopy_activity_task.class.php | 16 +- backup/moodle2/restore_annopy_stepslib.php | 89 ++-- classes/privacy/provider.php | 443 ++++++++++-------- lang/de/annopy.php | 45 +- lang/en/annopy.php | 45 +- lib.php | 110 +++-- version.php | 4 +- 13 files changed, 508 insertions(+), 345 deletions(-) delete mode 100644 backup/moodle2/backup_annopy_settingslib.php diff --git a/CHANGES.md b/CHANGES.md index afb17f0..f3bd30e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ ## Changelog ## +- [0.5]: + - Added backup functionality. + - Added privacy functionality. + - Added course reset functionality. + - [0.4]: - On the annotations summary you can now view the total amount of annotations for each user and each annotation type. - On the overview you can now view the annotations filtered by users. diff --git a/annotations_summary.php b/annotations_summary.php index 0272664..2dbc087 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -93,7 +93,12 @@ $type = $DB->get_record('annopy_atype_templates', array('id' => $addtoannopy)); if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { - $type->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + if ($annotationtypes) { + $priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + } else { + $priority = 1; + } + $type->annopy = $moduleinstance->id; $DB->insert_record('annopy_annotationtypes', $type); diff --git a/annotationtypes.php b/annotationtypes.php index 77989b0..ff06df9 100644 --- a/annotationtypes.php +++ b/annotationtypes.php @@ -152,7 +152,14 @@ } if ($mode == 2) { // If type is annopy annotation type. - $annotationtype->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + + if ($annotationtypes) { + $priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + } else { + $priority = 1; + } + + $annotationtype->priority = $priority; $annotationtype->annopy = $moduleinstance->id; } diff --git a/backup/moodle2/backup_annopy_activity_task.class.php b/backup/moodle2/backup_annopy_activity_task.class.php index 41b40d2..0bfaeda 100644 --- a/backup/moodle2/backup_annopy_activity_task.class.php +++ b/backup/moodle2/backup_annopy_activity_task.class.php @@ -29,7 +29,6 @@ // More information about the restore process: {@link https://docs.moodle.org/dev/Restore_API}. require_once($CFG->dirroot.'/mod/annopy/backup/moodle2/backup_annopy_stepslib.php'); -require_once($CFG->dirroot.'/mod/annopy/backup/moodle2/backup_annopy_settingslib.php'); /** * The class provides all the settings and steps to perform one complete backup of mod_annopy. @@ -62,10 +61,10 @@ public static function encode_content_links($content) { $base = preg_quote($CFG->wwwroot, "/"); // Link to the list of plugin instances. - $search = "/(".$base."\//mod\/annopy\/index.php\?id\=)([0-9]+)/"; + $search = "/(".$base."\/mod\/annopy\/index.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@ANNOPYINDEX*$2@$', $content); - // Link to view by moduleid with optional userid if only items of one user should be shown. + // Link to view by moduleid with optional userid if only annotations of one user should be shown. $search = "/(".$base."\/mod\/annopy\/view.php\?id\=)([0-9]+)(&|&)userid=([0-9]+)/"; $content = preg_replace($search, '$@ANNOPYVIEWBYID*$2*$4@$', $content); diff --git a/backup/moodle2/backup_annopy_settingslib.php b/backup/moodle2/backup_annopy_settingslib.php deleted file mode 100644 index 578297f..0000000 --- a/backup/moodle2/backup_annopy_settingslib.php +++ /dev/null @@ -1,27 +0,0 @@ -. - -/** - * Plugin custom settings are defined here. - * - * @package mod_annopy - * @category backup - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -// More information about the backup process: {@link https://docs.moodle.org/dev/Backup_API}. -// More information about the restore process: {@link https://docs.moodle.org/dev/Restore_API}. diff --git a/backup/moodle2/backup_annopy_stepslib.php b/backup/moodle2/backup_annopy_stepslib.php index ddf920c..e852c18 100644 --- a/backup/moodle2/backup_annopy_stepslib.php +++ b/backup/moodle2/backup_annopy_stepslib.php @@ -40,35 +40,57 @@ class backup_annopy_activity_structure_step extends backup_activity_structure_st protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); - /* // Replace with the attributes and final elements that the element will handle. + // Replace with the attributes and final elements that the element will handle. $annopy = new backup_nested_element('annopy', array('id'), array( 'name', 'intro', 'introformat', 'timecreated', 'timemodified')); - $entries = new backup_nested_element('entries'); - $entry = new backup_nested_element('entry', array('id'), array( - 'userid', 'timecreated', 'timemodified', 'text', 'format')); + $annotationtypes = new backup_nested_element('annotationtypes'); + $annotationtype = new backup_nested_element('annotationtype', array('id'), array( + 'timecreated', 'timemodified', 'name', 'color', 'priority')); + + $submissions = new backup_nested_element('submissions'); + $submission = new backup_nested_element('submission', array('id'), array( + 'author', 'title', 'content', 'currentversion', 'format', 'timecreated', 'timemodified')); + + $annotations = new backup_nested_element('annotations'); + $annotation = new backup_nested_element('annotation', array('id'), array( + 'userid', 'timecreated', 'timemodified', 'type', 'startcontainer', 'endcontainer', + 'startoffset', 'endoffset', 'annotationstart', 'annotationend', 'exact', 'prefix', 'suffix', 'text')); // Build the tree with these elements with $root as the root of the backup tree. - $annopy->add_child($entries); - $entries->add_child($entry); + $annopy->add_child($annotationtypes); + $annotationtypes->add_child($annotationtype); - // Define the source tables for the elements. + $annopy->add_child($submissions); + $submissions->add_child($submission); + $submission->add_child($annotations); + $annotations->add_child($annotation); + + // Define the source tables for the elements. $annopy->set_source_table('annopy', array('id' => backup::VAR_ACTIVITYID)); + // Annotation types. + $annotationtype->set_source_table('annopy_annotationtypes', array('annopy' => backup::VAR_PARENTID)); + if ($userinfo) { - // Entries. - $entry->set_source_table('annopy_entries', array('annopy' => backup::VAR_PARENTID)); + // Submissions. + $submission->set_source_table('annopy_submissions', array('annopy' => backup::VAR_PARENTID)); + + // Annotations. + $annotation->set_source_table('annopy_annotations', array('submission' => backup::VAR_PARENTID)); } // Define id annotations. - $rating->annotate_ids('user', 'userid'); + if ($userinfo) { + $submission->annotate_ids('user', 'author'); + $annotation->annotate_ids('user', 'userid'); + } // Define file annotations. $annopy->annotate_files('mod_annopy', 'intro', null); // This file area has no itemid. - $entry->annotate_files('mod_annopy', 'entry', 'id'); */ + $submission->annotate_files('mod_annopy', 'submission', 'id'); - //return $this->prepare_activity_structure($annopy); - return false; + return $this->prepare_activity_structure($annopy); } } diff --git a/backup/moodle2/restore_annopy_activity_task.class.php b/backup/moodle2/restore_annopy_activity_task.class.php index 11e6357..1e5979f 100644 --- a/backup/moodle2/restore_annopy_activity_task.class.php +++ b/backup/moodle2/restore_annopy_activity_task.class.php @@ -61,10 +61,9 @@ public static function define_decode_contents() { // Define the contents (files). // tablename, array(field1, field 2), $mapping. - /* $contents[] = new restore_decode_content('annopy', array('intro'), 'annopy'); - $contents[] = new restore_decode_content('annopy_entries', array('text', 'feedback'), 'annopy_entry'); - */ + $contents[] = new restore_decode_content('annopy_submissions', array('content'), 'annopy_submission'); + return $contents; } @@ -77,11 +76,9 @@ public static function define_decode_rules() { $rules = array(); // Define the rules. - /* $rules[] = new restore_decode_rule('ANNOPYINDEX', '/mod/annopy/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('ANNOPYVIEWBYID', '/mod/annopy/view.php?id=$1&userid=$2', array('course_module', 'userid')); - */ return $rules; } @@ -97,9 +94,14 @@ public static function define_restore_log_rules() { $rules = array(); // Define the rules to restore the logs (one rule for each event / file in the plugin/event/ folder). - /* $rules[] = new restore_log_rule('annopy', 'view', 'view.php?id={course_module}', '{annopy}'); - */ + $rules[] = new restore_log_rule('annopy', 'add submission', 'view.php?id={course_module}', '{annopy}'); + $rules[] = new restore_log_rule('annopy', 'update submission', 'view.php?id={course_module}', '{annopy}'); + $rules[] = new restore_log_rule('annopy', 'delete submission', 'view.php?id={course_module}', '{annopy}'); + $rules[] = new restore_log_rule('annopy', 'add annotation', 'view.php?id={course_module}', '{annopy}'); + $rules[] = new restore_log_rule('annopy', 'update annotation', 'view.php?id={course_module}', '{annopy}'); + $rules[] = new restore_log_rule('annopy', 'delete annotation', 'view.php?id={course_module}', '{annopy}'); + return $rules; } diff --git a/backup/moodle2/restore_annopy_stepslib.php b/backup/moodle2/restore_annopy_stepslib.php index 4089e28..6cb9f47 100644 --- a/backup/moodle2/restore_annopy_stepslib.php +++ b/backup/moodle2/restore_annopy_stepslib.php @@ -45,9 +45,12 @@ protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('annopy', '/activity/annopy'); + $paths[] = new restore_path_element('annopy_annotationtype', '/activity/annopy/annotationtypes/annotationtype'); if ($userinfo) { - $paths[] = new restore_path_element('annopy_entry', '/activity/annopy/entries/entry'); + $paths[] = new restore_path_element('annopy_submission', '/activity/annopy/submissions/submission'); + $paths[] = new restore_path_element('annopy_submission_annotation', + '/activity/annopy/submissions/submission/annotations/annotation'); } return $this->prepare_activity_structure($paths); @@ -65,37 +68,69 @@ protected function process_annopy($data) { $oldid = $data->id; $data->course = $this->get_courseid(); - // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. - // See MDL-9367. - if (!isset($data->assesstimestart)) { - $data->assesstimestart = 0; - } - $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); + $newitemid = $DB->insert_record('annopy', $data); + $this->apply_activity_instance($newitemid); + $this->newinstanceid = $newitemid; - if (!isset($data->assesstimefinish)) { - $data->assesstimefinish = 0; - } - $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); + return; + } - if (!isset($data->timeopen)) { - $data->timeopen = 0; - } - $data->timeopen = $this->apply_date_offset($data->timeopen); + /** + * Restore AnnoPy submission. + * + * @param object $data data. + */ + protected function process_annopy_submission($data) { + global $DB; - if (!isset($data->timeclose)) { - $data->timeclose = 0; - } - $data->timeclose = $this->apply_date_offset($data->timeclose); + $data = (object) $data; + $oldid = $data->id; - if ($data->scale < 0) { // Scale found, get mapping. - $data->scale = - ($this->get_mappingid('scale', abs($data->scale))); - } + $data->annopy = $this->get_new_parentid('annopy'); + $data->author = $this->get_mappingid('user', $data->author); - $newitemid = $DB->insert_record('annopy', $data); - $this->apply_activity_instance($newitemid); - $this->newinstanceid = $newitemid; + $newitemid = $DB->insert_record('annopy_submissions', $data); + $this->set_mapping('annopy_submission', $oldid, $newitemid, true); // The true parameter is necessary for file handling. + } - return; + /** + * Restore AnnoPy annotationtype. + * + * @param object $data data. + */ + protected function process_annopy_annotationtype($data) { + global $DB; + + $data = (object) $data; + $oldid = $data->id; + + $data->annopy = $this->get_new_parentid('annopy'); + + $newitemid = $DB->insert_record('annopy_annotationtypes', $data); + $this->set_mapping('annopy_annotationtype', $oldid, $newitemid); + + } + + /** + * Add annotations to restored AnnoPy submissions. + * + * @param stdClass $data Tag + */ + protected function process_annopy_submission_annotation($data) { + global $DB; + + $data = (object) $data; + + $oldid = $data->id; + + $data->annopy = $this->newinstanceid; + $data->submission = $this->get_new_parentid('annopy_submission'); + $data->userid = $this->get_mappingid('user', $data->userid); + $data->type = $this->get_mappingid('annopy_annotationtype', $data->type); + + $newitemid = $DB->insert_record('annopy_annotations', $data); + + $this->set_mapping('annopy_annotation', $oldid, $newitemid); } /** @@ -106,6 +141,6 @@ protected function after_execute() { $this->add_related_files('mod_annopy', 'intro', null); // Component, filearea, mapping. - $this->add_related_files('mod_annopy', 'entry', 'annopy_entry'); + $this->add_related_files('mod_annopy', 'submission', 'annopy_submission'); } } diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 032a3cc..a6dc971 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -33,9 +33,6 @@ use \core_privacy\local\request\transform; use \core_privacy\local\request\contextlist; -use \core_privacy\local\request\user_preference_provider; - - /** * Implementation of the privacy subsystem plugin provider for the activity module. * @@ -50,9 +47,7 @@ class provider implements \core_privacy\local\request\plugin\provider, // This plugin is capable of determining which users have data within it. - \core_privacy\local\request\core_userlist_provider, - - \core_privacy\local\request\user_preference_provider { + \core_privacy\local\request\core_userlist_provider { /** * Provides the meta data stored for a user stored by the plugin. @@ -62,23 +57,42 @@ class provider implements */ public static function get_metadata(collection $items) : collection { - /* // The table 'annopy_participants' stores all annopy participants and their data. - $items->add_database_table('annopy_participants', [ - 'annopy' => 'privacy:metadata:annopy_participants:annopy', - ], 'privacy:metadata:annopy_participants'); - - // The table 'annopy_submissions' stores all group subbissions. + // The table 'annopy_submissions' stores all submissions. $items->add_database_table('annopy_submissions', [ 'annopy' => 'privacy:metadata:annopy_submissions:annopy', + 'author' => 'privacy:metadata:annopy_submissions:author', + 'title' => 'privacy:metadata:annopy_submissions:title', + 'content' => 'privacy:metadata:annopy_submissions:content', + 'currentversion' => 'privacy:metadata:annopy_submissions:currentversion', + 'format' => 'privacy:metadata:annopy_submissions:format', + 'timecreated' => 'privacy:metadata:annopy_submissions:timecreated', + 'timemodified' => 'privacy:metadata:annopy_submissions:timemodified', ], 'privacy:metadata:annopy_submissions'); - // The plguin uses multiple subsystems that save personal data. + // The table 'annopy_annotations' stores all annotations. + $items->add_database_table('annopy_annotations', [ + 'annopy' => 'privacy:metadata:annopy_annotations:annopy', + 'submission' => 'privacy:metadata:annopy_annotations:submission', + 'userid' => 'privacy:metadata:annopy_annotations:userid', + 'timecreated' => 'privacy:metadata:annopy_annotations:timecreated', + 'timemodified' => 'privacy:metadata:annopy_annotations:timemodified', + 'type' => 'privacy:metadata:annopy_annotations:type', + 'text' => 'privacy:metadata:annopy_annotations:text', + ], 'privacy:metadata:annopy_annotations'); + + // The table 'annopy_atype_templates' stores all annotation type templates. + $items->add_database_table('annopy_atype_templates', [ + 'timecreated' => 'privacy:metadata:annopy_atype_templates:timecreated', + 'timemodified' => 'privacy:metadata:annopy_atype_templates:timemodified', + 'name' => 'privacy:metadata:annopy_atype_templates:name', + 'color' => 'privacy:metadata:annopy_atype_templates:color', + 'userid' => 'privacy:metadata:annopy_atype_templates:userid', + ], 'privacy:metadata:annopy_atype_templates'); + + // The plugin uses multiple subsystems that save personal data. $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); - $items->add_subsystem_link('core_rating', [], 'privacy:metadata:core_rating'); - $items->add_subsystem_link('core_message', [], 'privacy:metadata:core_message'); - // User preferences in the plugin. - $items->add_user_preference('annopy_sortoption', 'privacy:metadata:preference:annopy_sortoption'); */ + // No user preferences in the plugin. return $items; } @@ -100,17 +114,27 @@ public static function get_contexts_for_userid(int $userid) : contextlist { 'userid' => $userid, ]; - // Get contexts of ... . + // Get contexts of the submissions. + $sql = "SELECT c.id + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {annopy} a ON a.id = cm.instance + JOIN {annopy_submissions} s ON s.annopy = a.id + WHERE s.author = :userid + "; + + $contextlist->add_from_sql($sql, $params); - $sql; - /* $sql = "SELECT c.id - FROM {context} c - JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel - JOIN {modules} m ON m.id = cm.module AND m.name = :modulename - JOIN {annopy} e ON e.id = cm.instance - JOIN {annopy_participants} p ON p.annopy = e.id - WHERE p.moodleuserid = :userid - "; */ + // Get contexts for annotations. + $sql = "SELECT c.id + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {annopy} a ON a.id = cm.instance + JOIN {annopy_annotations} aa ON aa.annopy = a.id + WHERE aa.userid = :userid + "; $contextlist->add_from_sql($sql, $params); @@ -134,14 +158,26 @@ public static function get_users_in_context(userlist $userlist) { 'modulename' => 'annopy', ]; - // Get users. - $sql; - /* $sql = "SELECT p.moodleuserid + // Find users with submissions. + $sql = "SELECT s.author + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {annopy} a ON a.id = cm.instance + JOIN {annopy_submissions} s ON s.annopy = a.id + WHERE cm.id = :instanceid + "; + + $userlist->add_from_sql('author', $sql, $params); + + // Find users with annotations. + $sql = "SELECT aa.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename - JOIN {annopy} e ON e.id = cm.instance - JOIN {annopy_participants} p ON p.annopy = e.id - WHERE cm.id = :instanceid"; */ + JOIN {annopy} a ON a.id = cm.instance + JOIN {annopy_annotations} aa ON aa.annopy = a.id + WHERE cm.id = :instanceid + "; + $userlist->add_from_sql('userid', $sql, $params); } @@ -163,13 +199,14 @@ public static function export_user_data(approved_contextlist $contextlist) { list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $params = $contextparams; - /* $sql = "SELECT + // Get all instances. + $sql = "SELECT c.id AS contextid, - p.*, + a.*, cm.id AS cmid FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid - JOIN {annopy} p ON P.id = cm.instance + JOIN {annopy} a ON a.id = cm.instance WHERE ( c.id {$contextsql} ) @@ -181,9 +218,10 @@ public static function export_user_data(approved_contextlist $contextlist) { foreach ($annopys as $annopy) { if ($annopy) { + $context = \context::instance_by_id($annopy->contextid); - // Store the main annopy data. + // Store the main data. $contextdata = helper::get_context_data($context, $user); // Write it. @@ -192,7 +230,7 @@ public static function export_user_data(approved_contextlist $contextlist) { // Write generic module intro files. helper::export_context_files($context, $user); - self::export_entries_data($userid, $annopy->id, $annopy->contextid); + self::export_submissions_data($userid, $annopy->id, $annopy->contextid); self::export_annotations_data($userid, $annopy->id, $annopy->contextid); @@ -201,168 +239,178 @@ public static function export_user_data(approved_contextlist $contextlist) { } } - $annopys->close(); */ + $annopys->close(); } /** - * Store all information about all .... + * Store all information about all submissions made by this user. * * @param int $userid The userid of the user whose data is to be exported. - * @param int $annopyid The id of the annopy. - * @param int $annopycontextid The context id of the annopy. + * @param int $annopyid The id of the module. + * @param int $annopycontextid The context id of the module. */ - /* protected static function export_entries_data(int $userid, $annopyid, $annopycontextid) { + protected static function export_submissions_data(int $userid, $annopyid, $annopycontextid) { global $DB; - // Find all entries for this annopy written by the user. + // Find all submissions for this module written by the user. $sql = "SELECT - e.id, - e.annopy, - e.userid, - e.timecreated, - e.timemodified, - e.text, - e.format, - e.rating, - e.feedback, - e.formatfeedback, - e.teacher, - e.timemarked, - e.baseentry - FROM {annopy_entries} e + s.id, + s.annopy, + s.author, + s.title, + s.content, + s.currentversion, + s.format, + s.timecreated, + s.timemodified + FROM {annopy_submissions} s WHERE ( - e.annopy = :annopyid AND - e.userid = :userid + s.annopy = :annopyid AND + s.author = :userid ) "; $params['userid'] = $userid; $params['annopyid'] = $annopyid; - // Get the annopys from the entries. - $entries = $DB->get_recordset_sql($sql, $params); + // Get the submissions. + $submissions = $DB->get_recordset_sql($sql, $params); - if ($entries->valid()) { - foreach ($entries as $entry) { - if ($entry) { + if ($submissions->valid()) { + foreach ($submissions as $submission) { + if ($submission) { $context = \context::instance_by_id($annopycontextid); - self::export_entry_data($userid, $context, ['annopy-entry-' . $entry->id], $entry); + self::export_submission_data($userid, $context, ['annopy-submission-' . $submission->id], $submission); } } } - $entries->close(); - } */ + $submissions->close(); + } /** - * Export all data in the entry. + * Export all data in the submission. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the annopy context. * @param array $subcontext The location within the current context that this data belongs. - * @param \stdClass $entry The entry. + * @param \stdClass $submission The submission. */ - /* protected static function export_entry_data(int $userid, \context $context, $subcontext, $entry) { + protected static function export_submission_data(int $userid, \context $context, $subcontext, $submission) { - if ($entry->timecreated != 0) { - $timecreated = transform::datetime($entry->timecreated); + if ($submission->timecreated != 0) { + $timecreated = transform::datetime($submission->timecreated); } else { $timecreated = null; } - if ($entry->timemodified != 0) { - $timemodified = transform::datetime($entry->timemodified); + if ($submission->timemodified != 0) { + $timemodified = transform::datetime($submission->timemodified); } else { $timemodified = null; } - if ($entry->timemarked != 0) { - $timemarked = transform::datetime($entry->timemarked); - } else { - $timemarked = null; - } - // Store related metadata. - $entrydata = (object) [ - 'annopy' => $entry->annopy, - 'userid' => $entry->userid, + $submissiondata = (object) [ + 'annopy' => $submission->annopy, + 'author' => $submission->author, + 'title' => $submission->title, + 'currentversion' => $submission->currentversion, 'timecreated' => $timecreated, 'timemodified' => $timemodified, - 'rating' => $entry->rating, - 'teacher' => $entry->teacher, - 'timemarked' => $timemarked, - 'baseentry' => $entry->baseentry, ]; - $entrydata->text = writer::with_context($context)->rewrite_pluginfile_urls($subcontext, 'mod_annopy', - 'entry', $entry->id, $entry->text); - - $entrydata->text = format_text($entrydata->text, $entry->format, (object) [ - 'para' => false, - 'context' => $context, - ]); - - $entrydata->feedback = writer::with_context($context)->rewrite_pluginfile_urls($subcontext, 'mod_annopy', - 'feedback', $entry->id, $entry->feedback); + $submissiondata->content = writer::with_context($context)->rewrite_pluginfile_urls($subcontext, 'mod_annopy', + 'submission', $submission->id, $submission->content); - $entrydata->feedback = format_text($entrydata->feedback, $entry->formatfeedback, (object) [ + $submissiondata->content = format_text($submissiondata->content, $submission->format, (object) [ 'para' => false, 'context' => $context, ]); - // Store the entry data. + // Store the submission data. writer::with_context($context) - ->export_data($subcontext, $entrydata) - ->export_area_files($subcontext, 'mod_annopy', 'entry', $entry->id) - ->export_area_files($subcontext, 'mod_annopy', 'feedback', $entry->id); - - // Store all ratings against this entry as the entry belongs to the user. All ratings on it are ratings of their content. - \core_rating\privacy\provider::export_area_ratings($userid, $context, $subcontext, 'mod_annopy', - 'entry', $entry->id, false); - } */ + ->export_data($subcontext, $submissiondata) + ->export_area_files($subcontext, 'mod_annopy', 'submission', $submission->id); + } /** - * Store all user preferences for the plugin. + * Store all information about all annotations made by this user. * * @param int $userid The userid of the user whose data is to be exported. + * @param int $instanceid The id of the module instance. + * @param int $contextid The context id of the module instance. */ - /* public static function export_user_preferences(int $userid) { - $user = \core_user::get_user($userid); - - if ($annopysortoption = get_user_preferences('annopy_sortoption', 0, $userid)) { - switch ($annopysortoption) { - case 1: - $sortoption = get_string('currenttooldest', 'mod_annopy'); - break; - case 2: - $sortoption = get_string('oldesttocurrent', 'mod_annopy'); - break; - case 3: - $sortoption = get_string('lowestgradetohighest', 'mod_annopy'); - break; - case 4: - $sortoption = get_string('highestgradetolowest', 'mod_annopy'); - break; - default: - $sortoption = get_string('currenttooldest', 'mod_annopy'); - break; - } + protected static function export_annotations_data(int $userid, $instanceid, $contextid) { + global $DB; - writer::export_user_preference('mod_annopy', 'annopy_sortoption', $annopysortoption, $sortoption); - } + // Find all annotations for this module instance made by the user. + $sql = "SELECT + a.id, + a.annopy, + a.submission, + a.userid, + a.timecreated, + a.timemodified, + a.type, + a.text + FROM {annopy_annotations} a + WHERE ( + a.annopy = :instanceid AND + a.userid = :userid + ) + "; + + $params['userid'] = $userid; + $params['instanceid'] = $instanceid; + + // Get the annotations. + $annotations = $DB->get_recordset_sql($sql, $params); + + if ($annotations->valid()) { + foreach ($annotations as $annotation) { + if ($annotation) { + $context = \context::instance_by_id($contextid); - if ($annopypagecount = get_user_preferences('annopy_pagecount', 0, $userid)) { - writer::export_user_preference('mod_annopy', 'annopy_pagecount', $annopypagecount, - get_string('privacy:metadata:preference:annopy_pagecount', 'mod_annopy')); + self::export_annotation_data($userid, $context, ['annopy-annotation-' . $annotation->id], $annotation); + } + } } - if ($annopyactivepage = get_user_preferences('annopy_activepage', 0, $userid)) { - writer::export_user_preference('mod_annopy', 'annopy_activepage', $annopyactivepage, - get_string('privacy:metadata:preference:annopy_activepage', 'mod_annopy')); + $annotations->close(); + } + + /** + * Export all data of the annotation. + * + * @param int $userid The userid of the user whose data is to be exported. + * @param \context $context The instance of the context. + * @param array $subcontext The location within the current context that this data belongs. + * @param \stdClass $annotation The annotation. + */ + protected static function export_annotation_data(int $userid, \context $context, $subcontext, $annotation) { + + if ($annotation->timemodified != 0) { + $timemodified = transform::datetime($annotation->timemodified); + } else { + $timemodified = null; } - } */ + // Store related metadata. + $annotationdata = (object) [ + 'annopy' => $annotation->annopy, + 'submission' => $annotation->submission, + 'userid' => $annotation->userid, + 'timecreated' => transform::datetime($annotation->timecreated), + 'timemodified' => $timemodified, + 'type' => $annotation->type, + 'text' => format_text($annotation->text, 2, array('para' => false)), + ]; + + // Store the annotation data. + writer::with_context($context)->export_data($subcontext, $annotationdata); + } /** * Delete all data for all users in the specified context. @@ -382,24 +430,19 @@ public static function delete_data_for_all_users_in_context(\context $context) { return; } - // Delete advanced grading information (not implemented yet). - - /* // Delete all ratings in the context. - \core_rating\privacy\provider::delete_ratings($context, 'mod_annopy', 'entry'); - - // Delete all files from the entry. + // Delete all files from the submission. $fs = get_file_storage(); - $fs->delete_area_files($context->id, 'mod_annopy', 'entry'); - $fs->delete_area_files($context->id, 'mod_annopy', 'feedback'); - - // Delete all records. - if ($DB->record_exists('annopy_participants', ['annopy' => $cm->instance])) { - $DB->delete_records('annopy_participants', ['annopy' => $cm->instance]); - } + $fs->delete_area_files($context->id, 'mod_annopy', 'submission'); + // Delete all submissions. if ($DB->record_exists('annopy_submissions', ['annopy' => $cm->instance])) { $DB->delete_records('annopy_submissions', ['annopy' => $cm->instance]); - } */ + } + + // Delete all annotations. + if ($DB->record_exists('annopy_annotations', ['annopy' => $cm->instance])) { + $DB->delete_records('annopy_annotations', ['annopy' => $cm->instance]); + } } /** @@ -417,40 +460,44 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { // Get the course module. $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); - /* // Handle any advanced grading method data first (not implemented yet). - // Delete ratings. - $entriessql = "SELECT - e.id - FROM {annopy_entries} e - WHERE ( - e.annopy = :annopyid AND - e.userid = :userid - ) + $submissionssql = "SELECT + s.id + FROM {annopy_submissions} s + WHERE ( + s.annopy = :instanceid AND + s.author = :author + ) "; - $entriesparams = [ - 'annopyid' => $cm->instance, - 'userid' => $userid, + $submissionsparams = [ + 'instanceid' => $cm->instance, + 'author' => $userid, ]; - \core_rating\privacy\provider::delete_ratings_select($context, 'mod_annopy', - 'entry', "IN ($entriessql)", $entriesparams); - - // Delete all files from the entries. + // Delete all files from the submissions. $fs = get_file_storage(); - $fs->delete_area_files_select($context->id, 'mod_annopy', 'entry', "IN ($entriessql)", $entriesparams); - $fs->delete_area_files_select($context->id, 'mod_annopy', 'feedback', "IN ($entriessql)", $entriesparams); + $fs->delete_area_files_select($context->id, 'mod_annopy', 'submission', "IN ($submissionssql)", $submissionsparams); - // Delete entries for user. - if ($DB->record_exists('annopy_entries', ['annopy' => $cm->instance, 'userid' => $userid])) { + // Delete submissions for user. + if ($DB->record_exists('annopy_submissions', ['annopy' => $cm->instance, 'author' => $userid])) { - $DB->delete_records('annopy_entries', [ + $DB->delete_records('annopy_submissions', [ + 'annopy' => $cm->instance, + 'author' => $userid, + ]); + + } + + // Delete annotations for user. + if ($DB->record_exists('annopy_annotations', ['annopy' => $cm->instance, 'userid' => $userid])) { + + $DB->delete_records('annopy_annotations', [ 'annopy' => $cm->instance, 'userid' => $userid, ]); - } */ + } } } @@ -466,31 +513,35 @@ public static function delete_data_for_users(approved_userlist $userlist) { $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); - $params = array_merge(['annopyid' => $cm->instance], $userinparams); - - // Handle any advanced grading method data first (not implemented yet). - - // Delete ratings. - /* $entriesselect = "SELECT - e.id - FROM {annopy_entries} e - WHERE ( - e.annopy = :annopyid AND - userid {$userinsql} - ) + $params = array_merge(['instanceid' => $cm->instance], $userinparams); + + $submissionselect = "SELECT + s.id + FROM {annopy_submissions} s + WHERE ( + s.annopy = :instanceid AND + author {$userinsql} + ) "; - \core_rating\privacy\provider::delete_ratings_select($context, 'mod_annopy', 'entry', "IN ($entriesselect)", $params); - - // Delete all files from the entries. + // Delete all files from the submissions. $fs = get_file_storage(); - $fs->delete_area_files_select($context->id, 'mod_annopy', 'entry', "IN ($entriesselect)", $params); - $fs->delete_area_files_select($context->id, 'mod_annopy', 'feedback', "IN ($entriesselect)", $params); + $fs->delete_area_files_select($context->id, 'mod_annopy', 'submission', "IN ($submissionselect)", $params); - // Delete entries for users. - if ($DB->record_exists_select('annopy_entries', "annopy = :annopyid AND userid {$userinsql}", $params)) { - $DB->delete_records_select('annopy_entries', "annopy = :annopyid AND userid {$userinsql}", $params); - } */ + // Delete annotations for users submissions that should be deleted. + if ($DB->record_exists_select('annopy_annotations', "submission IN ({$submissionsselect})", $params)) { + $DB->delete_records_select('annopy_annotations', "submission IN ({$submissionsselect})", $params); + } + + // Delete submissions for users. + if ($DB->record_exists_select('annopy_submissions', "annopy = :instanceid AND author {$userinsql}", $params)) { + $DB->delete_records_select('annopy_submissions', "annopy = :instanceid AND author {$userinsql}", $params); + } + + // Delete annotations for users. + if ($DB->record_exists_select('annopy_annotations', "annopy = :instanceid AND userid {$userinsql}", $params)) { + $DB->delete_records_select('annopy_annotations', "annopy = :instanceid AND userid {$userinsql}", $params); + } } } diff --git a/lang/de/annopy.php b/lang/de/annopy.php index 73aadd9..6835906 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -128,8 +128,14 @@ $string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Annotationstyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre AnnoPys ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren AnnoPys verwendet werden.'; $string['viewannotationsofuser'] = 'Annotationen des Benutzers ansehen'; -// Strings for lib.php. -$string['deletealluserdata'] = 'Alle Benutzerdaten löschen'; +// Strings for course reset. +$string['deletealluserdata'] = 'Die Einreichung mitsamt verbundenen Dateien, alle Annotationen und alle Annotationstypen löschen'; +$string['deleteannotations'] = 'Alle Annotationen löschen'; +$string['annotationsdeleted'] = 'Annotationen wurden gelöscht'; +$string['deletesubmissionandfiles'] = 'Die Einreichung, alle verbundenen Dateien und alle zugehörigen Annotationen löschen'; +$string['submissionandfilesdeleted'] = 'Die Einreichung, alle verbundenen Dateien und alle zugehörigen Annotationen wurden gelöscht'; +$string['deleteannotationtypes'] = 'Alle Annotationstypen löschen'; +$string['annotationtypesdeleted'] = 'Alle Annotationstypen wurden gelöscht'; // Strings for the recent activity. @@ -167,6 +173,10 @@ // Strings for events. $string['eventsubmissioncreated'] = 'Einreichung abgegeben'; $string['eventsubmissionupdated'] = 'Einreichung aktualisiert'; +$string['eventsubmissiondeleted'] = 'Einreichung gelöscht'; +$string['eventannotationcreated'] = 'Annotation angelegt'; +$string['eventannotationupdated'] = 'Annotation aktualisiert'; +$string['eventannotationdeleted'] = 'Annotation gelöscht'; // Strings for error messages. $string['errfilloutfield'] = 'Bitte Feld ausfüllen'; @@ -176,10 +186,27 @@ $string['alreadyannotated'] = 'Der Text kann nicht mehr bearbeitet werden da Teilnehmende ihn bereits annotiert haben.'; // Strings for the privacy api. -/* -$string['privacy:metadata:annopy_participants'] = 'Enthält die persönlichen Daten aller AnnoPys Teilnehmenden.'; -$string['privacy:metadata:annopy_submissions'] = 'Enthält alle Daten zu AnnoPy Einreichungen.'; -$string['privacy:metadata:annopy_participants:annopy'] = 'ID des AnnoPys des Teilnehmers'; -$string['privacy:metadata:annopy_submissions:annopy'] = 'ID des AnnoPys der Einreichung'; -$string['privacy:metadata:core_message'] = 'Das AnnoPy Plugin sendet Nachrichten an Benutzer und speichert deren Inhalte in der Datenbank.'; -*/ +$string['privacy:metadata:annopy_submissions'] = 'Enthält die Einreichungen aller AnnoPys.'; +$string['privacy:metadata:annopy_annotations'] = 'Enthält die in allen AnnoPys gemacht Annotationen.'; +$string['privacy:metadata:annopy_atype_templates'] = 'Enthält die von Lehrenden angelegten Annotationstyp-Vorlagen.'; +$string['privacy:metadata:annopy_submissions:annopy'] = 'ID des AnnoPy, zu dem die Einreichung gehört.'; +$string['privacy:metadata:annopy_submissions:author'] = 'ID des Autors der Einreichung.'; +$string['privacy:metadata:annopy_submissions:title'] = 'Der Titel der Einreichung.'; +$string['privacy:metadata:annopy_submissions:content'] = 'Der Inhalt der Einreichung.'; +$string['privacy:metadata:annopy_submissions:currentversion'] = 'Aktuelle Version der Einreichung.'; +$string['privacy:metadata:annopy_submissions:format'] = 'Format der Einreichung.'; +$string['privacy:metadata:annopy_submissions:timecreated'] = 'Zeitpunkt, an dem die Einreichung erstellt wurde.'; +$string['privacy:metadata:annopy_submissions:timemodified'] = 'Zeitpunkt der letzten Änderung der Einreichung.'; +$string['privacy:metadata:annopy_annotations:annopy'] = 'ID des AnnoPys, zu dem die annotierte Einreichung gehört.'; +$string['privacy:metadata:annopy_annotations:submission'] = 'ID der Einreichung, zu dem die Annotation gehört.'; +$string['privacy:metadata:annopy_annotations:userid'] = 'ID des Benutzers, der die Annotation angelegt hat.'; +$string['privacy:metadata:annopy_annotations:timecreated'] = 'Datum, an dem die Annotation erstellt wurde.'; +$string['privacy:metadata:annopy_annotations:timemodified'] = 'Zeitpunkt der letzten Änderung der Annotation.'; +$string['privacy:metadata:annopy_annotations:type'] = 'ID des Typs der Annotation.'; +$string['privacy:metadata:annopy_annotations:text'] = 'Inhalt der Annotation.'; +$string['privacy:metadata:annopy_atype_templates:timecreated'] = 'Datum, an dem die Annotationstyp-Vorlage erstellt wurde.'; +$string['privacy:metadata:annopy_atype_templates:timemodified'] = 'Zeitpunkt der letzten Änderung der Annotationstyp-Vorlage.'; +$string['privacy:metadata:annopy_atype_templates:name'] = 'Name der Annotationstyp-Vorlage.'; +$string['privacy:metadata:annopy_atype_templates:color'] = 'Farbe der Annotationstyp-Vorlage als Hex-Wert.'; +$string['privacy:metadata:annopy_atype_templates:userid'] = 'ID des Benutzers, der die Annotationstyp-Vorlage erstellt hat.'; +$string['privacy:metadata:core_files'] = 'Es werden mit AnnoPy Einreichungen verknüpfte Dateien gespeichert.'; diff --git a/lang/en/annopy.php b/lang/en/annopy.php index 64b2aa5..8bf44e4 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -128,8 +128,14 @@ $string['explanationstandardtype'] = 'Here you can select whether the annotation type should be a default type. In this case teachers can select it as annotation type that can be used in their AnnoPys. Otherwise, only you can add this annotation type to your AnnoPys.'; $string['viewannotationsofuser'] = 'View annotations of the user'; -// Strings for lib.php. -$string['deletealluserdata'] = 'Delete all user data'; +// Strings for course reset. +$string['deletealluserdata'] = 'Delete the submission with associated files, all annotations and all annotation types.'; +$string['deleteannotations'] = 'Delete all annotations'; +$string['annotationsdeleted'] = 'Annotations were deleted'; +$string['deletesubmissionandfiles'] = 'Delete the submission, all associated files and all associated annotations'; +$string['submissionandfilesdeleted'] = 'The submission, all associated files and all associated annotations been deleted'; +$string['deleteannotationtypes'] = 'Delete all annotation types'; +$string['annotationtypesdeleted'] = 'All annotation types were deleted'; // Strings for the recent activity. @@ -167,6 +173,10 @@ // Strings for events. $string['eventsubmissioncreated'] = 'Submission created'; $string['eventsubmissionupdated'] = 'Submission updated'; +$string['eventsubmissiondeleted'] = 'Submission deleted'; +$string['eventannotationcreated'] = 'Annotation created'; +$string['eventannotationupdated'] = 'Annotation updated'; +$string['eventannotationdeleted'] = 'Annotation deleted'; // Strings for error messages. $string['errfilloutfield'] = 'Please fill out this field'; @@ -176,10 +186,27 @@ $string['alreadyannotated'] = 'The text can no longer be edited because participants have already annotated it.'; // Strings for the privacy api. -/* -$string['privacy:metadata:annopy_participants'] = 'Contains the personal data of all AnnoPy participants.'; -$string['privacy:metadata:annopy_submissions'] = 'Contains all data related to AnnoPy submissions.'; -$string['privacy:metadata:annopy_participants:annopy'] = 'Id of the AnnoPy activity the participant belongs to'; -$string['privacy:metadata:annopy_submissions:annopy'] = 'Id of the AnnoPy activity the submission belongs to'; -$string['privacy:metadata:core_message'] = 'The AnnoPy plugin sends messages to users and saves their content in the database.'; -*/ +$string['privacy:metadata:annopy_submissions'] = 'Contains the submissions of all AnnoPys.'; +$string['privacy:metadata:annopy_annotations'] = 'Contains the annotations made in all AnnoPys.'; +$string['privacy:metadata:annopy_atype_templates'] = 'Contains the annotation type templates created by teachers.'; +$string['privacy:metadata:annopy_submissions:annopy'] = 'ID of the AnnoPy the submission belongs to.'; +$string['privacy:metadata:annopy_submissions:author'] = 'ID of the author of the submission.'; +$string['privacy:metadata:annopy_submissions:title'] = 'Title of the submission.'; +$string['privacy:metadata:annopy_submissions:content'] = 'Content of the submission.'; +$string['privacy:metadata:annopy_submissions:currentversion'] = 'Current version of the submission.'; +$string['privacy:metadata:annopy_submissions:format'] = 'Submission format.'; +$string['privacy:metadata:annopy_submissions:timecreated'] = 'Date on which the submission was made.'; +$string['privacy:metadata:annopy_submissions:timemodified'] = 'Time the submission was last modified.'; +$string['privacy:metadata:annopy_annotations:annopy'] = 'ID of the AnnoPy the annotated submission belongs to.'; +$string['privacy:metadata:annopy_annotations:submission'] = 'ID of the submission the annotation belongs to.'; +$string['privacy:metadata:annopy_annotations:userid'] = 'ID of the user that made the annotation.'; +$string['privacy:metadata:annopy_annotations:timecreated'] = 'Date on which the annotation was created.'; +$string['privacy:metadata:annopy_annotations:timemodified'] = 'Time the annotation was last modified.'; +$string['privacy:metadata:annopy_annotations:type'] = 'ID of the type of the annotation.'; +$string['privacy:metadata:annopy_annotations:text'] = 'Content of the annotation.'; +$string['privacy:metadata:annopy_atype_templates:timecreated'] = 'Date on which the annotation type template was created.'; +$string['privacy:metadata:annopy_atype_templates:timemodified'] = 'Time the annotation type template was last modified.'; +$string['privacy:metadata:annopy_atype_templates:name'] = 'Name of the annotation type template.'; +$string['privacy:metadata:annopy_atype_templates:color'] = 'Color of the annotation type template as hexadecimal value.'; +$string['privacy:metadata:annopy_atype_templates:userid'] = 'ID of the user that made the annotation type template.'; +$string['privacy:metadata:core_files'] = 'Files associated with AnnoPy submissions are saved.'; diff --git a/lib.php b/lib.php index 1a1bb77..7892bd0 100644 --- a/lib.php +++ b/lib.php @@ -29,8 +29,6 @@ * @uses FEATURE_SHOW_DESCRIPTION * @uses FEATURE_GRADE_HAS_GRADE * @uses FEATURE_RATE - * @uses FEATURE_GROUPS - * @uses FEATURE_GROUPINGS * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE__BACKUP_MOODLE2 * @param string $feature Constant for requested feature. @@ -48,10 +46,6 @@ function annopy_supports($feature) { return true; case FEATURE_SHOW_DESCRIPTION: return true; - case FEATURE_GROUPS: - return true; - case FEATURE_GROUPINGS: - return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_BACKUP_MOODLE2: @@ -182,7 +176,7 @@ function annopy_delete_instance($id) { // Delete annotation types for the module instance. $DB->delete_records("annopy_annotationtypes", array("annopy" => $annopy->id)); - // Delete annopy, else return false. + // Delete module instance, else return false. if (!$DB->delete_records("annopy", array("id" => $annopy->id))) { return false; } @@ -534,7 +528,17 @@ function annopy_print_recent_mod_activity($activity, $courseid, $detail, $modnam function annopy_reset_course_form_definition(&$mform) { $mform->addElement('header', 'annopyheader', get_string('modulenameplural', 'mod_annopy')); + $mform->addElement('checkbox', 'reset_annopy_annotations', get_string('deleteannotations', 'mod_annopy')); + $mform->disabledIf('reset_annopy_annotations', 'reset_annopy_all', 'checked'); + + $mform->addElement('checkbox', 'reset_annopy_submissionandfiles', get_string('deletesubmissionandfiles', 'mod_annopy')); + $mform->disabledIf('reset_annopy_submissionandfiles', 'reset_annopy_all', 'checked'); + + $mform->addElement('checkbox', 'reset_annopy_annotationtypes', get_string('deleteannotationtypes', 'mod_annopy')); + $mform->disabledIf('reset_annopy_annotationtypes', 'reset_annopy_all', 'checked'); + $mform->addElement('checkbox', 'reset_annopy_all', get_string('deletealluserdata', 'mod_annopy')); + } /** @@ -544,12 +548,13 @@ function annopy_reset_course_form_definition(&$mform) { * @return array */ function annopy_reset_course_form_defaults($course) { - return array('reset_annopy_all' => 1); + return array('reset_annopy_annotations' => 0, 'reset_annopy_submissionandfiles' => 0, + 'reset_annopy_annotationtypes' => 0, 'reset_annopy_all' => 1); } /** * This function is used by the reset_course_userdata function in moodlelib. - * This function will remove all userdata from the specified annopy. + * This function will remove all userdata from the specified module. * * @param object $data The data submitted from the reset course. * @return array status array @@ -558,79 +563,84 @@ function annopy_reset_userdata($data) { global $CFG, $DB; require_once($CFG->libdir . '/filelib.php'); - require_once($CFG->dirroot . '/rating/lib.php'); - $componentstr = get_string('modulenameplural', 'annopy'); + $modulenameplural = get_string('modulenameplural', 'annopy'); $status = array(); - /* // Get annopys in course that should be resetted. - $sql = "SELECT m.id - FROM {annopy} m - WHERE m.course = ?"; + $sql = "SELECT a.id + FROM {annopy} a + WHERE a.course = ?"; - $params = array( - $data->courseid - ); + $params = array($data->courseid); $annopys = $DB->get_records_sql($sql, $params); - // Delete entries and their annotations, files and ratings. - if (!empty($data->reset_annopy_all)) { + // Delete all annotations. + if (!empty($data->reset_annopy_annotations)) { + $DB->delete_records_select('annopy_annotations', "annopy IN ($sql)", $params); - $fs = get_file_storage(); + $status[] = array( + 'component' => $modulenameplural, + 'item' => get_string('annotationsdeleted', 'mod_annopy'), + 'error' => false + ); + } + + // Delete submission and associated files. + if (!empty($data->reset_annopy_all) || !empty($data->reset_annopy_submissionandfiles)) { - // Get ratings manager. - $rm = new rating_manager(); - $ratingdeloptions = new stdClass; - $ratingdeloptions->component = 'mod_annopy'; - $ratingdeloptions->ratingarea = 'entry'; + $fs = get_file_storage(); foreach ($annopys as $annopyid => $unused) { if (!$cm = get_coursemodule_from_instance('annopy', $annopyid)) { continue; } - // Remove files. + // Remove associated files. $context = context_module::instance($cm->id); - $fs->delete_area_files($context->id, 'mod_annopy', 'entry'); - $fs->delete_area_files($context->id, 'mod_annopy', 'feedback'); - - // Remove ratings. - $ratingdeloptions->contextid = $context->id; - $rm->delete_ratings($ratingdeloptions); - } - - // Remove all grades from gradebook (if that is not already done by the reset_gradebook_grades). - if (empty($data->reset_gradebook_grades)) { - annopy_reset_gradebook($data->courseid); + $fs->delete_area_files($context->id, 'mod_annopy', 'submission'); } - // Delete the annotations of all entries. + // Delete annotations. $DB->delete_records_select('annopy_annotations', "annopy IN ($sql)", $params); - // Delete all entries. - $DB->delete_records_select('annopy_entries', "annopy IN ($sql)", $params); + // Delete submission. + $DB->delete_records_select('annopy_submissions', "annopy IN ($sql)", $params); $status[] = array( - 'component' => $modulename, - 'item' => get_string('alluserdatadeleted', 'annopy'), + 'component' => $modulenameplural, + 'item' => get_string('submissionandfilesdeleted', 'mod_annopy'), 'error' => false ); - } */ + } + + // Delete annotation types. + if (!empty($data->reset_annopy_all) || !empty($data->reset_annopy_annotationtypes)) { + $DB->delete_records_select('annopy_annotationtypes', "annopy IN ($sql)", $params); + + $status[] = array( + 'component' => $modulenameplural, + 'item' => get_string('annotationtypesdeleted', 'mod_annopy'), + 'error' => false + ); + } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. - shift_course_mod_dates('annopy', array('assesstimestart', 'assesstimefinish'), $data->timeshift, $data->courseid); - $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); + shift_course_mod_dates('annopy', array(), $data->timeshift, $data->courseid); + $status[] = array( + 'component' => $modulenameplural, + 'item' => get_string('datechanged'), + 'error' => false + ); } return $status; } - /** * Removes all grades in the annopy gradebook * @@ -666,13 +676,13 @@ function annopy_get_user_grades($annopy, $userid = 0) { $ratingoptions = new stdClass(); $ratingoptions->component = 'mod_annopy'; - $ratingoptions->ratingarea = 'entry'; + $ratingoptions->ratingarea = 'submission'; $ratingoptions->modulename = 'annopy'; $ratingoptions->moduleid = $annopy->id; $ratingoptions->userid = $userid; $ratingoptions->aggregationmethod = $annopy->assessed; $ratingoptions->scaleid = $annopy->scale; - $ratingoptions->itemtable = 'annopy_entries'; + $ratingoptions->itemtable = 'annopy_submissions'; $ratingoptions->itemtableusercolumn = 'userid'; $rm = new rating_manager(); @@ -794,7 +804,7 @@ function annopy_scale_used_anywhere($scaleid) { */ function annopy_get_file_areas($course, $cm, $context) { return array( - 'entry' => get_string('entry', 'mod_annopy'), + 'submission' => get_string('submission', 'mod_annopy'), ); } diff --git a/version.php b/version.php index 10f241e..3c2b3fa 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.4.0'; -$plugin->version = 2023090400; +$plugin->release = '0.5.0'; +$plugin->version = 2023091400; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; From a4615c0e0bb4b5b3feaeabd55ea63ebd3594713a Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 7 Dec 2023 15:48:54 +0100 Subject: [PATCH 6/9] fix(annotationtypes): Fix for a bug preventing the change of the priority of annotation types. --- annotations_summary.php | 8 +++++--- annotationtypes.php | 5 ++--- classes/forms/mod_annopy_colorpicker_form_element.php | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/annotations_summary.php b/annotations_summary.php index 2dbc087..56857cc 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -93,10 +93,11 @@ $type = $DB->get_record('annopy_atype_templates', array('id' => $addtoannopy)); if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { + if ($annotationtypes) { - $priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + $type->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; } else { - $priority = 1; + $type->priority = 1; } $type->annopy = $moduleinstance->id; @@ -124,7 +125,8 @@ $oldpriority = 0; - if ($type && $action == 1 && $type->priority != 1) { // Increase priority (show more in front). + // Increase priority (show more in front). + if ($type && $action == 1 && $type->priority != $annotationtypes[array_key_first($annotationtypes)]->priority) { $oldpriority = $type->priority; $type->priority -= 1; diff --git a/annotationtypes.php b/annotationtypes.php index ff06df9..ca26bcf 100644 --- a/annotationtypes.php +++ b/annotationtypes.php @@ -154,12 +154,11 @@ if ($mode == 2) { // If type is annopy annotation type. if ($annotationtypes) { - $priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; + $annotationtype->priority = $annotationtypes[array_key_last($annotationtypes)]->priority + 1; } else { - $priority = 1; + $annotationtype->priority = 1; } - $annotationtype->priority = $priority; $annotationtype->annopy = $moduleinstance->id; } diff --git a/classes/forms/mod_annopy_colorpicker_form_element.php b/classes/forms/mod_annopy_colorpicker_form_element.php index 56000de..1520819 100644 --- a/classes/forms/mod_annopy_colorpicker_form_element.php +++ b/classes/forms/mod_annopy_colorpicker_form_element.php @@ -117,8 +117,6 @@ public function tohtml() {
- Loading
_getAttrString($this->_attributes) . '"> From b2c5a9c34fcac05477d8fe0029ca25e3da98ca82 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 7 Dec 2023 15:50:53 +0100 Subject: [PATCH 7/9] feat(chore): Changed code to comply with new moodle coding standards for Moodle 4.3. --- annotation_form.php | 2 +- annotations.php | 54 ++--- annotations_summary.php | 40 ++-- annotationtypes.php | 36 ++-- annotationtypes_form.php | 2 +- backup/moodle2/backup_annopy_stepslib.php | 24 +-- .../restore_annopy_activity_task.class.php | 17 +- backup/moodle2/restore_annopy_stepslib.php | 2 +- classes/event/annotation_created.php | 8 +- classes/event/annotation_deleted.php | 8 +- classes/event/annotation_updated.php | 8 +- classes/event/course_module_viewed.php | 2 +- classes/event/submission_created.php | 8 +- classes/event/submission_deleted.php | 8 +- classes/event/submission_updated.php | 8 +- .../mod_annopy_colorpicker_form_element.php | 6 +- classes/local/helper.php | 42 ++-- classes/local/submissionstats.php | 2 +- classes/output/annopy_view.php | 4 +- classes/privacy/provider.php | 18 +- classes/search/activity.php | 6 +- classes/search/submission.php | 14 +- db/access.php | 204 +++++++++--------- db/events.php | 8 +- db/install.xml | 14 -- db/messages.php | 12 +- db/tasks.php | 4 +- index.php | 22 +- lib.php | 114 ++++------ mod_form.php | 8 +- submit.php | 50 ++--- submit_form.php | 4 +- view.php | 22 +- 33 files changed, 361 insertions(+), 420 deletions(-) diff --git a/annotation_form.php b/annotation_form.php index 177e965..e5dc541 100644 --- a/annotation_form.php +++ b/annotation_form.php @@ -105,6 +105,6 @@ public function definition() { * @return array Array with errors */ public function validation($data, $files) { - return array(); + return []; } } diff --git a/annotations.php b/annotations.php index 55b68b4..adb863b 100644 --- a/annotations.php +++ b/annotations.php @@ -53,7 +53,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } else if (!$course) { throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); -} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { +} else if (!$coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } @@ -68,9 +68,9 @@ if ($getannotations) { if ($userid) { - $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id, 'userid' => $userid)); + $annotations = $DB->get_records('annopy_annotations', ['annopy' => $moduleinstance->id, 'userid' => $userid]); } else { - $annotations = $DB->get_records('annopy_annotations', array('annopy' => $moduleinstance->id)); + $annotations = $DB->get_records('annopy_annotations', ['annopy' => $moduleinstance->id]); } $select = "annopy = " . $moduleinstance->id; @@ -78,9 +78,9 @@ foreach ($annotations as $key => $annotation) { if (!array_key_exists($annotation->type, $annotationtypes) && - $DB->record_exists('annopy_annotationtypes', array('id' => $annotation->type))) { + $DB->record_exists('annopy_annotationtypes', ['id' => $annotation->type])) { - $annotationtypes[$annotation->type] = $DB->get_record('annopy_annotationtypes', array('id' => $annotation->type)); + $annotationtypes[$annotation->type] = $DB->get_record('annopy_annotationtypes', ['id' => $annotation->type]); } if (isset($annotationtypes[$annotation->type])) { @@ -92,7 +92,7 @@ if ($annotations) { echo json_encode($annotations); } else { - echo json_encode(array()); + echo json_encode([]); } die; @@ -101,10 +101,10 @@ require_capability('mod/annopy:viewannotations', $context); // Header. -$PAGE->set_url('/mod/annopy/annotations.php', array('id' => $id)); +$PAGE->set_url('/mod/annopy/annotations.php', ['id' => $id]); $PAGE->set_title(format_string($moduleinstance->name)); -$urlparams = array('id' => $id); +$urlparams = ['id' => $id]; $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); @@ -114,17 +114,17 @@ global $USER; - if ($DB->record_exists('annopy_annotations', array('id' => $deleteannotation, 'annopy' => $moduleinstance->id, - 'userid' => $USER->id))) { + if ($DB->record_exists('annopy_annotations', ['id' => $deleteannotation, 'annopy' => $moduleinstance->id, + 'userid' => $USER->id])) { - $DB->delete_records('annopy_annotations', array('id' => $deleteannotation, 'annopy' => $moduleinstance->id, - 'userid' => $USER->id)); + $DB->delete_records('annopy_annotations', ['id' => $deleteannotation, 'annopy' => $moduleinstance->id, + 'userid' => $USER->id]); // Trigger module annotation deleted event. - $event = \mod_annopy\event\annotation_deleted::create(array( + $event = \mod_annopy\event\annotation_deleted::create([ 'objectid' => $deleteannotation, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); @@ -138,14 +138,14 @@ require_once($CFG->dirroot . '/mod/annopy/annotation_form.php'); // Instantiate form. -$mform = new mod_annopy_annotation_form(null, array('types' => helper::get_annotationtypes_for_form($annotationtypes))); +$mform = new mod_annopy_annotation_form(null, ['types' => helper::get_annotationtypes_for_form($annotationtypes)]); if ($fromform = $mform->get_data()) { // In this case you process validated data. $mform->get_data() returns data posted in form. if ((isset($fromform->annotationid) && $fromform->annotationid !== 0) && isset($fromform->text)) { // Update annotation. $annotation = $DB->get_record('annopy_annotations', - array('annopy' => $moduleinstance->id, 'submission' => $fromform->submission, 'id' => $fromform->annotationid)); + ['annopy' => $moduleinstance->id, 'submission' => $fromform->submission, 'id' => $fromform->annotationid]); // Prevent changes by user in hidden form fields. if (!$annotation) { @@ -159,20 +159,20 @@ } $annotation->timemodified = time(); - $annotation->text = format_text($fromform->text, 2, array('para' => false)); + $annotation->text = format_text($fromform->text, 2, ['para' => false]); $annotation->type = $fromform->type; $DB->update_record('annopy_annotations', $annotation); // Trigger module annotation updated event. - $event = \mod_annopy\event\annotation_updated::create(array( + $event = \mod_annopy\event\annotation_updated::create([ 'objectid' => $fromform->annotationid, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); - $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $fromform->annotationid); + $urlparams = ['id' => $id, 'annotationmode' => 1, 'focusannotation' => $fromform->annotationid]; $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); redirect($redirecturl, get_string('annotationedited', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); @@ -191,7 +191,7 @@ redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); } - if (!$DB->record_exists('annopy_submissions', array('annopy' => $moduleinstance->id, 'id' => $fromform->submission))) { + if (!$DB->record_exists('annopy_submissions', ['annopy' => $moduleinstance->id, 'id' => $fromform->submission])) { redirect($redirecturl, get_string('annotationinvalid', 'mod_annopy'), null, notification::NOTIFY_ERROR); } @@ -215,13 +215,13 @@ $newid = $DB->insert_record('annopy_annotations', $annotation); // Trigger module annotation created event. - $event = \mod_annopy\event\annotation_created::create(array( + $event = \mod_annopy\event\annotation_created::create([ 'objectid' => $newid, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); - $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $newid); + $urlparams = ['id' => $id, 'annotationmode' => 1, 'focusannotation' => $newid]; $redirecturl = new moodle_url('/mod/annopy/view.php', $urlparams); redirect($redirecturl, get_string('annotationadded', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); diff --git a/annotations_summary.php b/annotations_summary.php index 56857cc..6750900 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -57,7 +57,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } else if (!$course) { throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); -} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { +} else if (!$coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } @@ -84,13 +84,13 @@ if ($addtoannopy && $canaddannotationtype) { require_sesskey(); - $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', ['id' => $id]); - if ($DB->record_exists('annopy_atype_templates', array('id' => $addtoannopy))) { + if ($DB->record_exists('annopy_atype_templates', ['id' => $addtoannopy])) { global $USER; - $type = $DB->get_record('annopy_atype_templates', array('id' => $addtoannopy)); + $type = $DB->get_record('annopy_atype_templates', ['id' => $addtoannopy]); if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { @@ -115,13 +115,13 @@ // Change priority. if ($caneditannotationtype && $mode == 2 && $priority && $action && - $DB->record_exists('annopy_annotationtypes', array('id' => $priority))) { + $DB->record_exists('annopy_annotationtypes', ['id' => $priority])) { require_sesskey(); - $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', ['id' => $id]); - $type = $DB->get_record('annopy_annotationtypes', array('annopy' => $moduleinstance->id, 'id' => $priority)); + $type = $DB->get_record('annopy_annotationtypes', ['annopy' => $moduleinstance->id, 'id' => $priority]); $oldpriority = 0; @@ -131,7 +131,7 @@ $type->priority -= 1; $typeswitched = $DB->get_record('annopy_annotationtypes', - array('annopy' => $moduleinstance->id, 'priority' => $type->priority)); + ['annopy' => $moduleinstance->id, 'priority' => $type->priority]); if (!$typeswitched) { // If no type with priority+1 search for types with hihgher priority values. $typeswitched = $DB->get_records_select('annopy_annotationtypes', @@ -143,13 +143,13 @@ } } else if ($type && $action == 2 && $type->priority != $DB->count_records('annopy_annotationtypes', - array('annopy' => $moduleinstance->id)) + 1) { // Decrease priority (move further back). + ['annopy' => $moduleinstance->id]) + 1) { // Decrease priority (move further back). $oldpriority = $type->priority; $type->priority += 1; $typeswitched = $DB->get_record('annopy_annotationtypes', - array('annopy' => $moduleinstance->id, 'priority' => $type->priority)); + ['annopy' => $moduleinstance->id, 'priority' => $type->priority]); if (!$typeswitched) { // If no type with priority+1 search for types with higher priority values. $typeswitched = $DB->get_records_select('annopy_annotationtypes', @@ -182,7 +182,7 @@ require_sesskey(); - $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', ['id' => $id]); if ($mode == 1) { // If type is template annotation type. $table = 'annopy_atype_templates'; @@ -190,16 +190,16 @@ $table = 'annopy_annotationtypes'; } - if ($DB->record_exists($table, array('id' => $delete))) { + if ($DB->record_exists($table, ['id' => $delete])) { - $type = $DB->get_record($table, array('id' => $delete)); + $type = $DB->get_record($table, ['id' => $delete]); if ($mode == 2 && $candeleteannotationtype || ($type->defaulttype == 1 && has_capability('mod/annopy:managedefaultannotationtypetemplates', $context) && $candeleteannotationtypetemplate) || ($type->defaulttype == 0 && $type->userid == $USER->id && $candeleteannotationtypetemplate)) { - $DB->delete_records($table, array('id' => $delete)); + $DB->delete_records($table, ['id' => $delete]); redirect($redirecturl, get_string('annotationtypedeleted', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); } else { redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); @@ -210,11 +210,11 @@ } // Get the name for this module instance. -$modulename = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$modulename = format_string($moduleinstance->name, true, [ + 'context' => $context, +]); -$PAGE->set_url('/mod/annopy/annotations_summary.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/annopy/annotations_summary.php', ['id' => $cm->id]); $PAGE->navbar->add(get_string('annotationssummary', 'mod_annopy')); $PAGE->set_title(get_string('modulename', 'mod_annopy').': ' . $modulename); @@ -247,10 +247,10 @@ if (has_capability('mod/annopy:viewparticipants', $context)) { $annotationtypes[$i]->totalcount = $DB->count_records('annopy_annotations', - array('annopy' => $moduleinstance->id, 'type' => $type->id)); + ['annopy' => $moduleinstance->id, 'type' => $type->id]); } else { $annotationtypes[$i]->totalcount = $DB->count_records('annopy_annotations', - array('annopy' => $moduleinstance->id, 'type' => $type->id, 'userid' => $USER->id)); + ['annopy' => $moduleinstance->id, 'type' => $type->id, 'userid' => $USER->id]); } $annotationstotalcount += $annotationtypes[$i]->totalcount; diff --git a/annotationtypes.php b/annotationtypes.php index ca26bcf..df6b463 100644 --- a/annotationtypes.php +++ b/annotationtypes.php @@ -50,7 +50,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } else if (!$course) { throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); -} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { +} else if (!$coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } @@ -58,7 +58,7 @@ $context = context_module::instance($cm->id); -$redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', array('id' => $id)); +$redirecturl = new moodle_url('/mod/annopy/annotations_summary.php', ['id' => $id]); // Capabilities check. if (!$edit) { // If type or template should be added. @@ -78,7 +78,7 @@ // Get type or template to be edited. if ($edit !== 0) { if ($mode == 1) { // If type is template type. - $editedtype = $DB->get_record('annopy_atype_templates', array('id' => $edit)); + $editedtype = $DB->get_record('annopy_atype_templates', ['id' => $edit]); if (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && !has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { @@ -86,7 +86,7 @@ redirect($redirecturl, get_string('notallowedtodothis', 'mod_annopy'), null, notification::NOTIFY_ERROR); } } else if ($mode == 2) { // If type is annopy type. - $editedtype = $DB->get_record('annopy_annotationtypes', array('id' => $edit)); + $editedtype = $DB->get_record('annopy_annotationtypes', ['id' => $edit]); if ($moduleinstance->id !== $editedtype->annopy) { redirect($redirecturl, get_string('annotationtypecantbeedited', 'mod_annopy'), null, notification::NOTIFY_ERROR); @@ -114,18 +114,18 @@ // Instantiate form. $mform = new mod_annopy_annotationtypes_form(null, - array('editdefaulttype' => has_capability('mod/annopy:managedefaultannotationtypetemplates', $context), 'mode' => $mode)); + ['editdefaulttype' => has_capability('mod/annopy:managedefaultannotationtypetemplates', $context), 'mode' => $mode]); if (isset($editedtypeid)) { if ($mode == 1) { // If type is template annotation type. - $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, - 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype)); + $mform->set_data(['id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, + 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype]); } else if ($mode == 2) { - $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, - 'color' => $editedcolor)); + $mform->set_data(['id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, + 'color' => $editedcolor]); } } else { - $mform->set_data(array('id' => $id, 'mode' => $mode, 'color' => '#FFFF00')); + $mform->set_data(['id' => $id, 'mode' => $mode, 'color' => '#FFFF00']); } if ($mform->is_cancelled()) { @@ -138,7 +138,7 @@ $annotationtype = new stdClass(); $annotationtype->timecreated = time(); $annotationtype->timemodified = 0; - $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); + $annotationtype->name = format_text($fromform->typename, 1, ['para' => false]); $annotationtype->color = $fromform->color; if (isset($fromform->standardtype) && $fromform->standardtype === 1 && @@ -173,9 +173,9 @@ } else if ($fromform->typeid !== 0 && isset($fromform->typename)) { // Update existing annotation type. if ($mode == 1) { // If type is template annotation type. - $annotationtype = $DB->get_record('annopy_atype_templates', array('id' => $fromform->typeid)); + $annotationtype = $DB->get_record('annopy_atype_templates', ['id' => $fromform->typeid]); } else if ($mode == 2) { // If type is annopy annotation type. - $annotationtype = $DB->get_record('annopy_annotationtypes', array('id' => $fromform->typeid)); + $annotationtype = $DB->get_record('annopy_annotationtypes', ['id' => $fromform->typeid]); } if ($annotationtype && @@ -186,7 +186,7 @@ && $annotationtype->userid == $USER->id))) { $annotationtype->timemodified = time(); - $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); + $annotationtype->name = format_text($fromform->typename, 1, ['para' => false]); $annotationtype->color = $fromform->color; if ($mode == 1 && has_capability('mod/annopy:managedefaultannotationtypetemplates', $context)) { @@ -218,11 +218,11 @@ } // Get the name for this module instance. -$modulename = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$modulename = format_string($moduleinstance->name, true, [ + 'context' => $context, +]); -$PAGE->set_url('/mod/annopy/annotationtypes.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/annopy/annotationtypes.php', ['id' => $cm->id]); $navtitle = ''; diff --git a/annotationtypes_form.php b/annotationtypes_form.php index 6e66d70..2bf1626 100644 --- a/annotationtypes_form.php +++ b/annotationtypes_form.php @@ -92,7 +92,7 @@ public function definition() { * @return array Array with errors */ public function validation($data, $files) { - $errors = array(); + $errors = []; if (strlen($data['color']) !== 6 || preg_match("/[^a-fA-F0-9]/", $data['color'])) { $errors['color'] = get_string('errnohexcolor', 'mod_annopy'); diff --git a/backup/moodle2/backup_annopy_stepslib.php b/backup/moodle2/backup_annopy_stepslib.php index e852c18..2bdba4a 100644 --- a/backup/moodle2/backup_annopy_stepslib.php +++ b/backup/moodle2/backup_annopy_stepslib.php @@ -41,21 +41,21 @@ protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); // Replace with the attributes and final elements that the element will handle. - $annopy = new backup_nested_element('annopy', array('id'), array( - 'name', 'intro', 'introformat', 'timecreated', 'timemodified')); + $annopy = new backup_nested_element('annopy', ['id'], [ + 'name', 'intro', 'introformat', 'timecreated', 'timemodified']); $annotationtypes = new backup_nested_element('annotationtypes'); - $annotationtype = new backup_nested_element('annotationtype', array('id'), array( - 'timecreated', 'timemodified', 'name', 'color', 'priority')); + $annotationtype = new backup_nested_element('annotationtype', ['id'], [ + 'timecreated', 'timemodified', 'name', 'color', 'priority']); $submissions = new backup_nested_element('submissions'); - $submission = new backup_nested_element('submission', array('id'), array( - 'author', 'title', 'content', 'currentversion', 'format', 'timecreated', 'timemodified')); + $submission = new backup_nested_element('submission', ['id'], [ + 'author', 'title', 'content', 'currentversion', 'format', 'timecreated', 'timemodified']); $annotations = new backup_nested_element('annotations'); - $annotation = new backup_nested_element('annotation', array('id'), array( + $annotation = new backup_nested_element('annotation', ['id'], [ 'userid', 'timecreated', 'timemodified', 'type', 'startcontainer', 'endcontainer', - 'startoffset', 'endoffset', 'annotationstart', 'annotationend', 'exact', 'prefix', 'suffix', 'text')); + 'startoffset', 'endoffset', 'annotationstart', 'annotationend', 'exact', 'prefix', 'suffix', 'text']); // Build the tree with these elements with $root as the root of the backup tree. $annopy->add_child($annotationtypes); @@ -68,17 +68,17 @@ protected function define_structure() { $annotations->add_child($annotation); // Define the source tables for the elements. - $annopy->set_source_table('annopy', array('id' => backup::VAR_ACTIVITYID)); + $annopy->set_source_table('annopy', ['id' => backup::VAR_ACTIVITYID]); // Annotation types. - $annotationtype->set_source_table('annopy_annotationtypes', array('annopy' => backup::VAR_PARENTID)); + $annotationtype->set_source_table('annopy_annotationtypes', ['annopy' => backup::VAR_PARENTID]); if ($userinfo) { // Submissions. - $submission->set_source_table('annopy_submissions', array('annopy' => backup::VAR_PARENTID)); + $submission->set_source_table('annopy_submissions', ['annopy' => backup::VAR_PARENTID]); // Annotations. - $annotation->set_source_table('annopy_annotations', array('submission' => backup::VAR_PARENTID)); + $annotation->set_source_table('annopy_annotations', ['submission' => backup::VAR_PARENTID]); } // Define id annotations. diff --git a/backup/moodle2/restore_annopy_activity_task.class.php b/backup/moodle2/restore_annopy_activity_task.class.php index 1e5979f..d7529a1 100644 --- a/backup/moodle2/restore_annopy_activity_task.class.php +++ b/backup/moodle2/restore_annopy_activity_task.class.php @@ -57,12 +57,12 @@ protected function define_my_steps() { * @return array. */ public static function define_decode_contents() { - $contents = array(); + $contents = []; // Define the contents (files). - // tablename, array(field1, field 2), $mapping. - $contents[] = new restore_decode_content('annopy', array('intro'), 'annopy'); - $contents[] = new restore_decode_content('annopy_submissions', array('content'), 'annopy_submission'); + // tablename, [field1, field 2], $mapping. + $contents[] = new restore_decode_content('annopy', ['intro'], 'annopy'); + $contents[] = new restore_decode_content('annopy_submissions', ['content'], 'annopy_submission'); return $contents; } @@ -73,12 +73,11 @@ public static function define_decode_contents() { * @return array. */ public static function define_decode_rules() { - $rules = array(); + $rules = []; // Define the rules. $rules[] = new restore_decode_rule('ANNOPYINDEX', '/mod/annopy/index.php?id=$1', 'course'); - $rules[] = new restore_decode_rule('ANNOPYVIEWBYID', '/mod/annopy/view.php?id=$1&userid=$2', - array('course_module', 'userid')); + $rules[] = new restore_decode_rule('ANNOPYVIEWBYID', '/mod/annopy/view.php?id=$1&userid=$2', ['course_module', 'userid']); return $rules; } @@ -91,7 +90,7 @@ public static function define_decode_rules() { * @return array. */ public static function define_restore_log_rules() { - $rules = array(); + $rules = []; // Define the rules to restore the logs (one rule for each event / file in the plugin/event/ folder). $rules[] = new restore_log_rule('annopy', 'view', 'view.php?id={course_module}', '{annopy}'); @@ -117,7 +116,7 @@ public static function define_restore_log_rules() { * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { - $rules = array(); + $rules = []; $rules[] = new restore_log_rule('annopy', 'view all', 'index.php?id={course}', null); diff --git a/backup/moodle2/restore_annopy_stepslib.php b/backup/moodle2/restore_annopy_stepslib.php index 6cb9f47..43891bd 100644 --- a/backup/moodle2/restore_annopy_stepslib.php +++ b/backup/moodle2/restore_annopy_stepslib.php @@ -41,7 +41,7 @@ class restore_annopy_activity_structure_step extends restore_activity_structure_ * @return restore_path_element[]. */ protected function define_structure() { - $paths = array(); + $paths = []; $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('annopy', '/activity/annopy'); diff --git a/classes/event/annotation_created.php b/classes/event/annotation_created.php index f2e2542..5d152d1 100644 --- a/classes/event/annotation_created.php +++ b/classes/event/annotation_created.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_annotations', 'restore' => 'annopy_annotation'); + return ['db' => 'annopy_annotations', 'restore' => 'annopy_annotation']; } } diff --git a/classes/event/annotation_deleted.php b/classes/event/annotation_deleted.php index fdf61f7..19f971e 100644 --- a/classes/event/annotation_deleted.php +++ b/classes/event/annotation_deleted.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_annotations', 'restore' => 'annopy_annotation'); + return ['db' => 'annopy_annotations', 'restore' => 'annopy_annotation']; } } diff --git a/classes/event/annotation_updated.php b/classes/event/annotation_updated.php index d285750..9502264 100644 --- a/classes/event/annotation_updated.php +++ b/classes/event/annotation_updated.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_annotations', 'restore' => 'annopy_annotation'); + return ['db' => 'annopy_annotations', 'restore' => 'annopy_annotation']; } } diff --git a/classes/event/course_module_viewed.php b/classes/event/course_module_viewed.php index 6ba5f04..4088945 100644 --- a/classes/event/course_module_viewed.php +++ b/classes/event/course_module_viewed.php @@ -46,6 +46,6 @@ protected function init() { * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy', 'restore' => 'annopy'); + return ['db' => 'annopy', 'restore' => 'annopy']; } } diff --git a/classes/event/submission_created.php b/classes/event/submission_created.php index 018199a..c1ac757 100644 --- a/classes/event/submission_created.php +++ b/classes/event/submission_created.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_submissions', 'restore' => 'annopy_submission'); + return ['db' => 'annopy_submissions', 'restore' => 'annopy_submission']; } } diff --git a/classes/event/submission_deleted.php b/classes/event/submission_deleted.php index 2cb4399..7f293e6 100644 --- a/classes/event/submission_deleted.php +++ b/classes/event/submission_deleted.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_submissions', 'restore' => 'annopy_submission'); + return ['db' => 'annopy_submissions', 'restore' => 'annopy_submission']; } } diff --git a/classes/event/submission_updated.php b/classes/event/submission_updated.php index 7e50849..44ce830 100644 --- a/classes/event/submission_updated.php +++ b/classes/event/submission_updated.php @@ -66,15 +66,15 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/annopy/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/annopy/view.php', [ + 'id' => $this->contextinstanceid, + ]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'annopy_submissions', 'restore' => 'annopy_submission'); + return ['db' => 'annopy_submissions', 'restore' => 'annopy_submission']; } } diff --git a/classes/forms/mod_annopy_colorpicker_form_element.php b/classes/forms/mod_annopy_colorpicker_form_element.php index 1520819..ec11e16 100644 --- a/classes/forms/mod_annopy_colorpicker_form_element.php +++ b/classes/forms/mod_annopy_colorpicker_form_element.php @@ -82,7 +82,7 @@ public function freeze() { * @return string Frozen html */ public function getfrozenhtml() { - $attributes = array('readonly' => 'readonly'); + $attributes = ['readonly' => 'readonly']; $this->updateAttributes($attributes); return $this->_getTabs() . '_getAttrString($this->_attributes) . '/>' . $this->_getPersistantData(); } @@ -95,8 +95,8 @@ public function getfrozenhtml() { public function tohtml() { global $CFG, $PAGE; - $PAGE->requires->js_init_call('M.util.init_colour_picker', array('id_color', null)); - $PAGE->requires->js_call_amd('mod_annopy/colorpicker-layout', 'init', array('id_color')); + $PAGE->requires->js_init_call('M.util.init_colour_picker', ['id_color', null]); + $PAGE->requires->js_call_amd('mod_annopy/colorpicker-layout', 'init', ['id_color']); // Add the class at the last minute. if ($this->get_force_ltr()) { diff --git a/classes/local/helper.php b/classes/local/helper.php index 6a0c1a9..625bcc8 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -45,28 +45,28 @@ class helper { */ public static function annopy_get_editor_and_attachment_options($course, $context) { // For the editor. - $editoroptions = array( + $editoroptions = [ 'trusttext' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $course->maxbytes, 'context' => $context, 'subdirs' => false, - ); + ]; // If maxfiles would be set to an int and more files are given the editor saves them all but // saves the overcouting incorrect so that white box is displayed. // For a file attachments field (not really needed here). - $attachmentoptions = array( + $attachmentoptions = [ 'subdirs' => false, 'maxfiles' => 1, - 'maxbytes' => $course->maxbytes - ); + 'maxbytes' => $course->maxbytes, + ]; - return array( + return [ $editoroptions, - $attachmentoptions - ); + $attachmentoptions, + ]; } /** @@ -76,7 +76,7 @@ public static function annopy_get_editor_and_attachment_options($course, $contex * @return array action */ public static function get_annotationtypes_for_form($annotationtypes) { - $types = array(); + $types = []; $strmanager = get_string_manager(); foreach ($annotationtypes as $key => $type) { if ($strmanager->string_exists($type->name, 'mod_annopy')) { @@ -120,7 +120,7 @@ public static function get_annopy_participants($context, $moduleinstance, $annot foreach ($participants as $key => $participant) { if (has_capability('mod/annopy:viewparticipants', $context) || $participant->id == $USER->id) { - $participants[$key]->annotations = array(); + $participants[$key]->annotations = []; $participants[$key]->annotationscount = 0; foreach ($annotationtypesforform as $i => $type) { @@ -130,7 +130,7 @@ public static function get_annopy_participants($context, $moduleinstance, $annot WHERE s.annopy = :annopy AND a.userid = :userid AND a.type = :atype"; - $params = array('annopy' => $moduleinstance->id, 'userid' => $participant->id, 'atype' => $i); + $params = ['annopy' => $moduleinstance->id, 'userid' => $participant->id, 'atype' => $i]; $count = $DB->count_records_sql($sql, $params); $participants[$key]->annotations[$i] = $count; @@ -171,18 +171,18 @@ public static function prepare_annotations($cm, $course, $context, $submission, // Get annotations for submission. if ($userid) { $submission->annotations = array_values($DB->get_records('annopy_annotations', - array('annopy' => $cm->instance, 'submission' => $submission->id, 'userid' => $userid))); + ['annopy' => $cm->instance, 'submission' => $submission->id, 'userid' => $userid])); } else { $submission->annotations = array_values($DB->get_records('annopy_annotations', - array('annopy' => $cm->instance, 'submission' => $submission->id))); + ['annopy' => $cm->instance, 'submission' => $submission->id])); } $submission->totalannotationscount = $DB->count_records('annopy_annotations', - array('annopy' => $cm->instance, 'submission' => $submission->id)); + ['annopy' => $cm->instance, 'submission' => $submission->id]); foreach ($submission->annotations as $key => $annotation) { // If annotation type does not exist. - if (!$DB->record_exists('annopy_annotationtypes', array('id' => $annotation->type))) { + if (!$DB->record_exists('annopy_annotationtypes', ['id' => $annotation->type])) { $submission->annotations[$key]->color = 'FFFF00'; $submission->annotations[$key]->type = get_string('deletedannotationtype', 'mod_annopy'); } else { @@ -203,9 +203,9 @@ public static function prepare_annotations($cm, $course, $context, $submission, if ($annotationmode) { // Add annotater images to annotations. - $annotater = $DB->get_record('user', array('id' => $annotation->userid)); + $annotater = $DB->get_record('user', ['id' => $annotation->userid]); $annotaterimage = $OUTPUT->user_picture($annotater, - array('courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + ['courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 20]); $submission->annotations[$key]->userpicturestr = $annotaterimage; } else { @@ -231,10 +231,10 @@ public static function prepare_annotations($cm, $course, $context, $submission, if ($annotationmode) { // Add annotation form. require_once($CFG->dirroot . '/mod/annopy/annotation_form.php'); - $mform = new mod_annopy_annotation_form(new moodle_url('/mod/annopy/annotations.php', array('id' => $cm->id)), - array('types' => self::get_annotationtypes_for_form($annotationtypes))); + $mform = new mod_annopy_annotation_form(new moodle_url('/mod/annopy/annotations.php', ['id' => $cm->id]), + ['types' => self::get_annotationtypes_for_form($annotationtypes)]); // Set default data. - $mform->set_data(array('id' => $cm->id, 'submission' => $submission->id)); + $mform->set_data(['id' => $cm->id, 'submission' => $submission->id]); $submission->annotationform = $mform->render(); } @@ -260,7 +260,7 @@ public static function get_pagebar($context, $userid, $submission, $moduleinstan $participants = self::get_annopy_participants($context, $moduleinstance, $annotationtypesforform); - $pagebar = array(); + $pagebar = []; foreach ($participants as $user) { $obj = new stdClass(); diff --git a/classes/local/submissionstats.php b/classes/local/submissionstats.php index cb93ae8..d1dae1b 100644 --- a/classes/local/submissionstats.php +++ b/classes/local/submissionstats.php @@ -46,7 +46,7 @@ public static function get_submission_stats($submissiontext, $submissiontimecrea $cleantext = preg_replace('#<[^>]+>#', ' ', $submissiontext, -1, $replacementspacescount); - $submissionstats = array(); + $submissionstats = []; $submissionstats['words'] = self::get_stats_words($cleantext); $submissionstats['chars'] = self::get_stats_chars($cleantext) - $replacementspacescount; $submissionstats['sentences'] = self::get_stats_sentences($cleantext); diff --git a/classes/output/annopy_view.php b/classes/output/annopy_view.php index a3017b5..87b458a 100644 --- a/classes/output/annopy_view.php +++ b/classes/output/annopy_view.php @@ -88,9 +88,9 @@ public function export_for_template(renderer_base $output) { if ($data->submission) { // Set submission author. - $data->submission->author = $DB->get_record('user', array('id' => $data->submission->author)); + $data->submission->author = $DB->get_record('user', ['id' => $data->submission->author]); $data->submission->author->userpicture = $OUTPUT->user_picture($data->submission->author, - array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); + ['courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25]); // Submission stats. $data->submission->stats = submissionstats::get_submission_stats($data->submission->content, diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index a6dc971..b0c921f 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -24,14 +24,14 @@ namespace mod_annopy\privacy; -use \core_privacy\local\request\userlist; -use \core_privacy\local\request\approved_contextlist; -use \core_privacy\local\request\approved_userlist; -use \core_privacy\local\request\writer; -use \core_privacy\local\request\helper; -use \core_privacy\local\metadata\collection; -use \core_privacy\local\request\transform; -use \core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\writer; +use core_privacy\local\request\helper; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\transform; +use core_privacy\local\request\contextlist; /** * Implementation of the privacy subsystem plugin provider for the activity module. @@ -405,7 +405,7 @@ protected static function export_annotation_data(int $userid, \context $context, 'timecreated' => transform::datetime($annotation->timecreated), 'timemodified' => $timemodified, 'type' => $annotation->type, - 'text' => format_text($annotation->text, 2, array('para' => false)), + 'text' => format_text($annotation->text, 2, ['para' => false]), ]; // Store the annotation data. diff --git a/classes/search/activity.php b/classes/search/activity.php index 998ac0a..5e532b8 100644 --- a/classes/search/activity.php +++ b/classes/search/activity.php @@ -51,10 +51,10 @@ public function uses_file_indexing() { * @return array */ public function get_search_fileareas() { - $fileareas = array( + $fileareas = [ 'intro', - 'submission' - ); // Fileareas. + 'submission', + ]; // Fileareas. return $fileareas; } } diff --git a/classes/search/submission.php b/classes/search/submission.php index 7cb0d45..7243e46 100644 --- a/classes/search/submission.php +++ b/classes/search/submission.php @@ -41,7 +41,7 @@ class submission extends \core_search\base_mod { * * @var array Internal quick static cache. */ - protected $entriesdata = array(); + protected $entriesdata = []; /** * Returns recordset containing required data for indexing AnnoPy entries. @@ -76,7 +76,7 @@ public function get_document_recordset($modifiedfrom = 0, \context $context = nu * @param array $options * @return \core_search\document */ - public function get_document($submission, $options = array()) { + public function get_document($submission, $options = []) { /* try { $cm = $this->get_cm('annopy', $submission->annopy, $submission->course); $context = \context_module::instance($cm->id); @@ -156,9 +156,9 @@ public function get_doc_url(\core_search\document $doc) { $entryuserid = $doc->get('userid'); $url = '/mod/annopy/view.php'; - return new \moodle_url($url, array( + return new \moodle_url($url, [ 'id' => $contextmodule->instanceid - )); */ + ]); */ } /** @@ -169,9 +169,9 @@ public function get_doc_url(\core_search\document $doc) { */ public function get_context_url(\core_search\document $doc) { /* $contextmodule = \context::instance_by_id($doc->get('contextid')); - return new \moodle_url('/mod/annopy/view.php', array( + return new \moodle_url('/mod/annopy/view.php', [ 'id' => $contextmodule->instanceid - )); */ + ]); */ } /** @@ -187,6 +187,6 @@ protected function get_entry($entryid) { /* global $DB; return $DB->get_record_sql("SELECT me.*, m.course FROM {annopy_entries} me JOIN {annopy} m ON m.id = me.annopy - WHERE me.id = ?", array('id' => $entryid), MUST_EXIST); */ + WHERE me.id = ?", ['id' => $entryid], MUST_EXIST); */ } } diff --git a/db/access.php b/db/access.php index 40017c9..c145b98 100644 --- a/db/access.php +++ b/db/access.php @@ -25,225 +25,225 @@ defined('MOODLE_INTERNAL') || die(); -$capabilities = array( +$capabilities = [ - 'mod/annopy:addinstance' => array( + 'mod/annopy:addinstance' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, - 'archetypes' => array( + 'archetypes' => [ 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ), - 'clonepermissionsfrom' => 'moodle/course:manageactivities' - ), + 'manager' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], - 'mod/annopy:potentialparticipant' => array( + 'mod/annopy:potentialparticipant' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, - ) - ), + ], + ], - 'mod/annopy:viewparticipants' => array( + 'mod/annopy:viewparticipants' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:manageparticipants' => array( + 'mod/annopy:manageparticipants' => [ 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:addsubmission' => array( + 'mod/annopy:addsubmission' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:editsubmission' => array( + 'mod/annopy:editsubmission' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:deletesubmission' => array( + 'mod/annopy:deletesubmission' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:addannotation' => array( + 'mod/annopy:addannotation' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:editannotation' => array( + 'mod/annopy:editannotation' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:deleteannotation' => array( + 'mod/annopy:deleteannotation' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:viewannotations' => array( + 'mod/annopy:viewannotations' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:viewannotationsevaluation' => array( + 'mod/annopy:viewannotationsevaluation' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:viewmyannotationsummary' => array( + 'mod/annopy:viewmyannotationsummary' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:addannotationtype' => array( + 'mod/annopy:addannotationtype' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:editannotationtype' => array( + 'mod/annopy:editannotationtype' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:deleteannotationtype' => array( + 'mod/annopy:deleteannotationtype' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:addannotationtypetemplate' => array( + 'mod/annopy:addannotationtypetemplate' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:editannotationtypetemplate' => array( + 'mod/annopy:editannotationtypetemplate' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:deleteannotationtypetemplate' => array( + 'mod/annopy:deleteannotationtypetemplate' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/annopy:managedefaultannotationtypetemplates' => array( + 'mod/annopy:managedefaultannotationtypetemplates' => [ 'riskbitmask' => RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( - 'manager' => CAP_ALLOW - ) - ), -); + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], +]; diff --git a/db/events.php b/db/events.php index d9735b3..dbd4558 100644 --- a/db/events.php +++ b/db/events.php @@ -25,10 +25,4 @@ defined('MOODLE_INTERNAL') || die(); // List of observers. -$observers = array( - /* - 'eventname' => '\core\event\course_module_created', - 'callback' => '\plugintype_pluginname\event\observer\course_module_created::store', - 'priority' => 1000, - */ -); +$observers = []; diff --git a/db/install.xml b/db/install.xml index be83165..8b718f6 100644 --- a/db/install.xml +++ b/db/install.xml @@ -22,20 +22,6 @@
- - - - - - - - - - - - - -
diff --git a/db/messages.php b/db/messages.php index ec0df62..c675ea2 100644 --- a/db/messages.php +++ b/db/messages.php @@ -25,12 +25,12 @@ defined('MOODLE_INTERNAL') || die(); -$messageproviders = array ( -/* 'sendmessages' => array( +$messageproviders = [ +/* 'sendmessages' => [ 'capability' => 'mod/annopy:sendmessages', - 'defaults' => array( + 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, 'email' => MESSAGE_PERMITTED, - ), - ), */ -); + ], + ], */ +]; diff --git a/db/tasks.php b/db/tasks.php index bab3115..84021a1 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$tasks = array( +$tasks = [ /* [ 'classname' => 'mod_annopy\task\task', 'blocking' => 0, @@ -35,4 +35,4 @@ 'month' => '*', 'dayofweek' => '*', ], */ -); +]; diff --git a/index.php b/index.php index ce1e6aa..dee9b11 100644 --- a/index.php +++ b/index.php @@ -28,7 +28,7 @@ $id = required_param('id', PARAM_INT); // ID of the course. if ($id) { - if (!$course = $DB->get_record('course', array('id' => $id))) { + if (!$course = $DB->get_record('course', ['id' => $id])) { throw new moodle_exception('invalidcourseid'); } } else { @@ -40,9 +40,9 @@ $coursecontext = context_course::instance($course->id); // Trigger course_module_instance_list_viewed event. -$event = \mod_annopy\event\course_module_instance_list_viewed::create(array( - 'context' => $coursecontext -)); +$event = \mod_annopy\event\course_module_instance_list_viewed::create([ + 'context' => $coursecontext, +]); $event->add_record_snapshot('course', $course); $event->trigger(); @@ -51,7 +51,7 @@ $PAGE->set_pagelayout('incourse'); -$PAGE->set_url('/mod/annopy/index.php', array('id' => $id)); +$PAGE->set_url('/mod/annopy/index.php', ['id' => $id]); $PAGE->navbar->add($modulenameplural); @@ -72,12 +72,12 @@ } if (empty($moduleinstances)) { - notice(get_string('nonewmodules', 'mod_annopy'), new moodle_url('/course/view.php', array('id' => $course->id))); + notice(get_string('nonewmodules', 'mod_annopy'), new moodle_url('/course/view.php', ['id' => $course->id])); } $table = new html_table(); -$table->head = array(); -$table->align = array(); +$table->head = []; +$table->align = []; if ($usesections) { // Add column heading based on the course format. e.g. Week, Topic. $table->head[] = get_string('sectionname', 'format_' . $course->format); @@ -113,9 +113,9 @@ } // Link. - $annopyname = format_string($annopy->name, true, array( - 'context' => $context - )); + $annopyname = format_string($annopy->name, true, [ + 'context' => $context, + ]); if (! $annopy->visible) { // Show dimmed if the mod is hidden. $table->data[$i][] = "coursemodule\">" . $annopyname . ""; diff --git a/lib.php b/lib.php index 7892bd0..fbb2ae4 100644 --- a/lib.php +++ b/lib.php @@ -89,7 +89,7 @@ function annopy_add_instance($moduleinstance, $mform = null) { $priority = 1; foreach ($moduleinstance->annotationtypes as $id => $checked) { if ($checked) { - $type = $DB->get_record('annopy_atype_templates', array('id' => $id)); + $type = $DB->get_record('annopy_atype_templates', ['id' => $id]); $type->annopy = $moduleinstance->id; $type->priority = $priority; @@ -148,13 +148,13 @@ function annopy_update_instance($moduleinstance, $mform = null) { function annopy_delete_instance($id) { global $DB; - if (!$annopy = $DB->get_record('annopy', array('id' => $id))) { + if (!$annopy = $DB->get_record('annopy', ['id' => $id])) { return false; } if (!$cm = get_coursemodule_from_instance('annopy', $annopy->id)) { return false; } - if (!$course = $DB->get_record('course', array('id' => $cm->course))) { + if (!$course = $DB->get_record('course', ['id' => $cm->course])) { return false; } @@ -168,16 +168,16 @@ function annopy_delete_instance($id) { \core_completion\api::update_completion_date_event($cm->id, 'annopy', $annopy->id, null); */ // Delete submission. - $DB->delete_records("annopy_submissions", array("annopy" => $annopy->id)); + $DB->delete_records("annopy_submissions", ["annopy" => $annopy->id]); // Delete annotations. - $DB->delete_records("annopy_annotations", array("annopy" => $annopy->id)); + $DB->delete_records("annopy_annotations", ["annopy" => $annopy->id]); // Delete annotation types for the module instance. - $DB->delete_records("annopy_annotationtypes", array("annopy" => $annopy->id)); + $DB->delete_records("annopy_annotationtypes", ["annopy" => $annopy->id]); // Delete module instance, else return false. - if (!$DB->delete_records("annopy", array("id" => $annopy->id))) { + if (!$DB->delete_records("annopy", ["id" => $annopy->id])) { return false; } @@ -216,11 +216,11 @@ function annopy_user_outline($course, $user, $mod, $annopy) { function annopy_print_recent_activity($course, $viewfullnames, $timestart) { /* global $CFG, $USER, $DB, $OUTPUT; - $params = array( + $params = [ $timestart, $course->id, 'annopy' - ); + ]; // Moodle branch check. if ($CFG->branch < 311) { @@ -244,7 +244,7 @@ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { $modinfo = get_fast_modinfo($course); - $show = array(); + $show = []; foreach ($newentries as $entry) { if (! array_key_exists($entry->cmid, $modinfo->get_cms())) { @@ -340,13 +340,13 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour if ($COURSE->id == $courseid) { $course = $COURSE; } else { - $course = $DB->get_record('course', array('id' => $courseid)); + $course = $DB->get_record('course', ['id' => $courseid]); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cmid); - $params = array(); + $params = []; if ($userid) { $userselect = 'AND u.id = :userid'; $params['userid'] = $userid; @@ -395,7 +395,7 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); $teacher = has_capability('mod/annopy:manageentries', $cmcontext); - $show = array(); + $show = []; foreach ($entries as $entry) { if ($entry->userid == $USER->id) { $show[] = $entry; @@ -435,7 +435,7 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour if ($grader) { require_once($CFG->libdir.'/gradelib.php'); - $userids = array(); + $userids = []; foreach ($show as $id => $entry) { $userids[] = $entry->userid; } @@ -548,8 +548,8 @@ function annopy_reset_course_form_definition(&$mform) { * @return array */ function annopy_reset_course_form_defaults($course) { - return array('reset_annopy_annotations' => 0, 'reset_annopy_submissionandfiles' => 0, - 'reset_annopy_annotationtypes' => 0, 'reset_annopy_all' => 1); + return ['reset_annopy_annotations' => 0, 'reset_annopy_submissionandfiles' => 0, + 'reset_annopy_annotationtypes' => 0, 'reset_annopy_all' => 1]; } /** @@ -565,14 +565,14 @@ function annopy_reset_userdata($data) { require_once($CFG->libdir . '/filelib.php'); $modulenameplural = get_string('modulenameplural', 'annopy'); - $status = array(); + $status = []; // Get annopys in course that should be resetted. $sql = "SELECT a.id FROM {annopy} a WHERE a.course = ?"; - $params = array($data->courseid); + $params = [$data->courseid]; $annopys = $DB->get_records_sql($sql, $params); @@ -580,11 +580,11 @@ function annopy_reset_userdata($data) { if (!empty($data->reset_annopy_annotations)) { $DB->delete_records_select('annopy_annotations', "annopy IN ($sql)", $params); - $status[] = array( + $status[] = [ 'component' => $modulenameplural, 'item' => get_string('annotationsdeleted', 'mod_annopy'), - 'error' => false - ); + 'error' => false, + ]; } // Delete submission and associated files. @@ -608,34 +608,34 @@ function annopy_reset_userdata($data) { // Delete submission. $DB->delete_records_select('annopy_submissions', "annopy IN ($sql)", $params); - $status[] = array( + $status[] = [ 'component' => $modulenameplural, 'item' => get_string('submissionandfilesdeleted', 'mod_annopy'), - 'error' => false - ); + 'error' => false, + ]; } // Delete annotation types. if (!empty($data->reset_annopy_all) || !empty($data->reset_annopy_annotationtypes)) { $DB->delete_records_select('annopy_annotationtypes', "annopy IN ($sql)", $params); - $status[] = array( + $status[] = [ 'component' => $modulenameplural, 'item' => get_string('annotationtypesdeleted', 'mod_annopy'), - 'error' => false - ); + 'error' => false, + ]; } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. - shift_course_mod_dates('annopy', array(), $data->timeshift, $data->courseid); - $status[] = array( + shift_course_mod_dates('annopy', [], $data->timeshift, $data->courseid); + $status[] = [ 'component' => $modulenameplural, 'item' => get_string('datechanged'), - 'error' => false - ); + 'error' => false, + ]; } return $status; @@ -649,7 +649,7 @@ function annopy_reset_userdata($data) { function annopy_reset_gradebook($courseid) { /* global $DB; - $params = array($courseid); + $params = [$courseid]; $sql = "SELECT ma.*, cm.idnumber as cmidnumber, ma.course as courseid FROM {annopy} ma, {course_modules} cm, {modules} m @@ -732,10 +732,10 @@ function annopy_grade_item_update($moduleinstance, $grades = null) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); - $params = array( + $params = [ 'itemname' => $annopy->name, - 'idnumber' => $annopy->cmidnumber - ); + 'idnumber' => $annopy->cmidnumber, + ]; if (! $annopy->assessed || $annopy->scale == 0) { $params['gradetype'] = GRADE_TYPE_NONE; @@ -767,7 +767,7 @@ function annopy_grade_item_delete($moduleinstance) { require_once($CFG->libdir.'/gradelib.php'); return grade_update('/mod/annopy', $moduleinstance->course, 'mod', 'annopy', - $moduleinstance->id, 0, null, array('deleted' => 1)); + $moduleinstance->id, 0, null, ['deleted' => 1]); } /** @@ -803,9 +803,7 @@ function annopy_scale_used_anywhere($scaleid) { * @return string[] */ function annopy_get_file_areas($course, $cm, $context) { - return array( - 'submission' => get_string('submission', 'mod_annopy'), - ); + return ['submission' => get_string('submission', 'mod_annopy')]; } /** @@ -844,7 +842,7 @@ function annopy_get_file_info($browser, $areas, $course, $cm, $context, $fileare * @param array $options Additional options affecting the file serving. * @return bool false if file not found, does not return if found - just sends the file. */ -function annopy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options = array()) { +function annopy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options = []) { global $DB; if ($context->contextlevel != CONTEXT_MODULE) { @@ -872,39 +870,3 @@ function annopy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownl // Finally send the file. send_stored_file($file, null, 0, $forcedownload, $options); } - -/** - * Extends the global navigation tree by adding mod_annopy nodes if there is a relevant content. - * - * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. - * - * @param navigation_node $annopynode An object representing the navigation tree node. - * @param stdClass $course Course object - * @param context_course $coursecontext Course context - */ -function annopy_extend_navigation_course($annopynode, $course, $coursecontext) { - $modinfo = get_fast_modinfo($course); // Get mod_fast_modinfo from $course. - $index = 1; // Set index. - foreach ($modinfo->get_cms() as $cmid => $cm) { // Search existing course modules for this course. - if ($index == 1 && $cm->modname == "annopy" && $cm->uservisible && $cm->available) { - $url = new moodle_url("/mod/" . $cm->modname . "/index.php", - array("id" => $course->id)); // Set url for the link in the navigation node. - $node = navigation_node::create(get_string('viewallannopys', 'annopy'), $url, - navigation_node::TYPE_CUSTOM, null , null , null); - $annopynode->add_node($node); - $index++; - } - } -} - -/** - * Extends the settings navigation with the mod_annopy settings. - * - * This function is called when the context for the page is a mod_annopy module. - * This is not called by AJAX so it is safe to rely on the $PAGE. - * - * @param settings_navigation $settingsnav - * @param navigation_node $annopynode - */ -function annopy_extend_settings_navigation($settingsnav, $annopynode = null) { -} diff --git a/mod_form.php b/mod_form.php index 81fe630..4141f41 100644 --- a/mod_form.php +++ b/mod_form.php @@ -47,7 +47,7 @@ public function definition() { $mform->addElement('header', 'general', get_string('general', 'form')); // Adding the standard "name" field. - $mform->addElement('text', 'name', get_string('modulename', 'mod_annopy'), array('size' => '64')); + $mform->addElement('text', 'name', get_string('modulename', 'mod_annopy'), ['size' => '64']); if (!empty($CFG->formatstringstriptags)) { $mform->setType('name', PARAM_TEXT); @@ -91,7 +91,7 @@ public function definition() { $name .= '' . $type->name . ''; } - $mform->addElement('advcheckbox', 'annotationtypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); + $mform->addElement('advcheckbox', 'annotationtypes[' . $id . ']', $name, ' ', ['group' => 1], [0, 1]); } } @@ -120,8 +120,8 @@ public function validation($data, $files) { $maxwidth = 80; if (!$data['annotationareawidth'] || $data['annotationareawidth'] < $minwidth || $data['annotationareawidth'] > $maxwidth) { - $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'annopy', array('minwidth' => $minwidth, - 'maxwidth' => $maxwidth)); + $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'annopy', ['minwidth' => $minwidth, + 'maxwidth' => $maxwidth]); } */ return $errors; diff --git a/submit.php b/submit.php index 2d71b15..0ed4e57 100644 --- a/submit.php +++ b/submit.php @@ -51,7 +51,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } else if (!$course) { throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); -} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { +} else if (!$coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } @@ -65,8 +65,8 @@ $data->id = $cm->id; // Get submission that should be edited. -if ($DB->record_exists('annopy_submissions', array('annopy' => $moduleinstance->id))) { - $submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); +if ($DB->record_exists('annopy_submissions', ['annopy' => $moduleinstance->id])) { + $submission = $DB->get_record('annopy_submissions', ['annopy' => $moduleinstance->id]); // Prevent editing of submissions not started by this user. if ($submission->author != $USER->id) { @@ -102,7 +102,7 @@ 'mod_annopy', 'attachment', $data->submissionid); // Create form. -$form = new mod_annopy_submit_form(null, array('editoroptions' => $editoroptions)); +$form = new mod_annopy_submit_form(null, ['editoroptions' => $editoroptions]); // Set existing data for this submission. $form->set_data($data); @@ -118,7 +118,7 @@ if ($fromform->submissionid !== 0) { // Update existing submission. // Get existing submission. $submission = $DB->get_record('annopy_submissions', - array('annopy' => $moduleinstance->id, 'id' => $fromform->submissionid)); + ['annopy' => $moduleinstance->id, 'id' => $fromform->submissionid]); // Set new version and time modified. $submission->currentversion += 1; @@ -132,10 +132,10 @@ 'mod_annopy', 'submission', $submission->id); // Set submission title, content and format. - $submission->title = format_text($fromform->title, 1, array('para' => false)); + $submission->title = format_text($fromform->title, 1, ['para' => false]); $submission->content = format_text($submissiontext, - $fromform->submission_editor['format'], array('para' => false)); + $fromform->submission_editor['format'], ['para' => false]); $submission->format = (int) $fromform->submission_editor['format']; @@ -144,22 +144,22 @@ if ($updated) { // Trigger submission updated event. - $event = \mod_annopy\event\submission_updated::create(array( + $event = \mod_annopy\event\submission_updated::create([ 'objectid' => $submission->id, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); - redirect(new moodle_url('/mod/annopy/view.php', array('id' => $id)), + redirect(new moodle_url('/mod/annopy/view.php', ['id' => $id]), get_string('submissionmodified', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); } else { - redirect(new moodle_url('/mod/annopy/view.php', array('id' => $id)), + redirect(new moodle_url('/mod/annopy/view.php', ['id' => $id]), get_string('submissionnotmodified', 'mod_annopy'), null, notification::NOTIFY_ERROR); } } else if ($fromform->submissionid === 0) { // New submission. - if (!$DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id))) { // No submission made yet. + if (!$DB->get_record('annopy_submissions', ['annopy' => $moduleinstance->id])) { // No submission made yet. // Create new submission object. $submission = new stdClass(); @@ -172,7 +172,7 @@ $submission->timemodified = null; // Set submission title, content and format. - $submission->title = format_text($fromform->title, 1, array('para' => false)); + $submission->title = format_text($fromform->title, 1, ['para' => false]); $submission->content = ''; $submission->format = 1; @@ -188,7 +188,7 @@ // Set submission text and format. $submission->content = format_text($submissiontext, - $fromform->submission_editor['format'], array('para' => false)); + $fromform->submission_editor['format'], ['para' => false]); $submission->format = (int) $fromform->submission_editor['format']; // Update submission with formatted content. @@ -196,25 +196,25 @@ if ($updated) { // Trigger submission created event. - $event = \mod_annopy\event\submission_created::create(array( + $event = \mod_annopy\event\submission_created::create([ 'objectid' => $submission->id, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); redirect(new moodle_url('/mod/annopy/view.php', - array('id' => $id)), + ['id' => $id]), get_string('submissioncreated', 'mod_annopy'), null, notification::NOTIFY_SUCCESS); } else { redirect(new moodle_url('/mod/annopy/view.php', - array('id' => $id)), + ['id' => $id]), get_string('submissionnotcreated', 'mod_annopy'), null, notification::NOTIFY_ERROR); } } else { redirect(new moodle_url('/mod/annopy/view.php', - array('id' => $id)), + ['id' => $id]), get_string('submissionfaileddoubled', 'mod_annopy'), null, notification::NOTIFY_ERROR); } } @@ -223,11 +223,11 @@ } // Get the name for this activity. -$modulename = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$modulename = format_string($moduleinstance->name, true, [ + 'context' => $context, +]); -$PAGE->set_url('/mod/annopy/submit.php', array('id' => $id)); +$PAGE->set_url('/mod/annopy/submit.php', ['id' => $id]); $PAGE->navbar->add($title); $PAGE->set_title($modulename . ' - ' . $title); $PAGE->set_heading($course->fullname); diff --git a/submit_form.php b/submit_form.php index 10d979f..21df6bc 100644 --- a/submit_form.php +++ b/submit_form.php @@ -57,7 +57,7 @@ public function definition() { $mform->addElement('hidden', 'submissionid', null); $mform->setType('submissionid', PARAM_INT); - $mform->addElement('text', 'title', get_string('title', 'mod_annopy'), array('size' => '64')); + $mform->addElement('text', 'title', get_string('title', 'mod_annopy'), ['size' => '64']); $mform->setType('title', PARAM_TEXT); @@ -80,6 +80,6 @@ public function definition() { * @return array Array with errors */ public function validation($data, $files) { - return array(); + return []; } } diff --git a/view.php b/view.php index 6962c34..03d1d82 100644 --- a/view.php +++ b/view.php @@ -49,7 +49,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } else if (!$course) { throw new moodle_exception(get_string('incorrectcourseid', 'annopy')); -} else if (!$coursesections = $DB->get_record("course_sections", array("id" => $cm->section))) { +} else if (!$coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'annopy')); } @@ -58,10 +58,10 @@ $context = context_module::instance($cm->id); // Trigger course_module_viewed event. -$event = \mod_annopy\event\course_module_viewed::create(array( +$event = \mod_annopy\event\course_module_viewed::create([ 'objectid' => $moduleinstance->id, - 'context' => $context -)); + 'context' => $context, +]); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('course', $course); @@ -74,18 +74,18 @@ } // Get the name for this activity. -$modulename = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$modulename = format_string($moduleinstance->name, true, [ + 'context' => $context, +]); // Set $PAGE and completion. -$PAGE->set_url('/mod/annopy/view.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/annopy/view.php', ['id' => $cm->id]); $PAGE->navbar->add(get_string("overview", "annopy")); $PAGE->requires->js_call_amd('mod_annopy/annotations', 'init', - array( 'cmid' => $cm->id, 'canaddannotation' => has_capability('mod/annopy:addannotation', $context), 'myuserid' => $USER->id, - 'focusannotation' => $focusannotation, 'userid' => $userid)); + [ 'cmid' => $cm->id, 'canaddannotation' => has_capability('mod/annopy:addannotation', $context), 'myuserid' => $USER->id, + 'focusannotation' => $focusannotation, 'userid' => $userid]); $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -113,7 +113,7 @@ echo groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/annopy/view.php?id=$id"); // Get submission for the module. -$submission = $DB->get_record('annopy_submissions', array('annopy' => $moduleinstance->id)); +$submission = $DB->get_record('annopy_submissions', ['annopy' => $moduleinstance->id]); // Render and output page. $page = new annopy_view($cm, $course, $context, $moduleinstance, $submission, $userid); From 674f729b7eeeca136e44d31c27bc8fb2412b3c01 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 7 Dec 2023 16:51:29 +0100 Subject: [PATCH 8/9] feat(chore): Deleting unnecessary elements like tasks and grading. --- CHANGES.md | 6 ++ classes/task/task.php | 54 --------------- db/events.php | 28 -------- db/messages.php | 36 ---------- db/tasks.php | 38 ----------- db/uninstall.php | 32 --------- lib.php | 149 ------------------------------------------ tests/annopy_test.php | 45 ------------- version.php | 4 +- 9 files changed, 8 insertions(+), 384 deletions(-) delete mode 100644 classes/task/task.php delete mode 100644 db/events.php delete mode 100644 db/messages.php delete mode 100644 db/tasks.php delete mode 100644 db/uninstall.php delete mode 100644 tests/annopy_test.php diff --git a/CHANGES.md b/CHANGES.md index f3bd30e..acccffb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ ## Changelog ## +- [0.6]: + - Fix for a bug preventing the change of the priority of annotation types. + - Ensured compatibility with Moodle 4.3 and PHP 8.2. + - Changed code to comply with new moodle coding standards. + - Deleting unnecessary elements like tasks and grading. + - [0.5]: - Added backup functionality. - Added privacy functionality. diff --git a/classes/task/task.php b/classes/task/task.php deleted file mode 100644 index a12d3a3..0000000 --- a/classes/task/task.php +++ /dev/null @@ -1,54 +0,0 @@ -. - -/** - * A cron_task class for doing stuff in AnnoPy to be used by Tasks API. - * - * @package mod_annopy - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - namespace mod_annopy\task; - -/** - * A cron_task class to be used by Tasks API. - * - * @package mod_annopy - * @copyright 2023 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class task extends \core\task\scheduled_task { - /** - * Return the task's name as shown in admin screens. - * - * @return string - */ - public function get_name() { - return get_string('task', 'mod_annopy'); - } - - /** - * Execute the task. - */ - public function execute() { - global $DB; - - mtrace('Starting scheduled task ' . get_string('task', 'mod_annopy')); - - mtrace('Task finished'); - } -} diff --git a/db/events.php b/db/events.php deleted file mode 100644 index dbd4558..0000000 --- a/db/events.php +++ /dev/null @@ -1,28 +0,0 @@ -. - -/** - * Plugin event observers are registered here. All event subscriptions that the plugin needs to listen for are defined here. - * @package mod_annopy - * @category event - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -// List of observers. -$observers = []; diff --git a/db/messages.php b/db/messages.php deleted file mode 100644 index c675ea2..0000000 --- a/db/messages.php +++ /dev/null @@ -1,36 +0,0 @@ -. - -/** - * Plugin message providers are defined here. - * - * @package mod_annopy - * @category message - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -$messageproviders = [ -/* 'sendmessages' => [ - 'capability' => 'mod/annopy:sendmessages', - 'defaults' => [ - 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, - 'email' => MESSAGE_PERMITTED, - ], - ], */ -]; diff --git a/db/tasks.php b/db/tasks.php deleted file mode 100644 index 84021a1..0000000 --- a/db/tasks.php +++ /dev/null @@ -1,38 +0,0 @@ -. - -/** - * Sheduled tasks for the plugin. - * - * @package mod_annopy - * @category task - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -$tasks = [ - /* [ - 'classname' => 'mod_annopy\task\task', - 'blocking' => 0, - 'minute' => '*', - 'hour' => '*', - 'day' => '*', - 'month' => '*', - 'dayofweek' => '*', - ], */ -]; diff --git a/db/uninstall.php b/db/uninstall.php deleted file mode 100644 index 947dc63..0000000 --- a/db/uninstall.php +++ /dev/null @@ -1,32 +0,0 @@ -. - -/** - * Code that is executed before the tables and data are dropped during the plugin uninstallation. - * - * @package mod_annopy - * @category upgrade - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * Custom uninstallation procedure. - */ -function xmldb_annopy_uninstall() { - - return true; -} diff --git a/lib.php b/lib.php index fbb2ae4..c441c4b 100644 --- a/lib.php +++ b/lib.php @@ -27,8 +27,6 @@ * * @uses FEATURE_MOD_INTRO * @uses FEATURE_SHOW_DESCRIPTION - * @uses FEATURE_GRADE_HAS_GRADE - * @uses FEATURE_RATE * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE__BACKUP_MOODLE2 * @param string $feature Constant for requested feature. @@ -641,153 +639,6 @@ function annopy_reset_userdata($data) { return $status; } -/** - * Removes all grades in the annopy gradebook - * - * @param int $courseid - */ -function annopy_reset_gradebook($courseid) { - /* global $DB; - - $params = [$courseid]; - - $sql = "SELECT ma.*, cm.idnumber as cmidnumber, ma.course as courseid - FROM {annopy} ma, {course_modules} cm, {modules} m - WHERE m.name='annopy' AND m.id=cm.module AND cm.instance=ma.id AND ma.course=?"; - - if ($annopys = $DB->get_records_sql($sql, $params)) { - foreach ($annopys as $annopy) { - annopy_grade_item_update($annopy, 'reset'); - } - } */ -} - -/** - * Get annopy grades for a user (custom function used by annopy_update_grades). - * - * @param object $annopy If null, all annopys - * @param int $userid If false all users - * @return object $grades - */ -function annopy_get_user_grades($annopy, $userid = 0) { - global $CFG; - - require_once($CFG->dirroot . '/rating/lib.php'); - - $ratingoptions = new stdClass(); - $ratingoptions->component = 'mod_annopy'; - $ratingoptions->ratingarea = 'submission'; - $ratingoptions->modulename = 'annopy'; - $ratingoptions->moduleid = $annopy->id; - $ratingoptions->userid = $userid; - $ratingoptions->aggregationmethod = $annopy->assessed; - $ratingoptions->scaleid = $annopy->scale; - $ratingoptions->itemtable = 'annopy_submissions'; - $ratingoptions->itemtableusercolumn = 'userid'; - - $rm = new rating_manager(); - - return $rm->get_user_grades($ratingoptions); -} - -/** - * Update grades in the annopy gradebook. - * - * Needed by {@see grade_update_mod_grades()}. - * - * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. - * @param int $userid Update grade of specific user only, 0 means all participants. - */ -function annopy_update_grades($moduleinstance, $userid = 0) { - global $CFG; - require_once($CFG->libdir . '/gradelib.php'); - - $cm = get_coursemodule_from_instance('annopy', $moduleinstance->id); - $moduleinstance->cmidnumber = $cm->idnumber; - - if (!$moduleinstance->assessed) { - annopy_grade_item_update($moduleinstance); - } else if ($grades = annopy_get_user_grades($moduleinstance, $userid)) { - annopy_grade_item_update($moduleinstance, $grades); - } else if ($userid && $nullifnone) { - $grade = new stdClass(); - $grade->userid = $userid; - $grade->rawgrade = null; - annopy_grade_item_update($moduleinstance, $grade); - } else { - annopy_grade_item_update($moduleinstance); - } -} - -/** - * Creates or updates grade item for given annopy. - * - * Needed by {@see grade_update_mod_grades()}. - * - * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. - * @param array $grades optional array/object of grade(s); 'reset' means reset grades in gradebook - * @return void. - */ -function annopy_grade_item_update($moduleinstance, $grades = null) { - global $CFG; - require_once($CFG->libdir.'/gradelib.php'); - - $params = [ - 'itemname' => $annopy->name, - 'idnumber' => $annopy->cmidnumber, - ]; - - if (! $annopy->assessed || $annopy->scale == 0) { - $params['gradetype'] = GRADE_TYPE_NONE; - } else if ($annopy->scale > 0) { - $params['gradetype'] = GRADE_TYPE_VALUE; - $params['grademax'] = $annopy->scale; - $params['grademin'] = 0; - } else if ($annopy->scale < 0) { - $params['gradetype'] = GRADE_TYPE_SCALE; - $params['scaleid'] = - $annopy->scale; - } - - if ($grades === 'reset') { - $params['reset'] = true; - $grades = null; - } - - return grade_update('mod/annopy', $annopy->course, 'mod', 'annopy', $annopy->id, 0, $grades, $params); -} - -/** - * Delete grade item for given module instance. - * - * @param stdClass $moduleinstance Instance object. - * @return grade_item. - */ -function annopy_grade_item_delete($moduleinstance) { - global $CFG; - require_once($CFG->libdir.'/gradelib.php'); - - return grade_update('/mod/annopy', $moduleinstance->course, 'mod', 'annopy', - $moduleinstance->id, 0, null, ['deleted' => 1]); -} - -/** - * Checks if scale is being used by any instance of mod_annopy. - * - * This is used to find out if scale used anywhere. - * - * @param int $scaleid ID of the scale. - * @return bool True if the scale is used by any mod_annopy instance. - */ -function annopy_scale_used_anywhere($scaleid) { - global $DB; - - if (empty($scaleid)) { - return false; - } - - return $DB->record_exists_select('annopy', "scale = ? and assessed > 0", [$scaleid * -1]); -} - /** * Returns the lists of all browsable file areas within the given module context. * diff --git a/tests/annopy_test.php b/tests/annopy_test.php deleted file mode 100644 index 810f08a..0000000 --- a/tests/annopy_test.php +++ /dev/null @@ -1,45 +0,0 @@ -. - -/** - * File containing tests for annopy. - * - * @package mod_annopy - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -// For installation and usage of PHPUnit within Moodle please read: -// https://docs.moodle.org/dev/PHPUnit -// -// Documentation for writing PHPUnit tests for Moodle can be found here: -// https://docs.moodle.org/dev/PHPUnit_integration -// https://docs.moodle.org/dev/Writing_PHPUnit_tests -// -// The official PHPUnit homepage is at https://phpunit.de. - -/** - * The annopy test class. - * - * @package mod_annopy - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mod_annopy_view_testcase extends advanced_testcase { - - // Write the tests here as public funcions. - -} diff --git a/version.php b/version.php index 3c2b3fa..debb3de 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.5.0'; -$plugin->version = 2023091400; +$plugin->release = '0.6.0'; +$plugin->version = 2023120700; $plugin->requires = 2020061507; $plugin->maturity = MATURITY_ALPHA; From 4489313fbdccde78fa7228d4509b8630fa3f05c3 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 11 Dec 2023 14:07:58 +0100 Subject: [PATCH 9/9] fix(index): Fixed some minor bugs, removed some more unnecessary code and added recent activity overview and plugin description. --- CHANGES.md | 3 + README.md | 25 +++++- classes/output/annopy_view.php | 11 ++- classes/search/submission.php | 68 ++++++++-------- db/install.php | 32 -------- index.php | 2 +- lang/de/annopy.php | 24 +++++- lang/en/annopy.php | 24 +++++- lib.php | 141 +++++++++++++-------------------- mod_form.php | 47 +++++------ version.php | 6 +- 11 files changed, 187 insertions(+), 196 deletions(-) delete mode 100644 db/install.php diff --git a/CHANGES.md b/CHANGES.md index acccffb..d60fa05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,8 @@ ## Changelog ## +- [0.7]: + - Fixed some minor bugs, removed some more unnecessary code and added recent activity overview and plugin description. + - [0.6]: - Fix for a bug preventing the change of the priority of annotation types. - Ensured compatibility with Moodle 4.3 and PHP 8.2. diff --git a/README.md b/README.md index d0c7105..2655b73 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # License # -2023 coactum GmbH - This program 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 @@ -14,13 +12,32 @@ 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 this program. If not, see . +@copyright 2023 coactum GmbH + # AnnoPy # ## Description ## -TODO Describe the plugin shortly here. +In the AnnoPy activity, teachers can upload a wide variety of multimedia texts which participants can then annotate. + +As a collaborative tool, AnnoPy can be used in many different ways in school or university learning contexts, for example to improve participants literary skills or to evaluate their understanding of a text. + +For example, AnnoPy can be used in language didactics to promote comparative reading, to practise identifying linguistic patterns in texts or to open up a new perspective on explanatory texts. AnnoPy can also be used to analyze texts on a content level, for example with regard to semantic, grammatical, lexical or text-literary issues. In subjects such as mathematics or computer science, on the other hand, teachers can use AnnoPy to have their own lecture notes worked through and then see at a glance where there are still difficulties in understanding. + +Teachers can first upload any multimedia text for annotation; depending on the didactic context, this can also contain images, formulas or programming code, for example. +All participants can then annotate this text by marking the desired text passages and then selecting a type for each annotation and leaving a short comment if necessary. +Just like reusable templates, the available annotation types can be flexibly adapted by the teacher depending on the context. +Finally, teachers can view and analyze all of the participants annotations in detail in a clearly visualized evaluation. + +Core features of the plugin: + +* Upload of various types of multimedia texts by teachers +* Separate annotation of these texts including comments by each individual participant +* Cumulative display of all annotations on the overview page, sorted by participant +* Annotation types and templates can be individually customized by teachers +* A clear and detailed evaluation of all annotations -TODO Provide more detailed description here. +Further information on the concept behind AnnoPy and its possible use in teaching and learning can be found in German on the current project website (https://annopy.de/). ## Quick installation instructions ## diff --git a/classes/output/annopy_view.php b/classes/output/annopy_view.php index 87b458a..5c222d8 100644 --- a/classes/output/annopy_view.php +++ b/classes/output/annopy_view.php @@ -102,7 +102,8 @@ public function export_for_template(renderer_base $output) { get_string_manager(), $annotationtypes, $this->userid, true); // If submission can be edited. - if (has_capability('mod/annopy:editsubmission', $this->context) && !$data->submission->totalannotationscount) { + if (has_capability('mod/annopy:editsubmission', $this->context) && !$data->submission->totalannotationscount + && (isset($data->submission) && $data->submission->author->id == $USER->id)) { $data->submission->canbeedited = true; } else { $data->submission->canbeedited = false; @@ -110,7 +111,13 @@ public function export_for_template(renderer_base $output) { } $data->canaddsubmission = has_capability('mod/annopy:addsubmission', $this->context); - $data->caneditsubmission = has_capability('mod/annopy:editsubmission', $this->context); + + if (has_capability('mod/annopy:editsubmission', $this->context)) { + $data->caneditsubmission = true; + } else { + $data->caneditsubmission = false; + } + $data->canviewparticipants = has_capability('mod/annopy:viewparticipants', $this->context); $data->sesskey = sesskey(); diff --git a/classes/search/submission.php b/classes/search/submission.php index 7243e46..e85394d 100644 --- a/classes/search/submission.php +++ b/classes/search/submission.php @@ -38,35 +38,29 @@ class submission extends \core_search\base_mod { /** - * - * @var array Internal quick static cache. - */ - protected $entriesdata = []; - - /** - * Returns recordset containing required data for indexing AnnoPy entries. + * Returns recordset containing required data for indexing AnnoPy submissions. * * @param int $modifiedfrom timestamp * @param \context|null $context Optional context to restrict scope of returned results * @return moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, \context $context = null) { - /* global $DB; + global $DB; list ($contextjoin, $contextparams) = $this->get_context_restriction_sql($context, 'annopy', 'm', SQL_PARAMS_NAMED); if ($contextjoin === null) { return null; } - $sql = "SELECT me.*, m.course - FROM {annopy_entries} me - JOIN {annopy} m ON m.id = me.annopy + $sql = "SELECT s.*, a.course + FROM {annopy_submissions} s + JOIN {annopy} a ON a.id = s.annopy $contextjoin - WHERE me.timemodified >= :timemodified - ORDER BY me.timemodified ASC"; + WHERE s.timemodified >= :timemodified + ORDER BY s.timemodified ASC"; return $DB->get_recordset_sql($sql, array_merge($contextparams, [ - 'timemodified' => $modifiedfrom - ])); */ + 'timemodified' => $modifiedfrom, + ])); } /** @@ -77,7 +71,7 @@ public function get_document_recordset($modifiedfrom = 0, \context $context = nu * @return \core_search\document */ public function get_document($submission, $options = []) { - /* try { + try { $cm = $this->get_cm('annopy', $submission->annopy, $submission->course); $context = \context_module::instance($cm->id); } catch (\dml_missing_record_exception $ex) { @@ -95,20 +89,20 @@ public function get_document($submission, $options = []) { $doc = \core_search\document_factory::instance($submission->id, $this->componentname, $this->areaname); // I am using the submission date (timecreated) for the title. $doc->set('title', content_to_text((userdate($submission->timecreated)), $submission->format)); - $doc->set('content', content_to_text('Entry: ' . $submission->text, $submission->format)); + $doc->set('content', content_to_text('Submission: ' . $submission->text, $submission->format)); $doc->set('contextid', $context->id); $doc->set('courseid', $submission->course); - $doc->set('userid', $submission->userid); + $doc->set('userid', $submission->author); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $submission->timemodified); - $doc->set('description1', content_to_text('Feedback: ' . $submission->feedback, $submission->formatfeedback)); + $doc->set('description1', ''); // Check if this document should be considered new. if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $submission->timemodified)) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } - return $doc; */ + return $doc; } /** @@ -120,10 +114,10 @@ public function get_document($submission, $options = []) { * @return bool */ public function check_access($id) { - /* global $USER; + global $USER; try { - $submission = $this->get_entry($id); + $submission = $this->get_submission($id); $cminfo = $this->get_cm('annopy', $submission->annopy, $submission->course); } catch (\dml_missing_record_exception $ex) { return \core_search\manager::ACCESS_DELETED; @@ -135,11 +129,11 @@ public function check_access($id) { return \core_search\manager::ACCESS_DENIED; } - if ($submission->userid != $USER->id && ! has_capability('mod/annopy:manageentries', $cminfo->context)) { + if ($submission->author != $USER->id && ! has_capability('mod/annopy:viewparticipants', $cminfo->context)) { return \core_search\manager::ACCESS_DENIED; } - return \core_search\manager::ACCESS_GRANTED; */ + return \core_search\manager::ACCESS_GRANTED; } /** @@ -149,16 +143,16 @@ public function check_access($id) { * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { - /* global $USER; + global $USER; $contextmodule = \context::instance_by_id($doc->get('contextid')); - $entryuserid = $doc->get('userid'); + $submissionuserid = $doc->get('userid'); $url = '/mod/annopy/view.php'; return new \moodle_url($url, [ - 'id' => $contextmodule->instanceid - ]); */ + 'id' => $contextmodule->instanceid, + ]); } /** @@ -168,10 +162,10 @@ public function get_doc_url(\core_search\document $doc) { * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { - /* $contextmodule = \context::instance_by_id($doc->get('contextid')); + $contextmodule = \context::instance_by_id($doc->get('contextid')); return new \moodle_url('/mod/annopy/view.php', [ - 'id' => $contextmodule->instanceid - ]); */ + 'id' => $contextmodule->instanceid, + ]); } /** @@ -180,13 +174,13 @@ public function get_context_url(\core_search\document $doc) { * Store minimal information as this might grow. * * @throws \dml_exception - * @param int $entryid + * @param int $submissionid * @return stdClass */ - protected function get_entry($entryid) { - /* global $DB; - return $DB->get_record_sql("SELECT me.*, m.course FROM {annopy_entries} me - JOIN {annopy} m ON m.id = me.annopy - WHERE me.id = ?", ['id' => $entryid], MUST_EXIST); */ + protected function get_submission($submissionid) { + global $DB; + return $DB->get_record_sql("SELECT s.*, a.course FROM {annopy_submissions} s + JOIN {annopy} a ON a.id = s.annopy + WHERE s.id = ?", ['id' => $submissionid], MUST_EXIST); } } diff --git a/db/install.php b/db/install.php deleted file mode 100644 index 793a936..0000000 --- a/db/install.php +++ /dev/null @@ -1,32 +0,0 @@ -. - -/** - * Code to be executed after the plugin's database scheme has been installed is defined here. - * - * @package mod_annopy - * @category upgrade - * @copyright 2023 coactum GmbH - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * Custom code to be run on installing the plugin. - */ -function xmldb_annopy_install() { - - return true; -} diff --git a/index.php b/index.php index dee9b11..353af8f 100644 --- a/index.php +++ b/index.php @@ -63,7 +63,7 @@ // Build table with all instances. $modinfo = get_fast_modinfo($course); -$moduleinstances = $modinfo->get_instances_of('annopy'); +$moduleinstances = get_all_instances_in_course('annopy', $course); // Sections. $usesections = course_format_uses_sections($course->format); diff --git a/lang/de/annopy.php b/lang/de/annopy.php index 6835906..8c6bbac 100644 --- a/lang/de/annopy.php +++ b/lang/de/annopy.php @@ -30,9 +30,29 @@ // Strings for mod_form.php. $string['modulename'] = 'AnnoPy'; -$string['modulename_help'] = 'Die Aktivität AnnoPy erlaubt ... '; +$string['modulename_help'] = 'In der Aktivität AnnoPy können Lehrende unterschiedlichste multimediale Texte hochladen welche die Teilnehmerinnen und Teilnehmer dann annotieren können. + +Als kollaboratives Tool kann AnnoPy in zahlreichen schulischen oder universitären Lernkontexten auf verschiedenste Arten eingesetzt werden, um zum Beispiel die literalen Kompetenzen der Teilnehmenden zu fördern oder deren Verständnis eines Textes zu evaluieren. + +So kann AnnoPy zum Beispiel in der Sprachdidaktik eingesetzt werden, um das textvergleichende Lesen zu fördern, das Identifizieren sprachlicher Muster in Texten zu üben oder eine neue Perspektive auf erklärende Texte zu eröffnen. AnnoPy kann ebenfalls genutzt werden, um Texte auf inhaltlicher Ebene analysieren zu lassen, etwa im Hinblick auf semantische, grammatische, lexikalische oder textliterarische Fragestellungen. In Fächern wie Mathematik oder Informatik können Lehrkräfte AnnoPy hingegen nutzen, um ihre eigenen Vorlesungsskripte durcharbeiten zu lassen und dann auf einen Blick zu sehen, an welchen Stellen noch Verständnisschwierigkeiten auftreten. + +Lehrende können zunächst einen beliebigen multimedialen Text zur Annotation hochladen, dieser kann je nach didaktischem Einsatzkontext auch z.B. Bilder, Formeln oder Programmiercode enthalten. +Alle Teilnehmenden können diesen Text dann annotieren, indem sie die gewünschten Textstellen markieren und dann für jede Annotation einen Typ auswählen sowie ggf. einen kurzen Kommentar hinterlassen. +Die verfügbaren Annotationstypen können dabei genau wie wiederverwendbare Vorlagen durch die Lehrenden je nach Kontext flexibel angepasst werden. +In einer übersichtlich visualisierten Auswertung können Lehrende schließlich sämtliche Annotationen der Teilnehmenden ansehen und detailliert analysieren. + +Kernfeatures des Plugins: + +* Hochladen verschiedenster Arten multimedialer Texte durch die Lehrenden +* Separate Annotation dieser Texte inkl. Kommentar durch jeden einzelnen Teilnehmenden +* Kumulierte sowie nach Teilnehmenden sortierte Anzeige aller Annotationen auf der Übersichtsseite +* Durch Lehrende individuell anpassbare Annotationstypen sowie -Vorlagen +* Eine übersichtliche und detaillierte Auswertung aller Annotation + +Weitere Informationen zum Konzept hinter AnnoPy und dessen möglichem Einsatz in Unterricht und Lehre finden sich auf Deutsch auf der aktuellen Projektwebseite (https://annopy.de/).'; $string['modulename_link'] = 'mod/annopy/view'; $string['pluginadministration'] = 'Administration der AnnoPy-Instanz'; +$string['noannotationtypetemplates'] = 'Bisher sind keine Annotationstypvorlagen vorhanden. Nach dem Erstellen des AnnoPys müssen deshalb noch manuell Annotationstypen angelegt werden.'; // Strings for index.php. $string['modulenameplural'] = 'AnnoPys'; @@ -138,6 +158,7 @@ $string['annotationtypesdeleted'] = 'Alle Annotationstypen wurden gelöscht'; // Strings for the recent activity. +$string['newannopyannotations'] = 'Neue AnnoPy Annotationen'; // Strings for the capabilities. $string['annopy:addinstance'] = 'Neue AnnoPy-Instanz hinzufügen'; @@ -162,7 +183,6 @@ $string['annopy:managedefaultannotationtypetemplates'] = 'Standard Annotationstyp-Vorlagen verwalten'; // Strings for the tasks. -$string['task'] = 'Aufgabe'; // Strings for the messages. diff --git a/lang/en/annopy.php b/lang/en/annopy.php index 8bf44e4..71c9200 100644 --- a/lang/en/annopy.php +++ b/lang/en/annopy.php @@ -30,9 +30,29 @@ // Strings for mod_form.php. $string['modulename'] = 'AnnoPy'; -$string['modulename_help'] = 'The AnnoPy activity allows ... '; +$string['modulename_help'] = 'In the AnnoPy activity, teachers can upload a wide variety of multimedia texts which participants can then annotate. + +As a collaborative tool, AnnoPy can be used in many different ways in school or university learning contexts, for example to improve participants literary skills or to evaluate their understanding of a text. + +For example, AnnoPy can be used in language didactics to promote comparative reading, to practise identifying linguistic patterns in texts or to open up a new perspective on explanatory texts. AnnoPy can also be used to analyze texts on a content level, for example with regard to semantic, grammatical, lexical or text-literary issues. In subjects such as mathematics or computer science, on the other hand, teachers can use AnnoPy to have their own lecture notes worked through and then see at a glance where there are still difficulties in understanding. + +Teachers can first upload any multimedia text for annotation; depending on the didactic context, this can also contain images, formulas or programming code, for example. +All participants can then annotate this text by marking the desired text passages and then selecting a type for each annotation and leaving a short comment if necessary. +Just like reusable templates, the available annotation types can be flexibly adapted by the teacher depending on the context. +Finally, teachers can view and analyze all of the participants annotations in detail in a clearly visualized evaluation. + +Core features of the plugin: + +* Upload of various types of multimedia texts by teachers +* Separate annotation of these texts including comments by each individual participant +* Cumulative display of all annotations on the overview page, sorted by participant +* Annotation types and templates can be individually customized by teachers +* A clear and detailed evaluation of all annotations + +Further information on the concept behind AnnoPy and its possible use in teaching and learning can be found in German on the current project website (https://annopy.de/).'; $string['modulename_link'] = 'mod/annopy/view'; $string['pluginadministration'] = 'Administration of AnnoPy'; +$string['noannotationtypetemplates'] = 'No annotation type templates are available yet. After creating the AnnoPy, annotation types must therefore be created manually.'; // Strings for index.php. $string['modulenameplural'] = 'AnnoPys'; @@ -138,6 +158,7 @@ $string['annotationtypesdeleted'] = 'All annotation types were deleted'; // Strings for the recent activity. +$string['newannopyannotations'] = 'New AnnoPy annotations'; // Strings for the capabilities. $string['annopy:addinstance'] = 'Add new AnnoPy'; @@ -162,7 +183,6 @@ $string['annopy:managedefaultannotationtypetemplates'] = 'Manage default annotation type templates'; // Strings for the tasks. -$string['task'] = 'Task'; // Strings for the messages. diff --git a/lib.php b/lib.php index c441c4b..73dd5f2 100644 --- a/lib.php +++ b/lib.php @@ -73,15 +73,6 @@ function annopy_add_instance($moduleinstance, $mform = null) { $moduleinstance->id = $DB->insert_record('annopy', $moduleinstance); - /* // Add calendar dates. - helper::annopy_update_calendar($moduleinstance, $moduleinstance->coursemodule); - - // Add expected completion date. - if (! empty($moduleinstance->completionexpected)) { - \core_completion\api::update_completion_date_event($moduleinstance->coursemodule, - 'annopy', $moduleinstance->id, $moduleinstance->completionexpected); - }*/ - if (isset($moduleinstance->annotationtypes) && !empty($moduleinstance->annotationtypes)) { // Add annotation types for the module instance. $priority = 1; @@ -119,17 +110,6 @@ function annopy_update_instance($moduleinstance, $mform = null) { $DB->update_record('annopy', $moduleinstance); - /* // Update calendar. - helper::annopy_update_calendar($moduleinstance, $moduleinstance->coursemodule); - - // Update completion date. - $completionexpected = (! empty($moduleinstance->completionexpected)) ? $moduleinstance->completionexpected : null; - \core_completion\api::update_completion_date_event($moduleinstance->coursemodule, - 'annopy', $moduleinstance->id, $completionexpected); - - // Update grade. - annopy_grade_item_update($moduleinstance); */ - return true; } @@ -162,9 +142,6 @@ function annopy_delete_instance($id) { $fs = get_file_storage(); $fs->delete_area_files($context->id); - /* // Update completion for calendar events. - \core_completion\api::update_completion_date_event($cm->id, 'annopy', $annopy->id, null); */ - // Delete submission. $DB->delete_records("annopy_submissions", ["annopy" => $annopy->id]); @@ -212,12 +189,12 @@ function annopy_user_outline($course, $user, $mod, $annopy) { * @return boolean */ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { - /* global $CFG, $USER, $DB, $OUTPUT; + global $CFG, $USER, $DB, $OUTPUT; $params = [ $timestart, $course->id, - 'annopy' + 'annopy', ]; // Moodle branch check. @@ -228,40 +205,41 @@ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; } - $sql = "SELECT e.id, e.timecreated, cm.id AS cmid, $namefields - FROM {annopy_entries} e - JOIN {annopy} d ON d.id = e.annopy - JOIN {course_modules} cm ON cm.instance = d.id - JOIN {modules} md ON md.id = cm.module - JOIN {user} u ON u.id = e.userid - WHERE e.timecreated > ? AND d.course = ? AND md.name = ? + $sql = "SELECT aa.id, aa.timecreated, cm.id AS cmid, $namefields + FROM {annopy_annotations} aa + JOIN {annopy_submissions} s ON s.id = aa.submission + JOIN {annopy} a ON a.id = s.annopy + JOIN {course_modules} cm ON cm.instance = a.id + JOIN {modules} m ON m.id = cm.module + JOIN {user} u ON u.id = aa.userid + WHERE aa.timecreated > ? AND a.course = ? AND m.name = ? ORDER BY timecreated DESC "; - $newentries = $DB->get_records_sql($sql, $params); + $newannotations = $DB->get_records_sql($sql, $params); $modinfo = get_fast_modinfo($course); $show = []; - foreach ($newentries as $entry) { - if (! array_key_exists($entry->cmid, $modinfo->get_cms())) { + foreach ($newannotations as $annotation) { + if (! array_key_exists($annotation->cmid, $modinfo->get_cms())) { continue; } - $cm = $modinfo->get_cm($entry->cmid); + $cm = $modinfo->get_cm($annotation->cmid); if (! $cm->uservisible) { continue; } - if ($entry->userid == $USER->id) { - $show[] = $entry; + if ($annotation->userid == $USER->id) { + $show[] = $annotation; continue; } - $context = context_module::instance($entry->cmid); + $context = context_module::instance($annotation->cmid); - $teacher = has_capability('mod/annopy:manageentries', $context); + $teacher = has_capability('mod/annopy:viewparticipants', $context); - // Only teachers can see other students entries. + // Only teachers can see other students annotations. if (!$teacher) { continue; } @@ -278,7 +256,7 @@ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { if (! $modinfo->get_groups($cm->groupingid)) { continue; } - $usersgroups = groups_get_all_groups($course->id, $entry->userid, $cm->groupingid); + $usersgroups = groups_get_all_groups($course->id, $annotation->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); @@ -287,25 +265,24 @@ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { } } } - $show[] = $entry; + $show[] = $annotation; } if (empty($show)) { return false; } - echo $OUTPUT->heading(get_string('newannopyentries', 'annopy') . ':', 6); + echo $OUTPUT->heading(get_string('newannopyannotations', 'annopy') . ':', 6); - foreach ($show as $entry) { - $cm = $modinfo->get_cm($entry->cmid); - $context = context_module::instance($entry->cmid); + foreach ($show as $annotation) { + $cm = $modinfo->get_cm($annotation->cmid); + $context = context_module::instance($annotation->cmid); $link = $CFG->wwwroot . '/mod/annopy/view.php?id=' . $cm->id; - print_recent_activity_note($entry->timecreated, $entry, $cm->name, $link, false, $viewfullnames); + print_recent_activity_note($annotation->timecreated, $annotation, $cm->name, $link, false, $viewfullnames); echo '
'; } - return true; */ - return false; // True if anything was printed, otherwise false. + return true; } /** @@ -333,7 +310,7 @@ function annopy_print_recent_activity($course, $viewfullnames, $timestart) { */ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0) { - /* global $CFG, $COURSE, $USER, $DB; + global $CFG, $COURSE, $USER, $DB; if ($COURSE->id == $courseid) { $course = $COURSE; @@ -372,35 +349,35 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; } - $entries = $DB->get_records_sql( - 'SELECT e.id, e.timecreated, ' . $userfields . - ' FROM {annopy_entries} e - JOIN {annopy} m ON m.id = e.annopy - JOIN {user} u ON u.id = e.userid ' . $groupjoin . - ' WHERE e.timecreated > :timestart AND - m.id = :cminstance + $annotations = $DB->get_records_sql( + 'SELECT aa.id, aa.timecreated, ' . $userfields . + ' FROM {annopy_annotations} aa + JOIN {annopy_submissions} s ON s.id = aa.annopy + JOIN {annopy} a ON a.id = s.annopy + JOIN {user} u ON u.id = aa.userid ' . $groupjoin . + ' WHERE aa.timecreated > :timestart AND + a.id = :cminstance ' . $userselect . ' ' . $groupselect . - ' ORDER BY e.timecreated DESC', $params); + ' ORDER BY aa.timecreated DESC', $params); - if (!$entries) { + if (!$annotations) { return; } $groupmode = groups_get_activity_groupmode($cm, $course); $cmcontext = context_module::instance($cm->id); - $grader = has_capability('moodle/grade:viewall', $cmcontext); $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext); $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); - $teacher = has_capability('mod/annopy:manageentries', $cmcontext); + $teacher = has_capability('mod/annopy:viewparticipants', $cmcontext); $show = []; - foreach ($entries as $entry) { - if ($entry->userid == $USER->id) { - $show[] = $entry; + foreach ($annotations as $annotation) { + if ($annotation->userid == $USER->id) { + $show[] = $annotation; continue; } - // Only teachers can see other students entries. + // Only teachers can see other students annotations. if (!$teacher) { continue; } @@ -415,7 +392,7 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour if (!$modinfo->get_groups($cm->groupingid)) { continue; } - $usersgroups = groups_get_all_groups($course->id, $entry->userid, $cm->groupingid); + $usersgroups = groups_get_all_groups($course->id, $annotation->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); @@ -424,35 +401,23 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour } } } - $show[] = $entry; + $show[] = $annotation; } if (empty($show)) { return; } - if ($grader) { - require_once($CFG->libdir.'/gradelib.php'); - $userids = []; - foreach ($show as $id => $entry) { - $userids[] = $entry->userid; - } - $grades = grade_get_grades($courseid, 'mod', 'annopy', $cm->instance, $userids); - } - $aname = format_string($cm->name, true); - foreach ($show as $entry) { + foreach ($show as $annotation) { $activity = new stdClass(); $activity->type = 'annopy'; $activity->cmid = $cm->id; $activity->name = $aname; $activity->sectionnum = $cm->sectionnum; - $activity->timestamp = $entry->timecreated; + $activity->timestamp = $annotation->timecreated; $activity->user = new stdClass(); - if ($grader) { - $activity->grade = $grades->items[0]->grades[$entry->userid]->str_long_grade; - } if ($CFG->branch < 311) { $userfields = explode(',', user_picture::fields()); @@ -463,17 +428,17 @@ function annopy_get_recent_mod_activity(&$activities, &$index, $timestart, $cour foreach ($userfields as $userfield) { if ($userfield == 'id') { // Aliased in SQL above. - $activity->user->{$userfield} = $entry->userid; + $activity->user->{$userfield} = $annotation->userid; } else { - $activity->user->{$userfield} = $entry->{$userfield}; + $activity->user->{$userfield} = $annotation->{$userfield}; } } - $activity->user->fullname = fullname($entry, $viewfullnames); + $activity->user->fullname = fullname($annotation, $viewfullnames); $activities[$index++] = $activity; } - return; */ + return; } /** @@ -504,9 +469,9 @@ function annopy_print_recent_mod_activity($activity, $courseid, $detail, $modnam echo ''; } - echo '
'; + echo '
'; echo '' - . get_string('entryadded', 'mod_annopy') . ''; + . get_string('annotationadded', 'mod_annopy') . ''; echo '
'; echo '
'; diff --git a/mod_form.php b/mod_form.php index 4141f41..8535b64 100644 --- a/mod_form.php +++ b/mod_form.php @@ -72,28 +72,33 @@ public function definition() { $select .= " OR userid = " . $USER->id; $annotationtypetemplates = (array) $DB->get_records_select('annopy_atype_templates', $select); - $strmanager = get_string_manager(); + if ($annotationtypetemplates) { - $this->add_checkbox_controller(1); + $strmanager = get_string_manager(); - foreach ($annotationtypetemplates as $id => $type) { - if ($type->defaulttype == 1) { - $name = '(S)'; - } else { - $name = '(M)'; - } + $this->add_checkbox_controller(1); - if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_annopy')) { - $name .= '' . get_string($type->name, 'mod_annopy') . ''; - } else { - $name .= '' . $type->name . ''; - } + foreach ($annotationtypetemplates as $id => $type) { + if ($type->defaulttype == 1) { + $name = '(S)'; + } else { + $name = '(M)'; + } - $mform->addElement('advcheckbox', 'annotationtypes[' . $id . ']', $name, ' ', ['group' => 1], [0, 1]); - } + if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_annopy')) { + $name .= '' . get_string($type->name, 'mod_annopy') . ''; + } else { + $name .= '' . $type->name . ''; + } + $mform->addElement('advcheckbox', 'annotationtypes[' . $id . ']', $name, ' ', ['group' => 1], [0, 1]); + } + } else { + $mform->addElement('static', 'noannotationtypetemplates', '', + get_string('noannotationtypetemplates', 'mod_annopy')); + } } // Add standard grading elements. @@ -116,14 +121,6 @@ public function definition() { public function validation($data, $files) { $errors = parent::validation($data, $files); - /* $minwidth = 20; - $maxwidth = 80; - - if (!$data['annotationareawidth'] || $data['annotationareawidth'] < $minwidth || $data['annotationareawidth'] > $maxwidth) { - $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'annopy', ['minwidth' => $minwidth, - 'maxwidth' => $maxwidth]); - } */ - return $errors; } } diff --git a/version.php b/version.php index debb3de..9ee3617 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_annopy'; -$plugin->release = '0.6.0'; -$plugin->version = 2023120700; +$plugin->release = '0.7.0'; +$plugin->version = 2023121100; $plugin->requires = 2020061507; -$plugin->maturity = MATURITY_ALPHA; +$plugin->maturity = MATURITY_STABLE;