diff --git a/cms/lib/xblock/tagging/admin.py b/cms/lib/xblock/tagging/admin.py new file mode 100644 index 0000000000..003a4f767b --- /dev/null +++ b/cms/lib/xblock/tagging/admin.py @@ -0,0 +1,20 @@ +""" +Admin registration for tags models +""" +from django.contrib import admin +from .models import TagCategories, TagAvailableValues + + +class TagCategoriesAdmin(admin.ModelAdmin): + """Admin for TagCategories""" + search_fields = ('name', 'title') + list_display = ('id', 'name', 'title') + + +class TagAvailableValuesAdmin(admin.ModelAdmin): + """Admin for TagAvailableValues""" + list_display = ('id', 'category', 'value') + + +admin.site.register(TagCategories, TagCategoriesAdmin) +admin.site.register(TagAvailableValues, TagAvailableValuesAdmin) diff --git a/cms/lib/xblock/tagging/migrations/0002_auto_20170116_1541.py b/cms/lib/xblock/tagging/migrations/0002_auto_20170116_1541.py new file mode 100644 index 0000000000..70dcb0515d --- /dev/null +++ b/cms/lib/xblock/tagging/migrations/0002_auto_20170116_1541.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tagging', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tagavailablevalues', + options={'ordering': ('id',), 'verbose_name': 'available tag value'}, + ), + migrations.AlterModelOptions( + name='tagcategories', + options={'ordering': ('title',), 'verbose_name': 'tag category', 'verbose_name_plural': 'tag categories'}, + ), + ] diff --git a/cms/lib/xblock/tagging/models.py b/cms/lib/xblock/tagging/models.py index 4024197fcc..e4a4a7d9b6 100644 --- a/cms/lib/xblock/tagging/models.py +++ b/cms/lib/xblock/tagging/models.py @@ -14,6 +14,8 @@ class TagCategories(models.Model): class Meta(object): app_label = "tagging" ordering = ('title',) + verbose_name = "tag category" + verbose_name_plural = "tag categories" def __unicode__(self): return "[TagCategories] {}: {}".format(self.name, self.title) @@ -35,6 +37,7 @@ class TagAvailableValues(models.Model): class Meta(object): app_label = "tagging" ordering = ('id',) + verbose_name = "available tag value" def __unicode__(self): return "[TagAvailableValues] {}: {}".format(self.category, self.value) diff --git a/cms/lib/xblock/tagging/tagging.py b/cms/lib/xblock/tagging/tagging.py index ddc4327716..c6028f62b9 100644 --- a/cms/lib/xblock/tagging/tagging.py +++ b/cms/lib/xblock/tagging/tagging.py @@ -46,19 +46,26 @@ class StructuredTagsAside(XBlockAside): if isinstance(block, CapaModule): tags = [] for tag in self.get_available_tags(): - values = tag.get_values() - current_value = self.saved_tags.get(tag.name, None) + tag_available_values = tag.get_values() + tag_current_values = self.saved_tags.get(tag.name, []) - if current_value is not None and current_value not in values: - values.insert(0, current_value) + if isinstance(tag_current_values, basestring): + tag_current_values = [tag_current_values] + + tag_values_not_exists = [cur_val for cur_val in tag_current_values + if cur_val not in tag_available_values] + + tag_values_available_to_choose = tag_available_values + tag_values_not_exists + tag_values_available_to_choose.sort() tags.append({ 'key': tag.name, 'title': tag.title, - 'values': values, - 'current_value': current_value + 'values': tag_values_available_to_choose, + 'current_values': tag_current_values, }) fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags, + 'tags_count': len(tags), 'block_location': block.location})) fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js')) fragment.initialize_js('StructuredTagsInit') @@ -71,25 +78,36 @@ class StructuredTagsAside(XBlockAside): """ Handler to save choosen tags with connected XBlock """ - found = False - if 'tag' not in request.params: - return Response("The required parameter 'tag' is not passed", status=400) + try: + posted_data = request.json + except ValueError: + return Response("Invalid request body", status=400) - tag = request.params['tag'].split(':') + saved_tags = {} + need_update = False for av_tag in self.get_available_tags(): - if av_tag.name == tag[0]: - if tag[1] == '': - self.saved_tags[tag[0]] = None - found = True - elif tag[1] in av_tag.get_values(): - self.saved_tags[tag[0]] = tag[1] - found = True + if av_tag.name in posted_data and posted_data[av_tag.name]: + tag_available_values = av_tag.get_values() + tag_current_values = self.saved_tags.get(av_tag.name, []) - if not found: - return Response("Invalid 'tag' parameter", status=400) + if isinstance(tag_current_values, basestring): + tag_current_values = [tag_current_values] - return Response() + for posted_tag_value in posted_data[av_tag.name]: + if posted_tag_value not in tag_available_values and posted_tag_value not in tag_current_values: + return Response("Invalid tag value was passed: %s" % posted_tag_value, status=400) + + saved_tags[av_tag.name] = posted_data[av_tag.name] + need_update = True + if av_tag.name in posted_data: + need_update = True + + if need_update: + self.saved_tags = saved_tags + return Response() + else: + return Response("Tags parameters were not passed", status=400) def get_event_context(self, event_type, event): # pylint: disable=unused-argument """ diff --git a/cms/lib/xblock/tagging/test.py b/cms/lib/xblock/tagging/test.py index a6f9ae07ab..04cbc94620 100644 --- a/cms/lib/xblock/tagging/test.py +++ b/cms/lib/xblock/tagging/test.py @@ -3,6 +3,7 @@ Tests for the Studio Tagging XBlockAside """ import ddt +import json from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -38,6 +39,7 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): self.aside_name = 'tagging_aside' self.aside_tag_dif = 'difficulty' self.aside_tag_dif_value = 'Hard' + self.aside_tag_dif_value2 = 'Easy' self.aside_tag_lo = 'learning_outcome' course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) @@ -152,27 +154,27 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): self.assertEquals(div_node.get('data-runtime-version'), '1') self.assertIn('xblock_asides-v1', div_node.get('class')) - select_nodes = div_node.xpath('div/select') + select_nodes = div_node.xpath("div//select[@multiple='multiple']") self.assertEquals(len(select_nodes), 2) select_node1 = select_nodes[0] self.assertEquals(select_node1.get('name'), self.aside_tag_dif) option_nodes1 = select_node1.xpath('option') - self.assertEquals(len(option_nodes1), 4) + self.assertEquals(len(option_nodes1), 3) option_values1 = [opt_elem.text for opt_elem in option_nodes1] - self.assertEquals(option_values1, ['Not selected', 'Easy', 'Medium', 'Hard']) + self.assertEquals(option_values1, ['Easy', 'Hard', 'Medium']) select_node2 = select_nodes[1] self.assertEquals(select_node2.get('name'), self.aside_tag_lo) + self.assertEquals(select_node2.get('multiple'), 'multiple') option_nodes2 = select_node2.xpath('option') - self.assertEquals(len(option_nodes2), 4) + self.assertEquals(len(option_nodes2), 3) option_values2 = [opt_elem.text for opt_elem in option_nodes2 if opt_elem.text] - self.assertEquals(option_values2, ['Not selected', 'Learned nothing', - 'Learned a few things', 'Learned everything']) + self.assertEquals(option_values2, ['Learned a few things', 'Learned everything', 'Learned nothing']) # Now ensure the acid_aside is not in the result self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']") @@ -195,27 +197,44 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): client = AjaxEnabledTestClient() client.login(username=self.user.username, password=self.user_password) - response = client.post(path=handler_url, data={}) + response = client.post(handler_url, json.dumps({}), content_type="application/json") self.assertEqual(response.status_code, 400) - response = client.post(path=handler_url, data={'tag': 'undefined_tag:undefined'}) + response = client.post(handler_url, json.dumps({'undefined_tag': ['undefined1', 'undefined2']}), + content_type="application/json") self.assertEqual(response.status_code, 400) - val = '%s:undefined' % self.aside_tag_dif - response = client.post(path=handler_url, data={'tag': val}) + response = client.post(handler_url, json.dumps({self.aside_tag_dif: ['undefined1', 'undefined2']}), + content_type="application/json") self.assertEqual(response.status_code, 400) - val = '%s:%s' % (self.aside_tag_dif, self.aside_tag_dif_value) - response = client.post(path=handler_url, data={'tag': val}) + def _test_helper_func(problem_location): + """ + Helper function + """ + problem = modulestore().get_item(problem_location) + asides = problem.runtime.get_asides(problem) + tag_aside = None + for aside in asides: + if isinstance(aside, StructuredTagsAside): + tag_aside = aside + break + return tag_aside + + response = client.post(handler_url, json.dumps({self.aside_tag_dif: [self.aside_tag_dif_value]}), + content_type="application/json") self.assertEqual(response.status_code, 200) - problem = modulestore().get_item(self.problem.location) - asides = problem.runtime.get_asides(problem) - tag_aside = None - for aside in asides: - if isinstance(aside, StructuredTagsAside): - tag_aside = aside - break - + tag_aside = _test_helper_func(self.problem.location) self.assertIsNotNone(tag_aside, "Necessary StructuredTagsAside object isn't found") - self.assertEqual(tag_aside.saved_tags[self.aside_tag_dif], self.aside_tag_dif_value) + self.assertEqual(tag_aside.saved_tags[self.aside_tag_dif], [self.aside_tag_dif_value]) + + response = client.post(handler_url, json.dumps({self.aside_tag_dif: [self.aside_tag_dif_value, + self.aside_tag_dif_value2]}), + content_type="application/json") + self.assertEqual(response.status_code, 200) + + tag_aside = _test_helper_func(self.problem.location) + self.assertIsNotNone(tag_aside, "Necessary StructuredTagsAside object isn't found") + self.assertEqual(tag_aside.saved_tags[self.aside_tag_dif], [self.aside_tag_dif_value, + self.aside_tag_dif_value2]) diff --git a/cms/static/js/xblock_asides/structured_tags.js b/cms/static/js/xblock_asides/structured_tags.js index 40e6c3e0a8..5a3fd09a83 100644 --- a/cms/static/js/xblock_asides/structured_tags.js +++ b/cms/static/js/xblock_asides/structured_tags.js @@ -3,29 +3,36 @@ function StructuredTagsView(runtime, element) { var $element = $(element); + var saveTagsInProgress = false; - $element.find('select').each(function() { - var loader = this; - var sts = $(this).attr('structured-tags-select-init'); + $($element).find('.save_tags').click(function(e) { + var dataToPost = {}; + if (!saveTagsInProgress) { + saveTagsInProgress = true; - if (typeof sts === typeof undefined || sts === false) { - $(this).attr('structured-tags-select-init', 1); - $(this).change(function(e) { - e.preventDefault(); - var selectedKey = $(loader).find('option:selected').val(); + $element.find('select').each(function() { + dataToPost[$(this).attr('name')] = $(this).val(); + }); + + e.preventDefault(); + runtime.notify('save', { + state: 'start', + element: element, + message: gettext('Updating Tags') + }); + + $.ajax({ + type: 'POST', + url: runtime.handlerUrl(element, 'save_tags'), + data: JSON.stringify(dataToPost), + dataType: 'json', + contentType: 'application/json; charset=utf-8' + }).always(function() { runtime.notify('save', { - state: 'start', - element: element, - message: gettext('Updating Tags') - }); - $.post(runtime.handlerUrl(element, 'save_tags'), { - 'tag': $(loader).attr('name') + ':' + selectedKey - }).done(function() { - runtime.notify('save', { - state: 'end', - element: element - }); + state: 'end', + element: element }); + saveTagsInProgress = false; }); } }); diff --git a/cms/templates/structured_tags_block.html b/cms/templates/structured_tags_block.html index ae68b781c5..1585b4b9b4 100644 --- a/cms/templates/structured_tags_block.html +++ b/cms/templates/structured_tags_block.html @@ -1,16 +1,33 @@
- % for tag in tags: - : - + % for v in tag['values']: + <% + selected = '' + if v in tag['current_values']: + selected = 'selected' + %> + + % endfor + +
+ % endfor - - % endfor + + % if tags_count > 0: +
+
+ +
+
+ % endif +