From d1f8c1bd628f49957e7dddba966b7676db55bef6 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Thu, 28 Mar 2013 12:18:32 +1100 Subject: [PATCH] MDL-38708 Added course overview files (most often images) to courses --- admin/settings/appearance.php | 4 ++ backup/moodle2/backup_stepslib.php | 1 + backup/moodle2/restore_stepslib.php | 1 + course/edit.php | 7 +++ course/edit_form.php | 9 +++- course/lib.php | 54 ++++++++++++++++++++ course/renderer.php | 19 ++++++- lang/en/admin.php | 2 + lang/en/moodle.php | 4 ++ lang/en/repository.php | 1 + lib/coursecatlib.php | 47 +++++++++++++++++ lib/filebrowser/file_info_context_course.php | 35 +++++++++++++ lib/filelib.php | 4 +- theme/base/style/course.css | 9 +++- theme/formal_white/style/course.css | 4 +- theme/magazine/style/core.css | 16 ++++-- theme/splash/style/core.css | 2 +- theme/standard/style/course.css | 2 + version.php | 2 +- 19 files changed, 211 insertions(+), 12 deletions(-) diff --git a/admin/settings/appearance.php b/admin/settings/appearance.php index a11f019e99e..f9ca6d4d5d6 100644 --- a/admin/settings/appearance.php +++ b/admin/settings/appearance.php @@ -200,6 +200,10 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page new lang_string('courselistshortnames_desc', 'admin'), 0)); $temp->add(new admin_setting_configtext('coursesperpage', new lang_string('coursesperpage', 'admin'), new lang_string('configcoursesperpage', 'admin'), 20, PARAM_INT)); $temp->add(new admin_setting_configtext('courseswithsummarieslimit', new lang_string('courseswithsummarieslimit', 'admin'), new lang_string('configcourseswithsummarieslimit', 'admin'), 10, PARAM_INT)); + $temp->add(new admin_setting_configtext('courseoverviewfileslimit', new lang_string('courseoverviewfileslimit'), + new lang_string('configcourseoverviewfileslimit', 'admin'), 1, PARAM_INT)); + $temp->add(new admin_setting_configtext('courseoverviewfilesext', new lang_string('courseoverviewfilesext'), + new lang_string('configcourseoverviewfilesext', 'admin'), '.jpg,.gif,.png')); $ADMIN->add('appearance', $temp); $temp = new admin_settingpage('ajax', new lang_string('ajaxuse')); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 6d59c4b6027..d1098992360 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -511,6 +511,7 @@ class backup_course_structure_step extends backup_structure_step { $course->annotate_ids('grouping', 'defaultgroupingid'); $course->annotate_files('course', 'summary', null); + $course->annotate_files('course', 'overviewfiles', null); $course->annotate_files('course', 'legacy', null); // Return root element ($course) diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 5afd99e4a51..716d29c9475 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1459,6 +1459,7 @@ class restore_course_structure_step extends restore_structure_step { // Add course related files, without itemid to match $this->add_related_files('course', 'summary', null); + $this->add_related_files('course', 'overviewfiles', null); // Deal with legacy allowed modules. if ($this->legacyrestrictmodules) { diff --git a/course/edit.php b/course/edit.php index 409fdb1ba23..6d1150a0cc5 100644 --- a/course/edit.php +++ b/course/edit.php @@ -66,10 +66,14 @@ if ($id) { // editing course // Prepare course and the editor $editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true); +$overviewfilesoptions = course_overviewfiles_options($course); if (!empty($course)) { //add context for editor $editoroptions['context'] = $coursecontext; $course = file_prepare_standard_editor($course, 'summary', $editoroptions, $coursecontext, 'course', 'summary', 0); + if ($overviewfilesoptions) { + file_prepare_standard_filemanager($course, 'overviewfiles', $overviewfilesoptions, $coursecontext, 'course', 'overviewfiles', 0); + } // Inject current aliases $aliases = $DB->get_records('role_names', array('contextid'=>$coursecontext->id)); @@ -81,6 +85,9 @@ if (!empty($course)) { //editor should respect category context if course context is not set. $editoroptions['context'] = $catcontext; $course = file_prepare_standard_editor($course, 'summary', $editoroptions, null, 'course', 'summary', null); + if ($overviewfilesoptions) { + file_prepare_standard_filemanager($course, 'overviewfiles', $overviewfilesoptions, null, 'course', 'overviewfiles', 0); + } } // first create the form diff --git a/course/edit_form.php b/course/edit_form.php index bfa6ca6c2f6..aba81f4590b 100644 --- a/course/edit_form.php +++ b/course/edit_form.php @@ -105,9 +105,16 @@ class course_edit_form extends moodleform { $mform->addElement('editor','summary_editor', get_string('coursesummary'), null, $editoroptions); $mform->addHelpButton('summary_editor', 'coursesummary'); $mform->setType('summary_editor', PARAM_RAW); + $summaryfields = 'summary_editor'; + + if ($overviewfilesoptions = course_overviewfiles_options($course)) { + $mform->addElement('filemanager', 'overviewfiles_filemanager', get_string('courseoverviewfiles'), null, $overviewfilesoptions); + $mform->addHelpButton('overviewfiles_filemanager', 'courseoverviewfiles'); + $summaryfields .= ',overviewfiles_filemanager'; + } if (!empty($course->id) and !has_capability('moodle/course:changesummary', $coursecontext)) { - $mform->hardFreeze('summary_editor'); + $mform->hardFreeze($summaryfields); } $courseformats = get_sorted_course_formats(true); diff --git a/course/lib.php b/course/lib.php index c461ebc9f2d..6d040231d2c 100644 --- a/course/lib.php +++ b/course/lib.php @@ -2174,6 +2174,53 @@ function save_local_role_names($courseid, $data) { } } +/** + * Returns options to use in course overviewfiles filemanager + * + * @param null|stdClass|course_in_list|int $course either object that has 'id' property or just the course id; + * may be empty if course does not exist yet (course create form) + * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc. + * or null if overviewfiles are disabled + */ +function course_overviewfiles_options($course) { + global $CFG; + if (empty($CFG->courseoverviewfileslimit)) { + return null; + } + $accepted_types = preg_split('/\s*,\s*/', trim($CFG->courseoverviewfilesext), -1, PREG_SPLIT_NO_EMPTY); + if (in_array('*', $accepted_types) || empty($accepted_types)) { + $accepted_types = '*'; + } else { + // Since config for $CFG->courseoverviewfilesext is a text box, human factor must be considered. + // Make sure extensions are prefixed with dot unless they are valid typegroups + foreach ($accepted_types as $i => $type) { + if (substr($type, 0, 1) !== '.') { + require_once($CFG->libdir. '/filelib.php'); + if (!count(file_get_typegroup('extension', $type))) { + // It does not start with dot and is not a valid typegroup, this is most likely extension. + $accepted_types[$i] = '.'. $type; + $corrected = true; + } + } + } + if (!empty($corrected)) { + set_config('courseoverviewfilesext', join(',', $accepted_types)); + } + } + $options = array( + 'maxfiles' => $CFG->courseoverviewfileslimit, + 'maxbytes' => $CFG->maxbytes, + 'subdirs' => 0, + 'accepted_types' => $accepted_types + ); + if (!empty($course->id)) { + $options['context'] = context_course::instance($course->id); + } else if (is_int($course) && $course > 0) { + $options['context'] = context_course::instance($course); + } + return $options; +} + /** * Create a course and either return a $course object * @@ -2231,6 +2278,10 @@ function create_course($data, $editoroptions = NULL) { $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid)); $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid)); } + if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) { + // Save the course overviewfiles + $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); + } // update course format options course_get_format($newcourseid)->update_course_format_options($data); @@ -2285,6 +2336,9 @@ function update_course($data, $editoroptions = NULL) { if ($editoroptions) { $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); } + if ($overviewfilesoptions = course_overviewfiles_options($data->id)) { + $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); + } if (!isset($data->category) or empty($data->category)) { // prevent nulls and 0 in category field diff --git a/course/renderer.php b/course/renderer.php index 04168938416..5574e2fd1a7 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -1070,7 +1070,7 @@ class core_course_renderer extends plugin_renderer_base { // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page. $content .= html_writer::start_tag('div', array('class' => 'moreinfo')); if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { - if ($course->has_summary() || $course->has_course_contacts()) { + if ($course->has_summary() || $course->has_course_contacts() || $course->has_course_overviewfiles()) { $url = new moodle_url('/course/info.php', array('id' => $course->id)); $image = html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/info'), 'alt' => $this->strings->summary)); @@ -1126,6 +1126,23 @@ class core_course_renderer extends plugin_renderer_base { $content .= html_writer::end_tag('div'); // .summary } + // display course overview files + foreach ($course->get_course_overviewfiles() as $file) { + $isimage = $file->is_valid_image(); + $url = file_encode_url("$CFG->wwwroot/pluginfile.php", + '/'. $file->get_contextid(). '/'. $file->get_component(). '/'. + $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage); + if ($isimage) { + $content .= html_writer::tag('div', + html_writer::empty_tag('img', array('src' => $url)), + array('class' => 'courseimage')); + } else { + $content .= html_writer::tag('div', + html_writer::link($url, $file->get_filename()), + array('class' => 'coursefile')); + } + } + // display course contacts. See course_in_list::get_course_contacts() if ($course->has_course_contacts()) { $content .= html_writer::start_tag('ul', array('class' => 'teachers')); diff --git a/lang/en/admin.php b/lang/en/admin.php index 4d1bdc4e48d..c71e2972130 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -152,6 +152,8 @@ $string['configconvertformat'] = 'If latex, dvips and convertwwwroot address does not start with https:// this setting is turned off automatically.'; $string['configcountry'] = 'If you set a country here, then this country will be selected by default on new user accounts. To force users to choose a country, just leave this unset.'; +$string['configcourseoverviewfilesext'] = 'Comma-separated list of allowed course overview files extensions'; +$string['configcourseoverviewfileslimit'] = 'Limit the number of files course managers are allowed to add to the course. They will be accessible by anyone from outside of the course just like course name and/or summary'; $string['configcourserequestnotify'] = 'Type username of user to be notified when new course requested.'; $string['configcourserequestnotify2'] = 'Users who will be notified when a course is requested. Only users who can approve course requests are listed here.'; $string['configcoursesperpage'] = 'Enter the number of courses to be displayed per page in a course listing.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 5b5bc92f7dd..06e41ee09ce 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -320,6 +320,10 @@ $string['coursehelpnewsitemsnumber'] = 'Number of recent items appearing on the $string['coursehelpnumberweeks'] = 'Number of sections in the course (applies to certain course formats only).'; $string['coursehelpshowgrades'] = 'Enable the display of the gradebook. It does not prevent grades from being displayed within the individual activities.'; $string['coursehidden'] = 'This course is currently unavailable to students'; +$string['courseoverviewfiles'] = 'Course overview files'; +$string['courseoverviewfilesext'] = 'Course overview files extensions'; +$string['courseoverviewfileslimit'] = 'Course overview files limit'; +$string['courseoverviewfiles_help'] = 'Course overview files (usually images) are displayed in the list of courses together with summary'; $string['courseinfo'] = 'Course info'; $string['coursemessage'] = 'Message course users'; $string['coursenotaccessible'] = 'This course does not allow public access'; diff --git a/lang/en/repository.php b/lang/en/repository.php index f6063c59641..443eece1b1c 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -38,6 +38,7 @@ $string['sectionbackup'] = 'Section backups'; $string['activitybackup'] = 'Activity backup'; $string['areacategoryintro'] = 'Category introduction'; $string['areacourseintro'] = 'Course introduction'; +$string['areacourseoverviewfiles'] = 'Course overview files'; $string['arearoot'] = 'System'; $string['areauserdraft'] = 'Drafts'; $string['areauserbackup'] = 'User backup'; diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index 0faa0fbc1a0..874f2ac1436 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -2058,6 +2058,53 @@ class course_in_list implements IteratorAggregate { return $this->coursecontacts; } + /** + * Checks if course has any associated overview files + * + * @return bool + */ + public function has_course_overviewfiles() { + global $CFG; + if (empty($CFG->courseoverviewfileslimit)) { + return 0; + } + require_once($CFG->libdir. '/filestorage/file_storage.php'); + $fs = get_file_storage(); + $context = context_course::instance($this->id); + return $fs->is_area_empty($context->id, 'course', 'overviewfiles'); + } + + /** + * Returns all course overview files + * + * @return array array of stored_file objects + */ + public function get_course_overviewfiles() { + global $CFG; + if (empty($CFG->courseoverviewfileslimit)) { + return array(); + } + require_once($CFG->libdir. '/filestorage/file_storage.php'); + $fs = get_file_storage(); + $context = context_course::instance($this->id); + $files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false); + if (count($files)) { + $overviewfilesoptions = course_overviewfiles_options($this->id); + $acceptedtypes = $overviewfilesoptions['accepted_types']; + if ($acceptedtypes !== '*') { + // filter only files with allowed extensions + require_once($CFG->libdir. '/filelib.php'); + $files = array_filter($files, function ($file) use ($acceptedtypes) { + return file_extension_in_typegroup($file->get_filename(), $acceptedtypes);} ); + } + if (count($files) > $CFG->courseoverviewfileslimit) { + // return no more than $CFG->courseoverviewfileslimit files + $files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true); + } + } + return $files; + } + // ====== magic methods ======= public function __isset($name) { diff --git a/lib/filebrowser/file_info_context_course.php b/lib/filebrowser/file_info_context_course.php index c9d8c05e838..64fcc4b2969 100644 --- a/lib/filebrowser/file_info_context_course.php +++ b/lib/filebrowser/file_info_context_course.php @@ -120,6 +120,40 @@ class file_info_context_course extends file_info { return new file_info_stored($this->browser, $this->context, $storedfile, $urlbase, get_string('areacourseintro', 'repository'), false, true, true, false); } + /** + * Gets a stored file for the course images filearea directory + * + * @param int $itemid item ID + * @param string $filepath file path + * @param string $filename file name + * @return file_info|null file_info instance or null if not found or access not allowed + */ + protected function get_area_course_overviewfiles($itemid, $filepath, $filename) { + global $CFG; + + if (!has_capability('moodle/course:update', $this->context)) { + return null; + } + if (is_null($itemid)) { + return $this; + } + + $fs = get_file_storage(); + + $filepath = is_null($filepath) ? '/' : $filepath; + $filename = is_null($filename) ? '.' : $filename; + if (!$storedfile = $fs->get_file($this->context->id, 'course', 'overviewfiles', 0, $filepath, $filename)) { + if ($filepath === '/' and $filename === '.') { + $storedfile = new virtual_root_file($this->context->id, 'course', 'overviewfiles', 0); + } else { + // not found + return null; + } + } + $urlbase = $CFG->wwwroot.'/pluginfile.php'; + return new file_info_stored($this->browser, $this->context, $storedfile, $urlbase, get_string('areacourseoverviewfiles', 'repository'), false, true, true, false); + } + /** * Gets a stored file for the course section filearea directory * @@ -365,6 +399,7 @@ class file_info_context_course extends file_info { private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { $areas = array( array('course', 'summary'), + array('course', 'overviewfiles'), array('course', 'section'), array('backup', 'section'), array('backup', 'course'), diff --git a/lib/filelib.php b/lib/filelib.php index 47b8c1cec80..36589fcdceb 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -4075,14 +4075,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { send_file_not_found(); } - if ($filearea === 'summary') { + if ($filearea === 'summary' || $filearea === 'overviewfiles') { if ($CFG->forcelogin) { require_login(); } $filename = array_pop($args); $filepath = $args ? '/'.implode('/', $args).'/' : '/'; - if (!$file = $fs->get_file($context->id, 'course', 'summary', 0, $filepath, $filename) or $file->is_directory()) { + if (!$file = $fs->get_file($context->id, 'course', $filearea, 0, $filepath, $filename) or $file->is_directory()) { send_file_not_found(); } diff --git a/theme/base/style/course.css b/theme/base/style/course.css index 2cf18074e63..786d26ef3ed 100644 --- a/theme/base/style/course.css +++ b/theme/base/style/course.css @@ -116,7 +116,9 @@ li.section.hidden span.commands a.editing_show {cursor:default;} .coursebox .name a {display:block;background-image:url([[pix:moodle|i/course]]);background-repeat: no-repeat;padding-left:21px;background-position: center left;} .coursebox.remotehost .name a {background-image:url([[pix:moodle|i/mnethost]]);} .coursebox .name, -.coursebox .teachers {float:left;width: 40%;} +.coursebox .teachers, +.coursebox .content .courseimage, +.coursebox .content .coursefile {float:left;width:40%;clear:left;} .coursebox .teachers li {list-style-type:none;padding:0;margin:0;} .coursebox .enrolmenticons {padding:3px 0;float:right;} .coursebox .moreinfo {padding:3px 0;float:right;} @@ -127,10 +129,13 @@ li.section.hidden span.commands a.editing_show {cursor:default;} .coursebox .coursecat {float:right;width: 55%;} .coursebox .coursecat {text-align:right;clear:right;} .coursebox.remotecoursebox .remotecourseinfo {float:left;width: 40%;} +.coursebox .content .courseimage img {max-width:100px;max-height:100px;} .dir-rtl .coursebox .name a {padding-left:0;padding-right:21px;background-position: center right;} .dir-rtl .coursebox .name, -.dir-rtl .coursebox .teachers {float:right;} +.dir-rtl .coursebox .teachers, +.dir-rtl .coursebox .content .courseimage, +.dir-rtl .coursebox .content .coursefile {float:right;clear:right;} .dir-rtl .coursebox .enrolmenticons, .dir-rtl .coursebox .moreinfo {float:left;} .dir-rtl .coursebox .summary, diff --git a/theme/formal_white/style/course.css b/theme/formal_white/style/course.css index d00b394fbaa..ffcbce66a67 100644 --- a/theme/formal_white/style/course.css +++ b/theme/formal_white/style/course.css @@ -17,7 +17,9 @@ .coursebox .content {font-size:90%;} .coursebox .teachers {margin:5px 1em;} .coursebox .summary, -.coursebox .coursecat {margin:5px;} +.coursebox .coursecat, +.coursebox .content .courseimage, +.coursebox .content .coursefile {margin:5px;} .coursebox > .content:after, .coursebox > .info:after {clear:both;content:".";display:block;height:0;min-width:0;visibility:hidden;} diff --git a/theme/magazine/style/core.css b/theme/magazine/style/core.css index 6643e4f4474..b0e173d7a6a 100644 --- a/theme/magazine/style/core.css +++ b/theme/magazine/style/core.css @@ -915,15 +915,25 @@ div.coursebox h3.name a { padding-left: 5px; } -.dir-rtl .coursebox ul.teachers { +.coursebox .content .courseimage, +.coursebox .content .coursefile { + padding-left: 5px; +} + +.dir-rtl .coursebox ul.teachers, +.dir-rtl .coursebox .content .courseimage, +.dir-rtl .coursebox .content .coursefile { padding-right: 5px; } -.coursebox ul.teachers li { +.coursebox ul.teachers li, +.coursebox .coursecat { font-size: 10px; } -.coursebox ul.teachers li a { +.coursebox ul.teachers li a, +.coursebox .coursecat a, +.coursebox .content .coursefile a { font-size: 11px; } diff --git a/theme/splash/style/core.css b/theme/splash/style/core.css index 407e0af7096..73d7791ddb9 100644 --- a/theme/splash/style/core.css +++ b/theme/splash/style/core.css @@ -353,7 +353,7 @@ li.activity { padding-bottom: 5px; } .coursebox .summary{ - padding: 10px 0; + padding: 0 0 10px 0; } .que .info{ width: 10em; diff --git a/theme/standard/style/course.css b/theme/standard/style/course.css index bd34ece6f1e..662f5834791 100644 --- a/theme/standard/style/course.css +++ b/theme/standard/style/course.css @@ -82,6 +82,8 @@ .coursebox .teachers {padding:3px 0 3px 21px;margin:0;font-size:0.9em;} .coursebox .coursecat, .coursebox .summary, +.coursebox .courseimage, +.coursebox .coursefile, .coursebox.remotecoursebox .remotecourseinfo {padding:3px 5px;font-size:0.9em;} .dir-rtl .coursebox .teachers {padding:3px 21px 3px 0;} diff --git a/version.php b/version.php index 4655a2d7433..21805e63236 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2013032800.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2013032900.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes