diff options
26 files changed, 626 insertions, 509 deletions
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 764a3234..0386a39d 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -88,9 +88,10 @@ UNANSWERED_QUESTION_MEANING_CHOICES = ( #however it will be hard to expect that people will type #correct regexes - plus this must be an anchored regex #to do full string match -TAG_CHARS = '\w\+\.\-#' +TAG_CHARS = r'\w+.#-' TAG_REGEX = r'^[%s]+$' % TAG_CHARS TAG_SPLIT_REGEX = r'[ ,]+' +TAG_SEP = ',' # has to be valid TAG_SPLIT_REGEX char and MUST NOT be in const.TAG_CHARS EMAIL_REGEX = re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b', re.I) TYPE_ACTIVITY_ASK_QUESTION = 1 diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py index 627d91e6..d1641b49 100644 --- a/askbot/search/state_manager.py +++ b/askbot/search/state_manager.py @@ -1,6 +1,8 @@ """Search state manager object""" import re +import copy +from django.core import urlresolvers from django.utils.http import urlquote import askbot @@ -79,6 +81,11 @@ def parse_query(query): } class SearchState(object): + + @classmethod + def get_empty(cls): + return cls(scope=None, sort=None, query=None, tags=None, author=None, page=None, page_size=None, user_logged_in=None) + def __init__(self, scope, sort, query, tags, author, page, page_size, user_logged_in): # INFO: zip(*[('a', 1), ('b', 2)])[0] == ('a', 'b') @@ -87,12 +94,7 @@ class SearchState(object): else: self.scope = scope - if query: - self.query = ' '.join(query.split('+')).strip() - if self.query == '': - self.query = None - else: - self.query = None + self.query = query.strip() if query else None if self.query: #pull out values of [title:xxx], [user:some one] @@ -114,21 +116,10 @@ class SearchState(object): else: self.sort = sort - if tags: - # const.TAG_SPLIT_REGEX, const.TAG_REGEX - #' '.join(tags.split('+')) - self.tags = tags.split('+') - else: - self.tags = None - - if author: - self.author = int(author) - else: - self.author = None - - if page: - self.page = int(page) - else: + self.tags = [t.strip() for t in tags.split(const.TAG_SEP)] if tags else [] + self.author = int(author) if author else None + self.page = int(page) if page else 1 + if self.page == 0: # in case someone likes jokes :) self.page = 1 if not page_size or page_size not in zip(*const.PAGE_SIZE_CHOICES)[0]: @@ -137,34 +128,82 @@ class SearchState(object): self.page_size = int(page_size) def __str__(self): - out = 'scope=%s\n' % self.scope - out += 'query=%s\n' % self.query - if self.tags: - out += 'tags=%s\n' % ','.join(self.tags) - out += 'author=%s\n' % self.author - out += 'sort=%s\n' % self.sort - out += 'page_size=%d\n' % self.page_size - out += 'page=%d\n' % self.page - return out + return self.query_string() + + def full_url(self): + return urlresolvers.reverse('questions') + self.query_string() + + # + # Safe characters in urlquote() according to http://www.ietf.org/rfc/rfc1738.txt: + # + # Thus, only alphanumerics, the special characters "$-_.+!*'(),", and + # reserved characters used for their reserved purposes may be used + # unencoded within a URL. + # + # Tag separator (const.TAG_SEP) remains unencoded to clearly mark tag boundaries + # _+.- stay unencoded to keep tags in URL as verbose as possible + # (note that urllib.quote() in Python 2.7 treats _.- as safe chars, but let's be explicit) + # Hash (#) is not safe and has to be encodeded, as it's used as URL has delimiter + # + SAFE_CHARS = const.TAG_SEP + '_+.-' def query_string(self): - out = 'scope:%s' % self.scope - out += '/sort:%s' % self.sort + lst = [ + 'scope:%s' % self.scope, + 'sort:%s' % self.sort + ] if self.query: - out += '/query:%s' % urlquote(self.query) + lst.append('query:%s' % urlquote(self.query, safe=self.SAFE_CHARS)) if self.tags: - out += '/tags:%s' % '+'.join(self.tags) + lst.append('tags:%s' % urlquote(const.TAG_SEP.join(self.tags), safe=self.SAFE_CHARS)) if self.author: - out += '/author:%s' % self.author - return out+'/' - - def make_parameters(self): - params_dict = { - 'scope': self.scope, - 'sort': self.sort, - 'query': '+'.join(self.query.split(' ')) if self.query else None, - 'tags': '+'.join(self.tags) if self.tags else None, - 'author': self.author, - 'page_size': self.page_size - } - return params_dict + lst.append('author:%d' % self.author) + if self.page_size: + lst.append('page_size:%d' % self.page_size) + if self.page: + lst.append('page:%d' % self.page) + return '/'.join(lst) + '/' + + def deepcopy(self): + "Used to contruct a new SearchState for manipulation, e.g. for adding/removing tags" + return copy.deepcopy(self) + + def add_tag(self, tag): + ss = self.deepcopy() + ss.tags.append(tag) + return ss + + def remove_query(self): + ss = self.deepcopy() + ss.query = None + return ss + + def remove_author(self): + ss = self.deepcopy() + ss.author = None + return ss + + def remove_tags(self): + ss = self.deepcopy() + ss.tags = [] + return ss + + def change_scope(self, new_scope): + ss = self.deepcopy() + ss.scope = new_scope + return ss + + def change_sort(self, new_sort): + ss = self.deepcopy() + ss.sort = new_sort + return ss + + def change_page(self, new_page): + ss = self.deepcopy() + ss.page = new_page + return ss + + def change_page_size(self, new_page_size): + ss = self.deepcopy() + ss.page_size = new_page_size + return ss diff --git a/askbot/skins/common/media/js/live_search.js b/askbot/skins/common/media/js/live_search.js index 8dcfe85f..2b34d407 100644 --- a/askbot/skins/common/media/js/live_search.js +++ b/askbot/skins/common/media/js/live_search.js @@ -1,5 +1,5 @@ -var liveSearch = function(command, query_string) { +var liveSearch = function(query_string) { var query = $('input#keywords'); var query_val = function () {return $.trim(query.val());}; var prev_text = query_val(); @@ -38,17 +38,20 @@ var liveSearch = function(command, query_string) { } }; - var send_query = function(query_text){ - running = true; + var update_query_string = function(query_text){ if(query_text === undefined) { // handle missing parameter query_text = query_val(); } - query_string = patch_query_string( - query_string, - 'query:' + encodeURIComponent(query_text), - query_text === '' // remove if empty + query_string = QSutils.patch_query_string( + query_string, + 'query:' + encodeURIComponent(query_text), + query_text === '' // remove if empty ); + }; + var send_query = function(query_text){ + running = true; + update_query_string(query_text); var url = search_url + query_string; $.ajax({ url: url, @@ -63,20 +66,13 @@ var liveSearch = function(command, query_string) { prev_text = query_text; var context = { state:1, rand:Math.random() }; History.pushState( context, "Questions", url ); - }; - - var refresh_main_page = function (){ - $.ajax({ - url: askbot['urls']['questions'], - data: {preserve_state: true}, - dataType: 'json', - success: render_result - }); - - var context = { state:1, rand:Math.random() }; - var title = "Questions"; - var query = askbot['urls']['questions']; - History.pushState( context, title, query ); + setTimeout(function (){ + /* HACK: For some weird reson, sometimes something overrides the above pushState so we re-aplly it + This might be caused by some other JS plugin. + The delay of 10msec allows the other plugin to override the URL. + */ + History.replaceState( context, "Questions", url ); + }, 10); }; /* *********************************** */ @@ -136,67 +132,8 @@ var liveSearch = function(command, query_string) { /* *************************************** */ - var get_query_string_selector_value = function (query_string, selector) { - var params = query_string.split('/'); - for(var i=0; i<params.length; i++) { - var param_split = params[i].split(':'); - if(param_split[0] === selector) { - return param_split[1]; - } - } - return undefined; - }; - - var patch_query_string = function (query_string, patch, remove) { - var patch_split = patch.split(':'); - var mapping = {}; - var params = query_string.split('/'); - var new_query_string = ''; - - if(!remove) { - mapping[patch_split[0]] = patch_split[1]; // prepopulate the patched selector - } - - for (var i = 0; i < params.length; i++) { - var param_split = params[i].split(':'); - if(param_split[0] !== patch_split[0] && param_split[1]) { - mapping[param_split[0]] = param_split[1]; - } - } - - var add_selector = function(name) { - if(name in mapping) { - new_query_string += name + ':' + mapping[name] + '/'; - } - }; - - /* The order of selectors should match the Django URL */ - add_selector('scope'); - add_selector('sort'); - add_selector('query'); - add_selector('tags'); - add_selector('author'); - add_selector('page_size'); - add_selector('page'); - - return new_query_string; - }; - var remove_search_tag = function(tag){ - var tag_string = get_query_string_selector_value(query_string, 'tags'); - if(!tag_string) return; // early exit - - var tags = tag_string.split('+'); - var new_tags = []; - - for(var j = 0; j < tags.length; j++){ - if(tags[j] !== tag) { - new_tags.push(tags[j]); - } - } - - query_string = patch_query_string(query_string, 'tags:' + new_tags.join('+')); - + query_string = QSutils.remove_search_tag(query_string, tag); send_query(); }; @@ -207,7 +144,7 @@ var liveSearch = function(command, query_string) { var tab = $(element); var tab_name = tab.attr('id').replace(/^by_/,''); if (tab_name in sortButtonData){ - href = search_url + patch_query_string(query_string, 'sort:'+tab_name+'-desc'); + href = search_url + QSutils.patch_query_string(query_string, 'sort:'+tab_name+'-desc'); tab.attr('href', href); tab.attr('title', sortButtonData[tab_name]['desc_tooltip']); tab.html(sortButtonData[tab_name]['label']); @@ -274,38 +211,48 @@ var liveSearch = function(command, query_string) { /* *********************************** */ - if(command === 'refresh') { - refresh_main_page(); - } - else if(command === 'init') { - // Wire search tags - var search_tags = $('#searchTags .tag-left'); - $.each(search_tags, function(idx, element){ - var tag = new Tag(); - tag.decorate($(element)); - //todo: setDeleteHandler and setHandler - //must work after decorate & must have getName - tag.setDeleteHandler( - function(){ - remove_search_tag(tag.getName(), query_string); - } - ); - }); + // Wire search tags + var search_tags = $('#searchTags .tag-left'); + $.each(search_tags, function(idx, element){ + var tag = new Tag(); + tag.decorate($(element)); + //todo: setDeleteHandler and setHandler + //must work after decorate & must have getName + tag.setDeleteHandler( + function(){ + remove_search_tag(tag.getName(), query_string); + } + ); + }); - // Wire X button - x_button.click(function () { - restart_query(); /* wrapped in closure because it's not yet defined at this point */ - }); + // Wire X button + x_button.click(function () { + restart_query(); /* wrapped in closure because it's not yet defined at this point */ + }); + refresh_x_button(); + + // Wire query box + var main_page_eval_handle; + query.keyup(function(e){ refresh_x_button(); + if (running === false){ + clearTimeout(main_page_eval_handle); + main_page_eval_handle = setTimeout(eval_query, 400); + } + }); - // Wire query box - var main_page_eval_handle; - query.keyup(function(e){ - refresh_x_button(); - if (running === false){ - clearTimeout(main_page_eval_handle); - main_page_eval_handle = setTimeout(eval_query, 400); - } - }); - } + $("form#searchForm").submit(function(event) { + // if user clicks the button the s(h)e probably wants page reload, + // so provide that experience but first update the query string + event.preventDefault(); + update_query_string(); + window.location.href = search_url + query_string; + }); + + /* *********************************** */ + + // Hook for tag_selector.js + liveSearch.refresh = function () { + send_query(); + }; }; diff --git a/askbot/skins/common/media/js/tag_selector.js b/askbot/skins/common/media/js/tag_selector.js index 5d585184..445a1e44 100644 --- a/askbot/skins/common/media/js/tag_selector.js +++ b/askbot/skins/common/media/js/tag_selector.js @@ -143,7 +143,7 @@ function pickedTags(){ 'remove', function(){ deleteTagLocally(); - liveSearch('refresh'); + liveSearch.refresh(); } ); } @@ -274,7 +274,7 @@ function pickedTags(){ to_tag_container ); $(input_sel).val(''); - liveSearch('refresh'); + liveSearch.refresh(); } ); } @@ -328,7 +328,7 @@ function pickedTags(){ filter_value: $(this).val() }, success: function(){ - liveSearch('refresh'); + liveSearch.refresh(); } }); }); diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 4d8cb92d..58072771 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -130,6 +130,97 @@ var notify = function() { }; } (); +/* **************************************************** */ +// Search query-string manipulation utils +/* **************************************************** */ + +var QSutils = QSutils || {}; // TODO: unit-test me + +QSutils.TAG_SEP = ','; // should match const.TAG_SEP; TODO: maybe prepopulate this in javascript.html ? + +QSutils.get_query_string_selector_value = function (query_string, selector) { + var params = query_string.split('/'); + for(var i=0; i<params.length; i++) { + var param_split = params[i].split(':'); + if(param_split[0] === selector) { + return param_split[1]; + } + } + return undefined; +}; + +QSutils.patch_query_string = function (query_string, patch, remove) { + var params = query_string.split('/'); + var patch_split = patch.split(':'); + + var new_query_string = ''; + var mapping = {}; + + if(!remove) { + mapping[patch_split[0]] = patch_split[1]; // prepopulate the patched selector if it's not meant to be removed + } + + for (var i = 0; i < params.length; i++) { + var param_split = params[i].split(':'); + if(param_split[0] !== patch_split[0] && param_split[1]) { + mapping[param_split[0]] = param_split[1]; + } + } + + var add_selector = function(name) { + if(name in mapping) { + new_query_string += name + ':' + mapping[name] + '/'; + } + }; + + /* The order of selectors should match the Django URL */ + add_selector('scope'); + add_selector('sort'); + add_selector('query'); + add_selector('tags'); + add_selector('author'); + add_selector('page_size'); + add_selector('page'); + + return new_query_string; +}; + +QSutils.remove_search_tag = function(query_string, tag){ + var tag_string = this.get_query_string_selector_value(query_string, 'tags'); + if(!tag_string) { + return query_string; + } + + var tags = tag_string.split(this.TAG_SEP); + var new_tags = []; + + var pos = $.inArray(encodeURIComponent(tag), tags); + if(pos > -1) { + new_tags = tags.splice(pos, 1); + } + + if(new_tags.length === 0) { + return this.patch_query_string(query_string, 'tags:', true); + } else { + return this.patch_query_string(query_string, 'tags:' + new_tags.join(this.TAG_SEP)); + } +}; + +QSutils.add_search_tag = function(query_string, tag){ + var tag_string = this.get_query_string_selector_value(query_string, 'tags'); + tag = encodeURIComponent(tag); + if(!tag_string) { + tag_string = tag; + } else { + tag_string = [tag_string, tag].join(this.TAG_SEP); + } + + return this.patch_query_string(query_string, 'tags:' + tag_string); +}; + + +/* **************************************************** */ + /* some google closure-like code for the ui elements */ var inherits = function(childCtor, parentCtor) { /** @constructor taken from google closure */ @@ -358,39 +449,8 @@ Tag.prototype.createDom = function(){ var url = askbot['urls']['questions']; var flag = false var author = '' - if (this._url_params !== null){ - params = this._url_params.split('/') - for (var i = 0; i < params.length; i++){ - if (params[i] !== ''){ - if (params[i].substring(0, 5) == "tags:"){ - tags = params[i].substr(5).split('+'); - new_tags = '' - for(var j = 0; j < tags.length; j++){ - if(escape(tags[j]) !== escape(this.getName())){ - new_tags += escape(tags[j]) + '+'; - } - } - new_tags += escape(this.getName()) - url += 'tags:'+new_tags+'/' - flag = true - } - else if (params[i].substring(0, 7) == "author:"){ - author = params[i]; - } - else{ - url += params[i] + '/'; - } - } - } - if (flag == false) { - url += 'tags:'+escape(this.getName())+'/' - } - if (author !== '') { - url += author+'/' - } - } - else{ - url += 'tags:' + escape(this.getName()) + '/'; + if (this._url_params){ + url += QSutils.add_search_tag(this._url_params, this.getName()); } this._inner_element.attr('href', url); } diff --git a/askbot/skins/common/templates/widgets/related_tags.html b/askbot/skins/common/templates/widgets/related_tags.html index 9e1bfd86..84e2ce0c 100644 --- a/askbot/skins/common/templates/widgets/related_tags.html +++ b/askbot/skins/common/templates/widgets/related_tags.html @@ -10,7 +10,7 @@ html_tag = 'div', extra_content = '<span class="tag-number">× ' ~ tag.local_used_count|intcomma ~ '</span>', - url_params = query_string, + search_state = search_state, )}} </li> {% endfor %} diff --git a/askbot/skins/common/templates/widgets/tag_selector.html b/askbot/skins/common/templates/widgets/tag_selector.html index 7db1912d..ff298488 100644 --- a/askbot/skins/common/templates/widgets/tag_selector.html +++ b/askbot/skins/common/templates/widgets/tag_selector.html @@ -6,7 +6,8 @@ macros.tag_list_widget( interesting_tag_names, deletable = True, - css_class = 'interesting marked-tags' + css_class = 'interesting marked-tags', + search_state = search_state ) }} {# todo - add this via js @@ -22,7 +23,8 @@ macros.tag_list_widget( ignored_tag_names, deletable = True, - css_class = 'ignored marked-tags' + css_class = 'ignored marked-tags', + search_state = search_state ) }} {# todo: add this via javascript diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 55359e0f..3af57587 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -158,7 +158,7 @@ poor design of the data or methods on data objects #} id = None, deletable = False, make_links = True, - url_params = None, + search_state = None, css_class = None, tag_css_class = None, tag_html_tag = 'li' @@ -174,7 +174,7 @@ poor design of the data or methods on data objects #} css_class = tag_css_class, deletable = deletable, is_link = make_links, - url_params = url_params, + search_state = search_state, html_tag = tag_html_tag )}} {% endfor %} @@ -189,17 +189,20 @@ poor design of the data or methods on data objects #} is_link = True, delete_link_title = None, css_class = None, - url_params = None, + search_state = None, html_tag = 'div', extra_content = '' ) -%} - {% spaceless %} + {% if not search_state %} {# get empty SearchState() if there's none; CAUTION: for some reason this doesn't work inside `spaceless` tag below! #} + {% set search_state=search_state|get_empty_search_state %} + {% endif %} + {% spaceless %} <{{ html_tag }} class="tag-left{% if deletable %} deletable-tag{% endif %}"> <{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %} class="tag tag-right{% if css_class %} {{ css_class }}{% endif %}" {% if is_link %} - href="{% url questions %}{% if url_params %}{{url_params|add_tag_to_url(tag|urlencode)}}{% else %}tags:{{ tag|urlencode }}/{% endif %}" + href="{{ search_state.add_tag(tag).full_url() }}" title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" {% endif %} rel="tag" @@ -213,7 +216,7 @@ poor design of the data or methods on data objects #} {% endif %} </{{ html_tag }}> {{ extra_content }} - {% endspaceless %} + {% endspaceless %} {%- endmacro -%} {%- macro radio_select(name = None, value = None, choices = None) -%} @@ -234,7 +237,7 @@ poor design of the data or methods on data objects #} {% endfor %} {%- endmacro -%} -{%- macro question_summary(thread, question, extra_class=None, query_string=None) -%} +{%- macro question_summary(thread, question, extra_class=None, search_state=None) -%} {%include "widgets/question_summary.html" %} {%- endmacro -%} @@ -351,7 +354,7 @@ for the purposes of the AJAX comment editor #} {%- endmacro -%} {%- macro reversible_sort_button(button_sort_criterium=None, asc_tooltip=None, - desc_tooltip=None, label=None, current_sort_method=None, query_string=None) -%} + desc_tooltip=None, label=None, current_sort_method=None, search_state=None) -%} {# sort button where descending sort is default and the search method is togglable between ascending and descending @@ -363,19 +366,19 @@ for the purposes of the AJAX comment editor #} {% set key_name = button_sort_criterium %} {% if current_sort_method == key_name + "-asc" %}{# "worst" first #} <a id="by_{{key_name}}" - href={% url questions %}{{ query_string|replace_in_url("sort:"+key_name+"-desc") }} - class="rev on" - title="{{desc_tooltip}}"><span>{{label}} ▲</span></a> + href="{{ search_state.change_sort(key_name+"-desc").full_url() }}" + class="rev on" + title="{{desc_tooltip}}"><span>{{label}} ▲</span></a> {% elif current_sort_method == key_name + "-desc" %}{# "best first" #} <a id="by_{{key_name}}" - href={% url questions %}{{ query_string|replace_in_url("sort:"+key_name+"-asc") }} - class="rev on" - title="{{asc_tooltip}}"><span>{{label}} ▼</span></a> + href="{{ search_state.change_sort(key_name+"-asc").full_url() }}" + class="rev on" + title="{{asc_tooltip}}"><span>{{label}} ▼</span></a> {% else %}{# default, when other button is active #} <a id="by_{{key_name}}" - href={% url questions %}{{ query_string|replace_in_url("sort:"+key_name+"-desc") }} - class="off" - title="{{desc_tooltip}}"><span>{{label}}</span></a> + href="{{ search_state.change_sort(key_name+"-desc").full_url() }}" + class="off" + title="{{desc_tooltip}}"><span>{{label}}</span></a> {% endif %} <script type="text/javascript">{# need to pass on text translations to js #} var sortButtonData = sortButtonData || {}; @@ -524,17 +527,17 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_ {% endif %} {%- endmacro -%} -{%- macro paginator(p, position='left', active_tab='') -%}{# p is paginator context dictionary #} +{%- macro paginator(p, position='left', anchor='') -%}{# p is paginator context dictionary #} {% spaceless %} {% if p.is_paginated %} <div class="paginator" style="float:{{position}}"> {% if p.has_previous %} - <span class="prev"><a href="{% if active_tab == "questions" %}{% url questions%}{% endif %}{{p.base_url}}page{% if active_tab == "questions" %}:{%else%}={%endif%}{{ p.previous }}/{{ p.extend_url }}" title="{% trans %}previous{% endtrans %}"> + <span class="prev"><a href="{{p.base_url}}page={{ p.previous }}{{ anchor }}" title="{% trans %}previous{% endtrans %}"> « {% trans %}previous{% endtrans %}</a></span> {% endif %} {% if not p.in_leading_range %} {% for num in p.pages_outside_trailing_range %} - <span class="page"><a href="{% if active_tab == "questions" %}{% url questions%}{% endif %}{{p.base_url}}page{% if active_tab == "questions" %}:{%else%}={%endif%}{{ num }}/{{ p.extend_url }}" >{{ num }}</a></span> + <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" >{{ num }}</a></span> {% endfor %} ... {% endif %} @@ -543,51 +546,98 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_ {% if num == p.page and p.pages != 1%} <span class="curr" title="{% trans %}current page{% endtrans %}">{{ num }}</span> {% else %} - <span class="page"><a href="{% if active_tab == "questions" %}{% url questions%}{% endif %}{{p.base_url}}page{% if active_tab == "questions" %}:{%else%}={%endif%}{{ num }}/{{ p.extend_url }}" title="{% trans %}page number {{num}}{% endtrans %}">{{ num }}</a></span> + <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" title="{% trans %}page number {{num}}{% endtrans %}">{{ num }}</a></span> {% endif %} {% endfor %} {% if not p.in_trailing_range %} ... {% for num in p.pages_outside_leading_range|reverse %} - <span class="page"><a href="{% if active_tab == "questions" %}{% url questions%}{% endif %}{{p.base_url}}page{% if active_tab == "questions" %}:{%else%}={%endif%}{{ num }}/{{ p.extend_url }}" title="{% trans %}page number {{ num }}{% endtrans %}">{{ num }}</a></span> + <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" title="{% trans %}page number {{ num }}{% endtrans %}">{{ num }}</a></span> {% endfor %} {% endif %} {% if p.has_next %} - <span class="next"><a href="{% if active_tab == "questions" %}{% url questions%}{% endif %}{{p.base_url}}page{% if active_tab == "questions" %}:{%else%}={%endif%}{{ p.next }}/{{ p.extend_url }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} »</a></span> + <span class="next"><a href="{{p.base_url}}page={{ p.next }}{{ anchor }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} »</a></span> {% endif %} </div> {% endif %} {% endspaceless %} {%- endmacro -%} -{%- macro pagesize_switch(p, position='left') -%}{# p is paginator context #} -{% spaceless %} -{% if p.is_paginated %} - <div class="paginator" style="float:{{position}}"> - <span class="text">{% trans %}posts per page{% endtrans %}</span> - {% if p.page_size == 10 %} - <span class="curr">10</span> - {% else %} - <span class="page"><a href="{% url questions %}{{p.base_url}}page_size:10/">10</a></span> - {% endif %} - - {% if p.page_size == 30 %} - <span class="curr">30</span> - {% else %} - <span class="page"><a href="{% url questions %}{{p.base_url}}page_size:30/">30</a></span> + + +{%- macro paginator_main_page(p, position, search_state) -%} {# p is paginator context dictionary #} + {% spaceless %} + {% if p.is_paginated %} + <div class="paginator" style="float:{{position}}"> + {% if p.has_previous %} + <span class="prev"><a href="{{ search_state.change_page(p.previous).full_url() }}" title="{% trans %}previous{% endtrans %}"> + « {% trans %}previous{% endtrans %}</a></span> + {% endif %} + {% if not p.in_leading_range %} + {% for num in p.pages_outside_trailing_range %} + <span class="page"><a href="{{ search_state.change_page(num).full_url() }}" >{{ num }}</a></span> + {% endfor %} + ... + {% endif %} + + {% for num in p.page_numbers %} + {% if num == p.page and p.pages != 1%} + <span class="curr" title="{% trans %}current page{% endtrans %}">{{ num }}</span> + {% else %} + <span class="page"><a href="{{ search_state.change_page(num).full_url() }}" title="{% trans %}page number {{num}}{% endtrans %}">{{ num }}</a></span> + {% endif %} + {% endfor %} + + {% if not p.in_trailing_range %} + ... + {% for num in p.pages_outside_leading_range|reverse %} + <span class="page"><a href="{{ search_state.change_page(num).full_url() }}" title="{% trans %}page number {{ num }}{% endtrans %}">{{ num }}</a></span> + {% endfor %} + {% endif %} + {% if p.has_next %} + <span class="next"><a href="{{ search_state.change_page(p.next).full_url() }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} »</a></span> + {% endif %} + </div> {% endif %} - - {% if p.page_size == 50 %} - <span class="curr">50</span> - {% else %} - <span class="page"><a href="{% url questions %}{{p.base_url}}page_size:50/">50</a></span> + {% endspaceless %} +{%- endmacro -%} + +{%- macro pagesize_switch_main_page(p, position, search_state) -%} {# p is paginator context #} + {% spaceless %} + {% if p.is_paginated %} + <div class="paginator" style="float:{{position}}"> + <span class="text">{% trans %}posts per page{% endtrans %}</span> + {% if p.page_size == 10 %} + <span class="curr">10</span> + {% else %} + <span class="page"><a href="{{ search_state.change_page_size(10).full_url() }}">10</a></span> + {% endif %} + + {% if p.page_size == 30 %} + <span class="curr">30</span> + {% else %} + <span class="page"><a href="{{ search_state.change_page_size(30).full_url() }}">30</a></span> + {% endif %} + + {% if p.page_size == 50 %} + <span class="curr">50</span> + {% else %} + <span class="page"><a href="{{ search_state.change_page_size(50).full_url() }}">50</a></span> + {% endif %} + </div> {% endif %} - </div> -{% endif %} -{% endspaceless %} + {% endspaceless %} {%- endmacro -%} + + + + + + + + {%- macro inbox_link(user) -%} {% if user.new_response_count > 0 or user.seen_response_count > 0 %} <a id='ab-responses' href="{{user.get_absolute_url()}}?sort=inbox§ion=forum"> diff --git a/askbot/skins/default/templates/main_page/headline.html b/askbot/skins/default/templates/main_page/headline.html index 3199b894..19dc7063 100644 --- a/askbot/skins/default/templates/main_page/headline.html +++ b/askbot/skins/default/templates/main_page/headline.html @@ -14,7 +14,8 @@ search_tags, id = 'searchTags', deletable = True, - make_links = False + make_links = False, + search_state = search_state ) }} </div> @@ -23,10 +24,10 @@ <p class="search-tips"><b>{% trans %}Search tips:{% endtrans %}</b> {% if reset_method_count > 1 %} {% if author_name %} - <a href="{% url questions %}{{ query_string|remove_from_url('author') }}">{% trans %}reset author{% endtrans %}</a> + <a href="{{ search_state.remove_author().full_url() }}">{% trans %}reset author{% endtrans %}</a> {% endif %} {% if search_tags %}{% if author_name and query %}, {% elif author_name %}{% trans %} or {% endtrans %}{% endif %} - <a href="{% url questions %}{{ query_string|remove_from_url('tags') }}">{% trans %}reset tags{% endtrans %}</a> + <a href="{{ search_state.remove_tags().full_url() }}">{% trans %}reset tags{% endtrans %}</a> {% endif %} {% if query %}{% trans %} or {% endtrans %} <a href="{% url questions %}">{% trans %}start over{% endtrans %}</a> diff --git a/askbot/skins/default/templates/main_page/javascript.html b/askbot/skins/default/templates/main_page/javascript.html index 8e12f0a7..bbe435d3 100644 --- a/askbot/skins/default/templates/main_page/javascript.html +++ b/askbot/skins/default/templates/main_page/javascript.html @@ -5,7 +5,7 @@ $(document).ready(function(){ /*var on_tab = '#nav_questions'; $(on_tab).attr('className','on');*/ - liveSearch('init', '{{ query_string|escapejs }}'); + liveSearch('{{ search_state.query_string()|escapejs }}'); Hilite.exact = false; Hilite.elementid = "question-list"; Hilite.debug_referrer = location.href; @@ -34,19 +34,3 @@ <script type='text/javascript' src='{{"/js/tag_selector.js"|media}}'></script> {% endif %} <script type="text/javascript" src="{{"/js/live_search.js"|media}}"></script> -{% if active_tab != "tags" and active_tab != "users" %} -<script> -$("form#searchForm").submit(function(event) { - event.preventDefault(); - form_action = $("form#searchForm").attr('action') - query = $("input#keywords").attr('value').split(' ').join('+') - $("input#keywords").attr('value', '') - $("input#searchButton").attr('value', '') - form_action += 'section:{{parameters.scope}}/sort:{{parameters.sort}}/' - + 'query:' + query + '/search:search/' - + '{% if parameters.tags %}tags:{{parameters.tags}}/{% endif %}' - + '{% if parameters.author %}author:{{parameters.author}}/{% endif %}' - window.location.href = form_action; -}); -</script> -{% endif %} diff --git a/askbot/skins/default/templates/main_page/nothing_found.html b/askbot/skins/default/templates/main_page/nothing_found.html index 8e7624cc..1e2c5445 100644 --- a/askbot/skins/default/templates/main_page/nothing_found.html +++ b/askbot/skins/default/templates/main_page/nothing_found.html @@ -1,24 +1,24 @@ {# todo: add tips to widen selection #} <p class="evenMore" style="padding-top:30px;text-align:center;"> -{% if scope == "unanswered" %} +{% if search_state.scope == "unanswered" %} {% trans %}There are no unanswered questions here{% endtrans %} {% endif %} -{% if scope == "favorite" %} +{% if search_state.scope == "favorite" %} {% trans %}No questions here. {% endtrans %} {% trans %}Please follow some questions or follow some users.{% endtrans %} {% endif %} </p> -{% if query or search_tags or author_name %} +{% if search_state.query or search_state.tags or search_state.author %} <p class="evenMore" style="text-align:center"> {% trans %}You can expand your search by {% endtrans %} {% if reset_method_count > 1 %} - {% if author_name %} - <a href="{% url questions %}{{ query_string|remove_from_url('author') }}">{% trans %}resetting author{% endtrans %}</a> + {% if search_state.author %} + <a href="{{ search_state.remove_author().full_url() }}">{% trans %}resetting author{% endtrans %}</a> {% endif %} - {% if search_tags %}{% if author_name and query %}, {% elif author_name %}{% trans %} or {% endtrans %}{% endif %} - <a href="{% url questions %}{{ query_string|remove_from_url('tags') }}">{% trans %}resetting tags{% endtrans %}</a> + {% if search_state.tags %}{% if search_state.author and search_state.query %}, {% elif search_state.author %}{% trans %} or {% endtrans %}{% endif %} + <a href="{{ search_state.remove_tags().full_url() }}">{% trans %}resetting tags{% endtrans %}</a> {% endif %} - {% if query %}{% trans %} or {% endtrans %} + {% if search_state.query %}{% trans %} or {% endtrans %} <a href="{% url questions %}">{% trans %}starting over{% endtrans %}</a> {% endif %} {% else %} diff --git a/askbot/skins/default/templates/main_page/paginator.html b/askbot/skins/default/templates/main_page/paginator.html index 6766261b..6697ca08 100644 --- a/askbot/skins/default/templates/main_page/paginator.html +++ b/askbot/skins/default/templates/main_page/paginator.html @@ -1,8 +1,8 @@ {% import "macros.html" as macros %} {% if questions_count > page_size %}{# todo: remove magic number #} <div id="pager" class="pager"> - {{ macros.paginator(context|setup_paginator, position='left', active_tab=active_tab) }} - {{ macros.pagesize_switch(context, position='right') }} + {{ macros.paginator_main_page(context|setup_paginator, position='left', search_state=search_state) }} + {{ macros.pagesize_switch_main_page(context, position='right', search_state=search_state) }} <div class="clean"></div> </div> {% endif %} diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html index f9d62f9c..a6d4f21b 100644 --- a/askbot/skins/default/templates/main_page/questions_loop.html +++ b/askbot/skins/default/templates/main_page/questions_loop.html @@ -1,11 +1,9 @@ {% import "macros.html" as macros %} -{% cache 0 "questions" questions search_tags scope sort query context.page context.page_size language_code %} - {% for question_post in questions.object_list %} - {{macros.question_summary(question_post.thread, question_post, query_string=query_string)}} - {% endfor %} -{% endcache %} -{# comment todo: fix css here #} -{% if threadss_count == 0 %} +{# cache 0 "questions" questions search_tags scope sort query context.page context.page_size language_code #} +{% for question_post in questions.object_list %} + {{macros.question_summary(question_post.thread, question_post, search_state=search_state)}} +{% endfor %} +{% if questions.object_list|length == 0 %} {% include "main_page/nothing_found.html" %} {% else %} <div class="evenMore"> diff --git a/askbot/skins/default/templates/main_page/tab_bar.html b/askbot/skins/default/templates/main_page/tab_bar.html index 7a51aa94..533940d0 100644 --- a/askbot/skins/default/templates/main_page/tab_bar.html +++ b/askbot/skins/default/templates/main_page/tab_bar.html @@ -20,11 +20,11 @@ {% if query %} <a id="by_relevance" {% if sort == "relevance-desc" %} - href="{% url questions %}{{ query_string|replace_in_url("sort:relevance-desc") }}" + href="{{ search_state.change_sort('relevance-desc').full_url() }}" class="on" title="{{asc_relevance_tooltip}}"><span>{{relevance_label}} ▼</span> {% else %} - href="{% url questions %}{{ query_string|replace_in_url("sort:relevance-desc") }}" + href="{{ search_state.change_sort('relevance-desc').full_url() }}" class="off" title="{{desc_relevance_tooltip}}"><span>{{relevance_label}}</span> {% endif %} @@ -45,7 +45,7 @@ asc_tooltip = gettext('click to see the oldest questions'), desc_tooltip = gettext('click to see the newest questions'), current_sort_method = sort, - query_string = query_string, + search_state = search_state, ) }} {{macros.reversible_sort_button( @@ -54,7 +54,7 @@ asc_tooltip = gettext('click to see the least recently updated questions'), desc_tooltip = gettext('click to see the most recently updated questions'), current_sort_method = sort, - query_string = query_string, + search_state = search_state, ) }} {{macros.reversible_sort_button( @@ -63,7 +63,7 @@ asc_tooltip = gettext('click to see the least answered questions'), desc_tooltip = gettext('click to see the most answered questions'), current_sort_method = sort, - query_string = query_string, + search_state = search_state, ) }} {{macros.reversible_sort_button( @@ -72,7 +72,7 @@ asc_tooltip = gettext('click to see least voted questions'), desc_tooltip = gettext('click to see most voted questions'), current_sort_method = sort, - query_string = query_string, + search_state = search_state, ) }} </div> diff --git a/askbot/skins/default/templates/question/content.html b/askbot/skins/default/templates/question/content.html index bb0c9496..738738dd 100644 --- a/askbot/skins/default/templates/question/content.html +++ b/askbot/skins/default/templates/question/content.html @@ -17,7 +17,7 @@ {# ==== END: question/answer_tab_bar.html ==== #} <div class="clean"></div> - {{ macros.paginator(paginator_context) }} + {{ macros.paginator(paginator_context, anchor='#sort-top') }} <div class="clean"></div> {% for answer in answers %} @@ -25,7 +25,7 @@ {% include "question/answer_card.html" %} {# ==== END: question/answer_card.html ==== #} {% endfor %} - {{ macros.paginator(paginator_context) }} + {{ macros.paginator(paginator_context, anchor='#sort-top') }} <div class="clean"></div> {% else %} {# ==== START: question/sharing_prompt_phrase.html ==== #} diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html index eb56d47c..0b85f648 100644 --- a/askbot/skins/default/templates/user_profile/user_stats.html +++ b/askbot/skins/default/templates/user_profile/user_stats.html @@ -72,9 +72,7 @@ {{ macros.tag_widget( tag.name, html_tag = 'div', - url_params = - "author=" ~ view_user.id ~ - "&start_over=true", + search_state = search_state, extra_content = '<span class="tag-number">× ' ~ tag.user_tag_usage_count|intcomma ~ diff --git a/askbot/skins/default/templates/user_profile/users_questions.html b/askbot/skins/default/templates/user_profile/users_questions.html index c46e5f82..128c6612 100644 --- a/askbot/skins/default/templates/user_profile/users_questions.html +++ b/askbot/skins/default/templates/user_profile/users_questions.html @@ -2,7 +2,7 @@ {% import "macros.html" as macros %} <div class="user-stats-table"> {% for question in questions %} - {{macros.question_summary(question.thread, question, extra_class='narrow')}} + {{macros.question_summary(question.thread, question, extra_class='narrow', search_state=search_state)}} {% endfor %} </div> <!-- end users_questions.html --> diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html index db44e435..feebd27f 100644 --- a/askbot/skins/default/templates/widgets/question_summary.html +++ b/askbot/skins/default/templates/widgets/question_summary.html @@ -52,6 +52,6 @@ </div> </div> <h2><a title="{{question.summary|escape}}" href="{{ question.get_absolute_url() }}">{{thread.get_title(question)|escape}}</a></h2> - {{ tag_list_widget(thread.get_tag_names(), url_params=query_string) }} + {{ tag_list_widget(thread.get_tag_names(), search_state=search_state) }} </div> diff --git a/askbot/skins/default/templates/widgets/scope_nav.html b/askbot/skins/default/templates/widgets/scope_nav.html index 5f7cc158..a6bda630 100644 --- a/askbot/skins/default/templates/widgets/scope_nav.html +++ b/askbot/skins/default/templates/widgets/scope_nav.html @@ -1,11 +1,14 @@ {% if active_tab != "ask" %} + {% if not search_state %} {# get empty SearchState() if there's none #} + {% set search_state=search_state|get_empty_search_state %} + {% endif %} <a class="scope-selector {% if scope == 'all' %}on{% endif %}" - href="{% url questions %}?scope=all" title="{% trans %}see all questions{% endtrans %}">{% trans %}ALL{% endtrans %}</a> + href="{{ search_state.change_scope('all').full_url() }}" title="{% trans %}see all questions{% endtrans %}">{% trans %}ALL{% endtrans %}</a> <a class="scope-selector {% if scope == 'unanswered' %}on{% endif %}" - href="{% url questions %}?scope=unanswered&sort=answers-asc" title="{% trans %}see unanswered questions{% endtrans %}">{% trans %}UNANSWERED{% endtrans %}</a> + href="{{ search_state.change_scope('unanswered').change_sort('answers-asc').full_url() }}" title="{% trans %}see unanswered questions{% endtrans %}">{% trans %}UNANSWERED{% endtrans %}</a> {% if request.user.is_authenticated() %} - <a class="scope-selector {% if scope == 'favorite' %}on{% endif %}" - href="{% url questions %}?scope=favorite" title="{% trans %}see your followed questions{% endtrans %}">{% trans %}FOLLOWED{% endtrans %}</a> + <a class="scope-selector {% if scope == 'favorite' %}on{% endif %}" + href="{{ search_state.change_scope('favorite').full_url() }}" title="{% trans %}see your followed questions{% endtrans %}">{% trans %}FOLLOWED{% endtrans %}</a> {% endif %} {% else %} <div class="scope-selector ask-message">{% trans %}Please ask your question here{% endtrans %}</div> diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py index f4e0a5ee..e4fcebd4 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -275,3 +275,8 @@ def humanize_counter(number): @register.filter def absolute_value(number): return abs(number) + +@register.filter +def get_empty_search_state(unused): + from askbot.search.state_manager import SearchState + return SearchState.get_empty() diff --git a/askbot/templatetags/extra_tags.py b/askbot/templatetags/extra_tags.py index c7491901..dc9da5fc 100644 --- a/askbot/templatetags/extra_tags.py +++ b/askbot/templatetags/extra_tags.py @@ -82,56 +82,6 @@ def tag_font_size(max_size, min_size, current_size): return int(MIN_FONTSIZE + round((MAX_FONTSIZE - MIN_FONTSIZE) * weight)) -#todo: this function may need to be removed to simplify the paginator functionality -LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 5 -LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 4 -NUM_PAGES_OUTSIDE_RANGE = 1 -ADJACENT_PAGES = 2 -@register.inclusion_tag("paginator.html") -def cnprog_paginator(context): - """ - custom paginator tag - Inspired from http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/ - """ - if (context["is_paginated"]): - " Initialize variables " - in_leading_range = in_trailing_range = False - pages_outside_leading_range = pages_outside_trailing_range = range(0) - - if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED): - in_leading_range = in_trailing_range = True - page_numbers = [n for n in range(1, context["pages"] + 1) if n > 0 and n <= context["pages"]] - elif (context["page"] <= LEADING_PAGE_RANGE): - in_leading_range = True - page_numbers = [n for n in range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) if n > 0 and n <= context["pages"]] - pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] - elif (context["page"] > context["pages"] - TRAILING_PAGE_RANGE): - in_trailing_range = True - page_numbers = [n for n in range(context["pages"] - TRAILING_PAGE_RANGE_DISPLAYED + 1, context["pages"] + 1) if n > 0 and n <= context["pages"]] - pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] - else: - page_numbers = [n for n in range(context["page"] - ADJACENT_PAGES, context["page"] + ADJACENT_PAGES + 1) if n > 0 and n <= context["pages"]] - pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] - pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] - - extend_url = context.get('extend_url', '') - return { - "base_url": context["base_url"], - "is_paginated": context["is_paginated"], - "previous": context["previous"], - "has_previous": context["has_previous"], - "next": context["next"], - "has_next": context["has_next"], - "page": context["page"], - "pages": context["pages"], - "page_numbers": page_numbers, - "in_leading_range" : in_leading_range, - "in_trailing_range" : in_trailing_range, - "pages_outside_leading_range": pages_outside_leading_range, - "pages_outside_trailing_range": pages_outside_trailing_range, - "extend_url" : extend_url - } - class IncludeJinja(template.Node): """http://www.mellowmorning.com/2010/08/24/""" @@ -162,3 +112,4 @@ def include_jinja(parser, token): raise template.TemplateSyntaxError('file name must be quoted') return IncludeJinja(filename, request_var) + diff --git a/askbot/tests/search_state_tests.py b/askbot/tests/search_state_tests.py index 21c416f8..4e60dcd6 100644 --- a/askbot/tests/search_state_tests.py +++ b/askbot/tests/search_state_tests.py @@ -1,101 +1,201 @@ -import re -import unittest -from django.test import TestCase -from django.contrib.auth.models import AnonymousUser -from askbot.search.state_manager import SearchState, parse_query -from askbot import const - -DEFAULT_SORT = const.DEFAULT_POST_SORT_METHOD -class SearchStateTests(TestCase): - def setUp(self): - self.state = SearchState() - - def update(self, data, user = None): - if user is None: - user = AnonymousUser() - self.state.update_from_user_input(input_dict=data, user_logged_in=user.is_authenticated()) - - def add_tag(self, tag): - self.update({'tags': set([tag])}) - - def remove_tag(self, tag): - self.update({'remove_tag': tag}) - - def assert_tags_are(self, *args): - self.assertEqual(self.state.tags, set(args)) - - def test_add_remove_tags(self): - self.add_tag('tag1') - self.assert_tags_are('tag1') - self.add_tag('tag2') - self.assert_tags_are('tag1', 'tag2') - self.add_tag('tag3') - self.assert_tags_are('tag1', 'tag2', 'tag3') - self.remove_tag('tag3') - self.assert_tags_are('tag1', 'tag2') - self.remove_tag('tag2') - self.assert_tags_are('tag1') - self.remove_tag('tag1') - self.assertEqual(len(self.state.tags), 0) - - def test_query_and_tags1(self): - self.update({'query': 'hahaha'}) - self.add_tag('tag1') - self.assertEquals(self.state.query, 'hahaha') - self.assert_tags_are('tag1') - self.update({'reset_query': True}) - self.assertEquals(self.state.query, None) - self.assert_tags_are('tag1') - - def test_start_over(self): - self.update({'query': 'hahaha'}) - self.add_tag('tag1') - self.update({'start_over': True}) - self.assertEquals(self.state.query, None) - self.assertEquals(self.state.tags, None) - - def test_auto_reset_sort(self): - self.update({'sort': 'age-asc'}) - self.assertEquals(self.state.sort, 'age-asc') - self.update({}) - self.assertEquals(self.state.sort, DEFAULT_SORT) - - -class ParseQueryTests(unittest.TestCase): +from askbot.tests.utils import AskbotTestCase +from askbot.search.state_manager import SearchState +import askbot.conf + + +class SearchStateTests(AskbotTestCase): + def _ss(self, query=None, tags=None): + return SearchState( + scope=None, + sort=None, + query=query, + tags=tags, + author=None, + page=None, + page_size=None, + + user_logged_in=False + ) + + def test_no_selectors(self): + ss = self._ss() + self.assertEqual( + 'scope:all/sort:activity-desc/page_size:30/page:1/', # search defaults + ss.query_string() + ) + + def test_buggy_selectors(self): + ss = SearchState( + scope='blah1', + sort='blah2', + query=None, + tags=None, + + # INFO: URLs for the following selectors accept only digits! + author=None, + page='0', + page_size='59', + + user_logged_in=False + ) + self.assertEqual( + 'scope:all/sort:activity-desc/page_size:30/page:1/', # search defaults + ss.query_string() + ) + + def test_all_valid_selectors(self): + ss = SearchState( + scope='unanswered', + sort='age-desc', + query=' alfa', + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=False + ) + self.assertEqual( + 'scope:unanswered/sort:age-desc/query:alfa/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + ) + + def test_edge_cases_1(self): + ss = SearchState( + scope='favorite', # this is not a valid choice for non-logger users + sort='age-desc', + query=' alfa', + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=False + ) + self.assertEqual( + 'scope:all/sort:age-desc/query:alfa/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + ) + + ss = SearchState( + scope='favorite', + sort='age-desc', + query=' alfa', + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=True + ) + self.assertEqual( + 'scope:favorite/sort:age-desc/query:alfa/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + + ) + + def test_edge_cases_2(self): + old_func = askbot.conf.should_show_sort_by_relevance + askbot.conf.should_show_sort_by_relevance = lambda: True # monkey patch + + ss = SearchState( + scope='all', + sort='relevance-desc', + query='hejho', + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=False + ) + self.assertEqual( + 'scope:all/sort:relevance-desc/query:hejho/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + ) + + ss = SearchState( + scope='all', + sort='relevance-desc', # this is not a valid choice for empty queries + query=None, + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=False + ) + self.assertEqual( + 'scope:all/sort:activity-desc/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + ) + + askbot.conf.should_show_sort_by_relevance = lambda: False # monkey patch + + ss = SearchState( + scope='all', + sort='relevance-desc', # this is also invalid for db-s other than Postgresql + query='hejho', + tags='miki, mini', + author='12', + page='2', + page_size='50', + + user_logged_in=False + ) + self.assertEqual( + 'scope:all/sort:activity-desc/query:hejho/tags:miki,mini/author:12/page_size:50/page:2/', + ss.query_string() + ) + + askbot.conf.should_show_sort_by_relevance = old_func + + def test_query_escaping(self): + ss = self._ss(query=' alfa miki maki +-%#?= lalala/: ') # query coming from URL is already unescaped + self.assertEqual( + 'scope:all/sort:activity-desc/query:alfa%20miki%20maki%20+-%25%23%3F%3D%20lalala%2F%3A/page_size:30/page:1/', + ss.query_string() + ) + + def test_tag_escaping(self): + ss = self._ss(tags=' aA09_+.-#, miki ') # tag string coming from URL is already unescaped + self.assertEqual( + 'scope:all/sort:activity-desc/tags:aA09_+.-%23,miki/page_size:30/page:1/', + ss.query_string() + ) + def test_extract_users(self): - text = '@anna haha @"maria fernanda" @\'diego maradona\' hehe [user:karl marx] hoho user:\' george bush \'' - parse_results = parse_query(text) + ss = self._ss(query='"@anna haha @"maria fernanda" @\'diego maradona\' hehe [user:karl marx] hoho user:\' george bush \'') self.assertEquals( - sorted(parse_results['query_users']), + sorted(ss.query_users), sorted(['anna', 'maria fernanda', 'diego maradona', 'karl marx', 'george bush']) ) - self.assertEquals(parse_results['stripped_query'], 'haha hehe hoho') + self.assertEquals(ss.stripped_query, '" haha hehe hoho') + self.assertEqual( + 'scope:all/sort:activity-desc/query:%22%40anna%20haha%20%40%22maria%20fernanda%22%20%40%27diego%20maradona%27%20hehe%20%5Buser%3Akarl%20%20marx%5D%20hoho%20%20user%3A%27%20george%20bush%20%20%27/page_size:30/page:1/', + ss.query_string() + ) def test_extract_tags(self): - text = '#tag1 [tag: tag2] some text [tag3] query' - parse_results = parse_query(text) - self.assertEquals(set(parse_results['query_tags']), set(['tag1', 'tag2', 'tag3'])) - self.assertEquals(parse_results['stripped_query'], 'some text query') + ss = self._ss(query='#tag1 [tag: tag2] some text [tag3] query') + self.assertEquals(set(ss.query_tags), set(['tag1', 'tag2', 'tag3'])) + self.assertEquals(ss.stripped_query, 'some text query') def test_extract_title1(self): - text = 'some text query [title: what is this?]' - parse_results = parse_query(text) - self.assertEquals(parse_results['query_title'], 'what is this?') - self.assertEquals(parse_results['stripped_query'], 'some text query') + ss = self._ss(query='some text query [title: what is this?]') + self.assertEquals(ss.query_title, 'what is this?') + self.assertEquals(ss.stripped_query, 'some text query') def test_extract_title2(self): - text = 'some text query title:"what is this?"' - parse_results = parse_query(text) - self.assertEquals(parse_results['query_title'], 'what is this?') - self.assertEquals(parse_results['stripped_query'], 'some text query') + ss = self._ss(query='some text query title:"what is this?"') + self.assertEquals(ss.query_title, 'what is this?') + self.assertEquals(ss.stripped_query, 'some text query') def test_extract_title3(self): - text = 'some text query title:\'what is this?\'' - parse_results = parse_query(text) - self.assertEquals(parse_results['query_title'], 'what is this?') - self.assertEquals(parse_results['stripped_query'], 'some text query') + ss = self._ss(query='some text query title:\'what is this?\'') + self.assertEquals(ss.query_title, 'what is this?') + self.assertEquals(ss.stripped_query, 'some text query') def test_negative_match(self): - text = 'some query text' - parse_results = parse_query(text) - self.assertEquals(parse_results['stripped_query'], 'some query text') + ss = self._ss(query='some query text') + self.assertEquals(ss.stripped_query, 'some query text') diff --git a/askbot/urls.py b/askbot/urls.py index 3c080470..661581e5 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -66,7 +66,7 @@ urlpatterns = patterns('', r'(%s)?' % r'/scope:(?P<scope>\w+)' + r'(%s)?' % r'/sort:(?P<sort>[\w\-]+)' + r'(%s)?' % r'/query:(?P<query>[^/]+)' + # INFO: question string cannot contain slash (/), which is a section terminator - r'(%s)?' % r'/tags:(?P<tags>[\w\d\-\+\#]+)' + + r'(%s)?' % r'/tags:(?P<tags>[\w+.#,-]+)' + # Should match: const.TAG_CHARS + ','; TODO: Is `#` char decoded by the time URLs are processed ?? r'(%s)?' % r'/author:(?P<author>\d+)' + r'(%s)?' % r'/page_size:(?P<page_size>\d+)' + r'(%s)?' % r'/page:(?P<page>\d+)' + diff --git a/askbot/utils/functions.py b/askbot/utils/functions.py index d31d9027..f47f0d2a 100644 --- a/askbot/utils/functions.py +++ b/askbot/utils/functions.py @@ -118,7 +118,6 @@ def setup_paginator(context): pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)] - extend_url = context.get('extend_url', '') return { "base_url": context["base_url"], "is_paginated": context["is_paginated"], @@ -133,7 +132,6 @@ def setup_paginator(context): "in_trailing_range" : in_trailing_range, "pages_outside_leading_range": pages_outside_leading_range, "pages_outside_trailing_range": pages_outside_trailing_range, - "extend_url" : extend_url } def get_admin(): diff --git a/askbot/views/readers.py b/askbot/views/readers.py index aad42b60..538d8ad2 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -91,15 +91,16 @@ def questions(request, **kwargs): paginator_context = { 'is_paginated' : (paginator.count > search_state.page_size), + 'pages': paginator.num_pages, 'page': search_state.page, 'has_previous': page.has_previous(), 'has_next': page.has_next(), 'previous': page.previous_page_number(), 'next': page.next_page_number(), + 'base_url' : search_state.query_string(),#todo in T sort=>sort_method 'page_size' : search_state.page_size,#todo in T pagesize -> page_size - 'parameters': search_state.make_parameters(), } # We need to pass the rss feed url based @@ -121,87 +122,52 @@ def questions(request, **kwargs): reset_method_count = len(filter(None, [search_state.query, search_state.tags, meta_data.get('author_name', None)])) if request.is_ajax(): - q_count = paginator.count + if search_state.tags: - question_counter = ungettext( - '%(q_num)s question, tagged', - '%(q_num)s questions, tagged', - q_count - ) % { - 'q_num': humanize.intcomma(q_count), - } + question_counter = ungettext('%(q_num)s question, tagged', '%(q_num)s questions, tagged', q_count) else: - question_counter = ungettext( - '%(q_num)s question', - '%(q_num)s questions', - q_count - ) % { - 'q_num': humanize.intcomma(q_count), - } + question_counter = ungettext('%(q_num)s question', '%(q_num)s questions', q_count) + question_counter = question_counter % {'q_num': humanize.intcomma(q_count),} if q_count > search_state.page_size: paginator_tpl = get_template('main_page/paginator.html', request) - #todo: remove this patch on context after all templates are moved to jinja - #paginator_context['base_url'] = request.path + '?sort=%s&' % search_state.sort - data = { - 'context': extra_tags.cnprog_paginator(paginator_context), + paginator_html = paginator_tpl.render(Context({ + 'context': functions.setup_paginator(paginator_context), 'questions_count': q_count, 'page_size' : search_state.page_size, - } - paginator_html = paginator_tpl.render(Context(data)) + 'search_state': search_state, + })) else: paginator_html = '' - search_tags = list() - if search_state.tags: - search_tags = list(search_state.tags) - query_data = { - 'tags': search_tags, - 'sort_order': search_state.sort - } + + questions_tpl = get_template('main_page/questions_loop.html', request) + questions_html = questions_tpl.render(Context({ + 'questions': page, + 'search_state': search_state, + 'reset_method_count': reset_method_count, + })) + ajax_data = { - #current page is 1 by default now - #because ajax is only called by update in the search button - 'query_data': query_data, + 'query_data': { + 'tags': search_state.tags, + 'sort_order': search_state.sort + }, 'paginator': paginator_html, 'question_counter': question_counter, 'questions': list(), - 'related_tags': list(), 'faces': [extra_tags.gravatar(contributor, 48) for contributor in contributors], 'feed_url': context_feed_url, 'query_string': search_state.query_string(), - 'parameters': search_state.make_parameters(), 'page_size' : search_state.page_size, + 'questions': questions_html.replace('\n',''), } + ajax_data['related_tags'] = [{ + 'name': tag.name, + 'used_count': humanize.intcomma(tag.local_used_count) + } for tag in related_tags] - for tag in related_tags: - tag_data = { - 'name': tag.name, - 'used_count': humanize.intcomma(tag.local_used_count) - } - ajax_data['related_tags'].append(tag_data) - - #we render the template - #from django.template import RequestContext - questions_tpl = get_template('main_page/questions_loop.html', request) - #todo: remove this patch on context after all templates are moved to jinja - data = { - 'questions': page, - 'questions_count': q_count, - 'context': paginator_context, - 'language_code': translation.get_language(), - 'query': search_state.query, - 'reset_method_count': reset_method_count, - 'query_string': search_state.query_string(), - } - - questions_html = questions_tpl.render(Context(data)) - #import pdb; pdb.set_trace() - ajax_data['questions'] = questions_html.replace('\n','') - return HttpResponse( - simplejson.dumps(ajax_data), - mimetype = 'application/json' - ) + return HttpResponse(simplejson.dumps(ajax_data), mimetype = 'application/json') else: # non-AJAX branch @@ -232,7 +198,7 @@ def questions(request, **kwargs): 'tag_filter_strategy_choices': const.TAG_FILTER_STRATEGY_CHOICES, 'update_avatar_data': schedules.should_update_avatar_data(request), 'query_string': search_state.query_string(), - 'parameters': search_state.make_parameters(), + 'search_state': search_state, 'feed_url': context_feed_url, } @@ -285,7 +251,7 @@ def tags(request):#view showing a listing of available tags - plain list 'next': tags.next_page_number(), 'base_url' : reverse('tags') + '?sort=%s&' % sortby } - paginator_context = extra_tags.cnprog_paginator(paginator_data) + paginator_context = functions.setup_paginator(paginator_data) data = { 'active_tab': 'tags', 'page_class': 'tags-page', @@ -294,7 +260,7 @@ def tags(request):#view showing a listing of available tags - plain list 'stag' : stag, 'tab_id' : sortby, 'keywords' : stag, - 'paginator_context' : paginator_context + 'paginator_context' : paginator_context, } else: @@ -530,9 +496,8 @@ def question(request, id):#refactor - long subroutine. display question body, an 'previous': page_objects.previous_page_number(), 'next': page_objects.next_page_number(), 'base_url' : request.path + '?sort=%s&' % answer_sort_method, - 'extend_url' : "#sort-top" } - paginator_context = extra_tags.cnprog_paginator(paginator_data) + paginator_context = functions.setup_paginator(paginator_data) favorited = thread.has_favorite_by_user(request.user) user_question_vote = 0 @@ -561,7 +526,7 @@ def question(request, id):#refactor - long subroutine. display question body, an 'paginator_context' : paginator_context, 'show_post': show_post, 'show_comment': show_comment, - 'show_comment_position': show_comment_position + 'show_comment_position': show_comment_position, } return render_into_skin('question.html', data, request) diff --git a/askbot/views/users.py b/askbot/views/users.py index 6b151c62..2b779d44 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -25,10 +25,12 @@ from django.http import HttpResponseRedirect, Http404 from django.utils.translation import ugettext as _ from django.utils import simplejson from django.views.decorators import csrf + from askbot.utils.slug import slugify from askbot.utils.html import sanitize_html from askbot.utils.mail import send_mail from askbot.utils.http import get_request_info +from askbot.utils import functions from askbot import forms from askbot import const from askbot.conf import settings as askbot_settings @@ -37,6 +39,7 @@ from askbot import exceptions from askbot.models.badges import award_badges_signal from askbot.skins.loaders import render_into_skin from askbot.templatetags import extra_tags +from askbot.search.state_manager import SearchState def owner_or_moderator_required(f): @@ -106,7 +109,7 @@ def users(request): 'next': users_page.next_page_number(), 'base_url' : base_url } - paginator_context = extra_tags.cnprog_paginator(paginator_data) + paginator_context = functions.setup_paginator(paginator_data) # data = { 'active_tab': 'users', 'page_class': 'users-page', @@ -793,8 +796,20 @@ def user(request, id, slug=None, tab_name=None): user_view_func = USER_VIEW_CALL_TABLE.get(tab_name, user_stats) + search_state = SearchState( + scope=None, + sort=None, + query=None, + tags=None, + author=profile_owner.id, + page=None, + page_size=None, + user_logged_in=profile_owner.is_authenticated(), + ) + context = { 'view_user': profile_owner, + 'search_state': search_state, 'user_follow_feature_on': ('followit' in django_settings.INSTALLED_APPS), } return user_view_func(request, profile_owner, context) |