Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This file contains the definition for the library class for file feedback plugin
 *
 *
 * @package   assignfeedback_file
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

> use \mod_assign\output\assign_header; // File areas for file feedback assignment. >
define('ASSIGNFEEDBACK_FILE_FILEAREA', 'feedback_files'); define('ASSIGNFEEDBACK_FILE_BATCH_FILEAREA', 'feedback_files_batch'); define('ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA', 'feedback_files_import'); define('ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES', 5); define('ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS', 5); define('ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME', 120); /** * Library class for file feedback plugin extending feedback plugin base class. * * @package assignfeedback_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_feedback_file extends assign_feedback_plugin { /** * Get the name of the file feedback plugin. * * @return string */ public function get_name() { return get_string('file', 'assignfeedback_file'); } /** * Get file feedback information from the database. * * @param int $gradeid * @return mixed */ public function get_file_feedback($gradeid) { global $DB; return $DB->get_record('assignfeedback_file', array('grade'=>$gradeid)); } /** * File format options. * * @return array */ private function get_file_options() { global $COURSE; $fileoptions = array('subdirs'=>1, 'maxbytes'=>$COURSE->maxbytes, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); return $fileoptions; } /** * Has the feedback file been modified? * * @param stdClass $grade Grade object. * @param stdClass $data Form data. * @return boolean True if the file area has been modified, else false. */ public function is_feedback_modified(stdClass $grade, stdClass $data) { global $USER; $filekey = null; $draftareainfo = null; foreach ($data as $key => $value) { if (strpos($key, 'files_') === 0 && strpos($key, '_filemanager')) { $filekey = $key; } } if (isset($filekey)) { $draftareainfo = file_get_draft_area_info($data->$filekey); $filecount = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); if ($filecount != $draftareainfo['filecount']) { return true; } else { // We need to check that the files in the draft area are the same as in the file area. $usercontext = context_user::instance($USER->id); $fs = get_file_storage(); $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $data->$filekey, 'id', true); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id, 'id', false); foreach ($files as $key => $file) { // Flag for recording if we have a matching file. $matchflag = false; foreach ($draftfiles as $draftkey => $draftfile) { if (!$file->is_directory()) { // File name is the same, but it could be a different file with the same name. if ($draftfile->get_filename() == $file->get_filename()) { // If the file name is the same but the content hash is different, or // The file path for the file has changed, then we have a modification. if ($draftfile->get_contenthash() != $file->get_contenthash() || $draftfile->get_filepath() != $file->get_filepath()) { return true; } // These files match. Check the next file. $matchflag = true; // We have a match on the file name so we can move to the next file and not // proceed through the other draftfiles. break; } } } // If the file does not match then there has been a modification. if (!$matchflag) { return true; } } } } return false; } /** * Copy all the files from one file area to another. * * @param file_storage $fs - The source context id * @param int $fromcontextid - The source context id * @param string $fromcomponent - The source component * @param string $fromfilearea - The source filearea * @param int $fromitemid - The source item id * @param int $tocontextid - The destination context id * @param string $tocomponent - The destination component * @param string $tofilearea - The destination filearea * @param int $toitemid - The destination item id * @return boolean */ private function copy_area_files(file_storage $fs, $fromcontextid, $fromcomponent, $fromfilearea, $fromitemid, $tocontextid, $tocomponent, $tofilearea, $toitemid) { $newfilerecord = new stdClass(); $newfilerecord->contextid = $tocontextid; $newfilerecord->component = $tocomponent; $newfilerecord->filearea = $tofilearea; $newfilerecord->itemid = $toitemid; if ($files = $fs->get_area_files($fromcontextid, $fromcomponent, $fromfilearea, $fromitemid)) { foreach ($files as $file) { if ($file->is_directory() and $file->get_filepath() === '/') { // We need a way to mark the age of each draft area. // By not copying the root dir we force it to be created // automatically with current timestamp. continue; } $existingfile = $fs->get_file( $newfilerecord->contextid, $newfilerecord->component, $newfilerecord->filearea, $newfilerecord->itemid, $file->get_filepath(), $file->get_filename() ); if ($existingfile) { // If the file already exists, remove it so it can be updated. $existingfile->delete(); } $newfile = $fs->create_file_from_storedfile($newfilerecord, $file); } } return true; } /** * Get form elements for grading form. * * @param stdClass $grade * @param MoodleQuickForm $mform * @param stdClass $data * @param int $userid The userid we are currently grading * @return bool true if elements were added to the form */ public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) { $fileoptions = $this->get_file_options(); $gradeid = $grade ? $grade->id : 0; $elementname = 'files_' . $userid; $data = file_prepare_standard_filemanager($data, $elementname, $fileoptions, $this->assignment->get_context(), 'assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $gradeid); $mform->addElement('filemanager', $elementname . '_filemanager', $this->get_name(), null, $fileoptions); return true; } /** * Count the number of files. * * @param int $gradeid * @param string $area * @return int */ private function count_files($gradeid, $area) { $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignfeedback_file', $area, $gradeid, 'id', false); return count($files); } /** * Update the number of files in the file area. * * @param stdClass $grade The grade record * @return bool - true if the value was saved */ public function update_file_count($grade) { global $DB; $filefeedback = $this->get_file_feedback($grade->id); if ($filefeedback) { $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); return $DB->update_record('assignfeedback_file', $filefeedback); } else { $filefeedback = new stdClass(); $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); $filefeedback->grade = $grade->id; $filefeedback->assignment = $this->assignment->get_instance()->id; return $DB->insert_record('assignfeedback_file', $filefeedback) > 0; } } /** * Save the feedback files. * * @param stdClass $grade * @param stdClass $data * @return bool */ public function save(stdClass $grade, stdClass $data) { $fileoptions = $this->get_file_options(); // The element name may have been for a different user. foreach ($data as $key => $value) { if (strpos($key, 'files_') === 0 && strpos($key, '_filemanager')) { $elementname = substr($key, 0, strpos($key, '_filemanager')); } } $data = file_postupdate_standard_filemanager($data, $elementname, $fileoptions, $this->assignment->get_context(), 'assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id); return $this->update_file_count($grade); } /** * Display the list of files in the feedback status table. * * @param stdClass $grade * @param bool $showviewlink - Set to true to show a link to see the full list of files * @return string */ public function view_summary(stdClass $grade, & $showviewlink) { $count = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); // Show a view all link if the number of files is over this limit. $showviewlink = $count > ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES; if ($count <= ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES) { return $this->assignment->render_area_files('assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id); } else { return get_string('countfiles', 'assignfeedback_file', $count); } } /** * Display the list of files in the feedback status table. * * @param stdClass $grade * @return string */ public function view(stdClass $grade) { return $this->assignment->render_area_files('assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id); } /** * The assignment has been deleted - cleanup. * * @return bool */ public function delete_instance() { global $DB; // Will throw exception on failure. $DB->delete_records('assignfeedback_file', array('assignment'=>$this->assignment->get_instance()->id)); return true; } /** * Return true if there are no feedback files. * * @param stdClass $grade */ public function is_empty(stdClass $grade) { return $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA) == 0; } /** * Get file areas returns a list of areas this plugin stores files. * * @return array - An array of fileareas (keys) and descriptions (values) */ public function get_file_areas() { return array(ASSIGNFEEDBACK_FILE_FILEAREA=>$this->get_name()); } /** * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type * and version. * * @param string $type old assignment subtype * @param int $version old assignment version * @return bool True if upgrade is possible */ public function can_upgrade($type, $version) { if (($type == 'upload' || $type == 'uploadsingle') && $version >= 2011112900) { return true; } return false; } /** * Upgrade the settings from the old assignment to the new plugin based one. * * @param context $oldcontext - the context for the old assignment * @param stdClass $oldassignment - the data for the old assignment * @param string $log - can be appended to by the upgrade * @return bool was it a success? (false will trigger a rollback) */ public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) { // First upgrade settings (nothing to do). return true; } /** * Upgrade the feedback from the old assignment to the new one. * * @param context $oldcontext - the database for the old assignment context * @param stdClass $oldassignment The data record for the old assignment * @param stdClass $oldsubmission The data record for the old submission * @param stdClass $grade The data record for the new grade * @param string $log Record upgrade messages in the log * @return bool true or false - false will trigger a rollback */ public function upgrade(context $oldcontext, stdClass $oldassignment, stdClass $oldsubmission, stdClass $grade, & $log) { global $DB; // Now copy the area files. $this->assignment->copy_area_files_for_upgrade($oldcontext->id, 'mod_assignment', 'response', $oldsubmission->id, $this->assignment->get_context()->id, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id); // Now count them! $filefeedback = new stdClass(); $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); $filefeedback->grade = $grade->id; $filefeedback->assignment = $this->assignment->get_instance()->id; if (!$DB->insert_record('assignfeedback_file', $filefeedback) > 0) { $log .= get_string('couldnotconvertgrade', 'mod_assign', $grade->userid); return false; } return true; } /** * Return a list of the batch grading operations performed by this plugin. * This plugin supports batch upload files and upload zip. * * @return array The list of batch grading operations */ public function get_grading_batch_operations() { return array('uploadfiles'=>get_string('uploadfiles', 'assignfeedback_file')); } /** * Upload files and send them to multiple users. * * @param array $users - An array of user ids * @return string - The response html */ public function view_batch_upload_files($users) { global $CFG, $DB, $USER; require_capability('mod/assign:grade', $this->assignment->get_context()); require_once($CFG->dirroot . '/mod/assign/feedback/file/batchuploadfilesform.php'); require_once($CFG->dirroot . '/mod/assign/renderable.php'); $formparams = array('cm'=>$this->assignment->get_course_module()->id, 'users'=>$users, 'context'=>$this->assignment->get_context()); $usershtml = ''; $usercount = 0; foreach ($users as $userid) { if ($usercount >= ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS) { $moreuserscount = count($users) - ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS; $usershtml .= get_string('moreusers', 'assignfeedback_file', $moreuserscount); break; } $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $usersummary = new assign_user_summary($user, $this->assignment->get_course()->id, has_capability('moodle/site:viewfullnames', $this->assignment->get_course_context()), $this->assignment->is_blind_marking(), $this->assignment->get_uniqueid_for_user($user->id), // TODO Does not support custom user profile fields (MDL-70456). \core_user\fields::get_identity_fields($this->assignment->get_context(), false)); $usershtml .= $this->assignment->get_renderer()->render($usersummary); $usercount += 1; } $formparams['usershtml'] = $usershtml; $mform = new assignfeedback_file_batch_upload_files_form(null, $formparams); if ($mform->is_cancelled()) { redirect(new moodle_url('view.php', array('id'=>$this->assignment->get_course_module()->id, 'action'=>'grading'))); return; } else if ($data = $mform->get_data()) { // Copy the files from the draft area to a temporary import area. $data = file_postupdate_standard_filemanager($data, 'files', $this->get_file_options(), $this->assignment->get_context(), 'assignfeedback_file', ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, $USER->id); $fs = get_file_storage(); // Now copy each of these files to the users feedback file area. foreach ($users as $userid) { $grade = $this->assignment->get_user_grade($userid, true); $this->assignment->notify_grade_modified($grade); $this->copy_area_files($fs, $this->assignment->get_context()->id, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, $USER->id, $this->assignment->get_context()->id, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_FILEAREA, $grade->id); $filefeedback = $this->get_file_feedback($grade->id); if ($filefeedback) { $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); $DB->update_record('assignfeedback_file', $filefeedback); } else { $filefeedback = new stdClass(); $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); $filefeedback->grade = $grade->id; $filefeedback->assignment = $this->assignment->get_instance()->id; $DB->insert_record('assignfeedback_file', $filefeedback); } } // Now delete the temporary import area. $fs->delete_area_files($this->assignment->get_context()->id, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, $USER->id); redirect(new moodle_url('view.php', array('id'=>$this->assignment->get_course_module()->id, 'action'=>'grading'))); return; } else { $header = new assign_header($this->assignment->get_instance(), $this->assignment->get_context(), false, $this->assignment->get_course_module()->id, get_string('batchuploadfiles', 'assignfeedback_file')); $o = ''; $o .= $this->assignment->get_renderer()->render($header); $o .= $this->assignment->get_renderer()->render(new assign_form('batchuploadfiles', $mform)); $o .= $this->assignment->get_renderer()->render_footer(); } return $o; } /** * User has chosen a custom grading batch operation and selected some users. * * @param string $action - The chosen action * @param array $users - An array of user ids * @return string - The response html */ public function grading_batch_operation($action, $users) { if ($action == 'uploadfiles') { return $this->view_batch_upload_files($users); } return ''; } /** * View the upload zip form. * * @return string - The html response */ public function view_upload_zip() { global $CFG, $USER; require_capability('mod/assign:grade', $this->assignment->get_context()); require_once($CFG->dirroot . '/mod/assign/feedback/file/uploadzipform.php'); require_once($CFG->dirroot . '/mod/assign/feedback/file/importziplib.php'); require_once($CFG->dirroot . '/mod/assign/feedback/file/importzipform.php'); $formparams = array('context'=>$this->assignment->get_context(), 'cm'=>$this->assignment->get_course_module()->id); $mform = new assignfeedback_file_upload_zip_form(null, $formparams); $o = ''; $confirm = optional_param('confirm', 0, PARAM_BOOL); $renderer = $this->assignment->get_renderer(); // Delete any existing files. $importer = new assignfeedback_file_zip_importer(); $contextid = $this->assignment->get_context()->id; if ($mform->is_cancelled()) { $importer->delete_import_files($contextid); $urlparams = array('id'=>$this->assignment->get_course_module()->id, 'action'=>'grading'); $url = new moodle_url('view.php', $urlparams); redirect($url); return; } else if ($confirm) { $params = array('assignment'=>$this->assignment, 'importer'=>$importer); $mform = new assignfeedback_file_import_zip_form(null, $params); if ($mform->is_cancelled()) { $importer->delete_import_files($contextid); $urlparams = array('id'=>$this->assignment->get_course_module()->id, 'action'=>'grading'); $url = new moodle_url('view.php', $urlparams); redirect($url); return; } $o .= $importer->import_zip_files($this->assignment, $this); $importer->delete_import_files($contextid); } else if (($data = $mform->get_data()) && ($zipfile = $mform->save_stored_file('feedbackzip', $contextid, 'assignfeedback_file', ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA, $USER->id, '/', 'import.zip', true))) { $importer->extract_files_from_zip($zipfile, $contextid); $params = array('assignment'=>$this->assignment, 'importer'=>$importer); $mform = new assignfeedback_file_import_zip_form(null, $params); $header = new assign_header($this->assignment->get_instance(), $this->assignment->get_context(), false, $this->assignment->get_course_module()->id, get_string('confirmuploadzip', 'assignfeedback_file')); $o .= $renderer->render($header); $o .= $renderer->render(new assign_form('confirmimportzip', $mform)); $o .= $renderer->render_footer(); } else { $header = new assign_header($this->assignment->get_instance(), $this->assignment->get_context(), false, $this->assignment->get_course_module()->id, get_string('uploadzip', 'assignfeedback_file')); $o .= $renderer->render($header); $o .= $renderer->render(new assign_form('uploadfeedbackzip', $mform)); $o .= $renderer->render_footer(); } return $o; } /** * Called by the assignment module when someone chooses something from the * grading navigation or batch operations list. * * @param string $action - The page to view * @return string - The html response */ public function view_page($action) { if ($action == 'uploadfiles') { $users = required_param('selectedusers', PARAM_SEQUENCE); return $this->view_batch_upload_files(explode(',', $users)); } if ($action == 'uploadzip') { return $this->view_upload_zip(); } return ''; } /** * Return a list of the grading actions performed by this plugin. * This plugin supports upload zip. * * @return array The list of grading actions */ public function get_grading_actions() { return array('uploadzip'=>get_string('uploadzip', 'assignfeedback_file')); } /** * Return a description of external params suitable for uploading a feedback file from a webservice. * * @return external_description|null */ public function get_external_parameters() { return array( 'files_filemanager' => new external_value( PARAM_INT, 'The id of a draft area containing files for this feedback.', VALUE_OPTIONAL ) ); } /** * Return the plugin configs for external functions. * * @return array the list of settings * @since Moodle 3.2 */ public function get_config_for_external() { return (array) $this->get_config(); } }