diff options
-rw-r--r-- | askbot/conf/flatpages.py | 11 | ||||
-rw-r--r-- | askbot/conf/forum_data_rules.py | 16 | ||||
-rw-r--r-- | askbot/models/tag.py | 27 | ||||
-rw-r--r-- | askbot/skins/common/media/js/post.js | 175 | ||||
-rw-r--r-- | askbot/skins/common/media/js/utils.js | 56 | ||||
-rw-r--r-- | askbot/skins/common/templates/widgets/edit_post.html | 49 | ||||
-rw-r--r-- | askbot/skins/default/media/style/style.less | 31 | ||||
-rw-r--r-- | askbot/skins/default/templates/ask.html | 16 | ||||
-rw-r--r-- | askbot/skins/default/templates/macros.html | 3 | ||||
-rw-r--r-- | askbot/skins/default/templates/widgets/ask_form.html | 3 | ||||
-rw-r--r-- | askbot/skins/default/templates/widgets/tag_editor.html | 27 | ||||
-rw-r--r-- | askbot/skins/default/templates/widgets/three_column_category_selector.html | 10 | ||||
-rw-r--r-- | askbot/views/writers.py | 37 |
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) |