Commit 5fe79dbd by Dmitry Viskov

Allow multiple values for a single tag

parent 1912176c
"""
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
for posted_tag_value in posted_data[av_tag.name]:
if not found: if posted_tag_value not in tag_available_values and posted_tag_value not in tag_current_values:
return Response("Invalid 'tag' parameter", status=400) return Response("Invalid tag value was passed: %s" % posted_tag_value, status=400)
return Response() 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 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}) """
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) self.assertEqual(response.status_code, 200)
problem = modulestore().get_item(self.problem.location) tag_aside = _test_helper_func(self.problem.location)
asides = problem.runtime.get_asides(problem) self.assertIsNotNone(tag_aside, "Necessary StructuredTagsAside object isn't found")
tag_aside = None self.assertEqual(tag_aside.saved_tags[self.aside_tag_dif], [self.aside_tag_dif_value])
for aside in asides:
if isinstance(aside, StructuredTagsAside): response = client.post(handler_url, json.dumps({self.aside_tag_dif: [self.aside_tag_dif_value,
tag_aside = aside self.aside_tag_dif_value2]}),
break 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('select').each(function() { $($element).find('.save_tags').click(function(e) {
var loader = this; var dataToPost = {};
var sts = $(this).attr('structured-tags-select-init'); if (!saveTagsInProgress) {
saveTagsInProgress = true;
if (typeof sts === typeof undefined || sts === false) { $element.find('select').each(function() {
$(this).attr('structured-tags-select-init', 1); dataToPost[$(this).attr('name')] = $(this).val();
$(this).change(function(e) { });
e.preventDefault();
var selectedKey = $(loader).find('option:selected').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', { runtime.notify('save', {
state: 'start', state: 'end',
element: element, 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
});
}); });
saveTagsInProgress = false;
}); });
} }
}); });
......
<div class="xblock-render" class="studio-xblock-wrapper"> <div class="xblock-render" class="studio-xblock-wrapper">
% for tag in tags: <div class="wrapper">
<label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>: % for tag in tags:
<select id="tags_${tag['key']}_${block_location}" name="${tag['key']}"> <div class="wrapper-content left">
<option value="" ${'' if tag['current_value'] else 'selected=""'}>Not selected</option> <div><label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>:</div>
% for v in tag['values']: <div>
<% <select id="tags_${tag['key']}_${block_location}" name="${tag['key']}" multiple="multiple">
selected = '' % for v in tag['values']:
if v == tag['current_value']: <%
selected = 'selected' selected = ''
%> if v in tag['current_values']:
<option value="${v}" ${selected}>${v}</option> selected = 'selected'
%>
<option value="${v}" ${selected}>${v}</option>
% endfor
</select>
</div>
</div>
% endfor % endfor
</select>
% 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