summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-10-25 14:01:18 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-10-25 14:01:18 -0400
commit0b6edcbd782e63423984c1549319b98e4adad9a1 (patch)
tree5d4e1961dc1ccff3a7f461ec2b210d52d5741695
parent0f59c8c0bdcf93959e019aca01862a68d2c51e62 (diff)
parentad54be622d1f45d5086a3ccec45d31d6c338a0aa (diff)
downloadaskbot-0b6edcbd782e63423984c1549319b98e4adad9a1.tar.gz
askbot-0b6edcbd782e63423984c1549319b98e4adad9a1.tar.bz2
askbot-0b6edcbd782e63423984c1549319b98e4adad9a1.zip
Merge branch 'postgres-full-text'
-rw-r--r--askbot/__init__.py6
-rw-r--r--askbot/const/__init__.py33
-rw-r--r--askbot/management/commands/init_postgresql_full_text_search.py17
-rw-r--r--askbot/management/commands/setup_postgresql_full_text_search.plsql197
-rw-r--r--askbot/migrations/0022_init_postgresql_full_text_search.py300
-rw-r--r--askbot/models/answer.py7
-rw-r--r--askbot/models/question.py49
-rw-r--r--askbot/search/state_manager.py12
-rw-r--r--askbot/skins/default/media/js/live_search.js88
-rw-r--r--askbot/skins/default/templates/authopenid/complete.html8
-rw-r--r--askbot/skins/default/templates/macros.html39
-rw-r--r--askbot/skins/default/templates/questions.html150
-rw-r--r--askbot/views/readers.py2
13 files changed, 778 insertions, 130 deletions
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 16117d92..62387bbd 100644
--- a/askbot/__init__.py
+++ b/askbot/__init__.py
@@ -87,3 +87,9 @@ def mail_moderators(subject_line, body_text):
logging.critical(unicode(e))
if raise_on_failure == True:
raise exceptions.EmailNotSent(unicode(e))
+
+def should_show_sort_by_relevance():
+ """True if configuration support sorting
+ questions by search relevance
+ """
+ return (django_settings.DATABASE_ENGINE == 'postgresql_psycopg2')
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py
index dedbd115..7770f858 100644
--- a/askbot/const/__init__.py
+++ b/askbot/const/__init__.py
@@ -35,28 +35,29 @@ TYPE_REPUTATION = (
(10, 'assigned_by_moderator'),
)
-#do not translate these!!!
+#do not translate keys
POST_SORT_METHODS = (
- ('latest', _('newest')),
- ('oldest', _('oldest')),
- ('active', _('active')),
- ('inactive', _('inactive')),
- ('hottest', _('hottest')),
- ('coldest', _('coldest')),
- ('mostvoted', _('most voted')),
- ('leastvoted', _('least voted')),
- ('relevant', _('relevance')),
- )
+ ('age-desc', _('newest')),
+ ('age-asc', _('oldest')),
+ ('activity-desc', _('active')),
+ ('activity-asc', _('inactive')),
+ ('answers-desc', _('hottest')),
+ ('answers-asc', _('coldest')),
+ ('votes-desc', _('most voted')),
+ ('votes-asc', _('least voted')),
+ ('relevance-desc', _('relevance')),
+)
#todo: add assertion here that all sort methods are unique
#because they are keys to the hash used in implementations of Q.run_advanced_search
-DEFAULT_POST_SORT_METHOD = 'active'
+DEFAULT_POST_SORT_METHOD = 'activity-desc'
POST_SCOPE_LIST = (
- ('all', _('all')),
- ('unanswered', _('unanswered')),
- ('favorite', _('favorite')),
- )
+ ('all', _('all')),
+ ('unanswered', _('unanswered')),
+ ('favorite', _('favorite')),
+)
DEFAULT_POST_SCOPE = 'all'
+
PAGE_SIZE_CHOICES = (('10', '10',), ('30', '30',), ('50', '50',),)
#todo: remove this duplication
QUESTIONS_PER_PAGE_USER_CHOICES = (
diff --git a/askbot/management/commands/init_postgresql_full_text_search.py b/askbot/management/commands/init_postgresql_full_text_search.py
new file mode 100644
index 00000000..15d3a936
--- /dev/null
+++ b/askbot/management/commands/init_postgresql_full_text_search.py
@@ -0,0 +1,17 @@
+from django.core.management.base import NoArgsCommand
+from django.db import connection
+import os.path
+
+class Command(NoArgsCommand):
+ def handle_noargs(self, **options):
+
+ dir = os.path.dirname(__file__)
+ sql_file_name = 'setup_postgresql_full_text_search.plsql'
+ sql_file_name = os.path.join(dir, sql_file_name)
+ query = open(sql_file_name).read()
+
+ cursor = connection.cursor()
+ try:
+ cursor.execute(query)
+ finally:
+ cursor.close()
diff --git a/askbot/management/commands/setup_postgresql_full_text_search.plsql b/askbot/management/commands/setup_postgresql_full_text_search.plsql
new file mode 100644
index 00000000..35180003
--- /dev/null
+++ b/askbot/management/commands/setup_postgresql_full_text_search.plsql
@@ -0,0 +1,197 @@
+/* function testing for existence of a column in a table
+ if table does not exists, function will return "false" */
+CREATE OR REPLACE FUNCTION column_exists(colname text, tablename text)
+RETURNS boolean AS
+$$
+DECLARE
+ q text;
+ onerow record;
+BEGIN
+
+ q = 'SELECT attname FROM pg_attribute WHERE attrelid = ( SELECT oid FROM pg_class WHERE relname = '''||tablename||''') AND attname = '''||colname||'''';
+
+ FOR onerow IN EXECUTE q LOOP
+ RETURN true;
+ END LOOP;
+
+ RETURN false;
+END;
+$$ LANGUAGE plpgsql;
+
+/* function adding tsvector column to table if it does not exists */
+CREATE OR REPLACE FUNCTION add_tsvector_column(colname text, tablename text)
+RETURNS boolean AS
+$$
+DECLARE
+ q text;
+BEGIN
+ IF NOT column_exists(colname, tablename) THEN
+ q = 'ALTER TABLE ' || tablename || ' ADD COLUMN ' || colname || ' tsvector';
+ EXECUTE q;
+ RETURN true;
+ ELSE
+ q = 'UPDATE ' || tablename || ' SET ' || colname || '=NULL';
+ EXECUTE q;
+ RETURN false;
+ END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+/* aggregate function that concatenates tsvectors */
+CREATE OR REPLACE FUNCTION tsv_add(tsv1 tsvector, tsv2 tsvector)
+RETURNS tsvector AS
+$$
+BEGIN
+ RETURN tsv1 || tsv2;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION setup_aggregates() RETURNS boolean AS
+$$
+DECLARE
+ onerow record;
+BEGIN
+ FOR onerow IN SELECT * FROM pg_proc WHERE proname = 'concat_tsvectors' AND proisagg LOOP
+ DROP AGGREGATE concat_tsvectors(tsvector);
+ END LOOP;
+ CREATE AGGREGATE concat_tsvectors (
+ BASETYPE = tsvector,
+ SFUNC = tsv_add,
+ STYPE = tsvector,
+ INITCOND = ''
+ );
+ RETURN true;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT setup_aggregates();
+
+/* calculates text search vector for question
+DOES not include answers or comments */
+CREATE OR REPLACE FUNCTION get_question_tsv(title text, text text, tagnames text)
+RETURNS tsvector AS
+$$
+BEGIN
+ RETURN setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
+ setweight(to_tsvector('english', coalesce(text, '')), 'B') ||
+ setweight(to_tsvector('english', coalesce(tagnames, '')), 'A');
+END;
+$$ LANGUAGE plpgsql;
+
+/* calculates text search vector for answer text */
+CREATE OR REPLACE FUNCTION get_answer_tsv(text text) RETURNS tsvector AS
+$$
+BEGIN
+ RETURN setweight(to_tsvector('english', coalesce(text, '')), 'B');
+END;
+$$ LANGUAGE plpgsql;
+
+/* calculate text search vector for comment text */
+CREATE OR REPLACE FUNCTION get_comment_tsv(comment text) RETURNS tsvector AS
+$$
+BEGIN
+ RETURN setweight(to_tsvector('english', coalesce(comment, '')), 'C');
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION get_dependent_comments_tsv(object_id integer, tablename text)
+RETURNS tsvector AS
+$$
+DECLARE
+ query text;
+ onerow record;
+BEGIN
+ query = 'SELECT concat_tsvectors(text_search_vector) FROM comment' ||
+ ' WHERE object_id=' ||object_id|| ' AND content_type_id=(' ||
+ ' SELECT id FROM django_content_type' ||
+ ' WHERE app_label=''askbot'' AND name=''' || tablename || ''')';
+ FOR onerow IN EXECUTE query LOOP
+ RETURN onerow.concat_tsvectors;
+ END LOOP;
+ RETURN to_tsvector('');
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION get_dependent_answers_tsv(question_id integer)
+RETURNS tsvector AS
+$$
+DECLARE
+ query text;
+ onerow record;
+BEGIN
+ query = 'SELECT concat_tsvectors(text_search_vector) ' ||
+ 'FROM answer WHERE question_id = ' || question_id ||
+ ' AND deleted=false';
+ FOR onerow IN EXECUTE query LOOP
+ RETURN onerow.concat_tsvectors;
+ END LOOP;
+ RETURN to_tsvector('');
+END;
+$$ LANGUAGE plpgsql;
+
+/* create tsvector columns in the content tables */
+SELECT add_tsvector_column('text_search_vector', 'question');
+SELECT add_tsvector_column('text_search_vector', 'answer');
+SELECT add_tsvector_column('text_search_vector', 'comment');
+
+/* populate tsvectors with data */
+-- comment tsvectors
+UPDATE comment SET text_search_vector = get_comment_tsv(comment);
+
+-- answer tsvectors
+UPDATE answer SET text_search_vector = get_answer_tsv(text);
+UPDATE answer as a SET text_search_vector = text_search_vector ||
+ get_dependent_comments_tsv(a.id, 'answer');
+
+--question tsvectors
+UPDATE question SET text_search_vector = get_question_tsv(title, text, tagnames);
+
+UPDATE question as q SET text_search_vector = text_search_vector ||
+ get_dependent_comments_tsv(q.id, 'question');
+
+UPDATE question as q SET text_search_vector = text_search_vector ||
+ get_dependent_answers_tsv(q.id);
+
+/* set up update triggers */
+CREATE OR REPLACE FUNCTION question_trigger() RETURNS trigger AS
+$$
+BEGIN
+ new.text_search_vector = get_question_tsv(new.title, new.text, new.tagnames);
+ new.text_search_vector = new.text_search_vector ||
+ get_dependent_comments_tsv(new.id, 'question');
+ new.text_search_vector = new.text_search_vector ||
+ get_dependent_answers_tsv(new.id);
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS question_search_vector_update_trigger on question;
+CREATE TRIGGER question_search_vector_update_trigger
+BEFORE INSERT OR UPDATE ON question FOR EACH ROW EXECUTE PROCEDURE question_trigger();
+
+/* comment trigger */
+CREATE OR REPLACE FUNCTION comment_trigger() RETURNS trigger AS
+$$
+BEGIN
+ new.text_search_vector = get_comment_tsv(new.comment);
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS comment_search_vector_update_trigger on comment;
+CREATE TRIGGER comment_search_vector_update_trigger
+BEFORE INSERT OR UPDATE ON comment FOR EACH ROW EXECUTE PROCEDURE comment_trigger();
+
+/* answer trigger */
+CREATE OR REPLACE FUNCTION answer_trigger() RETURNS trigger AS
+$$
+BEGIN
+ new.text_search_vector = get_answer_tsv(new.text);
+ new.text_search_vector = new.text_search_vector ||
+ get_dependent_comments_tsv(new.id, 'answer');
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS answer_search_vector_update_trigger on answer;
+CREATE TRIGGER answer_search_vector_update_trigger
+BEFORE INSERT OR UPDATE ON answer FOR EACH ROW EXECUTE PROCEDURE answer_trigger();
+
+CREATE INDEX askbot_search_idx ON question USING gin(text_search_vector);
diff --git a/askbot/migrations/0022_init_postgresql_full_text_search.py b/askbot/migrations/0022_init_postgresql_full_text_search.py
new file mode 100644
index 00000000..721db899
--- /dev/null
+++ b/askbot/migrations/0022_init_postgresql_full_text_search.py
@@ -0,0 +1,300 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.core import management
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ management.call_command('init_postgresql_full_text_search')
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'to': "orm['auth.User']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Question']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+ },
+ 'askbot.answer': {
+ 'Meta': {'object_name': 'Answer', 'db_table': "u'answer'"},
+ 'accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answers'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answers'", 'to': "orm['askbot.Question']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.answerrevision': {
+ 'Meta': {'object_name': 'AnswerRevision', 'db_table': "u'answer_revision'"},
+ 'answer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['askbot.Answer']"}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answerrevisions'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.Badge']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badge': {
+ 'Meta': {'unique_together': "(('name', 'type'),)", 'object_name': 'Badge', 'db_table': "u'badge'"},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'through': "'Award'", 'to': "orm['auth.User']"}),
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'multiple': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
+ 'type': ('django.db.models.fields.SmallIntegerField', [], {})
+ },
+ 'askbot.comment': {
+ 'Meta': {'object_name': 'Comment', 'db_table': "u'comment'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '2048'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'html': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2048'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Question']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.flaggeditem': {
+ 'Meta': {'unique_together': "(('content_type', 'object_id', 'user'),)", 'object_name': 'FlaggedItem', 'db_table': "u'flagged_item'"},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'flagged_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'flaggeditems'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.question': {
+ 'Meta': {'object_name': 'Question', 'db_table': "u'question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'questions'", 'to': "orm['auth.User']"}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'closed_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'favorite_questions'", 'through': "'FavoriteQuestion'", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_questions'", 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_active_in_questions'", 'to': "orm['auth.User']"}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'questions'", 'to': "orm['askbot.Tag']"}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.questionrevision': {
+ 'Meta': {'object_name': 'QuestionRevision', 'db_table': "u'question_revision'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'questionrevisions'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['askbot.Question']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Question']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Question']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('content_type', 'object_id', 'user'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+ 'hide_ignored_questions': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'tag_filter_setting': ('django.db.models.fields.CharField', [], {'default': "'ignored'", 'max_length': '16'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['askbot']
diff --git a/askbot/models/answer.py b/askbot/models/answer.py
index d8cd2c27..71b8c8e0 100644
--- a/askbot/models/answer.py
+++ b/askbot/models/answer.py
@@ -2,6 +2,7 @@ import datetime
from django.db import models
from django.utils.http import urlquote as django_urlquote
from django.core.urlresolvers import reverse
+from django.conf import settings
from askbot.models.base import AnonymousContent, DeletableContent
from askbot.models.base import ContentRevision
from askbot.models.base import parse_post_text, parse_and_save_post
@@ -147,6 +148,12 @@ class Answer(content.Content, DeletableContent):
revision=rev_no
)
+ def save(self, *args, **kwargs):
+ super(Answer, self).save(*args, **kwargs);
+ if 'postgres' in settings.DATABASE_ENGINE:
+ #hit the database to trigger update of full text search vector
+ self.question.save()
+
def get_origin_post(self):
return self.question
diff --git a/askbot/models/question.py b/askbot/models/question.py
index cc52682b..27a718ae 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -9,6 +9,7 @@ from django.utils.http import urlquote as django_urlquote
from django.core.urlresolvers import reverse
from django.contrib.sitemaps import ping_google
from django.utils.translation import ugettext as _
+import askbot
from askbot.models.tag import Tag, MarkedTag
from askbot.models import signals
from askbot.models.base import AnonymousContent, DeletableContent, ContentRevision
@@ -22,15 +23,15 @@ from askbot.utils.html import sanitize_html
#todo: too bad keys are duplicated see const sort methods
QUESTION_ORDER_BY_MAP = {
- 'latest': '-added_at',
- 'oldest': 'added_at',
- 'active': '-last_activity_at',
- 'inactive': 'last_activity_at',
- 'hottest': '-answer_count',
- 'coldest': 'answer_count',
- 'mostvoted': '-score',
- 'leastvoted': 'score',
- 'relevant': None #this is a special case
+ 'age-desc': '-added_at',
+ 'age-asc': 'added_at',
+ 'activity-desc': '-last_activity_at',
+ 'activity-asc': 'last_activity_at',
+ 'answers-desc': '-answer_count',
+ 'answers-asc': 'answer_count',
+ 'votes-desc': '-score',
+ 'votes-asc': 'score',
+ 'relevance-desc': None#this is a special case for postges only
}
class QuestionManager(models.Manager):
@@ -111,6 +112,19 @@ class QuestionManager(models.Manager):
| models.Q(tagnames__search = search_query) \
| models.Q(answers__text__search = search_query)
)
+ elif settings.DATABASE_ENGINE == 'postgresql_psycopg2':
+ rank_clause = "ts_rank(question.text_search_vector, to_tsquery('%s'))";
+ search_query = '&'.join(search_query.split())
+ extra_kwargs = {
+ 'select': {'relevance': rank_clause % search_query},
+ 'where': ['text_search_vector @@ to_tsquery(%s)'],
+ 'params': ["'" + search_query + "'"]
+ }
+ if askbot.should_show_sort_by_relevance():
+ if sort_method == 'relevance-desc':
+ extra_kwargs['order_by'] = ['-relevance',]
+
+ qs = qs.extra(**extra_kwargs)
else:
#fallback to dumb title match search
qs = qs.extra(
@@ -206,12 +220,12 @@ class QuestionManager(models.Manager):
select_params = (uid_str, )
)
- #qs = qs.select_related(depth=1)
- #todo: fix orderby here
- orderby = QUESTION_ORDER_BY_MAP[sort_method]
- if orderby:
- #relevance will be ignored here
+ if sort_method != 'relevance-desc':
+ #relevance sort is set in the extra statement
+ #only for postgresql
+ orderby = QUESTION_ORDER_BY_MAP[sort_method]
qs = qs.order_by(orderby)
+
qs = qs.distinct()
qs = qs.select_related(
'last_activity_by__id',
@@ -227,7 +241,6 @@ class QuestionManager(models.Manager):
search_state = search_state,
ignored_tag_names = ignored_tag_names
)
-
return qs, meta_data, related_tags
#todo: this function is similar to get_response_receivers
@@ -237,11 +250,7 @@ class QuestionManager(models.Manager):
answer_list = []
#question_list = list(question_list)#important for MySQL, b/c it does not support
from askbot.models.answer import Answer
- if isinstance(question_list, list):
- q_id = [question.id for question in question_list]
- else:
- q_id = list(question_list.values_list('id', flat=True))
-
+ q_id = [question.id for question in question_list]
a_id = list(Answer.objects.filter(question__in=q_id).values_list('id', flat=True))
u_id = set(self.filter(id__in=q_id).values_list('author', flat=True))
u_id = u_id.union(
diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py
index 5edde679..53eb22bc 100644
--- a/askbot/search/state_manager.py
+++ b/askbot/search/state_manager.py
@@ -1,6 +1,7 @@
#search state manager object
#that lives in the session and takes care of the state
#persistece during the search session
+import askbot
from askbot import const
from askbot.conf import settings as askbot_settings
import logging
@@ -139,20 +140,21 @@ class SearchState(object):
if 'query' in input_dict:
self.update_value('query', input_dict)
- self.sort = 'relevant'
+ self.sort = 'relevance-desc'
elif 'search' in unprocessed_input:#a case of use nulling search query by hand
self.reset_query()
return
if 'sort' in input_dict:
- if input_dict['sort'] == 'relevant' and self.query is None:
+ if input_dict['sort'] == 'relevance-desc' and self.query is None:
self.reset_sort()
else:
self.update_value('sort', input_dict)
#todo: plug - mysql has no relevance sort
- if self.sort == 'relevant':
- self.reset_sort()
+ if not askbot.should_show_sort_by_relevance():
+ if self.sort == 'relevance-desc':
+ self.reset_sort()
def reset_page(self):
self.page = 1
@@ -161,7 +163,7 @@ class SearchState(object):
if self.query:
self.query = None
self.reset_page()
- if self.sort == 'relevant':
+ if self.sort == 'relevance-desc':
self.reset_sort()
def reset_sort(self):
diff --git a/askbot/skins/default/media/js/live_search.js b/askbot/skins/default/media/js/live_search.js
index 78318c75..e2a9848f 100644
--- a/askbot/skins/default/media/js/live_search.js
+++ b/askbot/skins/default/media/js/live_search.js
@@ -1,3 +1,4 @@
+var prevSortMethod = sortMethod;
$(document).ready(function(){
var query = $('input#keywords');
var prev_text = $.trim(query.val());
@@ -14,7 +15,10 @@ $(document).ready(function(){
x_button.click(
function(){
query.val('');
- reset_query();
+ if (sortMethod == 'relevance-desc'){
+ sortMethod = prevSortMethod;
+ }
+ reset_query(sortMethod);
}
);
query.after(x_button);
@@ -30,11 +34,20 @@ $(document).ready(function(){
cur_text = $.trim(query.val());
if (cur_text != prev_text && running === false){
if (cur_text.length >= minSearchWordLength){
- send_query(cur_text);
+ if (prev_text.length === 0 && showSortByRelevance){
+ if (sortMethod == 'activity-desc'){
+ prevSortMethod = sortMethod;
+ sortMethod = 'relevance-desc';
+ }
+ }
+ send_query(cur_text, sortMethod);
running = true;
}
else if (cur_text.length === 0){
- reset_query();
+ if (sortMethod == 'relevance-desc'){
+ sortMethod = prevSortMethod;
+ }
+ reset_query(sortMethod);
running = true;
}
}
@@ -190,11 +203,10 @@ $(document).ready(function(){
var render_question_list = function(questions){
var output = '';
- //alert(questions.length);
for (var i=0; i<questions.length; i++){
output += render_question(questions[i]);
}
- return output;//render_question(questions[2]);
+ return output;
};
var render_faces = function(faces){
@@ -206,7 +218,6 @@ $(document).ready(function(){
for (var i=0; i<faces.length; i++){
html += faces[i];
}
- //alert(html);
$('#contrib-users').append(html);
};
@@ -234,8 +245,62 @@ $(document).ready(function(){
var set_question_count = function(count_html){
$('#question-count').html(count_html);
+ };
+
+ var create_relevance_tab = function(){
+ relevance_tab = $('<a></a>');
+ relevance_tab.attr('href', '?sort=relevance-desc');
+ relevance_tab.attr('id', 'by_relevance');
+ relevance_tab.html(sortButtonData['relevance']['desc_label']);
+ return relevance_tab;
}
+ var set_active_sort_tab = function(sort_method){
+ var tabs = $('#sort_tabs>a');
+ tabs.attr('class', 'off');
+ tabs.each(function(index, element){
+ var tab = $(element);
+ var tab_name = tab.attr('id').replace(/^by_/,'');
+ if (tab_name in sortButtonData){
+ tab.attr(
+ 'href',
+ '?sort=' + tab_name + '-desc'
+ );
+ tab.attr(
+ 'title',
+ sortButtonData[tab_name]['desc_tooltip']
+ );
+ tab.html(sortButtonData[tab_name]['desc_label']);
+ }
+ });
+ var bits = sort_method.split('-', 2);
+ var name = bits[0];
+ var sense = bits[1];//sense of sort
+ var antisense = (sense == 'asc' ? 'desc':'asc');
+ var active_tab = $('#by_' + name);
+ active_tab.attr('class', 'on');
+ active_tab.attr('title', sortButtonData[name][antisense + '_tooltip']);
+ active_tab.html(sortButtonData[name][sense + '_label']);
+ };
+
+ var render_relevance_sort_tab = function(){
+ if (showSortByRelevance === false){
+ return;
+ }
+ var relevance_tab = $('#by_relevance');
+ if (prev_text && prev_text.length > 0){
+ if (relevance_tab.length == 0){
+ relevance_tab = create_relevance_tab();
+ $('#sort_tabs>span').after(relevance_tab);
+ }
+ }
+ else {
+ if (relevance_tab.length > 0){
+ relevance_tab.remove();
+ }
+ }
+ };
+
var render_result = function(data, text_status, xhr){
var old_list = $('#' + q_list_sel);
var new_list = $('<div></div>');
@@ -250,6 +315,8 @@ $(document).ready(function(){
set_question_count(data['question_counter']);
render_faces(data['faces']);
render_related_tags(data['related_tags']);
+ render_relevance_sort_tab();
+ set_active_sort_tab(sortMethod);
query.focus();
}
//show new div
@@ -260,10 +327,11 @@ $(document).ready(function(){
eval_query();
}
- var send_query = function(query_text){
+ var send_query = function(query_text, sort_method){
+ var post_data = {query: query_text};
$.ajax({
url: scriptUrl + $.i18n._('questions/'),
- data: {query: query_text},
+ data: {query: query_text, sort: sort_method},
dataType: 'json',
success: render_result,
complete: try_again,
@@ -271,11 +339,11 @@ $(document).ready(function(){
prev_text = query_text;
}
- var reset_query = function(){
+ var reset_query = function(sort_method){
refresh_x_button();
$.ajax({
url: scriptUrl + $.i18n._('questions/'),
- data: {reset_query: true},
+ data: {reset_query: true, sort: sort_method},
dataType: 'json',
success: render_result,
complete: try_again,
diff --git a/askbot/skins/default/templates/authopenid/complete.html b/askbot/skins/default/templates/authopenid/complete.html
index ee5bf241..77142e74 100644
--- a/askbot/skins/default/templates/authopenid/complete.html
+++ b/askbot/skins/default/templates/authopenid/complete.html
@@ -43,11 +43,9 @@ parameters:
</div>
{% if openid_register_form.errors %}
<ul class="errorlist">
- {% if openid_register_form.non_field_errors %}
- {% for error in openid_register_form.non_field_errors %}
- <li>{{error}}</li>
- {% endfor %}
- {% endif %}
+ {% for error in openid_register_form.non_field_errors() %}
+ <li>{{error}}</li>
+ {% endfor %}
</ul>
{% endif %}
<div class="login">
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 7995012a..f983de78 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -295,3 +295,42 @@ poor design of the data or methods on data objects #}
{% endif %}
</div>
{%- endmacro -%}
+
+{%- macro reversible_sort_button(button_sort_criterium=None, asc_tooltip=None, asc_label=None,
+ desc_tooltip=None, desc_label=None, current_sort_method=None) -%}
+{#
+ sort button where descending sort is default
+ and the search method is togglable between ascending and descending
+ buttons are rendered as links with id constructed as
+ "by_" + button_sort_criterium
+ class "on" is added when current_sort_method is one of
+ button_sort_criterium + "asc" or "desc"
+#}
+ {% set key_name = button_sort_criterium %}
+ {% set sort = current_sort_method %}
+ {% if sort == key_name + "-asc" %}{# "worst" first #}
+ <a id="by_{{key_name}}"
+ href="?sort={{key_name}}-desc"
+ class="on"
+ title="{{desc_tooltip}}">{{asc_label}}</a>
+ {% elif sort == key_name + "-desc" %}{# "best first" #}
+ <a id="by_{{key_name}}"
+ href="?sort={{key_name}}-asc"
+ class="on"
+ title="{{asc_tooltip}}">{{desc_label}}</a>
+ {% else %}{# default, when other button is active #}
+ <a id="by_{{key_name}}"
+ href="?sort={{key_name}}-desc"
+ class="off"
+ title="{{desc_tooltip}}">{{desc_label}}</a>
+ {% endif %}
+ <script type="text/javascript">{# need to pass on text translations to js #}
+ var sortButtonData = sortButtonData || {};
+ sortButtonData["{{key_name}}"] = {
+ asc_tooltip: "{{asc_tooltip}}",
+ asc_label: "{{asc_label}}",
+ desc_tooltip: "{{desc_tooltip}}",
+ desc_label: "{{desc_label}}"
+ };
+ </script>
+{%- endmacro %}
diff --git a/askbot/skins/default/templates/questions.html b/askbot/skins/default/templates/questions.html
index 20b0b0ce..22c3faba 100644
--- a/askbot/skins/default/templates/questions.html
+++ b/askbot/skins/default/templates/questions.html
@@ -5,12 +5,10 @@
{% block forejs %}
<script type="text/javascript">
var tags = {{ tags_autocomplete|safe }};
+ var sortMethod = '{{sort}}';
+ var showSortByRelevance = {% if show_sort_by_relevance %}true{% else %}false{% endif %};
var minSearchWordLength = {{settings.MIN_SEARCH_WORD_LENGTH}};
$().ready(function(){
- var sort_tab_id = "{{ sort }}";
- $("#"+sort_tab_id).attr('className',"on");
- var scope_tab_id = "{{ scope }}";
- $("#"+scope_tab_id).attr('className',"on");
var on_tab = '#nav_questions';
$(on_tab).attr('className','on');
Hilite.exact = false;
@@ -26,81 +24,85 @@
<div class="tabBar">
<div class="tabsC">
<span class="label">{% trans %}In:{% endtrans %}</span>
- <a id="all" class="off" href="?scope=all" title="{% trans %}see all questions{% endtrans %}">{% trans %}all{% endtrans %}</a>
- <a id="unanswered" class="off" href="?scope=unanswered&amp;sort=coldest" title="{% trans %}see unanswered questions{% endtrans %}">{% trans %}unanswered{% endtrans %}</a>
+ <a id="all"
+ class="{% if scope == 'all' %}on{% else %}off{% endif %}"
+ href="?scope=all"
+ title="{% trans %}see all questions{% endtrans %}"
+ >{% trans %}all{% endtrans %}</a>
+ <a id="unanswered"
+ class="{% if scope == 'unanswered' %}on{% else %}off{% endif %}"
+ href="?scope=unanswered&amp;sort=answers-asc"
+ title="{% trans %}see unanswered questions{% endtrans %}"
+ >{% trans %}unanswered{% endtrans %}</a>
{% if request.user.is_authenticated() %}
- <a id="favorite" class="off" href="?scope=favorite" title="{% trans %}see your favorite questions{% endtrans %}">{% trans %}favorite{% endtrans %}</a>
+ <a id="favorite"
+ class="{% if scope == 'favorite' %}on{% else %}off{% endif %}"
+ href="?scope=favorite"
+ title="{% trans %}see your favorite questions{% endtrans %}"
+ >{% trans %}favorite{% endtrans %}</a>
{% endif %}
</div>
- <div class="tabsA">
+ <div id="sort_tabs" class="tabsA">
<span class="label">{% trans %}Sort by:{% endtrans %}</span>
- {% if sort == "oldest" %}
- <a id="oldest"
- href="?sort=latest"
- class="off"
- title="{% trans %}click to see the newest questions{% endtrans %}">{% trans %}oldest{% endtrans %}</a>
- {% elif sort == "latest" %}
- <a id="latest"
- href="?sort=oldest"
- class="off"
- title="{% trans %}click to see the oldest questions{% endtrans %}">{% trans %}newest{% endtrans %}</a>
- {% else %}
- <a id="latest"
- href="?sort=latest"
- class="off"
- title="{% trans %}click to see the newest questions{% endtrans %}">{% trans %}newest{% endtrans %}</a>
- {% endif %}
-
- {% if sort == "inactive" %}
- <a id="inactive"
- href="?sort=active"
- class="off"
- title="{% trans %}click to see the most recently updated questions{% endtrans %}">{% trans %}inactive{% endtrans %}</a>
- {% elif sort == "active" %}
- <a id="active"
- href="?sort=inactive"
- class="off"
- title="{% trans %}click to see the least recently updated questions{% endtrans %}">{% trans %}active{% endtrans %}</a>
- {% else %}
- <a id="active"
- href="?sort=active"
- class="off"
- title="{% trans %}click to see the most recently updated questions{% endtrans %}">{% trans %}active{% endtrans %}</a>
- {% endif %}
-
- {% if sort == "coldest" %}
- <a id="coldest"
- href="?sort=hottest"
- class="off"
- title="{% trans %}click to see hottest questions{% endtrans %}">{% trans %}less answers{% endtrans %}</a>
- {% elif sort == "hottest" %}
- <a id="hottest"
- href="?sort=coldest"
- class="off"
- title="{% trans %}click to see coldest questions{% endtrans %}">{% trans %}more answers{% endtrans %}</a>
- {% else %}
- <a id="hottest"
- href="?sort=hottest"
- class="off"
- title="{% trans %}click to see hottest questions{% endtrans %}">{% trans %}more answers{% endtrans %}</a>
- {% endif %}
-
- {% if sort == "leastvoted" %}
- <a id="leastvoted"
- href="?sort=mostvoted"
- class="off"
- title="{% trans %}click to see most voted questions{% endtrans %}">{% trans %}unpopular{% endtrans %}</a>
- {% elif sort == "mostvoted" %}
- <a id="mostvoted"
- href="?sort=leastvoted"
- class="off"
- title="{% trans %}click to see least voted questions{% endtrans %}">{% trans %}popular{% endtrans %}</a>
- {% else %}
- <a id="mostvoted"
- href="?sort=mostvoted"
- class="off"
- title="{% trans %}click to see most voted questions{% endtrans %}">{% trans %}popular{% endtrans %}</a>
+ {% if show_sort_by_relevance %}
+ {% set asc_relevance_tooltip = gettext('most relevant questions') %}
+ {% set desc_relevance_tooltip = gettext('click to see most relevant questions') %}
+ {% set relevance_label = gettext('relevance') %}
+ {% if query %}
+ <a id="by_relevance"
+ {% if sort == "relevance-desc" %}
+ href="?sort=relevance-desc"
+ class="on"
+ title="{{asc_relevance_tooltip}}">{{relevance_label}}
+ {% else %}
+ href="?sort=relevance-desc"
+ class="off"
+ title="{{desc_relevance_tooltip}}">{{relevance_label}}
+ {% endif %}
+ </a>
+ {% endif %}
+ <script type="text/javascript">
+ var sortButtonData = sortButtonData || {};
+ sortButtonData['relevance'] = {
+ asc_tooltip: "{{asc_relevance_tooltip}}",
+ desc_tooltip: "{{desc_relevance_tooltip}}",
+ asc_label: "{{relevance_label}}",
+ desc_label: "{{relevance_label}}"
+ };
+ </script>
{% endif %}
+ {{macros.reversible_sort_button(
+ button_sort_criterium = 'age',
+ asc_tooltip = gettext('click to see the oldest questions'),
+ asc_label = gettext('oldest'),
+ desc_tooltip = gettext('click to see the newest questions'),
+ desc_label = gettext('newest'),
+ current_sort_method = sort)
+ }}
+ {{macros.reversible_sort_button(
+ button_sort_criterium = 'activity',
+ asc_tooltip = gettext('click to see the least recently updated questions'),
+ asc_label = gettext('inactive'),
+ desc_tooltip = gettext('click to see the most recently updated questions'),
+ desc_label = gettext('active'),
+ current_sort_method = sort)
+ }}
+ {{macros.reversible_sort_button(
+ button_sort_criterium = 'answers',
+ asc_tooltip = gettext('click to see the least answered questions'),
+ asc_label = gettext('less answers'),
+ desc_tooltip = gettext('click to see the most answered questions'),
+ desc_label = gettext('more answers'),
+ current_sort_method = sort)
+ }}
+ {{macros.reversible_sort_button(
+ button_sort_criterium = 'votes',
+ asc_tooltip = gettext('click to see least voted questions'),
+ asc_label = gettext('unpopular'),
+ desc_tooltip = gettext('click to see most voted questions'),
+ desc_label = gettext('popular'),
+ current_sort_method = sort)
+ }}
</div>
</div>
{% endcache %}
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 6b553570..fb22240c 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -24,6 +24,7 @@ from django.views.decorators.cache import cache_page
from django.core import exceptions as django_exceptions
from django.contrib.humanize.templatetags import humanize
+import askbot
from askbot.utils.html import sanitize_html
#from lxml.html.diff import htmldiff
from askbot.utils.diff import textDiff as htmldiff
@@ -296,6 +297,7 @@ def questions(request):
'interesting_tag_names': meta_data.get('interesting_tag_names',None),
'ignored_tag_names': meta_data.get('ignored_tag_names',None),
'sort': search_state.sort,
+ 'show_sort_by_relevance': askbot.should_show_sort_by_relevance(),
'scope': search_state.scope,
'context' : paginator_context,
})