summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-05-11 03:36:20 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-05-11 03:36:20 -0400
commit2bd9d7db2e724dbc544a5f7578171e9d2da6b2d0 (patch)
treecf2bc462d5c12b751041f5093af26b49cef7daa3
parentc92c21d4cf239e46262ac1956e03dc74889aeb26 (diff)
downloadaskbot-2bd9d7db2e724dbc544a5f7578171e9d2da6b2d0.tar.gz
askbot-2bd9d7db2e724dbc544a5f7578171e9d2da6b2d0.tar.bz2
askbot-2bd9d7db2e724dbc544a5f7578171e9d2da6b2d0.zip
basic category tree-like tag selector works, but the tree is not yet editable
-rw-r--r--askbot/conf/flatpages.py11
-rw-r--r--askbot/conf/forum_data_rules.py16
-rw-r--r--askbot/models/tag.py27
-rw-r--r--askbot/skins/common/media/js/post.js175
-rw-r--r--askbot/skins/common/media/js/utils.js56
-rw-r--r--askbot/skins/common/templates/widgets/edit_post.html49
-rw-r--r--askbot/skins/default/media/style/style.less31
-rw-r--r--askbot/skins/default/templates/ask.html16
-rw-r--r--askbot/skins/default/templates/macros.html3
-rw-r--r--askbot/skins/default/templates/widgets/ask_form.html3
-rw-r--r--askbot/skins/default/templates/widgets/tag_editor.html27
-rw-r--r--askbot/skins/default/templates/widgets/three_column_category_selector.html10
-rw-r--r--askbot/views/writers.py37
13 files changed, 417 insertions, 44 deletions
diff --git a/askbot/conf/flatpages.py b/askbot/conf/flatpages.py
index 62413797..6879e27c 100644
--- a/askbot/conf/flatpages.py
+++ b/askbot/conf/flatpages.py
@@ -51,3 +51,14 @@ settings.register(
)
)
)
+
+#todo: merge this with mandatory tags
+settings.register(#this field is not editable manually
+ LongStringValue(
+ FLATPAGES,
+ 'CATEGORY_TREE',
+ description = 'Category tree',#no need to translate
+ default = '[[]]',#empty array of arrays in json
+ hidden = True
+ )
+)
diff --git a/askbot/conf/forum_data_rules.py b/askbot/conf/forum_data_rules.py
index 7d98c9e8..64ce1302 100644
--- a/askbot/conf/forum_data_rules.py
+++ b/askbot/conf/forum_data_rules.py
@@ -128,6 +128,22 @@ settings.register(
)
)
+TAG_SOURCE_CHOICES = (
+ ('category-tree', _('category tree')),
+ ('mandatory-tags', _('mandatory tags')),
+)
+
+settings.register(
+ livesettings.StringValue(
+ FORUM_DATA_RULES,
+ 'TAG_SOURCE',
+ description = _('Source of tags'),
+ hidden = True,
+ choices = TAG_SOURCE_CHOICES,
+ default = 'mandatory-tags'
+ )
+)
+
settings.register(
livesettings.StringValue(
FORUM_DATA_RULES,
diff --git a/askbot/models/tag.py b/askbot/models/tag.py
index 779c0b68..d2d94b4e 100644
--- a/askbot/models/tag.py
+++ b/askbot/models/tag.py
@@ -20,12 +20,29 @@ def get_mandatory_tags():
"""returns list of mandatory tags,
or an empty list, if there aren't any"""
from askbot.conf import settings as askbot_settings
- raw_mandatory_tags = askbot_settings.MANDATORY_TAGS.strip()
- if len(raw_mandatory_tags) == 0:
- return []
+ #TAG_SOURCE setting is hidden
+ #and only is accessible via livesettings overrides
+ if askbot_settings.TAG_SOURCE == 'category-tree':
+ return []#hack: effectively we disable the mandatory tags feature
else:
- split_re = re.compile(const.TAG_SPLIT_REGEX)
- return split_re.split(raw_mandatory_tags)
+ #todo - in the future clean this up
+ #we might need to have settings:
+ #* prepopulated tags - json structure - either a flat list or a tree
+ # if structure is tree - then use some multilevel selector for choosing tags
+ # if it is a list - then make users click on tags to select them
+ #* use prepopulated tags (boolean)
+ #* tags are required
+ #* regular users can create tags (boolean)
+ #the category tree and the mandatory tag lists can be merged
+ #into the same setting - and mandatory tags should use json
+ #keep in mind that in the future multiword tags will be allowed
+ raw_mandatory_tags = askbot_settings.MANDATORY_TAGS.strip()
+ if len(raw_mandatory_tags) == 0:
+ return []
+ else:
+ split_re = re.compile(const.TAG_SPLIT_REGEX)
+ return split_re.split(raw_mandatory_tags)
+
class TagQuerySet(models.query.QuerySet):
def get_valid_tags(self, page_size):
diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js
index 294c5f41..ae067997 100644
--- a/askbot/skins/common/media/js/post.js
+++ b/askbot/skins/common/media/js/post.js
@@ -2384,6 +2384,181 @@ GroupJoinButton.prototype.getHandler = function(){
};
};
+var TagEditor = function() {
+ WrappedElement.call(this);
+};
+inherits(TagEditor, WrappedElement);
+
+TagEditor.prototype.getSelectedTags = function() {
+ return $.trim(this._tags_input.val()).split(/\s+/);
+};
+
+TagEditor.prototype.setSelectedTags = function(tag_names) {
+ this._tags_input.val($.trim(tag_names.join(' ')));
+};
+
+TagEditor.prototype.addSelectedTag = function(tag_name) {
+ var tag_names = this._tags_input.val();
+ this._tags_input.val(tag_names + ' ' + tag_name);
+ this._prompt.hide();
+};
+
+TagEditor.prototype.removeSelectedTag = function(tag_name) {
+ var tag_names = this.getSelectedTags();
+ var idx = $.inArray(tag_name, tag_names);
+ if (idx !== -1) {
+ tag_names.splice(idx, 1)
+ if (tag_names.length === 0) {
+ this._prompt.show();
+ }
+ }
+ this.setSelectedTags(tag_names);
+};
+
+TagEditor.prototype.getAddTagHandler = function() {
+ var me = this;
+ var tags_container = this._tags_container;
+ return function(tag_name) {
+ var tag_name = tag_name.replace(/\s+/, ' ').toLowerCase();
+ if ($.inArray(tag_name, me.getSelectedTags()) !== -1) {
+ return;
+ }
+ var tag = new Tag();
+ tag.setName(tag_name);
+ tag.setDeletable(true);
+ tag.setLinkable(true);
+ tag.setDeleteHandler(function(){
+ me.removeSelectedTag(tag_name);
+ tag.dispose();
+ });
+ tags_container.append(tag.getElement());
+ me.addSelectedTag(tag_name);
+ };
+};
+
+TagEditor.prototype.decorate = function(element) {
+ this._element = element;
+ this._tags_input = element.find('input[name="tags"]');
+ this._tags_container = element.find('ul.tags');
+ this._prompt = element.find('.enter-tags-prompt');
+};
+
+var CategorySelector = function() {
+ WrappedElement.call(this);
+ this._data = null;
+ this._is_editable = false;
+ this._select_handler = function(){};//dummy default
+};
+inherits(CategorySelector, WrappedElement);
+
+CategorySelector.prototype.setData = function(data) {
+ this._data = data;
+};
+
+CategorySelector.prototype.setEditable = function(is_editable) {
+ this._is_editable = is_editable;
+};
+
+CategorySelector.prototype.populateCategoryLevel = function(source_path) {
+ var level = source_path.length - 1;
+ if (level > 3) {
+ return;
+ }
+ //traverse the tree down to items pointed to by the path
+ var data = this._data[0];
+ for (var i = 1; i < source_path.length; i++) {
+ data = data[1][source_path[i]];
+ }
+
+ var items = data[1];
+
+ //clear current and above selectors
+ for (var i = level; i < 3; i++) {
+ this._selectors[i].removeAllItems();
+ }
+
+ //populate current selector
+ var selector = this._selectors[level];
+ $.each(items, function(idx, item) {
+ var tag_name = item[0];
+ selector.addItem(idx, tag_name, '');//first and last are dummy values
+ if (item[1].length > 0) {
+ selector.setItemClass(idx, 'tree');
+ }
+ });
+
+ selector.clearSelection();
+};
+
+CategorySelector.prototype.maybeAddEditButton = function(selector) {
+ return;
+};
+
+CategorySelector.prototype.getSelectedPath = function(selected_level) {
+ var path = [0];//root
+ /*
+ * upper limit capped by current clicked level
+ * we ignore all selection above the current level
+ */
+ for (var i = 0; i < selected_level + 1; i++) {
+ var data = this._selectors[i].getSelectedItemData();
+ if (data){
+ path.push(parseInt(data['id']));
+ } else {
+ return path;
+ }
+ }
+ return path;
+};
+
+/** getter and setter are not symmetric */
+CategorySelector.prototype.setSelectHandler = function(handler) {
+ this._select_handler = handler;
+};
+
+CategorySelector.prototype.getSelectHandlerInternal = function() {
+ return this._select_handler;
+};
+
+CategorySelector.prototype.getSelectHandler = function(level) {
+ var me = this;
+ return function(item_data) {
+ //1) run the assigned select handler
+ var tag_name = item_data['title']
+ me.getSelectHandlerInternal()(tag_name);
+ //2) if appropriate, populate the higher level
+ if (level < 2) {
+ var current_path = me.getSelectedPath(level);
+ me.populateCategoryLevel(current_path);
+ }
+ }
+};
+
+CategorySelector.prototype.decorate = function(element) {
+ this._element = element;
+ this._selectors = [];
+
+ var selector0 = new SelectBox();
+ selector0.decorate(element.find('.cat-col-0'));
+ selector0.setSelectHandler(this.getSelectHandler(0));
+ this.maybeAddEditButton(selector0);
+ this._selectors.push(selector0);
+
+ var selector1 = new SelectBox();
+ selector1.decorate(element.find('.cat-col-1'));
+ selector1.setSelectHandler(this.getSelectHandler(1));
+ this.maybeAddEditButton(selector1);
+ this._selectors.push(selector1)
+
+ var selector2 = new SelectBox();
+ selector2.decorate(element.find('.cat-col-2'));
+ selector2.setSelectHandler(this.getSelectHandler(2));
+ this.maybeAddEditButton(selector2);
+ this._selectors.push(selector2);
+
+ this.populateCategoryLevel([0]);
+};
+
$(document).ready(function() {
$('[id^="comments-for-"]').each(function(index, element){
var comments = new PostCommentsWidget();
diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js
index 297e3f9a..e740287e 100644
--- a/askbot/skins/common/media/js/utils.js
+++ b/askbot/skins/common/media/js/utils.js
@@ -833,6 +833,7 @@ TwoStateToggle.prototype.decorate = function(element){
var SelectBox = function(){
WrappedElement.call(this);
this._items = [];
+ this._select_handler = function(){};//empty default
};
inherits(SelectBox, WrappedElement);
@@ -842,10 +843,19 @@ SelectBox.prototype.removeItem = function(id){
item.remove();
};
+SelectBox.prototype.removeAllItems = function() {
+ $(this._element.find('li')).remove();
+};
+
SelectBox.prototype.getItem = function(id){
return $(this._element.find('li[data-item-id="' + id + '"]'));
};
+SelectBox.prototype.setItemClass = function(id, css_class) {
+ this.getItem(id).addClass(css_class);
+};
+
+/** @todo: rename to setItem?? have a problem when id's are all say 0 */
SelectBox.prototype.addItem = function(id, title, details){
/*this._items.push({
id: id,
@@ -858,47 +868,63 @@ SelectBox.prototype.addItem = function(id, title, details){
if (li.length !== 1){
li = this.makeElement('li');
new_li = true;
+ this._element.append(li);
}
li.attr('data-item-id', id)
.attr('data-original-title', details)
.html(title);
- if (new_li){
- this._element.append(li);
- }
this.selectItem($(li));
var me = this;
setupButtonEventHandlers(
$(li),
- function(){
- me.selectItem($(li));
- }
+ me.getSelectHandler($(li))
);
}
};
SelectBox.prototype.getSelectedItemData = function(){
var item = $(this._element.find('li.selected')[0]);
- return {
- id: item.attr('data-item-id'),
- title: item.html(),
- details: item.attr('data-original-title')
- };
+ if (item.length === 0) {
+ return null;
+ } else {
+ return {
+ id: item.attr('data-item-id'),
+ title: item.html(),
+ details: item.attr('data-original-title')
+ };
+ }
};
SelectBox.prototype.selectItem = function(item){
- this._element.find('li').removeClass('selected');
+ this.clearSelection();
item.addClass('selected');
};
+SelectBox.prototype.clearSelection = function(){
+ this._element.find('li').removeClass('selected');
+};
+
+SelectBox.prototype.setSelectHandler = function(handler) {
+ this._select_handler = handler;
+};
+
+SelectBox.prototype.getSelectHandler = function(item) {
+ var me = this;
+ var handler = this._select_handler;
+ return function(){
+ me.selectItem(item);
+ var data = me.getSelectedItemData();
+ handler(data);
+ };
+};
+
SelectBox.prototype.decorate = function(element){
this._element = element;
var me = this;
this._element.find('li').each(function(itx, item){
setupButtonEventHandlers(
$(item),
- function(){
- me.selectItem($(item));
- }
+ me.getSelectHandler($(item))
);
});
};
diff --git a/askbot/skins/common/templates/widgets/edit_post.html b/askbot/skins/common/templates/widgets/edit_post.html
index 66f79237..cb62e674 100644
--- a/askbot/skins/common/templates/widgets/edit_post.html
+++ b/askbot/skins/common/templates/widgets/edit_post.html
@@ -17,31 +17,36 @@
{# need label element for resizable input, b/c form validation won't find span #}
{% if post_type == 'question' %}
<div class="form-item">
- {% if tags_are_required %}
- <label for=id_tags">
- {% if mandatory_tags %}
- <strong>{% trans %}tags{% endtrans %}</strong>
- {% trans %}, one of these is required{% endtrans %}
- {{
- tag_list_widget(
- mandatory_tags,
- make_links = False,
- css_class = 'clearfix'
- )
- }}
+ {% if use_category_selector %}
+ {% include "widgets/tag_editor.html" %}
+ {% include "widgets/three_column_category_selector.html" %}
+ {% else %}
+ {% if tags_are_required %}
+ <label for=id_tags">
+ {% if mandatory_tags %}
+ <strong>{% trans %}tags{% endtrans %}</strong>
+ {% trans %}, one of these is required{% endtrans %}
+ {{
+ tag_list_widget(
+ mandatory_tags,
+ make_links = False,
+ css_class = 'clearfix'
+ )
+ }}
+ {% else %}
+ <strong>{% trans %}tags:{% endtrans %}</strong>
+ {% trans %}(required){% endtrans %}
+ {% endif %}
+ </label>
{% else %}
- <strong>{% trans %}tags:{% endtrans %}</strong>
- {% trans %}(required){% endtrans %}
+ <strong>{% trans %}tags:{% endtrans %}</strong>
{% endif %}
- </label>
- {% else %}
- <strong>{% trans %}tags:{% endtrans %}</strong>
+ <span class="form-error">{{ post_form.tags.errors }}</span><br/>
+ {{ post_form.tags }}
+ <div class="title-desc">
+ {{ post_form.tags.help_text }}
+ </div>
{% endif %}
- <span class="form-error">{{ post_form.tags.errors }}</span><br/>
- {{ post_form.tags }}
- <div class="title-desc">
- {{ post_form.tags.help_text }}
- </div>
</div>
{% endif %}
{% if 'summary' in post_form['fields'] %}
diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less
index 69cdede0..7d62d1b9 100644
--- a/askbot/skins/default/media/style/style.less
+++ b/askbot/skins/default/media/style/style.less
@@ -3538,6 +3538,37 @@ textarea.tipped-input {
}
}
+/* category selector */
+.category-selector {
+ td {
+ vertical-align: top;
+ }
+ li {
+ position: relative;
+ width: 232px;
+ color: #525252;
+ }
+ li.selected.tree:after {
+ content: ">";
+ position: absolute;
+ right: 5px;
+ font-weight: bold;
+ color: #C09853;
+ }
+}
+
+/* tag editor */
+.tag-editor {
+ height: 25px;
+ .enter-tags-prompt {
+ margin: 0px 10px;
+ color: #525252;
+ }
+ ul.tags {
+ margin: 15px 0 0 5px;
+ }
+}
+
/* fixes for bootstrap */
.caret {
margin-bottom: 7px;
diff --git a/askbot/skins/default/templates/ask.html b/askbot/skins/default/templates/ask.html
index 8bec61b7..cc6a4e49 100644
--- a/askbot/skins/default/templates/ask.html
+++ b/askbot/skins/default/templates/ask.html
@@ -27,6 +27,22 @@
{% if mandatory_tags %}
{% include "meta/mandatory_tags_js.html" %}
{% endif %}
+ {% if use_category_selector %}
+ <script type='text/javascript'>
+ (function(){
+ var selector = new CategorySelector();
+ selector.setData(JSON.parse("{{category_tree_data|escapejs}}"));
+ if (askbot['data']['userIsAdminOrMod']) {
+ selector.setEditable(true);
+ }
+ selector.decorate($('.category-selector'));
+
+ var tag_editor = new TagEditor();
+ tag_editor.decorate($('.tag-editor'));
+ selector.setSelectHandler(tag_editor.getAddTagHandler());
+ })();
+ </script>
+ {% endif %}
<script type='text/javascript'>
askbot['urls']['api_get_questions'] = '{% url api_get_questions %}';
{% if settings.ENABLE_MATHJAX or settings.MARKUP_CODE_FRIENDLY %}
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 4bae1e45..cf7529f1 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -460,7 +460,8 @@ for the purposes of the AJAX comment editor #}
post_form,
post_type = None,
mandatory_tags = None,
- edit_title = False
+ edit_title = False,
+ use_category_selector = False
)
-%}
{%include "widgets/edit_post.html" %}
diff --git a/askbot/skins/default/templates/widgets/ask_form.html b/askbot/skins/default/templates/widgets/ask_form.html
index b8a5ce2c..f031c2cf 100644
--- a/askbot/skins/default/templates/widgets/ask_form.html
+++ b/askbot/skins/default/templates/widgets/ask_form.html
@@ -26,7 +26,8 @@
form,
post_type = 'question',
edit_title = False,
- mandatory_tags = mandatory_tags
+ mandatory_tags = mandatory_tags,
+ use_category_selector = use_category_selector
)
}}
<div class="question-options">
diff --git a/askbot/skins/default/templates/widgets/tag_editor.html b/askbot/skins/default/templates/widgets/tag_editor.html
new file mode 100644
index 00000000..5c84a7c5
--- /dev/null
+++ b/askbot/skins/default/templates/widgets/tag_editor.html
@@ -0,0 +1,27 @@
+{% import "macros.html" as macros %}
+<div class="tag-editor">
+ {{ macros.tag_list_widget(
+ tag_names,
+ deletable = True,
+ make_links = False
+ )
+ }}
+ <div class="clearfix"></div>
+ <input
+ id="id_tags"
+ name="tags"
+ type="hidden"
+ value="{% if tag_names %}{{tag_names|join(' ')}}{% endif %}"
+ />
+ <span class="form-error" style="display:block;"></span>
+ <p class="enter-tags-prompt"
+ {% if tag_names %}style="display:none"{% endif %}
+ >
+ {% trans %}Please label your question with the categories below:{% endtrans %}
+ </p>
+ {#
+ tag input is hidden because we want to edit the list visually
+ we also want to eventually allow multiword tags, which are
+ not easy to enter into a simple input box
+ #}
+</div>
diff --git a/askbot/skins/default/templates/widgets/three_column_category_selector.html b/askbot/skins/default/templates/widgets/three_column_category_selector.html
new file mode 100644
index 00000000..2eb9d728
--- /dev/null
+++ b/askbot/skins/default/templates/widgets/three_column_category_selector.html
@@ -0,0 +1,10 @@
+{# just a skeleton for the category selector - filled by js #}
+<table class="category-selector">
+ <tbody>
+ <tr>
+ <td><ul class="select-box cat-col-0"></ul></td>
+ <td><ul class="select-box cat-col-1"></ul></td>
+ <td><ul class="select-box cat-col-2"></ul></td>
+ </tr>
+ </tbody>
+</table>
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index 44b80ed9..d4dea8f0 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -26,6 +26,7 @@ from django.views.decorators import csrf
from askbot import forms
from askbot import models
+from askbot.conf import settings as askbot_settings
from askbot.skins.loaders import render_into_skin
from askbot.utils import decorators
from askbot.utils.functions import diff_date
@@ -259,12 +260,47 @@ def ask(request):#view used to ask a new question
'is_anonymous': request.REQUEST.get('is_anonymous', False),
}
+ if askbot_settings.TAG_SOURCE == 'category-tree':
+ category_tree_data = askbot_settings.CATEGORY_TREE
+ else:
+ category_tree_data = None
+
+ cat_tree = [
+ ['dummy',
+ [
+ ['tires', [
+ ['michelin', [
+ ['trucks', []],
+ ['cars', []],
+ ['motorcycles', []]
+ ]
+ ],
+ ['good year', []],
+ ['honda', []],
+ ]
+ ],
+ ['abandonment', []],
+ ['chile', []],
+ ['vulcanization', []],
+ ]
+ ]
+ ]
+ """
+ cat_tree = [
+ ['dummy', []]
+ ]
+ """
+ category_tree_data = simplejson.dumps(cat_tree)
+
data = {
'active_tab': 'ask',
'page_class': 'ask-page',
'form' : form,
'mandatory_tags': models.tag.get_mandatory_tags(),
'email_validation_faq_url':reverse('faq') + '#validate',
+ 'use_category_selector': (askbot_settings.TAG_SOURCE == 'category-tree'),
+ 'category_tree_data': category_tree_data,
+ 'tag_names': list()#need to keep context in sync with edit_question for tag editor
}
return render_into_skin('ask.html', data, request)
@@ -401,6 +437,7 @@ def edit_question(request, id):
'revision_form': revision_form,
'mandatory_tags': models.tag.get_mandatory_tags(),
'form' : form,
+ 'tag_names': question.thread.get_tag_names()
}
return render_into_skin('question_edit.html', data, request)