2b8e28bdec23a54043e2aba340098208b6400af6
[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             } else { // Set it to empty.
359                 $data->assignfeedbackcomments = '';
360                 $data->assignfeedbackcommentsformat = FORMAT_HTML;
361             }
362         }
364         file_prepare_standard_editor(
365             $data,
366             'assignfeedbackcomments',
367             $this->get_editor_options(),
368             $this->assignment->get_context(),
369             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
370             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
371             $grade->id
372         );
374         $mform->addElement('editor', 'assignfeedbackcomments_editor', $this->get_name(), null, $this->get_editor_options());
376         return true;
377     }
379     /**
380      * Saving the comment content into database.
381      *
382      * @param stdClass $grade
383      * @param stdClass $data
384      * @return bool
385      */
386     public function save(stdClass $grade, stdClass $data) {
387         global $DB;
389         // Save the files.
390         $data = file_postupdate_standard_editor(
391             $data,
392             'assignfeedbackcomments',
393             $this->get_editor_options(),
394             $this->assignment->get_context(),
395             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
396             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
397             $grade->id
398         );
400         $feedbackcomment = $this->get_feedback_comments($grade->id);
401         if ($feedbackcomment) {
402             $feedbackcomment->commenttext = $data->assignfeedbackcomments;
403             $feedbackcomment->commentformat = $data->assignfeedbackcommentsformat;
404             return $DB->update_record('assignfeedback_comments', $feedbackcomment);
405         } else {
406             $feedbackcomment = new stdClass();
407             $feedbackcomment->commenttext = $data->assignfeedbackcomments;
408             $feedbackcomment->commentformat = $data->assignfeedbackcommentsformat;
409             $feedbackcomment->grade = $grade->id;
410             $feedbackcomment->assignment = $this->assignment->get_instance()->id;
411             return $DB->insert_record('assignfeedback_comments', $feedbackcomment) > 0;
412         }
413     }
415     /**
416      * Display the comment in the feedback table.
417      *
418      * @param stdClass $grade
419      * @param bool $showviewlink Set to true to show a link to view the full feedback
420      * @return string
421      */
422     public function view_summary(stdClass $grade, & $showviewlink) {
423         $feedbackcomments = $this->get_feedback_comments($grade->id);
424         if ($feedbackcomments) {
425             $text = $this->rewrite_feedback_comments_urls($feedbackcomments->commenttext, $grade->id);
426             $text = format_text(
427                 $text,
428                 $feedbackcomments->commentformat,
429                 [
430                     'context' => $this->assignment->get_context()
431                 ]
432             );
434             // Show the view all link if the text has been shortened.
435             $short = shorten_text($text, 140);
436             $showviewlink = $short != $text;
437             return $short;
438         }
439         return '';
440     }
442     /**
443      * Display the comment in the feedback table.
444      *
445      * @param stdClass $grade
446      * @return string
447      */
448     public function view(stdClass $grade) {
449         $feedbackcomments = $this->get_feedback_comments($grade->id);
450         if ($feedbackcomments) {
451             $text = $this->rewrite_feedback_comments_urls($feedbackcomments->commenttext, $grade->id);
452             $text = format_text(
453                 $text,
454                 $feedbackcomments->commentformat,
455                 [
456                     'context' => $this->assignment->get_context()
457                 ]
458             );
460             return $text;
461         }
462         return '';
463     }
465     /**
466      * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type
467      * and version.
468      *
469      * @param string $type old assignment subtype
470      * @param int $version old assignment version
471      * @return bool True if upgrade is possible
472      */
473     public function can_upgrade($type, $version) {
475         if (($type == 'upload' || $type == 'uploadsingle' ||
476              $type == 'online' || $type == 'offline') && $version >= 2011112900) {
477             return true;
478         }
479         return false;
480     }
482     /**
483      * Upgrade the settings from the old assignment to the new plugin based one
484      *
485      * @param context $oldcontext - the context for the old assignment
486      * @param stdClass $oldassignment - the data for the old assignment
487      * @param string $log - can be appended to by the upgrade
488      * @return bool was it a success? (false will trigger a rollback)
489      */
490     public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) {
491         if ($oldassignment->assignmenttype == 'online') {
492             $this->set_config('commentinline', $oldassignment->var1);
493             return true;
494         }
495         return true;
496     }
498     /**
499      * Upgrade the feedback from the old assignment to the new one
500      *
501      * @param context $oldcontext - the database for the old assignment context
502      * @param stdClass $oldassignment The data record for the old assignment
503      * @param stdClass $oldsubmission The data record for the old submission
504      * @param stdClass $grade The data record for the new grade
505      * @param string $log Record upgrade messages in the log
506      * @return bool true or false - false will trigger a rollback
507      */
508     public function upgrade(context $oldcontext,
509                             stdClass $oldassignment,
510                             stdClass $oldsubmission,
511                             stdClass $grade,
512                             & $log) {
513         global $DB;
515         $feedbackcomments = new stdClass();
516         $feedbackcomments->commenttext = $oldsubmission->submissioncomment;
517         $feedbackcomments->commentformat = FORMAT_HTML;
519         $feedbackcomments->grade = $grade->id;
520         $feedbackcomments->assignment = $this->assignment->get_instance()->id;
521         if (!$DB->insert_record('assignfeedback_comments', $feedbackcomments) > 0) {
522             $log .= get_string('couldnotconvertgrade', 'mod_assign', $grade->userid);
523             return false;
524         }
526         return true;
527     }
529     /**
530      * If this plugin adds to the gradebook comments field, it must specify the format of the text
531      * of the comment
532      *
533      * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment
534      * settings page.
535      *
536      * @param stdClass $grade The grade
537      * @return int
538      */
539     public function format_for_gradebook(stdClass $grade) {
540         $feedbackcomments = $this->get_feedback_comments($grade->id);
541         if ($feedbackcomments) {
542             return $feedbackcomments->commentformat;
543         }
544         return FORMAT_MOODLE;
545     }
547     /**
548      * If this plugin adds to the gradebook comments field, it must format the text
549      * of the comment
550      *
551      * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment
552      * settings page.
553      *
554      * @param stdClass $grade The grade
555      * @return string
556      */
557     public function text_for_gradebook(stdClass $grade) {
558         $feedbackcomments = $this->get_feedback_comments($grade->id);
559         if ($feedbackcomments) {
560             return $feedbackcomments->commenttext;
561         }
562         return '';
563     }
565     /**
566      * Return any files this plugin wishes to save to the gradebook.
567      *
568      * @param stdClass $grade The assign_grades object from the db
569      * @return array
570      */
571     public function files_for_gradebook(stdClass $grade) : array {
572         return [
573             'contextid' => $this->assignment->get_context()->id,
574             'component' => ASSIGNFEEDBACK_COMMENTS_COMPONENT,
575             'filearea' => ASSIGNFEEDBACK_COMMENTS_FILEAREA,
576             'itemid' => $grade->id
577         ];
578     }
580     /**
581      * The assignment has been deleted - cleanup
582      *
583      * @return bool
584      */
585     public function delete_instance() {
586         global $DB;
587         // Will throw exception on failure.
588         $DB->delete_records('assignfeedback_comments',
589                             array('assignment'=>$this->assignment->get_instance()->id));
590         return true;
591     }
593     /**
594      * Returns true if there are no feedback comments for the given grade.
595      *
596      * @param stdClass $grade
597      * @return bool
598      */
599     public function is_empty(stdClass $grade) {
600         return $this->view($grade) == '';
601     }
603     /**
604      * Get file areas returns a list of areas this plugin stores files
605      * @return array - An array of fileareas (keys) and descriptions (values)
606      */
607     public function get_file_areas() {
608         return array(ASSIGNFEEDBACK_COMMENTS_FILEAREA => $this->get_name());
609     }
611     /**
612      * Return a description of external params suitable for uploading an feedback comment from a webservice.
613      *
614      * @return external_description|null
615      */
616     public function get_external_parameters() {
617         $editorparams = array('text' => new external_value(PARAM_RAW, 'The text for this feedback.'),
618                               'format' => new external_value(PARAM_INT, 'The format for this feedback'));
619         $editorstructure = new external_single_structure($editorparams, 'Editor structure', VALUE_OPTIONAL);
620         return array('assignfeedbackcomments_editor' => $editorstructure);
621     }
623     /**
624      * Return the plugin configs for external functions.
625      *
626      * @return array the list of settings
627      * @since Moodle 3.2
628      */
629     public function get_config_for_external() {
630         return (array) $this->get_config();
631     }
633     /**
634      * Convert encoded URLs in $text from the @@PLUGINFILE@@/... form to an actual URL.
635      *
636      * @param string $text the Text to check
637      * @param int $gradeid The grade ID which refers to the id in the gradebook
638      */
639     private function rewrite_feedback_comments_urls(string $text, int $gradeid) {
640         return file_rewrite_pluginfile_urls(
641             $text,
642             'pluginfile.php',
643             $this->assignment->get_context()->id,
644             ASSIGNFEEDBACK_COMMENTS_COMPONENT,
645             ASSIGNFEEDBACK_COMMENTS_FILEAREA,
646             $gradeid
647         );
648     }
650     /**
651      * File format options.
652      *
653      * @return array
654      */
655     private function get_editor_options() {
656         global $COURSE;
658         return [
659             'subdirs' => 1,
660             'maxbytes' => $COURSE->maxbytes,
661             'accepted_types' => '*',
662             'context' => $this->assignment->get_context(),
663             'maxfiles' => EDITOR_UNLIMITED_FILES
664         ];
665     }