diff --git a/django_messages/__init__.py b/django_messages/__init__.py index 4d068f0..471f7f0 100644 --- a/django_messages/__init__.py +++ b/django_messages/__init__.py @@ -1,3 +1,4 @@ VERSION = (0, 5, 1,) __version__ = '.'.join(map(str, VERSION)) -default_app_config = 'django_messages.apps.DjangoMessagesConfig' \ No newline at end of file +default_app_config = 'django_messages.apps.DjangoMessagesConfig' +from . import signals diff --git a/django_messages/admin.py b/django_messages/admin.py index 7511b1a..ea18137 100644 --- a/django_messages/admin.py +++ b/django_messages/admin.py @@ -11,7 +11,7 @@ from notification import models as notification else: notification = None - + from django_messages.models import Message class MessageAdminForm(forms.ModelForm): @@ -75,7 +75,7 @@ def save_model(self, request, obj, form, change): the message is effectively resent to those users. """ obj.save() - + if notification: # Getting the appropriate notice labels for the sender and recipients. if obj.parent_msg is None: @@ -84,7 +84,7 @@ def save_model(self, request, obj, form, change): else: sender_label = 'messages_replied' recipients_label = 'messages_reply_received' - + # Notification for the sender. notification.send([obj.sender], sender_label, {'message': obj,}) @@ -108,5 +108,6 @@ def save_model(self, request, obj, form, change): if notification: # Notification for the recipient. notification.send([user], recipients_label, {'message' : obj,}) - -admin.site.register(Message, MessageAdmin) + +if getattr(settings, 'DJANGO_MESSAGES_ADMIN_PANEL', True): + admin.site.register(Message, MessageAdmin) diff --git a/django_messages/forms.py b/django_messages/forms.py index fd40d1a..19cb462 100644 --- a/django_messages/forms.py +++ b/django_messages/forms.py @@ -1,33 +1,27 @@ from django import forms -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone - -if "notification" in settings.INSTALLED_APPS and getattr(settings, 'DJANGO_MESSAGES_NOTIFY', True): - from notification import models as notification -else: - notification = None - from django_messages.models import Message from django_messages.fields import CommaSeparatedUserField +from django_messages import signals + class ComposeForm(forms.Form): + """ A simple default form for private messages. """ recipient = CommaSeparatedUserField(label=_(u"Recipient")) subject = forms.CharField(label=_(u"Subject"), max_length=120) body = forms.CharField(label=_(u"Body"), - widget=forms.Textarea(attrs={'rows': '12', 'cols':'55'})) - - + widget=forms.Textarea(attrs={'rows': '12', 'cols': '55'})) + def __init__(self, *args, **kwargs): recipient_filter = kwargs.pop('recipient_filter', None) super(ComposeForm, self).__init__(*args, **kwargs) if recipient_filter is not None: self.fields['recipient']._recipient_filter = recipient_filter - - + def save(self, sender, parent_msg=None): recipients = self.cleaned_data['recipient'] subject = self.cleaned_data['subject'] @@ -35,22 +29,20 @@ def save(self, sender, parent_msg=None): message_list = [] for r in recipients: msg = Message( - sender = sender, - recipient = r, - subject = subject, - body = body, + sender=sender, + recipient=r, + subject=subject, + body=body, ) if parent_msg is not None: msg.parent_msg = parent_msg parent_msg.replied_at = timezone.now() parent_msg.save() - msg.save() + msg.save() + signals.message_repled.send(sender=ComposeForm, message=msg, user=sender) + else: + msg.save() + signals.message_sent.send(sender=ComposeForm, message=msg, user=sender) + message_list.append(msg) - if notification: - if parent_msg is not None: - notification.send([sender], "messages_replied", {'message': msg,}) - notification.send([r], "messages_reply_received", {'message': msg,}) - else: - notification.send([sender], "messages_sent", {'message': msg,}) - notification.send([r], "messages_received", {'message': msg,}) return message_list diff --git a/django_messages/management.py b/django_messages/management.py index 366296b..a2fa618 100644 --- a/django_messages/management.py +++ b/django_messages/management.py @@ -12,6 +12,8 @@ def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("messages_reply_received", _("Reply Received"), _("you have received a reply to a message"), default=2) notification.create_notice_type("messages_deleted", _("Message Deleted"), _("you have deleted a message"), default=1) notification.create_notice_type("messages_recovered", _("Message Recovered"), _("you have undeleted a message"), default=1) + notification.create_notice_type("messages_marked_unread", _("Message Marked As Unread"), _("you have marked a message as unread"), default=1) + notification.create_notice_type("messages_purged", _("Message Purged"), _("you have purged a message"), default=1) signals.post_syncdb.connect(create_notice_types, sender=notification) else: diff --git a/django_messages/migrations/0001_initial.py b/django_messages/migrations/0001_initial.py new file mode 100644 index 0000000..bae9d8e --- /dev/null +++ b/django_messages/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), + ('subject', models.CharField(max_length=120, verbose_name='Subject')), + ('body', models.TextField(verbose_name='Body')), + ('sent_at', models.DateTimeField(null=True, verbose_name='sent at', blank=True)), + ('read_at', models.DateTimeField(null=True, verbose_name='read at', blank=True)), + ('replied_at', models.DateTimeField(null=True, verbose_name='replied at', blank=True)), + ('sender_deleted_at', models.DateTimeField(null=True, verbose_name='Sender deleted at', blank=True)), + ('recipient_deleted_at', models.DateTimeField(null=True, verbose_name='Recipient deleted at', blank=True)), + ('parent_msg', models.ForeignKey(null=True, to='django_messages.Message', verbose_name='Parent message', blank=True, related_name='next_messages')), + ('recipient', models.ForeignKey(null=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient', blank=True, related_name='received_messages')), + ('sender', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Sender', related_name='sent_messages')), + ], + options={ + 'ordering': ['-sent_at'], + 'verbose_name_plural': 'Messages', + 'verbose_name': 'Message', + }, + bases=(models.Model,), + ), + ] diff --git a/django_messages/migrations/0002_auto_20140926_1746.py b/django_messages/migrations/0002_auto_20140926_1746.py new file mode 100644 index 0000000..1b013da --- /dev/null +++ b/django_messages/migrations/0002_auto_20140926_1746.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_messages', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='purged_for_recipient', + field=models.BooleanField(db_index=True, verbose_name='Purged for recipient', default=False), + preserve_default=True, + ), + migrations.AddField( + model_name='message', + name='purged_for_sender', + field=models.BooleanField(db_index=True, verbose_name='Purged for sender', default=False), + preserve_default=True, + ), + ] diff --git a/django_messages/migrations/__init__.py b/django_messages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_messages/models.py b/django_messages/models.py index 5dd07f4..941b248 100644 --- a/django_messages/models.py +++ b/django_messages/models.py @@ -1,6 +1,5 @@ from django.conf import settings from django.db import models -from django.db.models import signals from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -38,14 +37,17 @@ def trash_for(self, user): return self.filter( recipient=user, recipient_deleted_at__isnull=False, + purged_for_recipient=False, ) | self.filter( sender=user, sender_deleted_at__isnull=False, + purged_for_sender=False, ) @python_2_unicode_compatible class Message(models.Model): + """ A private message from user to user """ @@ -59,6 +61,8 @@ class Message(models.Model): replied_at = models.DateTimeField(_("replied at"), null=True, blank=True) sender_deleted_at = models.DateTimeField(_("Sender deleted at"), null=True, blank=True) recipient_deleted_at = models.DateTimeField(_("Recipient deleted at"), null=True, blank=True) + purged_for_sender = models.BooleanField(_("Purged for sender"), default=False, db_index=True) + purged_for_recipient = models.BooleanField(_("Purged for recipient"), default=False, db_index=True) objects = MessageManager() @@ -98,8 +102,3 @@ def inbox_count_for(user): mark them seen """ return Message.objects.filter(recipient=user, read_at__isnull=True, recipient_deleted_at__isnull=True).count() - -# fallback for email notification if django-notification could not be found -if "notification" not in settings.INSTALLED_APPS and getattr(settings, 'DJANGO_MESSAGES_NOTIFY', True): - from django_messages.utils import new_message_email - signals.post_save.connect(new_message_email, sender=Message) diff --git a/django_messages/signals.py b/django_messages/signals.py index e69de29..5547ae1 100644 --- a/django_messages/signals.py +++ b/django_messages/signals.py @@ -0,0 +1,64 @@ +from django.conf import settings +from django.dispatch import Signal + +message_deleted = Signal(providing_args=["message", "user"]) +message_sent = Signal(providing_args=["message", "user"]) +message_repled = Signal(providing_args=["message", "user"]) +mesage_recovered = Signal(providing_args=["message", "user"]) +message_marked_as_unread = Signal(providing_args=["message", "user"]) +message_purge = Signal(providing_args=["message", "user"]) + +try: + #If it's during installation, we should configure the settings otherwise it fails + settings.configure() +except RuntimeError: + # Already configured (installation is complete) + pass + +if "notification" in settings.INSTALLED_APPS and getattr(settings, 'DJANGO_MESSAGES_NOTIFY', True): + from notification import models as notification + from django_messages.forms import ComposeForm + from django_messages.views import delete, undelete, unread, purge + + def sent_notification(sender, **kwargs): + msg = kwargs['message'] + notification.send([msg.sender], "messages_sent", {'message': msg}) + notification.send([msg.recipient], "messages_received", {'message': msg}) + + def replied_notification(sender, **kwargs): + msg = kwargs['message'] + notification.send([msg.sender], "messages_replied", {'message': msg}) + notification.send([msg.recipient], "messages_reply_received", {'message': msg}) + + def deleted_notification(sender, **kwargs): + msg = kwargs['message'] + user = kwargs['user'] + notification.send([user], "messages_deleted", {'message': msg}) + + def recovered_notification(sender, **kwargs): + msg = kwargs['message'] + user = kwargs['user'] + notification.send([user], "messages_recovered", {'message': msg}) + + def unread_notification(sender, **kwargs): + msg = kwargs['message'] + user = kwargs['user'] + notification.send([user], "messages_marked_unread", {'message': msg}) + + def purge_notification(sender, **kwargs): + msg = kwargs['message'] + user = kwargs['user'] + notification.send([user], "messages_purged", {'message': msg}) + + message_deleted.connect(deleted_notification, sender=delete) + message_sent.connect(sent_notification, sender=ComposeForm) + message_repled.connect(replied_notification, sender=ComposeForm) + mesage_recovered.connect(recovered_notification, sender=undelete) + message_marked_as_unread.connect(unread_notification, sender=unread) + message_purge.connect(purge_notification, sender=purge) + + # fallback for email notification if django-notification could not be found + from django_messages.utils import new_message_email + from django.db.models import signals + from django_messages.models import Message + signals.post_save.connect(new_message_email, sender=Message) diff --git a/django_messages/templates/django_messages/trash.html b/django_messages/templates/django_messages/trash.html index 02fb11e..a593a4b 100644 --- a/django_messages/templates/django_messages/trash.html +++ b/django_messages/templates/django_messages/trash.html @@ -1,30 +1,31 @@ -{% extends "django_messages/base.html" %} -{% load i18n %} +{% extends "django_messages/base.html" %} +{% load i18n %} {% load url from future %} -{% block content %} +{% block content %}

{% trans "Deleted Messages" %}

-{% if message_list %} +{% if message_list %} -{% for message in message_list %} +{% for message in message_list %} - + {% endfor %}
{% trans "Sender" %}{% trans "Subject" %}{% trans "Date" %}{% trans "Action" %}
{{ message.sender }} + {{ message.subject }} {{ message.sent_at|date:_("DATETIME_FORMAT") }} {% trans "undelete" %}{% trans "purge" %}
{% else %}

{% trans "No messages." %}

-{% endif %} +{% endif %}

{% trans "Deleted Messages are removed from the trash at unregular intervals, don't rely on this feature for long-time storage." %}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/django_messages/templates/django_messages/view.html b/django_messages/templates/django_messages/view.html index e360177..ebb9b83 100644 --- a/django_messages/templates/django_messages/view.html +++ b/django_messages/templates/django_messages/view.html @@ -20,6 +20,7 @@

{% trans "View Message" %}

{% trans "Reply" %} {% endifequal %} {% trans "Delete" %} +{% trans "Mark Unread" %} {% comment %}Example reply_form integration {% if reply_form %} @@ -33,4 +34,4 @@

{% trans "Compose reply"%}

{% endif %} {% endcomment %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/django_messages/templates/notification/messages_mark_unreed/full.txt b/django_messages/templates/notification/messages_mark_unreed/full.txt new file mode 100644 index 0000000..039dcbc --- /dev/null +++ b/django_messages/templates/notification/messages_mark_unreed/full.txt @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans %}You have marked the message as unread '{{ message }}'.{% endblocktrans %} diff --git a/django_messages/templates/notification/messages_mark_unreed/notice.html b/django_messages/templates/notification/messages_mark_unreed/notice.html new file mode 100644 index 0000000..46cbd11 --- /dev/null +++ b/django_messages/templates/notification/messages_mark_unreed/notice.html @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have marked the message as unread {{ message }}.{% endblocktrans %} diff --git a/django_messages/templatetags/inbox.py b/django_messages/templatetags/inbox.py index 9b65e34..6784714 100644 --- a/django_messages/templatetags/inbox.py +++ b/django_messages/templatetags/inbox.py @@ -1,13 +1,40 @@ from django.template import Library, Node, TemplateSyntaxError + class InboxOutput(Node): - def __init__(self, varname=None): + + INBOX = 'inbox' + OUTBOX = 'outbox' + TRASH = 'trash' + + def __init__(self, varname=None, box=INBOX, only_new=True, include_deleted=False): + """ + @param varname: If provided, will assign the result into varname variable in template context + @param box: name of the message box to count + @param only_new: If set to True, will only count the new messages + @param include_deleted: if set to True, will count deleted messages in the box too + """ self.varname = varname + self.only_new = only_new + self.include_deleted = include_deleted + self.box = box def render(self, context): try: user = context['user'] - count = user.received_messages.filter(read_at__isnull=True, recipient_deleted_at__isnull=True).count() + messages = { + 'inbox': user.received_messages.inbox_for(user), + 'outbox': user.sent_messages.outbox_for(user), + 'trash': user.received_messages.trash_for(user) | user.sent_messages.trash_for(user), + }.get(self.box, 'inbox') + + if self.only_new: + messages = messages.filter(read_at__isnull=True) + + if not self.include_deleted: + messages = messages.filter(recipient_deleted_at__isnull=True) + + count = messages.count() except (KeyError, AttributeError): count = '' if self.varname is not None: @@ -16,30 +43,77 @@ def render(self, context): else: return "%s" % (count) -def do_print_inbox_count(parser, token): + +def get_box_count(box=InboxOutput.INBOX, include_deleted=False, only_new=True): """ - A templatetag to show the unread-count for a logged in user. - Returns the number of unread messages in the user's inbox. - Usage:: + Creates and returns a templatetag callable. + @param box: the box name to retreive the objects from + @type box: str + @param include_deleted: if set to True will include deleted messages + @type include_deleted: bool + @param only_new: if set to True will only return the new un-read messages + @type include_deleted: bool + @rtype callable + """ + def do_print_inbox_count(parser, token): + """ + A templatetag to show the message count for a logged in user. + Returns the number of messages in the user's message box. + Usage: - {% load inbox %} - {% inbox_count %} + {% load inbox %} + {% new_inbox_count %} - {# or assign the value to a variable: #} + {# or assign the value to a variable: #} - {% inbox_count as my_var %} - {{ my_var }} + {% new_inbox_count as my_var %} + {{ my_var }} - """ - bits = token.contents.split() - if len(bits) > 1: - if len(bits) != 3: - raise TemplateSyntaxError("inbox_count tag takes either no arguments or exactly two arguments") - if bits[1] != 'as': - raise TemplateSyntaxError("first argument to inbox_count tag must be 'as'") - return InboxOutput(bits[2]) - else: - return InboxOutput() + Tags: + {% new_inbox_count %} {# count of new messages in INBOX #} + {% inbox_count %} {# count of all INBOX #} + {% new_outbox_count %} + {% outbox_count %} + {% trash_count %} + + @type token: django.template.base.Token + """ + bits = token.contents.split() + if len(bits) > 1: + if len(bits) != 3: + raise TemplateSyntaxError("inbox_count tag takes either no arguments or exactly two arguments") + if bits[1] != 'as': + raise TemplateSyntaxError("first argument to inbox_count tag must be 'as'") + return InboxOutput(varname=bits[2], box=box, include_deleted=include_deleted, only_new=only_new) + else: + return InboxOutput(box=box, include_deleted=include_deleted, only_new=only_new) + + return do_print_inbox_count register = Library() -register.tag('inbox_count', do_print_inbox_count) + +setup = { + 'new_inbox_count': { + 'box': InboxOutput.INBOX, + 'include_deleted': False, + }, + 'inbox_count': { + 'box': InboxOutput.INBOX, + 'only_new': False, + }, + 'new_outbox_count': { + 'box': InboxOutput.OUTBOX + }, + 'outbox_count': { + 'box': InboxOutput.OUTBOX, + 'only_new': False, + }, + 'trash_count': { + 'box': InboxOutput.TRASH, + 'include_deleted': True, + 'only_new': False, + }, +} + +for tagname, defaults in setup.items(): + register.tag(tagname, get_box_count(**defaults)) diff --git a/django_messages/urls.py b/django_messages/urls.py index 1effc02..f7194ff 100644 --- a/django_messages/urls.py +++ b/django_messages/urls.py @@ -13,5 +13,7 @@ url(r'^view/(?P[\d]+)/$', view, name='messages_detail'), url(r'^delete/(?P[\d]+)/$', delete, name='messages_delete'), url(r'^undelete/(?P[\d]+)/$', undelete, name='messages_undelete'), + url(r'^mark-unread/(?P[\d]+)/$', unread, name='messages_mark_unread'), url(r'^trash/$', trash, name='messages_trash'), + url(r'^purge/(?P[\d]+)/$', purge, name='messages_purge'), ) diff --git a/django_messages/views.py b/django_messages/views.py index 3341c32..4731cf0 100644 --- a/django_messages/views.py +++ b/django_messages/views.py @@ -1,23 +1,20 @@ from django.http import Http404, HttpResponseRedirect -from django.shortcuts import render_to_response, get_object_or_404 +from django.shortcuts import render_to_response, get_object_or_404, render from django.template import RequestContext from django.contrib import messages from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ from django.utils import timezone from django.core.urlresolvers import reverse -from django.conf import settings from django_messages.models import Message from django_messages.forms import ComposeForm from django_messages.utils import format_quote, get_user_model, get_username_field +from . import signals + User = get_user_model() -if "notification" in settings.INSTALLED_APPS and getattr(settings, 'DJANGO_MESSAGES_NOTIFY', True): - from notification import models as notification -else: - notification = None @login_required def inbox(request, template_name='django_messages/inbox.html'): @@ -72,7 +69,6 @@ def compose(request, recipient=None, form_class=ComposeForm, ``success_url``: where to redirect after successfull submission """ if request.method == "POST": - sender = request.user form = form_class(request.POST, recipient_filter=recipient_filter) if form.is_valid(): form.save(sender=request.user) @@ -109,7 +105,6 @@ def reply(request, message_id, form_class=ComposeForm, raise Http404 if request.method == "POST": - sender = request.user form = form_class(request.POST, recipient_filter=recipient_filter) if form.is_valid(): form.save(sender=request.user, parent_msg=parent) @@ -127,6 +122,7 @@ def reply(request, message_id, form_class=ComposeForm, 'form': form, }, context_instance=RequestContext(request)) + @login_required def delete(request, message_id, success_url=None): """ @@ -156,12 +152,13 @@ def delete(request, message_id, success_url=None): deleted = True if deleted: message.save() + signals.message_deleted.send(sender=delete, message=message, user=request.user) messages.info(request, _(u"Message successfully deleted.")) - if notification: - notification.send([user], "messages_deleted", {'message': message,}) + return HttpResponseRedirect(success_url) raise Http404 + @login_required def undelete(request, message_id, success_url=None): """ @@ -183,16 +180,17 @@ def undelete(request, message_id, success_url=None): undeleted = True if undeleted: message.save() + signals.mesage_recovered.send(sender=undelete, message=message, user=request.user) messages.info(request, _(u"Message successfully recovered.")) - if notification: - notification.send([user], "messages_recovered", {'message': message,}) + return HttpResponseRedirect(success_url) raise Http404 + @login_required def view(request, message_id, form_class=ComposeForm, quote_helper=format_quote, - subject_template=_(u"Re: %(subject)s"), - template_name='django_messages/view.html'): + subject_template=_(u"Re: %(subject)s"), + template_name='django_messages/view.html'): """ Shows a single message.``message_id`` argument is required. The user is only allowed to see the message, if he is either @@ -217,8 +215,60 @@ def view(request, message_id, form_class=ComposeForm, quote_helper=format_quote, form = form_class(initial={ 'body': quote_helper(message.sender, message.body), 'subject': subject_template % {'subject': message.subject}, - 'recipient': [message.sender,] + 'recipient': [message.sender] }) context['reply_form'] = form - return render_to_response(template_name, context, - context_instance=RequestContext(request)) + return render(request, template_name, context) + + +@login_required +def unread(request, message_id, success_url=None): + """ + Sets a message's state as unread. + @type request: django.http.HttpRequest + @type success_url: mix + """ + message = get_object_or_404(Message, id=message_id) + + if success_url is None: + success_url = reverse('messages_inbox') + if 'next' in request.GET: + success_url = request.GET['next'] + + message.read_at = None + message.save() + signals.message_marked_as_unread.send(sender=unread, message=message, user=request.user) + messages.info(request, _(u"Message successfully marked as unread.")) + + return HttpResponseRedirect(success_url) + + +@login_required +def purge(request, message_id, success_url=None): + """ + Purges a deleted message from database. + @type request: django.http.HttpRequest + @type success_url: mix + """ + message = get_object_or_404(Message, id=message_id) + + if success_url is None: + success_url = reverse('messages_trash') + if 'next' in request.GET: + success_url = request.GET['next'] + + if message.sender == request.user: + message.purged_for_sender = True + else: + message.purged_for_recipient = True + + if message.sender == message.recipient: + message.delete() + elif message.purged_for_recipient and message.purged_for_sender: + message.delete() + else: + message.save() + signals.message_purge.send(sender=purge, message=message, user=request.user) + messages.info(request, _(u"Message successfully purged.")) + + return HttpResponseRedirect(success_url) diff --git a/setup.py b/setup.py index 5530c06..bc395f6 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ packages=( 'django_messages', 'django_messages.templatetags', + 'django_messages.migrations', ), package_data={ 'django_messages': [ @@ -29,4 +30,4 @@ 'Topic :: Utilities', 'Framework :: Django', ), -) \ No newline at end of file +)