Просмотр исходного кода

[WIP] Rewriting the private message system

sh4nks 10 лет назад
Родитель
Сommit
057ddb4267

+ 14 - 8
flaskbb/app.py

@@ -22,7 +22,10 @@ from flask_whooshalchemy import whoosh_index
 
 # Import the user blueprint
 from flaskbb.user.views import user
-from flaskbb.user.models import User, Guest, PrivateMessage
+from flaskbb.user.models import User, Guest
+# Import the (private) message blueprint
+from flaskbb.message.views import message
+from flaskbb.message.models import Conversation
 # Import the auth blueprint
 from flaskbb.auth.views import auth
 # Import the admin blueprint
@@ -77,6 +80,9 @@ def configure_blueprints(app):
     app.register_blueprint(
         management, url_prefix=app.config["ADMIN_URL_PREFIX"]
     )
+    app.register_blueprint(
+        message, url_prefix=app.config["MESSAGE_URL_PREFIX"]
+    )
 
 
 def configure_extensions(app):
@@ -126,14 +132,14 @@ def configure_extensions(app):
     def load_user(user_id):
         """Loads the user. Required by the `login` extension."""
 
-        unread_count = db.session.query(db.func.count(PrivateMessage.id)).\
-            filter(PrivateMessage.unread,
-                   PrivateMessage.user_id == user_id).subquery()
-        u = db.session.query(User, unread_count).filter(User.id == user_id).\
-            first()
-
+        #unread_count = db.session.query(db.func.count(PrivateMessage.id)).\
+        #    filter(PrivateMessage.unread,
+        #           PrivateMessage.user_id == user_id).subquery()
+        #u = db.session.query(User, unread_count).filter(User.id == user_id).\
+        #    first()
+        u = User.query.get(user_id)
         if u:
-            user_instance, user_instance.pm_unread = u
+            user_instance, user_instance.pm_unread = u, 0
             return user_instance
         else:
             return None

+ 2 - 0
flaskbb/configs/default.py

@@ -84,7 +84,9 @@ class DefaultConfig(object):
     REDIS_URL = "redis://:password@localhost:6379"
     REDIS_DATABASE = 0
 
+    # URL Prefixes
     FORUM_URL_PREFIX = ""
     USER_URL_PREFIX = "/user"
+    MESSAGE_URL_PREFIX = "/message"
     AUTH_URL_PREFIX = "/auth"
     ADMIN_URL_PREFIX = "/admin"

+ 7 - 0
flaskbb/fixtures/settings.py

@@ -71,6 +71,13 @@ fixture = (
         'name': "Misc Settings",
         'description': "Miscellaneous settings.",
         'settings': (
+            ('message_quota', {
+                'value':        50,
+                'value_type':   "integer",
+                'extra':        {"min": 0},
+                'name':         "Private Message Quota",
+                'description':  "The amount of messages a user can have."
+            }),
             ('online_last_minutes', {
                 'value':        15,
                 'value_type':   "integer",

+ 1 - 1
flaskbb/forum/views.py

@@ -442,7 +442,7 @@ def report_post(post_id):
 @login_required
 def raw_post(post_id):
     post = Post.query.filter_by(id=post_id).first_or_404()
-    return format_quote(post)
+    return format_quote(username=post.username, content=post.content)
 
 
 @forum.route("/markread", methods=["POST"])

+ 0 - 0
flaskbb/message/__init__.py


+ 67 - 0
flaskbb/message/forms.py

@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.message.forms
+    ~~~~~~~~~~~~~~~~~~~~
+
+    It provides the forms that are needed for the message views.
+
+    :copyright: (c) 2014 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from flask_login import current_user
+from flask_wtf import Form
+from wtforms import StringField, TextAreaField, ValidationError, SubmitField
+from wtforms.validators import DataRequired
+from flask_babelex import lazy_gettext as _
+
+from flaskbb.user.models import User
+from flaskbb.message.models import Conversation, Message
+
+
+class ConversationForm(Form):
+    to_user = StringField(_("To User"), validators=[
+        DataRequired(message=_("A Username is required."))])
+
+    subject = StringField(_("Subject"), validators=[
+        DataRequired(message=_("A Subject is required."))])
+
+    message = TextAreaField(_("Message"), validators=[
+        DataRequired(message=_("A Message is required."))])
+
+    send_message = SubmitField(_("Start Conversation"))
+    save_message = SubmitField(_("Save Conversation"))
+
+    def validate_to_user(self, field):
+        user = User.query.filter_by(username=field.data).first()
+        if not user:
+            raise ValidationError(_("The Username you entered doesn't exist"))
+        if user.id == current_user.id:
+            raise ValidationError(_("You cannot send a PM to yourself."))
+
+    def save(self, from_user, to_user, user_id, unread, as_draft=False):
+        conversation = Conversation(subject=self.subject.data, draft=as_draft)
+        message = Message(message=self.message.data, user_id=from_user)
+        return conversation.save(message=message, from_user=from_user,
+                                 to_user=to_user, user_id=user_id)
+
+
+class MessageForm(Form):
+    message = TextAreaField(_("Message"), validators=[
+        DataRequired(message=_("A Message is required."))])
+    submit = SubmitField(_("Send Message"))
+
+    def save(self, conversation, user_id, reciever=True):
+        """Saves the form data to the model.
+        :param conversation: The Conversation object.
+        :param user_id: The id from the user who sent the message.
+        :param reciever: If the message should also be stored in the recievers
+                         inbox.
+        """
+        message = Message(message=self.message.data, user_id=user_id)
+        conversation.save(message)
+
+        if reciever:
+            conversation.unread = True
+            conversation.save(message)
+
+        return message.save(conversation)

+ 111 - 0
flaskbb/message/models.py

@@ -0,0 +1,111 @@
+from datetime import datetime
+
+from flaskbb.extensions import db
+
+
+class Conversation(db.Model):
+    __tablename__ = "conversations"
+
+    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"))
+    to_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
+
+    subject = db.Column(db.String(255))
+    date_created = db.Column(db.DateTime, default=datetime.utcnow())
+    trash = db.Column(db.Boolean, nullable=False, default=False)
+    draft = db.Column(db.Boolean, nullable=False, default=False)
+    unread = db.Column(db.Boolean, nullable=False, default=True)
+
+    messages = db.relationship(
+        "Message", lazy="dynamic", backref="conversation",
+        primaryjoin="Message.conversation_id == Conversation.id",
+        cascade="all, delete-orphan"
+    )
+
+    # this is actually the users message box
+    user = db.relationship("User", lazy="joined", foreign_keys=[user_id])
+    # the user to whom the conversation is addressed
+    to_user = db.relationship("User", lazy="joined", foreign_keys=[to_user_id])
+    # the user who sent the message
+    from_user = db.relationship("User", lazy="joined",
+                                foreign_keys=[from_user_id])
+
+    @property
+    def first_message(self):
+        return self.messages[0].message
+
+    @property
+    def last_message(self):
+        return self.messages[-1].message
+
+    def save(self, message=None, user_id=None, from_user=None, to_user=None):
+        """Saves a conversation.
+
+        :param message: The Message object.
+        :param user_id: The senders user id - This is the id to which user the
+                        conversation belongs.
+        :param from_user: The user who has created the conversation
+        :param to_user: The user who should recieve the conversation
+        """
+        if self.id:
+            db.session.add(self)
+            db.session.commit()
+            return self
+
+        if message is not None:
+            # create the conversation
+            self.user_id = user_id
+            self.from_user_id = from_user
+            self.to_user_id = to_user
+            self.date_created = datetime.utcnow()
+
+            db.session.add(self)
+            db.session.commit()
+
+            # create the actual message for the conversation
+            message.save(self)
+            return self
+
+    def delete(self):
+        """Deletes a private message"""
+
+        db.session.delete(self)
+        db.session.commit()
+        return self
+
+
+class Message(db.Model):
+    __tablename__ = "messages"
+
+    id = db.Column(db.Integer, primary_key=True)
+    conversation_id = db.Column(db.Integer, db.ForeignKey("conversations.id"),
+                                nullable=False)
+
+    # the user who wrote the message
+    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+
+    message = db.Column(db.Text, nullable=False)
+    date_created = db.Column(db.DateTime, default=datetime.utcnow())
+
+    user = db.relationship("User", lazy="joined")
+
+    def save(self, conversation):
+        """Saves a private message.
+
+        :param conversation_id: The id of the conversation to which the message
+                                belongs to.
+        """
+        self.conversation_id = conversation.id
+        self.date_created = datetime.utcnow()
+
+        db.session.add(self)
+        db.session.commit()
+        return self
+
+    def delete(self):
+        """Deletes a private message"""
+
+        db.session.delete(self)
+        db.session.commit()
+        return self

+ 251 - 0
flaskbb/message/views.py

@@ -0,0 +1,251 @@
+from datetime import datetime
+
+from flask import Blueprint, redirect, request, url_for, flash, abort
+from flask_login import login_required, current_user
+from flask_babelex import gettext as _
+
+from flaskbb.extensions import db
+from flaskbb.utils.settings import flaskbb_config
+from flaskbb.utils.helpers import render_template, format_quote
+from flaskbb.message.forms import ConversationForm, MessageForm
+from flaskbb.message.models import Conversation, Message
+from flaskbb.user.models import User
+
+message = Blueprint("message", __name__)
+
+
+@message.route("/")
+@message.route("/inbox")
+@login_required
+def inbox():
+    page = request.args.get('page', 1, type=int)
+
+    conversations = Conversation.query.\
+        filter(
+            Conversation.user_id == current_user.id,
+            Conversation.draft == False,
+            Conversation.trash == False
+        ).\
+        order_by(Conversation.id.asc()).\
+        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+    message_count = Conversation.query.\
+        filter(Conversation.user_id == current_user.id).\
+        count()
+
+    return render_template("message/inbox.html", conversations=conversations,
+                           message_count=message_count)
+
+
+@message.route("/<int:conversation_id>/view", methods=["GET", "POST"])
+def view_conversation(conversation_id):
+    conversation = Conversation.query.filter_by(
+        id=conversation_id).first_or_404()
+
+    if conversation.user_id != current_user.id:
+        # if a user tries to view a conversation which does not belong to him
+        # just abort with 404
+        abort(404)
+
+    form = MessageForm()
+    if form.validate_on_submit():
+
+        message_count = Conversation.query.\
+            filter(Conversation.user_id == current_user.id).\
+            count()
+
+        if message_count >= flaskbb_config["MESSAGE_QUOTA"]:
+            flash(_("You cannot send any messages anymore because you have"
+                    "reached your message limit."), "danger")
+            return redirect(url_for("message.view_conversation",
+                                    conversation_id=conversation.id))
+
+        form.save(conversation=conversation, user_id=current_user.id,
+                  reciever=True)
+
+        return redirect(url_for("message.view_conversation",
+                                conversation_id=conversation.id))
+
+    return render_template("message/conversation.html",
+                           conversation=conversation, form=form)
+
+
+@message.route("/new", methods=["POST", "GET"])
+@login_required
+def new_conversation():
+    form = ConversationForm()
+    to_user = request.args.get("to_user")
+
+    message_count = Conversation.query.\
+        filter(Conversation.user_id == current_user.id).\
+        count()
+
+    if message_count >= flaskbb_config["MESSAGE_QUOTA"]:
+        flash(_("You cannot send any messages anymore because you have"
+                "reached your message limit."), "danger")
+        return redirect(url_for("message.inbox"))
+
+    if request.method == "POST":
+        if "save_message" in request.form and form.validate():
+            to_user = User.query.filter_by(username=form.to_user.data).first()
+
+            form.save(from_user=current_user.id,
+                      to_user=to_user.id,
+                      user_id=current_user.id,
+                      unread=False,
+                      as_draft=True)
+
+            flash(_("Message saved."), "success")
+            return redirect(url_for("message.drafts"))
+
+        if "send_message" in request.form and form.validate():
+            to_user = User.query.filter_by(username=form.to_user.data).first()
+
+            # Save the message in the current users inbox
+            form.save(from_user=current_user.id,
+                      to_user=to_user.id,
+                      user_id=current_user.id,
+                      unread=False)
+
+            # Save the message in the recievers inbox
+            form.save(from_user=current_user.id,
+                      to_user=to_user.id,
+                      user_id=to_user.id,
+                      unread=True)
+
+            flash(_("Message sent."), "success")
+            return redirect(url_for("message.sent"))
+    else:
+        form.to_user.data = to_user
+
+    return render_template("message/message_form.html", form=form,
+                           title=_("Compose Message"))
+
+
+@message.route("/message/<int:message_id>/raw")
+@login_required
+def raw_message(message_id):
+    message = Message.query.filter_by(id=message_id).first_or_404()
+
+    if not (message.conversation.from_user_id == current_user.id or
+            message.conversation.to_user_id == current_user.id):
+        abort(404)
+
+    return format_quote(username=message.user.username,
+                        content=message.message)
+
+
+@message.route("/<int:conversation_id>/edit", methods=["POST", "GET"])
+@login_required
+def edit_conversation(conversation_id):
+    conversation = Conversation.query.filter_by(
+        id=conversation_id).first_or_404()
+
+    if conversation.user_id != current_user.id:
+        # if a user tries to view a conversation which does not belong to him
+        # just abort with 404
+        abort(404)
+
+    if not conversation.draft:
+        flash(_("You cannot edit a sent message."), "danger")
+        return redirect(url_for("message.inbox"))
+
+    form = ConversationForm()
+
+    if request.method == "POST":
+        if "save_message" in request.form:
+            to_user = User.query.filter_by(username=form.to_user.data).first()
+
+            conversation.draft = False
+            conversation.to_user = to_user.id
+            conversation.save()
+
+            flash(_("Message saved."), "success")
+            return redirect(url_for("message.drafts"))
+
+        if "send_message" in request.form and form.validate():
+            to_user = User.query.filter_by(username=form.to_user.data).first()
+            # Save the message in the recievers inbox
+            form.save(from_user=current_user.id,
+                      to_user=to_user.id,
+                      user_id=to_user.id,
+                      unread=True)
+
+            # Move the message from ``Drafts`` to ``Sent``.
+            conversation.draft = False
+            conversation.to_user = to_user
+            conversation.date_created = datetime.utcnow()
+            conversation.save()
+
+            flash(_("Message sent."), "success")
+            return redirect(url_for("message.sent"))
+    else:
+        form.to_user.data = conversation.to_user.username
+        form.subject.data = conversation.subject
+        form.message.data = conversation.first_message
+
+    return render_template("message/message_form.html", form=form,
+                           title=_("Edit Message"))
+
+
+@message.route("/sent")
+@login_required
+def sent():
+    page = request.args.get('page', 1, type=int)
+
+    conversations = Conversation.query.\
+        filter(
+            Conversation.user_id == current_user.id,
+            Conversation.draft == False,
+            Conversation.trash == False,
+            db.not_(Conversation.to_user_id == current_user.id)
+        ).\
+        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+    message_count = Conversation.query.\
+        filter(Conversation.user_id == current_user.id).\
+        count()
+
+    return render_template("message/sent.html", conversations=conversations,
+                           message_count=message_count)
+
+
+@message.route("/draft")
+@login_required
+def drafts():
+    page = request.args.get('page', 1, type=int)
+
+    conversations = Conversation.query.\
+        filter(
+            Conversation.user_id == current_user.id,
+            Conversation.draft == True,
+            Conversation.trash == False
+        ).\
+        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+    message_count = Conversation.query.\
+        filter(Conversation.user_id == current_user.id).\
+        count()
+
+    return render_template("message/drafts.html", conversations=conversations,
+                           message_count=message_count)
+
+
+@message.route("/trash")
+@login_required
+def trash():
+    page = request.args.get('page', 1, type=int)
+
+    conversations = Conversation.query.\
+        filter(
+            Conversation.user_id == current_user.id,
+            Conversation.trash == True,
+        ).\
+        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+    message_count = Conversation.query.\
+        filter(Conversation.user_id == current_user.id).\
+        count()
+
+    return render_template("message/trash.html", conversations=conversations,
+                           message_count=message_count)

+ 18 - 0
flaskbb/static/js/conversation.js

@@ -0,0 +1,18 @@
+/**
+ * topic.js
+ */
+$(document).ready(function () {
+    // Quote
+    $('.reply-btn').click(function (event) {
+        event.preventDefault();
+        var message_id = $(this).attr('data-message-id');
+
+        $.get('/message/message/' + message_id + '/raw', function(text) {
+            var $contents = $('.message-content .md-editor textarea');
+            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
+            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
+            $contents[0].scrollTop = $contents[0].scrollHeight;
+            window.location.href = '#content';
+        });
+    });
+});

+ 2 - 1
flaskbb/static/js/topic.js

@@ -8,7 +8,8 @@ $(document).ready(function () {
         var post_id = $(this).attr('data-post-id');
 
         $.get('/post/' + post_id + '/raw', function(text) {
-            var $contents = $('.reply-content textarea#content');
+            var $contents = $('.reply-content .md-editor textarea');
+            console.log($contents)
             $contents.val(($contents.val() + '\n' + text).trim() + '\n');
             $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
             $contents[0].scrollTop = $contents[0].scrollHeight;

+ 2 - 2
flaskbb/templates/forum/topic.html

@@ -108,7 +108,7 @@
             <td>
                 <span class="pull-left">
                     {% if current_user.is_authenticated() and post.user_id and post.user_id != current_user.id %}
-                    <a href="{{ url_for('user.new_message', to_user=post.user.username) }}">{% trans %}PM{% endtrans %}</a>
+                    <a href="{{ url_for('message.new_conversation', to_user=post.user.username) }}">{% trans %}PM{% endtrans %}</a>
                     {% endif %}
                     {% if user.website %}
                     {% if current_user.is_authenticated() %}| {% endif %}<a href="{{ user.website }}">{% trans %}Website{% endtrans %}</a>
@@ -159,7 +159,7 @@
     <form class="form" action="#" method="post">
         {{ form.hidden_tag() }}
 
-        {{ render_field(form.content, div_class="col-sm-12", rows="10", placeholder="", **{'data-provide': 'markdown', 'data-autofocus': 'true', 'id': 'quickreply-editor'}) }}
+        {{ render_field(form.content, div_class="col-sm-12 reply-content", rows="10", placeholder="", **{'data-provide': 'markdown', 'data-autofocus': 'true', 'id': 'quickreply-editor'}) }}
 
         <div class="col-sm-12" style="padding-top: 5px">
         {{ render_submit_field(form.submit) }}

+ 2 - 2
flaskbb/templates/layout.html

@@ -79,8 +79,8 @@
                                 <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
                             </button>
                             <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('user.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Private Messages{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('user.new_message') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Private Messages{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
                             </ul>
                         </div>
                     {% else %}

+ 20 - 0
flaskbb/templates/macros.html

@@ -345,6 +345,26 @@
 {% endmacro %}
 
 
+{% macro message_pagination(page_obj, url) %}
+<ul class='{%- if ul_class -%}{{ ul_class }}{%- else -%}pagination pagination-sm{%- endif -%}'>
+    {%- for page in page_obj.iter_pages() %}
+        {% if page %}
+            {% if page != page_obj.page %}
+                <li><a href="{{ url }}?page={{ page }}">{{ page }}</a></li>
+            {% else %}
+                <li class="active"><a href="#">{{ page }}</a></li>
+            {% endif %}
+        {% endif %}
+    {%- else -%}
+        <li class="active"><a href="#">1</a></li>
+    {%- endfor %}
+    {% if page_obj.has_next %}
+        <li><a href="{{ url }}?page={{ page_obj.next_num }}">&raquo;</a></li>
+    {% endif %}
+</ul>
+{% endmacro %}
+
+
 {%- macro topic_pages(topic_obj, per_page=10) -%}
 {% set topic_pages = (topic_obj.post_count / per_page)|round|int %}
 {%- if topic_pages >  1 -%}

+ 69 - 0
flaskbb/templates/message/conversation.html

@@ -0,0 +1,69 @@
+{% extends theme("message/message_layout.html") %}
+
+{% block css %}
+    {{ super() }}
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
+{% endblock %}
+
+{% block message_content %}
+<div class="panel panel-default">
+    <div class="panel-heading">Subject: <strong>{{ conversation.subject }}</strong></div>
+    <div class="panel-body">
+        <section class="conversation-list">
+            {% for message in conversation.messages %}
+            <!-- First Comment -->
+            <article class="row" id="mid{{message.id}}">
+                {% if message.conversation.from_user_id == message.user_id %}
+                <div class="col-md-2 col-sm-2 hidden-xs">
+                        <figure class="thumbnail">
+                        <img class="img-responsive" src="http://www.keita-gaming.com/assets/profile/default-avatar-c5d8ec086224cb6fc4e395f4ba3018c2.jpg" />
+                        <figcaption class="text-center"><strong><a href="{{ message.user.url }}" class="conversation-username">{{ message.user.username }}</a></strong></figcaption>
+                    </figure>
+                </div>
+                {% endif %}
+                <div class="col-md-10 col-sm-10">
+                    <div class="panel panel-default arrow {% if message.conversation.from_user_id == message.user_id %}left{% else %}right{% endif %}">
+                        <div class="panel-body">
+                            <header class="text-left">
+                                <time class="conversation-date" datetime="{{ message.date_created }}"><i class="fa fa-clock-o"></i> {{ message.date_created|format_date("%d %B %Y - %H:%M") }}</time>
+                            </header>
+                            <div class="conversation-message">
+                                {{ message.message|markup|safe }}
+                            </div>
+                            <p class="text-right"><a href="#" class="btn btn-default btn-sm reply-btn" data-message-id="{{ message.id }}"><i class="fa fa-reply"></i> reply</a></p>
+                        </div>
+                    </div>
+                </div>
+                {% if message.conversation.from_user_id != message.user_id %}
+                <div class="col-md-2 col-sm-2 hidden-xs">
+                        <figure class="thumbnail">
+                        <img class="img-responsive" src="http://www.keita-gaming.com/assets/profile/default-avatar-c5d8ec086224cb6fc4e395f4ba3018c2.jpg" />
+                        <figcaption class="text-center">{{ message.user.username }}</figcaption>
+                    </figure>
+                </div>
+                {% endif %}
+            </article>
+        {% endfor %}
+        </section>
+    </div>
+</div>
+{% if form %}
+    {% from "macros.html" import render_field, render_submit_field %}
+    <form class="form" action="#" method="post">
+        {{ form.hidden_tag() }}
+
+        {{ render_field(form.message, div_class="col-sm-12 message-content", rows="10", placeholder="", **{'data-provide': 'markdown', 'data-autofocus': 'true', 'id': 'quickreply-editor'}) }}
+
+        <div class="col-sm-12" style="padding-top: 5px">
+        {{ render_submit_field(form.submit) }}
+        </div>
+    </form>
+{% endif %}
+
+{% endblock %}
+
+{% block scripts %}
+    <script type="text/javascript" src="{{ url_for('static', filename='js/conversation.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/marked.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-markdown.js') }}"></script>
+{% endblock %}

+ 56 - 32
flaskbb/templates/message/drafts.html

@@ -1,37 +1,61 @@
 {% set page_title = _("Drafts") %}
 
+{% from theme('macros.html') import render_pagination %}
 {% extends theme("message/message_layout.html") %}
 {% block message_content %}
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>{% trans %}To{% endtrans %}</th>
-            <th>{% trans %}Subject{% endtrans %}</th>
-            <th>{% trans %}Date{% endtrans %}</th>
-            <th>{% trans %}Options{% endtrans %}</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for message in messages %}
-        <tr>
-            <td><a href="{{ url_for('user.profile', username=message.to_user.username) }}">{{ message.to_user.username }}</a></td>
-            <td><a href="{{ url_for('user.view_message', message_id=message.id) }}">{% if message.subject %}{{ message.subject }}{% else %}({% trans %}No Subject{% endtrans %}){% endif %}</a></td>
-            <td>{{ message.date_created|format_date('%d %B %Y') }}</td>
-            <td>
-                <a href="{{ url_for('user.edit_message', message_id=message.id) }}">{% trans %}Continue{% endtrans %}</a> |
-                <form class="inline-form" method="post" action="{{ url_for('user.move_message', message_id=message.id) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                </form>
-            </td>
-        </tr>
-        {% else %}
-        <tr>
-            <td colspan="4">
-                {% trans %}This message folder is empty.{% endtrans %}
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+
+<div class="panel panel-default conversation">
+    <div class="panel-heading">
+        <span class="glyphicon glyphicon-comment"></span>
+        <h3 class="panel-title">{% trans %}Saved Conversations{% endtrans %}</h3>
+        <span class="label label-info">{{ message_count }}/{{ flaskbb_config["MESSAGE_QUOTA"] }}</span>
+    </div>
+    <div class="panel-body">
+        <ul class="list-group">
+            {% for conversation in conversations.items %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-2 col-md-1">
+                        {% if conversation.to_user.avatar %}
+                        <img src="{{ url_for('conversation.to_user.avatar') }}" class="img-circle img-responsive" alt="" />
+                        {% else %}
+                        <span class="fa fa-user" style="font-size: 80px; color: #E5E5E5"></span>
+                        {% endif %}
+                    </div>
+                    <div class="col-xs-10 col-md-11">
+                        <div>
+                            <a href="{{ url_for('message.view_conversation', conversation_id=conversation.id) }}">
+                                <strong>{{ conversation.subject }}</strong>
+                            </a>
+                            <div class="mic-info">
+                                To: <a href="#">{{ conversation.to_user.username }}</a> on {{ conversation.date_created|format_date("%d %B %Y - %H:%M") }}
+                            </div>
+                        </div>
+                        <div class="comment-text">
+                            {# the first message of conversation is always the starting message #}
+                            {{ conversation.messages[0].message }}
+                        </div>
+                        <div class="action">
+                            <button type="button" class="btn btn-danger btn-xs" title="Delete">
+                                <span class="glyphicon glyphicon-trash"></span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            {% else %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {% trans %}No conversations found.{% endtrans %}
+                    </div>
+                </div>
+            </li>
+            {% endfor %}
+        </ul>
+    </div>
+</div>
+
+{{ render_pagination(conversations, url_for("message.drafts")) }}
+
 {% endblock %}

+ 56 - 39
flaskbb/templates/message/inbox.html

@@ -1,44 +1,61 @@
 {% set page_title = _("Inbox") %}
 
+{% from theme('macros.html') import render_pagination %}
 {% extends theme("message/message_layout.html") %}
 {% block message_content %}
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>{% trans %}From{% endtrans %}</th>
-            <th>{% trans %}Subject{% endtrans %}</th>
-            <th>{% trans %}Date{% endtrans %}</th>
-            <th>{% trans %}Options{% endtrans %}</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for message in messages %}
-        <tr>
-        {% if message.from_user %}
-            <td><a href="{{ url_for('user.profile', username=message.from_user.username) }}">{{ message.from_user.username }}</a></td>
-        {% else %}
-            <td>[{% trans %}deleted{% endtrans %}]</td>
-        {% endif %}
-            <td>
-                <a href="{{ url_for('user.view_message', message_id=message.id) }}">
-                    {% if message.unread %}<strong>{{ message.subject }}</strong>{% else %}{{ message.subject }}{% endif %}
-                </a>
-            </td>
-            <td>{{ message.date_created|time_since }}</td>
-            <td>
-                <form class="inline-form" method="post" action="{{ url_for('user.move_message', message_id=message.id) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                </form>
-            </td>
-        </tr>
-        {% else %}
-        <tr>
-            <td colspan="4">
-                {% trans %}This message folder is empty.{% endtrans %}
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+
+<div class="panel panel-default conversation">
+    <div class="panel-heading">
+        <span class="glyphicon glyphicon-comment"></span>
+        <h3 class="panel-title">{% trans %}Conversations{% endtrans %}</h3>
+        <span class="label label-info">{{ message_count }}/{{ flaskbb_config["MESSAGE_QUOTA"] }}</span>
+    </div>
+    <div class="panel-body">
+        <ul class="list-group">
+            {% for conversation in conversations.items %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-2 col-md-1">
+                        {% if conversation.from_user.avatar %}
+                        <img src="{{ url_for('conversation.from_user.avatar') }}" class="img-circle img-responsive" alt="" />
+                        {% else %}
+                        <span class="fa fa-user" style="font-size: 80px; color: #E5E5E5"></span>
+                        {% endif %}
+                    </div>
+                    <div class="col-xs-10 col-md-11">
+                        <div>
+                            <a href="{{ url_for('message.view_conversation', conversation_id=conversation.id) }}">
+                                <strong>{{ conversation.subject }}</strong>
+                            </a>
+                            <div class="mic-info">
+                                By: <a href="#">{{ conversation.from_user.username }}</a> on {{ conversation.date_created|format_date("%d %B %Y - %H:%M") }}
+                            </div>
+                        </div>
+                        <div class="comment-text">
+                            {# the first message of conversation is always the starting message #}
+                            {{ conversation.messages[0].message }}
+                        </div>
+                        <div class="action">
+                            <button type="button" class="btn btn-danger btn-xs" title="Delete">
+                                <span class="glyphicon glyphicon-trash"></span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            {% else %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {% trans %}No conversations found.{% endtrans %}
+                    </div>
+                </div>
+            </li>
+            {% endfor %}
+        </ul>
+    </div>
+</div>
+
+{{ render_pagination(conversations, url_for("message.inbox")) }}
+
 {% endblock %}

+ 7 - 7
flaskbb/templates/message/message_layout.html

@@ -9,18 +9,18 @@
 </ul>
 
 <div class="row">
-    <div class="col-sm-3">
+    <div class="col-sm-2">
         <div class="sidebar">
             <ul class="nav sidenav">
-                <li class="nav-header">{% trans %}Options{% endtrans %} | <small><a href="{{ url_for('user.new_message') }}">{% trans %}New PM{% endtrans %}</a></small></li>
-                {{ navlink('user.inbox', _('Inbox')) }}
-                {{ navlink('user.sent', _('Sent')) }}
-                {{ navlink('user.drafts', _('Drafts')) }}
-                {{ navlink('user.trash', _('Trash')) }}
+                <a href="#" class="btn btn-success btn-compose">Compose</a>
+                {{ navlink('message.inbox', _('Inbox')) }}
+                {{ navlink('message.sent', _('Sent')) }}
+                {{ navlink('message.drafts', _('Drafts')) }}
+                {{ navlink('message.trash', _('Trash')) }}
             </ul>
         </div><!--/.sidebar -->
     </div><!--/.col-sm-3 -->
-    <div class="col-sm-9">
+    <div class="col-sm-10">
         {% block message_content %}{% endblock %}
     </div><!--/.col-sm-9 -->
 </div><!--/.row -->

+ 57 - 35
flaskbb/templates/message/sent.html

@@ -1,40 +1,62 @@
 {% set page_title = _("Sent Messages") %}
 
+{% from theme('macros.html') import render_pagination %}
 {% extends theme("message/message_layout.html") %}
 {% block message_content %}
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>{% trans %}To{% endtrans %}</th>
-            <th>{% trans %}Subject{% endtrans %}</th>
-            <th>{% trans %}Date{% endtrans %}</th>
-            <th>{% trans %}Options{% endtrans %}</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for message in messages %}
-        <tr>
-        {% if message.to_user %}
-            <td><a href="{{ url_for('user.profile', username=message.to_user.username) }}">{{ message.to_user.username }}</a></td>
-        {% else %}
-            <td>[{% trans %}deleted{% endtrans %}]</td>
-        {% endif %}
-            <td><a href="{{ url_for('user.view_message', message_id=message.id) }}">{{ message.subject }}</a></td>
-            <td>{{ message.date_created|time_since }}</td>
-            <td>
-                <form class="inline-form" method="post" action="{{ url_for('user.move_message', message_id=message.id) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                </form>
-            </td>
-        </tr>
-        {% else %}
-        <tr>
-            <td colspan="4">
-                {% trans %}This message folder is empty.{% endtrans %}
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+
+<div class="panel panel-default conversation">
+    <div class="panel-heading">
+        <span class="glyphicon glyphicon-comment"></span>
+        <h3 class="panel-title">{% trans %}Sent Conversations{% endtrans %}</h3>
+        <span class="label label-info">{{ message_count }}/{{ flaskbb_config["MESSAGE_QUOTA"] }}</span>
+    </div>
+    <div class="panel-body">
+        <ul class="list-group">
+            {% for conversation in conversations.items %}
+
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-2 col-md-1">
+                        {% if conversation.to_user.avatar %}
+                        <img src="{{ url_for('conversation.to_user.avatar') }}" class="img-circle img-responsive" alt="" />
+                        {% else %}
+                        <span class="fa fa-user" style="font-size: 80px; color: #E5E5E5"></span>
+                        {% endif %}
+                    </div>
+                    <div class="col-xs-10 col-md-11">
+                        <div>
+                            <a href="{{ url_for('message.view_conversation', conversation_id=conversation.id) }}">
+                                <strong>{{ conversation.subject }}</strong>
+                            </a>
+                            <div class="mic-info">
+                                To: <a href="#">{{ conversation.to_user.username }}</a> on {{ conversation.date_created|format_date("%d %B %Y - %H:%M") }}
+                            </div>
+                        </div>
+                        <div class="comment-text">
+                            {# the first message of conversation is always the starting message #}
+                            {{ conversation.messages[0].message }}
+                        </div>
+                        <div class="action">
+                            <button type="button" class="btn btn-danger btn-xs" title="Delete">
+                                <span class="glyphicon glyphicon-trash"></span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            {% else %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {% trans %}No conversations found.{% endtrans %}
+                    </div>
+                </div>
+            </li>
+            {% endfor %}
+        </ul>
+    </div>
+</div>
+
+{{ render_pagination(conversations, url_for("message.inbox")) }}
+
 {% endblock %}

+ 57 - 35
flaskbb/templates/message/trash.html

@@ -1,40 +1,62 @@
 {% set page_title = _("Trash") %}
 
+{% from theme('macros.html') import render_pagination %}
 {% extends theme("message/message_layout.html") %}
 {% block message_content %}
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>{% trans %}To{% endtrans %}</th>
-            <th>{% trans %}Subject{% endtrans %}</th>
-            <th>{% trans %}Date{% endtrans %}</th>
-            <th>{% trans %}Options{% endtrans %}</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for message in messages %}
-        <tr>
-            <td>{% if message.to_user %}<a href="{{ url_for('user.profile', username=message.to_user.username) }}">{{ message.to_user.username }}</a>{% else %} ({% trans %}No User{% endtrans %}) {% endif %}</td>
-            <td><a href="{{ url_for('user.view_message', message_id=message.id) }}">{% if message.subject %}{{ message.subject }}{% else %}({% trans %}No Subject{% endtrans %}){% endif %}</a></td>
-            <td>{{ message.date_created|format_date }}</td>
-            <td>
-                <form class="inline-form" method="post" action="{{ url_for('user.restore_message', message_id=message.id) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Restore{% endtrans %}</button> |
-                </form>
-                <form class="inline-form" method="post" action="{{ url_for('user.delete_message', message_id=message.id) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                </form>
-            </td>
-        </tr>
-        {% else %}
-        <tr>
-            <td colspan="4">
-                {% trans %}This message folder is empty.{% endtrans %}
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+
+<div class="panel panel-default conversation">
+    <div class="panel-heading">
+        <span class="glyphicon glyphicon-comment"></span>
+        <h3 class="panel-title">{% trans %}Deleted Conversations{% endtrans %}</h3>
+        <span class="label label-info">{{ message_count }}/{{ flaskbb_config["MESSAGE_QUOTA"] }}</span>
+    </div>
+    <div class="panel-body">
+        <ul class="list-group">
+            {% for conversation in conversations.items %}
+
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-2 col-md-1">
+                        {% if conversation.from_user.avatar %}
+                        <img src="{{ url_for('conversation.from_user.avatar') }}" class="img-circle img-responsive" alt="" />
+                        {% else %}
+                        <span class="fa fa-user" style="font-size: 80px; color: #E5E5E5"></span>
+                        {% endif %}
+                    </div>
+                    <div class="col-xs-10 col-md-11">
+                        <div>
+                            <a href="{{ url_for('message.view_conversation', conversation_id=conversation.id) }}">
+                                <strong>{{ conversation.subject }}</strong>
+                            </a>
+                            <div class="mic-info">
+                                By: <a href="#">{{ conversation.from_user.username }}</a> on {{ conversation.date_created|format_date("%d %B %Y - %H:%M") }}
+                            </div>
+                        </div>
+                        <div class="comment-text">
+                            {# the first message of conversation is always the starting message #}
+                            {{ conversation.messages[0].message }}
+                        </div>
+                        <div class="action">
+                            <button type="button" class="btn btn-danger btn-xs" title="Delete">
+                                <span class="glyphicon glyphicon-trash"></span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </li>
+            {% else %}
+            <li class="list-group-item">
+                <div class="row">
+                    <div class="col-xs-12">
+                        {% trans %}No conversations found.{% endtrans %}
+                    </div>
+                </div>
+            </li>
+            {% endfor %}
+        </ul>
+    </div>
+</div>
+
+{{ render_pagination(conversations, url_for("message.inbox")) }}
+
 {% endblock %}

+ 0 - 58
flaskbb/templates/message/view_message.html

@@ -1,58 +0,0 @@
-{% extends theme("message/message_layout.html") %}
-{% block message_content %}
-<table class="table table-bordered">
-    <tbody>
-        <tr>
-        <td>
-            <table class="table table-borderless" border="0">
-
-                <tr>
-                {% if message.from_user %}
-                    {% if message.from_user.avatar %}
-                    <td width="1">
-                        <img src="{{ message.from_user.avatar }}" alt="Avatar" height="100" width="100">
-                    </td>
-                    {% endif %}
-
-                    <td>
-                        <a href="{{ url_for('user.profile', username=message.from_user.username) }}"><span style="color: green;"><strong><em>{{ message.from_user.username }}</em></strong></span></a>
-                            {% if message.from_user|is_online %}
-                            <span class="badge badge-success">Online</span>
-                            {% else %}
-                            <span class="badge badge-default">Offline</span>
-                            {% endif %}
-                        <br />
-                        {{ message.from_user.primary_group.name }}<br />
-                    </td>
-
-                    <td class="pull-right">
-                        {% trans %}Posts{% endtrans %}: {{ message.from_user.post_count }}<br />
-                        {% trans %}Registered since{% endtrans %}: {{ message.from_user.date_joined|format_date('%b %d %Y') }}<br />
-                    </td>
-                {% else %}
-                    [{% trans %}deleted{% endtrans %}]
-                {% endif %}
-                </tr>
-
-            </table>
-        </td>
-        </tr>
-
-        <tr>
-            <td>
-                <div class="message_body" id="mid{{ message.id }}">
-                    {% if message.message %}
-                        {% autoescape false %}
-                        {{ message.message|markup }}
-                        {% endautoescape %}
-                    {% else %}
-                        ({% trans %}No Message{% endtrans %})
-                    {% endif %}
-                </div>
-            </td>
-        </tr>
-
-    </tbody>
-</table>
-
-{% endblock %}

+ 2 - 2
flaskbb/themes/bootstrap2/templates/layout.html

@@ -80,8 +80,8 @@
                                 <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
                             </button>
                             <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('user.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Inbox{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('user.new_message') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Inbox{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
                             </ul>
                         </div>
                     {% else %}

+ 129 - 0
flaskbb/themes/bootstrap3/static/css/flaskbb.css

@@ -87,6 +87,11 @@ margin-bottom: 0px;
   border-radius: 5px;
 }
 
+.sidenav .btn-compose {
+    margin-left: 20px;
+    margin-bottom: 5px;
+}
+
 /* All levels of nav */
 .sidebar .nav > li > a {
   display: block;
@@ -240,3 +245,127 @@ margin-bottom: 0px;
   width: 20px;
   height: 20px;
 }
+
+.conversation .panel-body {
+    padding:0px;
+}
+
+.conversation .list-group {
+    margin-bottom: 0;
+}
+
+.conversation .panel-title {
+    display:inline;
+}
+
+.conversation .label-info {
+    float: right;
+}
+
+.conversation li.list-group-item {
+    border-radius: 0;
+    border: 0;
+    border-top: 1px solid #ddd;
+}
+
+.conversation li.list-group-item:hover {
+    background-color: rgba(86,61,124,.1);
+}
+
+.conversation .mic-info {
+    color: #666666;
+    font-size: 11px;
+}
+
+.conversation .action {
+    margin-top:5px;
+}
+
+.conversation .comment-text {
+    font-size: 12px;
+}
+
+.conversation .btn-block {
+    border-top-left-radius:0px;
+    border-top-right-radius:0px;
+}
+
+/*Comment List styles*/
+.conversation-list .row {
+  margin-bottom: 0px;
+}
+.conversation-list .panel .panel-heading {
+  padding: 4px 15px;
+  position: absolute;
+  border:none;
+  /*Panel-heading border radius*/
+  border-top-right-radius:0px;
+  top: 1px;
+}
+.conversation-list .panel .panel-heading.right {
+  border-right-width: 0px;
+  /*Panel-heading border radius*/
+  border-top-left-radius:0px;
+  right: 16px;
+}
+.conversation-list .panel .panel-heading .panel-body {
+  padding-top: 6px;
+}
+.conversation-list figcaption {
+  /*For wrapping text in thumbnail*/
+  word-wrap: break-word;
+}
+/* Portrait tablets and medium desktops */
+@media (min-width: 768px) {
+  .conversation-list .arrow:after, .conversation-list .arrow:before {
+    content: "";
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-color: transparent;
+  }
+  .conversation-list .panel.arrow.left:after, .conversation-list .panel.arrow.left:before {
+    border-left: 0;
+  }
+  /*****Left Arrow*****/
+  /*Outline effect style*/
+  .conversation-list .panel.arrow.left:before {
+    left: 0px;
+    top: 30px;
+    /*Use boarder color of panel*/
+    border-right-color: inherit;
+    border-width: 16px;
+  }
+  /*Background color effect*/
+  .conversation-list .panel.arrow.left:after {
+    left: 1px;
+    top: 31px;
+    /*Change for different outline color*/
+    border-right-color: #FFFFFF;
+    border-width: 15px;
+  }
+  /*****Right Arrow*****/
+  /*Outline effect style*/
+  .conversation-list .panel.arrow.right:before {
+    right: -16px;
+    top: 30px;
+    /*Use boarder color of panel*/
+    border-left-color: inherit;
+    border-width: 16px;
+  }
+  /*Background color effect*/
+  .conversation-list .panel.arrow.right:after {
+    right: -14px;
+    top: 31px;
+    /*Change for different outline color*/
+    border-left-color: #FFFFFF;
+    border-width: 15px;
+  }
+}
+.conversation-list .conversation-message {
+  margin-top: 6px;
+}
+.conversation-list .conversation-date {
+    color: #999999;
+}

+ 2 - 2
flaskbb/themes/bootstrap3/templates/layout.html

@@ -79,8 +79,8 @@
                                 <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
                             </button>
                             <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('user.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Inbox{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('user.new_message') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Inbox{% endtrans %}</a></li>
+                                <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
                             </ul>
                         </div>
                     {% else %}

+ 1 - 36
flaskbb/user/forms.py

@@ -16,7 +16,7 @@ from wtforms.validators import (Length, DataRequired, InputRequired, Email,
                                 EqualTo, Optional, URL)
 from flask_babelex import lazy_gettext as _
 
-from flaskbb.user.models import User, PrivateMessage
+from flaskbb.user.models import User
 from flaskbb.extensions import db
 from flaskbb.utils.widgets import SelectBirthdayWidget
 from flaskbb.utils.fields import BirthdayField
@@ -114,38 +114,3 @@ class ChangeUserDetailsForm(Form):
             if error is not None:
                 raise ValidationError(error)
             return status
-
-
-class NewMessageForm(Form):
-    to_user = StringField(_("To User"), validators=[
-        DataRequired(message=_("A Username is required."))])
-
-    subject = StringField(_("Subject"), validators=[
-        DataRequired(message=_("A Subject is required."))])
-
-    message = TextAreaField(_("Message"), validators=[
-        DataRequired(message=_("A Message is required."))])
-
-    send_message = SubmitField(_("Send Message"))
-    save_message = SubmitField(_("Save Message"))
-
-    def validate_to_user(self, field):
-        user = User.query.filter_by(username=field.data).first()
-        if not user:
-            raise ValidationError(_("The Username you entered doesn't exist"))
-        if user.id == current_user.id:
-            raise ValidationError(_("You cannot send a PM to yourself."))
-
-    def save(self, from_user, to_user, user_id, unread, as_draft=False):
-        message = PrivateMessage(
-            subject=self.subject.data,
-            message=self.message.data,
-            unread=unread)
-
-        if as_draft:
-            return message.save(from_user, to_user, user_id, draft=True)
-        return message.save(from_user, to_user, user_id)
-
-
-class EditMessageForm(NewMessageForm):
-    pass

+ 2 - 60
flaskbb/user/models.py

@@ -21,6 +21,7 @@ from flaskbb.extensions import db, cache
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
                                   ForumsRead)
+from flaskbb.message.models import Conversation
 
 
 groups_users = db.Table(
@@ -417,7 +418,7 @@ class User(db.Model, UserMixin):
         """Deletes the User."""
 
         # This isn't done automatically...
-        PrivateMessage.query.filter_by(user_id=self.id).delete()
+        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()
 
@@ -463,62 +464,3 @@ class Guest(AnonymousUserMixin):
         """Invalidates this objects cached metadata."""
 
         cache.delete_memoized(cls.get_permissions, cls)
-
-
-class PrivateMessage(db.Model):
-    __tablename__ = "privatemessages"
-
-    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"))
-    to_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
-    subject = db.Column(db.String(255))
-    message = db.Column(db.Text)
-    date_created = db.Column(db.DateTime, default=datetime.utcnow())
-    trash = db.Column(db.Boolean, nullable=False, default=False)
-    draft = db.Column(db.Boolean, nullable=False, default=False)
-    unread = db.Column(db.Boolean, nullable=False, default=True)
-
-    user = db.relationship("User", backref="pms", lazy="joined",
-                           foreign_keys=[user_id])
-    from_user = db.relationship("User", lazy="joined",
-                                foreign_keys=[from_user_id])
-    to_user = db.relationship("User", lazy="joined", foreign_keys=[to_user_id])
-
-    def save(self, from_user=None, to_user=None, user_id=None, draft=False):
-        """Saves a private message.
-
-        :param from_user: The user who has sent the message
-
-        :param to_user: The user who should recieve the message
-
-        :param user_id: The senders user id - This is the id to which user the
-                        Inbox belongs.
-
-        :param draft: If the message is a draft. Defaults to ``False``.
-        """
-
-        if self.id:
-            db.session.add(self)
-            db.session.commit()
-            return self
-
-        # Defaults to ``False``.
-        self.draft = draft
-
-        # Add the message to the user's pm box
-        self.user_id = user_id
-        self.from_user_id = from_user
-        self.to_user_id = to_user
-        self.date_created = datetime.utcnow()
-
-        db.session.add(self)
-        db.session.commit()
-        return self
-
-    def delete(self):
-        """Deletes a private message"""
-
-        db.session.delete(self)
-        db.session.commit()
-        return self

+ 4 - 179
flaskbb/user/views.py

@@ -9,19 +9,16 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
-from datetime import datetime
-
-from flask import Blueprint, flash, request, redirect, url_for
+from flask import Blueprint, flash, request
 from flask_login import login_required, current_user
 from flask_themes2 import get_themes_list
 from flask_babelex import gettext as _
 
-from flaskbb.extensions import db, babel
+from flaskbb.extensions import babel
 from flaskbb.utils.helpers import render_template
-from flaskbb.user.models import User, PrivateMessage
+from flaskbb.user.models import User
 from flaskbb.user.forms import (ChangePasswordForm, ChangeEmailForm,
-                                ChangeUserDetailsForm, GeneralSettingsForm,
-                                NewMessageForm, EditMessageForm)
+                                ChangeUserDetailsForm, GeneralSettingsForm)
 
 
 user = Blueprint("user", __name__)
@@ -110,175 +107,3 @@ def change_user_details():
         flash(_("Details updated."), "success")
 
     return render_template("user/change_user_details.html", form=form)
-
-
-@user.route("/messages")
-@user.route("/messages/inbox")
-@login_required
-def inbox():
-    messages = PrivateMessage.query.filter(
-        PrivateMessage.user_id == current_user.id,
-        PrivateMessage.draft == False,
-        PrivateMessage.trash == False,
-        db.not_(PrivateMessage.from_user_id == current_user.id)).all()
-    return render_template("message/inbox.html", messages=messages)
-
-
-@user.route("/messages/<int:message_id>/view")
-@login_required
-def view_message(message_id):
-    message = PrivateMessage.query.filter_by(id=message_id).first()
-    if message.unread:
-        message.unread = False
-        db.session.commit()
-    return render_template("message/view_message.html", message=message)
-
-
-@user.route("/messages/sent")
-@login_required
-def sent():
-    messages = PrivateMessage.query.filter(
-        PrivateMessage.user_id == current_user.id,
-        PrivateMessage.draft == False,
-        PrivateMessage.trash == False,
-        db.not_(PrivateMessage.to_user_id == current_user.id)).all()
-    return render_template("message/sent.html", messages=messages)
-
-
-@user.route("/messages/trash")
-@login_required
-def trash():
-    messages = PrivateMessage.query.filter(
-        PrivateMessage.user_id == current_user.id,
-        PrivateMessage.trash == True).all()
-    return render_template("message/trash.html", messages=messages)
-
-
-@user.route("/messages/draft")
-@login_required
-def drafts():
-    messages = PrivateMessage.query.filter(
-        PrivateMessage.user_id == current_user.id,
-        PrivateMessage.draft == True,
-        PrivateMessage.trash == False).all()
-    return render_template("message/drafts.html", messages=messages)
-
-
-@user.route("/messages/new", methods=["POST", "GET"])
-@login_required
-def new_message():
-    form = NewMessageForm()
-    to_user = request.args.get("to_user")
-
-    if request.method == "POST":
-        if "save_message" in request.form and form.validate():
-            to_user = User.query.filter_by(username=form.to_user.data).first()
-
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=current_user.id,
-                      unread=False,
-                      as_draft=True)
-
-            flash(_("Message saved."), "success")
-            return redirect(url_for("user.drafts"))
-
-        if "send_message" in request.form and form.validate():
-            to_user = User.query.filter_by(username=form.to_user.data).first()
-
-            # Save the message in the current users inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=current_user.id,
-                      unread=False)
-
-            # Save the message in the recievers inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=to_user.id,
-                      unread=True)
-
-            flash(_("Message sent."), "success")
-            return redirect(url_for("user.sent"))
-    else:
-        form.to_user.data = to_user
-
-    return render_template("message/message_form.html", form=form,
-                           title=_("Compose Message"))
-
-
-@user.route("/messages/<int:message_id>/edit", methods=["POST", "GET"])
-@login_required
-def edit_message(message_id):
-    message = PrivateMessage.query.filter_by(id=message_id).first_or_404()
-
-    if not message.draft:
-        flash(_("You cannot edit a sent message."), "danger")
-        return redirect(url_for("user.inbox"))
-
-    form = EditMessageForm()
-
-    if request.method == "POST":
-        if "save_message" in request.form:
-            to_user = User.query.filter_by(username=form.to_user.data).first()
-
-            # Move the message from ``Drafts`` to ``Sent``.
-            message.draft = False
-            message.to_user = to_user.id
-            message.save()
-
-            flash(_("Message saved."), "success")
-            return redirect(url_for("user.drafts"))
-
-        if "send_message" in request.form and form.validate():
-            to_user = User.query.filter_by(username=form.to_user.data).first()
-            # Save the message in the recievers inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=to_user.id,
-                      unread=True)
-
-            # Move the message from ``Drafts`` to ``Sent``.
-            message.draft = False
-            message.to_user = to_user
-            message.date_created = datetime.utcnow()
-            message.save()
-
-            flash(_("Message sent."), "success")
-            return redirect(url_for("user.sent"))
-    else:
-        form.to_user.data = message.to_user.username
-        form.subject.data = message.subject
-        form.message.data = message.message
-
-    return render_template("message/message_form.html", form=form,
-                           title=_("Edit Message"))
-
-
-@user.route("/messages/<int:message_id>/move", methods=["POST"])
-@login_required
-def move_message(message_id):
-    message = PrivateMessage.query.filter_by(id=message_id).first_or_404()
-    message.trash = True
-    message.save()
-    flash(_("Message moved to Trash."), "success")
-    return redirect(url_for("user.inbox"))
-
-
-@user.route("/messages/<int:message_id>/restore", methods=["POST"])
-@login_required
-def restore_message(message_id):
-    message = PrivateMessage.query.filter_by(id=message_id).first_or_404()
-    message.trash = False
-    message.save()
-    flash(_("Message restored from Trash."), "success")
-    return redirect(url_for("user.inbox"))
-
-
-@user.route("/messages/<int:message_id>/delete", methods=["POST"])
-@login_required
-def delete_message(message_id):
-    message = PrivateMessage.query.filter_by(id=message_id).first_or_404()
-    message.delete()
-    flash(_("Message deleted."), "success")
-    return redirect(url_for("user.inbox"))

+ 7 - 6
flaskbb/utils/helpers.py

@@ -323,15 +323,16 @@ def time_since(time):  # pragma: no cover
     return format_timedelta(delta, add_direction=True, locale=locale)
 
 
-def format_quote(post):
+def format_quote(username, content):
     """Returns a formatted quote depending on the markup language.
 
-    :param post: The quoted post.
+    :param username: The username of a user.
+    :param content: The content of the quote
     """
-    profile_url = url_for('user.profile', username=post.username)
-    content = "\n> ".join(post.content.strip().split('\n'))
-    quote = "**[{post.username}]({profile_url}) wrote:**\n> {content}\n".\
-            format(post=post, profile_url=profile_url, content=content)
+    profile_url = url_for('user.profile', username=username)
+    content = "\n> ".join(content.strip().split('\n'))
+    quote = "**[{username}]({profile_url}) wrote:**\n> {content}\n".\
+            format(username=username, profile_url=profile_url, content=content)
 
     return quote
 

+ 5 - 0
flaskbb/utils/populate.py

@@ -236,6 +236,11 @@ def create_test_data(users=5, categories=2, forums=2, topics=1, posts=1):
     user1 = User.query.filter_by(id=1).first()
     user2 = User.query.filter_by(id=2).first()
 
+    # lets send them a few private messages
+    for i in range(1, 3):
+        # TODO
+        pass
+
     # create 2 categories
     for i in range(1, categories + 1):
         category_title = "Test Category %s" % i