Browse Source

Merge pull request #412 from sh4nks/cascades

Fix some data inconsistencies
Peter Justin 7 years ago
parent
commit
0a0a8e6cb1

+ 9 - 1
docs/installation.rst

@@ -243,7 +243,15 @@ Both methods are included in the example configs.
 Installation
 Installation
 ------------
 ------------
 
 
-**Sqlite users:** create a DB file in your project source.
+**MySQL users:** Make sure that you create the database using the
+``utf8`` charset::
+
+    CREATE DATABASE flaskbb CHARACTER SET utf8;
+
+Even though the ``utf8mb4`` charset is prefered today
+(see `this <https://dba.stackexchange.com/a/152383>`_ SO answer), we have to
+create our database using the ``utf8`` charset. A good explanation about this
+issue can be found `here <https://stackoverflow.com/a/31474509>`_.
 
 
 For a guided install, run::
 For a guided install, run::
 
 

+ 10 - 2
flaskbb/extensions.py

@@ -27,7 +27,7 @@ from flask_whooshee import (DELETE_KWD, INSERT_KWD, UPDATE_KWD, Whooshee,
                             WhoosheeQuery)
                             WhoosheeQuery)
 from flask_wtf.csrf import CSRFProtect
 from flask_wtf.csrf import CSRFProtect
 from flaskbb.exceptions import AuthorizationRequired
 from flaskbb.exceptions import AuthorizationRequired
-from sqlalchemy import event
+from sqlalchemy import MetaData, event
 from sqlalchemy.orm import Query as SQLAQuery
 from sqlalchemy.orm import Query as SQLAQuery
 
 
 
 
@@ -73,7 +73,15 @@ class FlaskBBWhooshee(Whooshee):
 allows = Allows(throws=AuthorizationRequired)
 allows = Allows(throws=AuthorizationRequired)
 
 
 # Database
 # Database
-db = SQLAlchemy()
+metadata = MetaData(
+    naming_convention={
+        "ix": 'ix_%(column_0_label)s',
+        "uq": "uq_%(table_name)s_%(column_0_name)s",
+        "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+        "pk": "pk_%(table_name)s"
+    }
+)
+db = SQLAlchemy(metadata=metadata)
 
 
 # Whooshee (Full Text Search)
 # Whooshee (Full Text Search)
 whooshee = FlaskBBWhooshee()
 whooshee = FlaskBBWhooshee()

+ 41 - 48
flaskbb/forum/models.py

@@ -27,48 +27,47 @@ logger = logging.getLogger(__name__)
 
 
 moderators = db.Table(
 moderators = db.Table(
     'moderators',
     'moderators',
-    db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
+    db.Column('user_id', db.Integer(),
+              db.ForeignKey('users.id', ondelete="CASCADE"),
               nullable=False),
               nullable=False),
     db.Column('forum_id', db.Integer(),
     db.Column('forum_id', db.Integer(),
-              db.ForeignKey('forums.id', use_alter=True,
-                            name="fk_mods_forum_id"),
+              db.ForeignKey('forums.id', ondelete="CASCADE"),
               nullable=False))
               nullable=False))
 
 
 
 
 topictracker = db.Table(
 topictracker = db.Table(
     'topictracker',
     'topictracker',
-    db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
+    db.Column('user_id', db.Integer(),
+              db.ForeignKey('users.id', ondelete="CASCADE"),
               nullable=False),
               nullable=False),
     db.Column('topic_id', db.Integer(),
     db.Column('topic_id', db.Integer(),
-              db.ForeignKey('topics.id', use_alter=True,
-                            name="fk_tracker_topic_id"),
+              db.ForeignKey('topics.id', ondelete="CASCADE"),
               nullable=False))
               nullable=False))
 
 
 
 
 forumgroups = db.Table(
 forumgroups = db.Table(
     'forumgroups',
     'forumgroups',
-    db.Column('group_id', db.Integer(), db.ForeignKey('groups.id'),
+    db.Column('group_id', db.Integer(),
+              db.ForeignKey('groups.id', ondelete="CASCADE"),
               nullable=False),
               nullable=False),
     db.Column('forum_id', db.Integer(),
     db.Column('forum_id', db.Integer(),
-              db.ForeignKey('forums.id', use_alter=True,
-                            name="fk_fg_forum_id"),
+              db.ForeignKey('forums.id', ondelete="CASCADE"),
               nullable=False))
               nullable=False))
 
 
 
 
 class TopicsRead(db.Model, CRUDMixin):
 class TopicsRead(db.Model, CRUDMixin):
     __tablename__ = "topicsread"
     __tablename__ = "topicsread"
 
 
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+    user_id = db.Column(db.Integer,
+                        db.ForeignKey("users.id", ondelete="CASCADE"),
                         primary_key=True)
                         primary_key=True)
     user = db.relationship('User', uselist=False, foreign_keys=[user_id])
     user = db.relationship('User', uselist=False, foreign_keys=[user_id])
     topic_id = db.Column(db.Integer,
     topic_id = db.Column(db.Integer,
-                         db.ForeignKey("topics.id", use_alter=True,
-                                       name="fk_tr_topic_id"),
+                         db.ForeignKey("topics.id", ondelete="CASCADE"),
                          primary_key=True)
                          primary_key=True)
     topic = db.relationship('Topic', uselist=False, foreign_keys=[topic_id])
     topic = db.relationship('Topic', uselist=False, foreign_keys=[topic_id])
     forum_id = db.Column(db.Integer,
     forum_id = db.Column(db.Integer,
-                         db.ForeignKey("forums.id", use_alter=True,
-                                       name="fk_tr_forum_id"),
+                         db.ForeignKey("forums.id", ondelete="CASCADE"),
                          primary_key=True)
                          primary_key=True)
     forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id])
     forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id])
     last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
     last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
@@ -78,12 +77,12 @@ class TopicsRead(db.Model, CRUDMixin):
 class ForumsRead(db.Model, CRUDMixin):
 class ForumsRead(db.Model, CRUDMixin):
     __tablename__ = "forumsread"
     __tablename__ = "forumsread"
 
 
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+    user_id = db.Column(db.Integer,
+                        db.ForeignKey("users.id", ondelete="CASCADE"),
                         primary_key=True)
                         primary_key=True)
     user = db.relationship('User', uselist=False, foreign_keys=[user_id])
     user = db.relationship('User', uselist=False, foreign_keys=[user_id])
     forum_id = db.Column(db.Integer,
     forum_id = db.Column(db.Integer,
-                         db.ForeignKey("forums.id", use_alter=True,
-                                       name="fk_fr_forum_id"),
+                         db.ForeignKey("forums.id", ondelete="CASCADE"),
                          primary_key=True)
                          primary_key=True)
     forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id])
     forum = db.relationship('Forum', uselist=False, foreign_keys=[forum_id])
     last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
     last_read = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
@@ -149,10 +148,8 @@ class Post(HideableCRUDMixin, db.Model):
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
     topic_id = db.Column(db.Integer,
     topic_id = db.Column(db.Integer,
-                         db.ForeignKey("topics.id",
-                                       use_alter=True,
-                                       name="fk_post_topic_id",
-                                       ondelete="CASCADE"),
+                         db.ForeignKey("topics.id", ondelete="CASCADE",
+                                       use_alter=True),
                          nullable=True)
                          nullable=True)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
     username = db.Column(db.String(200), nullable=False)
     username = db.Column(db.String(200), nullable=False)
@@ -389,9 +386,7 @@ class Topic(HideableCRUDMixin, db.Model):
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
     forum_id = db.Column(db.Integer,
     forum_id = db.Column(db.Integer,
-                         db.ForeignKey("forums.id",
-                                       use_alter=True,
-                                       name="fk_topic_forum_id"),
+                         db.ForeignKey("forums.id", ondelete="CASCADE"),
                          nullable=False)
                          nullable=False)
     title = db.Column(db.String(255), nullable=False)
     title = db.Column(db.String(255), nullable=False)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
@@ -422,7 +417,8 @@ class Topic(HideableCRUDMixin, db.Model):
     # One-to-many
     # One-to-many
     posts = db.relationship("Post", backref="topic", lazy="dynamic",
     posts = db.relationship("Post", backref="topic", lazy="dynamic",
                             primaryjoin="Post.topic_id == Topic.id",
                             primaryjoin="Post.topic_id == Topic.id",
-                            cascade="all, delete-orphan", post_update=True)
+                            cascade="all, delete-orphan",
+                            post_update=True)
 
 
     # Properties
     # Properties
     @property
     @property
@@ -667,6 +663,12 @@ class Topic(HideableCRUDMixin, db.Model):
         db.session.delete(self)
         db.session.delete(self)
         self._fix_user_post_counts(users or self.involved_users().all())
         self._fix_user_post_counts(users or self.involved_users().all())
         self._fix_post_counts(forum)
         self._fix_post_counts(forum)
+
+        # forum.last_post_id shouldn't usually be none
+        if forum.last_post_id is None or \
+                self.last_post_id == forum.last_post_id:
+            forum.update_last_post(commit=False)
+
         db.session.commit()
         db.session.commit()
         return self
         return self
 
 
@@ -723,8 +725,6 @@ class Topic(HideableCRUDMixin, db.Model):
             self.forum.last_post_username = None
             self.forum.last_post_username = None
             self.forum.last_post_created = None
             self.forum.last_post_created = None
 
 
-        TopicsRead.query.filter_by(topic_id=self.id).delete()
-
     def _fix_user_post_counts(self, users=None):
     def _fix_user_post_counts(self, users=None):
         # Update the post counts
         # Update the post counts
         if users:
         if users:
@@ -789,7 +789,8 @@ class Forum(db.Model, CRUDMixin):
     __tablename__ = "forums"
     __tablename__ = "forums"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
-    category_id = db.Column(db.Integer, db.ForeignKey("categories.id"),
+    category_id = db.Column(db.Integer,
+                            db.ForeignKey("categories.id", ondelete="CASCADE"),
                             nullable=False)
                             nullable=False)
     title = db.Column(db.String(255), nullable=False)
     title = db.Column(db.String(255), nullable=False)
     description = db.Column(db.Text, nullable=True)
     description = db.Column(db.Text, nullable=True)
@@ -803,18 +804,18 @@ class Forum(db.Model, CRUDMixin):
 
 
     # One-to-one
     # One-to-one
     last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"),
     last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"),
-                             nullable=True)
+                             nullable=True)  # we handle this case ourselfs
     last_post = db.relationship("Post", backref="last_post_forum",
     last_post = db.relationship("Post", backref="last_post_forum",
                                 uselist=False, foreign_keys=[last_post_id])
                                 uselist=False, foreign_keys=[last_post_id])
 
 
-    last_post_user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+    # set to null if the user got deleted
+    last_post_user_id = db.Column(db.Integer,
+                                  db.ForeignKey("users.id",
+                                                ondelete="SET NULL"),
                                   nullable=True)
                                   nullable=True)
 
 
-    last_post_user = db.relationship(
-        "User",
-        uselist=False,
-        foreign_keys=[last_post_user_id]
-    )
+    last_post_user = db.relationship("User", uselist=False,
+                                     foreign_keys=[last_post_user_id])
 
 
     # Not nice, but needed to improve the performance; can be set to NULL
     # Not nice, but needed to improve the performance; can be set to NULL
     # if the forum has no posts
     # if the forum has no posts
@@ -824,12 +825,8 @@ class Forum(db.Model, CRUDMixin):
                                   default=time_utcnow, nullable=True)
                                   default=time_utcnow, nullable=True)
 
 
     # One-to-many
     # One-to-many
-    topics = db.relationship(
-        "Topic",
-        backref="forum",
-        lazy="dynamic",
-        cascade="all, delete-orphan"
-    )
+    topics = db.relationship("Topic", backref="forum", lazy="dynamic",
+                             cascade="all, delete-orphan")
 
 
     # Many-to-many
     # Many-to-many
     moderators = db.relationship(
     moderators = db.relationship(
@@ -872,7 +869,7 @@ class Forum(db.Model, CRUDMixin):
         """
         """
         return "<{} {}>".format(self.__class__.__name__, self.id)
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
 
-    def update_last_post(self):
+    def update_last_post(self, commit=True):
         """Updates the last post in the forum."""
         """Updates the last post in the forum."""
         last_post = Post.query.\
         last_post = Post.query.\
             filter(Post.topic_id == Topic.id,
             filter(Post.topic_id == Topic.id,
@@ -900,7 +897,8 @@ class Forum(db.Model, CRUDMixin):
             self.last_post_username = None
             self.last_post_username = None
             self.last_post_created = None
             self.last_post_created = None
 
 
-        db.session.commit()
+        if commit:
+            db.session.commit()
 
 
     def update_read(self, user, forumsread, topicsread):
     def update_read(self, user, forumsread, topicsread):
         """Updates the ForumsRead status for the user. In order to work
         """Updates the ForumsRead status for the user. In order to work
@@ -1021,11 +1019,6 @@ class Forum(db.Model, CRUDMixin):
         db.session.delete(self)
         db.session.delete(self)
         db.session.commit()
         db.session.commit()
 
 
-        # Delete the entries for the forum in the ForumsRead and TopicsRead
-        # relation
-        ForumsRead.query.filter_by(forum_id=self.id).delete()
-        TopicsRead.query.filter_by(forum_id=self.id).delete()
-
         # Update the users post count
         # Update the users post count
         if users:
         if users:
             users_list = []
             users_list = []

+ 2 - 6
flaskbb/management/models.py

@@ -10,13 +10,10 @@
 """
 """
 import logging
 import logging
 
 
-from flask_wtf import FlaskForm
-from flaskbb._compat import iteritems, text_type
+from flaskbb._compat import iteritems
 from flaskbb.extensions import cache, db
 from flaskbb.extensions import cache, db
 from flaskbb.utils.database import CRUDMixin
 from flaskbb.utils.database import CRUDMixin
 from flaskbb.utils.forms import SettingValueType, generate_settings_form
 from flaskbb.utils.forms import SettingValueType, generate_settings_form
-from wtforms import (BooleanField, FloatField, IntegerField, SelectField,
-                     SelectMultipleField, TextField, validators)
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -41,8 +38,7 @@ class Setting(db.Model, CRUDMixin):
     value = db.Column(db.PickleType, nullable=False)
     value = db.Column(db.PickleType, nullable=False)
     settingsgroup = db.Column(db.String(255),
     settingsgroup = db.Column(db.String(255),
                               db.ForeignKey('settingsgroup.key',
                               db.ForeignKey('settingsgroup.key',
-                                            use_alter=True,
-                                            name="fk_settingsgroup"),
+                                            ondelete="CASCADE"),
                               nullable=False)
                               nullable=False)
 
 
     # The name (displayed in the form)
     # The name (displayed in the form)

+ 14 - 7
flaskbb/message/models.py

@@ -23,10 +23,14 @@ class Conversation(db.Model, CRUDMixin):
     __tablename__ = "conversations"
     __tablename__ = "conversations"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
-    from_user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+    user_id = db.Column(db.Integer,
+                        db.ForeignKey("users.id", ondelete="CASCADE"),
+                        nullable=False)
+    from_user_id = db.Column(db.Integer, db.ForeignKey("users.id",
+                                                       ondelete="SET NULL"),
                              nullable=True)
                              nullable=True)
-    to_user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+    to_user_id = db.Column(db.Integer, db.ForeignKey("users.id",
+                                                     ondelete="SET NULL"),
                            nullable=True)
                            nullable=True)
     shared_id = db.Column(UUIDType, nullable=False)
     shared_id = db.Column(UUIDType, nullable=False)
     subject = db.Column(db.String(255), nullable=True)
     subject = db.Column(db.String(255), nullable=True)
@@ -41,8 +45,7 @@ class Conversation(db.Model, CRUDMixin):
     messages = db.relationship(
     messages = db.relationship(
         "Message", lazy="joined", backref="conversation",
         "Message", lazy="joined", backref="conversation",
         primaryjoin="Message.conversation_id == Conversation.id",
         primaryjoin="Message.conversation_id == Conversation.id",
-        order_by="asc(Message.id)",
-        cascade="all, delete-orphan"
+        order_by="asc(Message.id)", cascade="all, delete-orphan"
     )
     )
 
 
     # this is actually the users message box
     # this is actually the users message box
@@ -89,11 +92,15 @@ class Message(db.Model, CRUDMixin):
     __tablename__ = "messages"
     __tablename__ = "messages"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
-    conversation_id = db.Column(db.Integer, db.ForeignKey("conversations.id"),
+    conversation_id = db.Column(db.Integer,
+                                db.ForeignKey("conversations.id",
+                                              ondelete="CASCADE"),
                                 nullable=False)
                                 nullable=False)
 
 
     # the user who wrote the message
     # the user who wrote the message
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+    user_id = db.Column(db.Integer,
+                        db.ForeignKey("users.id", ondelete="SET NULL"),
+                        nullable=True)
     message = db.Column(db.Text, nullable=False)
     message = db.Column(db.Text, nullable=False)
     date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
     date_created = db.Column(UTCDateTime(timezone=True), default=time_utcnow,
                              nullable=False)
                              nullable=False)

+ 5 - 3
flaskbb/plugins/models.py

@@ -28,7 +28,9 @@ class PluginStore(CRUDMixin, db.Model):
     # Extra attributes like, validation things (min, max length...)
     # Extra attributes like, validation things (min, max length...)
     # For Select*Fields required: choices
     # For Select*Fields required: choices
     extra = db.Column(db.PickleType, nullable=True)
     extra = db.Column(db.PickleType, nullable=True)
-    plugin_id = db.Column(db.Integer, db.ForeignKey('plugin_registry.id'))
+    plugin_id = db.Column(db.Integer,
+                          db.ForeignKey("plugin_registry.id",
+                                        ondelete="CASCADE"))
 
 
     # Display stuff
     # Display stuff
     name = db.Column(db.Unicode(255), nullable=False)
     name = db.Column(db.Unicode(255), nullable=False)
@@ -62,8 +64,8 @@ class PluginRegistry(CRUDMixin, db.Model):
     values = db.relationship(
     values = db.relationship(
         'PluginStore',
         'PluginStore',
         collection_class=attribute_mapped_collection('key'),
         collection_class=attribute_mapped_collection('key'),
-        cascade='all, delete-orphan',
-        backref='plugin'
+        backref='plugin',
+        cascade="all, delete-orphan",
     )
     )
 
 
     @property
     @property

+ 7 - 2
flaskbb/templates/layout.html

@@ -97,13 +97,18 @@
                                     <li>
                                     <li>
                                         <a href="{{ url_for('message.view_conversation', conversation_id=message.id) }}">
                                         <a href="{{ url_for('message.view_conversation', conversation_id=message.id) }}">
                                             <div>
                                             <div>
-                                                <span class="author-name">{{ message.from_user.username }}</span> <span class="pull-right text-muted">{{ message.last_message.date_created|time_since }}</span>
+                                                {% if message.from_user %}
+                                                    <span class="author-name">{{ message.from_user.username }}</span>
+                                                {% else %}
+                                                    {% trans %}Deleted User{% endtrans %}
+                                                {% endif %}
+                                                <span class="pull-right text-muted">{{ message.last_message.date_created|time_since }}</span>
                                                 <div class="message-subject">{{ message.subject }}</div>
                                                 <div class="message-subject">{{ message.subject }}</div>
                                             </div>
                                             </div>
                                         </a>
                                         </a>
                                     </li>
                                     </li>
                                     {% else %}
                                     {% else %}
-                                    <li><a href="#">No unread messages.</a></li>
+                                    <li><a href="#">{% trans %}No unread messages.{% endtrans %}</a></li>
                                     {% endfor %}
                                     {% endfor %}
                                     <li class="divider"></li>
                                     <li class="divider"></li>
                                     <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope fa-fw"></span> {% trans %}Inbox{% endtrans %}</a></li>
                                     <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope fa-fw"></span> {% trans %}Inbox{% endtrans %}</a></li>

+ 2 - 2
flaskbb/templates/message/conversation.html

@@ -98,7 +98,7 @@
                         {% endif %}
                         {% endif %}
 
 
                         {% else %}
                         {% else %}
-                        <div class="author-name"><h4>{% trans %}Deleted{% endtrans %}</h4></div>
+                        <div class="author-name"><h4>{% trans %}Deleted User{% endtrans %}</h4></div>
                         <div class="author-title"><h5>{% trans %}Guest{% endtrans %}</h5></div>
                         <div class="author-title"><h5>{% trans %}Guest{% endtrans %}</h5></div>
                         {% endif %}
                         {% endif %}
                     </div>
                     </div>
@@ -110,7 +110,7 @@
     </div>
     </div>
 </div>
 </div>
 
 
-{% if not conversation.draft %}
+{% if not conversation.draft and conversation.from_user != None and conversation.to_user != None %}
 {% from "macros.html" import render_quickreply, render_submit_field %}
 {% from "macros.html" import render_quickreply, render_submit_field %}
 <form class="form" action="#" method="post">
 <form class="form" action="#" method="post">
     {{ form.hidden_tag() }}
     {{ form.hidden_tag() }}

+ 15 - 4
flaskbb/templates/message/conversation_list.html

@@ -18,7 +18,7 @@
         <div class="row conversation-row hover {% if conversation.unread %}unread{% endif %}">
         <div class="row conversation-row hover {% if conversation.unread %}unread{% endif %}">
             <!-- avatar -->
             <!-- avatar -->
             <div class="col-md-1 col-sm-2 col-xs-2 conversation-avatar">
             <div class="col-md-1 col-sm-2 col-xs-2 conversation-avatar">
-                {% if conversation.from_user.avatar %}
+                {% if conversation.from_user and conversation.from_user.avatar %}
                 <img src="{{ conversation.from_user.avatar }}" class="img-circle" alt="avatar" width="65px" height="65px" />
                 <img src="{{ conversation.from_user.avatar }}" class="img-circle" alt="avatar" width="65px" height="65px" />
                 {% else %}
                 {% else %}
                 <img src="{{ url_for('static', filename='img/avatar80x80.png') }}" class="img-circle" alt="avatar" width="65px" height="65px" />
                 <img src="{{ url_for('static', filename='img/avatar80x80.png') }}" class="img-circle" alt="avatar" width="65px" height="65px" />
@@ -41,9 +41,20 @@
                 </div>
                 </div>
                 <!-- meta info (date, user) -->
                 <!-- meta info (date, user) -->
                 <div class="conversation-meta">
                 <div class="conversation-meta">
-                    From <a href="{{ conversation.from_user.url }}">{{ conversation.from_user.username }}</a>
-                    to <a href="{{ conversation.to_user.url }}">{{ conversation.to_user.username }}</a>
-                    on {{ conversation.last_message.date_created|format_date("%d %B %Y - %H:%M") }}
+                    {% trans %}From{% endtrans %}
+                    {% if conversation.from_user %}
+                        <a href="{{ conversation.from_user.url }}">{{ conversation.from_user.username }}</a>
+                    {% else %}
+                        {% trans %}Deleted User{% endtrans %}
+                    {% endif %}
+
+                    {% trans %}to{% endtrans %}
+                    {% if conversation.to_user %}
+                        <a href="{{ conversation.to_user.url }}">{{ conversation.to_user.username }}</a>
+                    {% else %}
+                        {% trans %}Deleted User{% endtrans %}
+                    {% endif %}
+                    {% trans %}on{% endtrans %} {{ conversation.last_message.date_created|format_date("%d %B %Y - %H:%M") }}
                 </div>
                 </div>
                 <!-- actual content -->
                 <!-- actual content -->
                 <div class="conversation-content">
                 <div class="conversation-content">

+ 51 - 40
flaskbb/user/models.py

@@ -18,8 +18,7 @@ from flaskbb.exceptions import AuthenticationError
 from flaskbb.utils.helpers import time_utcnow
 from flaskbb.utils.helpers import time_utcnow
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.database import CRUDMixin, UTCDateTime, make_comparable
 from flaskbb.utils.database import CRUDMixin, UTCDateTime, make_comparable
-from flaskbb.forum.models import (Post, Topic, Forum, topictracker, TopicsRead,
-                                  ForumsRead)
+from flaskbb.forum.models import Post, Topic, Forum, topictracker
 from flaskbb.message.models import Conversation, Message
 from flaskbb.message.models import Conversation, Message
 
 
 
 
@@ -28,9 +27,11 @@ logger = logging.getLogger(__name__)
 
 
 groups_users = db.Table(
 groups_users = db.Table(
     'groups_users',
     'groups_users',
-    db.Column('user_id', db.Integer, db.ForeignKey('users.id'),
+    db.Column('user_id', db.Integer,
+              db.ForeignKey('users.id', ondelete="CASCADE"),
               nullable=False),
               nullable=False),
-    db.Column('group_id', db.Integer, db.ForeignKey('groups.id'),
+    db.Column('group_id', db.Integer,
+              db.ForeignKey('groups.id', ondelete="CASCADE"),
               nullable=False)
               nullable=False)
 )
 )
 
 
@@ -115,30 +116,57 @@ class User(db.Model, UserMixin, CRUDMixin):
     theme = db.Column(db.String(15), nullable=True)
     theme = db.Column(db.String(15), nullable=True)
     language = db.Column(db.String(15), default="en", nullable=True)
     language = db.Column(db.String(15), default="en", nullable=True)
 
 
-    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)
     post_count = db.Column(db.Integer, default=0)
 
 
     primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'),
     primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'),
                                  nullable=False)
                                  nullable=False)
 
 
-    primary_group = db.relationship('Group', lazy="joined",
-                                    backref="user_group", uselist=False,
-                                    foreign_keys=[primary_group_id])
-
-    secondary_groups = \
-        db.relationship('Group',
-                        secondary=groups_users,
-                        primaryjoin=(groups_users.c.user_id == id),
-                        backref=db.backref('users', lazy='dynamic'),
-                        lazy='dynamic')
-
-    tracked_topics = \
-        db.relationship("Topic", secondary=topictracker,
-                        primaryjoin=(topictracker.c.user_id == id),
-                        backref=db.backref("topicstracked", lazy="dynamic"),
-                        lazy="dynamic")
+    posts = db.relationship(
+        "Post",
+        backref="user",
+        primaryjoin="User.id == Post.user_id",
+        lazy="dynamic"
+    )
+
+    topics = db.relationship(
+        "Topic",
+        backref="user",
+        primaryjoin="User.id == Topic.user_id",
+        lazy="dynamic"
+    )
+
+    primary_group = db.relationship(
+        "Group",
+        backref="user_group",
+        uselist=False,
+        lazy="joined",
+        foreign_keys=[primary_group_id]
+    )
+
+    secondary_groups = db.relationship(
+        "Group",
+        secondary=groups_users,
+        primaryjoin=(groups_users.c.user_id == id),
+        backref=db.backref("users", lazy="dynamic"),
+        lazy="dynamic"
+    )
+
+    tracked_topics = db.relationship(
+        "Topic",
+        secondary=topictracker,
+        primaryjoin=(topictracker.c.user_id == id),
+        backref=db.backref("topicstracked", lazy="dynamic"),
+        lazy="dynamic",
+        cascade="all, delete-orphan",
+        single_parent=True
+    )
+
+    conversations = db.relationship(
+        "Conversation",
+        primaryjoin="Conversation.user_id == User.id",
+        lazy="dynamic",
+        passive_deletes="all"  # let the dbms handle the foreignkeys
+    )
 
 
     # Properties
     # Properties
     @property
     @property
@@ -194,7 +222,6 @@ class User(db.Model, UserMixin, CRUDMixin):
             Conversation.id == Message.conversation_id
             Conversation.id == Message.conversation_id
         ).count()
         ).count()
 
 
-
     @property
     @property
     def days_registered(self):
     def days_registered(self):
         """Returns the amount of days the user is registered."""
         """Returns the amount of days the user is registered."""
@@ -480,22 +507,6 @@ class User(db.Model, UserMixin, CRUDMixin):
 
 
     def delete(self):
     def delete(self):
         """Deletes the User."""
         """Deletes the User."""
-        # This isn't done automatically...
-        Conversation.query.filter_by(user_id=self.id).delete()
-        ForumsRead.query.filter_by(user_id=self.id).delete()
-        TopicsRead.query.filter_by(user_id=self.id).delete()
-
-        # This should actually be handeld by the dbms.. but dunno why it doesnt
-        # work here
-        from flaskbb.forum.models import Forum
-
-        last_post_forums = Forum.query.\
-            filter_by(last_post_user_id=self.id).all()
-
-        for forum in last_post_forums:
-            forum.last_post_user_id = None
-            forum.save()
-
         db.session.delete(self)
         db.session.delete(self)
         db.session.commit()
         db.session.commit()
 
 

+ 252 - 0
migrations/201802021027_af3f5579c84d_add_cascades.py

@@ -0,0 +1,252 @@
+"""Add cascades
+
+Revision ID: af3f5579c84d
+Revises: 7c3fcf8a3335
+Create Date: 2018-02-02 10:27:50.290095
+
+"""
+import logging
+
+from alembic import op
+import sqlalchemy as sa
+
+import flaskbb
+
+logger = logging.getLogger("alembic.runtime.migration")
+
+# revision identifiers, used by Alembic.
+revision = 'af3f5579c84d'
+down_revision = '7c3fcf8a3335'
+branch_labels = ()
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    con = op.get_bind()
+
+    if con.engine.dialect.name == "sqlite":
+        # its not possible to remove unnamed constraints...
+        logger.warning("SQLite is only partially supported for revision {}."
+                       .format(revision))
+
+    with op.batch_alter_table('conversations', schema=None) as batch_op:
+        if con.engine.dialect.name == "mysql":
+            # user_id
+            batch_op.drop_constraint("conversations_ibfk_3", type_='foreignkey')
+            # to_user_id
+            batch_op.drop_constraint("conversations_ibfk_2", type_='foreignkey')
+            # from_user_id
+            batch_op.drop_constraint("conversations_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('conversations_to_user_id_fkey', type_='foreignkey')
+            batch_op.drop_constraint('conversations_from_user_id_fkey', type_='foreignkey')
+            batch_op.drop_constraint('conversations_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_conversations_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_conversations_from_user_id_users'), 'users', ['from_user_id'], ['id'], ondelete='SET NULL')
+        batch_op.create_foreign_key(batch_op.f('fk_conversations_to_user_id_users'), 'users', ['to_user_id'], ['id'], ondelete='SET NULL')
+
+
+    with op.batch_alter_table('forumgroups', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_forum_id', type_='foreignkey')
+        elif con.engine.dialect.name == "mysql":
+            # group_id
+            batch_op.drop_constraint("forumgroups_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('forumgroups_group_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_forumgroups_group_id_groups'), 'groups', ['group_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_forumgroups_forum_id_forums'), 'forums', ['forum_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('forums', schema=None) as batch_op:
+        if con.engine.dialect.name == "mysql":
+            # category_id
+            batch_op.drop_constraint("forums_ibfk_1", type_='foreignkey')
+            # last_post_user_id
+            batch_op.drop_constraint("forums_ibfk_3", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('forums_category_id_fkey', type_='foreignkey')
+            batch_op.drop_constraint('forums_last_post_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_forums_category_id_categories'), 'categories', ['category_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_forums_last_post_user_id_users'), 'users', ['last_post_user_id'], ['id'], ondelete='SET NULL')
+
+    with op.batch_alter_table('forumsread', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_fr_forum_id', type_='foreignkey')
+        elif con.engine.dialect.name == "mysql":
+            # user_id
+            batch_op.drop_constraint("forumsread_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('forumsread_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_forumsread_forum_id_forums'), 'forums', ['forum_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_forumsread_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('groups_users', schema=None) as batch_op:
+        if con.engine.dialect.name == "mysql":
+            # group_id
+            batch_op.drop_constraint("groups_users_ibfk_1", type_='foreignkey')
+            # user_id
+            batch_op.drop_constraint("groups_users_ibfk_2", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('groups_users_group_id_fkey', type_='foreignkey')
+            batch_op.drop_constraint('groups_users_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_groups_users_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_groups_users_group_id_groups'), 'groups', ['group_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('messages', schema=None) as batch_op:
+        batch_op.alter_column('user_id',
+               existing_type=sa.INTEGER(),
+               nullable=True)
+
+        if con.engine.dialect.name == "mysql":
+            # conversation_id
+            batch_op.drop_constraint("messages_ibfk_1", type_='foreignkey')
+            # user_id
+            batch_op.drop_constraint("messages_ibfk_2", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('messages_conversation_id_fkey', type_='foreignkey')
+            batch_op.drop_constraint('messages_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_messages_conversation_id_conversations'), 'conversations', ['conversation_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_messages_user_id_users'), 'users', ['user_id'], ['id'], ondelete='SET NULL')
+
+    with op.batch_alter_table('moderators', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_forum_id', type_='foreignkey')
+        elif con.engine.dialect.name == "mysql":
+            # user_id
+            batch_op.drop_constraint("moderators_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('moderators_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_moderators_forum_id_forums'), 'forums', ['forum_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_moderators_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('plugin_store', schema=None) as batch_op:
+        if con.engine.dialect.name == "mysql":
+            # plugin_id
+            batch_op.drop_constraint("plugin_store_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('plugin_store_plugin_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_plugin_store_plugin_id_plugin_registry'), 'plugin_registry', ['plugin_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('posts', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_post_topic_id', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_posts_topic_id_topics'), 'topics', ['topic_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('settings', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_settingsgroup', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_settings_settingsgroup_settingsgroup'), 'settingsgroup', ['settingsgroup'], ['key'], ondelete='CASCADE')
+
+    with op.batch_alter_table('topics', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_topic_forum_id', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_topics_forum_id_forums'), 'forums', ['forum_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('topicsread', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_tr_forum_id', type_='foreignkey')
+            batch_op.drop_constraint('fk_tr_topic_id', type_='foreignkey')
+        elif con.engine.dialect.name == "mysql":
+            # user_id
+            batch_op.drop_constraint("topicsread_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('topicsread_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_topicsread_topic_id_topics'), 'topics', ['topic_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_topicsread_forum_id_forums'), 'forums', ['forum_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_topicsread_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+
+    with op.batch_alter_table('topictracker', schema=None) as batch_op:
+        if con.engine.dialect.name == "sqlite":
+            batch_op.drop_constraint('fk_tracker_topic_id', type_='foreignkey')
+        elif con.engine.dialect.name == "mysql":
+            # user_id
+            batch_op.drop_constraint("topictracker_ibfk_1", type_='foreignkey')
+        elif con.engine.dialect.name == "postgresql":
+            batch_op.drop_constraint('topictracker_user_id_fkey', type_='foreignkey')
+
+        batch_op.create_foreign_key(batch_op.f('fk_topictracker_topic_id_topics'), 'topics', ['topic_id'], ['id'], ondelete='CASCADE')
+        batch_op.create_foreign_key(batch_op.f('fk_topictracker_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
+
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table('topictracker', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_topictracker_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_topictracker_topic_id_topics'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_topictracker_user_id_users', 'users', ['user_id'], ['id'])
+
+    with op.batch_alter_table('topicsread', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_topicsread_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_topicsread_forum_id_forums'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_topicsread_topic_id_topics'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_topicsread_user_id_users', 'users', ['user_id'], ['id'])
+
+    with op.batch_alter_table('topics', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_topics_forum_id_forums'), type_='foreignkey')
+
+    with op.batch_alter_table('settings', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_settings_settingsgroup_settingsgroup'), type_='foreignkey')
+
+    with op.batch_alter_table('posts', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_posts_topic_id_topics'), type_='foreignkey')
+
+    with op.batch_alter_table('plugin_store', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_plugin_store_plugin_id_plugin_registry'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_plugin_store_plugin_id_plugin_registry', 'plugin_registry', ['plugin_id'], ['id'])
+
+    with op.batch_alter_table('moderators', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_moderators_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_moderators_forum_id_forums'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_moderators_user_id_users', 'users', ['user_id'], ['id'])
+
+    with op.batch_alter_table('messages', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_messages_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_messages_conversation_id_conversations'), type_='foreignkey')
+
+        batch_op.alter_column('user_id', existing_type=flaskbb.utils.database.UTCDateTime(timezone=True), nullable=False)
+
+        batch_op.create_foreign_key('fk_messages_user_id_users', 'users', ['user_id'], ['id'])
+        batch_op.create_foreign_key('fk_messages_conversation_id_conversations', 'conversations', ['conversation_id'], ['id'])
+
+    with op.batch_alter_table('groups_users', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_groups_users_group_id_groups'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_groups_users_user_id_users'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_groups_users_user_id_users', 'users', ['user_id'], ['id'])
+        batch_op.create_foreign_key('fk_groups_users_group_id_groups', 'groups', ['group_id'], ['id'])
+
+    with op.batch_alter_table('forumsread', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_forumsread_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_forumsread_forum_id_forums'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_forumsread_user_id_users', 'users', ['user_id'], ['id'])
+
+    with op.batch_alter_table('forums', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_forums_last_post_user_id_users'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_forums_category_id_categories'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_forums_last_post_user_id_users', 'users', ['last_post_user_id'], ['id'])
+        batch_op.create_foreign_key('fk_forums_category_id_categories', 'categories', ['category_id'], ['id'])
+
+    with op.batch_alter_table('forumgroups', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_forumgroups_forum_id_forums'), type_='foreignkey')
+        batch_op.drop_constraint(batch_op.f('fk_forumgroups_group_id_groups'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_forumgroups_group_id_groups', 'groups', ['group_id'], ['id'])
+
+    with op.batch_alter_table('conversations', schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f('fk_conversations_user_id_users'), type_='foreignkey')
+        batch_op.create_foreign_key('fk_conversations_user_id_users', 'users', ['user_id'], ['id'])
+
+    # ### end Alembic commands ###

+ 3 - 2
tests/endtoend/test_message_views.py

@@ -2,7 +2,7 @@ import pytest
 from werkzeug import exceptions
 from werkzeug import exceptions
 from flask_login import login_user
 from flask_login import login_user
 
 
-from flaskbb.message import views
+from flaskbb.message import views, models
 
 
 
 
 def test_message_not_logged_in(application):
 def test_message_not_logged_in(application):
@@ -18,7 +18,8 @@ def test_message_inbox(application, default_settings, conversation_msgs, user):
     with application.test_request_context():
     with application.test_request_context():
         login_user(user)
         login_user(user)
         resp = view.get()
         resp = view.get()
-        assert 'From <a href="/user/test_normal">test_normal</a>' in resp
+        assert '<a href="/message/1/view">' in resp
+        assert '<a href="/user/test_normal">test_normal</a>' in resp
 
 
 
 
 def test_message_view_conversation(
 def test_message_view_conversation(