From 700b6dec7e4d655fee91e26f9757442162047281 Mon Sep 17 00:00:00 2001 From: hernani Date: Thu, 15 Jul 2010 23:31:46 +0000 Subject: [PATCH] Adds a couple of options to manage the "accepting answers" workflow. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@533 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/actions/meta.py | 6 ++-- forum/models/question.py | 9 ++---- forum/models/user.py | 2 +- forum/settings/__init__.py | 1 + forum/settings/accept.py | 25 ++++++++++++++++ forum/skins/default/media/js/osqa.main.js | 5 +--- .../templates/osqaadmin/djstyle_base.html | 1 + forum/skins/default/templates/question.html | 4 +-- .../default/templates/question_list/item.html | 2 +- .../default/templates/users/responses.html | 23 --------------- forum/templatetags/node_tags.py | 29 +++++++++++++++---- forum/utils/pagination.py | 8 ++--- forum/views/admin.py | 2 +- forum/views/commands.py | 19 ++++++++---- forum/views/readers.py | 13 +++++++-- 15 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 forum/settings/accept.py delete mode 100644 forum/skins/default/templates/users/responses.html diff --git a/forum/actions/meta.py b/forum/actions/meta.py index ccdaa75..f5c12cd 100644 --- a/forum/actions/meta.py +++ b/forum/actions/meta.py @@ -133,18 +133,16 @@ class AcceptAnswerAction(ActionProxy): self.repute(self.node.author, int(settings.REP_GAIN_BY_ACCEPTED)) def process_action(self): - self.node.parent.extra_ref = self.node - self.node.parent.save() self.node.marked = True self.node.nstate.accepted = self self.node.save() + self.node.question.reset_accepted_count_cache() def cancel_action(self): - self.node.parent.extra_ref = None - self.node.parent.save() self.node.marked = False self.node.nstate.accepted = None self.node.save() + self.node.question.reset_accepted_count_cache() def describe(self, viewer=None): answer = self.node diff --git a/forum/models/question.py b/forum/models/question.py index 2cb362c..55f37c4 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -13,6 +13,7 @@ class Question(Node): proxy = True answer_count = DenormalizedField("children", ~models.Q(state_string__contains="(deleted)"), node_type="answer") + accepted_count = DenormalizedField("children", ~models.Q(state_string__contains="(deleted)"), node_type="answer", marked=True) favorite_count = DenormalizedField("actions", action_type="favorite", canceled=False) friendly_name = _("question") @@ -37,12 +38,8 @@ class Question(Node): return self.title @property - def answer_accepted(self): - return self.extra_ref is not None - - @property - def accepted_answer(self): - return self.extra_ref + def accepted_answers(self): + return self.answers.filter(~models.Q(state_string__contains="(deleted)"), marked=True) @models.permalink def get_absolute_url(self): diff --git a/forum/models/user.py b/forum/models/user.py index fb13c27..cabaabb 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -276,7 +276,7 @@ class User(BaseModel, DjangoUser): @true_if_is_super_or_staff def can_accept_answer(self, answer): - return self == answer.question.author + return self == answer.question.author and (settings.USERS_CAN_ACCEPT_OWN or answer.author != answer.question.author) @true_if_is_super_or_staff def can_create_tags(self): diff --git a/forum/settings/__init__.py b/forum/settings/__init__.py index 6d5b03f..32f11ee 100644 --- a/forum/settings/__init__.py +++ b/forum/settings/__init__.py @@ -37,6 +37,7 @@ from moderation import * from users import * from static import * from urls import * +from accept import * BADGES_SET = SettingSet('badges', _('Badges config'), _("Configure badges on your OSQA site."), 500) diff --git a/forum/settings/accept.py b/forum/settings/accept.py new file mode 100644 index 0000000..4212366 --- /dev/null +++ b/forum/settings/accept.py @@ -0,0 +1,25 @@ +from base import Setting, SettingSet +from django.forms.widgets import RadioSelect +from django.utils.translation import ugettext_lazy as _ + +ACCEPT_SET = SettingSet('accept', _('Accepting answers'), _("Settings to tweak the behaviour of accepting answers."), 500) + +DISABLE_ACCEPTING_FEATURE = Setting('DISABLE_ACCEPTING_FEATURE', False, ACCEPT_SET, dict( +label = _("Disallow answers to be accepted"), +help_text = _("Disable accepting answers feature. If you reenable it in the future, currently accepted answers will still be marked as accepted."), +required=False)) + +MAXIMUM_ACCEPTED_ANSWERS = Setting('MAXIMUM_ACCEPTED_ANSWERS', 1, ACCEPT_SET, dict( +label = _("Maximum accepted answers per question"), +help_text = _("How many accepted answers are allowed per question. Use 0 for no limit."))) + +MAXIMUM_ACCEPTED_PER_USER = Setting('MAXIMUM_ACCEPTED_PER_USER', 1, ACCEPT_SET, dict( +label = _("Maximum accepted answers per user/question"), +help_text = _("If more than one accpeted answer is allowed, how many can be accepted per single user per question."))) + +USERS_CAN_ACCEPT_OWN = Setting('USERS_CAN_ACCEPT_OWN', False, ACCEPT_SET, dict( +label = _("Users an accept own answer"), +help_text = _("Are normal users allowed to accept theyr own answers.."), +required=False)) + + diff --git a/forum/skins/default/media/js/osqa.main.js b/forum/skins/default/media/js/osqa.main.js index 85d21b9..c701b05 100644 --- a/forum/skins/default/media/js/osqa.main.js +++ b/forum/skins/default/media/js/osqa.main.js @@ -49,10 +49,7 @@ var response_commands = { } }, - mark_accepted: function(id) { - $('.accepted-answer').removeClass('accepted-answer'); - $('.accept-answer.on').removeClass('on'); - + mark_accepted: function(id) { var $answer = $('#answer-container-' + id); $answer.addClass('accepted-answer'); $answer.find('.accept-answer').addClass('on'); diff --git a/forum/skins/default/templates/osqaadmin/djstyle_base.html b/forum/skins/default/templates/osqaadmin/djstyle_base.html index d976268..9841138 100644 --- a/forum/skins/default/templates/osqaadmin/djstyle_base.html +++ b/forum/skins/default/templates/osqaadmin/djstyle_base.html @@ -78,6 +78,7 @@
  • {{ allsets.repgain.title }}
  • {{ allsets.minrep.title }}
  • {{ allsets.voting.title }}
  • +
  • {{ allsets.accept.title }}
  • {{ allsets.badges.title }}
  • diff --git a/forum/skins/default/templates/question.html b/forum/skins/default/templates/question.html index 4d5ff09..d03af25 100644 --- a/forum/skins/default/templates/question.html +++ b/forum/skins/default/templates/question.html @@ -64,7 +64,7 @@
    - +
    @@ -124,7 +124,7 @@ {% for answer in answers.paginator.page %} -
    +
    diff --git a/forum/skins/default/templates/question_list/item.html b/forum/skins/default/templates/question_list/item.html index e499848..7d6a117 100644 --- a/forum/skins/default/templates/question_list/item.html +++ b/forum/skins/default/templates/question_list/item.html @@ -9,7 +9,7 @@
    {{question.score|intcomma}}
    {% trans "votes" %}
    -
    +
    {{question.answer_count|intcomma}}
    {% trans "answers" %}
    diff --git a/forum/skins/default/templates/users/responses.html b/forum/skins/default/templates/users/responses.html deleted file mode 100644 index c4f4ffe..0000000 --- a/forum/skins/default/templates/users/responses.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "user.html" %} - -{% load extra_tags %} -{% load humanize %} - -{% block usercontent %} -
    - {% for response in responses %} -
    -
    {% diff_date response.time 3 %}
    - -
    - {{ response.type }}: - {{ response.title }}
    - {{ response.content|safe }} -
    -
    - -
    - {% endfor %} -
    -{% endblock %} - diff --git a/forum/templatetags/node_tags.py b/forum/templatetags/node_tags.py index 3124f3a..8a685ce 100644 --- a/forum/templatetags/node_tags.py +++ b/forum/templatetags/node_tags.py @@ -20,11 +20,14 @@ def vote_buttons(post, user): @register.inclusion_tag('node/accept_button.html') def accept_button(answer, user): - return { - 'can_accept': user.is_authenticated() and user.can_accept_answer(answer), - 'answer': answer, - 'user': user - } + if not settings.DISABLE_ACCEPTING_FEATURE: + return { + 'can_accept': user.is_authenticated() and user.can_accept_answer(answer), + 'answer': answer, + 'user': user + } + else: + return '' @register.inclusion_tag('node/wiki_symbol.html') def wiki_symbol(user, post): @@ -52,6 +55,22 @@ def favorite_mark(question, user): return {'favorited': favorited, 'favorite_count': question.favorite_count, 'question': question} +@register.simple_tag +def post_classes(post): + classes = [] + + if post.nis.deleted: + classes.append('deleted') + + if post.node_type == "answer": + if (not settings.DISABLE_ACCEPTING_FEATURE) and post.nis.accepted: + classes.append('accepted-answer') + + if post.author == post.question.author: + classes.append('answered-by-owner') + + return " ".join(classes) + def post_control(text, url, command=False, withprompt=False, confirm=False, title=""): classes = (command and "ajax-command" or " ") + (withprompt and " withprompt" or " ") + (confirm and " confirm" or " ") return {'text': text, 'url': url, 'classes': classes, 'title': title} diff --git a/forum/utils/pagination.py b/forum/utils/pagination.py index a573954..5951b33 100644 --- a/forum/utils/pagination.py +++ b/forum/utils/pagination.py @@ -19,11 +19,11 @@ class SimpleSort(SortBase): super(SimpleSort, self) .__init__(label, description) self.order_by = order_by + def _get_order_by(self): + return isinstance(self.order_by, (list, tuple)) and self.order_by or [self.order_by] + def apply(self, objects): - if isinstance(self.order_by, (list, tuple)): - return objects.order_by(*self.order_by) - else: - return objects.order_by(self.order_by) + return objects.order_by(*self._get_order_by()) class PaginatorContext(object): visible_page_range = 5 diff --git a/forum/views/admin.py b/forum/views/admin.py index 29fd454..7b10f7c 100644 --- a/forum/views/admin.py +++ b/forum/views/admin.py @@ -36,7 +36,7 @@ def admin_page(fn): context['allsets'] = Setting.sets context['othersets'] = sorted( [s for s in Setting.sets.values() if not s.name in - ('basic', 'users', 'email', 'paths', 'extkeys', 'repgain', 'minrep', 'voting', 'badges', 'about', 'faq', 'sidebar', + ('basic', 'users', 'email', 'paths', 'extkeys', 'repgain', 'minrep', 'voting', 'accept', 'badges', 'about', 'faq', 'sidebar', 'form', 'moderation', 'css', 'headandfoot', 'head', 'view', 'urls')] , lambda s1, s2: s1.weight - s2.weight) diff --git a/forum/views/commands.py b/forum/views/commands.py index 3c49076..956463a 100644 --- a/forum/views/commands.py +++ b/forum/views/commands.py @@ -289,6 +289,9 @@ def node_markdown(request, id): @decorate.withfn(command) def accept_answer(request, id): + if settings.DISABLE_ACCEPTING_FEATURE: + raise Http404() + user = request.user if not user.is_authenticated(): @@ -298,7 +301,7 @@ def accept_answer(request, id): question = answer.question if not user.can_accept_answer(answer): - raise CommandException(_("Sorry but only the question author can accept an answer")) + raise CommandException(_("Sorry but you cannot accept the answer")) commands = {} @@ -306,11 +309,17 @@ def accept_answer(request, id): answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR']) commands['unmark_accepted'] = [answer.id] else: - accepted = question.accepted_answer + if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS): + raise CommandException(ungettext("This question already has an accepted answer.", + "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS))) + + if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count: + accepted_from_author = question.accepted_answers.filter(author=answer.author).count() + + if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER: + raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.", + "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER))) - if accepted: - accepted.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR']) - commands['unmark_accepted'] = [accepted.id] AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save() commands['mark_accepted'] = [answer.id] diff --git a/forum/views/readers.py b/forum/views/readers.py index 8159902..a7906fa 100644 --- a/forum/views/readers.py +++ b/forum/views/readers.py @@ -46,12 +46,19 @@ class QuestionListPaginatorContext(pagination.PaginatorContext): (_('mostvoted'), pagination.SimpleSort(_('most voted'), '-score', _("most voted questions"))), ), pagesizes=(15, 30, 50), default_pagesize=default_pagesize, prefix=prefix) +class AnswerSort(pagination.SimpleSort): + def apply(self, answers): + if not settings.DISABLE_ACCEPTING_FEATURE: + return answers.order_by(*(['-marked'] + list(self._get_order_by()))) + else: + return super(AnswerSort, self).apply(answers) + class AnswerPaginatorContext(pagination.PaginatorContext): def __init__(self, id='ANSWER_LIST', prefix='', default_pagesize=10): super (AnswerPaginatorContext, self).__init__(id, sort_methods=( - (_('oldest'), pagination.SimpleSort(_('oldest answers'), ('-marked', 'added_at'), _("oldest answers will be shown first"))), - (_('newest'), pagination.SimpleSort(_('newest answers'), ('-marked', '-added_at'), _("newest answers will be shown first"))), - (_('votes'), pagination.SimpleSort(_('popular answers'), ('-marked', '-score', 'added_at'), _("most voted answers will be shown first"))), + (_('oldest'), AnswerSort(_('oldest answers'), 'added_at', _("oldest answers will be shown first"))), + (_('newest'), AnswerSort(_('newest answers'), '-added_at', _("newest answers will be shown first"))), + (_('votes'), AnswerSort(_('popular answers'), ('-score', 'added_at'), _("most voted answers will be shown first"))), ), default_sort=_('votes'), pagesizes=(5, 10, 20), default_pagesize=default_pagesize, prefix=prefix) class TagPaginatorContext(pagination.PaginatorContext): -- 2.45.2