view messages/views.py @ 887:9a15f7c27526

Actually save model object upon change. This commit was tested on the comments model. Additional logging added. Added check for Markdown image references. Added TODOs after observing behavior on comments.
author Brian Neal <bgneal@gmail.com>
date Tue, 03 Feb 2015 21:09:44 -0600
parents cf486a8e8b43
children 79a71b9d0a2a
line wrap: on
line source
"""
Views for the messages application.

"""
import datetime

from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.contrib import messages as django_messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect
from django.db.models import Q
from django.template.loader import render_to_string
from django.contrib.sites.models import Site
from django.conf import settings

from messages.models import Flag, Message, Options
from messages.forms import OptionsForm, ComposeForm, ReportForm
from messages.utils import reply_subject
from messages import MSG_BOX_LIMIT
from core.functions import email_admins, quote_message, send_mail


MSGS_PER_PAGE = 20      # message pagination value
PM_EMAIL_MAX = 100      # max messages we will send via email

REPORT_SUBJECT = 'A user has flagged a private message'
REPORT_MSG = """Hello,

A user has flagged a private message for review.
"""


def _quota_check(box_name, count, request):
    """
    Checks the message box count against MSG_BOX_LIMIT.
    Emits a message to the user if the quota is exceeded.

    Returns the percent used as an integer between 0-100.

    """
    if count >= MSG_BOX_LIMIT:
        django_messages.warning(request,
            "Your %s is full. Please delete some messages." % box_name)

    return 100 * count / MSG_BOX_LIMIT


def _get_page(request, qs):
    """Paginates the given queryset and returns a page object"""
    paginator = Paginator(qs, MSGS_PER_PAGE)
    try:
        page = paginator.page(request.GET.get('page', '1'))
    except PageNotAnInteger:
        page = paginator.page(1)
    except EmptyPage:
        page = paginator.page(paginator.num_pages)
    return page


@login_required
def inbox(request):

    msg_list = Message.objects.inbox(request.user)
    msg_count = msg_list.count()
    pct_used = _quota_check('inbox', msg_count, request)

    page = _get_page(request, msg_list)

    return render(request, 'messages/inbox.html', {
        'tab': 'inbox',
        'page': page,
        'inbox_pct': pct_used,
        'outbox_pct': None,
        })


@login_required
def outbox(request):

    msg_list = Message.objects.outbox(request.user)
    msg_count = msg_list.count()
    pct_used = _quota_check('outbox', msg_count, request)

    page = _get_page(request, msg_list)

    return render(request, 'messages/outbox.html', {
        'tab': 'outbox',
        'page': page,
        'inbox_pct': None,
        'outbox_pct': pct_used,
        })


@login_required
def trash(request):

    msg_list = Message.objects.trash(request.user)

    page = _get_page(request, msg_list)

    return render(request, 'messages/trash.html', {
        'tab': 'trash',
        'page': page,
        'inbox_pct': None,
        'outbox_pct': None,
        })


@login_required
def options(request):
    """
    This view handles the displaying and changing of private message options.

    """
    if request.method == 'POST':
        options = Options.objects.for_user(request.user)
        form = OptionsForm(request.POST, instance=options, prefix='opts')
        if form.is_valid():
            form.save()
            django_messages.success(request, 'Options saved.')
            return redirect('messages-options')
    else:
        options = Options.objects.for_user(request.user)
        form = OptionsForm(instance=options, prefix='opts')

    return render(request, 'messages/options.html', {
        'tab': 'options',
        'form': form,
        })


@login_required
def compose(request):
    """
    Process or prepare the compose form to create a new private message.

    """
    if request.method == 'POST':
        compose_form = ComposeForm(request.user, request.POST)
        if compose_form.is_valid():
            compose_form.save()
            django_messages.success(request, 'Message sent.')
            compose_form = ComposeForm(request.user)
    else:
        receiver = request.GET.get('to')
        if receiver:
            form_data = {'receiver': receiver}
            compose_form = ComposeForm(request.user, initial=form_data)
        else:
            compose_form = ComposeForm(request.user)

        _quota_check('outbox', Message.objects.outbox(request.user).count(), request)

    return render(request, 'messages/compose.html', {
        'tab': 'compose',
        'compose_form': compose_form,
        })


@login_required
def view(request, msg_id):
    """
    This view function displays a private message for reading to the user. If
    the user is a recipient of the message, a reply can be composed and sent.

    """
    msg = get_object_or_404(Message.objects.select_related(), pk=msg_id)

    if request.method == 'POST':
        form = ComposeForm(request.user, request.POST)
        if form.is_valid():
            form.save()
            django_messages.success(request, 'Reply sent.')
            return redirect('messages-inbox')
    else:
        if msg.sender != request.user and msg.receiver != request.user:
            django_messages.error(request,
                    "You don't have permission to read that message.")
            return redirect('messages-inbox')

        initial_data = {
            'receiver': msg.sender.username,
            'subject': reply_subject(msg.subject),
            'message': quote_message(msg.sender.username, msg.message),
            'parent_id': msg.pk,
        }

        if msg.receiver == request.user:
            if msg.read_date is None:
                msg.read_date = datetime.datetime.now()
                msg.save()
        else:
            initial_data['receiver'] = msg.receiver.username

        form = ComposeForm(request.user, initial=initial_data)

    try:
        msg_flag = msg.flag
    except Flag.DoesNotExist:
        msg_flag = None

    return render(request, 'messages/view_message.html', {
        'msg': msg,
        'form': form,
        'msg_flag': msg_flag,
        })


@login_required
@require_POST
def bulk(request):
    """
    Perform bulk action on a list of PMs.

    """
    pm_ids = request.POST.getlist('pm_ids')

    # Figure out what action to perform
    action = None
    action_val = request.POST.get('action', '')
    if action_val.startswith('Delete'):
        action = _delete_pms
    elif action_val.startswith('Email'):
        action = _email_pms

    if pm_ids and action:
        action(request, pm_ids)

    # Figure out where to redirect to
    src = request.POST.get('src', 'inbox')
    try:
        page = int(request.POST.get('page', '1'))
    except ValueError:
        page = 1

    view_name = 'messages-inbox' if src == 'inbox' else 'messages-outbox'
    url = reverse(view_name) + '?page={}'.format(page)
    return redirect(url)


def _delete_pms(request, pm_ids):
    """
    Process the request to delete the list of PM ids by user.

    Returns the number of PM's deleted.

    """
    user = request.user
    msgs = Message.objects.filter(id__in=pm_ids)
    now = datetime.datetime.now()

    count = 0
    for msg in msgs:
        if msg.sender == user:
            if msg.receiver_delete_date is not None or msg.read_date is None:
                # Both parties deleted the message or receiver hasn't read it
                # yet, we can delete it now
                msg.delete()
            else:
                # receiver still has PM in their inbox
                msg.sender_delete_date = now
                msg.save()
            count += 1
        elif msg.receiver == user:
            if msg.sender_delete_date is not None:
                # both parties deleted the message, we can delete it now
                msg.delete()
            else:
                # sender still has PM in their inbox
                msg.receiver_delete_date = now
                msg.save()
            count += 1

    msg = '{} message{} deleted.'.format(count, '' if count == 1 else 's')
    if count > 0:
        django_messages.success(request, msg)
    else:
        django_messages.error(request, msg)


def _undelete_pms(user, msg_ids):
    """
    Attempts to "undelete" the messages given by the msg_ids list.
    This will only succeed if the user is either the sender or receiver.

    Returns the number of PM's undeleted.

    """
    msgs = Message.objects.filter(id__in=msg_ids)
    count = 0
    for msg in msgs:
        if msg.sender == user:
            msg.sender_delete_date = None
            msg.save()
            count += 1
        elif msg.receiver == user:
            msg.receiver_delete_date = None
            msg.save()
            count += 1
    return count


@login_required
@require_POST
def undelete(request):
    """
    Undeletes the requested PM's. The user must be either a sender or receiver for
    this to work.

    """
    pm_ids = request.POST.getlist('pm_ids')
    if pm_ids:
        count = _undelete_pms(request.user, pm_ids)
        msg = '{} message{} undeleted.'.format(count, '' if count == 1 else 's')
        django_messages.success(request, msg)

    # Figure out where to redirect to
    try:
        page = int(request.POST.get('page', '1'))
    except ValueError:
        page = 1

    url = reverse('messages-trash') + '?page={}'.format(page)
    return redirect(url)


@login_required
def report(request, msg_id):
    """This view is for reporting a PM as spam or abuse.

    """
    msg = get_object_or_404(Message.objects.select_related(), pk=msg_id)
    if msg.receiver != request.user:
        django_messages.error(request, "You can't report this message.")
        return redirect('messages-inbox')
    try:
        msg.flag
    except Flag.DoesNotExist:
        pass
    else:
        django_messages.error(request, "This message has already been reported.")
        return redirect('messages-inbox')

    if request.method == 'POST':
        form = ReportForm(request.POST)
        if form.is_valid():
            flag = form.save(commit=False)
            flag.message = msg
            flag.save()
            email_admins(REPORT_SUBJECT, REPORT_MSG)
            django_messages.success(request,
                    'Message reported. An admin will be notified. Thank you.')
            return redirect('messages-inbox')
    else:
        form = ReportForm()

    return render(request, 'messages/report_message.html', {
        'msg': msg,
        'form': form,
        })


def _email_pms(request, msg_ids):
    """
    Attempts to email the messages given by the msg_ids list to the user.
    This will only succeed if the user is either the sender or receiver.

    Returns the number of PM's sent.

    """
    # Does the user have an email address?
    user = request.user
    email_addr = user.email
    if not email_addr:
        return 0

    msgs = Message.objects.filter(
            Q(sender=user) | Q(receiver=user),
            id__in=msg_ids).order_by('pk')[:PM_EMAIL_MAX]

    count = len(msgs)
    if count == 0:
        return 0

    site = Site.objects.get_current()
    admin_email = settings.ADMINS[0][1]

    subject = 'Private messages from {} - {}'.format(site.domain,
            msgs[0].subject)
    if count > 1:
        subject += ' (et al.)'
    from_email = '{}@{}'.format(settings.GPP_NO_REPLY_EMAIL, site.domain)
    msg_body = render_to_string('messages/pm_email.txt', {
                    'site': site,
                    'user': user,
                    'msgs': msgs,
                    'admin_email': admin_email,
                })
    send_mail(subject, msg_body, from_email, [email_addr], defer=False)

    msg = '{} message{} sent to email.'.format(count, '' if count == 1 else 's')
    if count > 0:
        django_messages.success(request, msg)
    else:
        django_messages.error(request, msg)