Browse Source

Make it all work!

Alec Nikolas Reiter 7 years ago
parent
commit
096fee7a66

+ 90 - 28
flaskbb/forum/models.py

@@ -246,23 +246,29 @@ class Post(HideableCRUDMixin, db.Model):
         db.session.commit()
         return self
 
-    def hide(self):
+    def hide(self, user):
+        if self.hidden:
+            return
+
         if self.topic.first_post == self:
-            self.topic.hide()
+            self.topic.hide(user)
             return self
 
         self._deal_with_last_post()
         self._update_counts()
-        super(Post, self).hide()
+        super(Post, self).hide(user)
         db.session.commit()
         return self
 
     def unhide(self):
+        if not self.hidden:
+            return
+
         if self.topic.first_post == self:
             self.topic.unhide()
             return self
 
-        self._restore_topic_to_forum()
+        self._restore_post_to_topic()
         super(Post, self).unhide()
         db.session.commit()
         return self
@@ -301,16 +307,37 @@ class Post(HideableCRUDMixin, db.Model):
             self.topic.last_updated = self.topic.last_post.date_created
 
     def _update_counts(self):
+        if self.hidden:
+            clauses = [Post.hidden != True, Post.id != self.id]
+        else:
+            clauses = [db.or_(Post.hidden != True, Post.id == self.id)]
+
+        user_post_clauses = clauses + [
+            Post.user_id == self.user.id,
+            Topic.id == Post.topic_id,
+            Topic.hidden != True,
+        ]
+
         # Update the post counts
-        self.user.post_count -= 1
-        self.topic.post_count -= 1
-        self.topic.forum.post_count -= 1
+        self.user.post_count = Post.query.filter(*user_post_clauses).count()
 
-    def _restore_post_to_topic(self):
-        self.user.post_count += 1
-        self.topic.post_count += 1
-        self.topic.forum.post_count += 1
+        if self.topic.hidden:
+            self.topic.post_count = 0
+        else:
+            topic_post_clauses = clauses + [
+                Post.topic_id == self.topic.id,
+            ]
+            self.topic.post_count = Post.query.filter(*topic_post_clauses).count()
+
+        forum_post_clauses = clauses + [
+            Post.topic_id == Topic.id,
+            Topic.forum_id == self.topic.forum.id,
+            Topic.hidden != True,
+        ]
+
+        self.topic.forum.post_count = Post.query.filter(*forum_post_clauses).count()
 
+    def _restore_post_to_topic(self):
         last_unhidden_post = Post.query.filter(
             Post.topic_id == self.topic_id,
             Post.id != self.id
@@ -322,7 +349,14 @@ class Post(HideableCRUDMixin, db.Model):
             self.second_last_post = last_unhidden_post
 
             # if we're the newest in the topic again, we might be the newest in the forum again
-            if self.date_created > self.topic.forum.last_post.date_created:
+            # only set if our parent topic isn't hidden
+            if (
+                not self.topic.hidden and
+                (
+                    not self.topic.forum.last_post or
+                    self.date_created > self.topic.forum.last_post.date_created
+                )
+            ):
                 self.topic.forum.last_post = self
                 self.topic.forum.last_post_title = self.topic.title
                 self.topic.forum.last_post_user = self.user
@@ -610,24 +644,32 @@ class Topic(HideableCRUDMixin, db.Model):
         forum = self.forum
         db.session.delete(self)
         self._fix_user_post_counts(users or self.involved_users().all())
-        self._fix_forum_counts(forum)
+        self._fix_post_counts(forum)
         db.session.commit()
         return self
 
-    def hide(self, users=None):
+    def hide(self, user, users=None):
         """Soft deletes a topic from a forum
         """
+        if self.hidden:
+            return
+
         self._remove_topic_from_forum()
-        super(Topic, self).hide()
+        super(Topic, self).hide(user)
+        self._handle_first_post()
         self._fix_user_post_counts(users or self.involved_users().all())
-        self._fix_forum_counts(self.forum)
+        self._fix_post_counts(self.forum)
         db.session.commit()
         return self
 
     def unhide(self, users=None):
         """Restores a hidden topic to a forum
         """
+        if not self.hidden:
+            return
+
         super(Topic, self).unhide()
+        self._handle_first_post()
         self._restore_topic_to_forum()
         self._fix_user_post_counts(users or self.involved_users().all())
         self.forum.recalculate()
@@ -642,7 +684,7 @@ class Topic(HideableCRUDMixin, db.Model):
             Topic.last_post_id.desc()
         ).limit(2).offset(0).all()
 
-        # do we want to delete the topic with the last post in the forum?
+        # do we want to replace the topic with the last post in the forum?
         if len(topics) > 1 and topics[0] == self:
             # Now the second last post will be the last post
             self.forum.last_post = topics[1].last_post
@@ -666,19 +708,34 @@ class Topic(HideableCRUDMixin, db.Model):
                 user.post_count = Post.query.filter(
                     Post.user_id == user.id,
                     Topic.id == Post.topic_id,
-                    Topic.hidden == False
+                    Topic.hidden != True,
+                    Post.hidden != True
                 ).count()
 
-    def _fix_forum_counts(self, forum):
-        forum.topic_count = Topic.query.filter_by(
-            forum_id=self.forum_id
-        ).count()
+    def _fix_post_counts(self, forum):
+        clauses = [
+            Topic.forum_id == forum.id
+        ]
+        if self.hidden:
+            clauses.extend([
+                Topic.id != self.id,
+                Topic.hidden != True,
+            ])
+        else:
+            clauses.append(db.or_(Topic.id == self.id, Topic.hidden != True))
+
+        forum.topic_count = Topic.query.filter(*clauses).count()
 
-        forum.post_count = Post.query.filter(
+        post_count = clauses + [
             Post.topic_id == Topic.id,
-            Topic.forum_id == self.forum_id,
-            Topic.hidden == False
-        ).count()
+        ]
+
+        if self.hidden:
+            post_count.append(Post.hidden != True)
+        else:
+            post_count.append(db.or_(Post.hidden != True, Post.id == self.first_post.id))
+
+        forum.post_count = Post.query.distinct().filter(*post_count).count()
 
     def _restore_topic_to_forum(self):
         if self.forum.last_post is None or self.forum.last_post_created < self.last_updated:
@@ -688,14 +745,19 @@ class Topic(HideableCRUDMixin, db.Model):
             self.forum.last_post_username = self.username
             self.forum.last_post_created = self.last_updated
 
+    def _handle_first_post(self):
+        # have to do this specially because otherwise we start recurisve calls
+        self.first_post.hidden = self.hidden
+        self.first_post.hidden_by = self.hidden_by
+        self.first_post.hidden_at = self.hidden_at
+
     def involved_users(self):
         """
         Returns a query of all users involved in the topic
         """
         # todo: Find circular import and break it
         from flaskbb.user.models import User
-        return User.query.filter(Post.topic_id == self.id, User.id == Post.user_id)
-
+        return User.query.distinct().filter(Post.topic_id == self.id, User.id == Post.user_id)
 
 
 @make_comparable

+ 57 - 6
flaskbb/forum/views.py

@@ -275,22 +275,24 @@ def trivialize_topic(topic_id=None, slug=None):
 @login_required
 def hide_topic(topic_id, slug=None):
     topic = Topic.query.with_hidden().filter_by(id=topic_id).first_or_404()
-    if not Permission(Has('makehidden') & IsAtleastModeratorInForum(forum=topic.forum)):
+    if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have permission to hide this topic"),
                 "danger")
         return redirect(topic.url)
-    topic.hide()
+    topic.hide(user=current_user)
     topic.save()
-    return redirect(url_for('forum.view_forum', forum_id=topic.forum.id, slug=topic.forum.slug))
+
+    if Permission(Has('viewhidden')):
+        return redirect(topic.url)
+    return redirect(topic.forum.url)
 
 
 @forum.route("/topic/<int:topic_id>/unhide", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/unhide", methods=["POST"])
 @login_required
-@allows.requires(Has('viewhidden'))
 def unhide_topic(topic_id, slug=None):
-    topic = Topic.query.with_hidden().filter_by(id=topic_id).first_or_404()
-    if not Permission(Has('makehidden') & IsAtleastModeratorInForum(forum=topic.forum)):
+    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+    if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=topic.forum)):
         flash(_("You do not have permission to unhide this topic"),
                 "danger")
         return redirect(topic.url)
@@ -548,6 +550,55 @@ def report_post(post_id):
     return render_template("forum/report_post.html", form=form)
 
 
+@forum.route("/post/<int:post_id>/hide", methods=["POST"])
+@login_required
+def hide_post(post_id):
+    post = Post.query.filter(Post.id == post_id).first_or_404()
+
+    if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=post.topic.forum)):
+        flash(_("You do not have permission to hide this post"),
+                "danger")
+        return redirect(post.topic.url)
+
+    if post.hidden:
+        flash(_("Post is already hidden"), "warning")
+        return redirect(post.topic.url)
+
+    first_post = post.first_post
+
+    post.hide(current_user)
+    post.save()
+
+    if first_post:
+        flash(_("Topic hidden"), "success")
+    else:
+        flash(_("Post hidden"), "success")
+
+    if post.first_post and not Permission(Has("viewhidden")):
+        return redirect(post.topic.forum.url)
+    return redirect(post.topic.url)
+
+
+@forum.route("/post/<int:post_id>/unhide", methods=["POST"])
+@login_required
+def unhide_post(post_id):
+    post = Post.query.filter(Post.id == post_id).first_or_404()
+
+    if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=post.topic.forum)):
+        flash(_("You do not have permission to unhide this post"),
+                "danger")
+        return redirect(post.topic.url)
+
+    if not post.hidden:
+        flash(_("Post is already unhidden"), "warning")
+        redirect(post.topic.url)
+
+    post.unhide()
+    post.save()
+    flash(_("Post unhidden"), "success")
+    return redirect(post.topic.url)
+
+
 @forum.route("/post/<int:post_id>/raw", methods=["POST", "GET"])
 @login_required
 def raw_post(post_id):

+ 8 - 0
flaskbb/templates/forum/forum.html

@@ -60,6 +60,8 @@
                         <div class="col-md-1 col-sm-2 col-xs-2 topic-status">
                         {% if topic.locked %}
                             <span class="fa fa-lock fa-fw topic-locked"></span>
+                        {% elif topic.hidden %}
+                            <span class="fa fa-user-secret"></span>
                         {% elif topic.important %}
                             {% if topic|topic_is_unread(topicread, current_user, forumsread) %}
                                 <span class="fa fa-star fa-fw topic-starred-unread"></span>
@@ -89,6 +91,12 @@
                                 {{ topic.username }}
                                 {% endif %}
                             </div>
+
+                            {% if topic.hidden %}
+                            <div class="topic-author">
+                                {% trans %}Hidden on{% endtrans %} {{ topic.hidden_at|format_date('%b %d %Y') }} {% trans %}by{% endtrans %} {{ topic.hidden_by.username }}
+                            </div>
+                            {% endif %}
                         </div>
                     </div>
                 </div>

+ 41 - 0
flaskbb/templates/forum/topic.html

@@ -14,6 +14,13 @@
     </ol>
 
     {% include theme('forum/topic_controls.html') %}
+    {% if topic.hidden %}
+    <div class="alert alert-warning">
+        <span class="fa fa-user-secret"></span>
+        {% trans %}This is topic is hidden{% endtrans %} ({{ topic.hidden_at|format_date('%b %d %Y') }} {% trans %} by {% endtrans %} {{ topic.hidden_by.username }})
+    </div>
+    {% endif %}
+
 
     <div class="panel topic-panel">
         <div class="panel-heading topic-head">
@@ -83,6 +90,12 @@
                     </div>
 
                         <div class="post-content clearfix" id="pid{{ post.id }}">
+                            {% if post.hidden %}
+                            <div class="alert alert-warning">
+                                <span class="fa fa-user-secret"></span>
+                                {% trans %}This is post is hidden{% endtrans %} ({{ post.hidden_at|format_date('%b %d %Y') }} {% trans %} by {% endtrans %} {{ post.hidden_by.username }})
+                            </div>
+                            {% endif %}
                         {{ post.content|markup }}
                         <!-- Signature Begin -->
                         {% if flaskbb_config["SIGNATURE_ENABLED"] and post.user_id and user.signature %}
@@ -133,6 +146,34 @@
                                 <a href="{{ url_for('forum.report_post', post_id=post.id) }}" onclick="window.open(this.href, 'wio_window','width=500,height=500'); return false;" class="btn btn-icon icon-report" data-toggle="tooltip" data-placement="top" title="Report this post"></a>
                             {% endif %}
 
+                            {% if current_user.permissions.get('makehidden') %}
+                                {% if post.first_post %}
+                                    {% if topic.hidden %}
+                                    <form class="inline-form" method="post" action="{{ url_for('forum.unhide_topic', topic_id=post.topic.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-icon fa fa-user" name="unhide" data-toggle="tooltip" data-placement="top" title="Unhide this topic"></button>
+                                    </form>
+                                    {% else %}
+                                    <form class="inline-form" method="post" action="{{ url_for('forum.hide_topic', topic_id=post.topic.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-icon fa fa-user-secret" name="hide" data-toggle="tooltip" data-placement="top" title="Hide this topic"></button>
+                                    </form>
+                                    {% endif %}
+                                {% else %}
+                                    {% if post.hidden %}
+                                    <form class="inline-form" method="post" action="{{ url_for('forum.unhide_post', post_id=post.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-icon fa fa-user" name="unhide" data-toggle="tooltip" data-placement="top" title="Unhide this post"></button>
+                                    </form>
+                                    {% else %}
+                                    <form class="inline-form" method="post" action="{{ url_for('forum.hide_post', post_id=post.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-icon fa fa-user-secret" name="hide" data-toggle="tooltip" data-placement="top" title="Hide this post"></button>
+                                    </form>
+                                    {% endif %}
+                                {% endif %}
+                            {% endif %}
+
                             </div> <!-- end post-menu -->
                         </div> <!-- end footer -->
 

+ 19 - 0
flaskbb/templates/forum/topic_controls.html

@@ -68,6 +68,25 @@
                     </li>
                     {% endif %}
                 {% endif %}
+                {% if current_user | can_moderate(topic.forum) and current_user.permissions.get('makehidden', False) %}
+                <li>
+                    {% if topic.hidden %}
+                    <form class="inline-form" method="post" action="{{ url_for('forum.unhide_topic', topic_id=topic.id, slug=topic.slug) }}">
+                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                        <button class="btn btn-link">
+                            <span class="fa fa-user fa-fw"></span> {% trans %}Unhide Topic{% endtrans %}
+                        </button>
+                    </form>
+                    {% else %}
+                    <form class="inline-form" method="post" action="{{ url_for('forum.hide_topic', topic_id=topic.id, slug=topic.slug) }}">
+                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                        <button class="btn btn-link">
+                            <span class="fa fa-user-secret fa-fw"></span> {% trans %}Hide Topic{% endtrans %}
+                        </button>
+                    </form>
+                    {% endif %}
+                </li>
+                {% endif %}
                 </ul>
             </div>
             <!-- end Moderation buttons -->

+ 2 - 2
flaskbb/user/models.py

@@ -111,8 +111,8 @@ class User(db.Model, UserMixin, CRUDMixin):
     theme = db.Column(db.String(15), nullable=True)
     language = db.Column(db.String(15), default="en", nullable=True)
 
-    posts = db.relationship("Post", backref="user", lazy="dynamic")
-    topics = db.relationship("Topic", backref="user", lazy="dynamic")
+    posts = db.relationship("Post", backref="user", lazy="dynamic", primaryjoin="User.id == Post.user_id")
+    topics = db.relationship("Topic", backref="user", lazy="dynamic", primaryjoin="User.id == Topic.user_id")
 
     post_count = db.Column(db.Integer, default=0)
 

+ 24 - 6
flaskbb/utils/database.py

@@ -11,7 +11,7 @@
 import pytz
 from flask_login import current_user
 from flask_sqlalchemy import BaseQuery
-
+from sqlalchemy.ext.declarative import declared_attr
 from flaskbb.extensions import db
 
 
@@ -87,8 +87,7 @@ class HideableQuery(BaseQuery):
         if args or kwargs:
             super(HideableQuery, inst).__init__(*args, **kwargs)
             entity = inst._mapper_zero().class_
-            return inst.filter(db.or_(entity.hidden == False, entity.hidden == None)
-                               ) if not with_hidden else inst
+            return inst.filter(entity.hidden != True) if not with_hidden else inst
         return inst
 
     def __init__(self, *args, **kwargs):
@@ -109,18 +108,37 @@ class HideableQuery(BaseQuery):
 
 
 class HideableMixin(object):
-    hidden = db.Column(db.Boolean, default=False)
-    hidden_at = db.Column(UTCDateTime(timezone=True), nullable=True)
     query_class = HideableQuery
 
-    def hide(self, *args, **kwargs):
+    hidden = db.Column(db.Boolean, default=False, nullable=True)
+    hidden_at = db.Column(UTCDateTime(timezone=True), nullable=True)
+
+    @declared_attr
+    def hidden_by_id(cls):
+        return db.Column(
+            db.Integer,
+            db.ForeignKey('users.id', name='fk_{}_hidden_by'.format(cls.__name__)),
+            nullable=True
+        )
+
+    @declared_attr
+    def hidden_by(cls):
+        return db.relationship(
+            'User',
+            uselist=False,
+            foreign_keys=[cls.hidden_by_id],
+        )
+
+    def hide(self, user, *args, **kwargs):
         from flaskbb.utils.helpers import time_utcnow
 
+        self.hidden_by = user
         self.hidden = True
         self.hidden_at = time_utcnow()
         return self
 
     def unhide(self, *args, **kwargs):
+        self.hidden_by = None
         self.hidden = False
         self.hidden_at = None
         return self

+ 2 - 2
flaskbb/utils/helpers.py

@@ -127,7 +127,7 @@ def do_topic_action(topics, user, action, reverse):
             if topic.hidden:
                 continue
             modified_topics += 1
-            topic.hide()
+            topic.hide(user)
 
     elif action == 'unhide':
         if not Permission(Has('makehidden')):
@@ -240,7 +240,7 @@ def forum_is_unread(forum, forumsread, user):
         return False
 
     # check if the last post is newer than the tracker length
-    if forum.last_post_created < read_cutoff:
+    if not forum.last_post or forum.last_post_created < read_cutoff:
         return False
 
     # If the user hasn't visited a topic in the forum - therefore,

+ 42 - 0
migrations/fd6ed1fd7d1a_add_hidden_by_column.py

@@ -0,0 +1,42 @@
+"""Add hidden_by column
+
+Revision ID: fd6ed1fd7d1a
+Revises: 63eabbb0e837
+Create Date: 2017-09-04 13:04:52.973752
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'fd6ed1fd7d1a'
+down_revision = '63eabbb0e837'
+branch_labels = ()
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table('posts', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('hidden_by_id', sa.Integer(), nullable=True))
+        batch_op.create_foreign_key('fk_Post_hidden_by', 'users', ['hidden_by_id'], ['id'])
+
+    with op.batch_alter_table('topics', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('hidden_by_id', sa.Integer(), nullable=True))
+        batch_op.create_foreign_key('fk_Topic_hidden_by', 'users', ['hidden_by_id'], ['id'])
+
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table('topics', schema=None) as batch_op:
+        batch_op.drop_constraint('fk_Topic_hidden_by', type_='foreignkey')
+        batch_op.drop_column('hidden_by_id')
+
+    with op.batch_alter_table('posts', schema=None) as batch_op:
+        batch_op.drop_constraint('fk_Post_hidden_by', type_='foreignkey')
+        batch_op.drop_column('hidden_by_id')
+
+    # ### end Alembic commands ###