Commit e71a6c24 by Calen Pennington Committed by GitHub

Merge pull request #13899 from CredoReference/possible-to-add-multiple-tag-values

Allow multiple values for a single tag
parents fec2fb4e 5fe79dbd
"""
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)
# -*- 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'},
),
]
...@@ -14,6 +14,8 @@ class TagCategories(models.Model): ...@@ -14,6 +14,8 @@ class TagCategories(models.Model):
class Meta(object): class Meta(object):
app_label = "tagging" app_label = "tagging"
ordering = ('title',) ordering = ('title',)
verbose_name = "tag category"
verbose_name_plural = "tag categories"
def __unicode__(self): def __unicode__(self):
return "[TagCategories] {}: {}".format(self.name, self.title) return "[TagCategories] {}: {}".format(self.name, self.title)
...@@ -35,6 +37,7 @@ class TagAvailableValues(models.Model): ...@@ -35,6 +37,7 @@ class TagAvailableValues(models.Model):
class Meta(object): class Meta(object):
app_label = "tagging" app_label = "tagging"
ordering = ('id',) ordering = ('id',)
verbose_name = "available tag value"
def __unicode__(self): def __unicode__(self):
return "[TagAvailableValues] {}: {}".format(self.category, self.value) return "[TagAvailableValues] {}: {}".format(self.category, self.value)
...@@ -46,19 +46,26 @@ class StructuredTagsAside(XBlockAside): ...@@ -46,19 +46,26 @@ class StructuredTagsAside(XBlockAside):
if isinstance(block, CapaModule): if isinstance(block, CapaModule):
tags = [] tags = []
for tag in self.get_available_tags(): for tag in self.get_available_tags():
values = tag.get_values() tag_available_values = tag.get_values()
current_value = self.saved_tags.get(tag.name, None) tag_current_values = self.saved_tags.get(tag.name, [])
if current_value is not None and current_value not in values: if isinstance(tag_current_values, basestring):
values.insert(0, current_value) 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({ tags.append({
'key': tag.name, 'key': tag.name,
'title': tag.title, 'title': tag.title,
'values': values, 'values': tag_values_available_to_choose,
'current_value': current_value 'current_values': tag_current_values,
}) })
fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags, fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags,
'tags_count': len(tags),
'block_location': block.location})) 'block_location': block.location}))
fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js')) fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js'))
fragment.initialize_js('StructuredTagsInit') fragment.initialize_js('StructuredTagsInit')
...@@ -71,25 +78,36 @@ class StructuredTagsAside(XBlockAside): ...@@ -71,25 +78,36 @@ class StructuredTagsAside(XBlockAside):
""" """
Handler to save choosen tags with connected XBlock Handler to save choosen tags with connected XBlock
""" """
found = False try:
if 'tag' not in request.params: posted_data = request.json
return Response("The required parameter 'tag' is not passed", status=400) 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(): for av_tag in self.get_available_tags():
if av_tag.name == tag[0]: if av_tag.name in posted_data and posted_data[av_tag.name]:
if tag[1] == '': tag_available_values = av_tag.get_values()
self.saved_tags[tag[0]] = None tag_current_values = self.saved_tags.get(av_tag.name, [])
found = True
elif tag[1] in av_tag.get_values(): if isinstance(tag_current_values, basestring):
self.saved_tags[tag[0]] = tag[1] tag_current_values = [tag_current_values]
found = True
if not found: for posted_tag_value in posted_data[av_tag.name]:
return Response("Invalid 'tag' parameter", status=400) 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() return Response()
else:
return Response("Tags parameters were not passed", status=400)
def get_event_context(self, event_type, event): # pylint: disable=unused-argument def get_event_context(self, event_type, event): # pylint: disable=unused-argument
""" """
......
...@@ -3,6 +3,7 @@ Tests for the Studio Tagging XBlockAside ...@@ -3,6 +3,7 @@ Tests for the Studio Tagging XBlockAside
""" """
import ddt import ddt
import json
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -38,6 +39,7 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): ...@@ -38,6 +39,7 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
self.aside_name = 'tagging_aside' self.aside_name = 'tagging_aside'
self.aside_tag_dif = 'difficulty' self.aside_tag_dif = 'difficulty'
self.aside_tag_dif_value = 'Hard' self.aside_tag_dif_value = 'Hard'
self.aside_tag_dif_value2 = 'Easy'
self.aside_tag_lo = 'learning_outcome' self.aside_tag_lo = 'learning_outcome'
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
...@@ -152,27 +154,27 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): ...@@ -152,27 +154,27 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
self.assertEquals(div_node.get('data-runtime-version'), '1') self.assertEquals(div_node.get('data-runtime-version'), '1')
self.assertIn('xblock_asides-v1', div_node.get('class')) 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) self.assertEquals(len(select_nodes), 2)
select_node1 = select_nodes[0] select_node1 = select_nodes[0]
self.assertEquals(select_node1.get('name'), self.aside_tag_dif) self.assertEquals(select_node1.get('name'), self.aside_tag_dif)
option_nodes1 = select_node1.xpath('option') 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] 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] select_node2 = select_nodes[1]
self.assertEquals(select_node2.get('name'), self.aside_tag_lo) self.assertEquals(select_node2.get('name'), self.aside_tag_lo)
self.assertEquals(select_node2.get('multiple'), 'multiple')
option_nodes2 = select_node2.xpath('option') 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] option_values2 = [opt_elem.text for opt_elem in option_nodes2 if opt_elem.text]
self.assertEquals(option_values2, ['Not selected', 'Learned nothing', self.assertEquals(option_values2, ['Learned a few things', 'Learned everything', 'Learned nothing'])
'Learned a few things', 'Learned everything'])
# Now ensure the acid_aside is not in the result # Now ensure the acid_aside is not in the result
self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']") self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']")
...@@ -195,27 +197,44 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase): ...@@ -195,27 +197,44 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
client = AjaxEnabledTestClient() client = AjaxEnabledTestClient()
client.login(username=self.user.username, password=self.user_password) 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) 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) self.assertEqual(response.status_code, 400)
val = '%s:undefined' % self.aside_tag_dif response = client.post(handler_url, json.dumps({self.aside_tag_dif: ['undefined1', 'undefined2']}),
response = client.post(path=handler_url, data={'tag': val}) content_type="application/json")
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
val = '%s:%s' % (self.aside_tag_dif, self.aside_tag_dif_value) def _test_helper_func(problem_location):
response = client.post(path=handler_url, data={'tag': val}) """
self.assertEqual(response.status_code, 200) Helper function
"""
problem = modulestore().get_item(self.problem.location) problem = modulestore().get_item(problem_location)
asides = problem.runtime.get_asides(problem) asides = problem.runtime.get_asides(problem)
tag_aside = None tag_aside = None
for aside in asides: for aside in asides:
if isinstance(aside, StructuredTagsAside): if isinstance(aside, StructuredTagsAside):
tag_aside = aside tag_aside = aside
break 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)
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])
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.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,
self.aside_tag_dif_value2])
...@@ -3,29 +3,36 @@ ...@@ -3,29 +3,36 @@
function StructuredTagsView(runtime, element) { function StructuredTagsView(runtime, element) {
var $element = $(element); var $element = $(element);
var saveTagsInProgress = false;
$($element).find('.save_tags').click(function(e) {
var dataToPost = {};
if (!saveTagsInProgress) {
saveTagsInProgress = true;
$element.find('select').each(function() { $element.find('select').each(function() {
var loader = this; dataToPost[$(this).attr('name')] = $(this).val();
var sts = $(this).attr('structured-tags-select-init'); });
if (typeof sts === typeof undefined || sts === false) {
$(this).attr('structured-tags-select-init', 1);
$(this).change(function(e) {
e.preventDefault(); e.preventDefault();
var selectedKey = $(loader).find('option:selected').val();
runtime.notify('save', { runtime.notify('save', {
state: 'start', state: 'start',
element: element, element: element,
message: gettext('Updating Tags') message: gettext('Updating Tags')
}); });
$.post(runtime.handlerUrl(element, 'save_tags'), {
'tag': $(loader).attr('name') + ':' + selectedKey $.ajax({
}).done(function() { 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', { runtime.notify('save', {
state: 'end', state: 'end',
element: element element: element
}); });
}); saveTagsInProgress = false;
}); });
} }
}); });
......
<div class="xblock-render" class="studio-xblock-wrapper"> <div class="xblock-render" class="studio-xblock-wrapper">
<div class="wrapper">
% for tag in tags: % for tag in tags:
<label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>: <div class="wrapper-content left">
<select id="tags_${tag['key']}_${block_location}" name="${tag['key']}"> <div><label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>:</div>
<option value="" ${'' if tag['current_value'] else 'selected=""'}>Not selected</option> <div>
<select id="tags_${tag['key']}_${block_location}" name="${tag['key']}" multiple="multiple">
% for v in tag['values']: % for v in tag['values']:
<% <%
selected = '' selected = ''
if v == tag['current_value']: if v in tag['current_values']:
selected = 'selected' selected = 'selected'
%> %>
<option value="${v}" ${selected}>${v}</option> <option value="${v}" ${selected}>${v}</option>
% endfor % endfor
</select> </select>
</div>
</div>
% endfor % endfor
% if tags_count > 0:
<div class="wrapper-content left">
<div class="outline-content">
<div class="add-item">
<a href="javascript: void(0);" class="button button-new save_tags" title="Save tags">
<span class="action-button-text">Save tags</span>
</a>
</div>
</div>
</div>
% endif
</div>
</div> </div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment