$data->availability = null;
}
}
- $DB->update_record('course_sections', $data);
- rebuild_course_cache($course->id, true);
- if (isset($data->section)) {
- // Usually edit form does not change relative section number but just in case.
- $sectionnum = $data->section;
- }
- course_get_format($course->id)->update_section_format_options($data);
-
- // Set section info, as this might not be present in form_data.
- if (!isset($data->section)) {
- $data->section = $sectionnum;
- }
- // Trigger an event for course section update.
- $event = \core\event\course_section_updated::create(
- array(
- 'objectid' => $data->id,
- 'courseid' => $course->id,
- 'context' => $context,
- 'other' => array('sectionnum' => $data->section)
- )
- );
- $event->trigger();
+ course_update_section($course, $section, $data);
$PAGE->navigation->clear_cache();
redirect(course_get_url($course, $section, array('sr' => $sectionreturn)));
return true;
}
+
+ /**
+ * Prepares the templateable object to display section name
+ *
+ * @param \section_info|\stdClass $section
+ * @param bool $linkifneeded
+ * @param bool $editable
+ * @param null|lang_string|string $edithint
+ * @param null|lang_string|string $editlabel
+ * @return \core\output\inplace_editable
+ */
+ public function inplace_editable_render_section_name($section, $linkifneeded = true,
+ $editable = null, $edithint = null, $editlabel = null) {
+ global $USER, $CFG;
+ require_once($CFG->dirroot.'/course/lib.php');
+
+ if ($editable === null) {
+ $editable = !empty($USER->editing) && has_capability('moodle/course:update',
+ context_course::instance($section->course));
+ }
+
+ $displayvalue = $title = get_section_name($section->course, $section);
+ if ($linkifneeded) {
+ // Display link under the section name if the course format setting is to display one section per page.
+ $url = course_get_url($section->course, $section->section, array('navigation' => true));
+ if ($url) {
+ $displayvalue = html_writer::link($url, $title);
+ }
+ $itemtype = 'sectionname';
+ } else {
+ // If $linkifneeded==false, we never display the link (this is used when rendering the section header).
+ // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered -
+ // there is no other way callback can know where we display the section name.
+ $itemtype = 'sectionnamenl';
+ }
+ if (empty($edithint)) {
+ $edithint = new lang_string('editsectionname');
+ }
+ if (empty($editlabel)) {
+ $editlabel = new lang_string('newsectionname', '', $title);
+ }
+
+ return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable,
+ $displayvalue, $section->name, $edithint, $editlabel);
+ }
+
+ /**
+ * Updates the value in the database and modifies this object respectively.
+ *
+ * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient
+ * or value is not legit.
+ *
+ * @param stdClass $section
+ * @param string $itemtype
+ * @param mixed $newvalue
+ * @return \core\output\inplace_editable
+ */
+ public function inplace_editable_update_section_name($section, $itemtype, $newvalue) {
+ if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
+ require_login($section->course, false, null, true, true);
+ $context = context_course::instance($section->course);
+ require_capability('moodle/course:update', $context);
+
+ $newtitle = clean_param($newvalue, PARAM_TEXT);
+ if (strval($section->name) !== strval($newtitle)) {
+ course_update_section($section->course, $section, array('name' => $newtitle));
+ }
+ return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true);
+ }
+ }
}
/**
abstract protected function page_title();
/**
- * Generate the section title
+ * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page
*
* @param stdClass $section The course_section entry from DB
* @param stdClass $course The course entry from DB
return $title;
}
+ /**
+ * Generate the section title to be displayed on the section page, without a link
+ *
+ * @param stdClass $section The course_section entry from DB
+ * @param stdClass $course The course entry from DB
+ * @return string HTML to output.
+ */
+ public function section_title_without_link($section, $course) {
+ return get_section_name($course, $section);
+ }
+
/**
* Generate the edit control action menu
*
'aria-label'=> get_section_name($course, $section)));
// Create a span that contains the section title to be used to create the keyboard section move menu.
- $o .= html_writer::tag('span', $this->section_title($section, $course), array('class' => 'hidden sectionname'));
+ $o .= html_writer::tag('span', get_section_name($course, $section), array('class' => 'hidden sectionname'));
$leftcontent = $this->section_left_content($section, $course, $onsectionpage);
$o.= html_writer::tag('div', $leftcontent, array('class' => 'left side'));
if (!$thissection->visible) {
$classes .= ' dimmed_text';
}
- $sectionname = html_writer::tag('span', get_section_name($course, $displaysection));
+ $sectionname = html_writer::tag('span', $this->section_title_without_link($thissection, $course));
$sectiontitle .= $this->output->heading($sectionname, 3, $classes);
$sectiontitle .= html_writer::end_tag('div');
$string['currentsection'] = 'This topic';
$string['editsection'] = 'Edit topic';
+$string['editsectionname'] = 'Edit topic name';
$string['deletesection'] = 'Delete topic';
+$string['newsectionname'] = 'New name for topic {$a}';
$string['sectionname'] = 'Topic';
$string['pluginname'] = 'Topics format';
$string['section0name'] = 'General';
public function can_delete_section($section) {
return true;
}
+
+ /**
+ * Prepares the templateable object to display section name
+ *
+ * @param \section_info|\stdClass $section
+ * @param bool $linkifneeded
+ * @param bool $editable
+ * @param null|lang_string|string $edithint
+ * @param null|lang_string|string $editlabel
+ * @return \core\output\inplace_editable
+ */
+ public function inplace_editable_render_section_name($section, $linkifneeded = true,
+ $editable = null, $edithint = null, $editlabel = null) {
+ if (empty($edithint)) {
+ $edithint = new lang_string('editsectionname', 'format_topics');
+ }
+ if (empty($editlabel)) {
+ $title = get_section_name($section->course, $section);
+ $editlabel = new lang_string('newsectionname', 'format_topics', $title);
+ }
+ return parent::inplace_editable_render_section_name($section, $linkifneeded, $editable, $edithint, $editlabel);
+ }
+}
+
+/**
+ * Implements callback inplace_editable() allowing to edit values in-place
+ *
+ * @param string $itemtype
+ * @param int $itemid
+ * @param mixed $newvalue
+ * @return \core\output\inplace_editable
+ */
+function format_topics_inplace_editable($itemtype, $itemid, $newvalue) {
+ global $DB, $CFG;
+ require_once($CFG->dirroot . '/course/lib.php');
+ if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
+ $section = $DB->get_record_sql(
+ 'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?',
+ array($itemid, 'topics'), MUST_EXIST);
+ return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue);
+ }
}
return get_string('topicoutline');
}
+ /**
+ * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page
+ *
+ * @param stdClass $section The course_section entry from DB
+ * @param stdClass $course The course entry from DB
+ * @return string HTML to output.
+ */
+ public function section_title($section, $course) {
+ return $this->render(course_get_format($course)->inplace_editable_render_section_name($section));
+ }
+
+ /**
+ * Generate the section title to be displayed on the section page, without a link
+ *
+ * @param stdClass $section The course_section entry from DB
+ * @param stdClass $course The course entry from DB
+ * @return string HTML to output.
+ */
+ public function section_title_without_link($section, $course) {
+ return $this->render(course_get_format($course)->inplace_editable_render_section_name($section, false));
+ }
+
/**
* Generate the edit control items of a section
*
Then I should see "This is the second topic" in the "li#section-2" "css_element"
And I should not see "Topic 2" in the "li#section-2" "css_element"
+ @javascript
+ Scenario: Inline edit section name in topics format
+ When I click on "Edit topic name" "link" in the "li#section-1" "css_element"
+ And I set the field "New name for topic Topic 1" to "Midterm evaluation"
+ And I press key "13" in the field "New name for topic Topic 1"
+ Then I should not see "Topic 1" in the "#region-main" "css_element"
+ And "New name for topic" "field" should not exist
+ And I should see "Midterm evaluation" in the "li#section-1" "css_element"
+ And I follow "Course 1"
+ And I should not see "Topic 1" in the "#region-main" "css_element"
+ And I should see "Midterm evaluation" in the "li#section-1" "css_element"
+
Scenario: Deleting the last section in topics format
When I delete section "5"
Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
}
}
}
+
+ /**
+ * Test web service updating section name
+ */
+ public function test_update_inplace_editable() {
+ global $CFG, $DB, $PAGE;
+ require_once($CFG->dirroot . '/lib/external/externallib.php');
+
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'topics'),
+ array('createsections' => true));
+ $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+
+ // Call webservice without necessary permissions.
+ try {
+ core_external::update_inplace_editable('format_topics', 'sectionname', $section->id, 'New section name');
+ $this->fail('Exception expected');
+ } catch (moodle_exception $e) {
+ $this->assertEquals('Course or activity not accessible. (Not enrolled)',
+ $e->getMessage());
+ }
+
+ // Change to teacher and make sure that section name can be updated using web service update_inplace_editable().
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
+
+ $res = core_external::update_inplace_editable('format_topics', 'sectionname', $section->id, 'New section name');
+ $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+ $this->assertEquals('New section name', $res['value']);
+ $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+ }
+
+ /**
+ * Test callback updating section name
+ */
+ public function test_inplace_editable() {
+ global $DB, $PAGE;
+
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'topics'),
+ array('createsections' => true));
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
+ $this->setUser($user);
+
+ $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+
+ // Call callback format_topics_inplace_editable() directly.
+ $tmpl = component_callback('format_topics', 'inplace_editable', array('sectionname', $section->id, 'Rename me again'));
+ $this->assertInstanceOf('core\output\inplace_editable', $tmpl);
+ $res = $tmpl->export_for_template($PAGE->get_renderer('core'));
+ $this->assertEquals('Rename me again', $res['value']);
+ $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+
+ // Try updating using callback from mismatching course format.
+ try {
+ $tmpl = component_callback('format_weeks', 'inplace_editable', array('sectionname', $section->id, 'New name'));
+ $this->fail('Exception expected');
+ } catch (moodle_exception $e) {
+ $this->assertEquals(1, preg_match('/^Can not find data record in database/', $e->getMessage()));
+ }
+ }
}
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
+=== 3.1 ===
+* Course format may use the inplace_editable template to allow quick editing of section names, see
+ https://docs.moodle.org/dev/Inplace_editable and MDL-51802 for example implementation.
+
=== 3.0 ===
* Course formats should now use section_edit_control_items and use the returned array of controls items and their attributes to create a
renderable menu or array of links. Plugin calls to section_edit_controls will now include the section edit control in the returned array.
$string['currentsection'] = 'This week';
$string['editsection'] = 'Edit week';
+$string['editsectionname'] = 'Edit week name';
$string['deletesection'] = 'Delete week';
+$string['newsectionname'] = 'New name for week {$a}';
$string['sectionname'] = 'Week';
$string['pluginname'] = 'Weekly format';
$string['section0name'] = 'General';
public function can_delete_section($section) {
return true;
}
+
+ /**
+ * Prepares the templateable object to display section name
+ *
+ * @param \section_info|\stdClass $section
+ * @param bool $linkifneeded
+ * @param bool $editable
+ * @param null|lang_string|string $edithint
+ * @param null|lang_string|string $editlabel
+ * @return \core\output\inplace_editable
+ */
+ public function inplace_editable_render_section_name($section, $linkifneeded = true,
+ $editable = null, $edithint = null, $editlabel = null) {
+ if (empty($edithint)) {
+ $edithint = new lang_string('editsectionname', 'format_weeks');
+ }
+ if (empty($editlabel)) {
+ $title = get_section_name($section->course, $section);
+ $editlabel = new lang_string('newsectionname', 'format_weeks', $title);
+ }
+ return parent::inplace_editable_render_section_name($section, $linkifneeded, $editable, $edithint, $editlabel);
+ }
+}
+
+/**
+ * Implements callback inplace_editable() allowing to edit values in-place
+ *
+ * @param string $itemtype
+ * @param int $itemid
+ * @param mixed $newvalue
+ * @return \core\output\inplace_editable
+ */
+function format_weeks_inplace_editable($itemtype, $itemid, $newvalue) {
+ global $DB, $CFG;
+ require_once($CFG->dirroot . '/course/lib.php');
+ if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
+ $section = $DB->get_record_sql(
+ 'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?',
+ array($itemid, 'weeks'), MUST_EXIST);
+ return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue);
+ }
}
protected function page_title() {
return get_string('weeklyoutline');
}
+
+ /**
+ * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page
+ *
+ * @param stdClass $section The course_section entry from DB
+ * @param stdClass $course The course entry from DB
+ * @return string HTML to output.
+ */
+ public function section_title($section, $course) {
+ return $this->render(course_get_format($course)->inplace_editable_render_section_name($section));
+ }
+
+ /**
+ * Generate the section title to be displayed on the section page, without a link
+ *
+ * @param stdClass $section The course_section entry from DB
+ * @param stdClass $course The course entry from DB
+ * @return string HTML to output.
+ */
+ public function section_title_without_link($section, $course) {
+ return $this->render(course_get_format($course)->inplace_editable_render_section_name($section, false));
+ }
}
Then I should see "This is the second week" in the "li#section-2" "css_element"
And I should not see "8 May - 14 May" in the "li#section-2" "css_element"
+ @javascript
+ Scenario: Inline edit section name in weeks format
+ When I click on "Edit week name" "link" in the "li#section-1" "css_element"
+ And I set the field "New name for week 1 May - 7 May" to "Midterm evaluation"
+ And I press key "13" in the field "New name for week 1 May - 7 May"
+ Then I should not see "1 May - 7 May" in the "#region-main" "css_element"
+ And "New name for week" "field" should not exist
+ And I should see "Midterm evaluation" in the "li#section-1" "css_element"
+ And I follow "Course 1"
+ And I should not see "1 May - 7 May" in the "#region-main" "css_element"
+ And I should see "Midterm evaluation" in the "li#section-1" "css_element"
+
Scenario: Deleting the last section in weeks format
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
When I delete section "5"
}
}
}
+
+ /**
+ * Test web service updating section name
+ */
+ public function test_update_inplace_editable() {
+ global $CFG, $DB, $PAGE;
+ require_once($CFG->dirroot . '/lib/external/externallib.php');
+
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'weeks'),
+ array('createsections' => true));
+ $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+
+ // Call webservice without necessary permissions.
+ try {
+ core_external::update_inplace_editable('format_weeks', 'sectionname', $section->id, 'New section name');
+ $this->fail('Exception expected');
+ } catch (moodle_exception $e) {
+ $this->assertEquals('Course or activity not accessible. (Not enrolled)',
+ $e->getMessage());
+ }
+
+ // Change to teacher and make sure that section name can be updated using web service update_inplace_editable().
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
+
+ $res = core_external::update_inplace_editable('format_weeks', 'sectionname', $section->id, 'New section name');
+ $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+ $this->assertEquals('New section name', $res['value']);
+ $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+ }
+
+ /**
+ * Test callback updating section name
+ */
+ public function test_inplace_editable() {
+ global $CFG, $DB, $PAGE;
+
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'weeks'),
+ array('createsections' => true));
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
+ $this->setUser($user);
+
+ $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+
+ // Call callback format_weeks_inplace_editable() directly.
+ $tmpl = component_callback('format_weeks', 'inplace_editable', array('sectionname', $section->id, 'Rename me again'));
+ $this->assertInstanceOf('core\output\inplace_editable', $tmpl);
+ $res = $tmpl->export_for_template($PAGE->get_renderer('core'));
+ $this->assertEquals('Rename me again', $res['value']);
+ $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+
+ // Try updating using callback from mismatching course format.
+ try {
+ $tmpl = component_callback('format_topics', 'inplace_editable', array('sectionname', $section->id, 'New name'));
+ $this->fail('Exception expected');
+ } catch (moodle_exception $e) {
+ $this->assertEquals(1, preg_match('/^Can not find data record in database/', $e->getMessage()));
+ }
+ }
}
$resourcestotoggle = array();
if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
- $DB->set_field("course_sections", "visible", "$visibility", array("id"=>$section->id));
-
- $event = \core\event\course_section_updated::create(array(
- 'context' => context_course::instance($courseid),
- 'objectid' => $section->id,
- 'other' => array(
- 'sectionnum' => $sectionnumber
- )
- ));
- $event->add_record_snapshot('course_sections', $section);
- $event->trigger();
-
- if (!empty($section->sequence)) {
- $modules = explode(",", $section->sequence);
- foreach ($modules as $moduleid) {
- if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
- if ($visibility) {
- // As we unhide the section, we use the previously saved visibility stored in visibleold.
- set_coursemodule_visible($moduleid, $cm->visibleold);
- } else {
- // We hide the section, so we hide the module but we store the original state in visibleold.
- set_coursemodule_visible($moduleid, 0);
- $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
- }
- \core\event\course_module_updated::create_from_cm($cm)->trigger();
- }
- }
- }
- rebuild_course_cache($courseid, true);
+ course_update_section($courseid, $section, array('visible' => $visibility));
// Determine which modules are visible for AJAX update
+ $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
if (!empty($modules)) {
list($insql, $params) = $DB->get_in_or_equal($modules);
$select = 'id ' . $insql . ' AND visible = ?';
return $result;
}
+/**
+ * Updates the course section
+ *
+ * This function does not check permissions or clean values - this has to be done prior to calling it.
+ *
+ * @param int|stdClass $course
+ * @param stdClass $section record from course_sections table - it will be updated with the new values
+ * @param array|stdClass $data
+ */
+function course_update_section($course, $section, $data) {
+ global $DB;
+
+ $courseid = (is_object($course)) ? $course->id : (int)$course;
+
+ // Some fields can not be updated using this method.
+ $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence'));
+ $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible);
+ if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) {
+ throw new moodle_exception('maximumchars', 'moodle', '', 255);
+ }
+
+ // Update record in the DB and course format options.
+ $data['id'] = $section->id;
+ $DB->update_record('course_sections', $data);
+ rebuild_course_cache($courseid, true);
+ course_get_format($courseid)->update_section_format_options($data);
+
+ // Update fields of the $section object.
+ foreach ($data as $key => $value) {
+ if (property_exists($section, $key)) {
+ $section->$key = $value;
+ }
+ }
+
+ // Trigger an event for course section update.
+ $event = \core\event\course_section_updated::create(
+ array(
+ 'objectid' => $section->id,
+ 'courseid' => $courseid,
+ 'context' => context_course::instance($courseid),
+ 'other' => array('sectionnum' => $section->section)
+ )
+ );
+ $event->trigger();
+
+ // If section visibility was changed, hide the modules in this section too.
+ if ($changevisibility && !empty($section->sequence)) {
+ $modules = explode(',', $section->sequence);
+ foreach ($modules as $moduleid) {
+ if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
+ if ($data['visible']) {
+ // As we unhide the section, we use the previously saved visibility stored in visibleold.
+ set_coursemodule_visible($moduleid, $cm->visibleold);
+ } else {
+ // We hide the section, so we hide the module but we store the original state in visibleold.
+ set_coursemodule_visible($moduleid, 0);
+ $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
+ }
+ \core\event\course_module_updated::create_from_cm($cm)->trigger();
+ }
+ }
+ }
+}
+
/**
* Checks if the current user can delete a section (if course format allows it and user has proper permissions).
*
$string['invalidcourse'] = 'Invalid course';
$string['invalidcourseid'] = 'You are trying to use an invalid course ID';
$string['invalidcourselevel'] = 'Incorrect context level';
+$string['invalidcourseformat'] = 'Invalid course format';
$string['invalidcoursemodule'] = 'Invalid course module ID';
$string['invalidcoursenameshort'] = 'Invalid short course name';
$string['invaliddata'] = 'Data submitted is invalid';
$string['editorsettings'] = 'Editor settings';
$string['editorshortcutkeys'] = 'Editor shortcut keys';
$string['editsection'] = 'Edit section';
+$string['editsectionname'] = 'Edit section name';
$string['editsummary'] = 'Edit summary';
$string['edittitle'] = 'Edit title';
$string['edittitleinstructions'] = 'Escape to cancel, Enter when finished';
{$a->signoff}';
$string['newpicture'] = 'New picture';
$string['newpicture_help'] = 'To add a new picture, browse and select an image (in JPG or PNG format) then click "Update profile". The image will be cropped to a square and resized to 100x100 pixels.';
+$string['newsectionname'] = 'New name for section {$a}';
$string['newsitem'] = 'news item';
$string['newsitems'] = 'news items';
$string['newsitemsnumber'] = 'News items to show';