Rafał Pitoń 7 лет назад
Родитель
Сommit
8702ac3345

+ 1 - 0
misago/conf/defaults.py

@@ -20,6 +20,7 @@ MISAGO_ACL_EXTENSIONS = [
     'misago.threads.permissions.polls',
     'misago.threads.permissions.polls',
     'misago.threads.permissions.threads',
     'misago.threads.permissions.threads',
     'misago.threads.permissions.privatethreads',
     'misago.threads.permissions.privatethreads',
+    'misago.threads.permissions.answers',
     'misago.search.permissions',
     'misago.search.permissions',
 ]
 ]
 
 

+ 31 - 0
misago/threads/migrations/0008_auto_20180304_2004.py

@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.9 on 2018-03-04 20:04
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_threads', '0007_auto_20171008_0131'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='thread',
+            name='answer',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='misago_threads.Post'),
+        ),
+        migrations.AddField(
+            model_name='thread',
+            name='answer_is_protected',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='thread',
+            name='answer_set_on',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+    ]

+ 11 - 1
misago/threads/models/post.py

@@ -134,12 +134,18 @@ class Post(models.Model):
         other_post.parsed = six.text_type('\n').join((other_post.parsed, self.parsed))
         other_post.parsed = six.text_type('\n').join((other_post.parsed, self.parsed))
         update_post_checksum(other_post)
         update_post_checksum(other_post)
 
 
+        if self.thread.answer_id == self.id:
+            self.thread.answer = other_post
+
         from misago.threads.signals import merge_post
         from misago.threads.signals import merge_post
         merge_post.send(sender=self, other_post=other_post)
         merge_post.send(sender=self, other_post=other_post)
 
 
     def move(self, new_thread):
     def move(self, new_thread):
         from misago.threads.signals import move_post
         from misago.threads.signals import move_post
 
 
+        if self.thread.answer_id == self.id:
+            self.thread.answer = None
+
         self.category = new_thread.category
         self.category = new_thread.category
         self.thread = new_thread
         self.thread = new_thread
         move_post.send(sender=self)
         move_post.send(sender=self)
@@ -213,4 +219,8 @@ class Post(models.Model):
 
 
     @property
     @property
     def is_first_post(self):
     def is_first_post(self):
-        return self.pk == self.thread.first_post_id
+        return self.id == self.thread.first_post_id
+
+    @property
+    def is_answer(self):
+        return self.id == self.thread.answer_id

+ 10 - 0
misago/threads/models/thread.py

@@ -78,6 +78,16 @@ class Thread(models.Model):
     is_hidden = models.BooleanField(default=False)
     is_hidden = models.BooleanField(default=False)
     is_closed = models.BooleanField(default=False)
     is_closed = models.BooleanField(default=False)
 
 
+    answer = models.ForeignKey(
+        'misago_threads.Post',
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+    )
+    answer_is_protected =  models.BooleanField(default=False)
+    answer_set_on = models.DateTimeField(null=True, blank=True)
+
     participants = models.ManyToManyField(
     participants = models.ManyToManyField(
         settings.AUTH_USER_MODEL,
         settings.AUTH_USER_MODEL,
         related_name='privatethread_set',
         related_name='privatethread_set',

+ 1 - 0
misago/threads/permissions/__init__.py

@@ -1,3 +1,4 @@
+from .answers import *
 from .privatethreads import *
 from .privatethreads import *
 from .threads import *
 from .threads import *
 from .polls import *
 from .polls import *

+ 273 - 0
misago/threads/permissions/answers.py

@@ -0,0 +1,273 @@
+from django import forms
+from django.core.exceptions import PermissionDenied
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _, ungettext
+
+from misago.acl import algebra
+from misago.acl.decorators import return_boolean
+from misago.categories.models import Category, CategoryRole
+from misago.categories.permissions import get_categories_roles
+from misago.core.forms import YesNoSwitch
+from misago.threads.models import Post
+
+
+__all__nope = [
+    'allow_select_answer',
+    'can_select_answer',
+    'allow_remove_answer',
+    'can_remove_answer',
+]
+
+
+class CategoryPermissionsForm(forms.Form):
+    legend = _("Answers")
+
+    can_set_answers = forms.TypedChoiceField(
+        label=_("Can set answers"),
+        coerce=int,
+        initial=0,
+        choices=[
+            (0, _("No")),
+            (1, _("Own threads")),
+            (2, _("All threads")),
+        ],
+    )
+    can_change_answers = forms.TypedChoiceField(
+        label=_("Can change answers"),
+        coerce=int,
+        initial=0,
+        choices=[
+            (0, _("No")),
+            (1, _("Own threads")),
+            (2, _("All threads")),
+        ],
+    )
+    answer_change_time = forms.IntegerField(
+        label=_("Time limit for owned thread answer change, in minutes"),
+        help_text=_("Enter 0 to don't limit time for changing own thread answer."),
+        initial=0,
+        min_value=0,
+    )
+
+
+def change_permissions_form(role):
+    if isinstance(role, CategoryRole):
+        return CategoryPermissionsForm
+    else:
+        return None
+
+
+def build_acl(acl, roles, key_name):
+    categories_roles = get_categories_roles(roles)
+    categories = list(Category.objects.all_categories(include_root=True))
+
+    for category in categories:
+        category_acl = acl['categories'].get(category.pk, {'can_browse': 0})
+        if category_acl['can_browse']:
+            category_acl = acl['categories'][category.pk] = build_category_acl(
+                category_acl, category, categories_roles, key_name
+            )
+
+    return acl
+
+
+def build_category_acl(acl, category, categories_roles, key_name):
+    category_roles = categories_roles.get(category.pk, [])
+
+    final_acl = {
+        'can_set_answers': 0,
+        'can_change_answers': 0,
+        'answer_change_time': 0,
+    }
+    final_acl.update(acl)
+
+    algebra.sum_acls(
+        final_acl,
+        roles=category_roles,
+        key=key_name,
+        can_set_answers=algebra.greater,
+        can_change_answers=algebra.greater,
+        answer_change_time=algebra.greater_or_zero,
+    )
+
+    return final_acl
+
+
+def add_acl_to_post(user, post):
+    post.acl.update({
+        'can_set_answer': can_set_answer(user, post),
+        'can_unset_answer': can_unset_answer(user, post),
+    })
+
+
+def register_with(registry):
+    registry.acl_annotator(Post, add_acl_to_post)
+
+
+def allow_set_answer(user, target):
+    if user.is_anonymous:
+        raise PermissionDenied(_("You have to sign in to set posts as answers."))
+
+    if target.is_event:
+        raise PermissionDenied(_("Events can't be set as answers."))
+
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_set_answers': 0,
+        }
+    )
+
+    if not category_acl['can_set_answers']:
+        raise PermissionDenied(
+            _(
+                'You don\'t have permission to set answers in the "%(category)s" category.'
+            ) % {
+                'category': target.category,
+            }
+        )
+
+    if category_acl['can_set_answers'] == 1 and target.thread.starter != user:
+        raise PermissionDenied(
+            _(
+                "You dont't have permission to set this post as an answer "
+                "because you are not the thread starter."
+            )
+        )
+
+    if target.is_first_post:
+        raise PermissionDenied(_("First post in a thread can't be set as an answer."))
+
+    if target.is_hidden:
+        raise PermissionDenied(_("Hidden posts can't be set as answers."))
+
+    if target.is_unapproved:
+        raise PermissionDenied(_("Unapproved posts can't be set as answers."))
+
+    if target.is_answer:
+        raise PermissionDenied(_("This post is already set as an answer."))
+
+    if category_acl['can_set_answers'] == 1 and target.thread.answer_id:
+        if not has_time_to_change_answer(user, target):
+            raise PermissionDenied(
+                ungettext(
+                    (
+                        "You don't have permission to change thread's answer that was set "
+                        "for more than %(minutes)s minute."),
+                    (
+                        "You don't have permission to change thread's answer that was set "
+                        "for more than %(minutes)s minutes."),
+                    category_acl['answer_change_time'],
+                ) % {
+                    'minutes': category_acl['answer_change_time'],
+                }
+            )
+
+        if target.thread.answer_is_protected and not category_acl['can_protect_posts']:
+            raise PermissionDenied(
+                _(
+                    "You don't have permission to change this thread's answer because moderator "
+                    "has protected it."
+                )
+            )
+        
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(
+                _(
+                    'You can\'t sets this post as an answer because it\'s category '
+                    '"%(category)s" is closed.'
+                ) % {
+                    'category': target.category,
+                }
+            )
+        if target.thread.is_closed:
+            raise PermissionDenied(
+                _(
+                    "You can't set this post as an answer because it's thread is closed and you "
+                    "don't have permission to open it."
+                )
+            )
+
+    if target.is_protected and not category_acl['can_protect_posts']:
+        raise PermissionDenied(
+            _("You can't sets this post as an answer because moderator has protected it.")
+        )
+
+
+can_set_answer = return_boolean(allow_set_answer)
+
+
+def allow_unset_answer(user, target):
+    if user.is_anonymous:
+        raise PermissionDenied(_("You have to sign in to unset threads answers."))
+
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_change_answers': 0,
+        }
+    )
+
+    if not category_acl['can_change_answers']:
+        raise PermissionDenied(
+            _(
+                'You don\'t have permission to unset threads answers in the "%(category)s" '
+                'category.'
+            ) % {
+                'category': target.category,
+            }
+        )
+
+    if not target.is_answer:
+        raise PermissionDenied(
+            _(
+                "You can't unset."
+            )
+        )
+
+    if category_acl['can_change_answers'] == 1 and target.thread.starter != user:
+        raise PermissionDenied(
+            _(
+                "You dont't have permission to unset this answer because "
+                "you are not a thread starter."
+            )
+        )
+        
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(
+                _(
+                    'You can\'t unset this answer because it\'s scategory "%(category)s" is closed.'
+                ) % {
+                    'category': target.category,
+                }
+            )
+        if target.thread.is_closed:
+            raise PermissionDenied(
+                _(
+                    "You don't have permission to unset this answer because it's thread is closed "
+                    "and you don't have permission to open it."
+                )
+            )
+
+    if target.is_protected and not category_acl['can_protect_posts']:
+        raise PermissionDenied(
+            _(
+                "You don't have permission to unset this thread's answer because moderator has "
+                "protected it."
+            )
+        )
+
+
+can_unset_answer = return_boolean(allow_unset_answer)
+
+
+def has_time_to_change_answer(user, target):
+    category_acl = user.acl_cache['categories'].get(target.category_id, {})
+    change_time = category_acl.get('answer_change_time', 0)
+
+    if change_time:
+        diff = timezone.now() - target.thread.answer_set_on
+        diff_minutes = int(diff.total_seconds() / 60)
+        return diff_minutes < change_time
+    else:
+        return True

+ 1 - 4
misago/threads/permissions/threads.py

@@ -3,8 +3,7 @@ from django.core.exceptions import PermissionDenied
 from django.db.models import Q
 from django.db.models import Q
 from django.http import Http404
 from django.http import Http404
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
-from django.utils.translation import ungettext
+from django.utils.translation import ugettext_lazy as _, ungettext
 
 
 from misago.acl import add_acl, algebra
 from misago.acl import add_acl, algebra
 from misago.acl.decorators import return_boolean
 from misago.acl.decorators import return_boolean
@@ -1228,7 +1227,6 @@ def has_time_to_edit_thread(user, target):
     if edit_time:
     if edit_time:
         diff = timezone.now() - target.started_on
         diff = timezone.now() - target.started_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
-
         return diff_minutes < edit_time
         return diff_minutes < edit_time
     else:
     else:
         return True
         return True
@@ -1239,7 +1237,6 @@ def has_time_to_edit_post(user, target):
     if edit_time:
     if edit_time:
         diff = timezone.now() - target.posted_on
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
-
         return diff_minutes < edit_time
         return diff_minutes < edit_time
     else:
     else:
         return True
         return True