]> git.openstreetmap.org Git - osqa.git/commitdiff
Adds a couple of options to manage the "accepting answers" workflow.
authorhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Thu, 15 Jul 2010 23:31:46 +0000 (23:31 +0000)
committerhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Thu, 15 Jul 2010 23:31:46 +0000 (23:31 +0000)
git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@533 0cfe37f9-358a-4d5e-be75-b63607b5c754

15 files changed:
forum/actions/meta.py
forum/models/question.py
forum/models/user.py
forum/settings/__init__.py
forum/settings/accept.py [new file with mode: 0644]
forum/skins/default/media/js/osqa.main.js
forum/skins/default/templates/osqaadmin/djstyle_base.html
forum/skins/default/templates/question.html
forum/skins/default/templates/question_list/item.html
forum/skins/default/templates/users/responses.html [deleted file]
forum/templatetags/node_tags.py
forum/utils/pagination.py
forum/views/admin.py
forum/views/commands.py
forum/views/readers.py

index ccdaa75f390396d09b2687b95e1277f3b450e9b9..f5c12cdcc038449e40cc55a2c277c576177526b4 100644 (file)
@@ -133,18 +133,16 @@ class AcceptAnswerAction(ActionProxy):
             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
index 2cb362cf8deee6b777387396a6510ceeeb3b56c1..55f37c47ca787ec207b992622141463983d23884 100644 (file)
@@ -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):
index fb13c27f7c5524c27b2aaa544d207f76ac978a97..cabaabbd156947e6456c3b82f999aa4d5920c18f 100644 (file)
@@ -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):
index 6d5b03f6f1f324d2214ab9581abbb6b135d8fdcd..32f11ee483bfbb023ebf76c3eacab9baf46ca041 100644 (file)
@@ -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 (file)
index 0000000..4212366
--- /dev/null
@@ -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))
+
+
index 85d21b96f7d062ee890ca5c32d8c9b9062000448..c701b05883003826cfa8507309abb54abff91da2 100644 (file)
@@ -49,10 +49,7 @@ var response_commands = {
         }\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
index d976268a1308c5a6385fc9b79bf9434b7d38b68f..98411382d205dc0f5ec7767e52e11b2c42e61b9d 100644 (file)
@@ -78,6 +78,7 @@
                         <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>
index 4d5ff090009aa196a96087b217d76ed45a975e27..d03af253a5b246e04267de41dc093ad242e94a60 100644 (file)
@@ -64,7 +64,7 @@
 </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
index e4998488d35816d89c946697ec5480e176a1901a..7d6a117238bd69dd4c3f60e4e3138c99abbb8006 100644 (file)
@@ -9,7 +9,7 @@
             <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
diff --git a/forum/skins/default/templates/users/responses.html b/forum/skins/default/templates/users/responses.html
deleted file mode 100644 (file)
index c4f4ffe..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-{% 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 -->
index 3124f3aa7ccc4887cb0d4f0ab2064abf8bbf0b49..8a685ce86abb50f082300cbfa94bc45a6760dc75 100644 (file)
@@ -20,11 +20,14 @@ def vote_buttons(post, user):
 \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
@@ -52,6 +55,22 @@ def favorite_mark(question, user):
 \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
index a573954e46df720761738d397e2eb280306f8078..5951b3384c22a7d324f88548d89e3df574be33af 100644 (file)
@@ -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
index 29fd454fcbfb2bda7d5f732dfb6f8ce5a6ed9dd8..7b10f7cec43c22835fa84a96622dc7fa7e32edfc 100644 (file)
@@ -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)
 
index 3c4907688a531a693fac96a68f782ffb4fc4b62a..956463ae03098fcba0706f7967c910fdb143e94d 100644 (file)
@@ -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]
index 81599027e47cb5a68b7f6f7eadf703628eeb5841..a7906fa8389e4a1ca72bdae1174f38ed7da9bd2d 100644 (file)
@@ -46,12 +46,19 @@ class QuestionListPaginatorContext(pagination.PaginatorContext):
             (_('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):