self.repute(self.node.author, int(settings.REP_GAIN_BY_ACCEPTED))\r
\r
def process_action(self):\r
- self.node.parent.extra_ref = self.node\r
- self.node.parent.save()\r
self.node.marked = True\r
self.node.nstate.accepted = self\r
self.node.save()\r
+ self.node.question.reset_accepted_count_cache()\r
\r
def cancel_action(self):\r
- self.node.parent.extra_ref = None\r
- self.node.parent.save()\r
self.node.marked = False\r
self.node.nstate.accepted = None\r
self.node.save()\r
+ self.node.question.reset_accepted_count_cache()\r
\r
def describe(self, viewer=None):\r
answer = self.node\r
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")
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):
@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):
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)
--- /dev/null
+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))
+
+
}\r
},\r
\r
- mark_accepted: function(id) {\r
- $('.accepted-answer').removeClass('accepted-answer');\r
- $('.accept-answer.on').removeClass('on');\r
- \r
+ mark_accepted: function(id) { \r
var $answer = $('#answer-container-' + id);\r
$answer.addClass('accepted-answer');\r
$answer.find('.accept-answer').addClass('on');\r
<li><a href="{% url admin_set allsets.repgain.name %}">{{ allsets.repgain.title }}</a></li>
<li><a href="{% url admin_set allsets.minrep.name %}">{{ allsets.minrep.title }}</a></li>
<li><a href="{% url admin_set allsets.voting.name %}">{{ allsets.voting.title }}</a></li>
+ <li><a href="{% url admin_set allsets.accept.name %}">{{ allsets.accept.title }}</a></li>
<li><a href="{% url admin_set allsets.badges.name %}">{{ allsets.badges.title }}</a></li>
</ul>
</div>
</div>\r
<div id="main-body" class="">\r
<div id="askform">\r
- <table style="width:100%;" id="question-table" {% if question.nis.deleted %}class="deleted"{%endif%}>\r
+ <table style="width:100%;" id="question-table" {% post_classes question %}>\r
<tr>\r
<td style="width:30px;vertical-align:top">\r
<div class="vote-buttons">\r
\r
{% for answer in answers.paginator.page %}\r
<a name="{{ answer.id }}"></a>\r
- <div id="answer-container-{{ answer.id }}" class="answer {% if answer.nis.accepted %}accepted-answer{% endif %} {% ifequal answer.author_id question.author_id %} answered-by-owner{% endifequal %} {% if answer.nis.deleted %}deleted{% endif %}">\r
+ <div id="answer-container-{{ answer.id }}" class="answer {% post_classes answer %}">\r
<table style="width:100%;">\r
<tr>\r
<td style="width:30px;vertical-align:top">\r
<div class="item-count">{{question.score|intcomma}}</div>\r
<div>{% trans "votes" %}</div>\r
</div >\r
- <div {% if question.answer_accepted %}title="{% trans "this question has an accepted answer" %}"{% endif %} class="status {% if question.answer_accepted %}answered-accepted{% endif %} {% ifequal question.answer_count 0 %}unanswered{% endifequal %}{% ifnotequal question.answer_count 0 %}answered{% endifnotequal %}">\r
+ <div {% if question.accepted_count %}title="{% trans "this question has an accepted answer" %}"{% endif %} class="status {% if question.accepted_count %}answered-accepted{% endif %} {% ifequal question.answer_count 0 %}unanswered{% endifequal %}{% ifnotequal question.answer_count 0 %}answered{% endifnotequal %}">\r
<div class="item-count">{{question.answer_count|intcomma}}</div>\r
<div>{% trans "answers" %}</div>\r
</div>\r
+++ /dev/null
-{% extends "user.html" %}
-<!-- user_responses.html -->
-{% load extra_tags %}
-{% load humanize %}
-
-{% block usercontent %}
- <div style="padding-top:5px;font-size:13px;">
- {% for response in responses %}
- <div style="clear:both;line-height:18px">
- <div style="width:150px;float:left">{% diff_date response.time 3 %}</div>
- <div style="width:100px;float:left"><a href="{{ response.userlink }}">{{ response.username }}</a></div>
- <div style="float:left;overflow:hidden;width:680px">
- <strong {% ifequal response.type "question_answered" %}class="user-action-2"{% endifequal %}{% ifequal response.type "answer_accepted" %}class="user-action-8"{% endifequal %}>{{ response.type }}</strong>:
- <a href="{{ response.titlelink }}">{{ response.title }}</a><br/>
- {{ response.content|safe }}
- <div style="height:10px"></div>
- </div>
-
- </div>
- {% endfor %}
- </div>
-{% endblock %}
-<!-- end user_responses.html -->
\r
@register.inclusion_tag('node/accept_button.html')\r
def accept_button(answer, user):\r
- return {\r
- 'can_accept': user.is_authenticated() and user.can_accept_answer(answer),\r
- 'answer': answer,\r
- 'user': user\r
- }\r
+ if not settings.DISABLE_ACCEPTING_FEATURE:\r
+ return {\r
+ 'can_accept': user.is_authenticated() and user.can_accept_answer(answer),\r
+ 'answer': answer,\r
+ 'user': user\r
+ }\r
+ else:\r
+ return ''\r
\r
@register.inclusion_tag('node/wiki_symbol.html')\r
def wiki_symbol(user, post):\r
\r
return {'favorited': favorited, 'favorite_count': question.favorite_count, 'question': question}\r
\r
+@register.simple_tag\r
+def post_classes(post):\r
+ classes = []\r
+\r
+ if post.nis.deleted:\r
+ classes.append('deleted')\r
+\r
+ if post.node_type == "answer":\r
+ if (not settings.DISABLE_ACCEPTING_FEATURE) and post.nis.accepted:\r
+ classes.append('accepted-answer')\r
+\r
+ if post.author == post.question.author:\r
+ classes.append('answered-by-owner')\r
+\r
+ return " ".join(classes)\r
+\r
def post_control(text, url, command=False, withprompt=False, confirm=False, title=""):\r
classes = (command and "ajax-command" or " ") + (withprompt and " withprompt" or " ") + (confirm and " confirm" or " ")\r
return {'text': text, 'url': url, 'classes': classes, 'title': title}\r
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
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)
@decorate.withfn(command)
def accept_answer(request, id):
+ if settings.DISABLE_ACCEPTING_FEATURE:
+ raise Http404()
+
user = request.user
if not user.is_authenticated():
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 = {}
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]
(_('mostvoted'), pagination.SimpleSort(_('most voted'), '-score', _("most <strong>voted</strong> 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):