Merge branch 'wip-MDL-51802-master4' of https://github.com/marinaglancy/moodle

This commit is contained in:
Eloy Lafuente (stronk7)
2016-02-16 23:22:35 +01:00
39 changed files with 1078 additions and 225 deletions
+1 -22
View File
@@ -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)));
+70
View File
@@ -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);
}
}
}
/**
+14 -3
View File
@@ -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';
+41
View File
@@ -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);
}
}
+22
View File
@@ -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()));
}
}
}
+4
View File
@@ -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';
+41
View File
@@ -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);
}
}
+22
View File
@@ -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
View File
@@ -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).
*
+2
View File
@@ -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';
+2
View File
@@ -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
View File
@@ -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)}}),{}});
+1 -1
View File
@@ -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})}}});
+138
View File
@@ -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
View File
@@ -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();
}
});
});
}
};
});
+8 -2
View File
@@ -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.
}
}
/**
+146
View File
@@ -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,
);
}
}
+9
View File
@@ -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 ===
+61
View File
@@ -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),
)
);
}
}
+23
View File
@@ -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']);
}
}
+10
View File
@@ -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));
}
}
/**
+60
View File
@@ -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}}
+2 -2
View File
@@ -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));
}
/**
+55
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
-57
View File
@@ -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>
+4 -4
View File
@@ -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"
+34
View File
@@ -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)));
}
}
+28 -8
View File
@@ -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;
}
+32 -21
View File
@@ -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
View File
@@ -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.