Merge branch 'wip-MDL-51802-master4' of https://github.com/marinaglancy/moodle
This commit is contained in:
+1
-22
@@ -114,28 +114,7 @@ if ($mform->is_cancelled()){
|
||||
$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)));
|
||||
|
||||
@@ -1030,6 +1030,76 @@ abstract class format_base {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
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
|
||||
@@ -86,6 +86,17 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
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
|
||||
*
|
||||
@@ -193,7 +204,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
'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'));
|
||||
@@ -788,7 +799,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
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');
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
|
||||
$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';
|
||||
|
||||
@@ -382,4 +382,45 @@ class format_topics extends format_base {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,28 @@ class format_topics_renderer extends format_section_renderer_base {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -56,6 +56,18 @@ Feature: Sections can be edited and deleted in topics format
|
||||
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?"
|
||||
|
||||
@@ -146,4 +146,69 @@ class format_topics_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ This files describes API changes for course formats
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
|
||||
$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';
|
||||
|
||||
@@ -432,4 +432,45 @@ class format_weeks extends format_base {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,26 @@ class format_weeks_renderer extends format_section_renderer_base {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ Feature: Sections can be edited and deleted in weeks format
|
||||
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"
|
||||
|
||||
@@ -152,4 +152,69 @@ class format_weeks_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-29
@@ -1198,37 +1198,10 @@ function set_section_visible($courseid, $sectionnumber, $visibility) {
|
||||
|
||||
$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 = ?';
|
||||
@@ -1877,6 +1850,70 @@ function course_delete_section($course, $section, $forcedeleteifnotempty = true)
|
||||
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).
|
||||
*
|
||||
|
||||
@@ -278,6 +278,7 @@ $string['idnumbertaken'] = 'This ID number is already in use';
|
||||
$string['idnumbertoolong'] = 'ID number is too long';
|
||||
$string['importformatnotimplement'] = 'Sorry, importing this format is not yet implemented!';
|
||||
$string['incorrectext'] = 'File has an incorrect extension';
|
||||
$string['inplaceeditableerror'] = 'Error calling update processor';
|
||||
$string['installproblem'] = 'It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix if you want to retry the installation.';
|
||||
$string['internalauthpassworderror'] = 'Missing password or invalid password policy for internal authentication';
|
||||
$string['invalidaccess'] = 'This page was not accessed correctly';
|
||||
@@ -302,6 +303,7 @@ $string['invalidcontext'] = 'Invalid context';
|
||||
$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';
|
||||
|
||||
@@ -557,6 +557,7 @@ $string['editorresettodefaults'] = 'Reset to default values';
|
||||
$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';
|
||||
@@ -1261,6 +1262,7 @@ Cheers from the \'{$a->sitename}\' administrator,
|
||||
{$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';
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return a("body").on("click keypress","[data-inplaceeditable] [data-inplaceeditablelink]",function(g){if("keypress"!==g.type||13===g.keyCode){g.stopImmediatePropagation(),g.preventDefault();var h=a(this),i=h.closest("[data-inplaceeditable]"),j=function(e,f){var g=b.call([{methodname:"core_update_inplace_editable",args:{itemid:e.attr("data-itemid"),component:e.attr("data-component"),itemtype:e.attr("data-itemtype"),value:f}}],!0);a.when.apply(a,g).done(function(a){var b=e.attr("data-value");c.render("core/inplace_editable",a).done(function(a,b){c.replaceNode(e,a,b),e.find("[data-inplaceeditablelink]").focus()}),e.trigger({type:"updated",ajaxreturn:a,oldvalue:b})}).fail(function(b){var c=a.Event("updatefailed",{exception:b,newvalue:f});e.trigger(c),c.isDefaultPrevented()||d.exception(b)})},k=function(a){var b=a.find("input");b.off(),a.html(a.attr("data-oldcontent")),a.removeAttr("data-oldcontent"),a.removeClass("inplaceeditingon")},l=function(){a("span.inplaceeditable.inplaceeditingon").each(function(){k(a(this))})},m=function(b,c){for(var d=b,e=0;c>e;e++)d+=String(Math.floor(10*Math.random()));return 0===a("#"+d).length?d:m(b,c)},n=function(b){b.addClass("inplaceeditingon"),b.attr("data-oldcontent",b.html()),e.get_string("edittitleinstructions").done(function(c){var d=a('<span class="editinstructions">'+c+"</span>").attr("id",m("id_editinstructions_",20)),e=a('<input type="text"/>').attr("id",m("id_inplacevalue_",20)).attr("value",b.attr("data-value")).attr("aria-describedby",d.attr("id")),g=a('<label class="accesshide">'+i.attr("data-editlabel")+"</label>").attr("for",e.attr("id"));b.html("").append(d).append(g).append(e),e.focus(),e.select(),e.on("keyup keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(j(b,e.val()),k(b)),("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&k(b))})})};l(),n(i)}}),{}});
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{init_tagindex_page:function(){a("body").delegate(".tagarea[data-ta] a[data-quickload=1]","click",function(d){d.preventDefault();var e=a(this),f=e.context.search.replace(/^\?/,""),g=e.closest(".tagarea[data-ta]"),h=f.split("&").reduce(function(a,b){var c=b.split("=");return a[c[0]]=decodeURIComponent(c[1]),a},{}),i=b.call([{methodname:"core_tag_get_tagindex",args:{tagindex:h}}],!0);a.when.apply(a,i).done(function(a){c.render("core_tag/index",a).done(function(a){g.replaceWith(a)})})})},init_manage_page:function(){var g=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagisstandard","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,isstandard:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagisstandard",b.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagisstandard").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var f=e.closest("tr").get(0);f&&(d.tags[0].flag?a(f).addClass("flagged-tag"):a(f).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1}),a(".tag-management-table").delegate(".tagnameedit","click keypress",function(h){if("keypress"!==h.type||13===h.keyCode){h.stopImmediatePropagation(),h.preventDefault();var i=a(this),j=a(i.closest("td").get(0)),k=a(j.find("input").get(0)),l=i.attr("data-id"),m=function(f,h){var i=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,rawname:h}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:f}]}}],!0);a.when.apply(a,i).done(function(b,f){void 0!==b.warnings[0]?e.get_string("error").done(function(a){d.alert(a,b.warnings[0].message)}):void 0!==f.tags[0]&&c.render("core_tag/tagname",f.tags[0]).done(function(b){g(j),j.html(b),a(j.find(".tagnameedit").get(0)).focus()})})},n=function(){a(".tag-management-table td.tageditingon").each(function(){var b=a(this),c=a(b.find("input").get(0));c.off(),b.removeClass("tageditingon"),c.val(b.attr("data-value"))})};n(),j.addClass("tageditingon"),j.attr("data-value",k.val()),k.select(),k.on("keypress focusout",function(a){f.behatsiterunning&&"focusout"===a.type||("keypress"===a.type&&13===a.keyCode&&(m(l,k.val()),n()),("keypress"===a.type&&27===a.keyCode||"focusout"===a.type)&&n())})}})}}});
|
||||
define(["jquery","core/ajax","core/templates","core/notification","core/str"],function(a,b,c,d,e){return{init_tagindex_page:function(){a("body").delegate(".tagarea[data-ta] a[data-quickload=1]","click",function(d){d.preventDefault();var e=a(this),f=e.context.search.replace(/^\?/,""),g=e.closest(".tagarea[data-ta]"),h=f.split("&").reduce(function(a,b){var c=b.split("=");return a[c[0]]=decodeURIComponent(c[1]),a},{}),i=b.call([{methodname:"core_tag_get_tagindex",args:{tagindex:h}}],!0);a.when.apply(a,i).done(function(a){c.render("core_tag/index",a).done(function(a){g.replaceWith(a)})})})},init_manage_page:function(){var f=function(b){var c=b.closest("tr").get(0);if(c){var d=a(c).find("td.col-timemodified").get(0);e.get_string("now").done(function(b){a(d).html(b)})}};a(".tag-management-table").delegate(".tagisstandard","click",function(d){d.preventDefault();var e=a(this),g=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:g,isstandard:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:g}]}}],!0);a.when.apply(a,j).done(function(a,b){void 0===a.warnings[0]&&void 0!==b.tags[0]&&c.render("core_tag/tagisstandard",b.tags[0]).done(function(a){f(e);var b=e.parent();e.replaceWith(a),b.find(".tagisstandard").get(0).focus()})})}),a(".tag-management-table").delegate(".tagflag","click",function(d){d.preventDefault();var e=a(this),g=e.attr("data-id"),h=e.attr("data-value"),i="0"===h?1:0,j=b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:g,flag:i}]}},{methodname:"core_tag_get_tags",args:{tags:[{id:g}]}}],!0);a.when.apply(a,j).done(function(b,d){if(void 0===b.warnings[0]&&void 0!==d.tags[0]){var g=e.closest("tr").get(0);g&&(d.tags[0].flag?a(g).addClass("flagged-tag"):a(g).removeClass("flagged-tag")),c.render("core_tag/tagflag",d.tags[0]).done(function(a){f(e);var b=e.parent();e.replaceWith(a),b.find(".tagflag").get(0).focus()})}})}),a(".tag-management-table").delegate("a.tagdelete","click",function(b){b.preventDefault();var c=a(this).attr("href");e.get_strings([{key:"delete"},{key:"confirmdeletetag",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){window.location.href=c})})}),a("#tag-management-delete").click(function(b){var c=a(this).closest("form").get(0),f=a(c).find("input[type=checkbox]:checked").length;return f?(b.preventDefault(),void e.get_strings([{key:"delete"},{key:"confirmdeletetags",component:"tag"},{key:"yes"},{key:"no"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){c.submit()})})):!1})}}});
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* AJAX helper for the inline editing a value.
|
||||
*
|
||||
* This script is automatically included from template core/inplace_editable
|
||||
* It registers a click-listener on [data-inplaceeditablelink] link (the "inplace edit" icon),
|
||||
* then replaces the displayed value with an input field. On "Enter" it sends a request
|
||||
* to web service core_update_inplace_editable, which invokes the specified callback.
|
||||
* Any exception thrown by the web service (or callback) is displayed as an error popup.
|
||||
*
|
||||
* @module core/inplace_editable
|
||||
* @package core
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.1
|
||||
*/
|
||||
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config'],
|
||||
function($, ajax, templates, notification, str, cfg) {
|
||||
|
||||
$('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
|
||||
if (e.type === 'keypress' && e.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
var target = $(this),
|
||||
mainelement = target.closest('[data-inplaceeditable]');
|
||||
|
||||
var update_value = function(mainelement, value) {
|
||||
var promises = ajax.call([{
|
||||
methodname: 'core_update_inplace_editable',
|
||||
args: { itemid : mainelement.attr('data-itemid'),
|
||||
component : mainelement.attr('data-component') ,
|
||||
itemtype : mainelement.attr('data-itemtype') ,
|
||||
value : value }
|
||||
}], true);
|
||||
|
||||
$.when.apply($, promises)
|
||||
.done( function(data) {
|
||||
var oldvalue = mainelement.attr('data-value');
|
||||
templates.render('core/inplace_editable', data).done(function(html, js) {
|
||||
templates.replaceNode(mainelement, html, js);
|
||||
mainelement.find('[data-inplaceeditablelink]').focus();
|
||||
});
|
||||
mainelement.trigger({type: 'updated', ajaxreturn: data, oldvalue: oldvalue});
|
||||
}).fail(function(ex) {
|
||||
var e = $.Event('updatefailed', { exception: ex, newvalue: value });
|
||||
mainelement.trigger(e);
|
||||
if (!e.isDefaultPrevented()) {
|
||||
notification.exception(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var turn_editing_off = function(el) {
|
||||
var input = el.find('input');
|
||||
input.off();
|
||||
el.html(el.attr('data-oldcontent'));
|
||||
el.removeAttr('data-oldcontent');
|
||||
el.removeClass('inplaceeditingon');
|
||||
};
|
||||
|
||||
var turn_editing_off_everywhere = function() {
|
||||
$('span.inplaceeditable.inplaceeditingon').each(function() {
|
||||
turn_editing_off($( this));
|
||||
});
|
||||
};
|
||||
|
||||
var unique_id = function(prefix, idlength) {
|
||||
var uniqid = prefix;
|
||||
for (var i = 0; i < idlength; i++) {
|
||||
uniqid += String(Math.floor(Math.random() * 10));
|
||||
}
|
||||
// Make sure this ID is not already taken by an existing element.
|
||||
if ($("#" + uniqid).length === 0) {
|
||||
return uniqid;
|
||||
}
|
||||
return unique_id(prefix, idlength);
|
||||
};
|
||||
|
||||
var turn_editing_on = function(el) {
|
||||
el.addClass('inplaceeditingon');
|
||||
el.attr('data-oldcontent', el.html());
|
||||
|
||||
str.get_string('edittitleinstructions').done(function(s) {
|
||||
var instr = $('<span class="editinstructions">' + s + '</span>').
|
||||
attr('id', unique_id('id_editinstructions_', 20)),
|
||||
inputelement = $('<input type="text"/>').
|
||||
attr('id', unique_id('id_inplacevalue_', 20)).
|
||||
attr('value', el.attr('data-value')).
|
||||
attr('aria-describedby', instr.attr('id')),
|
||||
lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>').
|
||||
attr('for', inputelement.attr('id'));
|
||||
el.html('').append(instr).append(lbl).append(inputelement);
|
||||
|
||||
inputelement.focus();
|
||||
inputelement.select();
|
||||
inputelement.on('keyup keypress focusout', function(e) {
|
||||
if (cfg.behatsiterunning && e.type === 'focusout') {
|
||||
// Behat triggers focusout too often.
|
||||
return;
|
||||
}
|
||||
if (e.type === 'keypress' && e.keyCode === 13) {
|
||||
// We need 'keypress' event for Enter because keyup/keydown would catch Enter that was
|
||||
// pressed in other fields.
|
||||
update_value(el, inputelement.val());
|
||||
turn_editing_off(el);
|
||||
}
|
||||
if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
|
||||
// We need 'keyup' event for Escape because keypress does not work with Escape.
|
||||
turn_editing_off(el);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Turn editing on for the current element and register handler for Enter/Esc keys.
|
||||
turn_editing_off_everywhere();
|
||||
turn_editing_on(mainelement);
|
||||
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
+2
-70
@@ -22,8 +22,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.0
|
||||
*/
|
||||
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config'],
|
||||
function($, ajax, templates, notification, str, cfg) {
|
||||
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'],
|
||||
function($, ajax, templates, notification, str) {
|
||||
return /** @alias module:core/tag */ {
|
||||
|
||||
/**
|
||||
@@ -174,74 +174,6 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Edit tag name.
|
||||
$('.tag-management-table').delegate('.tagnameedit', 'click keypress', function(e) {
|
||||
if (e.type === 'keypress' && e.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
var target = $(this),
|
||||
tdelement = $( target.closest('td').get(0) ),
|
||||
inputelement = $( tdelement.find('input').get(0) ),
|
||||
tagid = target.attr('data-id');
|
||||
|
||||
var change_name = function(tagid, newname) {
|
||||
var promises = ajax.call([{
|
||||
methodname: 'core_tag_update_tags',
|
||||
args: { tags : [ { id : tagid , rawname : newname } ] }
|
||||
}, {
|
||||
methodname: 'core_tag_get_tags',
|
||||
args: { tags : [ { id : tagid } ] }
|
||||
}], true);
|
||||
|
||||
$.when.apply($, promises)
|
||||
.done( function(updateresult, data) {
|
||||
if (updateresult.warnings[0] !== undefined) {
|
||||
str.get_string('error').done(function(s) {
|
||||
notification.alert(s, updateresult.warnings[0].message);
|
||||
});
|
||||
} else if (data.tags[0] !== undefined) {
|
||||
templates.render('core_tag/tagname', data.tags[0]).done(function(html) {
|
||||
update_modified(tdelement);
|
||||
tdelement.html(html);
|
||||
$(tdelement.find('.tagnameedit').get(0)).focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var turn_editing_off = function() {
|
||||
$('.tag-management-table td.tageditingon').each(function() {
|
||||
var td = $( this ),
|
||||
input = $( td.find('input').get(0) );
|
||||
input.off();
|
||||
td.removeClass('tageditingon');
|
||||
// Reset input value to the one that was there before editing.
|
||||
input.val(td.attr('data-value'));
|
||||
});
|
||||
};
|
||||
|
||||
// Turn editing on for the current element and register handler for Enter/Esc keys.
|
||||
turn_editing_off();
|
||||
tdelement.addClass('tageditingon');
|
||||
tdelement.attr('data-value', inputelement.val());
|
||||
inputelement.select();
|
||||
inputelement.on('keypress focusout', function(e) {
|
||||
if (cfg.behatsiterunning && e.type === 'focusout') {
|
||||
// Behat triggers focusout too often.
|
||||
return;
|
||||
}
|
||||
if (e.type === 'keypress' && e.keyCode === 13) {
|
||||
change_name(tagid, inputelement.val());
|
||||
turn_editing_off();
|
||||
}
|
||||
if ((e.type === 'keypress' && e.keyCode === 27) || e.type === 'focusout') {
|
||||
turn_editing_off();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -107,8 +107,14 @@ class behat_form_field {
|
||||
// dealing with a fgroup element.
|
||||
$instance = $this->guess_type();
|
||||
$instance->field->keyDown($char, $modifier);
|
||||
$instance->field->keyPress($char, $modifier);
|
||||
$instance->field->keyUp($char, $modifier);
|
||||
try {
|
||||
$instance->field->keyPress($char, $modifier);
|
||||
$instance->field->keyUp($char, $modifier);
|
||||
} catch (WebDriver\Exception $e) {
|
||||
// If the JS handler attached to keydown or keypress destroys the element
|
||||
// the later events may trigger errors because form element no longer exist
|
||||
// or is not visible. Ignore such exceptions here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Contains class \core\output\inplace_editable
|
||||
*
|
||||
* @package core
|
||||
* @category output
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\output;
|
||||
|
||||
use templatable;
|
||||
use renderable;
|
||||
use lang_string;
|
||||
|
||||
/**
|
||||
* Class allowing to quick edit a title inline
|
||||
*
|
||||
* This class is used for displaying an element that can be in-place edited by the user. To display call:
|
||||
* echo $OUTPUT->render($element);
|
||||
* or
|
||||
* echo $OUTPUT->render_from_template('core/inplace_editable', $element->export_for_template($OUTPUT));
|
||||
*
|
||||
* Template core/inplace_editable will automatically load javascript module with the same name
|
||||
* core/inplace_editable. Javascript module registers a click-listener on edit link and
|
||||
* then replaces the displayed value with an input field. On "Enter" it sends a request
|
||||
* to web service core_update_inplace_editable, which invokes the callback from the component.
|
||||
* Any exception thrown by the web service (or callback) is displayed as an error popup.
|
||||
*
|
||||
* Callback {$component}_inplace_editable($itemtype, $itemid, $newvalue) must be present in the lib.php file of
|
||||
* the component or plugin. It must return instance of this class.
|
||||
*
|
||||
* @package core
|
||||
* @category output
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class inplace_editable implements templatable, renderable {
|
||||
|
||||
/**
|
||||
* @var string component responsible for diplsying/updating
|
||||
*/
|
||||
protected $component = null;
|
||||
|
||||
/**
|
||||
* @var string itemtype inside the component
|
||||
*/
|
||||
protected $itemtype = null;
|
||||
|
||||
/**
|
||||
* @var int identifier of the editable element (usually database id)
|
||||
*/
|
||||
protected $itemid = null;
|
||||
|
||||
/**
|
||||
* @var string value of the editable element as it is present in the database
|
||||
*/
|
||||
protected $value = null;
|
||||
|
||||
/**
|
||||
* @var string value of the editable element as it should be displayed,
|
||||
* must be formatted and may contain links or other html tags
|
||||
*/
|
||||
protected $displayvalue = null;
|
||||
|
||||
/**
|
||||
* @var string label for the input element (for screenreaders)
|
||||
*/
|
||||
protected $editlabel = null;
|
||||
|
||||
/**
|
||||
* @var string hint for the input element (for screenreaders)
|
||||
*/
|
||||
protected $edithint = null;
|
||||
|
||||
/**
|
||||
* @var bool indicates if the current user is allowed to edit this element - set in constructor after permissions are checked
|
||||
*/
|
||||
protected $editable = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $component name of the component or plugin responsible for the updating of the value (must declare callback)
|
||||
* @param string $itemtype type of the item inside the component - each component/plugin may implement multiple inplace-editable elements
|
||||
* @param int $itemid identifier of the item that can be edited in-place
|
||||
* @param bool $editable whether this value is editable (check capabilities and editing mode), if false, only "displayvalue"
|
||||
* will be displayed without anything else
|
||||
* @param string $displayvalue what needs to be displayed to the user, it must be cleaned, with applied filters (call
|
||||
* {@link format_string()}). It may be wrapped in an html link, contain icons or other decorations
|
||||
* @param string $value what needs to be edited - usually raw value from the database, it may contain multilang tags
|
||||
* @param lang_string|string $edithint hint (title) that will be displayed under the edit link
|
||||
* @param lang_string|string $editlabel label for the input element in the editing mode (for screenreaders)
|
||||
*/
|
||||
public function __construct($component, $itemtype, $itemid, $editable,
|
||||
$displayvalue, $value = null, $edithint = null, $editlabel = null) {
|
||||
$this->component = $component;
|
||||
$this->itemtype = $itemtype;
|
||||
$this->itemid = $itemid;
|
||||
$this->editable = $editable;
|
||||
$this->displayvalue = $displayvalue;
|
||||
$this->value = $value;
|
||||
$this->edithint = $edithint;
|
||||
$this->editlabel = $editlabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export this data so it can be used as the context for a mustache template (core/inplace_editable).
|
||||
*
|
||||
* @param renderer_base $output typically, the renderer that's calling this function
|
||||
* @return array data context for a mustache template
|
||||
*/
|
||||
public function export_for_template(\renderer_base $output) {
|
||||
if (!$this->editable) {
|
||||
return array(
|
||||
'displayvalue' => (string)$this->displayvalue
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'component' => $this->component,
|
||||
'itemtype' => $this->itemtype,
|
||||
'itemid' => $this->itemid,
|
||||
'displayvalue' => (string)$this->displayvalue,
|
||||
'value' => (string)$this->value,
|
||||
'edithint' => (string)$this->edithint,
|
||||
'editlabel' => (string)$this->editlabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1060,6 +1060,15 @@ $functions = array(
|
||||
'ajax' => true,
|
||||
),
|
||||
|
||||
'core_update_inplace_editable' => array(
|
||||
'classname' => 'core_external',
|
||||
'methodname' => 'update_inplace_editable',
|
||||
'classpath' => 'lib/external/externallib.php',
|
||||
'description' => 'Generic service to update title',
|
||||
'type' => 'write',
|
||||
'loginrequired' => true,
|
||||
'ajax' => true
|
||||
),
|
||||
|
||||
// === Calendar related functions ===
|
||||
|
||||
|
||||
Vendored
+61
@@ -346,4 +346,65 @@ class core_external extends external_api {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for function update_inplace_editable()
|
||||
*
|
||||
* @since Moodle 3.1
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function update_inplace_editable_parameters() {
|
||||
return new external_function_parameters(
|
||||
array(
|
||||
'component' => new external_value(PARAM_COMPONENT, 'component responsible for the update', VALUE_REQUIRED),
|
||||
'itemtype' => new external_value(PARAM_NOTAGS, 'type of the updated item inside the component', VALUE_REQUIRED),
|
||||
'itemid' => new external_value(PARAM_INT, 'identifier of the updated item', VALUE_REQUIRED),
|
||||
'value' => new external_value(PARAM_RAW, 'new value', VALUE_REQUIRED),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any component's editable value assuming that component implements necessary callback
|
||||
*
|
||||
* @since Moodle 3.1
|
||||
* @param string $component
|
||||
* @param string $itemtype
|
||||
* @param string $itemid
|
||||
* @param string $value
|
||||
*/
|
||||
public static function update_inplace_editable($component, $itemtype, $itemid, $value) {
|
||||
global $PAGE;
|
||||
// Validate and normalize parameters.
|
||||
$params = self::validate_parameters(self::update_inplace_editable_parameters(),
|
||||
array('component' => $component, 'itemtype' => $itemtype, 'itemid' => $itemid, 'value' => $value));
|
||||
if (!$functionname = component_callback_exists($component, 'inplace_editable')) {
|
||||
throw new \moodle_exception('inplaceeditableerror');
|
||||
}
|
||||
$tmpl = component_callback($params['component'], 'inplace_editable',
|
||||
array($params['itemtype'], $params['itemid'], $params['value']));
|
||||
if (!$tmpl || !($tmpl instanceof \core\output\inplace_editable)) {
|
||||
throw new \moodle_exception('inplaceeditableerror');
|
||||
}
|
||||
return $tmpl->export_for_template($PAGE->get_renderer('core'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return structure for update_inplace_editable()
|
||||
*
|
||||
* @since Moodle 3.1
|
||||
* @return external_description
|
||||
*/
|
||||
public static function update_inplace_editable_returns() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'displayvalue' => new external_value(PARAM_RAW, 'display value (may contain link or other html tags)'),
|
||||
'component' => new external_value(PARAM_NOTAGS, 'component responsible for the update', VALUE_OPTIONAL),
|
||||
'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype', VALUE_OPTIONAL),
|
||||
'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL),
|
||||
'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL),
|
||||
'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL),
|
||||
'editlabel' => new external_value(PARAM_NOTAGS, 'label for editing element', VALUE_OPTIONAL),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+23
@@ -135,4 +135,27 @@ class core_external_testcase extends externallib_advanced_testcase {
|
||||
$this->assertSame($string['string'], $wsstrings[$string['stringid']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update_inplace_editable()
|
||||
*/
|
||||
public function test_update_inplace_editable() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Call service for component that does not have inplace_editable callback.
|
||||
try {
|
||||
core_external::update_inplace_editable('tool_log', 'itemtype', 1, 'newvalue');
|
||||
$this->fail('Exception expected');
|
||||
} catch (moodle_exception $e) {
|
||||
$this->assertEquals('Error calling update processor', $e->getMessage());
|
||||
}
|
||||
|
||||
// This is a very basic test for the return value of the external function.
|
||||
// More detailed test for tag updating can be found in core_tag component.
|
||||
$this->setAdminUser();
|
||||
$tag = $this->getDataGenerator()->create_tag();
|
||||
$res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
|
||||
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
|
||||
$this->assertEquals('new tag name', $res['value']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4127,6 +4127,16 @@ EOD;
|
||||
$list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext);
|
||||
return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders element for inline editing of any value
|
||||
*
|
||||
* @param \core\output\inplace_editable $element
|
||||
* @return string
|
||||
*/
|
||||
public function render_inplace_editable(\core\output\inplace_editable $element) {
|
||||
return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/inplace_editable
|
||||
|
||||
Displays the value that can be edited inline.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* data-inplaceeditable
|
||||
* data-inplaceeditablelink
|
||||
* data-identifier
|
||||
* data-callback
|
||||
|
||||
Context variables required for this template:
|
||||
* none
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"displayvalue" : "<a href=\"#\">Moodle</a>",
|
||||
"value" : "Moodle",
|
||||
"itemid" : "1",
|
||||
"component" : "core_tag",
|
||||
"itemtype" : "tagname",
|
||||
"edithint" : "Edit this",
|
||||
"editlabel" : "New name for this"
|
||||
}
|
||||
}}
|
||||
{{#component}}
|
||||
<span class="inplaceeditable" data-inplaceeditable="1" data-component="{{component}}" data-itemtype="{{itemtype}}" data-itemid="{{itemid}}"
|
||||
data-value="{{value}}" data-editlabel="{{editlabel}}">
|
||||
{{{displayvalue}}}
|
||||
<a href="#" class="quickeditlink visibleifjs" data-inplaceeditablelink="1" title="{{edithint}}">
|
||||
{{#pix}}t/editstring,core,{{edithint}}{{/pix}}
|
||||
</a>
|
||||
</span>
|
||||
{{#js}}
|
||||
require(['core/inplace_editable']);
|
||||
{{/js}}
|
||||
{{/component}}
|
||||
{{^component}}
|
||||
{{{displayvalue}}}
|
||||
{{/component}}
|
||||
|
||||
@@ -179,8 +179,8 @@ class core_tag_manage_table extends table_sql {
|
||||
*/
|
||||
public function col_name($tag) {
|
||||
global $OUTPUT;
|
||||
$tagoutput = new core_tag\output\tag($tag);
|
||||
return $OUTPUT->render_from_template('core_tag/tagname', $tagoutput->export_for_template($OUTPUT));
|
||||
$tagoutput = new core_tag\output\tagname($tag);
|
||||
return $OUTPUT->render_from_template('core/inplace_editable', $tagoutput->export_for_template($OUTPUT));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Contains class core_tag\output\tagname
|
||||
*
|
||||
* @package core_tag
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_tag\output;
|
||||
|
||||
use context_system;
|
||||
use lang_string;
|
||||
use html_writer;
|
||||
use core_tag_tag;
|
||||
|
||||
/**
|
||||
* Class to preapare a tag name for display.
|
||||
*
|
||||
* @package core_tag
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tagname extends \core\output\inplace_editable {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param stdClass|core_tag $tag
|
||||
*/
|
||||
public function __construct($tag) {
|
||||
$editable = has_capability('moodle/tag:manage', context_system::instance());
|
||||
$edithint = new lang_string('editname', 'core_tag');
|
||||
$editlabel = new lang_string('newnamefor', 'core_tag', $tag->rawname);
|
||||
$value = $tag->rawname;
|
||||
$displayvalue = html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname),
|
||||
core_tag_tag::make_display_name($tag));
|
||||
parent::__construct('core_tag', 'tagname', $tag->id, $editable, $displayvalue, $value, $edithint, $editlabel);
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -892,14 +892,12 @@ class core_tag_tag {
|
||||
$data['rawname'] = clean_param($data['rawname'], PARAM_TAG);
|
||||
$name = core_text::strtolower($data['rawname']);
|
||||
|
||||
if (!$name) {
|
||||
if (!$name || $data['rawname'] === $this->rawname) {
|
||||
unset($data['rawname']);
|
||||
} else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) {
|
||||
// Prevent the rename if a tag with that name already exists.
|
||||
if ($existing->id != $this->id) {
|
||||
debugging('New tag name already exists, you should check it before calling core_tag_tag::update()',
|
||||
DEBUG_DEVELOPER);
|
||||
unset($data['rawname']);
|
||||
throw new moodle_exception('namesalreadybeeingused', 'core_tag');
|
||||
}
|
||||
}
|
||||
if (isset($data['rawname'])) {
|
||||
|
||||
+17
@@ -40,3 +40,20 @@ function tag_page_type_list($pagetype, $parentcontext, $currentcontext) {
|
||||
'tag-manage'=>get_string('page-tag-manage', 'tag')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 core_tag_inplace_editable($itemtype, $itemid, $newvalue) {
|
||||
if ($itemtype === 'tagname') {
|
||||
require_capability('moodle/tag:manage', context_system::instance());
|
||||
$tag = core_tag_tag::get($itemid, '*', MUST_EXIST);
|
||||
$tag->update(array('rawname' => $newvalue));
|
||||
return new \core_tag\output\tagname($tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core_tag/tagname
|
||||
|
||||
Displays the tag name in the tag management table.
|
||||
|
||||
For the Javascript handlers it is important that this template contains
|
||||
an element with .tagnameedit class (to switch on editing) and also
|
||||
an input element.
|
||||
When element with class .tagnameedit is clicked, JS will add class
|
||||
.tageditingon to the container td tag.
|
||||
|
||||
Classes required for JS:
|
||||
* tagnameedit
|
||||
|
||||
Data attributes required for JS:
|
||||
* data-id
|
||||
|
||||
Context variables required for this template:
|
||||
* none
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id" : "1",
|
||||
"name" : "moodle",
|
||||
"rawname" : "Moodle",
|
||||
"viewurl" : "http://moodle.com/"
|
||||
}
|
||||
}}
|
||||
<span class="displaytagname">
|
||||
<a href="{{viewurl}}" data-id="{{id}}">
|
||||
{{rawname}}
|
||||
</a>
|
||||
<a href="#" class="tagnameedit visibleifjs" data-id="{{id}}" title="{{#str}}editname,core_tag{{/str}}">
|
||||
{{#pix}}t/editstring,core,{{#str}}editname,core_tag{{/str}}{{/pix}}
|
||||
</a>
|
||||
</span>
|
||||
<span class="edittagname">
|
||||
<span class="editinstructions" id="id_editinstructions_{{id}}">{{#str}}edittitleinstructions,core{{/str}}</span>
|
||||
<label class="accesshide" for="tagquickname{{id}}">{{#str}}newnamefor,core_tag,{{rawname}}{{/str}}</label>
|
||||
<input id="tagquickname{{id}}" type="text" value="{{rawname}}" data-id="{{id}}" aria-describedby="id_editinstructions_{{id}}"/>
|
||||
</span>
|
||||
@@ -153,7 +153,7 @@ Feature: Users can edit tags to add description or rename
|
||||
And I set the field "New name for tag Cat" to "Kitten"
|
||||
And I press key "13" in the field "New name for tag Cat"
|
||||
Then I should not see "Cat"
|
||||
And "New name for tag" "field" should not be visible
|
||||
And "New name for tag" "field" should not exist
|
||||
And I wait until "Kitten" "link" exists
|
||||
And I follow "Default collection"
|
||||
And I should see "Kitten"
|
||||
@@ -163,8 +163,8 @@ Feature: Users can edit tags to add description or rename
|
||||
And I set the field "New name for tag Turtle" to "DOG"
|
||||
And I press key "13" in the field "New name for tag Turtle"
|
||||
And I should see "Tag names already being used"
|
||||
And I press "Ok"
|
||||
And "New name for tag" "field" should not be visible
|
||||
And I press "Close"
|
||||
And "New name for tag" "field" should not exist
|
||||
And I should see "Turtle"
|
||||
And I should see "Dog"
|
||||
And I should not see "DOG"
|
||||
@@ -176,7 +176,7 @@ Feature: Users can edit tags to add description or rename
|
||||
And I click on "Edit tag name" "link" in the "Dog" "table_row"
|
||||
And I set the field "New name for tag Dog" to "Penguin"
|
||||
And I press key "27" in the field "New name for tag Dog"
|
||||
And "New name for tag" "field" should not be visible
|
||||
And "New name for tag" "field" should not exist
|
||||
And I should see "Turtle"
|
||||
And I should not see "Penguin"
|
||||
And I follow "Default collection"
|
||||
|
||||
@@ -147,4 +147,38 @@ class core_tag_external_testcase extends externallib_advanced_testcase {
|
||||
$this->assertEquals($tag->id, $result['warnings'][0]['item']);
|
||||
$this->assertEquals('namesalreadybeeingused', $result['warnings'][0]['warningcode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update_inplace_editable()
|
||||
*/
|
||||
public function test_update_inplace_editable() {
|
||||
global $CFG, $DB, $PAGE;
|
||||
require_once($CFG->dirroot . '/lib/external/externallib.php');
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$tag = $this->getDataGenerator()->create_tag();
|
||||
|
||||
// Call service for core_tag component without necessary permissions.
|
||||
try {
|
||||
core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
|
||||
$this->fail('Exception expected');
|
||||
} catch (moodle_exception $e) {
|
||||
$this->assertEquals('Sorry, but you do not currently have permissions to do that (Manage all tags)',
|
||||
$e->getMessage());
|
||||
}
|
||||
|
||||
// Change to admin user and make sure that tag name can be updated using web service update_inplace_editable().
|
||||
$this->setAdminUser();
|
||||
$res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'New tag name');
|
||||
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
|
||||
$this->assertEquals('New tag name', $res['value']);
|
||||
$this->assertEquals('New tag name', $DB->get_field('tag', 'rawname', array('id' => $tag->id)));
|
||||
|
||||
// Call callback core_tag_inplace_editable() directly.
|
||||
$tmpl = component_callback('core_tag', 'inplace_editable', array('tagname', $tag->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('tag', 'rawname', array('id' => $tag->id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,14 +751,7 @@ span.flagged-tag a,
|
||||
tr.flagged-tag a {color:#FF0000;}
|
||||
.tag-management-table td,
|
||||
.tag-management-table th {vertical-align: middle;padding: 4px;}
|
||||
.tag-management-table tr td.tageditingon .displaytagname,
|
||||
.tag-management-table td .edittagname {display: none;}
|
||||
.tag-management-table tr td.tageditingon .edittagname {display: inherit; position: relative;}
|
||||
.tag-management-table tr td.tageditingon .edittagname .editinstructions {margin-right: -300px; margin-left: 0;}
|
||||
.tag-management-table tr td .tagnameedit img {opacity: 0.2;}
|
||||
.tag-management-table tr:hover td .tagnameedit img,
|
||||
.tag-management-table tr td .tagnameedit:focus img {opacity: 1;}
|
||||
.tag-management-table tr:hover td.tageditingon .tagnameedit img {opacity: 0.2;}
|
||||
.tag-management-table span.inplaceeditable.inplaceeditingon input {width: 150px;}
|
||||
.tag_feed .media, .tag_feed .media-body {overflow: hidden;}
|
||||
.tag_feed.media-list .media .itemimage {float: left;}
|
||||
.dir-rtl .tag_feed.media-list .media .itemimage {float: right;}
|
||||
@@ -2330,3 +2323,30 @@ body.lockscroll {
|
||||
.ie10 .yui3-calendar-header-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span.inplaceeditable.inplaceeditingon {
|
||||
position: relative;
|
||||
}
|
||||
span.inplaceeditable.inplaceeditingon span.editinstructions {
|
||||
margin-top: -30px;
|
||||
font-weight: normal;
|
||||
margin-right: -300px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions {
|
||||
margin-left: -300px;
|
||||
margin-right: 0;
|
||||
}
|
||||
span.inplaceeditable .quickeditlink img {
|
||||
opacity: 0.2;
|
||||
}
|
||||
span.inplaceeditable:hover .quickeditlink img,
|
||||
span.inplaceeditable .quickeditlink:focus img {
|
||||
opacity: 1;
|
||||
}
|
||||
span.inplaceeditable.inplaceeditingon input {
|
||||
width: 330px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -826,27 +826,8 @@ tr.flagged-tag a {
|
||||
vertical-align: middle;
|
||||
padding: 4px;
|
||||
}
|
||||
.tag-management-table tr td.tageditingon .displaytagname,
|
||||
.tag-management-table td .edittagname {
|
||||
display: none;
|
||||
}
|
||||
.tag-management-table tr td.tageditingon .edittagname {
|
||||
display: inherit;
|
||||
position: relative;
|
||||
}
|
||||
.tag-management-table tr td.tageditingon .edittagname .editinstructions {
|
||||
margin-right: -300px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.tag-management-table tr td .tagnameedit img {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.tag-management-table tr:hover td .tagnameedit img,
|
||||
.tag-management-table tr td .tagnameedit:focus img {
|
||||
opacity: 1;
|
||||
}
|
||||
.tag-management-table tr:hover td.tageditingon .tagnameedit img {
|
||||
opacity: 0.2;
|
||||
.tag-management-table span.inplaceeditable.inplaceeditingon input {
|
||||
width: 150px;
|
||||
}
|
||||
.path-tag .tag-relatedtags {
|
||||
padding-top: 10px;
|
||||
@@ -2445,3 +2426,33 @@ dd:after {
|
||||
.nav-tabs > .active > a[href]:focus {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.inplaceeditable.inplaceeditingon {
|
||||
position: relative;
|
||||
}
|
||||
span.inplaceeditable.inplaceeditingon span.editinstructions {
|
||||
margin-top: -30px;
|
||||
font-weight: normal;
|
||||
margin-right: -300px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.dir-rtl span.inplaceeditable.inplaceeditingon span.editinstructions {
|
||||
margin-left: -300px;
|
||||
margin-right: 0;
|
||||
}
|
||||
span.inplaceeditable .quickeditlink img {
|
||||
opacity: 0.2;
|
||||
}
|
||||
span.inplaceeditable:hover .quickeditlink img,
|
||||
span.inplaceeditable .quickeditlink:focus img {
|
||||
opacity: 1;
|
||||
}
|
||||
span.inplaceeditable.inplaceeditingon input {
|
||||
width: 330px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h3.sectionname span.inplaceeditable.inplaceeditingon span.editinstructions {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2016021501.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2016021600.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user