From 1836065754e876dde314d5a3d7d21cdb7593fc06 Mon Sep 17 00:00:00 2001 From: Dmitry Viskov Date: Mon, 15 Feb 2016 22:38:09 +0300 Subject: [PATCH] Dynamic values for the selectboxes with tags (tags are stored in the database tables) --- cms/envs/common.py | 3 + cms/lib/xblock/tagging/__init__.py | 6 ++ .../xblock/tagging/migrations/0001_initial.py | 39 ++++++++ cms/lib/xblock/tagging/migrations/__init__.py | 0 cms/lib/xblock/tagging/models.py | 40 +++++++++ cms/lib/xblock/{ => tagging}/tagging.py | 90 ++++++------------- .../{test/test_tagging.py => tagging/test.py} | 72 ++++++++++++--- cms/templates/structured_tags_block.html | 16 ++-- 8 files changed, 185 insertions(+), 81 deletions(-) create mode 100644 cms/lib/xblock/tagging/__init__.py create mode 100644 cms/lib/xblock/tagging/migrations/0001_initial.py create mode 100644 cms/lib/xblock/tagging/migrations/__init__.py create mode 100644 cms/lib/xblock/tagging/models.py rename cms/lib/xblock/{ => tagging}/tagging.py (56%) rename cms/lib/xblock/{test/test_tagging.py => tagging/test.py} (66%) diff --git a/cms/envs/common.py b/cms/envs/common.py index a057dafd78..afa82c69a3 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -903,6 +903,9 @@ INSTALLED_APPS = ( # Management commands used for configuration automation 'edx_management_commands.management_commands', + + # Tagging + 'cms.lib.xblock.tagging', ) diff --git a/cms/lib/xblock/tagging/__init__.py b/cms/lib/xblock/tagging/__init__.py new file mode 100644 index 0000000000..98c9e4da77 --- /dev/null +++ b/cms/lib/xblock/tagging/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +""" +Structured Tagging based on XBlockAsides +""" + +from .tagging import StructuredTagsAside diff --git a/cms/lib/xblock/tagging/migrations/0001_initial.py b/cms/lib/xblock/tagging/migrations/0001_initial.py new file mode 100644 index 0000000000..a0d86d97ac --- /dev/null +++ b/cms/lib/xblock/tagging/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/cms/lib/xblock/tagging/migrations/__init__.py b/cms/lib/xblock/tagging/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/lib/xblock/tagging/models.py b/cms/lib/xblock/tagging/models.py new file mode 100644 index 0000000000..4024197fcc --- /dev/null +++ b/cms/lib/xblock/tagging/models.py @@ -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) diff --git a/cms/lib/xblock/tagging.py b/cms/lib/xblock/tagging/tagging.py similarity index 56% rename from cms/lib/xblock/tagging.py rename to cms/lib/xblock/tagging/tagging.py index 7a7710e187..15ac9c36bd 100644 --- a/cms/lib/xblock/tagging.py +++ b/cms/lib/xblock/tagging/tagging.py @@ -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) diff --git a/cms/lib/xblock/test/test_tagging.py b/cms/lib/xblock/tagging/test.py similarity index 66% rename from cms/lib/xblock/test/test_tagging.py rename to cms/lib/xblock/tagging/test.py index 49907e1298..1b6227e90f 100644 --- a/cms/lib/xblock/test/test_tagging.py +++ b/cms/lib/xblock/tagging/test.py @@ -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) diff --git a/cms/templates/structured_tags_block.html b/cms/templates/structured_tags_block.html index dedab140c1..ae68b781c5 100644 --- a/cms/templates/structured_tags_block.html +++ b/cms/templates/structured_tags_block.html @@ -1,16 +1,16 @@ -
+
% for tag in tags: - : - - % 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' %> - + % endfor - % endfor -
\ No newline at end of file + % endfor +