summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomasz Zielinski <tomasz.zielinski@pyconsultant.eu>2012-01-09 18:02:13 +0100
committerTomasz Zielinski <tomasz.zielinski@pyconsultant.eu>2012-01-09 18:02:13 +0100
commit2bf344dd7bde45de9013ff496ccb7ec31277b781 (patch)
tree50fa2e1498c8200c6e06f3685dab3c5d216e5733
parentb332d5d6b9f79491c2412989bc1e75679a9dc685 (diff)
downloadaskbot-2bf344dd7bde45de9013ff496ccb7ec31277b781.tar.gz
askbot-2bf344dd7bde45de9013ff496ccb7ec31277b781.tar.bz2
askbot-2bf344dd7bde45de9013ff496ccb7ec31277b781.zip
Optimized question() view
-rw-r--r--askbot/models/content.py828
-rw-r--r--askbot/models/post.py78
-rw-r--r--askbot/models/question.py19
-rw-r--r--askbot/skins/default/templates/macros.html4
-rw-r--r--askbot/skins/default/templates/question.html25
-rw-r--r--askbot/skins/default/templates/question/sidebar.html4
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html4
-rw-r--r--askbot/tests/db_api_tests.py3
-rw-r--r--askbot/tests/post_model_tests.py34
-rw-r--r--askbot/views/readers.py17
-rw-r--r--askbot/views/writers.py6
11 files changed, 128 insertions, 894 deletions
diff --git a/askbot/models/content.py b/askbot/models/content.py
deleted file mode 100644
index ca747cc8..00000000
--- a/askbot/models/content.py
+++ /dev/null
@@ -1,828 +0,0 @@
-import datetime
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django.core import urlresolvers
-from django.db import models
-from django.utils import html as html_utils
-from django.utils.datastructures import SortedDict
-from django.utils.translation import ugettext as _
-from django.utils.http import urlquote as django_urlquote
-from django.core import exceptions as django_exceptions
-
-from askbot.utils.slug import slugify
-from askbot import const
-from askbot.models.meta import Comment, Vote
-from askbot.models.user import EmailFeedSetting
-from askbot.models.tag import Tag, MarkedTag, tags_match_some_wildcard
-from askbot.models.post import PostRevision
-from askbot.models.base import parse_post_text, parse_and_save_post
-from askbot.conf import settings as askbot_settings
-from askbot import exceptions
-
-class Content(models.Model):
- """
- Base class for Question and Answer
- """
- author = models.ForeignKey(User, related_name='%(class)ss')
- added_at = models.DateTimeField(default=datetime.datetime.now)
-
- deleted = models.BooleanField(default=False)
- deleted_at = models.DateTimeField(null=True, blank=True)
- deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_%(class)ss')
-
- wiki = models.BooleanField(default=False)
- wikified_at = models.DateTimeField(null=True, blank=True)
-
- locked = models.BooleanField(default=False)
- locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_%(class)ss')
- locked_at = models.DateTimeField(null=True, blank=True)
-
- score = models.IntegerField(default=0)
- vote_up_count = models.IntegerField(default=0)
- vote_down_count = models.IntegerField(default=0)
-
- comment_count = models.PositiveIntegerField(default=0)
- offensive_flag_count = models.SmallIntegerField(default=0)
-
- last_edited_at = models.DateTimeField(null=True, blank=True)
- last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_%(class)ss')
-
- html = models.TextField(null=True)#html rendition of the latest revision
- text = models.TextField(null=True)#denormalized copy of latest revision
- comments = generic.GenericRelation(Comment)
- votes = generic.GenericRelation(Vote)
-
- _use_markdown = True
- _escape_html = False #markdow does the escaping
- _urlize = False
-
- class Meta:
- abstract = True
- app_label = 'askbot'
-
- parse = parse_post_text
- parse_and_save = parse_and_save_post
-
- def __unicode__(self):
- if self.is_question():
- return self.title
- elif self.is_answer():
- return self.html
- raise NotImplementedError
-
- def get_absolute_url(self, no_slug = False):
- if self.is_answer():
- return u'%(base)s%(slug)s?answer=%(id)d#answer-container-%(id)d' % \
- {
- 'base': urlresolvers.reverse('question', args=[self.question.id]),
- 'slug': django_urlquote(slugify(self.question.title)),
- 'id': self.id
- }
- elif self.is_question():
- url = urlresolvers.reverse('question', args=[self.id])
- if no_slug == True:
- return url
- else:
- return url + django_urlquote(self.slug)
- raise NotImplementedError
-
-
- def is_answer(self):
- return self.post_type == 'answer'
-
- def is_question(self):
- return self.post_type == 'question'
-
- def save(self, *args, **kwargs):
- models.Model.save(self, *args, **kwargs) # TODO: figure out how to use super() here
- if self.is_answer() and 'postgres' in settings.DATABASE_ENGINE:
- #hit the database to trigger update of full text search vector
- self.question.save()
-
-
- def get_comments(self, visitor = None):
- """returns comments for a post, annotated with
- ``upvoted_by_user`` parameter, if visitor is logged in
- otherwise, returns query set for all comments to a given post
- """
- if visitor.is_anonymous():
- return self.comments.all().order_by('id')
- else:
- comment_content_type = ContentType.objects.get_for_model(Comment)
- #a fancy query to annotate comments with the visitor votes
- comments = self.comments.extra(
- select = SortedDict([
- (
- 'upvoted_by_user',
- 'SELECT COUNT(*) from vote, comment '
- 'WHERE vote.user_id = %s AND '
- 'vote.content_type_id = %s AND '
- 'vote.object_id = comment.id',
- )
- ]),
- select_params = (visitor.id, comment_content_type.id)
- ).order_by('id')
- return comments
-
- #todo: maybe remove this wnen post models are unified
- def get_text(self):
- return self.text
-
- def get_snippet(self):
- """returns an abbreviated snippet of the content
- """
- return html_utils.strip_tags(self.html)[:120] + ' ...'
-
- def add_comment(self, comment=None, user=None, added_at=None):
- if added_at is None:
- added_at = datetime.datetime.now()
- if None in (comment ,user):
- raise Exception('arguments comment and user are required')
-
- #Comment = models.get_model('askbot','Comment')#todo: forum hardcoded
- comment = Comment(
- content_object=self,
- comment=comment,
- user=user,
- added_at=added_at
- )
- comment.parse_and_save(author = user)
- self.comment_count = self.comment_count + 1
- self.save()
-
- #tried to add this to bump updated question
- #in most active list, but it did not work
- #becase delayed email updates would be triggered
- #for cases where user did not subscribe for them
- #
- #need to redo the delayed alert sender
- #
- #origin_post = self.get_origin_post()
- #if origin_post == self:
- # self.last_activity_at = added_at
- # self.last_activity_by = user
- #else:
- # origin_post.last_activity_at = added_at
- # origin_post.last_activity_by = user
- # origin_post.save()
-
- return comment
-
- def get_global_tag_based_subscribers(
- self,
- tag_mark_reason = None,
- subscription_records = None
- ):
- """returns a list of users who either follow or "do not ignore"
- the given set of tags, depending on the tag_mark_reason
-
- ``subscription_records`` - query set of ``~askbot.models.EmailFeedSetting``
- this argument is used to reduce number of database queries
- """
- if tag_mark_reason == 'good':
- email_tag_filter_strategy = const.INCLUDE_INTERESTING
- user_set_getter = User.objects.filter
- elif tag_mark_reason == 'bad':
- email_tag_filter_strategy = const.EXCLUDE_IGNORED
- user_set_getter = User.objects.exclude
- else:
- raise ValueError('Uknown value of tag mark reason %s' % tag_mark_reason)
-
- #part 1 - find users who follow or not ignore the set of tags
- tag_names = self.get_tag_names()
- tag_selections = MarkedTag.objects.filter(
- tag__name__in = tag_names,
- reason = tag_mark_reason
- )
- subscribers = set(
- user_set_getter(
- tag_selections__in = tag_selections
- ).filter(
- notification_subscriptions__in = subscription_records
- ).filter(
- email_tag_filter_strategy = email_tag_filter_strategy
- )
- )
-
- #part 2 - find users who follow or not ignore tags via wildcard selections
- #inside there is a potentially time consuming loop
- if askbot_settings.USE_WILDCARD_TAGS:
- #todo: fix this
- #this branch will not scale well
- #because we have to loop through the list of users
- #in python
- if tag_mark_reason == 'good':
- empty_wildcard_filter = {'interesting_tags__exact': ''}
- wildcard_tags_attribute = 'interesting_tags'
- update_subscribers = lambda the_set, item: the_set.add(item)
- elif tag_mark_reason == 'bad':
- empty_wildcard_filter = {'ignored_tags__exact': ''}
- wildcard_tags_attribute = 'ignored_tags'
- update_subscribers = lambda the_set, item: the_set.discard(item)
-
- potential_wildcard_subscribers = User.objects.filter(
- notification_subscriptions__in = subscription_records
- ).filter(
- email_tag_filter_strategy = email_tag_filter_strategy
- ).exclude(
- **empty_wildcard_filter #need this to limit size of the loop
- )
- for potential_subscriber in potential_wildcard_subscribers:
- wildcard_tags = getattr(
- potential_subscriber,
- wildcard_tags_attribute
- ).split(' ')
-
- if tags_match_some_wildcard(tag_names, wildcard_tags):
- update_subscribers(subscribers, potential_subscriber)
-
- return subscribers
-
- def get_global_instant_notification_subscribers(self):
- """returns a set of subscribers to post according to tag filters
- both - subscribers who ignore tags or who follow only
- specific tags
-
- this method in turn calls several more specialized
- subscriber retrieval functions
- todo: retrieval of wildcard tag followers ignorers
- won't scale at all
- """
- subscriber_set = set()
-
- global_subscriptions = EmailFeedSetting.objects.filter(
- feed_type = 'q_all',
- frequency = 'i'
- )
-
- #segment of users who have tag filter turned off
- global_subscribers = User.objects.filter(
- email_tag_filter_strategy = const.INCLUDE_ALL
- )
- subscriber_set.update(global_subscribers)
-
- #segment of users who want emails on selected questions only
- subscriber_set.update(
- self.get_global_tag_based_subscribers(
- subscription_records = global_subscriptions,
- tag_mark_reason = 'good'
- )
- )
-
- #segment of users who want to exclude ignored tags
- subscriber_set.update(
- self.get_global_tag_based_subscribers(
- subscription_records = global_subscriptions,
- tag_mark_reason = 'bad'
- )
- )
- return subscriber_set
-
-
- def get_instant_notification_subscribers(
- self,
- potential_subscribers = None,
- mentioned_users = None,
- exclude_list = None,
- ):
- """get list of users who have subscribed to
- receive instant notifications for a given post
- this method works for questions and answers
-
- Arguments:
-
- * ``potential_subscribers`` is not used here! todo: why? - clean this out
- parameter is left for the uniformity of the interface
- (Comment method does use it)
- normally these methods would determine the list
- :meth:`~askbot.models.question.Question.get_response_recipients`
- :meth:`~askbot.models.question.Answer.get_response_recipients`
- - depending on the type of the post
- * ``mentioned_users`` - users, mentioned in the post for the first time
- * ``exclude_list`` - users who must be excluded from the subscription
-
- Users who receive notifications are:
-
- * of ``mentioned_users`` - those who subscribe for the instant
- updates on the @name mentions
- * those who follow the parent question
- * global subscribers (any personalized tag filters are applied)
- * author of the question who subscribe to instant updates
- on questions that they asked
- * authors or any answers who subsribe to instant updates
- on the questions which they answered
- """
- #print '------------------'
- #print 'in content function'
- subscriber_set = set()
- #print 'potential subscribers: ', potential_subscribers
-
- #1) mention subscribers - common to questions and answers
- if mentioned_users:
- mention_subscribers = EmailFeedSetting.objects.filter_subscribers(
- potential_subscribers = mentioned_users,
- feed_type = 'm_and_c',
- frequency = 'i'
- )
- subscriber_set.update(mention_subscribers)
-
- origin_post = self.get_origin_post()
-
- #print origin_post
-
- #2) individually selected - make sure that users
- #are individual subscribers to this question
- selective_subscribers = origin_post.followed_by.all()
- #print 'question followers are ', [s for s in selective_subscribers]
- if selective_subscribers:
- selective_subscribers = EmailFeedSetting.objects.filter_subscribers(
- potential_subscribers = selective_subscribers,
- feed_type = 'q_sel',
- frequency = 'i'
- )
- subscriber_set.update(selective_subscribers)
- #print 'selective subscribers: ', selective_subscribers
-
- #3) whole forum subscribers
- global_subscribers = origin_post.get_global_instant_notification_subscribers()
- subscriber_set.update(global_subscribers)
-
- #4) question asked by me (todo: not "edited_by_me" ???)
- question_author = origin_post.author
- if EmailFeedSetting.objects.filter(
- subscriber = question_author,
- frequency = 'i',
- feed_type = 'q_ask'
- ):
- subscriber_set.add(question_author)
-
- #4) questions answered by me -make sure is that people
- #are authors of the answers to this question
- #todo: replace this with a query set method
- answer_authors = set()
- for answer in origin_post.answers.all():
- authors = answer.get_author_list()
- answer_authors.update(authors)
-
- if answer_authors:
- answer_subscribers = EmailFeedSetting.objects.filter_subscribers(
- potential_subscribers = answer_authors,
- frequency = 'i',
- feed_type = 'q_ans',
- )
- subscriber_set.update(answer_subscribers)
- #print 'answer subscribers: ', answer_subscribers
-
- #print 'exclude_list is ', exclude_list
- subscriber_set -= set(exclude_list)
-
- #print 'final subscriber set is ', subscriber_set
- return list(subscriber_set)
-
- def get_latest_revision(self):
- return self.revisions.all().order_by('-revised_at')[0]
-
- def get_latest_revision_number(self):
- return self.get_latest_revision().revision
-
- def get_time_of_last_edit(self):
- if self.last_edited_at:
- return self.last_edited_at
- else:
- return self.added_at
-
- def get_owner(self):
- return self.author
-
- def get_author_list(
- self,
- include_comments = False,
- recursive = False,
- exclude_list = None):
-
- #todo: there may be a better way to do these queries
- authors = set()
- authors.update([r.author for r in self.revisions.all()])
- if include_comments:
- authors.update([c.user for c in self.comments.all()])
- if recursive:
- if hasattr(self, 'answers'):
- for a in self.answers.exclude(deleted = True):
- authors.update(a.get_author_list( include_comments = include_comments ) )
- if exclude_list:
- authors -= set(exclude_list)
- return list(authors)
-
- def passes_tag_filter_for_user(self, user):
-
- question = self.get_origin_post()
- if user.email_tag_filter_strategy == const.INCLUDE_INTERESTING:
- #at least some of the tags must be marked interesting
- return user.has_affinity_to_question(
- question,
- affinity_type = 'like'
- )
- elif user.email_tag_filter_strategy == const.EXCLUDE_IGNORED:
- return not user.has_affinity_to_question(
- question,
- affinity_type = 'dislike'
- )
- elif user.email_tag_filter_strategy == const.INCLUDE_ALL:
- return True
- else:
- raise ValueError(
- 'unexpected User.email_tag_filter_strategy %s' \
- % user.email_tag_filter_strategy
- )
-
- def post_get_last_update_info(self):#todo: rename this subroutine
- when = self.added_at
- who = self.author
- if self.last_edited_at and self.last_edited_at > when:
- when = self.last_edited_at
- who = self.last_edited_by
- comments = self.comments.all()
- if len(comments) > 0:
- for c in comments:
- if c.added_at > when:
- when = c.added_at
- who = c.user
- return when, who
-
- def tagname_meta_generator(self):
- return u','.join([unicode(tag) for tag in self.get_tag_names()])
-
- def get_origin_post(self):
- if self.is_answer():
- return self.question
- elif self.is_question():
- return self
- raise NotImplementedError
-
- def _repost_as_question(self, new_title = None):
- """posts answer as question, together with all the comments
- while preserving time stamps and authors
- does not delete the answer itself though
- """
- if not self.is_answer():
- raise NotImplementedError
- revisions = self.revisions.all().order_by('revised_at')
- rev0 = revisions[0]
- new_question = rev0.author.post_question(
- title = new_title,
- body_text = rev0.text,
- tags = self.question.tagnames,
- wiki = self.question.wiki,
- is_anonymous = self.question.is_anonymous,
- timestamp = rev0.revised_at
- )
- if len(revisions) > 1:
- for rev in revisions[1:]:
- rev.author.edit_question(
- question = new_question,
- body_text = rev.text,
- revision_comment = rev.summary,
- timestamp = rev.revised_at
- )
- for comment in self.comments.all():
- comment.content_object = new_question
- comment.save()
- return new_question
-
- def swap_with_question(self, new_title = None):
- """swaps answer with the question it belongs to and
- sets the title of question to ``new_title``
- """
- if not self.is_answer():
- raise NotImplementedError
- #1) make new question by using new title, tags of old question
- # and the answer body, as well as the authors of all revisions
- # and repost all the comments
- new_question = self._repost_as_question(new_title = new_title)
-
- #2) post question (all revisions and comments) as answer
- new_answer = self.question.repost_as_answer(question = new_question)
-
- #3) assign all remaining answers to the new question
- self.question.answers.update(question = new_question)
- self.question.delete()
- self.delete()
- return new_question
-
-
- def get_page_number(self, answers = None):
- """When question has many answers, answers are
- paginated. This function returns number of the page
- on which the answer will be shown, using the default
- sort order. The result may depend on the visitor."""
- if self.is_question():
- return 1
- elif self.is_answer():
- order_number = 0
- for answer in answers:
- if self == answer:
- break
- order_number += 1
- return int(order_number/const.ANSWERS_PAGE_SIZE) + 1
- raise NotImplementedError
-
- def get_user_vote(self, user):
- if not self.is_answer():
- raise NotImplementedError
-
- if user.is_anonymous():
- return None
-
- votes = self.votes.filter(user=user)
- if votes and votes.count() > 0:
- return votes[0]
- else:
- return None
-
-
- def _question__assert_is_visible_to(self, user):
- """raises QuestionHidden"""
- if self.deleted:
- message = _(
- 'Sorry, this question has been '
- 'deleted and is no longer accessible'
- )
- if user.is_anonymous():
- raise exceptions.QuestionHidden(message)
- try:
- user.assert_can_see_deleted_post(self)
- except django_exceptions.PermissionDenied:
- raise exceptions.QuestionHidden(message)
-
- def _answer__assert_is_visible_to(self, user):
- """raises QuestionHidden or AnswerHidden"""
- try:
- self.question.assert_is_visible_to(user)
- except exceptions.QuestionHidden:
- message = _(
- 'Sorry, the answer you are looking for is '
- 'no longer available, because the parent '
- 'question has been removed'
- )
- raise exceptions.QuestionHidden(message)
- if self.deleted:
- message = _(
- 'Sorry, this answer has been '
- 'removed and is no longer accessible'
- )
- if user.is_anonymous():
- raise exceptions.AnswerHidden(message)
- try:
- user.assert_can_see_deleted_post(self)
- except django_exceptions.PermissionDenied:
- raise exceptions.AnswerHidden(message)
-
- def assert_is_visible_to(self, user):
- if self.is_question():
- return self._question__assert_is_visible_to(user)
- elif self.is_answer():
- return self._answer__assert_is_visible_to(user)
- raise NotImplementedError
-
- def get_updated_activity_data(self, created = False):
- if self.is_answer():
- #todo: simplify this to always return latest revision for the second
- #part
- if created:
- return const.TYPE_ACTIVITY_ANSWER, self
- else:
- latest_revision = self.get_latest_revision()
- return const.TYPE_ACTIVITY_UPDATE_ANSWER, latest_revision
- elif self.is_question():
- if created:
- return const.TYPE_ACTIVITY_ASK_QUESTION, self
- else:
- latest_revision = self.get_latest_revision()
- return const.TYPE_ACTIVITY_UPDATE_QUESTION, latest_revision
- raise NotImplementedError
-
- def get_tag_names(self):
- if self.is_question():
- """Creates a list of Tag names from the ``tagnames`` attribute."""
- return self.tagnames.split(u' ')
- elif self.is_answer():
- """return tag names on the question"""
- return self.question.get_tag_names()
- raise NotImplementedError
-
-
- def _answer__apply_edit(self, edited_at=None, edited_by=None, text=None, comment=None, wiki=False):
-
- if text is None:
- text = self.get_latest_revision().text
- if edited_at is None:
- edited_at = datetime.datetime.now()
- if edited_by is None:
- raise Exception('edited_by is required')
-
- self.last_edited_at = edited_at
- self.last_edited_by = edited_by
- #self.html is denormalized in save()
- self.text = text
- #todo: bug wiki has no effect here
-
- #must add revision before saving the answer
- self.add_revision(
- author = edited_by,
- revised_at = edited_at,
- text = text,
- comment = comment
- )
-
- self.parse_and_save(author = edited_by)
-
- self.question.last_activity_at = edited_at
- self.question.last_activity_by = edited_by
- self.question.save()
-
- def _question__apply_edit(self, edited_at=None, edited_by=None, title=None,\
- text=None, comment=None, tags=None, wiki=False, \
- edit_anonymously = False):
-
- latest_revision = self.get_latest_revision()
- #a hack to allow partial edits - important for SE loader
- if title is None:
- title = self.title
- if text is None:
- text = latest_revision.text
- if tags is None:
- tags = latest_revision.tagnames
-
- if edited_by is None:
- raise Exception('parameter edited_by is required')
-
- if edited_at is None:
- edited_at = datetime.datetime.now()
-
- # Update the Question itself
- self.title = title
- self.last_edited_at = edited_at
- self.last_activity_at = edited_at
- self.last_edited_by = edited_by
- self.last_activity_by = edited_by
- self.tagnames = tags
- self.text = text
- self.is_anonymous = edit_anonymously
-
- #wiki is an eternal trap whence there is no exit
- if self.wiki == False and wiki == True:
- self.wiki = True
-
- # Update the Question tag associations
- if latest_revision.tagnames != tags:
- self.update_tags(tagnames = tags, user = edited_by, timestamp = edited_at)
-
- # Create a new revision
- self.add_revision(
- author = edited_by,
- text = text,
- revised_at = edited_at,
- is_anonymous = edit_anonymously,
- comment = comment,
- )
-
- self.parse_and_save(author = edited_by)
-
- def apply_edit(self, *kargs, **kwargs):
- if kwargs['text'] == '':
- kwargs['text'] = ' '#a hack allowing empty body text in posts
- if self.is_answer():
- return self._answer__apply_edit(*kargs, **kwargs)
- elif self.is_question():
- return self._question__apply_edit(*kargs, **kwargs)
- raise NotImplementedError
-
- def _answer__add_revision(self, author=None, revised_at=None, text=None, comment=None):
- #todo: this may be identical to Question.add_revision
- if None in (author, revised_at, text):
- raise Exception('arguments author, revised_at and text are required')
- rev_no = self.revisions.all().count() + 1
- if comment in (None, ''):
- if rev_no == 1:
- comment = const.POST_STATUS['default_version']
- else:
- comment = 'No.%s Revision' % rev_no
- return PostRevision.objects.create_answer_revision(
- answer=self,
- author=author,
- revised_at=revised_at,
- text=text,
- summary=comment,
- revision=rev_no
- )
-
- def _question__add_revision(
- self,
- author = None,
- is_anonymous = False,
- text = None,
- comment = None,
- revised_at = None
- ):
- if None in (author, text, comment):
- raise Exception('author, text and comment are required arguments')
- rev_no = self.revisions.all().count() + 1
- if comment in (None, ''):
- if rev_no == 1:
- comment = const.POST_STATUS['default_version']
- else:
- comment = 'No.%s Revision' % rev_no
-
- return PostRevision.objects.create_question_revision(
- question = self,
- revision = rev_no,
- title = self.title,
- author = author,
- is_anonymous = is_anonymous,
- revised_at = revised_at,
- tagnames = self.tagnames,
- summary = comment,
- text = text
- )
-
- def add_revision(self, *kargs, **kwargs):
- if self.is_answer():
- return self._answer__add_revision(*kargs, **kwargs)
- elif self.is_question():
- return self._question__add_revision(*kargs, **kwargs)
- raise NotImplementedError
-
- def _answer__get_response_receivers(self, exclude_list = None):
- """get list of users interested in this response
- update based on their participation in the question
- activity
-
- exclude_list is required and normally should contain
- author of the updated so that he/she is not notified of
- the response
- """
- assert(exclude_list is not None)
- recipients = set()
- recipients.update(
- self.get_author_list(
- include_comments = True
- )
- )
- recipients.update(
- self.question.get_author_list(
- include_comments = True
- )
- )
- for answer in self.question.answers.all():
- recipients.update(answer.get_author_list())
-
- recipients -= set(exclude_list)
-
- return list(recipients)
-
- def _question__get_response_receivers(self, exclude_list = None):
- """returns list of users who might be interested
- in the question update based on their participation
- in the question activity
-
- exclude_list is mandatory - it normally should have the
- author of the update so the he/she is not notified about the update
- """
- assert(exclude_list != None)
- recipients = set()
- recipients.update(
- self.get_author_list(
- include_comments = True
- )
- )
- #do not include answer commenters here
- for a in self.answers.all():
- recipients.update(a.get_author_list())
-
- recipients -= set(exclude_list)
- return recipients
-
- def get_response_receivers(self, exclude_list = None):
- if self.is_answer():
- return self._answer__get_response_receivers(exclude_list)
- elif self.is_question():
- return self._question__get_response_receivers(exclude_list)
- raise NotImplementedError
-
- def get_question_title(self):
- if self.is_answer():
- return self.question.title
- elif self.is_question():
- if self.closed:
- attr = const.POST_STATUS['closed']
- elif self.deleted:
- attr = const.POST_STATUS['deleted']
- else:
- attr = None
- if attr is not None:
- return u'%s %s' % (self.title, attr)
- else:
- return self.title
- raise NotImplementedError
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 531989ab..2956a9da 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
import datetime
import operator
import cgi
@@ -393,6 +394,49 @@ class PostManager(BaseQuerySetManager):
return answer
+ def precache_comments(self, for_posts, visitor):
+ """
+ Fetches comments for given posts, and stores them in post._cached_comments
+ Additionally, annotates posts with ``upvoted_by_user`` parameter, if visitor is logged in
+
+ """
+ qs = Post.objects.get_comments().filter(parent__in=for_posts).select_related('author')
+
+ if visitor.is_anonymous():
+ comments = list(qs.order_by('added_at'))
+ else:
+ upvoted_by_user = list(qs.filter(votes__user=visitor).distinct())
+ not_upvoted_by_user = list(qs.exclude(votes__user=visitor).distinct())
+
+ for c in upvoted_by_user:
+ c.upvoted_by_user = 1 # numeric value to maintain compatibility with previous version of this code
+
+ comments = upvoted_by_user + not_upvoted_by_user
+ comments.sort(key=operator.attrgetter('added_at'))
+
+ post_map = defaultdict(list)
+ for cm in comments:
+ post_map[cm.parent_id].append(cm)
+ for post in for_posts:
+ post._cached_comments = post_map[post.id]
+
+ # Old Post.get_comment(self, visitor=None) method:
+ # if visitor.is_anonymous():
+ # return self.comments.order_by('added_at')
+ # else:
+ # upvoted_by_user = list(self.comments.filter(votes__user=visitor).distinct())
+ # not_upvoted_by_user = list(self.comments.exclude(votes__user=visitor).distinct())
+ #
+ # for c in upvoted_by_user:
+ # c.upvoted_by_user = 1 # numeric value to maintain compatibility with previous version of this code
+ #
+ # comments = upvoted_by_user + not_upvoted_by_user
+ # comments.sort(key=operator.attrgetter('added_at'))
+ #
+ # return comments
+
+
+
class Post(models.Model):
post_type = models.CharField(max_length=255)
@@ -616,11 +660,13 @@ class Post(models.Model):
def is_comment(self):
return self.post_type == 'comment'
- def get_absolute_url(self, no_slug = False):
+ def get_absolute_url(self, no_slug = False, question_post=None):
from askbot.utils.slug import slugify
if self.is_answer():
+ if not question_post:
+ question_post = self.thread._question_post()
return u'%(base)s%(slug)s?answer=%(id)d#answer-container-%(id)d' % {
- 'base': urlresolvers.reverse('question', args=[self.thread._question_post().id]),
+ 'base': urlresolvers.reverse('question', args=[question_post.id]),
'slug': django_urlquote(slugify(self.thread.title)),
'id': self.id
}
@@ -707,25 +753,6 @@ class Post(models.Model):
return slugify(self.thread.title)
slug = property(_get_slug)
- def get_comments(self, visitor = None):
- """returns comments for a post, annotated with
- ``upvoted_by_user`` parameter, if visitor is logged in
- otherwise, returns query set for all comments to a given post
- """
- if visitor.is_anonymous():
- return self.comments.order_by('added_at')
- else:
- upvoted_by_user = list(self.comments.filter(votes__user=visitor))
- not_upvoted_by_user = list(self.comments.exclude(votes__user=visitor))
-
- for c in upvoted_by_user:
- c.upvoted_by_user = 1 # numeric value to maintain compatibility with previous version of this code
-
- comments = upvoted_by_user + not_upvoted_by_user
- comments.sort(key=operator.attrgetter('added_at'))
-
- return comments
-
def get_snippet(self):
"""returns an abbreviated snippet of the content
"""
@@ -1590,14 +1617,9 @@ class Post(models.Model):
#####
#####
- def is_answer_accepted(self):
- if not self.is_answer():
- raise NotImplementedError
- return self == self.thread.accepted_answer
-
def accepted(self):
if self.is_answer():
- return self.thread.accepted_answer == self
+ return self.thread.accepted_answer_id == self.id
raise NotImplementedError
def get_page_number(self, answer_posts):
@@ -1643,6 +1665,8 @@ class Post(models.Model):
parent=self.parent
).exists() is False
+ def hack_template_marker(self, name):
+ list(Post.objects.filter(text=name))
class PostRevisionManager(models.Manager):
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 660e5c4b..b3ce8e68 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -416,8 +416,23 @@ class Thread(models.Model):
thread.similarity = self.get_similarity(other_thread=thread)
similar_threads.sort(key=operator.attrgetter('similarity'), reverse=True)
+ similar_threads = similar_threads[:10]
- return similar_threads[:10]
+ # Denormalize questions to speed up template rendering
+ thread_map = dict([(thread.id, thread) for thread in similar_threads])
+ questions = Post.objects.get_questions().select_related('thread').filter(thread__in=similar_threads)
+ for q in questions:
+ thread_map[q.thread_id].question_denorm = q
+
+ # Postprocess data
+ similar_threads = [
+ {
+ 'url': thread.question_denorm.get_absolute_url(),
+ 'title': thread.get_title(thread.question_denorm)
+ } for thread in similar_threads
+ ]
+
+ return similar_threads
return LazyList(get_data)
@@ -557,7 +572,7 @@ class Thread(models.Model):
return FavoriteQuestion.objects.filter(thread=self, user=user).exists()
def get_last_update_info(self):
- posts = self.posts.all()
+ posts = list(self.posts.select_related('author', 'last_edited_by'))
last_updated_at = posts[0].added_at
last_updated_by = posts[0].author
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 19eaebde..d80c070a 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -277,9 +277,9 @@ for the purposes of the AJAX comment editor #}
<div class="comments" id="{{widget_id}}">
<div class="content">
{% if show_post == post and show_comment and show_comment_position > max_comments %}
- {% set comments = post.get_comments(visitor = user)[:show_comment_position] %}
+ {% set comments = post._cached_comments[:show_comment_position] %}
{% else %}
- {% set comments = post.get_comments(visitor = user)[:max_comments] %}
+ {% set comments = post._cached_comments[:max_comments] %}
{% endif %}
{% for comment in comments %}
{# Warning! Any changes to the comment markup IN THIS `FOR` LOOP must be duplicated in post.js
diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html
index c809b3dc..fab7be10 100644
--- a/askbot/skins/default/templates/question.html
+++ b/askbot/skins/default/templates/question.html
@@ -1,12 +1,12 @@
{% extends "two_column_body.html" %}
<!-- question.html -->
-{% block title %}{% spaceless %}{{ thread.get_title() }}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ thread.get_title(question) }}{% endspaceless %}{% endblock %}
{% block meta_description %}
<meta name="description" content="{{question.summary|striptags|escape}}" />
{% endblock %}
{% block keywords %}{{thread.tagname_meta_generator()}}{% endblock %}
{% block forestyle %}
- <link rel="canonical" href="{{settings.APP_URL}}{{thread.get_absolute_url()}}" />
+ <link rel="canonical" href="{{settings.APP_URL}}{{question.get_absolute_url()}}" />
<link rel="stylesheet" type="text/css" href="{{'/js/wmd/wmd.css'|media}}" />
{% endblock %}
{% block content %}
@@ -27,7 +27,7 @@
</div>
<div class="question-content">
- <h1><a href="{{ thread.get_absolute_url() }}">{{ thread.get_title() }}</a></h1>
+ <h1><a href="{{ question.get_absolute_url() }}">{{ thread.get_title(question) }}</a></h1>
{# ==== START: question/question_tags.html" #}
{{ macros.tag_list_widget(
tags = thread.get_tag_names(),
@@ -108,6 +108,7 @@
}}
{# ==== END: question/question_comments.html ==== #}
</div>
+
</div>
<div class="clean"></div>
{# ==== END: question/question_card.html ==== #}
@@ -120,6 +121,7 @@
</div>
{# ==== END: question/closed_question_info.html ==== #}
{% endif %}
+
{% if answers %}
{# ==== START: question/answer_tab_bar.html ==== #}
<div class="tabBar tabBar-answer">
@@ -134,13 +136,13 @@
<span class="label">
Sort by ยป
</span>
- <a id="oldest" href="{{ thread.get_absolute_url() }}?sort=oldest#sort-top"
+ <a id="oldest" href="{{ question.get_absolute_url() }}?sort=oldest#sort-top"
title="{% trans %}oldest answers will be shown first{% endtrans %}"
><span>{% trans %}oldest answers{% endtrans %}</span></a>
- <a id="latest" href="{{ thread.get_absolute_url() }}?sort=latest#sort-top"
+ <a id="latest" href="{{ question.get_absolute_url() }}?sort=latest#sort-top"
title="{% trans %}newest answers will be shown first{% endtrans %}"
><span>{% trans %}newest answers{% endtrans %}</span></a>
- <a id="votes" href="{{ thread.get_absolute_url() }}?sort=votes#sort-top"
+ <a id="votes" href="{{ question.get_absolute_url() }}?sort=votes#sort-top"
title="{% trans %}most voted answers will be shown first{% endtrans %}"
><span>{% trans %}popular answers{% endtrans %}</span></a>
</div>
@@ -150,6 +152,7 @@
{{ macros.paginator(paginator_context) }}
<div class="clean"></div>
+
{% for answer in answers %}
{# ==== START: question/answer_card.html ==== #}
<a name="{{ answer.id }}"></a>
@@ -164,7 +167,7 @@
{# ==== START: question/answer_vote_buttons.html ==== #}
{{ macros.post_vote_buttons(post = answer, visitor_vote = user_answer_votes[answer.id]) }}
<img id="answer-img-accept-{{ answer.id }}" class="answer-img-accept"
- {% if answer.is_answer_accepted() %}
+ {% if answer.accepted() %}
src="{{'/images/vote-accepted-on.png'|media}}"
{% else %}
src="{{'/images/vote-accepted.png'|media}}"
@@ -196,11 +199,12 @@
{% set pipe=joiner('<span class="sep">|</span>') %}
<span class="linksopt">{{ pipe() }}
<a class="permant-link"
- href="{{ answer.get_absolute_url() }}"
+ href="{{ answer.get_absolute_url(question_post=question) }}"
title="{% trans %}answer permanent link{% endtrans %}">
{% trans %}permanent link{% endtrans %}
</a>
</span>
+
{% if request.user|can_edit_post(answer) %}{{ pipe() }}
<span class="action-link"><a class="question-edit" href="{% url edit_answer answer.id %}">{% trans %}edit{% endtrans %}</a></span>
{% endif %}
@@ -234,6 +238,7 @@
<a id="swap-question-with-answer-{{answer.id}}">{% trans %}swap with question{% endtrans %}</a>
</span>
{% endif %}
+
{# ==== END: question/answer_controls.html ==== #}
</div>
{# ==== START: question/answer_comments.html ==== #}
@@ -259,7 +264,7 @@
<div class="clean"></div>
{% else %}
{# ==== START: question/sharing_prompt_phrase.html ==== #}
- {% set question_url=settings.APP_URL+thread.get_absolute_url()|urlencode %}
+ {% set question_url=settings.APP_URL+question.get_absolute_url()|urlencode %}
<h2 class="share-question">{% trans %}Know someone who can answer? Share a <a href="{{ question_url }}">link</a> to this question via{% endtrans %}
{% if settings.ENABLE_SHARING_TWITTER %}{{ macros.share(site = 'twitter', site_label = 'Twitter') }},{% endif %}
{% if settings.ENABLE_SHARING_FACEBOOK %}{{ macros.share(site = 'facebook', site_label = 'Facebook') }},{% endif %}
@@ -272,6 +277,7 @@
</h2>
{# ==== END: question/sharing_prompt_phrase.html ==== #}
{% endif %}
+
{# ==== START: question/new_answer_form.html ==== #}
<form
id="fmanswer"
@@ -359,6 +365,7 @@
<input type="button" class="submit after-editor" id="fmanswer_button" value="{% trans %}Answer Your Own Question{% endtrans %}"/>
{%endif%}
{# ==== END: question/content.html ==== #}
+
{% endblock %}
{% block sidebar %}
{%include "question/sidebar.html" %}
diff --git a/askbot/skins/default/templates/question/sidebar.html b/askbot/skins/default/templates/question/sidebar.html
index b10c58f2..c2eb3842 100644
--- a/askbot/skins/default/templates/question/sidebar.html
+++ b/askbot/skins/default/templates/question/sidebar.html
@@ -62,9 +62,9 @@
<div class="box">
<h2>{% trans %}Related questions{% endtrans %}</h2>
<div class="questions-related">
- {% for thread in similar_threads.data() %}
+ {% for thread_dict in similar_threads.data() %}
<p>
- <a href="{{ thread.get_absolute_url() }}">{{ thread.get_title() }}</a>
+ <a href="{{ thread_dict.url }}">{{ thread_dict.title }}</a>
</p>
{% endfor %}
</div>
diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html
index 06e0e9a3..eb56d47c 100644
--- a/askbot/skins/default/templates/user_profile/user_stats.html
+++ b/askbot/skins/default/templates/user_profile/user_stats.html
@@ -20,8 +20,8 @@
<div class="answer-summary">
<a title="{{ top_answer.summary|collapse }}"
href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{ top_answer.id }}">
- <span class="answer-votes {% if top_answer.is_answer_accepted() %}answered-accepted{% endif %}"
- title="{% trans answer_score=top_answer.score %}the answer has been voted for {{ answer_score }} times{% endtrans %} {% if top_answer.is_answer_accepted() %}{% trans %}this answer has been selected as correct{% endtrans %}{%endif%}">
+ <span class="answer-votes {% if top_answer.accepted() %}answered-accepted{% endif %}"
+ title="{% trans answer_score=top_answer.score %}the answer has been voted for {{ answer_score }} times{% endtrans %} {% if top_answer.accepted() %}{% trans %}this answer has been selected as correct{% endtrans %}{%endif%}">
{{ top_answer.score }}
</span>
</a>
diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py
index 06a40921..1f3d3b9b 100644
--- a/askbot/tests/db_api_tests.py
+++ b/askbot/tests/db_api_tests.py
@@ -374,7 +374,8 @@ class CommentTests(AskbotTestCase):
def test_other_user_can_upvote_comment(self):
self.other_user.upvote(self.comment)
- comments = self.question.get_comments(visitor = self.other_user)
+ models.Post.objects.precache_comments(for_posts=[self.question], visitor = self.other_user)
+ comments = self.question._cached_comments
self.assertEquals(len(comments), 1)
self.assertEquals(comments[0].upvoted_by_user, True)
self.assertEquals(comments[0].is_upvoted_by(self.other_user), True)
diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py
index 03d4023c..e11d2e81 100644
--- a/askbot/tests/post_model_tests.py
+++ b/askbot/tests/post_model_tests.py
@@ -2,7 +2,7 @@ import datetime
from django.core.exceptions import ValidationError
from askbot.tests.utils import AskbotTestCase
-from askbot.models import PostRevision
+from askbot.models import Post, PostRevision
class PostModelTests(AskbotTestCase):
@@ -115,14 +115,38 @@ class PostModelTests(AskbotTestCase):
c2 = q.add_comment(user=self.user, comment='blah blah')
c3 = self.post_comment(parent_post=q)
- self.assertListEqual([c1, c2, c3], q.get_comments(visitor=self.user))
- self.assertListEqual([c1, c2, c3], q.get_comments(visitor=self.u2))
+ Post.objects.precache_comments(for_posts=[q], visitor=self.user)
+ self.assertListEqual([c1, c2, c3], q._cached_comments)
+ Post.objects.precache_comments(for_posts=[q], visitor=self.u2)
+ self.assertListEqual([c1, c2, c3], q._cached_comments)
c1.added_at, c3.added_at = c3.added_at, c1.added_at
c1.save()
c3.save()
- self.assertListEqual([c3, c2, c1], q.get_comments(visitor=self.user))
- self.assertListEqual([c3, c2, c1], q.get_comments(visitor=self.u2))
+ Post.objects.precache_comments(for_posts=[q], visitor=self.user)
+ self.assertListEqual([c3, c2, c1], q._cached_comments)
+ Post.objects.precache_comments(for_posts=[q], visitor=self.u2)
+ self.assertListEqual([c3, c2, c1], q._cached_comments)
+
+ del self.user
+
+ def test_comment_precaching(self):
+ self.user = self.u1
+ q = self.post_question()
+
+ c1 = self.post_comment(parent_post=q)
+ c2 = q.add_comment(user=self.user, comment='blah blah')
+ c3 = self.post_comment(parent_post=q)
+
+ Post.objects.precache_comments(for_posts=[q], visitor=self.user)
+ self.assertListEqual([c1, c2, c3], q._cached_comments)
+
+ c1.added_at, c3.added_at = c3.added_at, c1.added_at
+ c1.save()
+ c3.save()
+
+ Post.objects.precache_comments(for_posts=[q], visitor=self.user)
+ self.assertListEqual([c3, c2, c1], q._cached_comments)
del self.user \ No newline at end of file
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index edcf2436..3ef70472 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -10,13 +10,10 @@ import datetime
import logging
import urllib
import operator
-from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect, HttpResponse, Http404
-from django.conf import settings as django_settings
from django.core.paginator import Paginator, EmptyPage, InvalidPage
from django.template import Context
-from django.utils.http import urlencode
from django.utils import simplejson
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
@@ -25,7 +22,6 @@ from django.views.decorators import csrf
from django.core.urlresolvers import reverse
from django.core import exceptions as django_exceptions
from django.contrib.humanize.templatetags import humanize
-from django.views.decorators.cache import cache_page
from django.http import QueryDict
import askbot
@@ -40,7 +36,6 @@ from askbot.utils import functions
from askbot.utils.decorators import anonymous_forbidden, ajax_only, get_only
from askbot.search.state_manager import SearchState
from askbot.templatetags import extra_tags
-from askbot.templatetags import extra_filters
import askbot.conf
from askbot.conf import settings as askbot_settings
from askbot.skins.loaders import render_into_skin, get_template#jinja2 template loading enviroment
@@ -478,17 +473,10 @@ def question(request, id):#refactor - long subroutine. display question body, an
answers = thread.get_answers(user = request.user)
answers = answers.select_related('thread', 'author', 'last_edited_by')
answers = answers.order_by({"latest":"-added_at", "oldest":"added_at", "votes":"-score" }[answer_sort_method])
+ answers = list(answers)
- # TODO: Instead of fetching answer posts, fetch all posts that belong to this thread,
- # then manually process them so that `answers` variable is unchanged, and for each answer within it
- # answer.get_comments() return a pre-cached list of comment posts
- #
- # Then, in macros.html, we have to adjust the lines with post.get_comments() calls, like:
- # {% set comments = post.get_comments(visitor = user)[:show_comment_position] %}
- #
+ Post.objects.precache_comments(for_posts=[question_post] + answers, visitor=request.user)
- answers = list(answers)
-
if thread.accepted_answer: # Put the accepted answer to front
answers.remove(thread.accepted_answer)
answers.insert(0, thread.accepted_answer)
@@ -524,6 +512,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
request.session['question_view_times'] = {}
last_seen = request.session['question_view_times'].get(question_post.id, None)
+
updated_when, updated_who = thread.get_last_update_info()
if updated_who != request.user:
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index 5ffc67cf..12cc2471 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -537,7 +537,9 @@ def answer(request, id):#process a new answer
def __generate_comments_json(obj, user):#non-view generates json data for the post comments
"""non-view generates json data for the post comments
"""
- comments = obj.get_comments(visitor = user)
+ models.Post.objects.precache_comments(for_posts=[obj], visitor=user)
+ comments = obj._cached_comments
+
# {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null}
json_comments = []
for comment in comments:
@@ -556,7 +558,7 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po
is_editable = False
- comment_owner = comment.get_owner()
+ comment_owner = comment.author
comment_data = {'id' : comment.id,
'object_id': obj.id,
'comment_age': diff_date(comment.added_at),