Allow multiple values for a single tag
This commit is contained in:
20
cms/lib/xblock/tagging/admin.py
Normal file
20
cms/lib/xblock/tagging/admin.py
Normal file
@@ -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)
|
||||
22
cms/lib/xblock/tagging/migrations/0002_auto_20170116_1541.py
Normal file
22
cms/lib/xblock/tagging/migrations/0002_auto_20170116_1541.py
Normal file
@@ -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'},
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<div class="xblock-render" class="studio-xblock-wrapper">
|
||||
% for tag in tags:
|
||||
<label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>:
|
||||
<select id="tags_${tag['key']}_${block_location}" name="${tag['key']}">
|
||||
<option value="" ${'' if tag['current_value'] else 'selected=""'}>Not selected</option>
|
||||
% for v in tag['values']:
|
||||
<%
|
||||
selected = ''
|
||||
if v == tag['current_value']:
|
||||
selected = 'selected'
|
||||
%>
|
||||
<option value="${v}" ${selected}>${v}</option>
|
||||
<div class="wrapper">
|
||||
% for tag in tags:
|
||||
<div class="wrapper-content left">
|
||||
<div><label for="tags_${tag['key']}_${block_location}">${tag['title']}</label>:</div>
|
||||
<div>
|
||||
<select id="tags_${tag['key']}_${block_location}" name="${tag['key']}" multiple="multiple">
|
||||
% for v in tag['values']:
|
||||
<%
|
||||
selected = ''
|
||||
if v in tag['current_values']:
|
||||
selected = 'selected'
|
||||
%>
|
||||
<option value="${v}" ${selected}>${v}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
% 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>
|
||||
|
||||
Reference in New Issue
Block a user