Rafał Pitoń 10 лет назад
Родитель
Сommit
5995ee01a0

+ 33 - 0
misago/static/misago/css/misago/posts.less

@@ -176,7 +176,26 @@
       margin-top: @line-height-computed / 2;
       margin-bottom: 0;
 
+      font-size: @font-size-base + 2px;
       text-align: center;
+
+      &.alert-default {
+        background-color: @alert-default-bg;
+
+        color: @alert-default-text;
+      }
+
+      .user-avatar {
+        &, &:link, &:active, &:hover {
+          text-decoration: none;
+        }
+
+        img {
+          height: 20px;
+          position: relative;
+          bottom: 2px;
+        }
+      }
     }
   }
 }
@@ -191,6 +210,20 @@
       .corrupted-message {
         margin: 0px;
       }
+
+      .hidden-message {
+        margin: (@line-height-computed * .25) 0px;
+
+        img {
+          height: 32px;
+          position: relative;
+          bottom: 2px;
+        }
+
+        a, strong {
+          font-weight: normal;
+        }
+      }
     }
   }
 }

+ 3 - 0
misago/static/misago/css/misago/variables.less

@@ -363,6 +363,9 @@
 @alert-danger-bg:             @state-danger-bg;
 @alert-danger-text:           @state-danger-text-contrast;
 
+@alert-default-bg:            @gray-lighter;
+@alert-default-text:          @gray;
+
 
 //== Modals
 //

+ 43 - 0
misago/templates/misago/thread/hidden_message.html

@@ -0,0 +1,43 @@
+{% load i18n misago_avatars misago_capture %}
+
+{% if post.hidden_by_name %}
+
+  {% capture trimmed as hidden_by %}
+    {% if post.hidden_by_id %}
+    <a class="user-avatar" href="{% url USER_PROFILE_URL user_slug=post.hidden_by_slug user_id=post.hidden_by_id %}">
+      <img src="{{ post.hidden_by_id|avatar:32 }}" alt="{% trans "User avatar" %}">
+    </a>
+    <a class="item-title" href="{% url USER_PROFILE_URL user_slug=post.hidden_by_slug user_id=post.hidden_by_id %}">{{ post.hidden_by_name }}</a>
+    {% else %}
+    <span class="user-avatar">
+      <img src="{% blankavatar 32 %}" alt="{% trans "User avatar" %}">
+    </span>
+    <strong class="item-title">{{ post.hidden_by_name }}</strong>
+    {% endif %}
+  {% endcapture %}
+
+  {% capture trimmed as hidden_on %}
+  <abbr class="tooltip-top dynamic time-ago" title="{{ post.hidden_on }}" data-timestamp="{{ post.hidden_on|date:"c" }}">
+    {{ post.hidden_on|date }}
+  </abbr>
+  {% endcapture %}
+
+  {% if post.id == thread.first_post_id %}
+    {% blocktrans trimmed with user=hidden_by|safe date=hidden_on|safe %}
+    This thread was hidden by {{ user }} {{ date }}.
+    {% endblocktrans %}
+  {% else %}
+    {% blocktrans trimmed with user=hidden_by|safe date=hidden_on|safe %}
+    This post was hidden by {{ user }} {{ date }}.
+    {% endblocktrans %}
+  {% endif %}
+
+{% else %}
+
+  {% if post.id == thread.first_post_id %}
+    {% trans "This thread is hidden. Only it's author and moderators can see it." %}
+  {% else %}
+    {% trans "This post is hidden. Only it's author and moderators can see it." %}
+  {% endif %}
+
+{% endif %}

+ 68 - 54
misago/templates/misago/thread/post.html

@@ -71,66 +71,80 @@
       </div>
       {% endif %}
 
-      {% if post.is_valid %}
-      <div class="panel-body">
-        <article class="post-body misago-markup">
-          {{ post.parsed|safe }}
-        <article>
-      </div>
-      {% else %}
-      <div class="alert alert-danger">
-        <span class="fa fa-exclamation-triangle fa-fw fa-lg"></span>
-        {% trans "Post can't be displayed due to invalid message checksum." %}
+      {% if post.is_hidden and post.acl.can_see_hidden %}
+      <div class="alert alert-default">
+        {% include "misago/thread/hidden_message.html" %}
       </div>
       {% endif %}
 
-      <div class="panel-footer">
-
-        {% if thread.acl.can_reply %}
-        <button type="button" class="btn btn-reply btn-primary btn-flat pull-right">
-          <span class="fa fa-reply">
-          {% trans "Reply" %}
-        </button>
-        {% endif %}
-
-        {% if post.acl.can_edit %}
-        <button type="button" class="btn btn-edit btn-default btn-flat pull-right">
-          <span class="fa fa-pencil">
-          {% trans "Edit" %}
-        </button>
+      {% if not post.is_hidden or post.acl.can_see_hidden %}
+        {% if post.is_valid %}
+        <div class="panel-body">
+          <article class="post-body misago-markup">
+            {{ post.parsed|safe }}
+          <article>
+        </div>
+        {% else %}
+        <div class="alert alert-danger">
+          <span class="fa fa-exclamation-triangle fa-fw fa-lg"></span>
+          {% trans "Post can't be displayed due to invalid message checksum." %}
+        </div>
         {% endif %}
 
-
-        <button type="button" class="btn btn-warning btn-flat pull-right">
-          <span class="fa fa-eye-slash">
-          {% trans "Hide" %}
-        </button>
-
-        <button type="button" class="btn btn-default btn-flat pull-right">
-          <span class="fa fa-eye">
-          {% trans "Show" %}
-        </button>
-
-        <button type="button" class="btn btn-danger btn-flat pull-right">
-          <span class="fa fa-times">
-          {% trans "Delete" %}
-        </button>
-
-        <button type="button" class="btn btn-warning btn-flat pull-right">
-          <span class="fa fa-exclamation-triangle">
-          {% trans "Report" %}
-        </button>
-
-        <button type="button" class="btn btn-success btn-flat pull-right">
-          <span class="fa fa-check">
-          {% trans "Approve" %}
-        </button>
-
-        <button type="button" class="btn btn-success btn-flat pull-left">
-          <span class="fa fa-heart">
-          {% trans "Like" %}
-        </button>
+        <div class="panel-footer">
+
+          {% if thread.acl.can_reply %}
+          <button type="button" class="btn btn-reply btn-primary btn-flat pull-right">
+            <span class="fa fa-reply">
+            {% trans "Reply" %}
+          </button>
+          {% endif %}
+
+          {% if post.acl.can_edit %}
+          <button type="button" class="btn btn-edit btn-default btn-flat pull-right">
+            <span class="fa fa-pencil">
+            {% trans "Edit" %}
+          </button>
+          {% endif %}
+
+          <button type="button" class="btn btn-warning btn-flat pull-right">
+            <span class="fa fa-eye-slash">
+            {% trans "Hide" %}
+          </button>
+
+          <button type="button" class="btn btn-default btn-flat pull-right">
+            <span class="fa fa-eye">
+            {% trans "Show" %}
+          </button>
+
+          <button type="button" class="btn btn-danger btn-flat pull-right">
+            <span class="fa fa-times">
+            {% trans "Delete" %}
+          </button>
+
+          <button type="button" class="btn btn-warning btn-flat pull-right">
+            <span class="fa fa-exclamation-triangle">
+            {% trans "Report" %}
+          </button>
+
+          <button type="button" class="btn btn-success btn-flat pull-right">
+            <span class="fa fa-check">
+            {% trans "Approve" %}
+          </button>
+
+          <button type="button" class="btn btn-success btn-flat pull-left">
+            <span class="fa fa-heart">
+            {% trans "Like" %}
+          </button>
+        </div>
+      {% else %}
+      <div class="panel-body">
+        <p class="hidden-message lead text-muted">
+          {% include "misago/thread/hidden_message.html" %}
+        </p>
       </div>
+      {% endif %}
+
     </div>
 
     {% if post.events %}

+ 1 - 1
misago/templates/misago/thread/post_actions.html → misago/templates/misago/thread/posts_actions.html

@@ -14,7 +14,7 @@
     </ul>
     <button type="button" class="btn btn-default dropdown-toggle mass-controller" data-toggle="dropdown">
       <span class="fa fa-gears"></span>
-      {% trans "With posts" %}
+      {% trans "Posts moderation" %}
     </button>
   </form>
 </div>

+ 1 - 1
misago/templates/misago/thread/replies.html

@@ -93,7 +93,7 @@
       {% include "misago/thread/pagination.html" %}
 
       {% if posts_actions %}
-      {% include "misago/thread/post_actions.html" %}
+      {% include "misago/thread/posts_actions.html" %}
       {% endif %}
 
     </div>

+ 5 - 0
misago/threads/migrations/0001_initial.py

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 from django.conf import settings
 from django.db import models, migrations
 import django.db.models.deletion
+import django.utils.timezone
 
 from misago.core.pgutils import CreatePartialIndex
 
@@ -45,6 +46,10 @@ class Migration(migrations.Migration):
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('last_editor_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('last_editor_slug', models.SlugField(max_length=255, null=True, blank=True)),
+                ('hidden_by', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+                ('hidden_by_name', models.CharField(max_length=255, null=True, blank=True)),
+                ('hidden_by_slug', models.SlugField(max_length=255, null=True, blank=True)),
+                ('hidden_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('is_reported', models.BooleanField(default=False)),
                 ('is_moderated', models.BooleanField(default=False, db_index=True)),
                 ('is_hidden', models.BooleanField(default=False)),

+ 12 - 0
misago/threads/models/post.py

@@ -1,6 +1,7 @@
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.dispatch import receiver
+from django.utils import timezone
 
 from misago.conf import settings
 
@@ -19,16 +20,27 @@ class Post(models.Model):
     checksum = models.CharField(max_length=64, default='-')
     mentions = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                       related_name="mention_set")
+
     has_attachments = models.BooleanField(default=False)
     pickled_attachments = models.TextField(null=True, blank=True)
+
     posted_on = models.DateTimeField()
     updated_on = models.DateTimeField()
+
     edits = models.PositiveIntegerField(default=0)
     last_editor = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='+',
                                     null=True, blank=True,
                                     on_delete=models.SET_NULL)
     last_editor_name = models.CharField(max_length=255, null=True, blank=True)
     last_editor_slug = models.SlugField(max_length=255, null=True, blank=True)
+
+    hidden_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='+',
+                                  null=True, blank=True,
+                                  on_delete=models.SET_NULL)
+    hidden_by_name = models.CharField(max_length=255, null=True, blank=True)
+    hidden_by_slug = models.SlugField(max_length=255, null=True, blank=True)
+    hidden_on = models.DateTimeField(default=timezone.now)
+
     is_reported = models.BooleanField(default=False)
     is_moderated = models.BooleanField(default=False, db_index=True)
     is_hidden = models.BooleanField(default=False)

+ 12 - 1
misago/threads/moderation/posts.py

@@ -1,4 +1,5 @@
 from django.db.transaction import atomic
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 
 from misago.threads.moderation.exceptions import ModerationError
@@ -26,7 +27,17 @@ def hide_post(user, post):
 
     if not post.is_hidden:
         post.is_hidden = True
-        post.save(update_fields=['is_hidden'])
+        post.hidden_by = user
+        post.hidden_by_name = user.username
+        post.hidden_by_slug = user.slug
+        post.hidden_on = timezone.now()
+        post.save(update_fields=[
+            'is_hidden',
+            'hidden_by',
+            'hidden_by_name',
+            'hidden_by_slug',
+            'hidden_on',
+        ])
         return True
     else:
         return False

+ 14 - 0
misago/threads/moderation/threads.py

@@ -1,4 +1,5 @@
 from django.db.transaction import atomic
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 
 from misago.threads.events import record_event
@@ -160,6 +161,19 @@ def hide_thread(user, thread):
         message = _("%(user)s hid thread.")
         record_event(user, thread, "eye-slash", message, {'user': user})
 
+        thread.first_post.is_hidden = True
+        thread.first_post.hidden_by = user
+        thread.first_post.hidden_by_name = user.username
+        thread.first_post.hidden_by_slug = user.slug
+        thread.first_post.hidden_on = timezone.now()
+        thread.first_post.save(update_fields=[
+            'is_hidden',
+            'hidden_by',
+            'hidden_by_name',
+            'hidden_by_slug',
+            'hidden_on',
+        ])
+
         thread.is_hidden = True
         thread.save(update_fields=['has_events', 'is_hidden'])
         return True

+ 8 - 1
misago/threads/permissions.py

@@ -312,7 +312,8 @@ def add_acl_to_post(user, post):
     post.acl.update({
         'can_reply': can_reply_thread(user, post.thread),
         'can_edit': can_edit_post(user, post),
-        'can_unhide': forum_acl.get('can_hide_threads'),
+        'can_see_hidden': forum_acl.get('can_hide_posts'),
+        'can_unhide': post.is_hidden and forum_acl.get('can_hide_posts'),
         'can_hide': forum_acl.get('can_hide_threads'),
         'can_delete': forum_acl.get('can_hide_threads'),
         'can_protect': forum_acl.get('can_protect_posts'),
@@ -321,6 +322,12 @@ def add_acl_to_post(user, post):
         'can_approve': forum_acl.get('can_review_moderated_content'),
     })
 
+    if not post.acl['can_see_hidden']:
+        if user.is_authenticated() and user.id == post.poster_id:
+            post.acl['can_see_hidden'] = True
+        else:
+            post.acl['can_see_hidden'] = post.id == post.thread.first_post_id
+
 
 def add_acl_to_event(user, event):
     forum_acl = user.acl['forums'].get(event.forum_id, {})

+ 3 - 3
misago/threads/tests/test_forumthreads_view.py

@@ -230,7 +230,7 @@ class ActionsTests(ForumViewHelperTestCase):
             {
                 'action': 'unhide',
                 'icon': 'eye',
-                'name': _("Unhide threads")
+                'name': _("Reveal threads")
             },
             {
                 'action': 'hide',
@@ -248,7 +248,7 @@ class ActionsTests(ForumViewHelperTestCase):
             {
                 'action': 'unhide',
                 'icon': 'eye',
-                'name': _("Unhide threads")
+                'name': _("Reveal threads")
             },
             {
                 'action': 'hide',
@@ -1219,7 +1219,7 @@ class ForumThreadsViewTests(AuthenticatedUserTestCase):
         self.override_acl(test_acl)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertIn("Unhide threads", response.content)
+        self.assertIn("Reveal threads", response.content)
         self.assertIn("Hide threads", response.content)
 
         threads = [testutils.post_thread(self.forum) for t in xrange(10)]

+ 56 - 0
misago/threads/tests/test_thread_view.py

@@ -343,6 +343,62 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         })
         self.assertEqual(response.status_code, 200)
 
+    def test_hide_unhide_posts(self):
+        """moderation allows for hiding and unhiding multiple posts"""
+        posts = [reply_thread(self.thread) for t in xrange(4)]
+
+        self.thread.synchronize()
+        self.assertEqual(self.thread.replies, 4)
+
+        test_acl = {
+            'can_hide_posts': 1
+        }
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("Hide posts", response.content)
+        self.assertIn("Reveal posts", response.content)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'hide', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(pk=self.thread.pk)
+        self.assertEqual(thread.replies, 4)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'hide', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(pk=self.thread.pk)
+        self.assertEqual(thread.replies, 4)
+
+        posts_queryset = self.thread.post_set
+        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
+            self.assertTrue(post.is_hidden)
+            self.assertNotNull(post.hidden_by)
+            self.assertNotNull(post.hidden_by_name)
+            self.assertNotNull(post.hidden_by_slug)
+            self.assertNotNull(post.hidden_on)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'unhide', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn('hidden-message', response.content)
+
+        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
+            self.assertFalse(post.is_hidden)
+
     def test_delete_posts(self):
         """moderation allows for deleting posts"""
         posts = [reply_thread(self.thread) for t in xrange(10)]

+ 1 - 1
misago/threads/views/generic/forum/actions.py

@@ -85,7 +85,7 @@ class ForumActions(Actions):
             actions.append({
                 'action': 'unhide',
                 'icon': 'eye',
-                'name': _("Unhide threads")
+                'name': _("Reveal threads")
             })
             actions.append({
                 'action': 'hide',

+ 1 - 1
misago/threads/views/generic/thread/postsactions.py

@@ -62,7 +62,7 @@ class PostsActions(ActionsBase):
             actions.append({
                 'action': 'unhide',
                 'icon': 'eye',
-                'name': _("Unhide posts")
+                'name': _("Reveal posts")
             })
             actions.append({
                 'action': 'hide',

+ 1 - 1
misago/threads/views/generic/thread/threadactions.py

@@ -90,7 +90,7 @@ class ThreadActions(ActionsBase):
                 actions.append({
                     'action': 'unhide',
                     'icon': 'eye',
-                    'name': _("Unhide thread")
+                    'name': _("Reveal thread")
                 })
             else:
                 actions.append({