Merge branch 'MDL-63632-master' of git://github.com/andrewnicols/moodle
[moodle.git] / mod / assign / feedback / comments / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This file contains the definition for the library class for comment feedback plugin
19  *
20  * @package   assignfeedback_comments
21  * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 // File component for feedback comments.
28 define('ASSIGNFEEDBACK_COMMENTS_COMPONENT', 'assignfeedback_comments');
30 // File area for feedback comments.
31 define('ASSIGNFEEDBACK_COMMENTS_FILEAREA', 'feedback');
33 /**
34  * Library class for comment feedback plugin extending feedback plugin base class.
35  *
36  * @package   assignfeedback_comments
37  * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
38  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class assign_feedback_comments extends assign_feedback_plugin {
42     /**
43      * Get the name of the online comment feedback plugin.
44      * @return string
45      */
46     public function get_name() {
47         return get_string('pluginname', 'assignfeedback_comments');
48     }
50     /**
51      * Get the feedback comment from the database.
52      *
53      * @param int $gradeid
54      * @return stdClass|false The feedback comments for the given grade if it exists.
55      *                        False if it doesn't.
56      */
57     public function get_feedback_comments($gradeid) {
58         global $DB;
59         return $DB->get_record('assignfeedback_comments', array('grade'=>$gradeid));
60     }
62     /**
63      * Get quickgrading form elements as html.
64      *
65      * @param int $userid The user id in the table this quickgrading element relates to
66      * @param mixed $grade - The grade data - may be null if there are no grades for this user (yet)
67      * @return mixed - A html string containing the html form elements required for quickgrading
68      */
69     public function get_quickgrading_html($userid, $grade) {
70         $commenttext = '';
71         if ($grade) {
72             $feedbackcomments = $this->get_feedback_comments($grade->id);
73             if ($feedbackcomments) {
74                 $commenttext = $feedbackcomments->commenttext;
75             }
76         }
78         $pluginname = get_string('pluginname', 'assignfeedback_comments');
79         $labeloptions = array('for'=>'quickgrade_comments_' . $userid,
80                               'class'=>'accesshide');
81         $textareaoptions = array('name'=>'quickgrade_comments_' . $userid,
82                                  'id'=>'quickgrade_comments_' . $userid,
83                                  'class'=>'quickgrade');
84         return html_writer::tag('label', $pluginname, $labeloptions) .
85                html_writer::tag('textarea', $commenttext, $textareaoptions);
86     }
88     /**
89      * Has the plugin quickgrading form element been modified in the current form submission?
90      *
91      * @param int $userid The user id in the table this quickgrading element relates to
92      * @param stdClass $grade The grade
93      * @return boolean - true if the quickgrading form element has been modified
94      */
95     public function is_quickgrading_modified($userid, $grade) {
96         $commenttext = '';
97         if ($grade) {
98             $feedbackcomments = $this->get_feedback_comments($grade->id);
99             if ($feedbackcomments) {
100                 $commenttext = $feedbackcomments->commenttext;
101             }
102         }
103         // Note that this handles the difference between empty and not in the quickgrading
104         // form at all (hidden column).
105         $newvalue = optional_param('quickgrade_comments_' . $userid, false, PARAM_RAW);
106         return ($newvalue !== false) && ($newvalue != $commenttext);
107     }
109     /**
110      * Has the comment feedback been modified?
111      *
112      * @param stdClass $grade The grade object.
113      * @param stdClass $data Data from the form submission.
114      * @return boolean True if the comment feedback has been modified, else false.
115      */
116     public function is_feedback_modified(stdClass $grade, stdClass $data) {
117         $commenttext = '';
118         if ($grade) {
119             $feedbackcomments = $this->get_feedback_comments($grade->id);
120             if ($feedbackcomments) {
121                 $commenttext = $feedbackcomments->commenttext;
122             }
123         }
125         $formtext = $data->assignfeedbackcomments_editor['text'];
127         // Need to convert the form text to use @@PLUGINFILE@@ and format it so we can compare it with what is stored in the DB.
128         if (isset($data->assignfeedbackcomments_editor['itemid'])) {
129             $formtext = file_rewrite_urls_to_pluginfile($formtext, $data->assignfeedbackcomments_editor['itemid']);
130             $formtext = format_text($formtext, FORMAT_HTML);
131         }
133         if ($commenttext == $formtext) {
134             return false;
135         } else {
136             return true;
137         }
138     }
141     /**
142      * Override to indicate a plugin supports quickgrading.
143      *
144      * @return boolean - True if the plugin supports quickgrading
145      */
146     public function supports_quickgrading() {
147         return true;
148     }
150     /**
151      * Return a list of the text fields that can be imported/exported by this plugin.
152      *
153      * @return array An array of field names and descriptions. (name=>description, ...)
154      */
155     public function get_editor_fields() {
156         return array('comments' => get_string('pluginname', 'assignfeedback_comments'));
157     }
159     /**
160      * Get the saved text content from the editor.
161      *
162      * @param string $name
163      * @param int $gradeid
164      * @return string
165      */
166     public function get_editor_text($name, $gradeid) {
167         if ($name == 'comments') {
168             $feedbackcomments = $this->get_feedback_comments($gradeid);
169             if ($feedbackcomments) {
170                 return $feedbackcomments->commenttext;
171             }
172         }
174         return '';
175     }
177     /**
178      * Get the saved text content from the editor.
179      *
180      * @param string $name
181      * @param string $value
182      * @param int $gradeid
183      * @return string
184      */
185     public function set_editor_text($name, $value, $gradeid) {
186         global $DB;
188         if ($name == 'comments') {
189             $feedbackcomment = $this->get_feedback_comments($gradeid);
190             if ($feedbackcomment) {
191                 $feedbackcomment->commenttext = $value;
192                 return $DB->update_record('assignfeedback_comments', $feedbackcomment);
193             } else {
194                 $feedbackcomment = new stdClass();
195                 $feedbackcomment->commenttext = $value;
196                 $feedbackcomment->commentformat = FORMAT_HTML;
197                 $feedbackcomment->grade = $gradeid;
198                 $feedbackcomment->assignment = $this->assignment->get_instance()->id;
199                 return $DB->insert_record('assignfeedback_comments', $feedbackcomment) > 0;
200             }
201         }
203         return false;
204     }
206     /**
207      * Save quickgrading changes.
208      *
209      * @param int $userid The user id in the table this quickgrading element relates to
210      * @param stdClass $grade The grade
211      * @return boolean - true if the grade changes were saved correctly
212      */
213     public function save_quickgrading_changes($userid, $grade) {
214         global $DB;
215         $feedbackcomment = $this->get_feedback_comments($grade->id);
216         $quickgradecomments = optional_param('quickgrade_comments_' . $userid, null, PARAM_RAW);
217         if (!$quickgradecomments && $quickgradecomments !== '') {
218             return true;
219         }
220         if ($feedbackcomment) {
221             $feedbackcomment->commenttext = $quickgradecomments;
222             return $DB->update_record('assignfeedback_comments', $feedbackcomment);
223         } else {
224             $feedbackcomment = new stdClass();
225             $feedbackcomment->commenttext = $quickgradecomments;
226             $feedbackcomment->commentformat = FORMAT_HTML;
227             $feedbackcomment->grade = $grade->id;
228             $feedbackcomment->assignment = $this->assignment->get_instance()->id;
229             return $DB->insert_record('assignfeedback_comments', $feedbackcomment) > 0;
230         }
231     }
233     /**
234      * Save the settings for feedback comments plugin
235      *
236      * @param stdClass $data
237      * @return bool
238      */
239     public function save_settings(stdClass $data) {
240         $this->set_config('commentinline', !empty($data->assignfeedback_comments_commentinline));
241         return true;
242     }
244     /**
245      * Get the default setting for feedback comments plugin
246      *
247      * @param MoodleQuickForm $mform The form to add elements to
248      * @return void
249      */
250     public function get_settings(MoodleQuickForm $mform) {
251         $default = $this->get_config('commentinline');
252         if ($default === false) {
253             // Apply the admin default if we don't have a value yet.
254             $default = get_config('assignfeedback_comments', 'inline');
255         }
256         $mform->addElement('selectyesno',
257                            'assignfeedback_comments_commentinline',
258                            get_string('commentinline', 'assignfeedback_comments'));
259         $mform->addHelpButton('assignfeedback_comments_commentinline', 'commentinline', 'assignfeedback_comments');
260         $mform->setDefault('assignfeedback_comments_commentinline', $default);
261         // Disable comment online if comment feedback plugin is disabled.
262         $mform->disabledIf('assignfeedback_comments_commentinline', 'assignfeedback_comments_enabled', 'notchecked');
263    }
265     /**
266      * Convert the text from any submission plugin that has an editor field to
267      * a format suitable for inserting in the feedback text field.
268      *
269      * @param stdClass $submission
270      * @param stdClass $data - Form data to be filled with the converted submission text and format.
271      * @param stdClass|null $grade
272      * @return boolean - True if feedback text was set.
273      */
274     protected function convert_submission_text_to_feedback($submission, $data, $grade) {
275         global $DB;
277         $format = false;
278         $text = '';
280         foreach ($this->assignment->get_submission_plugins() as $plugin) {
281             $fields = $plugin->get_editor_fields();
282             if ($plugin->is_enabled() && $plugin->is_visible() && !$plugin->is_empty($submission) && !empty($fields)) {
283                 $user = $DB->get_record('user', ['id' => $submission->userid]);
284                 // Copy the files to the feedback area.
285                 if ($files = $plugin->get_files($submission, $user)) {
286                     $fs = get_file_storage();
287                     $component = 'assignfeedback_comments';
288                     $filearea = ASSIGNFEEDBACK_COMMENTS_FILEAREA;
289                     $itemid = $grade->id;
290                     $fieldupdates = [
291                         'component' => $component,
292                         'filearea' => $filearea,
293                         'itemid' => $itemid
294                     ];
295                     foreach ($files as $file) {
296                         if ($file instanceof stored_file) {
297                             // Before we create it, check that it doesn't already exist.
298                             if (!$fs->file_exists(
299                                     $file->get_contextid(),
300                                     $component,
301                                     $filearea,
302                                     $itemid,
303                                     $file->get_filepath(),
304                                     $file->get_filename())) {
305                                 $fs->create_file_from_storedfile($fieldupdates, $file);
306                             }
307                         }
308                     }
309                 }
310                 foreach ($fields as $key => $description) {
311                     $rawtext = clean_text($plugin->get_editor_text($key, $submission->id));
312                     $newformat = $plugin->get_editor_format($key, $submission->id);
314                     if ($format !== false && $newformat != $format) {
315                         // There are 2 or more editor fields using different formats, set to plain as a fallback.
316                         $format = FORMAT_PLAIN;
317                     } else {
318                         $format = $newformat;
319                     }
320                     $text .= $rawtext;
321                 }
322             }
323         }
325         if ($format === false) {
326             $format = FORMAT_HTML;
327         }
328         $data->assignfeedbackcomments = $text;
329         $data->assignfeedbackcommentsformat = $format;
331         return true;
332     }
334     /**
335      * Get form elements for the grading page
336      *
337      * @param stdClass|null $grade
338      * @param MoodleQuickForm $mform
339      * @param stdClass $data
340      * @return bool true if elements were added to the form
341      */
342     public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
343         $commentinlinenabled = $this->get_config('commentinline');
344         $submission = $this->assignment->get_user_submission($userid, false);
345         $feedbackcomments = false;
347         if ($grade) {
348             $feedbackcomments = $this->get_feedback_comments($grade->id);
349         }
351         if ($feedbackcomments && !empty($feedbackcomments->commenttext)) {
352             $data->assignfeedbackcomments = $feedbackcomments->commenttext;
353             $data->assignfeedbackcommentsformat = $feedbackcomments->commentformat;
354         } else {
355             // No feedback given yet - maybe we need to copy the text from the submission?
356             if (!empty($commentinlinenabled) && $submission) {
357                 $this->convert_submission_text_to_feedback($submission, $data, $grade);
358             }
359         }
361         file_prepare_standard_editor(
362             $data,
363             'assignfeedbackcomments',
364             $this->get_editor_options(),
365             $this->assignment->get_context(),
366             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
367             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
368             $grade->id
369         );
370         $mform->addElement('editor', 'assignfeedbackcomments_editor', $this->get_name(), null, $this->get_editor_options());
372         return true;
373     }
375     /**
376      * Saving the comment content into database.
377      *
378      * @param stdClass $grade
379      * @param stdClass $data
380      * @return bool
381      */
382     public function save(stdClass $grade, stdClass $data) {
383         global $DB;
385         // Save the files.
386         $data = file_postupdate_standard_editor(
387             $data,
388             'assignfeedbackcomments',
389             $this->get_editor_options(),
390             $this->assignment->get_context(),
391             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
392             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
393             $grade->id
394         );
396         $feedbackcomment = $this->get_feedback_comments($grade->id);
397         if ($feedbackcomment) {
398             $feedbackcomment->commenttext = $data->assignfeedbackcomments;
399             $feedbackcomment->commentformat = $data->assignfeedbackcommentsformat;
400             return $DB->update_record('assignfeedback_comments', $feedbackcomment);
401         } else {
402             $feedbackcomment = new stdClass();
403             $feedbackcomment->commenttext = $data->assignfeedbackcomments;
404             $feedbackcomment->commentformat = $data->assignfeedbackcommentsformat;
405             $feedbackcomment->grade = $grade->id;
406             $feedbackcomment->assignment = $this->assignment->get_instance()->id;
407             return $DB->insert_record('assignfeedback_comments', $feedbackcomment) > 0;
408         }
409     }
411     /**
412      * Display the comment in the feedback table.
413      *
414      * @param stdClass $grade
415      * @param bool $showviewlink Set to true to show a link to view the full feedback
416      * @return string
417      */
418     public function view_summary(stdClass $grade, & $showviewlink) {
419         $feedbackcomments = $this->get_feedback_comments($grade->id);
420         if ($feedbackcomments) {
421             $text = $this->rewrite_feedback_comments_urls($feedbackcomments->commenttext, $grade->id);
422             $text = format_text(
423                 $text,
424                 $feedbackcomments->commentformat,
425                 [
426                     'context' => $this->assignment->get_context()
427                 ]
428             );
430             // Show the view all link if the text has been shortened.
431             $short = shorten_text($text, 140);
432             $showviewlink = $short != $text;
433             return $short;
434         }
435         return '';
436     }
438     /**
439      * Display the comment in the feedback table.
440      *
441      * @param stdClass $grade
442      * @return string
443      */
444     public function view(stdClass $grade) {
445         $feedbackcomments = $this->get_feedback_comments($grade->id);
446         if ($feedbackcomments) {
447             $text = $this->rewrite_feedback_comments_urls($feedbackcomments->commenttext, $grade->id);
448             $text = format_text(
449                 $text,
450                 $feedbackcomments->commentformat,
451                 [
452                     'context' => $this->assignment->get_context()
453                 ]
454             );
456             return $text;
457         }
458         return '';
459     }
461     /**
462      * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type
463      * and version.
464      *
465      * @param string $type old assignment subtype
466      * @param int $version old assignment version
467      * @return bool True if upgrade is possible
468      */
469     public function can_upgrade($type, $version) {
471         if (($type == 'upload' || $type == 'uploadsingle' ||
472              $type == 'online' || $type == 'offline') && $version >= 2011112900) {
473             return true;
474         }
475         return false;
476     }
478     /**
479      * Upgrade the settings from the old assignment to the new plugin based one
480      *
481      * @param context $oldcontext - the context for the old assignment
482      * @param stdClass $oldassignment - the data for the old assignment
483      * @param string $log - can be appended to by the upgrade
484      * @return bool was it a success? (false will trigger a rollback)
485      */
486     public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) {
487         if ($oldassignment->assignmenttype == 'online') {
488             $this->set_config('commentinline', $oldassignment->var1);
489             return true;
490         }
491         return true;
492     }
494     /**
495      * Upgrade the feedback from the old assignment to the new one
496      *
497      * @param context $oldcontext - the database for the old assignment context
498      * @param stdClass $oldassignment The data record for the old assignment
499      * @param stdClass $oldsubmission The data record for the old submission
500      * @param stdClass $grade The data record for the new grade
501      * @param string $log Record upgrade messages in the log
502      * @return bool true or false - false will trigger a rollback
503      */
504     public function upgrade(context $oldcontext,
505                             stdClass $oldassignment,
506                             stdClass $oldsubmission,
507                             stdClass $grade,
508                             & $log) {
509         global $DB;
511         $feedbackcomments = new stdClass();
512         $feedbackcomments->commenttext = $oldsubmission->submissioncomment;
513         $feedbackcomments->commentformat = FORMAT_HTML;
515         $feedbackcomments->grade = $grade->id;
516         $feedbackcomments->assignment = $this->assignment->get_instance()->id;
517         if (!$DB->insert_record('assignfeedback_comments', $feedbackcomments) > 0) {
518             $log .= get_string('couldnotconvertgrade', 'mod_assign', $grade->userid);
519             return false;
520         }
522         return true;
523     }
525     /**
526      * If this plugin adds to the gradebook comments field, it must specify the format of the text
527      * of the comment
528      *
529      * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment
530      * settings page.
531      *
532      * @param stdClass $grade The grade
533      * @return int
534      */
535     public function format_for_gradebook(stdClass $grade) {
536         $feedbackcomments = $this->get_feedback_comments($grade->id);
537         if ($feedbackcomments) {
538             return $feedbackcomments->commentformat;
539         }
540         return FORMAT_MOODLE;
541     }
543     /**
544      * If this plugin adds to the gradebook comments field, it must format the text
545      * of the comment
546      *
547      * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment
548      * settings page.
549      *
550      * @param stdClass $grade The grade
551      * @return string
552      */
553     public function text_for_gradebook(stdClass $grade) {
554         $feedbackcomments = $this->get_feedback_comments($grade->id);
555         if ($feedbackcomments) {
556             return $feedbackcomments->commenttext;
557         }
558         return '';
559     }
561     /**
562      * Return any files this plugin wishes to save to the gradebook.
563      *
564      * @param stdClass $grade The assign_grades object from the db
565      * @return array
566      */
567     public function files_for_gradebook(stdClass $grade) : array {
568         return [
569             'contextid' => $this->assignment->get_context()->id,
570             'component' => ASSIGNFEEDBACK_COMMENTS_COMPONENT,
571             'filearea' => ASSIGNFEEDBACK_COMMENTS_FILEAREA,
572             'itemid' => $grade->id
573         ];
574     }
576     /**
577      * The assignment has been deleted - cleanup
578      *
579      * @return bool
580      */
581     public function delete_instance() {
582         global $DB;
583         // Will throw exception on failure.
584         $DB->delete_records('assignfeedback_comments',
585                             array('assignment'=>$this->assignment->get_instance()->id));
586         return true;
587     }
589     /**
590      * Returns true if there are no feedback comments for the given grade.
591      *
592      * @param stdClass $grade
593      * @return bool
594      */
595     public function is_empty(stdClass $grade) {
596         return $this->view($grade) == '';
597     }
599     /**
600      * Return a description of external params suitable for uploading an feedback comment from a webservice.
601      *
602      * @return external_description|null
603      */
604     public function get_external_parameters() {
605         $editorparams = array('text' => new external_value(PARAM_RAW, 'The text for this feedback.'),
606                               'format' => new external_value(PARAM_INT, 'The format for this feedback'));
607         $editorstructure = new external_single_structure($editorparams, 'Editor structure', VALUE_OPTIONAL);
608         return array('assignfeedbackcomments_editor' => $editorstructure);
609     }
611     /**
612      * Return the plugin configs for external functions.
613      *
614      * @return array the list of settings
615      * @since Moodle 3.2
616      */
617     public function get_config_for_external() {
618         return (array) $this->get_config();
619     }
621     /**
622      * Convert encoded URLs in $text from the @@PLUGINFILE@@/... form to an actual URL.
623      *
624      * @param string $text the Text to check
625      * @param int $gradeid The grade ID which refers to the id in the gradebook
626      */
627     private function rewrite_feedback_comments_urls(string $text, int $gradeid) {
628         return file_rewrite_pluginfile_urls(
629             $text,
630             'pluginfile.php',
631             $this->assignment->get_context()->id,
632             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
633             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
634             $gradeid
635         );
636     }
638     /**
639      * File format options.
640      *
641      * @return array
642      */
643     private function get_editor_options() {
644         global $COURSE;
646         return [
647             'subdirs' => 1,
648             'maxbytes' => $COURSE->maxbytes,
649             'accepted_types' => '*',
650             'context' => $this->assignment->get_context(),
651             'maxfiles' => EDITOR_UNLIMITED_FILES
652         ];
653     }