summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomasz Zielinski <tomasz.zielinski@pyconsultant.eu>2011-12-17 23:19:00 +0100
committerTomasz Zielinski <tomasz.zielinski@pyconsultant.eu>2011-12-17 23:19:00 +0100
commita9e085110e24c69fa711732e6330c618ce672fdd (patch)
tree5dcf8071f97251eb3b2377e6b02534566515fc3f
parentecced343a72c947b5ef791e1a7c4f8239263e84f (diff)
downloadaskbot-a9e085110e24c69fa711732e6330c618ce672fdd.tar.gz
askbot-a9e085110e24c69fa711732e6330c618ce672fdd.tar.bz2
askbot-a9e085110e24c69fa711732e6330c618ce672fdd.zip
Question view migration - partial
-rw-r--r--askbot/models/post.py32
-rw-r--r--askbot/models/question.py648
-rw-r--r--askbot/skins/default/templates/macros.html90
-rw-r--r--askbot/skins/default/templates/question.html360
-rw-r--r--askbot/skins/default/templates/question/javascript.html6
-rw-r--r--askbot/skins/default/templates/question/sidebar.html16
-rw-r--r--askbot/views/readers.py47
7 files changed, 840 insertions, 359 deletions
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 62389e7a..1526e448 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -5,7 +5,7 @@ from django.utils.http import urlquote as django_urlquote
from askbot.utils import markup
from askbot.utils.html import sanitize_html
-from askbot.models import content
+from askbot.models import content, const
class PostManager(models.Manager):
def get_questions(self):
@@ -17,10 +17,11 @@ class PostManager(models.Manager):
class Post(content.Content):
post_type = models.CharField(max_length=255)
- parent = models.ForeignKey('Post', blank=True, null=True)
+ parent = models.ForeignKey('Post', blank=True, null=True, related_name='comment_posts') # Answer or Question for Comment
- self_answer = models.ForeignKey('Answer', blank=True, null=True)
- self_question = models.ForeignKey('Question', blank=True, null=True)
+ self_answer = models.ForeignKey('Answer', blank=True, null=True, related_name='unused__posts')
+ self_question = models.ForeignKey('Question', blank=True, null=True, related_name='unused__posts')
+ self_comment = models.ForeignKey('Comment', blank=True, null=True, related_name='unused__posts')
question = property(fget=lambda self: self.self_answer.question) # to simulate Answer model
question_id = property(fget=lambda self: self.self_answer.question_id) # to simulate Answer model
@@ -34,6 +35,9 @@ class Post(content.Content):
db_table = 'askbot_post'
managed = False
+ def is_comment(self):
+ return self.post_type == 'comment'
+
def get_absolute_url(self, no_slug = False): # OVERRIDE for Content.get_absolute_url()
from askbot.utils.slug import slugify
if self.is_answer():
@@ -63,6 +67,26 @@ class Post(content.Content):
raise NotImplementedError
return self.thread.accepted_answer_id and (self.thread.accepted_answer_id == self.self_answer_id)
+ def get_page_number(self, answer_posts):
+ """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 not self.is_answer() and not self.is_comment():
+ raise NotImplementedError
+
+ if self.is_comment():
+ post = self.parent
+ else:
+ post = self
+
+ order_number = 0
+ for answer_post in answer_posts:
+ if post == answer_post:
+ break
+ order_number += 1
+ return int(order_number/const.ANSWERS_PAGE_SIZE) + 1
+
for field in Post._meta.fields:
if isinstance(field, models.ForeignKey):
diff --git a/askbot/models/question.py b/askbot/models/question.py
index e4516a91..b9d18c7f 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -21,19 +21,6 @@ from askbot.utils.lists import LazyList
from askbot.utils.slug import slugify
from askbot.utils import mysql
-#todo: too bad keys are duplicated see const sort methods
-QUESTION_ORDER_BY_MAP = {
- 'age-desc': '-added_at',
- 'age-asc': 'added_at',
- 'activity-desc': '-thread__last_activity_at',
- 'activity-asc': 'thread__last_activity_at',
- 'answers-desc': '-thread__answer_count',
- 'answers-asc': 'thread__answer_count',
- 'votes-desc': '-score',
- 'votes-asc': 'score',
- 'relevance-desc': None#this is a special case for postges only
-}
-
class ThreadManager(models.Manager):
def get_tag_summary_from_threads(self, threads):
"""returns a humanized string containing up to
@@ -110,6 +97,173 @@ class ThreadManager(models.Manager):
return thread
+ def get_for_query(self, search_query):
+ """returns a query set of questions,
+ matching the full text query
+ """
+ return self.filter(
+ models.Q(title__icontains=search_query) |
+ models.Q(tagnames__icontains=search_query) |
+ models.Q(posts__deleted=False, posts__text__icontains = search_query)
+ )
+
+# 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 self.filter(posts__post_type='question', posts__deleted=False, posts__self_question_id__in=question_ids)
+# elif settings.DATABASE_ENGINE == 'mysql' and mysql.supports_full_text_search():
+# return self.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():
+# # TODO: !! Fix Postgres search
+# rank_clause = "ts_rank(question.text_search_vector, plainto_tsquery(%s))";
+# search_query = '&'.join(search_query.split())
+# extra_params = (search_query,)
+# extra_kwargs = {
+# 'select': {'relevance': rank_clause},
+# 'where': ['text_search_vector @@ plainto_tsquery(%s)'],
+# 'params': extra_params,
+# 'select_params': extra_params,
+# }
+# return self.extra(**extra_kwargs)
+# else:
+# #fallback to dumb title match search
+# return self.filter(title__icontains=search_query)
+
+ def run_advanced_search(self, request_user=None, search_state=None):
+ """
+ all parameters are guaranteed to be clean
+ however may not relate to database - in that case
+ a relvant filter will be silently dropped
+
+ """
+ from askbot.conf import settings as askbot_settings # Avoid circular import
+
+ search_query = search_state.query
+ tag_selector = search_state.tags
+ author_selector = search_state.author
+ scope_selector = getattr(search_state, 'scope', const.DEFAULT_POST_SCOPE)
+ sort_method = getattr(search_state, 'sort', const.DEFAULT_POST_SORT_METHOD)
+
+ qs = self.filter(posts__post_type='question', posts__deleted=False) # TODO: add a possibility to see deleted questions
+
+ meta_data = {}
+
+ if search_query:
+ if search_state.stripped_query:
+ qs = self.get_for_query(search_state.stripped_query)
+ #a patch for postgres search sort method
+ if askbot.conf.should_show_sort_by_relevance():
+ if sort_method == 'relevance-desc':
+ qs = qs.extra(order_by = ['-relevance',]) # TODO: !! Fix for Postgres
+ if search_state.query_title:
+ qs = qs.filter(title__icontains = search_state.query_title)
+ if search_state.query_tags:
+ qs = qs.filter(tags__name__in = search_state.query_tags)
+ if search_state.query_users:
+ query_users = User.objects.filter(username__in=search_state.query_users)
+ if query_users:
+ qs = qs.filter(posts__post_type__in=('question', 'answer'), posts__author__in=query_users)
+
+ if tag_selector:
+ qs = qs.filter(tags__name__in=tag_selector)
+
+ if scope_selector == 'unanswered':
+ qs = qs.filter(closed = False) # Do not show closed questions in unanswered section
+ if askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS':
+ qs = qs.filter(answer_count=0) # TODO: expand for different meanings of this
+ elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS':
+ qs = qs.filter(accepted_answer__isnull=True)
+ elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_UPVOTED_ANSWERS':
+ raise NotImplementedError()
+ else:
+ raise Exception('UNANSWERED_QUESTION_MEANING setting is wrong')
+
+ elif scope_selector == 'favorite':
+ favorite_filter = models.Q(favorited_by=request_user)
+ if 'followit' in settings.INSTALLED_APPS:
+ followed_users = request_user.get_followed_users()
+ favorite_filter |= models.Q(posts__post_type__in=('question', 'answer'), posts__author__in=followed_users)
+ qs = qs.filter(favorite_filter)
+
+ #user contributed questions & answers
+ if author_selector:
+ try:
+ # TODO: maybe support selection by multiple authors
+ u = User.objects.get(id=int(author_selector))
+ except User.DoesNotExist:
+ meta_data['author_name'] = None
+ else:
+ qs = qs.filter(posts__post_type__in=('question', 'answer'), posts__author=u, posts__deleted=False)
+ meta_data['author_name'] = u.username
+
+ #get users tag filters
+ ignored_tag_names = None
+ if request_user and request_user.is_authenticated():
+ #mark questions tagged with interesting tags
+ #a kind of fancy annotation, would be nice to avoid it
+ interesting_tags = Tag.objects.filter(user_selections__user=request_user, user_selections__reason='good')
+ ignored_tags = Tag.objects.filter(user_selections__user=request_user, user_selections__reason='bad')
+
+ meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags]
+
+ ignored_tag_names = [tag.name for tag in ignored_tags]
+ meta_data['ignored_tag_names'] = ignored_tag_names
+
+ if request_user.display_tag_filter_strategy == const.INCLUDE_INTERESTING and (interesting_tags or request_user.has_interesting_wildcard_tags()):
+ #filter by interesting tags only
+ interesting_tag_filter = models.Q(tags__in=interesting_tags)
+ if request_user.has_interesting_wildcard_tags():
+ interesting_wildcards = request_user.interesting_tags.split()
+ extra_interesting_tags = Tag.objects.get_by_wildcards(interesting_wildcards)
+ interesting_tag_filter |= models.Q(tags__in=extra_interesting_tags)
+ qs = qs.filter(interesting_tag_filter)
+
+ # get the list of interesting and ignored tags (interesting_tag_names, ignored_tag_names) = (None, None)
+ if request_user.display_tag_filter_strategy == const.EXCLUDE_IGNORED and (ignored_tags or request_user.has_ignored_wildcard_tags()):
+ #exclude ignored tags if the user wants to
+ qs = qs.exclude(tags__in=ignored_tags)
+ if request_user.has_ignored_wildcard_tags():
+ ignored_wildcards = request_user.ignored_tags.split()
+ extra_ignored_tags = Tag.objects.get_by_wildcards(ignored_wildcards)
+ qs = qs.exclude(tags__in = extra_ignored_tags)
+
+ ###
+ qs_thread = qs.distinct()
+ ###
+
+ qs = Question.objects.filter(thread__in=qs_thread) # HACH: GO BACK To QUESTIONS
+ qs = qs.select_related('thread__last_activity_by')
+
+ if sort_method != 'relevance-desc':
+ #relevance sort is set in the extra statement
+ #only for postgresql
+ #todo: too bad keys are duplicated see const sort methods
+ QUESTION_ORDER_BY_MAP = {
+ 'age-desc': '-added_at', # TODO: !! Question.added_at
+ 'age-asc': 'added_at', # TODO: !! Question.added_at
+ 'activity-desc': '-thread__last_activity_at',
+ 'activity-asc': 'thread__last_activity_at',
+ 'answers-desc': '-thread__answer_count',
+ 'answers-asc': 'thread__answer_count',
+ 'votes-desc': '-score', # TODO: !! Question.score
+ 'votes-asc': 'score', # TODO: !! Question.score
+ #'relevance-desc': None #this is a special case for postges only
+ }
+ orderby = QUESTION_ORDER_BY_MAP[sort_method]
+ qs = qs.order_by(orderby)
+
+ related_tags = Tag.objects.get_related_to_search(questions = qs, search_state = search_state, ignored_tag_names = ignored_tag_names) # TODO: !!
+
+ if askbot_settings.USE_WILDCARD_TAGS and request_user.is_authenticated():
+ meta_data['interesting_tag_names'].extend(request_user.interesting_tags.split())
+ meta_data['ignored_tag_names'].extend(request_user.ignored_tags.split())
+
+ return qs, meta_data, related_tags
+
class Thread(models.Model):
title = models.CharField(max_length=300)
@@ -147,6 +301,9 @@ class Thread(models.Model):
def _question(self):
return Question.objects.get(thread=self)
+ def _question_post(self):
+ return Post.objects.get(thread=self)
+
def get_absolute_url(self):
return self._question().get_absolute_url()
@@ -217,18 +374,18 @@ class Thread(models.Model):
)
- def get_similarity(self, other_question = None):
+ def get_similarity(self, other_thread = None):
"""return number of tags in the other question
that overlap with the current question (self)
"""
- my_tags = set(self._question().get_tag_names())
- others_tags = set(other_question.get_tag_names())
+ my_tags = set(self.get_tag_names())
+ others_tags = set(other_thread.get_tag_names())
return len(my_tags & others_tags)
- def get_similar_questions(self):
+ def get_similar_threads(self):
"""
- Get 10 similar questions for given one.
- Questions with the individual tags will be added to list if above questions are not full.
+ Get 10 similar threads for given one.
+ Threads with the individual tags will be added to list if above questions are not full.
This function has a limitation that it will
retrieve only 100 records then select 10 most similar
@@ -238,18 +395,17 @@ class Thread(models.Model):
"""
def get_data():
- thread_question = self._question()
-
tags_list = self.tags.all()
- similar_questions = Question.objects.filter(thread__tags__in=tags_list).\
- exclude(id = self.id).exclude(deleted = True).distinct()[:100]
+ similar_threads = Thread.objects.filter(tags__in=tags_list).\
+ exclude(id = self.id).exclude(posts__post_type='question', posts__deleted = True).distinct()[:100]
+ similar_threads = list(similar_threads)
+
+ for thread in similar_threads:
+ thread.similarity = self.get_similarity(other_thread=thread)
- similar_questions = list(similar_questions)
- for question in similar_questions:
- question.similarity = self.get_similarity(other_question=question)
+ similar_threads.sort(key=operator.attrgetter('similarity'), reverse=True)
- similar_questions.sort(key=operator.attrgetter('similarity'), reverse=True)
- return similar_questions[:10]
+ return similar_threads[:10]
return LazyList(get_data)
@@ -389,30 +545,6 @@ class Thread(models.Model):
return FavoriteQuestion.objects.filter(thread=self, user=user).exists()
def get_last_update_info(self):
-# thread_question = self._question()
-#
-# when, who = thread_question.post_get_last_update_info()
-#
-# answers = thread_question.answers.all()
-# for a in answers:
-# a_when, a_who = a.post_get_last_update_info()
-# if a_when > when:
-# when = a_when
-# who = a_who
-#
-# return when, who
-
- # INFO: "CASE" is supported by all databases:
- # - http://dev.mysql.com/doc/refman/5.0/en/control-flow-functions.html#operator_case
- # - http://www.sqlite.org/lang_expr.html ("The CASE expression")
- # - http://www.postgresql.org/docs/8.2/static/functions-conditional.html
- # But the problem is that `extra_last_updated_at` is returned as a string which can differ depedning on the backend,
- # version etc. so for now let's do it manually
-# posts = self.posts.extra(select={
-# 'extra_last_updated_at': 'CASE WHEN added_at > last_edited_at THEN added_at ELSE last_edited_at END',
-# 'extra_last_updated_by_id': 'CASE WHEN added_at > last_edited_at THEN author_id ELSE last_edited_by_id END',
-# }).order_by('-extra_last_updated_at')
-
posts = self.posts.all()
last_updated_at = posts[0].added_at
@@ -462,211 +594,213 @@ class QuestionQuerySet(models.query.QuerySet):
#fallback to dumb title match search
return self.filter(thread__title__icontains=search_query)
- def run_advanced_search(
- self,
- request_user = None,
- search_state = None
- ):
- """all parameters are guaranteed to be clean
- however may not relate to database - in that case
- a relvant filter will be silently dropped
- """
- #todo: same as for get_by_text_query - goes to Tread
- scope_selector = getattr(
- search_state,
- 'scope',
- const.DEFAULT_POST_SCOPE
- )
-
- search_query = search_state.query
- tag_selector = search_state.tags
- author_selector = search_state.author
-
- sort_method = getattr(
- search_state,
- 'sort',
- const.DEFAULT_POST_SORT_METHOD
- )
- qs = self.filter(deleted=False)#todo - add a possibility to see deleted questions
-
- #return metadata
- meta_data = {}
- if search_query:
- if search_state.stripped_query:
- qs = qs.get_by_text_query(search_state.stripped_query)
- #a patch for postgres search sort method
- if askbot.conf.should_show_sort_by_relevance():
- if sort_method == 'relevance-desc':
- qs = qs.extra(order_by = ['-relevance',])
- if search_state.query_title:
- qs = qs.filter(thread__title__icontains = search_state.query_title)
- if len(search_state.query_tags) > 0:
- qs = qs.filter(thread__tags__name__in = search_state.query_tags)
- if len(search_state.query_users) > 0:
- query_users = list()
- for username in search_state.query_users:
- try:
- user = User.objects.get(username__iexact = username)
- query_users.append(user)
- except User.DoesNotExist:
- pass
- if len(query_users) > 0:
- qs = qs.filter(author__in = query_users)
-
- if tag_selector:
- for tag in tag_selector:
- qs = qs.filter(thread__tags__name = tag)
-
-
- #have to import this at run time, otherwise there
- #a circular import dependency...
- from askbot.conf import settings as askbot_settings
- if scope_selector:
- if scope_selector == 'unanswered':
- qs = qs.filter(thread__closed = False)#do not show closed questions in unanswered section
- if askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS':
- qs = qs.filter(thread__answer_count=0)#todo: expand for different meanings of this
- elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS':
- qs = qs.filter(thread__accepted_answer__isnull=True) #answer_accepted=False
- elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_UPVOTED_ANSWERS':
- raise NotImplementedError()
- else:
- raise Exception('UNANSWERED_QUESTION_MEANING setting is wrong')
- elif scope_selector == 'favorite':
- favorite_filter = models.Q(thread__favorited_by = request_user)
- if 'followit' in settings.INSTALLED_APPS:
- followed_users = request_user.get_followed_users()
- favorite_filter |= models.Q(author__in = followed_users)
- favorite_filter |= models.Q(answers__author__in = followed_users)
- qs = qs.filter(favorite_filter)
-
- #user contributed questions & answers
- if author_selector:
- try:
- #todo maybe support selection by multiple authors
- u = User.objects.get(id=int(author_selector))
- qs = qs.filter(
- models.Q(author=u, deleted=False) \
- | models.Q(answers__author=u, answers__deleted=False)
- )
- meta_data['author_name'] = u.username
- except User.DoesNotExist:
- meta_data['author_name'] = None
-
- #get users tag filters
- ignored_tag_names = None
- if request_user and request_user.is_authenticated():
- uid_str = str(request_user.id)
- #mark questions tagged with interesting tags
- #a kind of fancy annotation, would be nice to avoid it
- interesting_tags = Tag.objects.filter(
- user_selections__user=request_user,
- user_selections__reason='good'
- )
- ignored_tags = Tag.objects.filter(
- user_selections__user=request_user,
- user_selections__reason='bad'
- )
-
- meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags]
-
- ignored_tag_names = [tag.name for tag in ignored_tags]
- meta_data['ignored_tag_names'] = ignored_tag_names
-
- if interesting_tags or request_user.has_interesting_wildcard_tags():
- #expensive query
- if request_user.display_tag_filter_strategy == \
- const.INCLUDE_INTERESTING:
- #filter by interesting tags only
- interesting_tag_filter = models.Q(thread__tags__in = interesting_tags)
- if request_user.has_interesting_wildcard_tags():
- interesting_wildcards = request_user.interesting_tags.split()
- extra_interesting_tags = Tag.objects.get_by_wildcards(
- interesting_wildcards
- )
- interesting_tag_filter |= models.Q(thread__tags__in = extra_interesting_tags)
-
- qs = qs.filter(interesting_tag_filter)
- else:
- pass
- #simply annotate interesting questions
-# qs = qs.extra(
-# select = SortedDict([
-# (
-# # TODO: [tags] Update this query so that it fetches tags from Thread
-# 'interesting_score',
-# 'SELECT COUNT(1) FROM askbot_markedtag, question_tags '
-# + 'WHERE askbot_markedtag.user_id = %s '
-# + 'AND askbot_markedtag.tag_id = question_tags.tag_id '
-# + 'AND askbot_markedtag.reason = \'good\' '
-# + 'AND question_tags.question_id = question.id'
-# ),
-# ]),
-# select_params = (uid_str,),
-# )
-
- # get the list of interesting and ignored tags (interesting_tag_names, ignored_tag_names) = (None, None)
-
- if ignored_tags or request_user.has_ignored_wildcard_tags():
- if request_user.display_tag_filter_strategy == const.EXCLUDE_IGNORED:
- #exclude ignored tags if the user wants to
- qs = qs.exclude(thread__tags__in=ignored_tags)
- if request_user.has_ignored_wildcard_tags():
- ignored_wildcards = request_user.ignored_tags.split()
- extra_ignored_tags = Tag.objects.get_by_wildcards(
- ignored_wildcards
- )
- qs = qs.exclude(thread__tags__in = extra_ignored_tags)
- else:
- pass
-# #annotate questions tagged with ignored tags
-# #expensive query
-# qs = qs.extra(
-# select = SortedDict([
-# (
-# 'ignored_score',
-# # TODO: [tags] Update this query so that it fetches tags from Thread
-# 'SELECT COUNT(1) '
-# + 'FROM askbot_markedtag, question_tags '
-# + 'WHERE askbot_markedtag.user_id = %s '
-# + 'AND askbot_markedtag.tag_id = question_tags.tag_id '
-# + 'AND askbot_markedtag.reason = \'bad\' '
-# + 'AND question_tags.question_id = question.id'
-# )
-# ]),
-# select_params = (uid_str, )
-# )
-
- 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(
- 'thread__last_activity_by__id',
- 'thread__last_activity_by__username',
- 'thread__last_activity_by__reputation',
- 'thread__last_activity_by__gold',
- 'thread__last_activity_by__silver',
- 'thread__last_activity_by__bronze',
- 'thread__last_activity_by__country',
- 'thread__last_activity_by__show_country',
- )
-
- related_tags = Tag.objects.get_related_to_search(
- questions = qs,
- search_state = search_state,
- ignored_tag_names = ignored_tag_names
- )
- if askbot_settings.USE_WILDCARD_TAGS == True \
- and request_user.is_authenticated() == True:
- tagnames = request_user.interesting_tags
- meta_data['interesting_tag_names'].extend(tagnames.split())
- tagnames = request_user.ignored_tags
- meta_data['ignored_tag_names'].extend(tagnames.split())
- return qs, meta_data, related_tags
+# def run_advanced_search(
+# self,
+# request_user = None,
+# search_state = None
+# ):
+# """all parameters are guaranteed to be clean
+# however may not relate to database - in that case
+# a relvant filter will be silently dropped
+# """
+# #todo: same as for get_by_text_query - goes to Tread
+# scope_selector = getattr(
+# search_state,
+# 'scope',
+# const.DEFAULT_POST_SCOPE
+# )
+#
+# search_query = search_state.query
+# tag_selector = search_state.tags
+# author_selector = search_state.author
+#
+# import ipdb; ipdb.set_trace()
+#
+# sort_method = getattr(
+# search_state,
+# 'sort',
+# const.DEFAULT_POST_SORT_METHOD
+# )
+# qs = self.filter(deleted=False)#todo - add a possibility to see deleted questions
+#
+# #return metadata
+# meta_data = {}
+# if search_query:
+# if search_state.stripped_query:
+# qs = qs.get_by_text_query(search_state.stripped_query)
+# #a patch for postgres search sort method
+# if askbot.conf.should_show_sort_by_relevance():
+# if sort_method == 'relevance-desc':
+# qs = qs.extra(order_by = ['-relevance',])
+# if search_state.query_title:
+# qs = qs.filter(thread__title__icontains = search_state.query_title)
+# if len(search_state.query_tags) > 0:
+# qs = qs.filter(thread__tags__name__in = search_state.query_tags)
+# if len(search_state.query_users) > 0:
+# query_users = list()
+# for username in search_state.query_users:
+# try:
+# user = User.objects.get(username__iexact = username)
+# query_users.append(user)
+# except User.DoesNotExist:
+# pass
+# if len(query_users) > 0:
+# qs = qs.filter(author__in = query_users)
+#
+# if tag_selector:
+# for tag in tag_selector:
+# qs = qs.filter(thread__tags__name = tag)
+#
+#
+# #have to import this at run time, otherwise there
+# #a circular import dependency...
+# from askbot.conf import settings as askbot_settings
+# if scope_selector:
+# if scope_selector == 'unanswered':
+# qs = qs.filter(thread__closed = False)#do not show closed questions in unanswered section
+# if askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS':
+# qs = qs.filter(thread__answer_count=0)#todo: expand for different meanings of this
+# elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS':
+# qs = qs.filter(thread__accepted_answer__isnull=True) #answer_accepted=False
+# elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_UPVOTED_ANSWERS':
+# raise NotImplementedError()
+# else:
+# raise Exception('UNANSWERED_QUESTION_MEANING setting is wrong')
+# elif scope_selector == 'favorite':
+# favorite_filter = models.Q(thread__favorited_by = request_user)
+# if 'followit' in settings.INSTALLED_APPS:
+# followed_users = request_user.get_followed_users()
+# favorite_filter |= models.Q(author__in = followed_users)
+# favorite_filter |= models.Q(answers__author__in = followed_users)
+# qs = qs.filter(favorite_filter)
+#
+# #user contributed questions & answers
+# if author_selector:
+# try:
+# #todo maybe support selection by multiple authors
+# u = User.objects.get(id=int(author_selector))
+# qs = qs.filter(
+# models.Q(author=u, deleted=False) \
+# | models.Q(answers__author=u, answers__deleted=False)
+# )
+# meta_data['author_name'] = u.username
+# except User.DoesNotExist:
+# meta_data['author_name'] = None
+#
+# #get users tag filters
+# ignored_tag_names = None
+# if request_user and request_user.is_authenticated():
+# uid_str = str(request_user.id)
+# #mark questions tagged with interesting tags
+# #a kind of fancy annotation, would be nice to avoid it
+# interesting_tags = Tag.objects.filter(
+# user_selections__user=request_user,
+# user_selections__reason='good'
+# )
+# ignored_tags = Tag.objects.filter(
+# user_selections__user=request_user,
+# user_selections__reason='bad'
+# )
+#
+# meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags]
+#
+# ignored_tag_names = [tag.name for tag in ignored_tags]
+# meta_data['ignored_tag_names'] = ignored_tag_names
+#
+# if interesting_tags or request_user.has_interesting_wildcard_tags():
+# #expensive query
+# if request_user.display_tag_filter_strategy == \
+# const.INCLUDE_INTERESTING:
+# #filter by interesting tags only
+# interesting_tag_filter = models.Q(thread__tags__in = interesting_tags)
+# if request_user.has_interesting_wildcard_tags():
+# interesting_wildcards = request_user.interesting_tags.split()
+# extra_interesting_tags = Tag.objects.get_by_wildcards(
+# interesting_wildcards
+# )
+# interesting_tag_filter |= models.Q(thread__tags__in = extra_interesting_tags)
+#
+# qs = qs.filter(interesting_tag_filter)
+# else:
+# pass
+# #simply annotate interesting questions
+## qs = qs.extra(
+## select = SortedDict([
+## (
+## # TODO: [tags] Update this query so that it fetches tags from Thread
+## 'interesting_score',
+## 'SELECT COUNT(1) FROM askbot_markedtag, question_tags '
+## + 'WHERE askbot_markedtag.user_id = %s '
+## + 'AND askbot_markedtag.tag_id = question_tags.tag_id '
+## + 'AND askbot_markedtag.reason = \'good\' '
+## + 'AND question_tags.question_id = question.id'
+## ),
+## ]),
+## select_params = (uid_str,),
+## )
+#
+# # get the list of interesting and ignored tags (interesting_tag_names, ignored_tag_names) = (None, None)
+#
+# if ignored_tags or request_user.has_ignored_wildcard_tags():
+# if request_user.display_tag_filter_strategy == const.EXCLUDE_IGNORED:
+# #exclude ignored tags if the user wants to
+# qs = qs.exclude(thread__tags__in=ignored_tags)
+# if request_user.has_ignored_wildcard_tags():
+# ignored_wildcards = request_user.ignored_tags.split()
+# extra_ignored_tags = Tag.objects.get_by_wildcards(
+# ignored_wildcards
+# )
+# qs = qs.exclude(thread__tags__in = extra_ignored_tags)
+# else:
+# pass
+## #annotate questions tagged with ignored tags
+## #expensive query
+## qs = qs.extra(
+## select = SortedDict([
+## (
+## 'ignored_score',
+## # TODO: [tags] Update this query so that it fetches tags from Thread
+## 'SELECT COUNT(1) '
+## + 'FROM askbot_markedtag, question_tags '
+## + 'WHERE askbot_markedtag.user_id = %s '
+## + 'AND askbot_markedtag.tag_id = question_tags.tag_id '
+## + 'AND askbot_markedtag.reason = \'bad\' '
+## + 'AND question_tags.question_id = question.id'
+## )
+## ]),
+## select_params = (uid_str, )
+## )
+#
+# 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(
+# 'thread__last_activity_by__id',
+# 'thread__last_activity_by__username',
+# 'thread__last_activity_by__reputation',
+# 'thread__last_activity_by__gold',
+# 'thread__last_activity_by__silver',
+# 'thread__last_activity_by__bronze',
+# 'thread__last_activity_by__country',
+# 'thread__last_activity_by__show_country',
+# )
+#
+# related_tags = Tag.objects.get_related_to_search(
+# questions = qs,
+# search_state = search_state,
+# ignored_tag_names = ignored_tag_names
+# )
+# if askbot_settings.USE_WILDCARD_TAGS == True \
+# and request_user.is_authenticated() == True:
+# tagnames = request_user.interesting_tags
+# meta_data['interesting_tag_names'].extend(tagnames.split())
+# tagnames = request_user.ignored_tags
+# meta_data['ignored_tag_names'].extend(tagnames.split())
+# return qs, meta_data, related_tags
def added_between(self, start, end):
"""questions added between ``start`` and ``end`` timestamps"""
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index f7005d3c..29b2fe0f 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -132,25 +132,6 @@ poor design of the data or methods on data objects #}
{% endif %}
{%- endmacro -%}
-{%- macro post_last_updater_and_creator_info(post, min_rep_to_edit_wiki) -%}
- {{
- post_contributor_info(
- post,
- "original_author",
- post.wiki,
- min_rep_to_edit_wiki
- )
- }}
- {{
- post_contributor_info(
- post,
- "last_updater",
- post.wiki,
- min_rep_to_edit_wiki,
- )
- }}
-{%- endmacro -%}
-
{%- macro if_else(condition, if_true, if_false) -%}
{%- if condition == True -%}
{{if_true}}
@@ -252,43 +233,9 @@ poor design of the data or methods on data objects #}
{%include "widgets/question_summary.html" %}
{%- endmacro -%}
-{%- macro comment_votes(comment = None) -%}
- <div class="comment-votes">
- {% if comment.score > 0 %}
- <div class="upvote{% if comment.upvoted_by_user %} upvoted{% endif %}">{{comment.score}}</div>
- {% else %}
- <div class="upvote"></div>
- {% endif %}
- </div>
-{%- endmacro -%}
-
{# Warning! Any changes to the comment markup here must be duplicated in post.js
for the purposes of the AJAX comment editor #}
-{%- macro comment_list(comments = None, user = None) -%}
- {% for comment in comments %}
- <div class="comment" id="comment-{{comment.id}}">
- {{ comment_votes(comment = comment) }}
- <div class="comment-delete">
- {% if user|can_delete_comment(comment) %}
- <span class="delete-icon" title="{% trans %}delete this comment{% endtrans %}"></span>
- {% endif %}
- </div>
- <div class="comment-body">
- {{comment.html}}
- <a
- class="author"
- href="{{comment.user.get_profile_url()}}"
- >{{comment.user.username}}</a>
- <span class="age">&nbsp;({{comment.added_at|diff_date}})</span>
- {% if user|can_edit_comment(comment) %}
- &nbsp;<a class="edit">{% trans %}edit{% endtrans %}</a>
- {% endif %}
- </div>
- </div>
- {% endfor %}
-{%- endmacro -%}
-
{%- macro add_or_show_comments_button(post = None, can_post = None, max_comments = None, widget_id = None) -%}
<script type="text/javascript">
askbot['data']['{{widget_id}}'] = {
@@ -328,18 +275,37 @@ for the purposes of the AJAX comment editor #}
{% set widget_id = 'comments-for-' + post.post_type + '-' + post.id|string %}
<div class="comments" id="{{widget_id}}">
<div class="content">
- {% if show_post == post and show_comment %}
- {% if show_comment_position > max_comments %}
- {% set comments = post.get_comments(visitor = user)[:show_comment_position] %}
- {{ comment_list(comments = comments, user = user) }}
- {% else %}
- {% set comments = post.get_comments(visitor = user)[:max_comments] %}
- {{ comment_list(comments = comments, user = user) }}
- {% endif %}
+ {% if show_post == post and show_comment and show_comment_position > max_comments %}
+ {% set comments = post.get_comments(visitor = user)[:show_comment_position] %}
{% else %}
{% set comments = post.get_comments(visitor = user)[:max_comments] %}
- {{ comment_list(comments = comments, user = user) }}
{% endif %}
+ {% for comment in comments %}
+ {# Warning! Any changes to the comment markup IN THIS `FOR` LOOP must be duplicated in post.js
+ for the purposes of the AJAX comment editor #}
+ <div class="comment" id="comment-{{comment.id}}">
+ <div class="comment-votes">
+ {% if comment.score > 0 %}
+ <div class="upvote{% if comment.upvoted_by_user %} upvoted{% endif %}">{{comment.score}}</div>
+ {% else %}
+ <div class="upvote"></div>
+ {% endif %}
+ </div>
+ <div class="comment-delete">
+ {% if user|can_delete_comment(comment) %}
+ <span class="delete-icon" title="{% trans %}delete this comment{% endtrans %}"></span>
+ {% endif %}
+ </div>
+ <div class="comment-body">
+ {{comment.html}}
+ <a class="author" href="{{comment.user.get_profile_url()}}">{{comment.user.username}}</a>
+ <span class="age">&nbsp;({{comment.added_at|diff_date}})</span>
+ {% if user|can_edit_comment(comment) %}
+ &nbsp;<a class="edit">{% trans %}edit{% endtrans %}</a>
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
</div>
<div class="controls">
{% set can_post = user|can_post_comment(post) %}
diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html
index 7dc85d84..6fff53b0 100644
--- a/askbot/skins/default/templates/question.html
+++ b/askbot/skins/default/templates/question.html
@@ -1,16 +1,368 @@
{% extends "two_column_body.html" %}
<!-- question.html -->
-{% block title %}{% spaceless %}{{ question.get_question_title() }}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ thread.get_title() }}{% endspaceless %}{% endblock %}
{% block meta_description %}
<meta name="description" content="{{question.summary|striptags|escape}}" />
{% endblock %}
-{% block keywords %}{{question.tagname_meta_generator()}}{% endblock %}
+{% block keywords %}{{thread.tagname_meta_generator()}}{% endblock %}
{% block forestyle %}
- <link rel="canonical" href="{{settings.APP_URL}}{{question.get_absolute_url()}}" />
+ <link rel="canonical" href="{{settings.APP_URL}}{{thread.get_absolute_url()}}" />
<link rel="stylesheet" type="text/css" href="{{'/js/wmd/wmd.css'|media}}" />
{% endblock %}
{% block content %}
- {%include "question/content.html" %}
+ {# ==== BEGIN: question/content.html ==== #}
+ {% import "macros.html" as macros %}
+ {# ==== BEGIN: question/question_card.html ==== #}
+ <div class="vote-buttons">
+ {# ==== BEGIN: question/question_vote_buttons.html ==== #}
+ {{ macros.post_vote_buttons(post = question.self_question, visitor_vote = user_question_vote) }}
+ {# ==== END: question/question_vote_buttons.html ==== #}
+ {# ==== BEGIN: question/share_buttons.html ==== #}
+ {% if settings.ENABLE_SHARING_TWITTER %}{{ macros.share(site = 'twitter', icon = True) }}{% endif %}
+ {% if settings.ENABLE_SHARING_FACEBOOK %}{{ macros.share(site = 'facebook', icon = True) }}{% endif %}
+ {% if settings.ENABLE_SHARING_LINKEDIN %}{{ macros.share(site = 'linkedin', icon = True) }}{% endif %}
+ {% if settings.ENABLE_SHARING_IDENTICA %}{{ macros.share(site = 'identica', icon = True) }}{% endif %}
+ {% if settings.ENABLE_SHARING_GOOGLE %}<g:plusone size="small" count="false"></g:plusone>{% endif %}
+ {# ==== END: question/share_buttons.html ==== #}
+ </div>
+ <div class="question-content">
+
+ <h1><a href="{{ thread.get_absolute_url() }}">{{ thread.get_title() }}</a></h1>
+ {# ==== START: question/question_tags.html" #}
+ {{ macros.tag_list_widget(
+ tags = thread.get_tag_names(),
+ id = 'question-tags',
+ css_class = 'post-tags tags',
+ tag_css_class = 'post-tag',
+ )
+ }}
+ {# ==== END: question/question_tags.html" #}
+
+ <div id="question-table" {% if question.deleted %}class="deleted"{%endif%}>
+ <div class="question-body">
+ <div class="post-update-info-container">
+ {%- macro post_last_updater_and_creator_info(post, min_rep_to_edit_wiki) -%} {# INFO: Unrolled macro from macros.html #}
+ {{ macros.post_contributor_info(post, "original_author", post.wiki, min_rep_to_edit_wiki) }}
+ {{ macros.post_contributor_info(post, "last_updater", post.wiki, min_rep_to_edit_wiki) }}
+ {%- endmacro -%}
+ {{ post_last_updater_and_creator_info(question.self_question, settings.MIN_REP_TO_EDIT_WIKI) }}
+ {# ==== START: "question/question_author_info.html"
+ {{ macros.post_last_updater_and_creator_info(question.self_question, settings.MIN_REP_TO_EDIT_WIKI) }}
+ ==== END: "question/question_author_info.html" #}
+ </div>
+ {{ question.html }}
+ </div>
+ <div id="question-controls" class="post-controls">
+ {# ==== START: include "question/question_controls.html" #}
+ {% set pipe=joiner('<span class="sep">|</span>') %}
+ {% if request.user|can_edit_post(question.self_question) %}{{ pipe() }}
+ <a class="question-edit" href="{% url edit_question question.self_question_id %}">{% trans %}edit{% endtrans %}</a>
+ {% endif %}
+ {% if request.user|can_retag_question(question.self_question) %}{{ pipe() }}
+ <a id="retag" class="question-retag"href="{% url retag_question question.self_question_id %}">{% trans %}retag{% endtrans %}</a>
+ <script type="text/javascript">
+ var retagUrl = "{% url retag_question question.self_question_id %}";
+ </script>
+ {% endif %}
+ {% if thread.closed %}
+ {% if request.user|can_reopen_question(question.self_question) %}{{ pipe() }}
+ <a class="question-close" href="{% url reopen question.self_question_id %}">{% trans %}reopen{% endtrans %}</a>
+ {% endif %}
+ {% else %}
+ {% if request.user|can_close_question(question.self_question) %}{{ pipe() }}
+ <a class="question-close" href="{% url close question.self_question_id %}">{% trans %}close{% endtrans %}</a>
+ {% endif %}
+ {% endif %}
+ {% if request.user|can_flag_offensive(question.self_question) %}{{ pipe() }}
+ <span id="question-offensive-flag-{{ question.self_question_id }}" class="offensive-flag"
+ title="{% trans %}report as offensive (i.e containing spam, advertising, malicious text, etc.){% endtrans %}">
+ <a class="question-flag">{% trans %}flag offensive{% endtrans %}</a>
+ {% if request.user|can_see_offensive_flags(question.self_question) %}
+ <span class="darkred">{% if question.offensive_flag_count > 0 %}({{ question.offensive_flag_count }}){% endif %}</span>
+ {% endif %}
+ </span>
+ {% elif request.user|can_remove_flag_offensive(question.self_question)%}{{ pipe() }}
+ <span id="question-offensive-flag-remove-{{ question.self_question_id }}" class="offensive-flag"
+ title="{% trans %}report as offensive (i.e containing spam, advertising, malicious text, etc.){% endtrans %}">
+ <a class="question-flag">{% trans %}remove flag{% endtrans %}</a>
+ {% if request.user|can_see_offensive_flags(question.self_question) %}
+ <span class="darkred">{% if question.offensive_flag_count > 0 %}({{ question.offensive_flag_count }}){% endif %}</span>
+ {% endif %}
+ </span>
+ {% endif %}
+ {% if request.user|can_delete_post(question.self_question) %}{{ pipe() }}
+ <a id="question-delete-link-{{question.self_question_id}}" class="question-delete">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}{% trans %}delete{% endtrans %}{% endif %}</a>
+ {% endif %}
+ {# ==== END: include "question/question_controls.html" #}
+ </div>
+ {# ==== START: question/question_comments.html ==== #}
+ {{
+ macros.post_comments_widget(
+ post = question.self_question,
+ show_post = show_post.self_answer,
+ show_comment = show_comment.self_comment,
+ show_comment_position = show_comment_position,
+ user = request.user,
+ max_comments = settings.MAX_COMMENTS_TO_SHOW
+ )
+ }}
+ {# ==== END: question/question_comments.html ==== #}
+ </div>
+ </div>
+ <div class="clean"></div>
+ {# ==== END: question/question_card.html ==== #}
+ {% if thread.closed %}
+ {# ==== START: question/closed_question_info.html ==== #}
+ <div class="question-status">
+ <h3>{% trans close_reason=thread.get_close_reason_display() %}The question has been closed for the following reason <b>"{{ close_reason }}"</b> <i>by{% endtrans %}
+ <a href="{{ thread.closed_by.get_profile_url() }}">{{ thread.closed_by.username }}</a> </i><br>
+ {% trans closed_at=thread.closed_at %}close date {{closed_at}}{% endtrans %}</h3>
+ </div>
+ {# ==== END: question/closed_question_info.html ==== #}
+ {% endif %}
+ {% if answers %}
+ {# ==== START: question/answer_tab_bar.html ==== #}
+ <div class="tabBar tabBar-answer">
+ <h2 id="questionCount">
+ {% trans counter=answers|length %}
+ {{counter}} Answer
+ {% pluralize %}
+ {{counter}} Answers
+ {% endtrans %}
+ </h2>
+ <div class="tabsA">
+ <span class="label">
+ Sort by ยป
+ </span>
+ <a id="oldest" href="{{ thread.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"
+ 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"
+ title="{% trans %}most voted answers will be shown first{% endtrans %}"
+ ><span>{% trans %}popular answers{% endtrans %}</span></a>
+ </div>
+ </div>
+ <div class="clean"></div>
+ {# ==== END: question/answer_tab_bar.html ==== #}
+
+ {{ macros.paginator(paginator_context) }}
+ <div class="clean"></div>
+ {% for answer in answers %}
+ {# ==== START: question/answer_card.html ==== #}
+ <a name="{{ answer.self_answer_id }}"></a>
+ <div
+ id="answer-container-{{ answer.self_answer_id }}"
+ class="{{ macros.answer_classes(answer.self_answer, question.self_question) }}">
+ <div class="vote-buttons">
+ {# ==== START: question/answer_vote_buttons.html ==== #}
+ {{ macros.post_vote_buttons(post = answer.self_answer, visitor_vote = user_answer_votes[answer.self_answer_id]) }}
+ {% if request.user == question.author or (request.user.is_authenticated() and (request.user.is_moderator() or request.user.is_administrator())) %}
+ <img id="answer-img-accept-{{ answer.self_answer_id }}" class="answer-img-accept"
+ {% if answer.accepted() %}
+ src="{{'/images/vote-accepted-on.png'|media}}"
+ {% else %}
+ src="{{'/images/vote-accepted.png'|media}}"
+ {% endif %}
+ alt="{% trans %}mark this answer as correct (click again to undo){% endtrans %}"
+ title="{% trans %}mark this answer as correct (click again to undo){% endtrans %}" />
+ {% else %}
+ {% if answer.accepted() %}
+ <img id="answer-img-accept-{{ answer.self_answer_id }}" class="answer-img-accept"
+ {% if answer.accepted() %}
+ src="{{'/images/vote-accepted-on.png'|media}}"
+ {% else %}
+ src="{{'/images/vote-accepted.png'|media}}"
+ {% endif %}
+ alt="{% trans question_author=question.author.username %}{{question_author}} has selected this answer as correct{% endtrans %}"
+ title="{% trans questsion_author=question.author.username%}{{question_author}} has selected this answer as correct{% endtrans %}"
+ />
+ {% endif %}
+ {% endif %}
+ {# ==== END: question/answer_vote_buttons.html ==== #}
+ </div>
+ <div class="answer-table">
+
+ <div class="item-right">
+ <div class="answer-body">
+ <div class="post-update-info-container">
+ {{ post_last_updater_and_creator_info(answer.self_answer, settings.MIN_REP_TO_EDIT_WIKI) }}
+ {# ==== START: question/answer_author_info.html ====
+ {{ macros.post_last_updater_and_creator_info(answer.self_answer, settings.MIN_REP_TO_EDIT_WIKI) }}
+ ==== END: question/answer_author_info.html ==== #}
+ </div>
+ {{ answer.html }}
+ </div>
+ <div class="answer-controls post-controls">
+ {# ==== START: question/answer_controls.html ==== #}
+ {% set pipe=joiner('<span class="sep">|</span>') %}
+ <span class="linksopt">{{ pipe() }}
+ <a class="permant-link"
+ href="{{ answer.get_absolute_url() }}"
+ title="{% trans %}answer permanent link{% endtrans %}">
+ {% trans %}permanent link{% endtrans %}
+ </a>
+ </span>
+ {% if request.user|can_edit_post(answer.self_answer) %}{{ pipe() }}
+ <span class="action-link"><a class="question-edit" href="{% url edit_answer answer.self_answer_id %}">{% trans %}edit{% endtrans %}</a></span>
+ {% endif %}
+ {% if request.user|can_flag_offensive(answer.self_answer) %}{{ pipe() }}
+ <span id="answer-offensive-flag-{{ answer.self_answer_id }}" class="offensive-flag"
+ title="{% trans %}report as offensive (i.e containing spam, advertising, malicious text, etc.){% endtrans %}">
+ <a class="question-flag">{% trans %}flag offensive{% endtrans %}</a>
+ {% if request.user|can_see_offensive_flags(answer.self_answer) %}
+ <span class="darkred">{% if answer.offensive_flag_count > 0 %}({{ answer.offensive_flag_count }}){% endif %}</span>
+ {% endif %}
+ </span>
+ {% elif request.user|can_remove_flag_offensive(answer.self_answer)%}{{ pipe() }}
+ <span id="answer-offensive-flag-remove-{{ answer.self_answer_id }}" class="offensive-flag"
+ title="{% trans %}report as offensive (i.e containing spam, advertising, malicious text, etc.){% endtrans %}">
+ <a class="question-flag">{% trans %}remove flag{% endtrans %}</a>
+ {% if request.user|can_see_offensive_flags(answer.self_answer) %}
+ <span class="darkred">{% if answer.offensive_flag_count > 0 %}({{ answer.offensive_flag_count }}){% endif %}</span>
+ {% endif %}
+ </span>
+ {% endif %}
+ {% if request.user|can_delete_post(answer.self_answer) %}{{ pipe() }}
+ {% spaceless %}
+ <span class="action-link">
+ <a class="question-delete" id="answer-delete-link-{{answer.self_answer_id}}">
+ {% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}{% trans %}delete{% endtrans %}{% endif %}</a>
+ </span>
+ {% endspaceless %}
+ {% endif %}
+ {% if settings.ALLOW_SWAPPING_QUESTION_WITH_ANSWER and request.user.is_authenticated() and request.user.is_administrator_or_moderator() %}{{ pipe() }}
+ <span class="action-link">
+ <a id="swap-question-with-answer-{{answer.self_answer.id}}">{% trans %}swap with question{% endtrans %}</a>
+ </span>
+ {% endif %}
+ {# ==== END: question/answer_controls.html ==== #}
+ </div>
+ {# ==== START: question/answer_comments.html ==== #}
+ {{
+ macros.post_comments_widget(
+ post = answer.self_answer,
+ show_post = show_post.self_answer,
+ show_comment = show_comment.self_comment,
+ show_comment_position = show_comment_position,
+ user = request.user,
+ max_comments = settings.MAX_COMMENTS_TO_SHOW
+ )
+ }}
+ {# ==== END: question/answer_comments.html ==== #}
+ </div>
+ </div>
+ <div class="clean"></div>
+ </div>
+ <div class="clean"></div>
+ {# ==== END: question/answer_card.html ==== #}
+ {% endfor %}
+ {{ macros.paginator(paginator_context) }}
+ <div class="clean"></div>
+ {% else %}
+ {# ==== START: question/sharing_prompt_phrase.html ==== #}
+ {% set question_url=settings.APP_URL+thread.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 %}
+ {% if settings.ENABLE_SHARING_LINKEDIN %}{{ macros.share(site = 'linkedin', site_label = 'LinkedIn') }},{% endif %}
+ {% if settings.ENABLE_SHARING_IDENTICA %}{{ macros.share(site = 'identica', site_label = 'Identi.ca') }},{% endif %}
+ {%- if settings.ENABLE_SHARING_TWITTER or settings.ENABLE_SHARING_FACEBOOK or settings.ENABLE_SHARING_LINKEDIN or settings.ENABLE_SHARING_IDENTICA -%}
+ {% trans %} or{% endtrans %}
+ {% endif %}
+ <a href="mailto:?subject={{ settings.APP_SHORT_NAME|urlencode }}&amp;body={{ question_url }}">{% trans %}email{% endtrans %}</a>.
+ </h2>
+ {# ==== END: question/sharing_prompt_phrase.html ==== #}
+ {% endif %}
+ {# ==== START: question/new_answer_form.html ==== #}
+ <form
+ id="fmanswer"
+ {% if user == question.author %}style="display:none"{% endif %}
+ action="{% url answer question.self_question_id %}"
+ method="post"
+ >{% csrf_token %}
+ {# ==== START: question/subscribe_by_email_prompt.html ==== #}
+ {% if request.user.is_authenticated() %}
+ <p>
+ {{ answer.email_notify }}
+ <label for="question-subscribe-updates">
+ {% set email_feed_frequency = request.user.get_followed_question_alert_frequency() %}
+ {% if email_feed_frequency =='n' %}
+ {% trans %}Notify me once a day when there are any new answers{% endtrans %}
+ {% elif email_feed_frequency =='d' %}
+ {% trans %}Notify me once a day when there are any new answers{% endtrans %}
+ {% elif email_feed_frequency =='w' %}
+ {% trans %}Notify me weekly when there are any new answers{% endtrans %}
+ {% elif email_feed_frequency =='i' %}
+ {% trans %}Notify me immediately when there are any new answers{% endtrans %}
+ {% endif %}
+ </label>
+ {% trans profile_url=request.user.get_profile_url() %}You can always adjust frequency of email updates from your {{profile_url}}{% endtrans %}
+ </p>
+ {% else %}
+ <p>
+ {{ answer.email_notify }}
+ <label>{% trans %}once you sign in you will be able to subscribe for any updates here{% endtrans %}</label>
+ </p>
+ {% endif %}
+ {# ==== END: question/subscribe_by_email_prompt.html ==== #}
+ <div style="clear:both"></div>
+ {% if request.user.is_anonymous() and settings.ALLOW_POSTING_BEFORE_LOGGING_IN == False %}
+ {% if not thread.closed %}
+ <a
+ class="submit"
+ href="{{settings.LOGIN_URL}}?next={% url question question.self_question_id %}"
+ >{% trans %}Login/Signup to Answer{% endtrans %}</a>
+ {% endif %}
+ {% else %}
+ {% if not thread.closed %}
+ <div>
+ {% spaceless %}
+ <h2>
+ {% if answers %}
+ {% trans %}Your answer{% endtrans %}
+ {% else %}
+ {% trans %}Be the first one to answer this question!{% endtrans %}
+ {% endif %}
+ </h2>
+ {% endspaceless %}
+ </div>
+ {% if request.user.is_anonymous() %}
+ <div class="message">{% trans %}you can answer anonymously and then login{% endtrans %}</div>
+ {% else %}
+ <p class="message">
+ {% if request.user==question.author %}
+ {% trans %}answer your own question only to give an answer{% endtrans %}
+ {% else %}
+ {% trans %}please only give an answer, no discussions{% endtrans %}
+ {% endif %}
+ </p>
+ {% endif %}
+ {{ macros.edit_post(answer) }}
+ <input type="submit"
+ {% if user.is_anonymous() %}
+ value="{% trans %}Login/Signup to Post Your Answer{% endtrans %}"
+ {% else %}
+ {% if user == question.author %}
+ value="{% trans %}Answer Your Own Question{% endtrans %}"
+ {% else %}
+ value="{% trans %}Answer the question{% endtrans %}"
+ {% endif %}
+ {% endif %}
+ class="submit after-editor" style="float:left"/>
+ {% if settings.WIKI_ON %}
+ {{ macros.checkbox_in_div(answer.wiki) }}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ </form>
+ {# ==== END: question/new_answer_form.html ==== #}
+ {% if request.user == question.author %}{# this is outside the form on purpose #}
+ <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/javascript.html b/askbot/skins/default/templates/question/javascript.html
index fe96bf9d..0e9162d4 100644
--- a/askbot/skins/default/templates/question/javascript.html
+++ b/askbot/skins/default/templates/question/javascript.html
@@ -1,4 +1,4 @@
-{% if not question.thread.closed %}
+{% if not thread.closed %}
<script type='text/javascript' src='{{"/js/editor.js"|media}}'></script>
<script type='text/javascript'>
{% if settings.ENABLE_MATHJAX or settings.MARKUP_CODE_FRIENDLY %}
@@ -46,9 +46,9 @@
var answer_sort_tab = "{{ tab_id }}";
$("#" + answer_sort_tab).attr('className',"on");
- Vote.init({{ question.id }}, '{{ question.thread.title|slugify }}', '{{ question.author.id }}','{{ request.user.id }}');
+ Vote.init({{ question.self_question_id }}, '{{ thread.title|slugify }}', '{{ question.author.id }}','{{ request.user.id }}');
- {% if not question.thread.closed and request.user.is_authenticated %}initEditor();{% endif %}
+ {% if not thread.closed and request.user.is_authenticated %}initEditor();{% endif %}
lanai.highlightSyntax();
$('#btLogin').bind('click', function(){window.location.href='{{ settings.LOGIN_URL }}'; } )
diff --git a/askbot/skins/default/templates/question/sidebar.html b/askbot/skins/default/templates/question/sidebar.html
index b58a3a98..4266c2c8 100644
--- a/askbot/skins/default/templates/question/sidebar.html
+++ b/askbot/skins/default/templates/question/sidebar.html
@@ -16,7 +16,7 @@
{% endif %}
<div class="clearfix"></div>
<div id="favorite-number" class="favorite-number{% if favorited %} my-favorite-number{% endif %}">
- {% set follower_count = question.thread.favourite_count %}
+ {% set follower_count = thread.favourite_count %}
{% if follower_count > 0 %}
{% trans count=follower_count %}{{count}} follower{% pluralize %}{{count}} followers{% endtrans %}
{% endif %}
@@ -31,13 +31,13 @@
{%endif%}
<p class="rss">
<a
- href="{{settings.APP_URL}}/feeds/question/{{ question.id }}"
+ href="{{settings.APP_URL}}/feeds/question/{{ question.self_question_id }}"
title="{% trans %}subscribe to this question rss feed{% endtrans %}"
>{% trans %}subscribe to rss feed{% endtrans %}</a>
</p>
</div>
</div>
-{% cache 0 "questions_tags" questions_tags question.id language_code %}
+{% cache 0 "questions_tags" questions_tags question.self_question_id language_code %}
{% if settings.SIDEBAR_QUESTION_SHOW_META %}
@@ -48,23 +48,23 @@
{% trans %}question asked{% endtrans %}: <strong title="{{ question.added_at }}">{{question.added_at|diff_date}}</strong>
</p>
<p>
- {% trans %}question was seen{% endtrans %}: <strong>{{ question.thread.view_count|intcomma }} {% trans %}times{% endtrans %}</strong>
+ {% trans %}question was seen{% endtrans %}: <strong>{{ thread.view_count|intcomma }} {% trans %}times{% endtrans %}</strong>
</p>
<p>
- {% trans %}last updated{% endtrans %}: <strong title="{{ question.thread.last_activity_at }}">{{question.thread.last_activity_at|diff_date}}</strong>
+ {% trans %}last updated{% endtrans %}: <strong title="{{ thread.last_activity_at }}">{{thread.last_activity_at|diff_date}}</strong>
</p>
</div>
{% endif %}
{% endcache %}
-{% if similar_questions.data and settings.SIDEBAR_QUESTION_SHOW_RELATED %}
+{% if similar_threads.data and settings.SIDEBAR_QUESTION_SHOW_RELATED %}
{#% cache 1800 "related_questions" related_questions question.id language_code %#}
<div class="box">
<h2>{% trans %}Related questions{% endtrans %}</h2>
<div class="questions-related">
- {% for question in similar_questions.data() %}
+ {% for threads in similar_threads.data() %}
<p>
- <a href="{{ question.get_absolute_url() }}">{{ question.get_question_title() }}</a>
+ <a href="{{ threads.get_absolute_url() }}">{{ threads.get_question_title() }}</a>
</p>
{% endfor %}
</div>
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 0e768c56..d08d9bf2 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -126,7 +126,7 @@ def questions(request, scope=const.DEFAULT_POST_SCOPE, sort=const.DEFAULT_POST_S
#request.session.modified = True
#todo: have this call implemented for sphinx, mysql and pgsql
- (qs, meta_data, related_tags) = models.Question.objects.run_advanced_search(
+ (qs, meta_data, related_tags) = models.Thread.objects.run_advanced_search(
request_user = request.user,
search_state = search_state,
)
@@ -450,11 +450,11 @@ def question(request, id):#refactor - long subroutine. display question body, an
#in addition - if url points to a comment and the comment
#is for the answer - we need the answer object
try:
- show_comment = models.Comment.objects.get(id = show_comment)
- if str(show_comment.get_origin_post().id) != id:
- return HttpResponseRedirect(show_comment.get_absolute_url())
- show_post = show_comment.content_object
- show_comment.assert_is_visible_to(request.user)
+ show_comment = models.Post.objects.get(self_comment=show_comment)
+ if str(show_comment.thread._question_post().self_question_id) != id:
+ return HttpResponseRedirect(show_comment.thread.get_absolute_url())
+ show_post = show_comment.parent
+ show_comment.self_comment.assert_is_visible_to(request.user)
except models.Comment.DoesNotExist:
error_message = _(
'Sorry, the comment you are looking for has been '
@@ -476,8 +476,8 @@ def question(request, id):#refactor - long subroutine. display question body, an
#whether answer is actually corresponding to the current question
#and that the visitor is allowed to see it
try:
- show_post = get_object_or_404(models.Answer, id=show_answer)
- if str(show_post.question.id) != id:
+ show_post = get_object_or_404(models.Post, self_answer=show_answer)
+ if str(show_post.question_id) != id:
return HttpResponseRedirect(show_post.get_absolute_url())
show_post.assert_is_visible_to(request.user)
except django_exceptions.PermissionDenied, error:
@@ -487,7 +487,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
#load question and maybe refuse showing deleted question
try:
question_post = get_object_or_404(models.Post, self_question=id)
- question_post.assert_is_visible_to(request.user)
+ question_post.self_question.assert_is_visible_to(request.user)
except exceptions.QuestionHidden, error:
request.user.message_set.create(message = unicode(error))
return HttpResponseRedirect(reverse('index'))
@@ -498,7 +498,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
if request.path.split('/')[-1] != question_post.slug:
logging.debug('no slug match!')
question_url = '?'.join((
- question.get_absolute_url(),
+ question_post.get_absolute_url(),
urllib.urlencode(request.GET)
))
return HttpResponseRedirect(question_url)
@@ -510,6 +510,14 @@ def question(request, id):#refactor - long subroutine. display question body, an
answers = answers.select_related('self_answer', 'thread', 'author', 'last_edited_by')
answers = answers.order_by({"latest":"-added_at", "oldest":"added_at", "votes":"-score" }[answer_sort_method])
+ # 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] %}
+ #
+
answers = list(answers)
if thread.accepted_answer: # Put the accepted answer to front
@@ -523,17 +531,15 @@ def question(request, id):#refactor - long subroutine. display question body, an
for vote in votes:
user_answer_votes[vote.object_id] = int(vote) # INFO: vote.object_id == Answer.id == Post.self_answer_id
- filtered_answer_posts = [answer for answer in answers if ((not answer.deleted) or (answer.deleted and answer.author_id == request.user.id))]
-
- filtered_answers = [answer.self_answer for answer in filtered_answer_posts] # TODO: for now we convert back to Answer instances
+ filtered_answers = [answer for answer in answers if ((not answer.deleted) or (answer.deleted and answer.author_id == request.user.id))]
#resolve page number and comment number for permalinks
show_comment_position = None
if show_comment:
- show_page = show_comment.get_page_number(answers = filtered_answers)
- show_comment_position = show_comment.get_order_number()
+ show_page = show_comment.get_page_number(answer_posts=filtered_answers)
+ show_comment_position = show_comment.self_comment.get_order_number()
elif show_answer:
- show_page = show_post.get_page_number(answers = filtered_answers)
+ show_page = show_post.get_page_number(answer_posts=filtered_answers)
objects_list = Paginator(filtered_answers, const.ANSWERS_PAGE_SIZE)
if show_page > objects_list.num_pages:
@@ -550,7 +556,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.self_question_id, None)
- updated_when, updated_who = thread.get_last_update_info() # TODO: This is a real resource hog
+ updated_when, updated_who = thread.get_last_update_info()
if updated_who != request.user:
if last_seen:
@@ -603,7 +609,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
data = {
'page_class': 'question-page',
'active_tab': 'questions',
- 'question' : question_post.self_question,
+ 'question' : question_post,
'thread': thread,
'user_question_vote' : user_question_vote,
'question_comment_count': question_post.self_question.comments.count(),
@@ -613,7 +619,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
'tags' : thread.tags.all(),
'tab_id' : answer_sort_method,
'favorited' : favorited,
- 'similar_questions' : thread.get_similar_questions(),
+ 'similar_threads' : thread.get_similar_threads(),
'language_code': translation.get_language(),
'paginator_context' : paginator_context,
'show_post': show_post,
@@ -659,10 +665,9 @@ def get_comment(request):
@ajax_only
@get_only
def get_question_body(request):
- # TODO: Is this used anywhere?
search_state = request.session.get('search_state', SearchState())
view_log = request.session['view_log']
- (qs, meta_data, related_tags) = models.Question.objects.run_advanced_search(
+ (qs, meta_data, related_tags) = models.Thread.objects.run_advanced_search(
request_user = request.user,
search_state = search_state)
paginator = Paginator(qs, search_state.page_size)