diff --git a/lang/en/tag.php b/lang/en/tag.php index 5f05faac0e6..543767455fc 100644 --- a/lang/en/tag.php +++ b/lang/en/tag.php @@ -59,6 +59,7 @@ $string['name'] = 'Tag name'; $string['namesalreadybeeingused'] = 'Tag names already being used'; $string['newnamefor'] = 'New name for tag {$a}'; $string['noresultsfor'] = 'No results for "{$a}"'; +$string['nothingtoupdate'] = 'Nothing to update'; $string['officialtag'] = 'Official'; $string['otags'] = 'Official tags'; $string['othertags'] = 'Other tags (enter tags separated by commas)'; diff --git a/lib/amd/build/tag.min.js b/lib/amd/build/tag.min.js index 704f209afa9..8d684ae99fe 100644 --- a/lib/amd/build/tag.min.js +++ b/lib/amd/build/tag.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{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(".tagtype","click",function(d){d.preventDefault();var e=a(this),f=e.attr("data-id"),h=e.attr("data-value"),i="1"===h?0:1;b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,official:i}]},done:function(a){void 0!==a.tags[0]&&c.render("core_tag/tagtype",a.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagtype").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;b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,flag:i}]},done:function(b){if(void 0!==b.tags[0]){var d=e.closest("tr").get(0);d&&(b.tags[0].flag?a(d).addClass("flagged-tag"):a(d).removeClass("flagged-tag")),c.render("core_tag/tagflag",b.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){b.call([{methodname:"core_tag_update_tags",args:{tags:[{id:f,rawname:h}]},done:function(b){void 0!==b.tags[0]?c.render("core_tag/tagname",b.tags[0]).done(function(b){g(j),j.html(b),a(j.find(".tagnameedit").get(0)).focus()}):void 0!==b.warnings[0]&&e.get_string("error").done(function(a){d.alert(a,b.warnings[0].message)})}}])},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())})}})}}}); \ No newline at end of file +define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config"],function(a,b,c,d,e,f){return{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(".tagtype","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,official: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/tagtype",b.tags[0]).done(function(a){g(e);var b=e.parent();e.replaceWith(a),b.find(".tagtype").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())})}})}}}); \ No newline at end of file diff --git a/lib/amd/src/tag.js b/lib/amd/src/tag.js index 8b6a98cfc7c..0edf6ef2aa4 100644 --- a/lib/amd/src/tag.js +++ b/lib/amd/src/tag.js @@ -51,11 +51,17 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' currentvalue = target.attr('data-value'), official = (currentvalue === "1") ? 0 : 1; - ajax.call([{ + var promises = ajax.call([{ methodname: 'core_tag_update_tags', - args: { tags : [ { id : tagid , official : official } ] }, - done: function(data) { - if (data.tags[0] !== undefined) { + args: { tags : [ { id : tagid , official : official } ] } + }, { + methodname: 'core_tag_get_tags', + args: { tags : [ { id : tagid } ] } + }], true); + + $.when.apply($, promises) + .done( function(updateresult, data) { + if (updateresult.warnings[0] === undefined && data.tags[0] !== undefined) { templates.render('core_tag/tagtype', data.tags[0]).done(function(html) { update_modified(target); var parent = target.parent(); @@ -63,8 +69,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' parent.find('.tagtype').get(0).focus(); }); } - } - }]); + }); }); // Click handler for flagging/resetting tag flag. @@ -75,11 +80,17 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' currentvalue = target.attr('data-value'), flag = (currentvalue === "0") ? 1 : 0; - ajax.call([{ + var promises = ajax.call([{ methodname: 'core_tag_update_tags', - args: { tags : [ { id : tagid , flag : flag } ] }, - done: function(data) { - if (data.tags[0] !== undefined) { + args: { tags : [ { id : tagid , flag : flag } ] } + }, { + methodname: 'core_tag_get_tags', + args: { tags : [ { id : tagid } ] } + }], true); + + $.when.apply($, promises) + .done( function(updateresult, data) { + if (updateresult.warnings[0] === undefined && data.tags[0] !== undefined) { var row = target.closest('tr').get(0); if (row) { if (data.tags[0].flag) { @@ -95,8 +106,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' parent.find('.tagflag').get(0).focus(); }); } - } - }]); + }); }); // Confirmation for single tag delete link. @@ -150,23 +160,29 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str' tagid = target.attr('data-id'); var change_name = function(tagid, newname) { - ajax.call([{ + var promises = ajax.call([{ methodname: 'core_tag_update_tags', - args: { tags : [ { id : tagid , rawname : newname } ] }, - done: function(data) { - if (data.tags[0] !== undefined) { + 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(); }); - } else if (data.warnings[0] !== undefined) { - str.get_string('error').done(function(s) { - notification.alert(s, data.warnings[0].message); - }); } - } - }]); }; + }); + }; var turn_editing_off = function() { $('.tag-management-table td.tageditingon').each(function() { diff --git a/lib/db/services.php b/lib/db/services.php index 3b9a5edd47c..70187dab52d 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1073,6 +1073,13 @@ $functions = array( 'type' => 'write', ), + 'core_tag_get_tags' => array( + 'classname' => 'core_tag_external', + 'methodname' => 'get_tags', + 'description' => 'Gets tags by their ids', + 'type' => 'read', + ), + ); $services = array( diff --git a/tag/classes/external.php b/tag/classes/external.php index 343d9107988..ee8f1a0e3b6 100644 --- a/tag/classes/external.php +++ b/tag/classes/external.php @@ -83,7 +83,6 @@ class core_tag_external extends external_api { $systemcontext = context_system::instance(); $canmanage = has_capability('moodle/tag:manage', $systemcontext); $canedit = has_capability('moodle/tag:edit', $systemcontext); - $return = array(); $warnings = array(); if (empty($CFG->usetags)) { @@ -112,7 +111,11 @@ class core_tag_external extends external_api { array('description' => 1, 'descriptionformat' => 1)); } if (count($tag) <= 1) { - // Nothing to do. + $warnings[] = array( + 'item' => $tag['id'], + 'warningcode' => 'nothingtoupdate', + 'message' => get_string('nothingtoupdate', 'core_tag') + ); continue; } if (!$tagobject = $DB->get_record('tag', array('id' => $tag['id']))) { @@ -155,10 +158,8 @@ class core_tag_external extends external_api { ) )); $event->trigger(); - $tagoutput = new \core_tag\output\tag($tagobject); - $return[] = $tagoutput->export_for_template($renderer); } - return array('tags' => $return, 'warnings' => $warnings); + return array('warnings' => $warnings); } /** @@ -167,6 +168,99 @@ class core_tag_external extends external_api { * @return external_description */ public static function update_tags_returns() { + return new external_single_structure( + array( + 'warnings' => new external_warnings() + ) + ); + } + + /** + * Parameters for function get_tags() + * + * @return external_function_parameters + */ + public static function get_tags_parameters() { + return new external_function_parameters( + array( + 'tags' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'tag id'), + ) + ) + ) + ) + ); + } + + /** + * Expose to AJAX + * + * @return boolean + */ + public static function get_tags_is_allowed_from_ajax() { + return true; + } + + /** + * Get tags by their ids + * + * @param array $tags + */ + public static function get_tags($tags) { + global $CFG, $PAGE, $DB; + require_once($CFG->dirroot.'/tag/lib.php'); + + // Validate and normalize parameters. + $tags = self::validate_parameters(self::get_tags_parameters(), array('tags' => $tags)); + + require_login(null, false, null, false, true); + + $systemcontext = context_system::instance(); + $canmanage = has_capability('moodle/tag:manage', $systemcontext); + $canedit = has_capability('moodle/tag:edit', $systemcontext); + + $return = array(); + $warnings = array(); + + if (empty($CFG->usetags)) { + throw new moodle_exception('tagsaredisabled', 'tag'); + } + + $renderer = $PAGE->get_renderer('core'); + foreach ($tags['tags'] as $tag) { + $tag = (array)$tag; + if (!$tagobject = $DB->get_record('tag', array('id' => $tag['id']))) { + $warnings[] = array( + 'item' => $tag['id'], + 'warningcode' => 'tagnotfound', + 'message' => get_string('tagnotfound', 'error') + ); + continue; + } + $tagoutput = new \core_tag\output\tag($tagobject); + // Do not return some information to users without permissions. + $rv = $tagoutput->export_for_template($renderer); + if (!$canmanage) { + if (!$canedit) { + unset($rv->official); + } + unset($rv->flag); + unset($rv->changetypeurl); + unset($rv->changeflagurl); + } + $return[] = $rv; + } + return array('tags' => $return, 'warnings' => $warnings); + } + + /** + * Return structure for get_tag() + * + * @return external_description + */ + public static function get_tags_returns() { return new external_single_structure( array( 'tags' => new external_multiple_structure( new external_single_structure( @@ -175,17 +269,16 @@ class core_tag_external extends external_api { 'name' => new external_value(PARAM_TAG, 'name'), 'rawname' => new external_value(PARAM_RAW, 'tag raw name (may contain capital letters)'), 'description' => new external_value(PARAM_RAW, 'tag description'), - 'descriptionformat' => new external_value(PARAM_INT, 'tag description format'), - 'flag' => new external_value(PARAM_INT, 'flag'), - 'official' => new external_value(PARAM_INT, 'whether this flag is official'), + 'descriptionformat' => new external_format_value(PARAM_INT, 'tag description format'), + 'flag' => new external_value(PARAM_INT, 'flag', VALUE_OPTIONAL), + 'official' => new external_value(PARAM_INT, 'whether this flag is official', VALUE_OPTIONAL), 'viewurl' => new external_value(PARAM_URL, 'URL to view'), - 'changetypeurl' => new external_value(PARAM_URL, 'URL to change type (official or not)'), - 'changeflagurl' => new external_value(PARAM_URL, 'URL to set or reset flag'), - ), 'event') + 'changetypeurl' => new external_value(PARAM_URL, 'URL to change type (official or not)', VALUE_OPTIONAL), + 'changeflagurl' => new external_value(PARAM_URL, 'URL to set or reset flag', VALUE_OPTIONAL), + ), 'information about one tag') ), 'warnings' => new external_warnings() ) ); } - } \ No newline at end of file diff --git a/tag/tests/behat/edit_tag.feature b/tag/tests/behat/edit_tag.feature index 1a717babf18..e6e4fc40068 100644 --- a/tag/tests/behat/edit_tag.feature +++ b/tag/tests/behat/edit_tag.feature @@ -140,6 +140,7 @@ Feature: Users can edit tags to add description or rename And I click on "Edit tag name" "link" in the "Cat" "table_row" 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" + And I wait until the page is ready Then I should not see "Cat" And "New name for tag" "field" should not be visible And I wait until "Kitten" "link" exists diff --git a/tag/tests/external_test.php b/tag/tests/external_test.php index 4457de10993..23ea50260f3 100644 --- a/tag/tests/external_test.php +++ b/tag/tests/external_test.php @@ -55,27 +55,43 @@ class core_tag_external_testcase extends externallib_advanced_testcase { 'flag' => 0, 'official' => 1, ); + $gettag = array( + 'id' => $tag->id, + ); - // User without any caps can not change anything about a tag. + // User without any caps can not change anything about a tag but can request [partial] tag data. $this->setUser($this->getDataGenerator()->create_user()); $result = core_tag_external::update_tags(array($updatetag)); $result = external_api::clean_returnvalue(core_tag_external::update_tags_returns(), $result); - $this->assertEmpty($result['warnings']); - $this->assertEmpty($result['tags']); + $this->assertEquals($tag->id, $result['warnings'][0]['item']); + $this->assertEquals('nothingtoupdate', $result['warnings'][0]['warningcode']); $this->assertEquals($originaltag['rawname'], $DB->get_field('tag', 'rawname', - array('id' => $tag->id))); + array('id' => $tag->id))); $this->assertEquals($originaltag['description'], $DB->get_field('tag', 'description', - array('id' => $tag->id))); + array('id' => $tag->id))); + + $result = core_tag_external::get_tags(array($gettag)); + $result = external_api::clean_returnvalue(core_tag_external::get_tags_returns(), $result); + $this->assertEquals($originaltag['rawname'], $result['tags'][0]['rawname']); + $this->assertEquals($originaltag['description'], $result['tags'][0]['description']); + $this->assertNotEmpty($result['tags'][0]['viewurl']); + $this->assertArrayNotHasKey('changetypeurl', $result['tags'][0]); + $this->assertArrayNotHasKey('changeflagurl', $result['tags'][0]); + $this->assertArrayNotHasKey('flag', $result['tags'][0]); + $this->assertArrayNotHasKey('official', $result['tags'][0]); // User with editing only capability can change description but not the tag name. $roleid = $this->assignUserCapability('moodle/tag:edit', $context->id); $result = core_tag_external::update_tags(array($updatetag)); $result = external_api::clean_returnvalue(core_tag_external::update_tags_returns(), $result); $this->assertEmpty($result['warnings']); + + $result = core_tag_external::get_tags(array($gettag)); + $result = external_api::clean_returnvalue(core_tag_external::get_tags_returns(), $result); $this->assertEquals($updatetag['id'], $result['tags'][0]['id']); $this->assertEquals($updatetag['description'], $result['tags'][0]['description']); $this->assertEquals($originaltag['rawname'], $result['tags'][0]['rawname']); - $this->assertEquals($originaltag['flag'], $result['tags'][0]['flag']); + $this->assertArrayNotHasKey('flag', $result['tags'][0]); // 'Flag' is not available unless 'moodle/tag:manage' cap exists. $this->assertEquals(0, $result['tags'][0]['official']); $this->assertEquals($originaltag['rawname'], $DB->get_field('tag', 'rawname', array('id' => $tag->id))); @@ -90,6 +106,9 @@ class core_tag_external_testcase extends externallib_advanced_testcase { $result = core_tag_external::update_tags(array($updatetag)); $result = external_api::clean_returnvalue(core_tag_external::update_tags_returns(), $result); $this->assertEmpty($result['warnings']); + + $result = core_tag_external::get_tags(array($gettag)); + $result = external_api::clean_returnvalue(core_tag_external::get_tags_returns(), $result); $this->assertEquals($updatetag['id'], $result['tags'][0]['id']); $this->assertEquals($updatetag['rawname'], $result['tags'][0]['rawname']); $this->assertEquals(core_text::strtolower($updatetag['rawname']), $result['tags'][0]['name']); @@ -100,13 +119,21 @@ class core_tag_external_testcase extends externallib_advanced_testcase { $this->assertEquals('official', $DB->get_field('tag', 'tagtype', array('id' => $tag->id))); - // Updating non-existing tag. + // Updating and getting non-existing tag. $nonexistingtag = array( 'id' => 123, 'description' => 'test' ); + $getnonexistingtag = array( + 'id' => 123, + ); $result = core_tag_external::update_tags(array($nonexistingtag)); $result = external_api::clean_returnvalue(core_tag_external::update_tags_returns(), $result); + $this->assertEquals(123, $result['warnings'][0]['item']); + $this->assertEquals('tagnotfound', $result['warnings'][0]['warningcode']); + + $result = core_tag_external::get_tags(array($getnonexistingtag)); + $result = external_api::clean_returnvalue(core_tag_external::get_tags_returns(), $result); $this->assertEmpty($result['tags']); $this->assertEquals(123, $result['warnings'][0]['item']); $this->assertEquals('tagnotfound', $result['warnings'][0]['warningcode']); @@ -116,7 +143,6 @@ class core_tag_external_testcase extends externallib_advanced_testcase { $updatetag2 = array('id' => $tag->id, 'rawname' => 'MYTAG'); $result = core_tag_external::update_tags(array($updatetag2)); $result = external_api::clean_returnvalue(core_tag_external::update_tags_returns(), $result); - $this->assertEmpty($result['tags']); $this->assertEquals($tag->id, $result['warnings'][0]['item']); $this->assertEquals('namesalreadybeeingused', $result['warnings'][0]['warningcode']); }