From 80e81e8ba3e132d6b51a0bb4c794d8f2c1f600d9 Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 19 May 2010 23:06:16 +0000 Subject: [PATCH] Improvements on full text search. Related question suggestions when asking (thanks Justin). Option to make sidebar custom blocks not wraped by the default container. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@306 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/models/base.py | 2 +- forum/models/question.py | 7 ++ forum/settings/sidebar.py | 9 +++ forum/skins/default/media/style/style.css | 4 ++ forum/skins/default/templates/ask.html | 51 ++++++++++++- .../notifications/answeraccepted.html | 10 ++- .../templates/sidebar/user_blocks.html | 8 +-- forum/templatetags/email_tags.py | 2 + forum/templatetags/extra_tags.py | 71 +++++++++++++------ forum/templatetags/general_sidebar_tags.py | 2 + forum/urls.py | 4 ++ forum/views/commands.py | 10 ++- forum/views/readers.py | 6 +- forum_modules/pgfulltext/handlers.py | 11 ++- forum_modules/pgfulltext/pg_fts_install.sql | 9 +-- forum_modules/pgfulltext/startup.py | 2 +- settings.py | 2 +- settings_local.py.dist | 1 + 18 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 forum/templatetags/email_tags.py diff --git a/forum/models/base.py b/forum/models/base.py index 1f15bde..627888f 100644 --- a/forum/models/base.py +++ b/forum/models/base.py @@ -273,7 +273,7 @@ class CancelableContent(models.Model): app_label = 'forum' -from node import Node, NodeRevision +from node import Node, NodeRevision, NodeManager diff --git a/forum/models/question.py b/forum/models/question.py index 5484016..9e8dcf6 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -1,9 +1,15 @@ from base import * from tag import Tag from django.utils.translation import ugettext as _ +from forum.modules.decorators import decoratable question_view = django.dispatch.Signal(providing_args=['instance', 'user']) +class QuestionManager(NodeManager): + @decoratable.method + def search(self, keywords): + return self.filter(models.Q(title__icontains=keywords) | models.Q(body__icontains=keywords)) + class Question(Node): class Meta(Node.Meta): proxy = True @@ -12,6 +18,7 @@ class Question(Node): favorite_count = DenormalizedField("actions", action_type="favorite", canceled=False) friendly_name = _("question") + objects = QuestionManager() @property def closed(self): diff --git a/forum/settings/sidebar.py b/forum/settings/sidebar.py index 2ff349e..a2b3f23 100644 --- a/forum/settings/sidebar.py +++ b/forum/settings/sidebar.py @@ -20,6 +20,10 @@ label = "Show Upper Block", help_text = "Check if your pages should display the upper sidebar block.", required=False)) +SIDEBAR_UPPER_DONT_WRAP = Setting('SIDEBAR_UPPER_DONT_WRAP', False, SIDEBAR_SET, dict( +label = "Don't Wrap Upper Block", +help_text = "Don't wrap upper block with the standard style.", +required=False)) SIDEBAR_UPPER_TEXT = Setting('SIDEBAR_UPPER_TEXT', u""" @@ -42,6 +46,11 @@ label = "Show Lower Block", help_text = "Check if your pages should display the lower sidebar block.", required=False)) +SIDEBAR_LOWER_DONT_WRAP = Setting('SIDEBAR_LOWER_DONT_WRAP', False, SIDEBAR_SET, dict( +label = "Don't Wrap Lower Block", +help_text = "Don't wrap lower block with the standard style.", +required=False)) + SIDEBAR_LOWER_TEXT = Setting('SIDEBAR_LOWER_TEXT', u""" ## Learn more about OSQA diff --git a/forum/skins/default/media/style/style.css b/forum/skins/default/media/style/style.css index a8a43ae..373b1d5 100644 --- a/forum/skins/default/media/style/style.css +++ b/forum/skins/default/media/style/style.css @@ -1368,3 +1368,7 @@ div.comment-tools a:hover { color: orange; } +#ask-related-questions { + max-height: 150px; + overflow-y: auto; +} \ No newline at end of file diff --git a/forum/skins/default/templates/ask.html b/forum/skins/default/templates/ask.html index 35a6c2c..ac393cd 100644 --- a/forum/skins/default/templates/ask.html +++ b/forum/skins/default/templates/ask.html @@ -7,6 +7,14 @@ + {% endblock %} @@ -80,11 +128,10 @@ {{ form.title.help_text }} - +
{{ form.text }} {{ form.text.errors }} -
diff --git a/forum/skins/default/templates/notifications/answeraccepted.html b/forum/skins/default/templates/notifications/answeraccepted.html index 504970b..bdbf79a 100644 --- a/forum/skins/default/templates/notifications/answeraccepted.html +++ b/forum/skins/default/templates/notifications/answeraccepted.html @@ -3,11 +3,17 @@ {% load extra_tags %} {% block content %} + {% var accepted_by = answer.accepted.by.username %} + {% var answer_author = answer.author.username %} + {% var app_url = settings.APP_URL %} + {% var question_url = question.get_absolute_url %} + {% var question_title = question.title %} +

{% trans "Hello" %} {% user_var username %},

- {% blocktrans with answer.accepted.by.username as accepter and answer.author.username as answer_author and settings.APP_SHORT_NAME as app_title and settings.APP_URL as app_url and question.get_absolute_url as question_url and question.title as question_title %} - Just to let you know that {{ accepter }} has just accepted {{ answer_author }}'s answer on his question + {% blocktrans %} + Just to let you know that {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question {{ question_title }}: {% endblocktrans %}

diff --git a/forum/skins/default/templates/sidebar/user_blocks.html b/forum/skins/default/templates/sidebar/user_blocks.html index 4303e31..b7fd4b2 100644 --- a/forum/skins/default/templates/sidebar/user_blocks.html +++ b/forum/skins/default/templates/sidebar/user_blocks.html @@ -1,9 +1,9 @@ {% load markup %} {% if show %} -
-
+{% if wrap %}
+
{% endif %} {{ content|markdown }} -
-
+ {% if wrap %}
+
{% endif %} {% endif %} \ No newline at end of file diff --git a/forum/templatetags/email_tags.py b/forum/templatetags/email_tags.py new file mode 100644 index 0000000..0f62bad --- /dev/null +++ b/forum/templatetags/email_tags.py @@ -0,0 +1,2 @@ +from django import template + diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 9f595c8..9f215b5 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -372,30 +372,61 @@ def user_var(parser, token): return UserVarNode(tokens) -class VariablesNode(template.Node): - def __init__(self, nodelist, var_name): - self.nodelist = nodelist - self.var_name = var_name +class SimpleVarNode(template.Node): + def __init__(self, name, value): + self.name = name + self.value = template.Variable(value) def render(self, context): - source = self.nodelist.render(context) - context[self.var_name] = simplejson.loads(source) + context[self.name] = self.value.resolve(context) return '' +class BlockVarNode(template.Node): + def __init__(self, name, block): + self.name = name + self.block = block + + def render(self, context): + source = self.block.render(context) + context[self.name] = source.strip() + return '' + + @register.tag(name='var') -def do_variables(parser, token): - try: - tag_name, arg = token.contents.split(None, 1) - except ValueError: - msg = '"%s" tag requires arguments' % token.contents.split()[0] - raise template.TemplateSyntaxError(msg) - m = re.search(r'as (\w+)', arg) - if m: - var_name, = m.groups() - else: - msg = '"%s" tag had invalid arguments' % tag_name - raise template.TemplateSyntaxError(msg) +def do_var(parser, token): + tokens = token.split_contents()[1:] + + if not len(tokens) or not re.match('^\w+$', tokens[0]): + raise template.TemplateSyntaxError("Expected variable name") + + if len(tokens) == 1: + nodelist = parser.parse(('endvar',)) + parser.delete_first_token() + return BlockVarNode(tokens[0], nodelist) + elif len(tokens) == 3: + return SimpleVarNode(tokens[0], tokens[2]) + + raise template.TemplateSyntaxError("Invalid number of arguments") + +class DeclareNode(template.Node): + dec_re = re.compile('^\s*(\w+)\s*(:?=)\s*(.*)$') + + def __init__(self, block): + self.block = block + + def render(self, context): + source = self.block.render(context) + + for line in source.splitlines(): + m = self.dec_re.search(line) + if m and m.group(2) == '=': + context[m.group(1).strip()] = m.group(3).strip() + elif m and m.group(2) == ':=': + context[m.group(1).strip()] = template.Variable(m.group(3).strip()).resolve(context) + return '' - nodelist = parser.parse(('endvar',)) +@register.tag(name='declare') +def do_declare(parser, token): + nodelist = parser.parse(('enddeclare',)) parser.delete_first_token() - return VariablesNode(nodelist, var_name) + return DeclareNode(nodelist) diff --git a/forum/templatetags/general_sidebar_tags.py b/forum/templatetags/general_sidebar_tags.py index 5741d1b..09f30cc 100644 --- a/forum/templatetags/general_sidebar_tags.py +++ b/forum/templatetags/general_sidebar_tags.py @@ -21,6 +21,7 @@ def sidebar_upper(): return { 'show': settings.SIDEBAR_UPPER_SHOW, 'content': settings.SIDEBAR_UPPER_TEXT, + 'wrap': not settings.SIDEBAR_UPPER_DONT_WRAP, 'blockid': 'sidebar-upper' } @@ -29,5 +30,6 @@ def sidebar_lower(): return { 'show': settings.SIDEBAR_LOWER_SHOW, 'content': settings.SIDEBAR_LOWER_TEXT, + 'wrap': not settings.SIDEBAR_LOWER_DONT_WRAP, 'blockid': 'sidebar-lower' } diff --git a/forum/urls.py b/forum/urls.py index 58ccc2c..54624fb 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -3,6 +3,7 @@ import startup import os.path from forum import settings from django.conf.urls.defaults import * +from django.conf import settings as djsettings from django.contrib import admin from forum import views as app from forum.feed import RssLastestQuestionsFeed @@ -37,6 +38,7 @@ urlpatterns += patterns('', #(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/media/images/favicon.ico'}), #(r'^favicon\.gif$', 'django.views.generic.simple.redirect_to', {'url': '/media/images/favicon.gif'}), (r'^favicon\.ico$', app.meta.favicon), + url(r'^m/(?P.*)$', 'django.views.static.serve', {'document_root': os.path.join(APP_PATH,'skins').replace('\\','/')}, name='osqa_media', @@ -56,6 +58,8 @@ urlpatterns += patterns('', url(r'^%s(?P\d+)/$' % _('revisions/'), app.readers.revisions, name='revisions'), url(r'^%s$' % _('questions/'), app.readers.questions, name='questions'), url(r'^%s%s$' % (_('questions/'), _('ask/')), app.writers.ask, name='ask'), + url(r'^%s%s$' % (_('questions/'), _('related_questions/')), app.commands.related_questions, name='related_questions'), + url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.readers.unanswered, name='unanswered'), url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('edit/')), app.writers.edit_question, name='edit_question'), url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('close/')), app.commands.close, kwargs=dict(close=True), name='close'), diff --git a/forum/views/commands.py b/forum/views/commands.py index d829554..e09924b 100644 --- a/forum/views/commands.py +++ b/forum/views/commands.py @@ -2,7 +2,7 @@ import datetime from forum import settings from django.core.exceptions import ObjectDoesNotExist from django.utils import simplejson -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 from django.shortcuts import get_object_or_404, render_to_response from django.utils.translation import ungettext, ugettext as _ from django.template import RequestContext @@ -410,6 +410,14 @@ def matching_tags(request): return HttpResponse(tag_output, mimetype="text/plain") +def related_questions(request): + if request.POST and request.POST.get('title', None): + 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'])[0:10]]), mimetype="application/json") + else: + raise Http404() + diff --git a/forum/views/readers.py b/forum/views/readers.py index b865fbb..a48c35b 100644 --- a/forum/views/readers.py +++ b/forum/views/readers.py @@ -128,13 +128,9 @@ def search(request): else: return render_to_response("search.html", context_instance=RequestContext(request)) -@decoratable -def do_question_search(keywords): - return Question.objects.filter(Q(title__icontains=keywords) | Q(body__icontains=keywords)) - @decorators.render('questions.html') def question_search(request, keywords): - initial = do_question_search(keywords) + initial = Question.objects.search(keywords) return question_list(request, initial, _("questions matching '%(keywords)s'") % {'keywords': keywords}, base_path="%s?t=question&q=%s" % (reverse('search'), django_urlquote(keywords)), sort=False) diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py index 2b57e96..e1d98f2 100644 --- a/forum_modules/pgfulltext/handlers.py +++ b/forum_modules/pgfulltext/handlers.py @@ -1,13 +1,12 @@ -from forum.models import Question +from forum.models.question import Question, QuestionManager from forum.modules.decorators import decorate -from forum.views.readers import do_question_search -@decorate(do_question_search, needs_origin=False) -def question_search(keywords): - return Question.objects.all().extra( +@decorate(QuestionManager.search, needs_origin=False) +def question_search(self, keywords): + return self.extra( tables=['forum_rootnode_doc'], select={ - 'ranking': 'ts_rank_cd("forum_rootnode_doc"."document", plainto_tsquery(\'english\', %s), 32)', + 'ranking': 'ts_rank_cd(\'{0.1, 0.2, 0.8, 1.0}\'::float4[], "forum_rootnode_doc"."document", plainto_tsquery(\'english\', %s), 32)', }, where=['"forum_rootnode_doc"."node_id" = "forum_node"."id"', '"forum_rootnode_doc"."document" @@ plainto_tsquery(\'english\', %s)'], params=[keywords], diff --git a/forum_modules/pgfulltext/pg_fts_install.sql b/forum_modules/pgfulltext/pg_fts_install.sql index bba7eef..ddc48a1 100644 --- a/forum_modules/pgfulltext/pg_fts_install.sql +++ b/forum_modules/pgfulltext/pg_fts_install.sql @@ -53,10 +53,11 @@ begin return new; END IF; - doc := - setweight(to_tsvector('english', coalesce(new.tagnames,'')), 'A') || - setweight(to_tsvector('english', coalesce(new.title,'')), 'B') || - setweight(to_tsvector('english', coalesce(new.body,'')), 'C'); + SELECT + setweight(to_tsvector('english', coalesce(tagnames,'')), 'A') || + setweight(to_tsvector('english', coalesce(title,'')), 'B') || + setweight(to_tsvector('english', coalesce(body,'')), 'C') INTO doc + FROM forum_node WHERE id = root_id; SELECT count(*)::int INTO rcount FROM forum_node WHERE abs_parent_id = root_id AND deleted_id IS NULL; diff --git a/forum_modules/pgfulltext/startup.py b/forum_modules/pgfulltext/startup.py index 0eb3f39..c7cd5ea 100644 --- a/forum_modules/pgfulltext/startup.py +++ b/forum_modules/pgfulltext/startup.py @@ -3,7 +3,7 @@ from forum.models import KeyValue from django.db import connection, transaction import settings -VERSION = 6 +VERSION = 7 if int(settings.PG_FTSTRIGGERS_VERSION) < VERSION: f = open(os.path.join(os.path.dirname(__file__), 'pg_fts_install.sql'), 'r') diff --git a/settings.py b/settings.py index 7b9e1f0..a7c5893 100644 --- a/settings.py +++ b/settings.py @@ -71,7 +71,7 @@ INSTALLED_APPS = [ 'forum', ] -if DEBUG and False: +if DEBUG: try: import debug_toolbar MIDDLEWARE_CLASSES.append('debug_toolbar.middleware.DebugToolbarMiddleware') diff --git a/settings_local.py.dist b/settings_local.py.dist index 3b1cd45..053032b 100644 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -23,6 +23,7 @@ DEBUG_TOOLBAR_CONFIG = { TEMPLATE_DEBUG = DEBUG INTERNAL_IPS = ('127.0.0.1',) + DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. -- 2.39.5