diff options
42 files changed, 864 insertions, 299 deletions
diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py index 229edf96..19b5e69d 100644 --- a/askbot/conf/minimum_reputation.py +++ b/askbot/conf/minimum_reputation.py @@ -195,15 +195,6 @@ settings.register( settings.register( livesettings.IntegerValue( MIN_REP, - 'MIN_REP_TO_LOCK_POSTS', - default=400, - description=_('Lock posts') - ) -) - -settings.register( - livesettings.IntegerValue( - MIN_REP, 'MIN_REP_TO_HAVE_STRONG_URL', default=25, description=_('Remove rel=nofollow from own homepage'), diff --git a/askbot/conf/settings_wrapper.py b/askbot/conf/settings_wrapper.py index b6b5f302..0a4ba45f 100644 --- a/askbot/conf/settings_wrapper.py +++ b/askbot/conf/settings_wrapper.py @@ -53,6 +53,10 @@ class ConfigSettings(object): """return the defalut value for the setting""" return getattr(self.__instance, key).default + def get_description(self, key): + """returns descriptive title of the setting""" + return unicode(getattr(self.__instance, key).description) + def reset(self, key): """returns setting to the default value""" self.update(key, self.get_default(key)) diff --git a/askbot/conf/sidebar_main.py b/askbot/conf/sidebar_main.py index 8fa4bdf0..97b89e37 100644 --- a/askbot/conf/sidebar_main.py +++ b/askbot/conf/sidebar_main.py @@ -32,6 +32,15 @@ settings.register( settings.register( values.BooleanValue( SIDEBAR_MAIN, + 'SIDEBAR_MAIN_HEADER_ANON_ONLY', + description=_('Show above only to anonymous users'), + default=False + ) +) + +settings.register( + values.BooleanValue( + SIDEBAR_MAIN, 'SIDEBAR_MAIN_SHOW_AVATARS', description = _('Show avatar block in sidebar'), help_text = _( @@ -94,3 +103,12 @@ settings.register( ) ) +settings.register( + values.BooleanValue( + SIDEBAR_MAIN, + 'SIDEBAR_MAIN_FOOTER_ANON_ONLY', + default=False, + description=_('Show above only to anonymous users') + ) +) + diff --git a/askbot/conf/sidebar_profile.py b/askbot/conf/sidebar_profile.py index 948daa4a..a216de4b 100644 --- a/askbot/conf/sidebar_profile.py +++ b/askbot/conf/sidebar_profile.py @@ -16,8 +16,8 @@ SIDEBAR_PROFILE = ConfigurationGroup( settings.register( values.LongStringValue( SIDEBAR_PROFILE, - 'SIDEBAR_PROFILE_HEADER', - description = _('Custom sidebar header'), + 'SIDEBAR_PROFILE', + description = _('Custom sidebar'), default = '', help_text = _( 'Use this area to enter content at the TOP of the sidebar' @@ -25,23 +25,16 @@ settings.register( '(as well as the sidebar footer), please ' 'use the HTML validation service to make sure that ' 'your input is valid and works well in all browsers.' - ) + ) ) ) + settings.register( - values.LongStringValue( + values.BooleanValue( SIDEBAR_PROFILE, - 'SIDEBAR_PROFILE_FOOTER', - description = _('Custom sidebar footer'), - default = '', - help_text = _( - 'Use this area to enter content at the BOTTOM of the sidebar' - 'in HTML format. When using this option ' - '(as well as the sidebar header), please ' - 'use the HTML validation service to make sure that ' - 'your input is valid and works well in all browsers.' - ) + 'SIDEBAR_PROFILE_ANON_ONLY', + description=_('Show above only to anonymous users'), + default=False ) ) - diff --git a/askbot/conf/sidebar_question.py b/askbot/conf/sidebar_question.py index ffe2f783..8d38692a 100644 --- a/askbot/conf/sidebar_question.py +++ b/askbot/conf/sidebar_question.py @@ -27,6 +27,15 @@ settings.register( ) settings.register( + values.BooleanValue( + SIDEBAR_QUESTION, + 'QUESTION_PAGE_TOP_BANNER_ANON_ONLY', + default=False, + description=_('Show above only to anonymous users'), + ) +) + +settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'QUESTION_PAGE_ANSWER_BANNER', @@ -42,6 +51,16 @@ settings.register( ) settings.register( + values.BooleanValue( + SIDEBAR_QUESTION, + 'QUESTION_PAGE_ANSWER_BANNER_ANON_ONLY', + default=False, + description=_('Show above only to anonymous users'), + ) +) + + +settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'SIDEBAR_QUESTION_HEADER', @@ -60,6 +79,15 @@ settings.register( settings.register( values.BooleanValue( SIDEBAR_QUESTION, + 'SIDEBAR_QUESTION_HEADER_ANON_ONLY', + default=False, + description=_('Show above only to anonymous users') + ) +) + +settings.register( + values.BooleanValue( + SIDEBAR_QUESTION, 'SIDEBAR_QUESTION_SHOW_TAGS', description = _('Show tag list in sidebar'), help_text = _( @@ -113,3 +141,11 @@ settings.register( ) ) +settings.register( + values.BooleanValue( + SIDEBAR_QUESTION, + 'SIDEBAR_QUESTION_FOOTER_ANON_ONLY', + default=False, + description=_('Show above only to anonymous users') + ) +) diff --git a/askbot/conf/site_modes.py b/askbot/conf/site_modes.py index 7c81341e..feadd32b 100644 --- a/askbot/conf/site_modes.py +++ b/askbot/conf/site_modes.py @@ -27,7 +27,6 @@ LARGE_SITE_MODE_SETTINGS = { 'MIN_REP_TO_EDIT_OTHERS_POSTS': 2000, 'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS': 2000, 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS': 2000, - 'MIN_REP_TO_LOCK_POSTS': 4000, 'MIN_REP_TO_HAVE_STRONG_URL': 250, #badge settings 'NOTABLE_QUESTION_BADGE_MIN_VIEWS': 250, diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index cda70e77..48afb5ea 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,9 @@ Changes in Askbot Development version ------------------- +* Added management command `apply_hinted_tags` to batch-apply tags from a list +* Added hovercard on the user's karma display in the header +* Added option to hide ad blocks from logged in users * Applied Askbot templates to the settings control panel * Added option to auto-follow questions by the question posters with default "on" * Support for Django 1.5 diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index 44cd8ccb..85d252bc 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -47,6 +47,7 @@ Programming, bug fixes and documentation * `Jorge López Pérez <https://github.com/adobo>`_ * `Zafer Cakmak <https://github.com/xaph>`_ * `Kevin Porterfield <http://www.shotgunsoftware.com>_` +* `Robert Martin <https://github.com/bobbydavid>_` Translations ------------ diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst index cc5e952f..da93dcb5 100644 --- a/askbot/doc/source/management-commands.rst +++ b/askbot/doc/source/management-commands.rst @@ -25,6 +25,11 @@ The bulk of the management commands fall into this group and will probably be th | `add_admin <user_id>` | Turn user into an administrator | | | `<user_id>` is a numeric user id of the account | +---------------------------------+-------------------------------------------------------------+ +| `apply_hinted_tags | Apply tags to all questions in batch given the list of tags | +| --tag-names <file>` | provided with a file. The file must contain tags - | +| | one per line. If many tags match - only the most frequent | +| | will be selected. | ++---------------------------------+-------------------------------------------------------------+ | `remove_admin <user_id>` | Remove admin status from a user account - the opposite of | | | the `add_admin` command | +---------------------------------+-------------------------------------------------------------+ diff --git a/askbot/management/commands/apply_hinted_tags.py b/askbot/management/commands/apply_hinted_tags.py new file mode 100644 index 00000000..94bf2383 --- /dev/null +++ b/askbot/management/commands/apply_hinted_tags.py @@ -0,0 +1,58 @@ +import datetime +from django.core.management.base import BaseCommand +from django.core.management.base import CommandError +from optparse import make_option +from askbot.utils.console import ProgressBar +from askbot.models import Thread +from askbot.models import User + +class Command(BaseCommand): + help = """Adds tags to questions. Tags should be given via a file + with one tag per line. The tags will be matched with the words + found in the question title. Then, most frequently used matching tags + will be applied. This command respects the maximum number of tags + allowed per question. + """ + option_list = BaseCommand.option_list + ( + make_option('--tags-file', '-t', + action = 'store', + type = 'str', + dest = 'tags_file', + default = None, + help = 'file containing tag names, one per line' + ), + ) + def handle(self, *args, **kwargs): + """reads the tags file, parses it, + then applies tags to questions by matching them + with the question titles and content + """ + if kwargs['tags_file'] is None: + raise CommandError('parameter --tags-file is required') + try: + tags_input = open(kwargs['tags_file']).read() + except IOError: + raise CommandError('file "%s" not found' % kwargs['tags_file']) + + tags_list = map(lambda v: v.strip(), tags_input.split('\n')) + + multiword_tags = list() + for tag in tags_list: + if ' ' in tag: + multiword_tags.append(tag) + + if len(multiword_tags): + message = 'multiword tags tags not allowed, have: %s' % ', '.join(multiword_tags) + raise CommandError(message) + + threads = Thread.objects.all() + count = threads.count() + message = 'Applying tags to questions' + + user = User.objects.all().order_by('-id')[0] + now = datetime.datetime.now() + + for thread in ProgressBar(threads.iterator(), count, message): + thread.apply_hinted_tags( + tags_list, user=user, timestamp=now, silent=True + ) diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index b7432296..06f40c65 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -29,7 +29,8 @@ def get_all_origin_posts(mentions): def extend_question_list( src, dst, cutoff_time = None, limit=False, add_mention=False, - add_comment = False + add_comment = False, + languages=None ): """src is a query set with questions or None @@ -48,6 +49,8 @@ def extend_question_list( raise ValueError('cutoff_time is a mandatory parameter') for q in src: + if languages and src.language_code not in languages: + continue if q in dst: meta_data = dst[q] else: @@ -162,6 +165,11 @@ class Command(NoArgsCommand): Q_set_A = not_seen_qs Q_set_B = seen_before_last_mod_qs + if django_settings.ASKBOT_MULTILINGUAL: + languages = user.languages.split() + else: + languages = None + for feed in user_feeds: if feed.feed_type == 'm_and_c': #alerts on mentions and comments are processed separately @@ -216,8 +224,8 @@ class Command(NoArgsCommand): q_list = SortedDict() #todo: refactor q_list into a separate class? - extend_question_list(q_sel_A, q_list) - extend_question_list(q_sel_B, q_list) + extend_question_list(q_sel_A, q_list, languages=languages) + extend_question_list(q_sel_B, q_list, languages=languages) #build list of comment and mention responses here #it is separate because posts are not marked as changed @@ -247,8 +255,9 @@ class Command(NoArgsCommand): extend_question_list( q_commented, q_list, - cutoff_time = cutoff_time, - add_comment = True + cutoff_time=cutoff_time, + add_comment=True, + languages=languages ) mentions = Activity.objects.get_mentions( @@ -267,27 +276,37 @@ class Command(NoArgsCommand): q_mentions_A = Q_set_A.filter(id__in = q_mentions_id) q_mentions_A.cutoff_time = cutoff_time - extend_question_list(q_mentions_A, q_list, add_mention=True) + extend_question_list( + q_mentions_A, + q_list, + add_mention=True, + languages=languages + ) q_mentions_B = Q_set_B.filter(id__in = q_mentions_id) q_mentions_B.cutoff_time = cutoff_time - extend_question_list(q_mentions_B, q_list, add_mention=True) + extend_question_list( + q_mentions_B, + q_list, + add_mention=True, + languages=languages + ) except EmailFeedSetting.DoesNotExist: pass if user.email_tag_filter_strategy == const.INCLUDE_INTERESTING: - extend_question_list(q_all_A, q_list) - extend_question_list(q_all_B, q_list) + extend_question_list(q_all_A, q_list, languages=languages) + extend_question_list(q_all_B, q_list, languages=languages) - extend_question_list(q_ask_A, q_list, limit=True) - extend_question_list(q_ask_B, q_list, limit=True) + extend_question_list(q_ask_A, q_list, limit=True, languages=languages) + extend_question_list(q_ask_B, q_list, limit=True, languages=languages) - extend_question_list(q_ans_A, q_list, limit=True) - extend_question_list(q_ans_B, q_list, limit=True) + extend_question_list(q_ans_A, q_list, limit=True, languages=languages) + extend_question_list(q_ans_B, q_list, limit=True, languages=languages) if user.email_tag_filter_strategy == const.EXCLUDE_IGNORED: - extend_question_list(q_all_A, q_list, limit=True) - extend_question_list(q_all_B, q_list, limit=True) + extend_question_list(q_all_A, q_list, limit=True, languages=languages) + extend_question_list(q_all_B, q_list, limit=True, languages=languages) ctype = ContentType.objects.get_for_model(Post) EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT diff --git a/askbot/media/js/live_search.js b/askbot/media/js/live_search.js index b812070c..2cb62b2b 100644 --- a/askbot/media/js/live_search.js +++ b/askbot/media/js/live_search.js @@ -14,6 +14,14 @@ SearchDropMenu.prototype.setAskHandler = function(handler) { this._askHandler = handler; }; +SearchDropMenu.prototype.setSearchWidget = function(widget) { + this._searchWidget = widget; +}; + +SearchDropMenu.prototype.getSearchWidget = function() { + return this._searchWidget; +}; + SearchDropMenu.prototype.setAskButtonEnabled = function(isEnabled) { this._askButtonEnabled = isEnabled; }; @@ -42,6 +50,11 @@ SearchDropMenu.prototype.render = function() { } }; +SearchDropMenu.prototype.clearSelectedItem = function() { + this._selectedItemIndex = 0; + this._resultsList.find('li').removeClass('selected'); +} + /** * @param {number} idx position of item starting from 1 for the topmost * Selects item inentified by position. @@ -126,7 +139,17 @@ SearchDropMenu.prototype.makeKeyHandler = function() { return false; } } - me.selectItem(curItem); + + var widget = me.getSearchWidget(); + if (curItem === 0) { + //activate key handlers on input box + widget.setFullTextSearchEnabled(true); + me.clearSelectedItem(); + } else { + //deactivate key handlers on input box + widget.setFullTextSearchEnabled(false); + me.selectItem(curItem); + } return false } }; @@ -772,6 +795,14 @@ FullTextSearch.prototype.updateToolTip = function() { } }; +FullTextSearch.prototype.setFullTextSearchEnabled = function(enabled) { + this._fullTextSearchEnabled = enabled; +}; + +FullTextSearch.prototype.getFullTextSearchEnabled = function() { + return this._fullTextSearchEnabled; +}; + /** * keydown handler operates on the tooltip and the X button * also opens and closes drop menu according to the min search word threshold @@ -793,8 +824,12 @@ FullTextSearch.prototype.makeKeyDownHandler = function() { return false; } } else if (keyCode === 13) { - formSubmitHandler(e); - return false; + if (me.getFullTextSearchEnabled()) { + formSubmitHandler(e); + return false; + } else { + return true; + } } var query = me.getSearchQuery(); @@ -856,6 +891,7 @@ FullTextSearch.prototype.decorate = function(element) { this._toolTip = toolTip; var dropMenu = new SearchDropMenu(); + dropMenu.setSearchWidget(this); dropMenu.setAskHandler(this.makeAskHandler()); dropMenu.setAskButtonEnabled(this._askButtonEnabled); this._dropMenu = dropMenu; diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index 0380e6fe..ec3af964 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -59,6 +59,11 @@ var setController = function(controller, name) { askbot['controllers'][name] = controller; }; +var sortChildNodes = function(node, cmpFunc) { + var items = node.children().sort(cmpFunc); + node.append(items); +}; + var getUniqueValues = function(values) { var uniques = new Object(); var out = new Array(); @@ -2011,8 +2016,18 @@ SelectBox.prototype.createDom = function() { var GroupDropdown = function(groups){ WrappedElement.call(this); this._group_list = groups; +}; +inherits(GroupDropdown, WrappedElement); + +GroupDropdown.prototype.createDom = function(){ + this._element = this.makeElement('ul'); + this._element.attr('class', 'dropdown-menu'); + this._element.attr('id', 'groups-dropdown'); + this._element.attr('role', 'menu'); + this._element.attr('aria-labelledby', 'navGroups'); + this._input_box = new TippedInput(); - this._input_box.setInstruction('group name'); + this._input_box.setInstruction(gettext('group name')); this._input_box.createDom(); this._input_box_element = this._input_box.getElement(); this._input_box_element.attr('class', 'group-name'); @@ -2021,115 +2036,128 @@ var GroupDropdown = function(groups){ this._add_link.attr('href', '#'); this._add_link.attr('class', 'group-name'); this._add_link.text(gettext('add new group')); + + for (var i=0; i<this._group_list.length; i++){ + var li = this.makeElement('li'); + var a = this.makeElement('a'); + a.text(this._group_list[i].name); + a.attr('href', this._group_list[i].link); + a.attr('class', 'group-name'); + li.append(a); + this._element.append(li); + } + if (askbot['data']['userIsAdmin']) { + this.enableAddGroups(); + } }; -inherits(GroupDropdown, WrappedElement); -GroupDropdown.prototype.createDom = function(){ - this._element = this.makeElement('ul'); - this._element.attr('class', 'dropdown-menu'); - this._element.attr('id', 'groups-dropdown'); - this._element.attr('role', 'menu'); - this._element.attr('aria-labelledby', 'navGroups'); - - for (i=0; i<this._group_list.length; i++){ - li_element = this.makeElement('li'); - a_element = this.makeElement('a'); - a_element.text(this._group_list[i].name); - a_element.attr('href', this._group_list[i].link); - a_element.attr('class', 'group-name'); - li_element.append(a_element); - this._element.append(li_element); - } +/** + * inserts a link to group with a given url to the group page + * and name + */ +GroupDropdown.prototype.insertGroup = function(group_name, url){ + + //1) take out first and last list elements: + // everyone and the "add group" item + var list = this._element.children(); + var everyoneGroup = list.first().detach(); + var groupAdder = list.last().detach(); + var divider = this._element.find('.divider').detach(); + + //2) append group link into the list + var li = this.makeElement('li'); + var a = this.makeElement('a'); + a.attr('href', url); + a.attr('class', 'group-name'); + a.text(group_name); + li.append(a); + li.hide(); + this._element.append(li); + + //3) sort rest of the list alphanumerically + sortChildNodes( + this._element, + function(a, b) { + var valA = $(a).find('a').text().toLowerCase(); + var valB = $(b).find('a').text().toLowerCase(); + return (valA < valB) ? -1: (valA > valB) ? 1: 0; + } + ); + + //a dramatic effect + li.fadeIn(); + + //4) reinsert the first and last elements of the list: + this._element.prepend(everyoneGroup); + this._element.append(divider); + this._element.append(groupAdder); }; -GroupDropdown.prototype.decorate = function(element){ - this._element = element; - this._element.attr('class', 'dropdown-menu'); - this._element.attr('id', 'groups-dropdown'); - this._element.attr('role', 'menu'); - this._element.attr('aria-labelledby', 'navGroups'); - - for (i=0; i<this._group_list.length; i++){ - li_element = this.makeElement('li'); - a_element = this.makeElement('a'); - a_element.text(this._group_list[i].name); - a_element.attr('href', this._group_list[i].link); - a_element.attr('class', 'group-name'); - li_element.append(a_element); - this._element.append(li_element); - } +GroupDropdown.prototype.setState = function(state) { + if (state === 'display') { + this._input_box_element.hide(); + this._add_link.show(); + } }; -GroupDropdown.prototype.insertGroup = function(group_name, url){ - var new_group_li = this.makeElement('li'); - new_group_a = this.makeElement('a'); - new_group_a.attr('href', url); - new_group_a.attr('class', 'group-name'); - new_group_a.text(group_name); - new_group_li.append(new_group_a); - links_array = this._element.find('a') - for (i=1; i < links_array.length; i++){ - var listedName = links_array[i].text; - var cleanedListedName = listedName.toLowerCase(); - var cleanedNewName = group_name.toLowerCase() - if (listedName < newName) { - if (i == links_array.length - 1){ - new_group_li.insertAfter(this._element.find('li')[i-1]) - break; - } else { - continue; - } - } else if (cleanedNewName === cleanedNewName) { - var message = interpolate(gettext( - 'Group %(name)s already exists. Group names are case-insensitive.' - ), {'name': listedName}, true - ); - notify.show(message); - return; - } else { - new_group_li.insertAfter(this._element.find('li')[i-1]) - break; +GroupDropdown.prototype.hasGroup = function(groupName) { + var items = this._element.find('li'); + for (var i=1; i < items.length - 1; i++) { + var cGroupName = $(items[i]).find('a').text(); + if (cGroupName.toLowerCase() === groupName.toLowerCase()) { + return true; } } + return false; }; GroupDropdown.prototype._add_group_handler = function(group_name){ - var group_name = this._input_box_element.val(); - self = this; - if (!group_name){ - return; - } + var group_name = this._input_box_element.val(); + var me = this; + if (!group_name){ + return; + } - $.ajax({ - type: 'POST', - url: askbot['urls']['add_group'], - data: {group: group_name}, - success: function(data){ - if (data.success){ - self.insertGroup(data.group_name, data.url); - self._input_box_element.hide(); - self._add_link.show(); - return true; - } else{ - return false; - } - }, - error: function(){console.log('error');} - }); + $.ajax({ + type: 'POST', + url: askbot['urls']['add_group'], + data: {group: group_name}, + success: function(data){ + if (data['success']){ + var groupName = data['group_name']; + if (me.hasGroup(groupName)) { + var message = interpolate(gettext( + 'Group %(name)s already exists. Group names are case-insensitive.' + ), {'name': groupName}, true + ); + notify.show(message); + return false; + } else { + me.insertGroup(data['group_name'], data['url']); + me.setState('display'); + return true; + } + } else{ + notify.show(data['message']); + return false; + } + }, + error: function(){console.log('error');} + }); }; GroupDropdown.prototype.enableAddGroups = function(){ var self = this; this._add_link.click(function(){ - self._add_link.hide(); - self._input_box_element.show(); - self._input_box_element.focus(); + self._add_link.hide(); + self._input_box_element.show(); + self._input_box_element.focus(); }); this._input_box_element.keydown(function(event){ - if (event.which == 13 || event.keyCode==13){ - self._add_group_handler(); - self._input_box_element.val(''); - } + if (event.which == 13 || event.keyCode==13){ + self._add_group_handler(); + self._input_box_element.val(''); + } }); var divider = this.makeElement('li'); @@ -2297,6 +2325,149 @@ Tag.prototype.createDom = function(){ } }; +var PermsHoverCard = function() { + WrappedElement.call(this); + this._isLoaded = false; +}; +inherits(PermsHoverCard, WrappedElement); + +PermsHoverCard.prototype.setContent = function(data) { + this._element.html(data['html']); +}; + +PermsHoverCard.prototype.setTrigger = function(trigger) { + this._trigger = trigger; +}; + +PermsHoverCard.prototype.setPosition = function() { + var trigger = this._trigger.getElement(); + var coors = trigger.offset(); + var height = trigger.outerHeight(); + var triangle = this._element.find('.triangle') + var triangleHeight = triangle.outerHeight(); + this._element.css({ + 'top': coors.top + height + triangleHeight, + 'left': coors.left + }); +}; + +PermsHoverCard.prototype.setUrl = function(url) { + this._url = url; +}; + +PermsHoverCard.prototype.startLoading = function(onLoad) { + var me = this; + $.ajax({ + type: 'GET', + dataType: 'json', + cache: false, + url: this._url, + success: function(data) { + if (data['success']) { + me.setContent(data); + me.setPosition(); + onLoad(); + me.setIsLoaded(); + } else { + notify.show(data['message']); + } + } + }); +}; + +PermsHoverCard.prototype.isLoaded = function() { + return this._isLoaded; +}; + +PermsHoverCard.prototype.setIsLoaded = function() { + this._isLoaded = true; +}; + +PermsHoverCard.prototype.getOpenHandler = function() { + var me = this; + return function() { + me.clearCancelOpenTimeout(); + if (me.isLoaded()) { + me.getElement().show(); + } else { + var onload = function() { + me.getElement().show(); + } + me.startLoading(onload); + } + }; +}; + +PermsHoverCard.prototype.setCancelOpenTimoutId = function(timeoutId) { + this._cancelOpenTimeoutId = timeoutId; +}; + +PermsHoverCard.prototype.clearCancelOpenTimeout = function() { + var timeout = this._cancelOpenTimeoutId; + if (timeout) { + clearTimeout(timeout); + } +}; + +PermsHoverCard.prototype.getCloseHandler = function() { + var me = this; + return function() { + var element = me.getElement(); + //start timeout to close + var timeout = setTimeout(function() { + element.hide(); + //element.fadeOut('fast'); + }, 200); + me.setCancelOpenTimoutId(timeout); + }; +}; + +PermsHoverCard.prototype.getImmediateCloseHandler = function() { + var me = this; + return function() { + me.getElement().hide(); + }; +}; + +PermsHoverCard.prototype.getKeepHandler = function() { + var me = this; + return function() { + me.clearCancelOpenTimeout(); + }; +}; + +PermsHoverCard.prototype.createDom = function() { + var element = this.makeElement('div'); + this._element = element; + element.addClass('hovercard'); + element.hover( + this.getKeepHandler(), + this.getCloseHandler() + ); + this._element.hide(); +}; + +var ShowPermsTrigger = function() { + WrappedElement.call(this); +}; +inherits(ShowPermsTrigger, WrappedElement); + +ShowPermsTrigger.prototype.decorate = function(element) { + this._element = element; + var hoverCard = new PermsHoverCard(); + this._hoverCard = hoverCard; + $('body').append(hoverCard.getElement()); + + hoverCard.setTrigger(this); + hoverCard.setUrl(element.data('url')); + + var onEnter = hoverCard.getOpenHandler(); + var onExit = hoverCard.getCloseHandler(); + element.hover(onEnter, onExit); + var onClose = hoverCard.getImmediateCloseHandler(); + $('body').click(onClose); +}; + //Search Engine Keyword Highlight with Javascript //http://scott.yang.id.au/code/se-hilite/ Hilite={elementid:"content",exact:true,max_nodes:1000,onload:true,style_name:"hilite",style_name_suffix:true,debug_referrer:""};Hilite.search_engines=[["local","q"],["cnprog\\.","q"],["google\\.","q"],["search\\.yahoo\\.","p"],["search\\.msn\\.","q"],["search\\.live\\.","query"],["search\\.aol\\.","userQuery"],["ask\\.com","q"],["altavista\\.","q"],["feedster\\.","q"],["search\\.lycos\\.","q"],["alltheweb\\.","q"],["technorati\\.com/search/([^\\?/]+)",1],["dogpile\\.com/info\\.dogpl/search/web/([^\\?/]+)",1,true]];Hilite.decodeReferrer=function(d){var g=null;var e=new RegExp("");for(var c=0;c<Hilite.search_engines.length;c++){var f=Hilite.search_engines[c];e.compile("^http://(www\\.)?"+f[0],"i");var b=d.match(e);if(b){var a;if(isNaN(f[1])){a=Hilite.decodeReferrerQS(d,f[1])}else{a=b[f[1]+1]}if(a){a=decodeURIComponent(a);if(f.length>2&&f[2]){a=decodeURIComponent(a)}a=a.replace(/\'|"/g,"");a=a.split(/[\s,\+\.]+/);return a}break}}return null};Hilite.decodeReferrerQS=function(f,d){var b=f.indexOf("?");var c;if(b>=0){var a=new String(f.substring(b+1));b=0;c=0;while((b>=0)&&((c=a.indexOf("=",b))>=0)){var e,g;e=a.substring(b,c);b=a.indexOf("&",c)+1;if(e==d){if(b<=0){return a.substring(c+1)}else{return a.substring(c+1,b-1)}}else{if(b<=0){return null}}}}return null};Hilite.hiliteElement=function(f,e){if(!e||f.childNodes.length==0){return}var c=new Array();for(var b=0;b<e.length;b++){e[b]=e[b].toLowerCase();if(Hilite.exact){c.push("\\b"+e[b]+"\\b")}else{c.push(e[b])}}c=new RegExp(c.join("|"),"i");var a={};for(var b=0;b<e.length;b++){if(Hilite.style_name_suffix){a[e[b]]=Hilite.style_name+(b+1)}else{a[e[b]]=Hilite.style_name}}var d=function(m){var j=c.exec(m.data);if(j){var n=j[0];var i="";var h=m.splitText(j.index);var g=h.splitText(n.length);var l=m.ownerDocument.createElement("SPAN");m.parentNode.replaceChild(l,h);l.className=a[n.toLowerCase()];l.appendChild(h);return l}else{return m}};Hilite.walkElements(f.childNodes[0],1,d)};Hilite.hilite=function(){var a=Hilite.debug_referrer?Hilite.debug_referrer:document.referrer;var b=null;a=Hilite.decodeReferrer(a);if(a&&((Hilite.elementid&&(b=document.getElementById(Hilite.elementid)))||(b=document.body))){Hilite.hiliteElement(b,a)}};Hilite.walkElements=function(d,f,e){var a=/^(script|style|textarea)/i;var c=0;while(d&&f>0){c++;if(c>=Hilite.max_nodes){var b=function(){Hilite.walkElements(d,f,e)};setTimeout(b,50);return}if(d.nodeType==1){if(!a.test(d.tagName)&&d.childNodes.length>0){d=d.childNodes[0];f++;continue}}else{if(d.nodeType==3){d=e(d)}}if(d.nextSibling){d=d.nextSibling}else{while(f>0){d=d.parentNode;f--;if(d.nextSibling){d=d.nextSibling;break}}}}};if(Hilite.onload){if(window.attachEvent){window.attachEvent("onload",Hilite.hilite)}else{if(window.addEventListener){window.addEventListener("load",Hilite.hilite,false)}else{var __onload=window.onload;window.onload=function(){Hilite.hilite();__onload()}}}}; diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 3aab9f21..fbbd206a 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -218,6 +218,43 @@ body.user-messages { text-align: center; margin: 5px 0 8px; } +.hovercard { + background: white; + border: 3px solid #ddd; + -webkit-box-shadow: 0 0 3px #929292; + -moz-box-shadow: 0 0 3px #929292; + box-shadow: 0 0 3px #929292; + font-size: 13px; + display: block; + max-width: 250px; + padding: 10px; + position: absolute; + border-radius: 5px; + -ms-border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -khtml-border-radius: 5px; + z-index: 1001; +} +.hovercard p:last-child { + margin-bottom: 0; +} +.hovercard ul { + margin-bottom: 0; +} +.hovercard ul li { + font-size: 13px; + line-height: 16px; + margin: 5px 0; +} +.hovercard .triangle { + border-left: 5px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #ddd; + height: 0; + margin: -20px 0 10px 0; + width: 0; +} #closeNotify { position: absolute; right: 5px; @@ -354,23 +391,17 @@ body.user-messages { } #metaNav a.group-name { padding: 0px; - float: center; - margin: 5px 0px 5px 10px; + float: none; + margin: 5px 10px; } #metaNav input.group-name { - border: none; - height: 25px; - font-size: 18px; - font-weight: 100; - text-decoration: none; + border: 1px solid #c9c9b5; + color: #464646; display: block; - margin: 0px 10px 0px 10px; - width: 140px; - font-family: 'Open Sans Condensed', Arial, sans-serif; - font-weight: 100; -} -#metaNav input.group-name:focus { - border: none; + font-size: 14px; + height: 25px; + margin: 0px 10px 5px 10px; + padding: 0 5px; } #metaNav a.group-name:hover { background-color: transparent; @@ -386,7 +417,7 @@ body.user-messages { } #metaNav .dropdown-menu { border-top: none; - left: 7%; + left: 33px; z-index: 10100; } #metaNav .dropdown-menu a { @@ -1742,7 +1773,7 @@ ul#related-tags li { } .groups-input, .users-input { - width: 152px; + width: 150px; padding-left: 5px; border: #c9c9b5 1px solid; height: 25px; @@ -1751,7 +1782,7 @@ ul#related-tags li { .add-groups, .add-users { border: 0; - margin-top: -2px; + margin: -2px 0 0 0 !important; } .share-input-col { width: 160px; @@ -1771,32 +1802,6 @@ ul#related-tags li { width: 395px; font-size: 14px; } -.groups-input, -.users-input { - width: 152px; - padding-left: 5px; - border: #c9c9b5 1px solid; - height: 25px; - font-size: 14px; -} -.add-groups, -.add-users { - border: 0; - margin-top: -2px; -} -.add-everyone-group { - text-align: center; - margin: auto; - display: block; - padding: 0 10px; -} -#id_user, -#id_user_author { - height: 25px; - padding-left: 5px; - width: 395px; - font-size: 14px; -} .title-desc { color: #707070; font-size: 13px; @@ -1821,6 +1826,10 @@ ul#related-tags li { margin-right: 7px; } .folded-editor { + box-shadow: inset 0 0 3px 1px #aaa; + -moz-box-shadow: inset 0 0 3px 1px #aaa; + -webkit-box-shadow: inset 0 0 3px 1px #aaa; + cursor: text; height: 100px; outline: none; width: 100%; @@ -1828,6 +1837,12 @@ ul#related-tags li { .folded-editor .editor-proper { display: none; } +.folded-editor.unfolded { + cursor: default; + box-shadow: 0 0 0 0; + -moz-box-shadow: 0 0 0 0; + -webkit-box-shadow: 0 0 0 0; +} .folded-editor p.prompt { margin: 5px 8px; display: block; @@ -2280,7 +2295,9 @@ ul#related-tags li { line-height: 18px; margin-top: -2px; margin-left: 4px; - box-shadow: none; + -webkit-box-shadow: 0 0 0 #929292; + -moz-box-shadow: 0 0 0 #929292; + box-shadow: 0 0 0 #929292; } .question-page .post-controls .answer-convert input:hover, .question-page .answer-controls .answer-convert input:hover { diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index efde976d..4a957dd6 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -232,6 +232,38 @@ body.user-messages { margin: 5px 0 8px; } +.hovercard { + background: white; + border: 3px solid #ddd; + .box-shadow(0, 0, 3px); + font-size: 13px; + display: block; + max-width: 250px; + padding: 10px; + position: absolute; + .rounded-corners(5px); + z-index: 1001; + p:last-child { + margin-bottom: 0; + } + ul { + margin-bottom: 0; + li { + font-size: 13px; + line-height: 16px; + margin: 5px 0; + } + } + .triangle { + border-left: 5px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #ddd; + height: 0; + margin: -20px 0 10px 0; + width: 0; + } +} + #closeNotify { position: absolute; right: 5px; @@ -381,25 +413,18 @@ body.user-messages { a.group-name { padding: 0px; - float:center; - margin:5px 0px 5px 10px; + float: none; + margin: 5px 10px; } - input.group-name{ - border:none; - height: 25px; - font-size: 18px; - font-weight: 100; - text-decoration: none; + input.group-name { + border: 1px solid #c9c9b5; + color: #464646; display: block; - margin: 0px 10px 0px 10px; - width: 140px; - font-family: @main-font; - font-weight: 100; - } - - input.group-name:focus{ - border:none; + font-size: 14px; + height: 25px; + margin: 0px 10px 5px 10px; + padding: 0 5px; } a.group-name:hover{ @@ -418,11 +443,11 @@ body.user-messages { float:left; } - .dropdown-menu{ + .dropdown-menu { border-top: none; - left: 7%; + left: 33px; z-index: 10100; - a{ + a { color: #666; height: 25px; } @@ -1838,17 +1863,17 @@ ul#related-tags li { .groups-input, .users-input { - width:152px; - padding-left:5px; - border:#c9c9b5 1px solid; - height:25px; + width: 150px; + padding-left: 5px; + border: #c9c9b5 1px solid; + height: 25px; font-size: 14px; } .add-groups, .add-users { - border:0; - margin-top:-2px; + border: 0; + margin: -2px 0 0 0 !important; } .share-input-col { @@ -1872,36 +1897,6 @@ ul#related-tags li { font-size:14px; } -.groups-input, -.users-input { - width:152px; - padding-left:5px; - border:#c9c9b5 1px solid; - height:25px; - font-size: 14px; -} - -.add-groups, -.add-users { - border:0; - margin-top:-2px; -} - -.add-everyone-group { - text-align: center; - margin: auto; - display: block; - padding: 0 10px; -} - -#id_user, -#id_user_author { - height:25px; - padding-left:5px; - width:395px; - font-size:14px; -} - .title-desc { color: @info-text; font-size: 13px; @@ -1928,6 +1923,10 @@ ul#related-tags li { } .folded-editor { + box-shadow: inset 0 0 3px 1px #aaa; + -moz-box-shadow: inset 0 0 3px 1px #aaa; + -webkit-box-shadow: inset 0 0 3px 1px #aaa; + cursor: text; height: 100px; outline: none; width: 100%; @@ -1935,6 +1934,13 @@ ul#related-tags li { .editor-proper { display: none; } + + &.unfolded { + cursor: default; + box-shadow: 0 0 0 0; + -moz-box-shadow: 0 0 0 0; + -webkit-box-shadow: 0 0 0 0; + } p.prompt { margin: 5px 8px; display: block; @@ -2401,7 +2407,7 @@ ul#related-tags li { line-height:18px; margin-top:-2px; margin-left:4px; - box-shadow: none; + .box-shadow(0, 0, 0); } .answer-convert input:hover{ diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 8e3e3d01..797aa6a3 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2532,12 +2532,21 @@ def toggle_favorite_question( about processing the "cancel" option another strange thing is that this function unlike others below returns a value + + todo: the on-screen follow and email subscription is not + fully merged yet - see use of FavoriteQuestion and follow/unfollow question + btw, names of the objects/methods is quite misleading ATM """ try: + #this attempts to remove the on-screen follow fave = FavoriteQuestion.objects.get(thread=question.thread, user=self) fave.delete() result = False question.thread.update_favorite_count() + #this removes email subscription + if question.thread.is_followed_by(self): + self.unfollow_question(question) + except FavoriteQuestion.DoesNotExist: if timestamp is None: timestamp = datetime.datetime.now() @@ -2547,6 +2556,11 @@ def toggle_favorite_question( added_at = timestamp, ) fave.save() + + #this removes email subscription + if question.thread.is_followed_by(self) is False: + self.follow_question(question) + result = True question.thread.update_favorite_count() award_badges_signal.send(None, diff --git a/askbot/models/question.py b/askbot/models/question.py index ecb7185a..d85a0b69 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -602,6 +602,64 @@ class Thread(models.Model): self._question_cache = Post.objects.get(post_type='question', thread=self) return self._question_cache + def apply_hinted_tags(self, hints=None, user=None, timestamp=None, silent=False): + """match words in title and body with hints + and apply some of the hints as tags, + so that total number of tags in no more + than the maximum allowed number of tags""" + + #1) see how many tags we're missing, + #if we don't need more we return + existing_tags = self.get_tag_names() + tags_count = len(existing_tags) + if tags_count >= askbot_settings.MAX_TAGS_PER_POST: + return + + #2) get set of words from title and body + post_text = self.title + ' ' + self._question_post().text + post_text = post_text.lower()#normalize + post_words = set(post_text.split()) + + #3) get intersection set + #normalize hints and tags and remember the originals + orig_hints = dict() + for hint in hints: + orig_hints[hint.lower()] = hint + + norm_hints = orig_hints.keys() + norm_tags = map(lambda v: v.lower(), existing_tags) + + common_words = (set(norm_hints) & post_words) - set(norm_tags) + + #4) for each common word count occurances in corpus + counts = dict() + for word in common_words: + counts[word] = sum(map(lambda w: w.lower() == word.lower(), post_words)) + + #5) sort words by count + sorted_words = sorted( + common_words, + lambda a, b: cmp(counts[b], counts[a]) + ) + + #6) extract correct number of most frequently used tags + need_tags = askbot_settings.MAX_TAGS_PER_POST - len(existing_tags) + add_tags = sorted_words[0:need_tags] + add_tags = map(lambda h: orig_hints[h], add_tags) + + tagnames = ' '.join(existing_tags + add_tags) + + if askbot_settings.FORCE_LOWERCASE_TAGS: + tagnames = tagnames.lower() + + self.retag( + retagged_by=user, + retagged_at=timestamp or datetime.datetime.now(), + tagnames =' '.join(existing_tags + add_tags), + silent=silent + ) + + def get_absolute_url(self): return self._question_post().get_absolute_url(thread = self) #question_id = self._question_post().id diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index 258a5989..1427e506 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -307,5 +307,9 @@ if 'ASKBOT_CSS_DEVEL' in locals() and ASKBOT_CSS_DEVEL == True: ) COMPRESS_JS_FILTERS = [] -COMPRESS_PARSER = 'compressor.parser.HtmlParser' +COMPRESS_PARSER = 'compressor.parser.HtmlParser' JINJA2_EXTENSIONS = ('compressor.contrib.jinja2ext.CompressorExtension',) + +# Use syncdb for tests instead of South migrations. Without this, some tests +# fail spuriously in MySQL. +SOUTH_TESTS_MIGRATE = False diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache index f3c0a35b..f30297d7 100644 --- a/askbot/setup_templates/settings.py.mustache +++ b/askbot/setup_templates/settings.py.mustache @@ -27,6 +27,8 @@ DATABASES = { 'PASSWORD': '{{database_password}}', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. + 'TEST_CHARSET': 'utf8', # Setting the character set and collation to utf-8 + 'TEST_COLLATION': 'utf8_general_ci', # is necessary for MySQL tests to work properly. } } @@ -46,7 +48,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #go to the site's live settings and enable the feature #"Email settings" -> "allow asking by email" # -# WARNING: command post_emailed_questions DELETES all +# WARNING: command post_emailed_questions DELETES all # emails from the mailbox each time # do not use your personal mail box here!!! # @@ -125,7 +127,7 @@ ROOT_URLCONF = os.path.basename(os.path.dirname(__file__)) + '.urls' #UPLOAD SETTINGS FILE_UPLOAD_TEMP_DIR = os.path.join( - os.path.dirname(__file__), + os.path.dirname(__file__), 'tmp' ).replace('\\','/') @@ -258,7 +260,7 @@ HAYSTACK_SITECONF = 'askbot.search.haystack' #http://django-haystack.readthedocs.org/en/v1.2.7/settings.html HAYSTACK_SEARCH_ENGINE = 'simple' -TINYMCE_COMPRESSOR = True +TINYMCE_COMPRESSOR = True TINYMCE_SPELLCHECKER = False TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'default/media/js/tinymce/') #TINYMCE_JS_URL = STATIC_URL + 'default/media/js/tinymce/tiny_mce.js' @@ -287,7 +289,7 @@ TINYMCE_DEFAULT_CONFIG = { } #delayed notifications, time in seconds, 15 mins by default -NOTIFICATION_DELAY_TIME = 60 * 15 +NOTIFICATION_DELAY_TIME = 60 * 15 GROUP_MESSAGING = { 'BASE_URL_GETTER_FUNCTION': 'askbot.models.user_get_profile_url', @@ -303,5 +305,9 @@ if 'ASKBOT_CSS_DEVEL' in locals() and ASKBOT_CSS_DEVEL == True: ) COMPRESS_JS_FILTERS = [] -COMPRESS_PARSER = 'compressor.parser.HtmlParser' +COMPRESS_PARSER = 'compressor.parser.HtmlParser' JINJA2_EXTENSIONS = ('compressor.contrib.jinja2ext.CompressorExtension',) + +# Use syncdb for tests instead of South migrations. Without this, some tests +# fail spuriously in MySQL. +SOUTH_TESTS_MIGRATE = False diff --git a/askbot/templates/base.html b/askbot/templates/base.html index 332fa093..93f1c170 100644 --- a/askbot/templates/base.html +++ b/askbot/templates/base.html @@ -12,6 +12,7 @@ {% if settings.GOOGLE_SITEMAP_CODE %} <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> {% endif %} + <meta name="referrer" content="always" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> {% if settings.SITE_FAVICON %} <link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" /> diff --git a/askbot/templates/main_page/sidebar.html b/askbot/templates/main_page/sidebar.html index 7acbe091..610d2b60 100644 --- a/askbot/templates/main_page/sidebar.html +++ b/askbot/templates/main_page/sidebar.html @@ -1,6 +1,6 @@ {% import "macros.html" as macros %} -{% if settings.SIDEBAR_MAIN_HEADER %} +{% if 'SIDEBAR_MAIN_HEADER'|show_block_to(request.user) %} <div class="box"> {{ settings.SIDEBAR_MAIN_HEADER }} </div> @@ -22,7 +22,7 @@ {% include "widgets/related_tags.html" %} {% endif %} -{% if settings.SIDEBARE_MAIN_FOOTER %} +{% if 'SIDEBAR_MAIN_FOOTER'|show_block_to(request.user) %} <div class="box"> {{ settings.SIDEBAR_MAIN_FOOTER }} </div> diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html index eec4256d..5c398358 100644 --- a/askbot/templates/meta/bottom_scripts.html +++ b/askbot/templates/meta/bottom_scripts.html @@ -97,9 +97,11 @@ askbot['urls']['add_group'] = "{% url add_group %}"; var group_dropdown = new GroupDropdown({{ group_list }}); $('.dropdown').append(group_dropdown.getElement()); - if (askbot['data']['userIsAdmin']) { - group_dropdown.enableAddGroups(); - } + } + var userRep = $('#userToolsNav .reputation'); + if (userRep.length) { + var showPermsTrigger = new ShowPermsTrigger(); + showPermsTrigger.decorate(userRep); } }); if (askbot['data']['haveFlashNotifications']) { diff --git a/askbot/templates/meta/html_head_javascript.html b/askbot/templates/meta/html_head_javascript.html index 5d73d175..1a1531f7 100644 --- a/askbot/templates/meta/html_head_javascript.html +++ b/askbot/templates/meta/html_head_javascript.html @@ -7,7 +7,7 @@ {% if request.user.is_authenticated() %} askbot['data']['userId'] = {{ request.user.id }}; askbot['data']['userName'] = '{{ request.user.username|escape }}'; - askbot['data']['userIsAdminOrMod'] = {{ request.user.is_administrator()|as_js_bool }}; + askbot['data']['userIsAdminOrMod'] = {{ request.user.is_administrator_or_moderator()|as_js_bool }}; askbot['data']['userIsAdmin'] = {{ request.user.is_administrator()|as_js_bool }}; askbot['data']['userReputation'] = {{ request.user.reputation }}; {% else %} diff --git a/askbot/templates/question.html b/askbot/templates/question.html index 13593adc..ca2f4022 100644 --- a/askbot/templates/question.html +++ b/askbot/templates/question.html @@ -285,7 +285,7 @@ </script> {% endblock %} {% block content %} - {% if settings.QUESTION_PAGE_TOP_BANNER %} + {% if 'QUESTION_PAGE_TOP_BANNER'|show_block_to(request.user) %} <div class="banner">{{ settings.QUESTION_PAGE_TOP_BANNER|safe }}</div> {% endif %} {% if is_cacheable %} diff --git a/askbot/templates/question/content.html b/askbot/templates/question/content.html index 23286ce0..c8fef9a6 100644 --- a/askbot/templates/question/content.html +++ b/askbot/templates/question/content.html @@ -12,7 +12,7 @@ <div class="clean"></div> {% for answer in answers %} - {% if answers|length > 1 and loop.index == 2 %} + {% if loop.index == 2 and 'QUESTION_PAGE_ANSWER_BANNER'|show_block_to(request.user) %} <div class="banner">{{ settings.QUESTION_PAGE_ANSWER_BANNER|safe }}</div> {% endif %} {% include "question/answer_card.html" %} diff --git a/askbot/templates/question/sidebar.html b/askbot/templates/question/sidebar.html index c11a4336..c4301b6c 100644 --- a/askbot/templates/question/sidebar.html +++ b/askbot/templates/question/sidebar.html @@ -1,5 +1,5 @@ {% import "macros.html" as macros %} -{% if settings.SIDEBAR_QUESTION_HEADER %} +{% if 'SIDEBAR_QUESTION_HEADER'|show_block_to(request.user) %} <div class="box"> {{ settings.SIDEBAR_QUESTION_HEADER }} </div> @@ -26,19 +26,6 @@ {% endif %} </div> <div class="notify-sidebar"> - {%if request.user.is_authenticated() %} - <input - type="checkbox" - id="question-subscribe-sidebar" - {% if thread.is_followed_by(request.user) %} - checked="checked" - {% endif %} - /> - <label for="question-subscribe-sidebar">{% trans %}email the updates{% endtrans %}</label> - {%else%} - <input type="checkbox" id="question-subscribe-sidebar"/> - <label for="question-subscribe-sidebar">{% trans %}<strong>Here</strong> (once you log in) you will be able to sign up for the periodic email updates about this question.{% endtrans %}</label> - {%endif%} {% if settings.RSS_ENABLED %} <p class="rss"> <a @@ -173,7 +160,7 @@ {#% endcache %#} {% endif %} -{% if settings.SIDEBAR_QUESTION_FOOTER %} +{% if 'SIDEBAR_QUESTION_FOOTER'|show_block_to(request.user) %} <div class="box"> {{ settings.SIDEBAR_QUESTION_FOOTER }} </div> diff --git a/askbot/templates/user_profile/user.html b/askbot/templates/user_profile/user.html index 3aee3cfa..c72dc857 100644 --- a/askbot/templates/user_profile/user.html +++ b/askbot/templates/user_profile/user.html @@ -36,11 +36,10 @@ {% endblock %} {% endblock %} {% block sidebar %} -<div class="box"> - {{ settings.SIDEBAR_PROFILE_HEADER }} -</div> -<div class="box"> - {{ settings.SIDEBAR_PROFILE_FOOTER }} -</div> + {% if 'SIDEBAR_PROFILE'|show_block_to(request.user) %} + <div class="box"> + {{ settings.SIDEBAR_PROFILE }} + </div> + {% endif %} {% endblock %} <!-- end of user.html --> diff --git a/askbot/templates/widgets/ask_form.html b/askbot/templates/widgets/ask_form.html index 3ddb07dd..1d5029f1 100644 --- a/askbot/templates/widgets/ask_form.html +++ b/askbot/templates/widgets/ask_form.html @@ -13,6 +13,7 @@ </div> </div> {% set editor_is_folded = ( + settings.QUESTION_BODY_EDITOR_MODE == 'folded' and settings.MIN_QUESTION_BODY_LENGTH == 0 and form.text.value()|is_empty_editor_value() ) diff --git a/askbot/templates/widgets/user_long_score_and_badge_summary.html b/askbot/templates/widgets/user_long_score_and_badge_summary.html index 35e4cb67..771a8f3f 100644 --- a/askbot/templates/widgets/user_long_score_and_badge_summary.html +++ b/askbot/templates/widgets/user_long_score_and_badge_summary.html @@ -1,7 +1,9 @@ {% set have_badges = user.gold or user.silver or user.bronze %} {%- if karma_mode != 'hidden' -%} -<a class="user-micro-info" - href="{{user.get_absolute_url()}}?sort=reputation" +<a + class="user-micro-info reputation" + href="{{user.get_absolute_url()}}?sort=reputation" + data-url="{% url 'get_perms_data' %}" >{% trans %}karma:{% endtrans %} {{user.reputation}}</a>{% if badges_mode == 'public' and have_badges %},{% endif %} {%- endif -%} {% if badges_mode == 'public' and have_badges %} diff --git a/askbot/templates/widgets/user_perms.html b/askbot/templates/widgets/user_perms.html new file mode 100644 index 00000000..887a712f --- /dev/null +++ b/askbot/templates/widgets/user_perms.html @@ -0,0 +1,28 @@ +<!--h2>{% trans karma=user.reputation %}Your karma is {{ karma }}{% endtrans %}</h2--> +<div class="triangle"></div> +<p> + {% trans %}Karma reflects the value of your contribution to this community.{% endtrans %} +</p> +<p> + {% if user.is_administrator_or_moderator() %} + {% if user.is_moderator() %} + {% set role=gettext('moderator') %} + {% else %} + {% set role=gettext('administrator') %} + {% endif %} + {% trans %}Since you are the site {{ role }}, you have access to all functions regardless of your karma.{% endtrans %} + {% else %} + {% trans %}The higher is your karma, the more rights you have on this site.{% endtrans %} + {% endif %} +</p> +{% if not user.is_administrator_or_moderator() %} +<p> {% trans %}Currently, you can:{% endtrans %}</p> +<ul> + <li>{% trans %}Post questions, answers and comments{% endtrans %}</li> + {% for perm in perms_data %} + {% if user.reputation >= perm[1] %} + <li>{{ perm[0] }}</li> + {% endif %} + {% endfor %} +</ul> +{% endif %} diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py index dccd9a2a..93acea84 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -79,6 +79,15 @@ def safe_urlquote(text, quote_plus = False): return urllib.quote(text.encode('utf8')) @register.filter +def show_block_to(block_name, user): + block = getattr(askbot_settings, block_name) + if block: + flag_name = block_name + '_ANON_ONLY' + require_anon = getattr(askbot_settings, flag_name, False) + return (require_anon is False) or user.is_anonymous() + return False + +@register.filter def strip_path(url): """removes path part of the url""" return url_utils.strip_path(url) diff --git a/askbot/tests/cache_tests.py b/askbot/tests/cache_tests.py index 5740cc2a..e0703d08 100644 --- a/askbot/tests/cache_tests.py +++ b/askbot/tests/cache_tests.py @@ -11,16 +11,19 @@ class CacheTests(AskbotTestCase): self.post_answer(user=user, question=self.question) settings.DEBUG = True # because it's forsed to False + def tearDown(self): + settings.DEBUG = False + def visit_question(self): self.client.get(self.question.get_absolute_url(), follow=True) - + def test_anonymous_question_cache(self): self.visit_question() - counter = len(connection.queries) - print 'we have %d queries' % counter + before_count = len(connection.queries) self.visit_question() - #second hit to the same question should give fewer queries - self.assertTrue(counter > len(connection.queries)) - settings.DEBUG = False + after_count = len(connection.queries) + self.assertTrue(before_count > after_count, + ('Expected fewer queries after calling visit_question. ' + + 'Before visit: %d. After visit: %d.') % (before_count, after_count)) diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py index 8d775fd0..1a7ad9e4 100644 --- a/askbot/tests/db_api_tests.py +++ b/askbot/tests/db_api_tests.py @@ -514,9 +514,14 @@ class GroupTests(AskbotTestCase): self.assertEqual(qa.groups.filter(name='private').exists(), True) def test_global_group_name_setting_changes_group_name(self): + orig_group_name = askbot_settings.GLOBAL_GROUP_NAME; askbot_settings.update('GLOBAL_GROUP_NAME', 'all-people') group = models.Group.objects.get_global_group() self.assertEqual(group.name, 'all-people') + # Revert the global group name, so we don't mess up other tests! + askbot_settings.update('GLOBAL_GROUP_NAME', orig_group_name); + group = models.Group.objects.get_global_group() + self.assertEqual(group.name, orig_group_name) def test_ask_global_group_by_id_works(self): group = models.Group.objects.get_global_group() @@ -698,5 +703,3 @@ class LinkPostingTests(AskbotTestCase): self.assert_no_link(question.html) self.edit_question(user=admin, question=question, body_text=text + ' ok') self.assert_has_link(question.html, 'http://wikipedia.org') - - diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index 199fd12d..ce107c9a 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -595,6 +595,19 @@ class BlankWeeklySelectedQuestionsEmailAlertTests(EmailAlertTests): def setUp(self): self.notification_schedule['q_sel'] = 'w' self.setup_timestamp = datetime.datetime.now() - datetime.timedelta(14) + self.expected_results['q_ask'] = {'message_count': 1, } + self.expected_results['q_ask_delete_answer'] = {'message_count': 0, } + self.expected_results['question_comment'] = {'message_count': 0, } + self.expected_results['question_comment_delete'] = {'message_count': 0, } + self.expected_results['answer_comment'] = {'message_count': 0, } + self.expected_results['answer_delete_comment'] = {'message_count': 0, } + self.expected_results['mention_in_question'] = {'message_count': 0, } + self.expected_results['mention_in_answer'] = {'message_count': 0, } + self.expected_results['question_edit'] = {'message_count': 1, } + self.expected_results['answer_edit'] = {'message_count': 1, } + self.expected_results['question_and_answer_by_target'] = {'message_count': 0, } + self.expected_results['q_ans'] = {'message_count': 0, } + self.expected_results['q_ans_new_answer'] = {'message_count': 0, } class BlankInstantSelectedQuestionsEmailAlertTests(EmailAlertTests): """blank means that this is testing for the absence of email @@ -605,6 +618,19 @@ class BlankInstantSelectedQuestionsEmailAlertTests(EmailAlertTests): def setUp(self): self.notification_schedule['q_sel'] = 'i' self.setup_timestamp = datetime.datetime.now() - datetime.timedelta(1) + self.expected_results['q_ask'] = {'message_count': 1, } + self.expected_results['q_ask_delete_answer'] = {'message_count': 1, } + self.expected_results['question_comment'] = {'message_count': 1, } + self.expected_results['question_comment_delete'] = {'message_count': 1, } + self.expected_results['answer_comment'] = {'message_count': 0, } + self.expected_results['answer_delete_comment'] = {'message_count': 0, } + self.expected_results['mention_in_question'] = {'message_count': 0, } + self.expected_results['mention_in_answer'] = {'message_count': 0, } + self.expected_results['question_edit'] = {'message_count': 1, } + self.expected_results['answer_edit'] = {'message_count': 1, } + self.expected_results['question_and_answer_by_target'] = {'message_count': 0, } + self.expected_results['q_ans'] = {'message_count': 0, } + self.expected_results['q_ans_new_answer'] = {'message_count': 0, } class LiveWeeklySelectedQuestionsEmailAlertTests(EmailAlertTests): """live means that this is testing for the presence of email diff --git a/askbot/tests/email_parsing_tests.py b/askbot/tests/email_parsing_tests.py index 3ed0908a..9a5ff126 100644 --- a/askbot/tests/email_parsing_tests.py +++ b/askbot/tests/email_parsing_tests.py @@ -18,12 +18,7 @@ class EmailParsingTests(utils.AskbotTestCase): def test_clean_email_body(self): cleaned_body = mail.clean_html_email(self.rendered_template) - print "EXPECTED BODY" - print self.expected_output - print '==================================================' - print cleaned_body - print "CLEANED BODY" - self.assertEqual(cleaned_body, self.expected_output) + self.assertEqual(self.expected_output, cleaned_body) def test_gmail_rich_text_response_stripped(self): text = u'\n\nthis is my reply!\n\nOn Wed, Oct 31, 2012 at 1:45 AM, <kp@kp-dev.askbot.com> wrote:\n\n> **\n> ' diff --git a/askbot/tests/management_command_tests.py b/askbot/tests/management_command_tests.py index db45b405..a44bb792 100644 --- a/askbot/tests/management_command_tests.py +++ b/askbot/tests/management_command_tests.py @@ -35,22 +35,21 @@ class ManagementCommandTests(AskbotTestCase): question = self.post_question(user=user_one) comment = self.post_comment(user=user_one, parent_post=question) number_of_gold = 50 - user_one.gold = number_of_gold + user_one.gold = number_of_gold reputation = 20 - user_one.reputation = reputation + user_one.reputation = reputation user_one.save() # Create a second user and transfer all objects from 'user_one' to 'user_two' user_two = self.create_user(username='unique') + user_two_pk = user_two.pk management.call_command('merge_users', user_one.id, user_two.id) # Check that the first user was deleted self.assertEqual(models.User.objects.filter(pk=user_one.id).count(), 0) # Explicitly check that the values assigned to user_one are now user_two's self.assertEqual(user_two.posts.get_questions().filter(pk=question.id).count(), 1) self.assertEqual(user_two.posts.get_comments().filter(pk=comment.id).count(), 1) - #todo: change groups to django groups - #then replace to 3 back to 2 in the line below - user_two = models.User.objects.get(pk=2) - self.assertEqual(user_two.gold, number_of_gold) + user_two = models.User.objects.get(pk=user_two_pk) + self.assertEqual(user_two.gold, number_of_gold) self.assertEqual(user_two.reputation, reputation) def test_create_tag_synonym(self): diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 703603c3..2e785802 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -546,7 +546,8 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): }) self.assertEqual(2, Post.objects.count()) answer = Post.objects.get_answers()[0] - self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + expected_url=answer.get_absolute_url() + self.assertRedirects(response=response, expected_url=expected_url) thread = answer.thread self.assertEqual(1, thread.answer_count) diff --git a/askbot/tests/question_views_tests.py b/askbot/tests/question_views_tests.py index 9486b854..da3e081d 100644 --- a/askbot/tests/question_views_tests.py +++ b/askbot/tests/question_views_tests.py @@ -33,7 +33,7 @@ class PrivateQuestionViewsTests(AskbotTestCase): dom = BeautifulSoup(response2.content) title = dom.find('h1').text self.assertTrue(unicode(const.POST_STATUS['private']) in title) - question = models.Thread.objects.get(id=1) + question = models.Thread.objects.get() self.assertEqual(question.title, self.qdata['title']) self.assertFalse(models.Group.objects.get_global_group() in set(question.groups.all())) @@ -114,7 +114,7 @@ class PrivateQuestionViewsTests(AskbotTestCase): class PrivateAnswerViewsTests(AskbotTestCase): - + def setUp(self): self._backup = askbot_settings.GROUPS_ENABLED askbot_settings.update('GROUPS_ENABLED', True) @@ -147,7 +147,6 @@ class PrivateAnswerViewsTests(AskbotTestCase): response = self.client.get(self.question.get_absolute_url()) self.assertFalse('some answer text' in response.content) - def test_private_checkbox_is_on_when_editing_private_answer(self): answer = self.post_answer( question=self.question, user=self.user, is_private=True diff --git a/askbot/urls.py b/askbot/urls.py index f71962f1..e82ad0df 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -534,7 +534,12 @@ urlpatterns = patterns('', service_url( r'^widgets/questions/(?P<widget_id>\d+)/$', views.widgets.question_widget, - name = 'question_widget' + name='question_widget' + ), + service_url( + r'^get-perms-data/$', + views.readers.get_perms_data, + name='get_perms_data' ), service_url( r'^start-sharing-twitter/$', diff --git a/askbot/utils/slug.py b/askbot/utils/slug.py index f9e30cbf..1c95e1d4 100644 --- a/askbot/utils/slug.py +++ b/askbot/utils/slug.py @@ -50,10 +50,13 @@ def slugify(input_text, max_length=150): return input_text allow_unicode_slugs = getattr(settings, 'ALLOW_UNICODE_SLUGS', False) - if allow_unicode_slugs: + if isinstance(input_text, unicode) and not allow_unicode_slugs: + input_text = unidecode(input_text) + + if isinstance(input_text, unicode): slug = unicode_slugify(input_text) else: - slug = defaultfilters.slugify(unidecode(input_text)) + slug = defaultfilters.slugify(input_text) while len(slug) > max_length: # try to shorten word by word until len(slug) <= max_length temp = slug[:slug.rfind('-')] diff --git a/askbot/views/readers.py b/askbot/views/readers.py index f738da25..b2a27324 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -638,3 +638,65 @@ def get_comment(request): comment = models.Post.objects.get(post_type='comment', id=id) request.user.assert_can_edit_comment(comment) return {'text': comment.text} + + +@csrf.csrf_exempt +@ajax_only +@anonymous_forbidden +@get_only +def get_perms_data(request): + """returns details about permitted activities + according to the users reputation + """ + + items = ( + 'MIN_REP_TO_VOTE_UP', + 'MIN_REP_TO_VOTE_DOWN', + ) + + if askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION > 0: + items += ('MIN_REP_TO_ANSWER_OWN_QUESTION',) + + if askbot_settings.ACCEPTING_ANSWERS_ENABLED: + items += ( + 'MIN_REP_TO_ACCEPT_OWN_ANSWER', + 'MIN_REP_TO_ACCEPT_ANY_ANSWER', + ) + + items += ( + 'MIN_REP_TO_FLAG_OFFENSIVE', + 'MIN_REP_TO_DELETE_OTHERS_COMMENTS', + 'MIN_REP_TO_DELETE_OTHERS_POSTS', + 'MIN_REP_TO_UPLOAD_FILES', + 'MIN_REP_TO_INSERT_LINK', + 'MIN_REP_TO_SUGGEST_LINK', + 'MIN_REP_TO_CLOSE_OWN_QUESTIONS', + 'MIN_REP_TO_REOPEN_OWN_QUESTIONS', + 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS', + 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS', + 'MIN_REP_TO_EDIT_WIKI', + 'MIN_REP_TO_EDIT_OTHERS_POSTS', + 'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS', + ) + + if askbot_settings.ALLOW_ASKING_BY_EMAIL or askbot_settings.REPLY_BY_EMAIL: + items += ( + 'MIN_REP_TO_POST_BY_EMAIL', + 'MIN_REP_TO_TWEET_ON_OTHERS_ACCOUNTS', + ) + + data = list() + for item in items: + setting = ( + askbot_settings.get_description(item), + getattr(askbot_settings, item) + ) + data.append(setting) + + template = get_template('widgets/user_perms.html') + html = template.render({ + 'user': request.user, + 'perms_data': data + }) + + return {'html': html} diff --git a/askbot/views/users.py b/askbot/views/users.py index 179392d2..173ccb85 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -60,6 +60,7 @@ def owner_or_moderator_required(f): return f(request, profile_owner, context) return wrapped_func + def show_users(request, by_group=False, group_id=None, group_slug=None): """Users view, including listing of users by group""" if askbot_settings.GROUPS_ENABLED and not by_group: @@ -1111,7 +1112,7 @@ def user(request, id, slug=None, tab_name=None): sort=None, query=None, tags=None, - author=profile_owner.id, + author=None, page=None, user_logged_in=profile_owner.is_authenticated(), ) |