Dynamic values for the selectboxes with tags (tags are stored in the database tables)
This commit is contained in:
@@ -903,6 +903,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Management commands used for configuration automation
|
||||
'edx_management_commands.management_commands',
|
||||
|
||||
# Tagging
|
||||
'cms.lib.xblock.tagging',
|
||||
)
|
||||
|
||||
|
||||
|
||||
6
cms/lib/xblock/tagging/__init__.py
Normal file
6
cms/lib/xblock/tagging/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Structured Tagging based on XBlockAsides
|
||||
"""
|
||||
|
||||
from .tagging import StructuredTagsAside
|
||||
39
cms/lib/xblock/tagging/migrations/0001_initial.py
Normal file
39
cms/lib/xblock/tagging/migrations/0001_initial.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TagAvailableValues',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('value', models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('id',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TagCategories',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('title',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tagavailablevalues',
|
||||
name='category',
|
||||
field=models.ForeignKey(to='tagging.TagCategories'),
|
||||
),
|
||||
]
|
||||
0
cms/lib/xblock/tagging/migrations/__init__.py
Normal file
0
cms/lib/xblock/tagging/migrations/__init__.py
Normal file
40
cms/lib/xblock/tagging/models.py
Normal file
40
cms/lib/xblock/tagging/models.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Django Model for tags
|
||||
"""
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TagCategories(models.Model):
|
||||
"""
|
||||
This model represents tag categories.
|
||||
"""
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
title = models.CharField(max_length=255)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "tagging"
|
||||
ordering = ('title',)
|
||||
|
||||
def __unicode__(self):
|
||||
return "[TagCategories] {}: {}".format(self.name, self.title)
|
||||
|
||||
def get_values(self):
|
||||
"""
|
||||
Return the list of available values for the particular category
|
||||
"""
|
||||
return [t.value for t in TagAvailableValues.objects.filter(category=self)]
|
||||
|
||||
|
||||
class TagAvailableValues(models.Model):
|
||||
"""
|
||||
This model represents available values for tags.
|
||||
"""
|
||||
category = models.ForeignKey(TagCategories, db_index=True)
|
||||
value = models.CharField(max_length=255)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "tagging"
|
||||
ordering = ('id',)
|
||||
|
||||
def __unicode__(self):
|
||||
return "[TagAvailableValues] {}: {}".format(self.category, self.value)
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Structured Tagging based on XBlockAsides
|
||||
"""
|
||||
@@ -7,64 +8,15 @@ from xblock.fragment import Fragment
|
||||
from xblock.fields import Scope, Dict
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
from xmodule.capa_module import CapaModule
|
||||
from abc import ABCMeta, abstractproperty
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from django.conf import settings
|
||||
from webob import Response
|
||||
from collections import OrderedDict
|
||||
from .models import TagCategories
|
||||
|
||||
|
||||
_ = lambda text: text
|
||||
|
||||
|
||||
class AbstractTag(object):
|
||||
"""
|
||||
Abstract class for tags
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractproperty
|
||||
def key(self):
|
||||
"""
|
||||
Subclasses must implement key
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement key')
|
||||
|
||||
@abstractproperty
|
||||
def name(self):
|
||||
"""
|
||||
Subclasses must implement name
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement name')
|
||||
|
||||
@abstractproperty
|
||||
def allowed_values(self):
|
||||
"""
|
||||
Subclasses must implement allowed_values
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement allowed_values')
|
||||
|
||||
|
||||
class DifficultyTag(AbstractTag):
|
||||
"""
|
||||
Particular implementation tags for difficulty
|
||||
"""
|
||||
@property
|
||||
def key(self):
|
||||
""" Identifier for the difficulty selector """
|
||||
return 'difficulty_tag'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Label for the difficulty selector """
|
||||
return _('Difficulty')
|
||||
|
||||
@property
|
||||
def allowed_values(self):
|
||||
""" Allowed values for the difficulty selector """
|
||||
return OrderedDict([('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')])
|
||||
|
||||
|
||||
class StructuredTagsAside(XBlockAside):
|
||||
"""
|
||||
Aside that allows tagging blocks
|
||||
@@ -72,7 +24,12 @@ class StructuredTagsAside(XBlockAside):
|
||||
saved_tags = Dict(help=_("Dictionary with the available tags"),
|
||||
scope=Scope.content,
|
||||
default={},)
|
||||
available_tags = [DifficultyTag()]
|
||||
|
||||
def get_available_tags(self):
|
||||
"""
|
||||
Return available tags
|
||||
"""
|
||||
return TagCategories.objects.all()
|
||||
|
||||
def _get_studio_resource_url(self, relative_url):
|
||||
"""
|
||||
@@ -88,14 +45,21 @@ class StructuredTagsAside(XBlockAside):
|
||||
"""
|
||||
if isinstance(block, CapaModule):
|
||||
tags = []
|
||||
for tag in self.available_tags:
|
||||
for tag in self.get_available_tags():
|
||||
values = tag.get_values()
|
||||
current_value = self.saved_tags.get(tag.name, None)
|
||||
|
||||
if current_value is not None and current_value not in values:
|
||||
values.insert(0, current_value)
|
||||
|
||||
tags.append({
|
||||
'key': tag.key,
|
||||
'title': tag.name,
|
||||
'values': tag.allowed_values,
|
||||
'current_value': self.saved_tags.get(tag.key, None),
|
||||
'key': tag.name,
|
||||
'title': tag.title,
|
||||
'values': values,
|
||||
'current_value': current_value
|
||||
})
|
||||
fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags}))
|
||||
fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags,
|
||||
'block_location': block.location}))
|
||||
fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js'))
|
||||
fragment.initialize_js('StructuredTagsInit')
|
||||
return fragment
|
||||
@@ -113,14 +77,14 @@ class StructuredTagsAside(XBlockAside):
|
||||
|
||||
tag = request.params['tag'].split(':')
|
||||
|
||||
for av_tag in self.available_tags:
|
||||
if av_tag.key == tag[0]:
|
||||
if tag[1] in av_tag.allowed_values:
|
||||
self.saved_tags[tag[0]] = tag[1]
|
||||
found = True
|
||||
elif tag[1] == '':
|
||||
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 not found:
|
||||
return Response("Invalid 'tag' parameter", status=400)
|
||||
@@ -7,7 +7,11 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xblock_config.models import StudioConfig
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
||||
from xblock.test.tools import TestRuntime
|
||||
from cms.lib.xblock.tagging import StructuredTagsAside
|
||||
from cms.lib.xblock.tagging.models import TagCategories, TagAvailableValues
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
from contentstore.utils import reverse_usage_url
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
@@ -30,8 +34,9 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
self.user_password = super(StructuredTagsAsideTestCase, self).setUp()
|
||||
self.aside_name = 'tagging_aside'
|
||||
self.aside_tag = 'difficulty_tag'
|
||||
self.aside_tag_value = 'hard'
|
||||
self.aside_tag_dif = 'difficulty'
|
||||
self.aside_tag_dif_value = 'Hard'
|
||||
self.aside_tag_lo = 'learning_outcome'
|
||||
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
|
||||
self.course = ItemFactory.create(
|
||||
@@ -75,16 +80,47 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
user_id=self.user.id
|
||||
)
|
||||
|
||||
_init_data = [
|
||||
{
|
||||
'name': 'difficulty',
|
||||
'title': 'Difficulty',
|
||||
'values': ['Easy', 'Medium', 'Hard'],
|
||||
},
|
||||
{
|
||||
'name': 'learning_outcome',
|
||||
'title': 'Learning outcome',
|
||||
'values': ['Learned nothing', 'Learned a few things', 'Learned everything']
|
||||
}
|
||||
]
|
||||
|
||||
for tag in _init_data:
|
||||
category = TagCategories.objects.create(name=tag['name'], title=tag['title'])
|
||||
for val in tag['values']:
|
||||
TagAvailableValues.objects.create(category=category, value=val)
|
||||
|
||||
config = StudioConfig.current()
|
||||
config.enabled = True
|
||||
config.save()
|
||||
|
||||
def tearDown(self):
|
||||
TagAvailableValues.objects.all().delete()
|
||||
TagCategories.objects.all().delete()
|
||||
super(StructuredTagsAsideTestCase, self).tearDown()
|
||||
|
||||
def test_aside_contains_tags(self):
|
||||
"""
|
||||
Checks that available_tags list is not empty
|
||||
"""
|
||||
self.assertGreater(len(StructuredTagsAside.available_tags), 0,
|
||||
"StructuredTagsAside should contains at least one available tag")
|
||||
sids = ScopeIds(user_id="bob",
|
||||
block_type="bobs-type",
|
||||
def_id="definition-id",
|
||||
usage_id="usage-id")
|
||||
key_store = DictKeyValueStore()
|
||||
field_data = KvsFieldData(key_store)
|
||||
runtime = TestRuntime(services={'field-data': field_data}) # pylint: disable=abstract-class-instantiated
|
||||
xblock_aside = StructuredTagsAside(scope_ids=sids, runtime=runtime)
|
||||
available_tags = xblock_aside.get_available_tags()
|
||||
self.assertEquals(len(available_tags), 2, "StructuredTagsAside should contains two tag categories")
|
||||
|
||||
def test_preview_html(self):
|
||||
"""
|
||||
@@ -115,10 +151,26 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
self.assertIn('xblock_asides-v1', div_node.get('class'))
|
||||
|
||||
select_nodes = div_node.xpath('div/select')
|
||||
self.assertEquals(len(select_nodes), 1)
|
||||
self.assertEquals(len(select_nodes), 2)
|
||||
|
||||
select_node = select_nodes[0]
|
||||
self.assertEquals(select_node.get('name'), self.aside_tag)
|
||||
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)
|
||||
|
||||
option_values1 = [opt_elem.text for opt_elem in option_nodes1]
|
||||
self.assertEquals(option_values1, ['Not selected', 'Easy', 'Medium', 'Hard'])
|
||||
|
||||
select_node2 = select_nodes[1]
|
||||
self.assertEquals(select_node2.get('name'), self.aside_tag_lo)
|
||||
|
||||
option_nodes2 = select_node2.xpath('option')
|
||||
self.assertEquals(len(option_nodes2), 4)
|
||||
|
||||
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'])
|
||||
|
||||
# Now ensure the acid_aside is not in the result
|
||||
self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']")
|
||||
@@ -146,11 +198,11 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
response = client.post(path=handler_url, data={'tag': 'undefined_tag:undefined'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
val = '%s:undefined' % self.aside_tag
|
||||
val = '%s:undefined' % self.aside_tag_dif
|
||||
response = client.post(path=handler_url, data={'tag': val})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
val = '%s:%s' % (self.aside_tag, self.aside_tag_value)
|
||||
val = '%s:%s' % (self.aside_tag_dif, self.aside_tag_dif_value)
|
||||
response = client.post(path=handler_url, data={'tag': val})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -163,4 +215,4 @@ class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
break
|
||||
|
||||
self.assertIsNotNone(tag_aside, "Necessary StructuredTagsAside object isn't found")
|
||||
self.assertEqual(tag_aside.saved_tags[self.aside_tag], self.aside_tag_value)
|
||||
self.assertEqual(tag_aside.saved_tags[self.aside_tag_dif], self.aside_tag_dif_value)
|
||||
@@ -1,16 +1,16 @@
|
||||
<div class="xblock-render">
|
||||
<div class="xblock-render" class="studio-xblock-wrapper">
|
||||
% for tag in tags:
|
||||
<label for="problem_tags_${tag['key']}">${tag['title']}</label>:
|
||||
<select id="problem_tags_${tag['key']}" name="${tag['key']}">
|
||||
<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 k,v in tag['values'].iteritems():
|
||||
% for v in tag['values']:
|
||||
<%
|
||||
selected = ''
|
||||
if k == tag['current_value']:
|
||||
if v == tag['current_value']:
|
||||
selected = 'selected'
|
||||
%>
|
||||
<option value="${k}" ${selected}>${v}</option>
|
||||
<option value="${v}" ${selected}>${v}</option>
|
||||
% endfor
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user