diff options
-rw-r--r-- | askbot/models/post.py | 32 | ||||
-rw-r--r-- | askbot/models/question.py | 648 | ||||
-rw-r--r-- | askbot/skins/default/templates/macros.html | 90 | ||||
-rw-r--r-- | askbot/skins/default/templates/question.html | 360 | ||||
-rw-r--r-- | askbot/skins/default/templates/question/javascript.html | 6 | ||||
-rw-r--r-- | askbot/skins/default/templates/question/sidebar.html | 16 | ||||
-rw-r--r-- | askbot/views/readers.py | 47 |
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"> ({{comment.added_at|diff_date}})</span> - {% if user|can_edit_comment(comment) %} - <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"> ({{comment.added_at|diff_date}})</span> + {% if user|can_edit_comment(comment) %} + <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 }}&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) |