From 5c27752e396ef1b6e794a489b577b0f7be3126b6 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 6 Jul 2010 20:25:06 +0000 Subject: [PATCH] Improves the pagination, adds a new sorting method for searches. Fixes some issues with the module system. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@490 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/models/question.py | 2 +- forum/settings/base.py | 1 + forum/skins/default/media/style/style.css | 19 +- .../templates/paginator/page_numbers.html | 25 ++ .../templates/paginator/page_sizes.html | 13 + .../templates/paginator/sort_tabs.html | 13 + forum/skins/default/templates/questions.html | 13 +- forum/templatetags/ui_registry.py | 24 +- forum/utils/pagination.py | 231 ++++++++++++++++++ forum/views/commands.py | 4 +- forum/views/decorators.py | 48 ---- forum/views/readers.py | 45 ++-- forum_modules/pgfulltext/handlers.py | 3 +- 13 files changed, 355 insertions(+), 86 deletions(-) create mode 100644 forum/skins/default/templates/paginator/page_numbers.html create mode 100644 forum/skins/default/templates/paginator/page_sizes.html create mode 100644 forum/skins/default/templates/paginator/sort_tabs.html create mode 100644 forum/utils/pagination.py diff --git a/forum/models/question.py b/forum/models/question.py index 7902a50..2cb362c 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -6,7 +6,7 @@ question_view = django.dispatch.Signal(providing_args=['instance', 'user']) class QuestionManager(NodeManager): def search(self, keywords): - return self.filter(models.Q(title__icontains=keywords) | models.Q(body__icontains=keywords)) + return False, self.filter(models.Q(title__icontains=keywords) | models.Q(body__icontains=keywords)) class Question(Node): class Meta(Node.Meta): diff --git a/forum/settings/base.py b/forum/settings/base.py index ae36e25..7436f46 100644 --- a/forum/settings/base.py +++ b/forum/settings/base.py @@ -60,6 +60,7 @@ class BaseSetting(object): return v except KeyValue.DoesNotExist: self._temp = (self.default, datetime.now() + timedelta(seconds=TMP_MINICACHE_SECONDS)) + self.save(self.default) except Exception, e: logging.error("Error retrieving setting from database (%s): %s" % (self.name, str(e))) diff --git a/forum/skins/default/media/style/style.css b/forum/skins/default/media/style/style.css index c61c065..6cbca13 100644 --- a/forum/skins/default/media/style/style.css +++ b/forum/skins/default/media/style/style.css @@ -579,7 +579,7 @@ a.medal:hover { position: relative; } -.tabsA a { +.tabsA a, .sticky-sort-tabs { background: none repeat scroll 0 0 #EEEEEE; border-bottom: 1px solid #CCCCCC; border-right: 1px solid #CCCCCC; @@ -589,10 +589,25 @@ a.medal:hover { height: 20px; line-height: 22px; margin: 5px 4px 0 0; - padding: 0 11px; text-decoration: none; } +.tabsA a { + padding: 0 11px; +} + +.sticky-sort-tabs { + width: 20px; + padding: 0; +} + +.sticky-sort-tabs input { + border: 0; + height: 14px; + width: 14px; + margin: 2px; +} + .tabsA a.on, .tabsA a:hover { background: none repeat scroll 0 0 #FFFFFF; color: #A40000; diff --git a/forum/skins/default/templates/paginator/page_numbers.html b/forum/skins/default/templates/paginator/page_numbers.html new file mode 100644 index 0000000..50e5252 --- /dev/null +++ b/forum/skins/default/templates/paginator/page_numbers.html @@ -0,0 +1,25 @@ +{% spaceless %} +{% load i18n %} + +
+ {% if has_previous %} + « {% trans "previous" %} + {% endif %} + {% for range in page_numbers %} + {% if range %} + {% for num, url in range %} + {% ifequal num current %} + {{ num }} + {% else %} + {{ num }} + {% endifequal %} + {% endfor %} + {% else %} + ... + {% endif %} + {% endfor %} + {% if has_next %} + {% trans "next page" %} » + {% endif %} +
+{% endspaceless %} \ No newline at end of file diff --git a/forum/skins/default/templates/paginator/page_sizes.html b/forum/skins/default/templates/paginator/page_sizes.html new file mode 100644 index 0000000..f0859b5 --- /dev/null +++ b/forum/skins/default/templates/paginator/page_sizes.html @@ -0,0 +1,13 @@ +{% spaceless %} +{% load i18n %} +
+ {% trans "posts per page" %} + {% for size, url in sizes %} + {% ifequal size current %} + {{ size }} + {% else %} + {{ size }} + {% endifequal %} + {% endfor %} +
+{% endspaceless %} \ No newline at end of file diff --git a/forum/skins/default/templates/paginator/sort_tabs.html b/forum/skins/default/templates/paginator/sort_tabs.html new file mode 100644 index 0000000..bd4cb3c --- /dev/null +++ b/forum/skins/default/templates/paginator/sort_tabs.html @@ -0,0 +1,13 @@ +{% load i18n %} +{% spaceless %} +
+ {% for name, label, url, descr in sorts %} + {{ label }} + {% endfor %} + {% comment %} + + + + {% endcomment %} +
+{% endspaceless %} \ No newline at end of file diff --git a/forum/skins/default/templates/questions.html b/forum/skins/default/templates/questions.html index 9c3523a..c0476d8 100644 --- a/forum/skins/default/templates/questions.html +++ b/forum/skins/default/templates/questions.html @@ -8,23 +8,24 @@ {% block title %}{% spaceless %}{{ page_title }}{% endspaceless %}{% endblock %} {% block forejs %} - + {% endblock %} {% block content %} +
{% question_list_title %} - {% question_sort_tabs sort_context %} + {{ questions.sort_tabs }}
-
{% for question in questions %} +
{% for question in questions.page %} {% question_list_item question %} {% endfor %}
{% endblock %} {% block tail %} -
{% cnprog_paginator pagination_context %}
-
{% cnprog_pagesize pagination_context %}
+
{{ questions.page_numbers }}
+
{{ questions.page_sizes }}
{% endblock %} {% block sidebar %} @@ -32,7 +33,7 @@ {% sidebar_upper %} {% tag_selector %} {% sidebar_lower %} - {% question_list_related_tags questions %} + {% question_list_related_tags questions.page %} {% endblock %} diff --git a/forum/templatetags/ui_registry.py b/forum/templatetags/ui_registry.py index 6627b2b..1e76972 100644 --- a/forum/templatetags/ui_registry.py +++ b/forum/templatetags/ui_registry.py @@ -1,5 +1,6 @@ from django import template from forum.modules import ui +import logging register = template.Library() @@ -15,9 +16,15 @@ class LoadRegistryNode(template.Node): for ui_object in self.registry: if ui_object.can_render(context): - if result: - result += separator - result += ui_object.render(context) + try: + if result: + result += separator + result += ui_object.render(context) + except Exception, e: + import traceback + logging.error("Exception %s rendering ui objects %s: \n%s" % ( + e, ui_object, traceback.format_exc() + )) return result @@ -45,8 +52,15 @@ class LoopRegistryNode(template.Node): for ui_object in self.registry: if ui_object.can_render(context): - ui_object.update_context(context) - result += self.nodelist.render(context) + try: + ui_object.update_context(context) + result += self.nodelist.render(context) + except Exception, e: + import traceback + logging.error("Exception %s updating ui loop context %s: \n%s" % ( + e, ui_object, traceback.format_exc() + )) + return result diff --git a/forum/utils/pagination.py b/forum/utils/pagination.py new file mode 100644 index 0000000..684992c --- /dev/null +++ b/forum/utils/pagination.py @@ -0,0 +1,231 @@ +import math +from django.utils.datastructures import SortedDict +from django import template +from django.core.paginator import Paginator, EmptyPage +from django.utils.translation import ugettext as _ +from django.http import Http404 +from django.utils.safestring import mark_safe +from django.utils.http import urlquote +import logging + +class SimpleSort(object): + def __init__(self, label, order_by, description=''): + self.label = label + self.description = description + self.order_by = order_by + + def apply(self, objects): + return objects.order_by(self.order_by) + + +class PaginatorContext(object): + visible_page_range = 5 + outside_page_range = 1 + + base_path = None + + def __init__(self, id, sort_methods=None, default_sort=None, pagesizes=None, default_pagesize=None): + self.id = id + if sort_methods: + self.has_sort = True + self.sort_methods = SortedDict(data=sort_methods) + + if not default_sort: + default_sort = sort_methods[0][0] + + self.default_sort = default_sort + else: + self.has_sort = False + + + if pagesizes: + self.has_pagesize = True + self.pagesizes = pagesizes + + if not default_pagesize: + self.default_pagesize = pagesizes[int(math.ceil(float(len(pagesizes)) / 2)) - 1] + else: + self.default_pagesize = default_pagesize + else: + self.has_pagesize = False + + + +class labels(object): + PAGESIZE = _('pagesize') + PAGE = _('page') + SORT = _('sort') + +page_numbers_template = template.loader.get_template('paginator/page_numbers.html') +page_sizes_template = template.loader.get_template('paginator/page_sizes.html') +sort_tabs_template = template.loader.get_template('paginator/sort_tabs.html') + +def paginated(request, list_name, context, tpl_context): + session_prefs = request.session.get('paginator_%s' % context.id, {}) + objects = tpl_context[list_name] + + if context.has_pagesize: + if request.GET.get(labels.PAGESIZE, None): + try: + pagesize = int(request.GET[labels.PAGESIZE]) + except ValueError: + logging.error('Found invalid page size "%s", loading %s, refered by %s' % ( + request.GET.get(labels.PAGESIZE, ''), request.path, request.META.get('HTTP_REFERER', 'UNKNOWN') + )) + raise Http404() + + session_prefs[labels.PAGESIZE] = pagesize + else: + pagesize = session_prefs.get(labels.PAGESIZE, context.default_pagesize) + + if not pagesize in context.pagesizes: + pagesize = context.default_pagesize + else: + pagesize = 30 + + + + + + try: + page = int(request.GET.get(labels.PAGE, 1)) + except ValueError: + logging.error('Found invalid page number "%s", loading %s, refered by %s' % ( + request.GET.get(labels.PAGE, ''), request.path, request.META.get('HTTP_REFERER', 'UNKNOWN') + )) + raise Http404() + + sort = None + if context.has_sort: + if request.GET.get(labels.SORT, None): + sort = request.GET[labels.SORT] + if session_prefs.get('sticky_sort', False): + session_prefs[labels.SORT] = sort + else: + sort = session_prefs.get(labels.SORT, context.default_sort) + + if not sort in context.sort_methods: + sort = context.default_sort + + objects = context.sort_methods[sort].apply(objects) + + paginator = Paginator(objects, pagesize) + + try: + page_obj = paginator.page(page) + except EmptyPage: + logging.error('Found invalid page number "%s", loading %s, refered by %s' % ( + request.GET.get(labels.PAGE, ''), request.path, request.META.get('HTTP_REFERER', 'UNKNOWN') + )) + raise Http404() + + if context.base_path: + base_path = context.base_path + else: + base_path = request.path + get_params = ["%s=%s" % (k, v) for k, v in request.GET.items() if not k in (labels.PAGE, labels.PAGESIZE, labels.SORT)] + + if get_params: + base_path += "?" + "&".join(get_params) + + url_joiner = "?" in base_path and "&" or "?" + + + def get_page(): + object_list = page_obj.object_list + + if hasattr(object_list, 'lazy'): + return object_list.lazy() + return page_obj.object_list + objects.page = get_page + + total_pages = paginator.num_pages + + if total_pages > 1: + def page_nums(): + total_pages = paginator.num_pages + + has_previous = page > 1 + has_next = page < total_pages + + range_start = page - context.visible_page_range / 2 + range_end = page + context.visible_page_range / 2 + + if range_start < 1: + range_end = context.visible_page_range + range_start = 1 + + if range_end > total_pages: + range_start = total_pages - context.visible_page_range + 1 + range_end = total_pages + if range_start < 1: + range_start = 1 + + page_numbers = [] + + if sort: + url_builder = lambda n: mark_safe("%s%s%s=%s&%s=%s" % (base_path, url_joiner, labels.SORT, sort, labels.PAGE, n)) + else: + url_builder = lambda n: mark_safe("%s%s%s=%s" % (base_path, url_joiner, labels.PAGE, n)) + + if range_start > (context.outside_page_range + 1): + page_numbers.append([(n, url_builder(n)) for n in range(1, context.outside_page_range + 1)]) + page_numbers.append(None) + elif range_start > 1: + page_numbers.append([(n, url_builder(n)) for n in range(1, range_start)]) + + page_numbers.append([(n, url_builder(n)) for n in range(range_start, range_end + 1)]) + + if range_end < (total_pages - context.outside_page_range): + page_numbers.append(None) + page_numbers.append([(n, url_builder(n)) for n in range(total_pages - context.outside_page_range + 1, total_pages + 1)]) + elif range_end < total_pages: + page_numbers.append([(n, url_builder(n)) for n in range(range_end + 1, total_pages + 1)]) + + return page_numbers_template.render(template.Context({ + 'has_previous': has_previous, + 'previous_url': has_previous and url_builder(page - 1) or None, + 'has_next': has_next, + 'next_url': has_next and url_builder(page + 1) or None, + 'current': page, + 'page_numbers': page_numbers + })) + objects.page_numbers = page_nums + else: + objects.page_numbers = '' + + if pagesize: + def page_sizes(): + if sort: + url_builder = lambda s: mark_safe("%s%s%s=%s&%s=%s" % (base_path, url_joiner, labels.SORT, sort, labels.PAGESIZE, s)) + else: + url_builder = lambda s: mark_safe("%s%s%s=%s" % (base_path, url_joiner, labels.PAGESIZE, s)) + + sizes = [(s, url_builder(s)) for s in context.pagesizes] + + return page_sizes_template.render(template.Context({ + 'current': pagesize, + 'sizes': sizes + })) + + objects.page_sizes = page_sizes + else: + objects.page_sizes = '' + + if sort: + def sort_tabs(): + url_builder = lambda s: mark_safe("%s%s%s=%s" % (base_path, url_joiner, labels.SORT, s)) + sorts = [(n, s.label, url_builder(n), s.description) for n, s in context.sort_methods.items()] + + return sort_tabs_template.render(template.Context({ + 'current': sort, + 'sorts': sorts, + 'sticky': session_prefs.get('sticky_sort', False) + })) + objects.sort_tabs = sort_tabs() + else: + objects.sort_tabs = '' + + request.session['paginator_%s' % context.id] = session_prefs + tpl_context[list_name] = objects + return tpl_context \ No newline at end of file diff --git a/forum/views/commands.py b/forum/views/commands.py index ed72bd1..3c49076 100644 --- a/forum/views/commands.py +++ b/forum/views/commands.py @@ -480,10 +480,10 @@ def matching_tags(request): def related_questions(request): if request.POST and request.POST.get('title', None): + can_rank, questions = Question.objects.search(request.POST['title']) return HttpResponse(simplejson.dumps( [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary) - for q in Question.objects.search(request.POST['title']).filter_state(deleted=False)[0:10]]), - mimetype="application/json") + for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json") else: raise Http404() diff --git a/forum/views/decorators.py b/forum/views/decorators.py index a98f1be..d86a62e 100644 --- a/forum/views/decorators.py +++ b/forum/views/decorators.py @@ -30,54 +30,6 @@ def render(template=None, tab=None, tab_title='', weight=500, tabbed=True): return decorator -def list(paginate, default_page_size): - def decorator(func): - def decorated(request, *args, **kwargs): - context = func(request, *args, **kwargs) - - if isinstance(context, HttpResponse): - return context - - pagesize = request.utils.page_size(default_page_size) - page = int(request.GET.get('page', 1)) - - big_list = context[paginate] - paginator = Paginator(big_list, pagesize) - - try: - page_obj = paginator.page(page) - except EmptyPage: - raise Http404() - - context[paginate] = page_obj.object_list.lazy() - - base_path = context.get('base_path', None) or request.path - sort = request.utils.sort_method('') - - context["pagination_context"] = { - 'is_paginated' : True, - 'pages': paginator.num_pages, - 'page': page, - 'has_previous': page_obj.has_previous(), - 'has_next': page_obj.has_next(), - 'previous': page_obj.previous_page_number(), - 'next': page_obj.next_page_number(), - 'base_url' : "%s%ssort=%s&" % (base_path, ('?' in base_path) and '&' or '?', sort), - 'pagesize' : pagesize - } - - context['sort_context'] = { - 'base_url': "%s%ssort=" % (base_path, ('?' in base_path) and '&' or '?'), - 'current': sort, - } - - return context - - return decorated - - return decorator - - class CommandException(Exception): pass diff --git a/forum/views/readers.py b/forum/views/readers.py index 07f6a3f..ea555df 100644 --- a/forum/views/readers.py +++ b/forum/views/readers.py @@ -22,6 +22,7 @@ from django.utils.safestring import mark_safe from forum.utils.html import sanitize_html, hyperlink from forum.utils.diff import textDiff as htmldiff +from forum.utils import pagination from forum.forms import * from forum.models import * from forum.forms import get_next_url @@ -42,6 +43,15 @@ QUESTIONS_PAGE_SIZE = 30 # used in answers ANSWERS_PAGE_SIZE = 10 +class QuestionListPaginatorContext(pagination.PaginatorContext): + def __init__(self): + super (QuestionListPaginatorContext, self).__init__('QUESTIONS_LIST', sort_methods=( + (_('active'), pagination.SimpleSort(_('active'), '-last_activity_at', _("most recently updated questions"))), + (_('newest'), pagination.SimpleSort(_('newest'), '-added_at', _("most recently asked questions"))), + (_('hottest'), pagination.SimpleSort(_('hottest'), '-extra_count', _("hottest questions"))), + (_('mostvoted'), pagination.SimpleSort(_('most voted'), '-score', _("most voted questions"))), + ), pagesizes=(15, 30, 50)) + def feed(request): return RssQuestionFeed( Question.objects.filter_state(deleted=False).order_by('-last_activity_at'), @@ -109,31 +119,19 @@ def user_questions(request, mode, user, slug): request.utils.set_sort_method('active'), page_title=description % user.username) - -@decorators.list('questions', QUESTIONS_PAGE_SIZE) def question_list(request, initial, list_description=_('questions'), sort=None, base_path=None, page_title=_("All Questions"), allowIgnoreTags=True, - feed_url=None): + feed_url=None, + paginator_context=None): questions = initial.filter_state(deleted=False) if request.user.is_authenticated() and allowIgnoreTags: - questions = questions.filter(~Q(tags__id__in = request.user.marked_tags.filter(user_selections__reason = 'bad')) - ) - - if sort is not False: - if sort is None: - sort = request.utils.sort_method('latest') - else: - request.utils.set_sort_method(sort) - - view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-extra_count", "mostvoted":"-score" } - - questions=questions.order_by(view_dic.get(sort, '-added_at')) + questions = questions.filter(~Q(tags__id__in = request.user.marked_tags.filter(user_selections__reason = 'bad'))) if page_title is None: page_title = _("Questions") @@ -149,13 +147,13 @@ def question_list(request, initial, answer_description = _("answers") if not feed_url: - req_params = "&".join(["%s=%s" % (k, v) for k, v in request.GET.items() if not k in ('page', 'pagesize', 'sort')]) + req_params = "&".join(["%s=%s" % (k, v) for k, v in request.GET.items() if not k in (_('page'), _('pagesize'), _('sort'))]) if req_params: req_params = '&' + req_params feed_url = mark_safe(request.path + "?type=rss" + req_params) - return { + return pagination.paginated(request, 'questions', paginator_context or QuestionListPaginatorContext(), { "questions" : questions, "questions_count" : questions.count(), "answer_count" : answer_count, @@ -166,7 +164,7 @@ def question_list(request, initial, "page_title" : page_title, "tab" : "questions", 'feed_url': feed_url, - } + }) def search(request): @@ -187,13 +185,20 @@ def search(request): @decorators.render('questions.html') def question_search(request, keywords): - initial = Question.objects.search(keywords) + can_rank, initial = Question.objects.search(keywords) + + if can_rank: + paginator_context = QuestionListPaginatorContext() + paginator_context.sort_methods[_('ranking')] = pagination.SimpleSort(_('ranking'), '-ranking', _("most relevant questions")) + else: + paginator_context = None return question_list(request, initial, _("questions matching '%(keywords)s'") % {'keywords': keywords}, False, "%s?t=question&q=%s" % (reverse('search'),django_urlquote(keywords)), - _("questions matching '%(keywords)s'") % {'keywords': keywords}) + _("questions matching '%(keywords)s'") % {'keywords': keywords}, + paginator_context=paginator_context) @decorators.render('tags.html', 'tags', _('tags'), weight=100) diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py index 5f18270..04d3a67 100644 --- a/forum_modules/pgfulltext/handlers.py +++ b/forum_modules/pgfulltext/handlers.py @@ -10,7 +10,7 @@ def question_search(self, keywords): tsquery = " | ".join(word_re.findall(keywords)) ilike = keywords + u"%%" - return self.extra( + return True, self.extra( tables = ['forum_rootnode_doc'], select={ 'ranking': """ @@ -23,7 +23,6 @@ def question_search(self, keywords): """], params=[tsquery, ilike], select_params=[tsquery], - order_by=['-ranking'] ) -- 2.39.5