diff options
-rw-r--r-- | askbot/models/__init__.py | 41 | ||||
-rw-r--r-- | askbot/models/badges.py | 30 | ||||
-rw-r--r-- | askbot/models/post.py | 10 | ||||
-rw-r--r-- | askbot/models/question.py | 94 | ||||
-rw-r--r-- | askbot/models/repute.py | 16 | ||||
-rw-r--r-- | askbot/models/tag.py | 1 | ||||
-rw-r--r-- | askbot/search/haystack/__init__.py | 59 | ||||
-rw-r--r-- | askbot/setup_templates/settings.py | 8 | ||||
-rw-r--r-- | askbot/setup_templates/settings.py.mustache | 9 | ||||
-rw-r--r-- | askbot/startup_procedures.py | 20 | ||||
-rw-r--r-- | askbot/tests/__init__.py | 1 | ||||
-rw-r--r-- | askbot/tests/__init__.py.orig | 19 | ||||
-rw-r--r-- | askbot/tests/badge_tests.py | 62 | ||||
-rw-r--r-- | askbot/tests/db_api_tests.py | 10 | ||||
-rw-r--r-- | askbot/tests/haystack_search_tests.py | 95 | ||||
-rw-r--r-- | askbot/tests/post_model_tests.py | 6 | ||||
-rw-r--r-- | askbot/views/commands.py | 7 | ||||
-rw-r--r-- | askbot/views/users.py | 6 | ||||
-rw-r--r-- | askbot/views/writers.py | 8 |
19 files changed, 355 insertions, 147 deletions
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index c16087f7..4cddacb9 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -93,23 +93,28 @@ def get_users_by_text_query(search_query, users_query_set = None): """Runs text search in user names and profile. For postgres, search also runs against user group names. """ - import askbot - if users_query_set is None: - users_query_set = User.objects.all() - if 'postgresql_psycopg2' in askbot.get_database_engine_name(): - from askbot.search import postgresql - return postgresql.run_full_text_search(users_query_set, search_query) + if django_settings.ENABLE_HAYSTACK_SEARCH: + from askbot.search.haystack import AskbotSearchQuerySet + qs = AskbotSearchQuerySet().filter(content=search_query).models(User).get_django_queryset(User) + return qs else: - return users_query_set.filter( - models.Q(username__icontains=search_query) | - models.Q(about__icontains=search_query) - ) - #if askbot.get_database_engine_name().endswith('mysql') \ - # and mysql.supports_full_text_search(): - # return User.objects.filter( - # models.Q(username__search = search_query) | - # models.Q(about__search = search_query) - # ) + import askbot + if users_query_set is None: + users_query_set = User.objects.all() + if 'postgresql_psycopg2' in askbot.get_database_engine_name(): + from askbot.search import postgresql + return postgresql.run_full_text_search(users_query_set, search_query) + else: + return users_query_set.filter( + models.Q(username__icontains=search_query) | + models.Q(about__icontains=search_query) + ) + #if askbot.get_database_engine_name().endswith('mysql') \ + # and mysql.supports_full_text_search(): + # return User.objects.filter( + # models.Q(username__search = search_query) | + # models.Q(about__search = search_query) + # ) User.add_to_class( 'status', @@ -846,7 +851,7 @@ def user_assert_can_delete_question(self, question = None): #if there are answers by other people, #then deny, unless user in admin or moderator answer_count = question.thread.all_answers()\ - .exclude(author=self).exclude(score__lte=0).count() + .exclude(author=self).exclude(points__lte=0).count() if answer_count > 0: if self.is_administrator() or self.is_moderator(): @@ -2443,7 +2448,7 @@ def _process_vote(user, post, timestamp=None, cancel=False, vote_type=None): if post.post_type == 'question': #denormalize the question post score on the thread - post.thread.score = post.score + post.thread.points = post.points post.thread.save() post.thread.update_summary_html() diff --git a/askbot/models/badges.py b/askbot/models/badges.py index 61149df3..244c8e2f 100644 --- a/askbot/models/badges.py +++ b/askbot/models/badges.py @@ -43,13 +43,13 @@ class Badge(object): """ def __init__(self, key = '', - name = '', + name = '', level = None, description = None, multiple = False): #key - must be an ASCII only word - self.key = key + self.key = key self.name = name self.level = level self.description = description @@ -114,11 +114,11 @@ class Badge(object): def consider_award(self, actor = None, context_object = None, timestamp = None): - """Normally this method should be reimplemented + """Normally this method should be reimplemented in subclass, but some badges are awarded without checks. Those do no need to override this method - actor - user who committed some action, context_object - + actor - user who committed some action, context_object - the object related to the award situation, e.g. answer """ return self.award(actor, context_object, timestamp) @@ -141,7 +141,7 @@ class Disciplined(Badge): if context_object.author != actor: return False - if context_object.score >= \ + if context_object.points>= \ askbot_settings.DISCIPLINED_BADGE_MIN_UPVOTES: return self.award(actor, context_object, timestamp) @@ -163,7 +163,7 @@ class PeerPressure(Badge): if context_object.author != actor: return False - if context_object.score <= \ + if context_object.points<= \ -1 * askbot_settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES: return self.award(actor, context_object, timestamp) return False @@ -181,12 +181,12 @@ class Teacher(Badge): multiple = False ) - def consider_award(self, actor = None, + def consider_award(self, actor = None, context_object = None, timestamp = None): if context_object.post_type != 'answer': return False - if context_object.score >= askbot_settings.TEACHER_BADGE_MIN_UPVOTES: + if context_object.points>= askbot_settings.TEACHER_BADGE_MIN_UPVOTES: return self.award(context_object.author, context_object, timestamp) return False @@ -268,7 +268,7 @@ class SelfLearner(Badge): question = context_object.thread._question_post() answer = context_object - if question.author == answer.author and answer.score >= min_upvotes: + if question.author == answer.author and answer.points >= min_upvotes: self.award(context_object.author, context_object, timestamp) class QualityPost(Badge): @@ -294,7 +294,7 @@ class QualityPost(Badge): context_object = None, timestamp = None): if context_object.post_type not in ('answer', 'question'): return False - if context_object.score >= self.min_votes: + if context_object.points >= self.min_votes: return self.award(context_object.author, context_object, timestamp) return False @@ -485,7 +485,7 @@ class VotedAcceptedAnswer(Badge): if context_object.post_type != 'answer': return None answer = context_object - if answer.score >= self.min_votes and answer.accepted(): + if answer.points >= self.min_votes and answer.accepted(): return self.award(answer.author, answer, timestamp) class Enlightened(VotedAcceptedAnswer): @@ -537,7 +537,7 @@ class Necromancer(Badge): delta = datetime.timedelta(askbot_settings.NECROMANCER_BADGE_MIN_DELAY) min_score = askbot_settings.NECROMANCER_BADGE_MIN_UPVOTES if answer.added_at - question.added_at >= delta \ - and answer.score >= min_score: + and answer.points >= min_score: return self.award(answer.author, answer, timestamp) return False @@ -723,7 +723,7 @@ class Enthusiast(Badge): return False class Commentator(Badge): - """Commentator is a bronze badge that is + """Commentator is a bronze badge that is awarded once when user posts a certain number of comments""" def __init__(self): @@ -778,7 +778,7 @@ class Expert(Badge): ) ORIGINAL_DATA = """ - + extra badges from stackexchange * commentator - left n comments (single) * enthusiast, fanatic - visited site n days in a row (s) @@ -894,7 +894,7 @@ award_badges_signal = Signal( #context_object - database object related to the event, e.g. question @auto_now_timestamp -def award_badges(event = None, actor = None, +def award_badges(event = None, actor = None, context_object = None, timestamp = None, **kwargs): """function that is called when signal `award_badges_signal` is sent """ diff --git a/askbot/models/post.py b/askbot/models/post.py index 8b8ff76a..c7b162b8 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -358,7 +358,7 @@ class Post(models.Model): locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_posts') locked_at = models.DateTimeField(null=True, blank=True) - score = models.IntegerField(default=0) + points = models.IntegerField(default=0, db_column='score') vote_up_count = models.IntegerField(default=0) vote_down_count = models.IntegerField(default=0) @@ -389,6 +389,14 @@ class Post(models.Model): app_label = 'askbot' db_table = 'askbot_post' + #property to support legacy themes in case there are. + @property + def score(self): + return int(self.points) + @score.setter + def score(self, number): + if number: + self.points = int(number) def parse_post_text(self): """typically post has a field to store raw source text diff --git a/askbot/models/question.py b/askbot/models/question.py index 6b233188..5b726257 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -176,28 +176,33 @@ class ThreadManager(BaseQuerySetManager): """returns a query set of questions, matching the full text query """ - if not qs: - qs = self.all() -# if getattr(settings, 'USE_SPHINX_SEARCH', False): -# matching_questions = Question.sphinx_search.query(search_query) -# question_ids = [q.id for q in matching_questions] -# return qs.filter(posts__post_type='question', posts__deleted=False, posts__self_question_id__in=question_ids) - if askbot.get_database_engine_name().endswith('mysql') \ - and mysql.supports_full_text_search(): - return qs.filter( - models.Q(title__search = search_query) | - models.Q(tagnames__search = search_query) | - models.Q(posts__deleted=False, posts__text__search = search_query) - ) - elif 'postgresql_psycopg2' in askbot.get_database_engine_name(): - from askbot.search import postgresql - return postgresql.run_full_text_search(qs, search_query) + if settings.ENABLE_HAYSTACK_SEARCH: + from askbot.search.haystack import AskbotSearchQuerySet + hs_qs = AskbotSearchQuerySet().filter(content=search_query) + return hs_qs.get_django_queryset() else: - return qs.filter( - models.Q(title__icontains=search_query) | - models.Q(tagnames__icontains=search_query) | - models.Q(posts__deleted=False, posts__text__icontains = search_query) - ) + if not qs: + qs = self.all() + # if getattr(settings, 'USE_SPHINX_SEARCH', False): + # matching_questions = Question.sphinx_search.query(search_query) + # question_ids = [q.id for q in matching_questions] + # return qs.filter(posts__post_type='question', posts__deleted=False, posts__self_question_id__in=question_ids) + if askbot.get_database_engine_name().endswith('mysql') \ + and mysql.supports_full_text_search(): + return qs.filter( + models.Q(title__search = search_query) | + models.Q(tagnames__search = search_query) | + models.Q(posts__deleted=False, posts__text__search = search_query) + ) + elif 'postgresql_psycopg2' in askbot.get_database_engine_name(): + from askbot.search import postgresql + return postgresql.run_full_text_search(qs, search_query) + else: + return qs.filter( + models.Q(title__icontains=search_query) | + models.Q(tagnames__icontains=search_query) | + models.Q(posts__deleted=False, posts__text__icontains = search_query) + ) def run_advanced_search(self, request_user, search_state): # TODO: !! review, fix, and write tests for this @@ -211,8 +216,8 @@ class ThreadManager(BaseQuerySetManager): # TODO: add a possibility to see deleted questions qs = self.filter( - posts__post_type='question', - posts__deleted=False + posts__post_type='question', + posts__deleted=False, ) # (***) brings `askbot_post` into the SQL query, see the ordering section below if askbot_settings.ENABLE_CONTENT_MODERATION: @@ -249,7 +254,7 @@ class ThreadManager(BaseQuerySetManager): ) # TODO: unify with search_state.author ? #unified tags - is list of tags taken from the tag selection - #plus any tags added to the query string with #tag or [tag:something] + #plus any tags added to the query string with #tag or [tag:something] #syntax. #run tag search in addition to these unified tags meta_data = {} @@ -271,7 +276,7 @@ class ThreadManager(BaseQuerySetManager): existing_tags.add(tag_record.name) except Tag.DoesNotExist: non_existing_tags.add(tag) - + meta_data['non_existing_tags'] = list(non_existing_tags) tags = existing_tags else: @@ -370,13 +375,18 @@ class ThreadManager(BaseQuerySetManager): 'activity-asc': 'last_activity_at', 'answers-desc': '-answer_count', 'answers-asc': 'answer_count', - 'votes-desc': '-score', - 'votes-asc': 'score', + 'votes-desc': '-points', + 'votes-asc': 'points', 'relevance-desc': '-relevance', # special Postgresql-specific ordering, 'relevance' quaso-column is added by get_for_query() } + orderby = QUESTION_ORDER_BY_MAP[search_state.sort] - qs = qs.extra(order_by=[orderby]) + + if not (settings.ENABLE_HAYSTACK_SEARCH and orderby=='-relevance'): + #FIXME: this does not produces the very same results as postgres. + qs = qs.extra(order_by=[orderby]) + # HACK: We add 'ordering_key' column as an alias and order by it, because when distict() is used, # qs.extra(order_by=[orderby,]) is lost if only `orderby` column is from askbot_post! @@ -403,7 +413,7 @@ class ThreadManager(BaseQuerySetManager): page_questions = Post.objects.filter( post_type='question', thread__id__in = thread_ids ).only(# pick only the used fields - 'id', 'thread', 'score', 'is_anonymous', + 'id', 'thread', 'points', 'is_anonymous', 'summary', 'post_type', 'deleted' ) page_question_map = {} @@ -514,13 +524,23 @@ class Thread(models.Model): answer_accepted_at = models.DateTimeField(null=True, blank=True) added_at = models.DateTimeField(default = datetime.datetime.now) - score = models.IntegerField(default = 0) + #db_column will be removed later + points = models.IntegerField(default = 0, db_column='score') objects = ThreadManager() - + class Meta: app_label = 'askbot' + #property to support legacy themes in case there are. + @property + def score(self): + return int(self.points) + @score.setter + def score(self, number): + if number: + self.points = int(number) + def _question_post(self, refresh=False): if refresh and hasattr(self, '_question_cache'): delattr(self, '_question_cache') @@ -751,7 +771,7 @@ class Thread(models.Model): return 'thread-data-%s-%s' % (self.id, sort_method) def invalidate_cached_post_data(self): - """needs to be called when anything notable + """needs to be called when anything notable changes in the post data - on votes, adding, deleting, editing content""" #we can call delete_many() here if using Django > 1.2 @@ -798,7 +818,7 @@ class Thread(models.Model): { 'latest':'-added_at', 'oldest':'added_at', - 'votes':'-score' + 'votes':'-points' }[sort_method] ) #1) collect question, answer and comment posts and list of post id's @@ -852,7 +872,7 @@ class Thread(models.Model): #todo: there may be > 1 enquirers published_answer_ids = list() if self.is_moderated() and user != question_post.author: - #if moderated - then author is guaranteed to be the + #if moderated - then author is guaranteed to be the #limited visibility enquirer published_answers = self.posts.get_answers( user=question_post.author#todo: may be > 1 @@ -937,8 +957,8 @@ class Thread(models.Model): url = question_post.get_absolute_url() title = thread.get_title(question_post) result.append({'url': url, 'title': title}) - - return result + + return result def get_cached_data(): """similar thread data will expire @@ -983,7 +1003,7 @@ class Thread(models.Model): return False def add_child_posts_to_groups(self, groups): - """adds questions and answers of the thread to + """adds questions and answers of the thread to given groups, comments are taken care of implicitly by the underlying ``Post`` methods """ diff --git a/askbot/models/repute.py b/askbot/models/repute.py index 33ec3a42..a6e9d7d1 100644 --- a/askbot/models/repute.py +++ b/askbot/models/repute.py @@ -75,14 +75,14 @@ class Vote(models.Model): """ #importing locally because of circular dependency from askbot import auth - score_before = self.voted_post.score + score_before = self.voted_post.points if self.vote > 0: # cancel upvote auth.onUpVotedCanceled(self, self.voted_post, self.user) else: # cancel downvote auth.onDownVotedCanceled(self, self.voted_post, self.user) - score_after = self.voted_post.score + score_after = self.voted_post.points return score_after - score_before @@ -94,7 +94,7 @@ class BadgeData(models.Model): awarded_to = models.ManyToManyField(User, through='Award', related_name='badges') def _get_meta_data(self): - """retrieves badge metadata stored + """retrieves badge metadata stored in a file""" from askbot.models import badges return badges.get_badge(self.slug) @@ -171,9 +171,9 @@ class ReputeManager(models.Manager): tomorrow = today + datetime.timedelta(1) rep_types = (1,-8) sums = self.filter(models.Q(reputation_type__in=rep_types), - user=user, + user=user, reputed_at__range=(today, tomorrow), - ).aggregate(models.Sum('positive'), models.Sum('negative')) + ).aggregate(models.Sum('positive'), models.Sum('negative')) if sums: pos = sums['positive__sum'] neg = sums['negative__sum'] @@ -200,7 +200,7 @@ class Repute(models.Model): #assigned_by_moderator - so that reason can be displayed #in that case Question field will be blank comment = models.CharField(max_length=128, null=True) - + objects = ReputeManager() def __unicode__(self): @@ -214,7 +214,7 @@ class Repute(models.Model): """returns HTML snippet with a link to related question or a text description for a the reason of the reputation change - in the implementation description is returned only + in the implementation description is returned only for Repute.reputation_type == 10 - "assigned by the moderator" part of the purpose of this method is to hide this idiosyncracy @@ -242,7 +242,7 @@ class Repute(models.Model): return '<a href="%(url)s" title="%(link_title)s">%(question_title)s</a>' \ % { - 'url': self.question.get_absolute_url(), + 'url': self.question.get_absolute_url(), 'question_title': escape(self.question.thread.title), 'link_title': escape(link_title) } diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 38555e49..647ea5cf 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -3,6 +3,7 @@ import logging from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from django.conf import settings from askbot.models.base import BaseQuerySetManager from askbot import const from askbot.conf import settings as askbot_settings diff --git a/askbot/search/haystack/__init__.py b/askbot/search/haystack/__init__.py new file mode 100644 index 00000000..71f04d00 --- /dev/null +++ b/askbot/search/haystack/__init__.py @@ -0,0 +1,59 @@ +try: + from haystack import indexes, site + from haystack.query import SearchQuerySet + from askbot.models import Post, Thread, User + + + class ThreadIndex(indexes.SearchIndex): + text = indexes.CharField(document=True, use_template=True) + title = indexes.CharField(model_attr='title') + post_text = indexes.CharField(model_attr='posts__text__search') + + def index_queryset(self): + return Thread.objects.filter(posts__deleted=False) + + def prepare(self, obj): + self.prepared_data = super(ThreadIndex, self).prepare(object) + + self.prepared_data['tags'] = [tag.name for tag in objects.tags.all()] + + class PostIndex(indexes.SearchIndex): + text = indexes.CharField(document=True, use_template=True) + post_text = indexes.CharField(model_attr='text') + author = indexes.CharField(model_attr='user') + thread_id = indexes.CharField(model_attr='thread') + + def index_queryset(self): + return Post.objects.filter(deleted=False) + + class UserIndex(indexes.SearchIndex): + text = indexes.CharField(document=True, use_template=True) + + def index_queryset(self): + return User.objects.all() + + site.register(Post, PostIndex) + site.register(Thread, ThreadIndex) + site.register(User, UserIndex) + + class AskbotSearchQuerySet(SearchQuerySet): + + def get_django_queryset(self, model_klass=Thread): + '''dirty hack because models() method from the + SearchQuerySet does not work </3''' + id_list = [] + for r in self: + if r.model_name in ['thread','post'] \ + and model_klass._meta.object_name.lower() == 'thread': + if getattr(r, 'thread_id'): + id_list.append(r.thread_id) + else: + id_list.append(r.pk) + elif r.model_name == model_klass._meta.object_name.lower(): + #FIXME: add a highlight here? + id_list.append(r.pk) + + return model_klass.objects.filter(id__in=set(id_list)) + +except: + pass diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index 3d24fc5e..94df6f29 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -162,6 +162,7 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'django.contrib.sitemaps', #'debug_toolbar', + #'haystack', 'askbot', 'askbot.deps.django_authopenid', #'askbot.importers.stackexchange', #se loader @@ -235,6 +236,13 @@ STATICFILES_DIRS = ( RECAPTCHA_USE_SSL = True +#HAYSTACK_SETTINGS +ENABLE_HAYSTACK_SEARCH = False +HAYSTACK_SITECONF = 'askbot.search.haystack' +#more information +#http://django-haystack.readthedocs.org/en/v1.2.7/settings.html +HAYSTACK_SEARCH_ENGINE = 'simple' + TINYMCE_COMPRESSOR = True TINYMCE_SPELLCHECKER = False TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'common/media/js/tinymce/') diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache index a800edec..74295513 100644 --- a/askbot/setup_templates/settings.py.mustache +++ b/askbot/setup_templates/settings.py.mustache @@ -161,6 +161,8 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'django.contrib.sitemaps', #'debug_toolbar', + #Optional, to enable haystack search + #'haystack', 'askbot', 'askbot.deps.django_authopenid', #'askbot.importers.stackexchange', #se loader @@ -236,6 +238,13 @@ STATICFILES_DIRS = ( RECAPTCHA_USE_SSL = True +#HAYSTACK_SETTINGS +ENABLE_HAYSTACK_SEARCH = False +HAYSTACK_SITECONF = 'askbot.search.haystack' +#more information +#http://django-haystack.readthedocs.org/en/v1.2.7/settings.html +HAYSTACK_SEARCH_ENGINE = 'simple' + TINYMCE_COMPRESSOR = True TINYMCE_SPELLCHECKER = False TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'common/media/js/tinymce/') diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py index 087fc957..e90bd048 100644 --- a/askbot/startup_procedures.py +++ b/askbot/startup_procedures.py @@ -531,13 +531,23 @@ def test_avatar(): short_message = True ) +def test_haystack(): + if 'haystack' in django_settings.INSTALLED_APPS: + try_import('haystack', 'django-haystack', short_message = True) + if not hasattr(django_settings, 'HAYSTACK_SEARCH_ENGINE'): + message = 'Please add HAYSTACK_SEARCH_ENGINE = simple, for more info please checkout: http://django-haystack.readthedocs.org/en/v1.2.7/settings.html#haystack-search-engine' + raise AskbotConfigError(message) + if not hasattr(django_settings, 'HAYSTACK_SITECONF'): + message = 'Please add HAYSTACK_SITECONF = "askbot.search.haystack"' + raise AskbotConfigError(message) + def test_custom_user_profile_tab(): setting_name = 'ASKBOT_CUSTOM_USER_PROFILE_TAB' tab_settings = getattr(django_settings, setting_name, None) if tab_settings: if not isinstance(tab_settings, dict): print "Setting %s must be a dictionary!!!" % setting_name - + name = tab_settings.get('NAME', None) slug = tab_settings.get('SLUG', None) func_name = tab_settings.get('CONTENT_GENERATOR', None) @@ -691,6 +701,7 @@ def run_startup_tests(): test_new_skins() test_longerusername() test_avatar() + test_haystack() settings_tester = SettingsTester({ 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY': { 'value': True, @@ -720,6 +731,13 @@ def run_startup_tests(): 'RECAPTCHA_USE_SSL': { 'value': True, 'message': 'Please add: RECAPTCHA_USE_SSL = True' + }, + 'ENABLE_HAYSTACK_SEARCH': { + 'message': 'Please add: ENABLE_HAYSTACK_SEARCH = False or True according to your setup' + }, + 'HAYSTACK_SITECONF': { + 'value': 'askbot.search.haystack', + 'message': 'Please add: HAYSTACK_SITECONF = "askbot.search.haystack"' } }) settings_tester.run() diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py index fcef288b..96ba2045 100644 --- a/askbot/tests/__init__.py +++ b/askbot/tests/__init__.py @@ -14,6 +14,7 @@ from askbot.tests.markup_test import * from askbot.tests.post_model_tests import * from askbot.tests.thread_model_tests import * from askbot.tests.reply_by_email_tests import * +from askbot.tests.haystack_search_tests import * from askbot.tests.email_parsing_tests import * from askbot.tests.widget_tests import * from askbot.tests.category_tree_tests import CategoryTreeTests diff --git a/askbot/tests/__init__.py.orig b/askbot/tests/__init__.py.orig deleted file mode 100644 index 905c90df..00000000 --- a/askbot/tests/__init__.py.orig +++ /dev/null @@ -1,19 +0,0 @@ -from askbot.tests.cache_tests import * -from askbot.tests.email_alert_tests import * -from askbot.tests.on_screen_notification_tests import * -from askbot.tests.page_load_tests import * -from askbot.tests.permission_assertion_tests import * -from askbot.tests.db_api_tests import * -from askbot.tests.skin_tests import * -from askbot.tests.badge_tests import * -from askbot.tests.management_command_tests import * -from askbot.tests.search_state_tests import * -from askbot.tests.form_tests import * -from askbot.tests.follow_tests import * -from askbot.tests.templatefilter_tests import * -from askbot.tests.markup_test import * -from askbot.tests.post_model_tests import * -from askbot.tests.thread_model_tests import * -from askbot.tests.reply_by_email_tests import * -from askbot.tests.category_tree_tests import CategoryTreeTests -from askbot.tests.user_model_tests import UserModelTests diff --git a/askbot/tests/badge_tests.py b/askbot/tests/badge_tests.py index 0ed4b343..8e4458f6 100644 --- a/askbot/tests/badge_tests.py +++ b/askbot/tests/badge_tests.py @@ -24,7 +24,7 @@ class BadgeTests(AskbotTestCase): def assert_accepted_answer_badge_works(self, badge_key = None, - min_score = None, + min_points = None, expected_count = 1, previous_count = 0, trigger = None @@ -32,7 +32,7 @@ class BadgeTests(AskbotTestCase): assert(trigger in ('accept_best_answer', 'upvote_answer')) question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u2, question = question) - answer.score = min_score - 1 + answer.points = min_points - 1 answer.save() recipient = answer.author @@ -47,26 +47,26 @@ class BadgeTests(AskbotTestCase): self.u1.upvote(answer) self.assert_have_badge(badge_key, recipient, expected_count) - def assert_upvoted_answer_badge_works(self, + def assert_upvoted_answer_badge_works(self, badge_key = None, - min_score = None, + min_points = None, multiple = False ): """test answer badge where answer author is the recipient where badge award is triggered by upvotes - * min_score - minimum # of upvotes required + * min_points - minimum # of upvotes required * multiple - multiple award or not * badge_key - key on askbot.models.badges.Badge object """ question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u2, question = question) - answer.score = min_score - 1 + answer.points = min_points - 1 answer.save() self.u1.upvote(answer) self.assert_have_badge(badge_key, recipient = self.u2) self.u3.upvote(answer) self.assert_have_badge(badge_key, recipient = self.u2, expected_count = 1) - + #post another question and check that there are no new badges question2 = self.post_question(user = self.u1) answer2 = self.post_answer(user = self.u2, question = question2) @@ -85,28 +85,28 @@ class BadgeTests(AskbotTestCase): expected_count = expected_count ) - def assert_upvoted_question_badge_works(self, + def assert_upvoted_question_badge_works(self, badge_key = None, - min_score = None, + min_points = None, multiple = False ): """test question badge where question author is the recipient where badge award is triggered by upvotes - * min_score - minimum # of upvotes required + * min_points - minimum # of upvotes required * multiple - multiple award or not * badge_key - key on askbot.models.badges.Badge object """ question = self.post_question(user = self.u1) - question.score = min_score - 1 + question.points = min_points - 1 question.save() self.u2.upvote(question) self.assert_have_badge(badge_key, recipient = self.u1) self.u3.upvote(question) self.assert_have_badge(badge_key, recipient = self.u1, expected_count = 1) - + #post another question and check that there are no new badges question2 = self.post_question(user = self.u1) - question2.score = min_score - 1 + question2.points = min_points - 1 question2.save() self.u2.upvote(question2) @@ -123,13 +123,13 @@ class BadgeTests(AskbotTestCase): def test_disciplined_badge(self): question = self.post_question(user = self.u1) - question.score = settings.DISCIPLINED_BADGE_MIN_UPVOTES + question.points = settings.DISCIPLINED_BADGE_MIN_UPVOTES question.save() self.u1.delete_question(question) self.assert_have_badge('disciplined', recipient = self.u1) question2 = self.post_question(user = self.u1) - question2.score = settings.DISCIPLINED_BADGE_MIN_UPVOTES + question2.points = settings.DISCIPLINED_BADGE_MIN_UPVOTES question2.save() self.u1.delete_question(question2) self.assert_have_badge('disciplined', recipient = self.u1, expected_count = 2) @@ -137,7 +137,7 @@ class BadgeTests(AskbotTestCase): def test_peer_pressure_badge(self): question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u1, question = question) - answer.score = -1*settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES + answer.points = -1*settings.PEER_PRESSURE_BADGE_MIN_DOWNVOTES answer.save() self.u1.delete_answer(answer) self.assert_have_badge('peer-pressure', recipient = self.u1) @@ -145,21 +145,21 @@ class BadgeTests(AskbotTestCase): def test_teacher_badge(self): self.assert_upvoted_answer_badge_works( badge_key = 'teacher', - min_score = settings.TEACHER_BADGE_MIN_UPVOTES, + min_points = settings.TEACHER_BADGE_MIN_UPVOTES, multiple = False ) def test_nice_answer_badge(self): self.assert_upvoted_answer_badge_works( badge_key = 'nice-answer', - min_score = settings.NICE_ANSWER_BADGE_MIN_UPVOTES, + min_points = settings.NICE_ANSWER_BADGE_MIN_UPVOTES, multiple = True ) def test_nice_question_badge(self): self.assert_upvoted_question_badge_works( badge_key = 'nice-question', - min_score = settings.NICE_QUESTION_BADGE_MIN_UPVOTES, + min_points = settings.NICE_QUESTION_BADGE_MIN_UPVOTES, multiple = True ) @@ -227,7 +227,7 @@ class BadgeTests(AskbotTestCase): question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u1, question = question) min_votes = settings.SELF_LEARNER_BADGE_MIN_UPVOTES - answer.score = min_votes - 1 + answer.points = min_votes - 1 answer.save() self.u2.upvote(answer) self.assert_have_badge('self-learner', recipient = self.u1) @@ -235,14 +235,14 @@ class BadgeTests(AskbotTestCase): #copy-paste of the first question, except expect second badge question = self.post_question(user = self.u1) answer = self.post_answer(user = self.u1, question = question) - answer.score = min_votes - 1 + answer.points = min_votes - 1 answer.save() self.u2.upvote(answer) self.assert_have_badge('self-learner', recipient = self.u1, expected_count = 2) question = self.post_question(user = self.u2) answer = self.post_answer(user = self.u1, question = question) - answer.score = min_votes - 1 + answer.points = min_votes - 1 answer.save() self.u2.upvote(answer) #no badge when asker != answerer @@ -282,13 +282,13 @@ class BadgeTests(AskbotTestCase): def assert_enlightened_badge_works(self, trigger): self.assert_accepted_answer_badge_works( 'enlightened', - min_score = settings.ENLIGHTENED_BADGE_MIN_UPVOTES, + min_points = settings.ENLIGHTENED_BADGE_MIN_UPVOTES, expected_count = 1, trigger = trigger ) self.assert_accepted_answer_badge_works( 'enlightened', - min_score = settings.ENLIGHTENED_BADGE_MIN_UPVOTES, + min_points = settings.ENLIGHTENED_BADGE_MIN_UPVOTES, expected_count = 1, previous_count = 1, trigger = trigger @@ -297,13 +297,13 @@ class BadgeTests(AskbotTestCase): def assert_guru_badge_works(self, trigger): self.assert_accepted_answer_badge_works( 'guru', - min_score = settings.GURU_BADGE_MIN_UPVOTES, + min_points = settings.GURU_BADGE_MIN_UPVOTES, expected_count = 1, trigger = trigger ) self.assert_accepted_answer_badge_works( 'guru', - min_score = settings.GURU_BADGE_MIN_UPVOTES, + min_points = settings.GURU_BADGE_MIN_UPVOTES, previous_count = 1, expected_count = 2, trigger = trigger @@ -330,8 +330,8 @@ class BadgeTests(AskbotTestCase): user = self.u2, question = question, timestamp = future - ) - answer.score = settings.NECROMANCER_BADGE_MIN_UPVOTES - 1 + ) + answer.points = settings.NECROMANCER_BADGE_MIN_UPVOTES - 1 answer.save() self.assert_have_badge('necromancer', self.u2, expected_count = 0) self.u1.upvote(answer) @@ -457,7 +457,7 @@ class BadgeTests(AskbotTestCase): self.u1.toggle_favorite_question(question) """no gaming""" self.assert_have_badge('stellar-question', self.u1, 0) - + def test_stellar_badge3(self): question = self.post_question(user = self.u1) settings.update('STELLAR_QUESTION_BADGE_MIN_STARS', 2) @@ -480,9 +480,9 @@ class BadgeTests(AskbotTestCase): self.post_comment(user = self.u1, parent_post = question) self.assert_have_badge('commentator', self.u1, 0) - self.post_comment(user = self.u1, parent_post = question) + self.post_comment(user = self.u1, parent_post = question) self.assert_have_badge('commentator', self.u1, 1) - self.post_comment(user = self.u1, parent_post = question) + self.post_comment(user = self.u1, parent_post = question) self.assert_have_badge('commentator', self.u1, 1) def test_taxonomist_badge(self): diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py index 65b0a950..8fadce17 100644 --- a/askbot/tests/db_api_tests.py +++ b/askbot/tests/db_api_tests.py @@ -165,13 +165,13 @@ class DBApiTests(AskbotTestCase): count = models.Tag.objects.filter(name='one-tag').count() self.assertEquals(count, 0) - + @with_settings(MAX_TAG_LENGTH=200, MAX_TAGS_PER_POST=50) def test_retag_tags_too_long_raises(self): tags = "aoaoesuouooeueooeuoaeuoeou aostoeuoaethoeastn oasoeoa nuhoasut oaeeots aoshootuheotuoehao asaoetoeatuoasu o aoeuethut aoaoe uou uoetu uouuou ao aouosutoeh" question = self.post_question(user=self.user) self.assertRaises( - exceptions.ValidationError, + exceptions.ValidationError, self.user.retag_question, question=question, tags=tags ) @@ -407,10 +407,10 @@ class CommentTests(AskbotTestCase): def test_other_user_can_cancel_upvote(self): self.test_other_user_can_upvote_comment() comment = models.Post.objects.get_comments().get(id = self.comment.id) - self.assertEquals(comment.score, 1) + self.assertEquals(comment.points, 1) self.other_user.upvote(comment, cancel = True) comment = models.Post.objects.get_comments().get(id = self.comment.id) - self.assertEquals(comment.score, 0) + self.assertEquals(comment.points, 0) class GroupTests(AskbotTestCase): def setUp(self): @@ -507,7 +507,7 @@ class GroupTests(AskbotTestCase): #because answer groups always inherit thread groups self.edit_answer(user=self.u1, answer=answer, is_private=True) self.assertEqual(answer.groups.count(), 1) - + #here we have a simple case - the comment to answer was posted #by the answer author!!! #won't work when comment was by someone else diff --git a/askbot/tests/haystack_search_tests.py b/askbot/tests/haystack_search_tests.py new file mode 100644 index 00000000..d5935ff6 --- /dev/null +++ b/askbot/tests/haystack_search_tests.py @@ -0,0 +1,95 @@ +"""Tests haystack indexes and queries""" +from django.core import exceptions +from django.conf import settings +from django.contrib.auth.models import User +from askbot.tests.utils import AskbotTestCase, skipIf +from askbot import models +import datetime + +class HaystackSearchTests(AskbotTestCase): + """tests methods on User object, + that were added for askbot + """ + def setUp(self): + self.user = self.create_user(username='gepeto') + self.other_user = self.create_user(username = 'pinocho') + self.other_user.location = 'Managua' + self.other_user.about = "I'm made of wood, gepeto made me" + self.other_user.save() + body_1 = '''Lorem turpis purus? Amet mattis eu et sociis phasellus + montes elementum proin ut urna enim, velit, tincidunt quis ut, + et integer mus? Nunc! Vut sed? Ac tincidunt egestas adipiscing, + magna et pulvinar mid est urna ultricies, turpis tristique nisi, + cum. Urna. Purus elit porttitor nisi porttitor ridiculus tincidunt + amet duis, gepeto''' + #from Baldy of Nome by Esther Birdsall Darling + body_2 = ''' With unseeing eyes and dragging steps, the boy trudged along the snowy + trail, dreading the arrival at Golconda Camp. For there was the House of + Judgment, where all of the unfortunate events of that most unhappy day + would be reviewed sternly, lorem''' + self.question1 = self.post_question( + user=self.user, + body_text=body_1, + title='Test title 1' + ) + self.question2 = self.post_question( + user=self.other_user, + body_text=body_2, + title='Test title 2, Baldy of Nome' + ) + self.answer1 = self.post_answer( + user=self.user, + question = self.question1, + body_text="This is a answer for question 1" + ) + self.answer1 = self.post_answer( + user=self.other_user, + question = self.question2, + body_text="Just a random text to fill the space" + ) + + @skipIf('haystack' not in settings.INSTALLED_APPS, + 'Haystack not setup') + def test_title_search(self): + #title search + title_search_qs = models.Thread.objects.get_for_query('title') + self.assertEquals(title_search_qs.count(), 2) + title_search_qs_2 = models.Thread.objects.get_for_query('Nome') + self.assertEquals(title_search_qs_2.count(), 1) + + @skipIf('haystack' not in settings.INSTALLED_APPS, + 'Haystack not setup') + def test_body_search(self): + + #bodysearch + body_search_qs = models.Thread.objects.get_for_query('Lorem') + self.assertEquals(body_search_qs.count(), 2) + body_search_qs_2 = models.Thread.objects.get_for_query('steps') + self.assertEquals(body_search_qs_2.count(), 1) + + @skipIf('haystack' not in settings.INSTALLED_APPS, + 'Haystack not setup') + def test_user_profile_search(self): + #must return pinocho + user_profile_qs = models.get_users_by_text_query('wood') + self.assertEquals(user_profile_qs.count(), 1) + + #returns both gepeto and pinocho because gepeto nickname + #and gepeto name in pinocho's profile + user_profile_qs = models.get_users_by_text_query('gepeto') + self.assertEquals(user_profile_qs.count(), 2) + + @skipIf('haystack' not in settings.INSTALLED_APPS, + 'Haystack not setup') + def test_get_django_queryset(self): + '''makes a query that can return multiple models and test + get_django_queryset() method from AskbotSearchQuerySet''' + #gepeto is present in profile and in question + from askbot.search.haystack import AskbotSearchQuerySet + qs = AskbotSearchQuerySet().filter(content='gepeto').get_django_queryset(User) + for instance in qs: + self.assertTrue(isinstance(instance, User)) + + qs = AskbotSearchQuerySet().filter(content='gepeto').get_django_queryset(models.Thread) + for instance in qs: + self.assertTrue(isinstance(instance, models.Thread)) diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 1a3a9c49..e61fcd2d 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -618,7 +618,7 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): def test_question_upvote_downvote(self): question = self.post_question() - question.score = 5 + question.points = 5 question.vote_up_count = 7 question.vote_down_count = 2 question.save() @@ -631,7 +631,7 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): data = simplejson.loads(response.content) self.assertEqual(1, data['success']) - self.assertEqual(6, data['count']) # 6 == question.score(5) + 1 + self.assertEqual(6, data['count']) # 6 == question.points(5) + 1 thread = Thread.objects.get(id=question.thread.id) @@ -647,7 +647,7 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): data = simplejson.loads(response.content) self.assertEqual(1, data['success']) - self.assertEqual(5, data['count']) # 6 == question.score(6) - 1 + self.assertEqual(5, data['count']) # 6 == question.points(6) - 1 thread = Thread.objects.get(id=question.thread.id) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index f05cc9e2..1a753098 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -169,7 +169,7 @@ def process_vote(user = None, vote_direction = None, post = None): if vote != None: user.assert_can_revoke_old_vote(vote) score_delta = vote.cancel() - response_data['count'] = post.score + score_delta + response_data['count'] = post.points+ score_delta response_data['status'] = 1 #this means "cancel" else: @@ -192,7 +192,7 @@ def process_vote(user = None, vote_direction = None, post = None): else: vote = user.downvote(post = post) - response_data['count'] = post.score + response_data['count'] = post.points response_data['status'] = 0 #this means "not cancel", normal operation response_data['success'] = 1 @@ -842,7 +842,8 @@ def upvote_comment(request): ) else: raise ValueError - return {'score': comment.score} + #FIXME: rename js + return {'score': comment.points} @csrf.csrf_exempt @decorators.ajax_only diff --git a/askbot/views/users.py b/askbot/views/users.py index dbcbda5c..ee3c7d91 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -370,7 +370,7 @@ def user_stats(request, user, context): # Questions # questions = user.posts.get_questions().filter(**question_filter).\ - order_by('-score', '-thread__last_activity_at').\ + order_by('-points', '-thread__last_activity_at').\ select_related('thread', 'thread__last_activity_by')[:100] #added this if to avoid another query if questions is less than 100 @@ -724,7 +724,7 @@ def user_responses(request, user, context): and "flags" - moderation items for mods only """ - #0) temporary, till urls are fixed: update context + #0) temporary, till urls are fixed: update context # to contain response counts for all sub-sections context.update(view_context.get_for_inbox(request.user)) @@ -904,7 +904,7 @@ def user_favorites(request, user, context): favorite_threads = user.user_favorite_questions.values_list('thread', flat=True) questions = models.Post.objects.filter(post_type='question', thread__in=favorite_threads)\ .select_related('thread', 'thread__last_activity_by')\ - .order_by('-score', '-thread__last_activity_at')[:const.USER_VIEW_DATA_SIZE] + .order_by('-points', '-thread__last_activity_at')[:const.USER_VIEW_DATA_SIZE] data = { 'active_tab':'users', diff --git a/askbot/views/writers.py b/askbot/views/writers.py index 4024b4b0..8fcea796 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -473,7 +473,7 @@ def edit_answer(request, id): if request.POST['select_revision'] == 'true': # user has changed revistion number revision_form = forms.RevisionForm( - answer, + answer, revision, request.POST ) @@ -618,7 +618,8 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po 'user_id': comment_owner.id, 'is_deletable': is_deletable, 'is_editable': is_editable, - 'score': comment.score, + 'points': comment.points, + 'score': comment.points, #to support js 'upvoted_by_user': getattr(comment, 'upvoted_by_user', False) } json_comments.append(comment_data) @@ -685,7 +686,8 @@ def edit_comment(request): 'user_id': comment_post.author.id, 'is_deletable': is_deletable, 'is_editable': is_editable, - 'score': comment_post.score, + 'score': comment_post.points, #to support unchanged js + 'points': comment_post.points, 'voted': comment_post.is_upvoted_by(request.user), } |