See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402]
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/>. 16 17 /** 18 * This file contains the definition for the library class for file feedback plugin 19 * 20 * 21 * @package assignfeedback_file 22 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 use mod_assign\output\assign_header; 29 30 /** 31 * library class for importing feedback files from a zip 32 * 33 * @package assignfeedback_file 34 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class assignfeedback_file_zip_importer { 38 39 /** 40 * Is this filename valid (contains a unique participant ID) for import? 41 * 42 * @param assign $assignment - The assignment instance 43 * @param stored_file $fileinfo - The fileinfo 44 * @param array $participants - A list of valid participants for this module indexed by unique_id or group id. 45 * @param array $users - Set to array with the user(s) that matches by participant id 46 * @param assign_plugin $plugin - Set to the plugin that exported the file 47 * @param string $filename - Set to truncated filename (prefix stripped) 48 * @return bool If the participant Id can be extracted and this is a valid user 49 */ 50 public function is_valid_filename_for_import($assignment, $fileinfo, $participants, & $users, & $plugin, & $filename) { 51 if ($fileinfo->is_directory()) { 52 return false; 53 } 54 55 // Ignore hidden files. 56 if (strpos($fileinfo->get_filename(), '.') === 0) { 57 return false; 58 } 59 // Ignore hidden files. 60 if (strpos($fileinfo->get_filename(), '~') === 0) { 61 return false; 62 } 63 64 // Break the full path-name into path parts. 65 $pathparts = explode('/', $fileinfo->get_filepath() . $fileinfo->get_filename()); 66 67 while (!empty($pathparts)) { 68 // Get the next path part and break it up by underscores. 69 $pathpart = array_shift($pathparts); 70 $info = explode('_', $pathpart, 5); 71 72 // Expected format for the directory names in $pathpart is fullname_userid_plugintype_pluginname (as created by zip 73 // export in Moodle >= 4.1) resp. fullname_userid_plugintype_pluginname_ (as created by earlier versions). We ensure 74 // compatibility with both ways here. 75 if (count($info) < 4) { 76 continue; 77 } 78 79 // Check the participant id. 80 $participantid = $info[1]; 81 82 if (!is_numeric($participantid)) { 83 continue; 84 } 85 86 // Convert to int. 87 $participantid += 0; 88 89 if (empty($participants[$participantid])) { 90 continue; 91 } 92 93 // Set user, which is by reference, so is used by the calling script. 94 $users = $participants[$participantid]; 95 96 // Set the plugin. This by reference, and is used by the calling script. 97 $plugin = $assignment->get_plugin_by_type($info[2], $info[3]); 98 99 if (!$plugin) { 100 continue; 101 } 102 103 // To get clean path names, we need to have at least an empty entry for $info[4]. 104 if (count($info) == 4) { 105 $info[4] = ''; 106 } 107 // Take any remaining text in this part and put it back in the path parts array. 108 array_unshift($pathparts, $info[4]); 109 110 // Combine the remaining parts and set it as the filename. 111 // Note that filename is a 'by reference' variable, so we need to set it before returning. 112 $filename = implode('/', $pathparts); 113 114 return true; 115 } 116 117 return false; 118 } 119 120 /** 121 * Does this file exist in any of the current files supported by this plugin for this user? 122 * 123 * @param assign $assignment - The assignment instance 124 * @param array|stdClass $users The user(s) matching this uploaded file 125 * @param assign_plugin $plugin The matching plugin from the filename 126 * @param string $filename The parsed filename from the zip 127 * @param stored_file $fileinfo The info about the extracted file from the zip 128 * @return bool - True if the file has been modified or is new 129 */ 130 public function is_file_modified($assignment, $users, $plugin, $filename, $fileinfo) { 131 $sg = null; 132 133 if (is_array($users)) { 134 $user = $users[0]; 135 } else { 136 $user = $users; 137 } 138 139 if ($plugin->get_subtype() == 'assignsubmission') { 140 if ($assignment->get_instance()->teamsubmission) { 141 $sg = $assignment->get_group_submission($user->id, 0, false); 142 } else { 143 $sg = $assignment->get_user_submission($user->id, false); 144 } 145 } else if ($plugin->get_subtype() == 'assignfeedback') { 146 $sg = $assignment->get_user_grade($user->id, false); 147 } else { 148 return false; 149 } 150 151 if (!$sg) { 152 return true; 153 } 154 foreach ($plugin->get_files($sg, $user) as $pluginfilename => $file) { 155 if ($pluginfilename == $filename) { 156 // Extract the file and compare hashes. 157 $contenthash = ''; 158 if (is_array($file)) { 159 $content = reset($file); 160 $contenthash = file_storage::hash_from_string($content); 161 } else { 162 $contenthash = $file->get_contenthash(); 163 } 164 if ($contenthash != $fileinfo->get_contenthash()) { 165 return true; 166 } else { 167 return false; 168 } 169 } 170 } 171 return true; 172 } 173 174 /** 175 * Delete all temp files used when importing a zip 176 * 177 * @param int $contextid - The context id of this assignment instance 178 * @return bool true if all files were deleted 179 */ 180 public function delete_import_files($contextid) { 181 global $USER; 182 183 $fs = get_file_storage(); 184 185 return $fs->delete_area_files($contextid, 186 'assignfeedback_file', 187 ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA, 188 $USER->id); 189 } 190 191 /** 192 * Extract the uploaded zip to a temporary import area for this user 193 * 194 * @param stored_file $zipfile The uploaded file 195 * @param int $contextid The context for this assignment 196 * @return bool - True if the files were unpacked 197 */ 198 public function extract_files_from_zip($zipfile, $contextid) { 199 global $USER; 200 201 $feedbackfilesupdated = 0; 202 $feedbackfilesadded = 0; 203 $userswithnewfeedback = array(); 204 205 // Unzipping a large zip file is memory intensive. 206 raise_memory_limit(MEMORY_EXTRA); 207 208 $packer = get_file_packer('application/zip'); 209 core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME); 210 211 return $packer->extract_to_storage($zipfile, 212 $contextid, 213 'assignfeedback_file', 214 ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA, 215 $USER->id, 216 'import'); 217 218 } 219 220 /** 221 * Get the list of files extracted from the uploaded zip 222 * 223 * @param int $contextid 224 * @return array of stored_files 225 */ 226 public function get_import_files($contextid) { 227 global $USER; 228 229 $fs = get_file_storage(); 230 $files = $fs->get_directory_files($contextid, 231 'assignfeedback_file', 232 ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA, 233 $USER->id, 234 '/import/', true); // Get files recursive (all levels). 235 236 $keys = array_keys($files); 237 238 return $files; 239 } 240 241 /** 242 * Returns a mapping from unique user / group ids in folder names to array of moodle users. 243 * 244 * @param assign $assignment - The assignment instance 245 * @return array the mapping. 246 */ 247 public function get_participant_mapping(assign $assignment): array { 248 $currentgroup = groups_get_activity_group($assignment->get_course_module(), true); 249 $allusers = $assignment->list_participants($currentgroup, false); 250 $participants = array(); 251 foreach ($allusers as $user) { 252 if ($assignment->get_instance()->teamsubmission) { 253 $group = $assignment->get_submission_group($user->id); 254 if (!$group) { 255 continue; 256 } 257 if (!isset($participants[$group->id])) { 258 $participants[$group->id] = []; 259 } 260 $participants[$group->id][] = $user; 261 } else { 262 $participants[$assignment->get_uniqueid_for_user($user->id)] = [$user]; 263 } 264 } 265 return $participants; 266 } 267 268 /** 269 * Process an uploaded zip file 270 * 271 * @param assign $assignment - The assignment instance 272 * @param assign_feedback_file $fileplugin - The file feedback plugin 273 * @return string - The html response 274 */ 275 public function import_zip_files($assignment, $fileplugin) { 276 global $CFG, $PAGE, $DB; 277 278 core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME); 279 $packer = get_file_packer('application/zip'); 280 281 $feedbackfilesupdated = 0; 282 $feedbackfilesadded = 0; 283 $userswithnewfeedback = array(); 284 $contextid = $assignment->get_context()->id; 285 286 $fs = get_file_storage(); 287 $files = $this->get_import_files($contextid); 288 289 $participants = $this->get_participant_mapping($assignment); 290 291 foreach ($files as $unzippedfile) { 292 $users = null; 293 $plugin = null; 294 $filename = ''; 295 296 if ($this->is_valid_filename_for_import($assignment, $unzippedfile, $participants, $users, $plugin, $filename)) { 297 if ($this->is_file_modified($assignment, $users, $plugin, $filename, $unzippedfile)) { 298 foreach ($users as $user) { 299 $grade = $assignment->get_user_grade($user->id, true); 300 301 // In 3.1 the default download structure of the submission files changed so that each student had their own 302 // separate folder, the files were not renamed and the folder structure was kept. It is possible that 303 // a user downloaded the submission files in 3.0 (or earlier) and edited the zip to add feedback or 304 // changed the behavior back to the previous format, the following code means that we will still support the 305 // old file structure. For more information please see - MDL-52489 / MDL-56022. 306 $path = pathinfo($filename); 307 if ($path['dirname'] == '.') { // Student submissions are not in separate folders. 308 $basename = $filename; 309 $dirname = "/"; 310 $dirnamewslash = "/"; 311 } else { 312 $basename = $path['basename']; 313 $dirname = $path['dirname']; 314 $dirnamewslash = $dirname . "/"; 315 } 316 317 if ($oldfile = $fs->get_file($contextid, 318 'assignfeedback_file', 319 ASSIGNFEEDBACK_FILE_FILEAREA, 320 $grade->id, 321 $dirname, 322 $basename)) { 323 // Update existing feedback file. 324 $oldfile->replace_file_with($unzippedfile); 325 $feedbackfilesupdated++; 326 } else { 327 // Create a new feedback file. 328 $newfilerecord = new stdClass(); 329 $newfilerecord->contextid = $contextid; 330 $newfilerecord->component = 'assignfeedback_file'; 331 $newfilerecord->filearea = ASSIGNFEEDBACK_FILE_FILEAREA; 332 $newfilerecord->filename = $basename; 333 $newfilerecord->filepath = $dirnamewslash; 334 $newfilerecord->itemid = $grade->id; 335 $fs->create_file_from_storedfile($newfilerecord, $unzippedfile); 336 $feedbackfilesadded++; 337 } 338 $userswithnewfeedback[$user->id] = 1; 339 340 // Update the number of feedback files for this user. 341 $fileplugin->update_file_count($grade); 342 343 // Update the last modified time on the grade which will trigger student notifications. 344 $assignment->notify_grade_modified($grade); 345 } 346 } 347 } 348 } 349 350 require_once($CFG->dirroot . '/mod/assign/feedback/file/renderable.php'); 351 $importsummary = new assignfeedback_file_import_summary($assignment->get_course_module()->id, 352 count($userswithnewfeedback), 353 $feedbackfilesadded, 354 $feedbackfilesupdated); 355 356 $assignrenderer = $assignment->get_renderer(); 357 $renderer = $PAGE->get_renderer('assignfeedback_file'); 358 359 $o = ''; 360 361 $o .= $assignrenderer->render(new assign_header($assignment->get_instance(), 362 $assignment->get_context(), 363 false, 364 $assignment->get_course_module()->id, 365 get_string('uploadzipsummary', 'assignfeedback_file'))); 366 367 $o .= $renderer->render($importsummary); 368 369 $o .= $assignrenderer->render_footer(); 370 return $o; 371 } 372 373 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body