summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askbot/const/__init__.py3
-rw-r--r--askbot/search/state_manager.py133
-rw-r--r--askbot/skins/common/media/js/live_search.js173
-rw-r--r--askbot/skins/common/media/js/tag_selector.js6
-rw-r--r--askbot/skins/common/media/js/utils.js126
-rw-r--r--askbot/skins/common/templates/widgets/related_tags.html2
-rw-r--r--askbot/skins/common/templates/widgets/tag_selector.html6
-rw-r--r--askbot/skins/default/templates/macros.html142
-rw-r--r--askbot/skins/default/templates/main_page/headline.html7
-rw-r--r--askbot/skins/default/templates/main_page/javascript.html18
-rw-r--r--askbot/skins/default/templates/main_page/nothing_found.html16
-rw-r--r--askbot/skins/default/templates/main_page/paginator.html4
-rw-r--r--askbot/skins/default/templates/main_page/questions_loop.html12
-rw-r--r--askbot/skins/default/templates/main_page/tab_bar.html12
-rw-r--r--askbot/skins/default/templates/question/content.html4
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html4
-rw-r--r--askbot/skins/default/templates/user_profile/users_questions.html2
-rw-r--r--askbot/skins/default/templates/widgets/question_summary.html2
-rw-r--r--askbot/skins/default/templates/widgets/scope_nav.html11
-rw-r--r--askbot/templatetags/extra_filters_jinja.py5
-rw-r--r--askbot/templatetags/extra_tags.py51
-rw-r--r--askbot/tests/search_state_tests.py274
-rw-r--r--askbot/urls.py2
-rw-r--r--askbot/utils/functions.py2
-rw-r--r--askbot/views/readers.py101
-rw-r--r--askbot/views/users.py17
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">&#215; ' ~
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}} &#9650;</span></a>
+ href="{{ search_state.change_sort(key_name+"-desc").full_url() }}"
+ class="rev on"
+ title="{{desc_tooltip}}"><span>{{label}} &#9650;</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}} &#9660;</span></a>
+ href="{{ search_state.change_sort(key_name+"-asc").full_url() }}"
+ class="rev on"
+ title="{{asc_tooltip}}"><span>{{label}} &#9660;</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 %}">
&laquo; {% 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 %} &raquo;</a></span>
+ <span class="next"><a href="{{p.base_url}}page={{ p.next }}{{ anchor }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} &raquo;</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 %}">
+ &laquo; {% 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 %} &raquo;</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&section=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}} &#9660;</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">&#215; ' ~
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&amp;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&amp;' % 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&amp;' % 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)