Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Gidsy/django-threaded-messages
Browse files Browse the repository at this point in the history
Conflicts:
	threaded_messages/admin.py
  • Loading branch information
Philipp Wassibauer committed Feb 20, 2012
2 parents 6333c3c + e0f9d7f commit 0b01a1c
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 123 deletions.
16 changes: 8 additions & 8 deletions threaded_messages/fields.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


class CommaSeparatedUserInput(widgets.Input): class CommaSeparatedUserInput(widgets.Input):
input_type = 'text' input_type = 'text'

def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
if value is None: if value is None:
value = '' value = ''
Expand All @@ -22,37 +22,37 @@ def render(self, name, value, attrs=None):


class CommaSeparatedUserField(forms.Field): class CommaSeparatedUserField(forms.Field):
widget = CommaSeparatedUserInput widget = CommaSeparatedUserInput

def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
recipient_filter = kwargs.pop('recipient_filter', None) recipient_filter = kwargs.pop('recipient_filter', None)
self._recipient_filter = recipient_filter self._recipient_filter = recipient_filter
super(CommaSeparatedUserField, self).__init__(*args, **kwargs) super(CommaSeparatedUserField, self).__init__(*args, **kwargs)

def clean(self, value): def clean(self, value):
super(CommaSeparatedUserField, self).clean(value) super(CommaSeparatedUserField, self).clean(value)
if not value: if not value:
return '' return ''
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
return value return value

#test if last char is a seperator, some tokenizers are not that smart #test if last char is a seperator, some tokenizers are not that smart
if value[-1] == ',': if value[-1] == ',':
value = value[0:-1] value = value[0:-1]

names = set(value.split(',')) names = set(value.split(','))
names_set = set([name.strip() for name in names]) names_set = set([name.strip() for name in names])
users = list(User.objects.filter(username__in=names_set)) users = list(User.objects.filter(username__in=names_set))
unknown_names = names_set ^ set([user.username for user in users]) unknown_names = names_set ^ set([user.username for user in users])

recipient_filter = self._recipient_filter recipient_filter = self._recipient_filter
invalid_users = [] invalid_users = []
if recipient_filter is not None: if recipient_filter is not None:
for r in users: for r in users:
if recipient_filter(r) is False: if recipient_filter(r) is False:
users.remove(r) users.remove(r)
invalid_users.append(r.username) invalid_users.append(r.username)

if unknown_names or invalid_users: if unknown_names or invalid_users:
raise forms.ValidationError(_(u"The following usernames are incorrect: %(users)s") % {'users': ', '.join(list(unknown_names)+invalid_users)}) raise forms.ValidationError(_(u"The following usernames are incorrect: %(users)s") % {'users': ', '.join(list(unknown_names)+invalid_users)})

return users return users
34 changes: 17 additions & 17 deletions threaded_messages/forms.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from django.contrib.auth.models import User from django.contrib.auth.models import User
from models import * from .models import *
from fields import CommaSeparatedUserField from .fields import CommaSeparatedUserField
from utils import reply_to_thread from .utils import reply_to_thread, now


if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID: if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID:
from sendgrid_parse_api.utils import create_reply_email from sendgrid_parse_api.utils import create_reply_email


notification = None notification = None
if "notification" in settings.INSTALLED_APPS: if "notification" in settings.INSTALLED_APPS:
from notification import models as notification from notification import models as notification

class ComposeForm(forms.Form): class ComposeForm(forms.Form):
""" """
A simple default form for private messages. A simple default form for private messages.
Expand All @@ -24,49 +24,49 @@ class ComposeForm(forms.Form):
subject = forms.CharField(label=_(u"Subject")) subject = forms.CharField(label=_(u"Subject"))
body = forms.CharField(label=_(u"Body"), 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): def __init__(self, *args, **kwargs):
recipient_filter = kwargs.pop('recipient_filter', None) recipient_filter = kwargs.pop('recipient_filter', None)
super(ComposeForm, self).__init__(*args, **kwargs) super(ComposeForm, self).__init__(*args, **kwargs)
if recipient_filter is not None: if recipient_filter is not None:
self.fields['recipient']._recipient_filter = recipient_filter self.fields['recipient']._recipient_filter = recipient_filter

def save(self, sender, send=True): def save(self, sender, send=True):
recipients = self.cleaned_data['recipient'] recipients = self.cleaned_data['recipient']
subject = self.cleaned_data['subject'] subject = self.cleaned_data['subject']
body = self.cleaned_data['body'] body = self.cleaned_data['body']

new_message = Message.objects.create(body=body, sender=sender) new_message = Message.objects.create(body=body, sender=sender)

thread = Thread.objects.create(subject=subject, thread = Thread.objects.create(subject=subject,
latest_msg=new_message, latest_msg=new_message,
creator=sender) creator=sender)
thread.all_msgs.add(new_message) thread.all_msgs.add(new_message)


for recipient in recipients: for recipient in recipients:
Participant.objects.create(thread=thread, user=recipient) Participant.objects.create(thread=thread, user=recipient)

(sender_part, created) = Participant.objects.get_or_create(thread=thread, user=sender) (sender_part, created) = Participant.objects.get_or_create(thread=thread, user=sender)
sender_part.replied_at = sender_part.read_at = datetime.datetime.now() sender_part.replied_at = sender_part.read_at = now()
sender_part.save() sender_part.save()

thread.save() #save this last, since this updates the search index thread.save() #save this last, since this updates the search index

#send notifications #send notifications
if send and notification: if send and notification:
if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID: if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID:
for r in recipients: for r in recipients:
reply_email = create_reply_email(sendgrid_settings.THREADED_MESSAGES_ID, r, thread) reply_email = create_reply_email(sendgrid_settings.THREADED_MESSAGES_ID, r, thread)
notification.send(recipients, "received_email", notification.send(recipients, "received_email",
{"thread": thread, {"thread": thread,
"message": new_message}, sender=sender, "message": new_message}, sender=sender,
from_email= reply_email.get_from_email(), from_email= reply_email.get_from_email(),
headers = {'Reply-To': reply_email.get_reply_to_email()}) headers = {'Reply-To': reply_email.get_reply_to_email()})
else: else:
notification.send(recipients, "received_email", notification.send(recipients, "received_email",
{"thread": thread, {"thread": thread,
"message": new_message}, sender=sender) "message": new_message}, sender=sender)

return (thread, new_message) return (thread, new_message)




Expand All @@ -75,8 +75,8 @@ class ReplyForm(forms.Form):
A simple default form for private messages. A simple default form for private messages.
""" """
body = forms.CharField(label=_(u"Reply"), body = forms.CharField(label=_(u"Reply"),
widget=forms.Textarea(attrs={'rows': '4', 'cols':'55'})) widget=forms.Textarea(attrs={'rows': '4', 'cols': '55'}))

def save(self, sender, thread): def save(self, sender, thread):
body = self.cleaned_data['body'] body = self.cleaned_data['body']
return reply_to_thread(thread, sender, body) return reply_to_thread(thread, sender, body)
41 changes: 21 additions & 20 deletions threaded_messages/models.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Avg, Max, Min, Count from django.db.models import Avg, Max, Min, Count


from listeners import start_listening from .listeners import start_listening
start_listening() start_listening()


class MessageManager(models.Manager): class MessageManager(models.Manager):

def inbox_for(self, user, read=None, only_unreplied=None): def inbox_for(self, user, read=None, only_unreplied=None):
""" """
Returns all messages that were received by the given user and are not Returns all messages that were received by the given user and are not
Expand All @@ -36,9 +36,9 @@ def inbox_for(self, user, read=None, only_unreplied=None):
if only_unreplied == True: if only_unreplied == True:
inbox = inbox.filter(Q(replied_at__isnull=True) inbox = inbox.filter(Q(replied_at__isnull=True)
|Q(replied_at__lt=F("thread__latest_msg__sent_at"))) |Q(replied_at__lt=F("thread__latest_msg__sent_at")))

return inbox return inbox

def outbox_for(self, user): def outbox_for(self, user):
""" """
Returns all messages that were sent by the given user and are not Returns all messages that were sent by the given user and are not
Expand All @@ -49,7 +49,7 @@ def outbox_for(self, user):
replied_at__isnull=False, replied_at__isnull=False,
deleted_at__isnull=True, deleted_at__isnull=True,
) )

def trash_for(self, user): def trash_for(self, user):
""" """
Returns all messages that were either received or sent by the given Returns all messages that were either received or sent by the given
Expand All @@ -69,15 +69,16 @@ class Message(models.Model):
sender = models.ForeignKey(User, related_name='sent_messages', blank=True, null=True, verbose_name=_("sender")) sender = models.ForeignKey(User, related_name='sent_messages', blank=True, null=True, verbose_name=_("sender"))
parent_msg = models.ForeignKey('self', related_name='next_messages', blank=True, null=True, verbose_name=_("parent message")) parent_msg = models.ForeignKey('self', related_name='next_messages', blank=True, null=True, verbose_name=_("parent message"))
sent_at = models.DateTimeField(_("sent at"), auto_now_add=True) sent_at = models.DateTimeField(_("sent at"), auto_now_add=True)

def __unicode__(self): def __unicode__(self):
return "%s - %s" % (str(self.sender), self.sent_at) return "%s - %s" % (str(self.sender), self.sent_at)

def save(self, **kwargs): def save(self, **kwargs):
if not self.id: if not self.id:
self.sent_at = datetime.datetime.now() from .utils import now
self.sent_at = now()
super(Message, self).save(**kwargs) super(Message, self).save(**kwargs)

class Meta: class Meta:
ordering = ['-sent_at'] ordering = ['-sent_at']
verbose_name = _("Message") verbose_name = _("Message")
Expand All @@ -94,20 +95,20 @@ class Thread(models.Model):
# the following fields are used to filter out messages that have not been replied to in the inbox # the following fields are used to filter out messages that have not been replied to in the inbox
creator = models.ForeignKey(User, related_name='created_threads', verbose_name=_("creator")) creator = models.ForeignKey(User, related_name='created_threads', verbose_name=_("creator"))
replied = models.BooleanField(editable=False, default=False) replied = models.BooleanField(editable=False, default=False)

def __unicode__(self): def __unicode__(self):
return self.subject return self.subject

def get_absolute_url(self): def get_absolute_url(self):
return ('messages_detail', [self.id]) return ('messages_detail', [self.id])
get_absolute_url = models.permalink(get_absolute_url) get_absolute_url = models.permalink(get_absolute_url)

class Meta: class Meta:
ordering = ['latest_msg'] ordering = ['latest_msg']
verbose_name = _("Thread") verbose_name = _("Thread")
verbose_name_plural = _("Threads") verbose_name_plural = _("Threads")


class Participant(models.Model): class Participant(models.Model):
""" """
Thread manager for each participant Thread manager for each participant
Expand All @@ -117,9 +118,9 @@ class Participant(models.Model):
read_at = models.DateTimeField(_("read at"), null=True, blank=True) read_at = models.DateTimeField(_("read at"), null=True, blank=True)
replied_at = models.DateTimeField(_("replied at"), null=True, blank=True) replied_at = models.DateTimeField(_("replied at"), null=True, blank=True)
deleted_at = models.DateTimeField(_("deleted at"), null=True, blank=True) deleted_at = models.DateTimeField(_("deleted at"), null=True, blank=True)

objects = MessageManager() objects = MessageManager()

def new(self): def new(self):
"""returns whether the recipient has read the message or not""" """returns whether the recipient has read the message or not"""
if self.read_at is None or self.read_at < self.thread.latest_msg.sent_at: if self.read_at is None or self.read_at < self.thread.latest_msg.sent_at:
Expand All @@ -132,7 +133,7 @@ def replied(self):
or self.replied_at < self.thread.latest_msg.sent_at: or self.replied_at < self.thread.latest_msg.sent_at:
return True return True
return False return False

def last_other_sender(self): def last_other_sender(self):
"""returns the last sender thats not the viewing user. if nobody """returns the last sender thats not the viewing user. if nobody
besides you sent a message to the thread we take a random one besides you sent a message to the thread we take a random one
Expand All @@ -145,7 +146,7 @@ def last_other_sender(self):
if others: if others:
return others[0].user return others[0].user
return None return None

def others(self): def others(self):
"""returns the other participants of the thread""" """returns the other participants of the thread"""
return self.thread.participants.exclude(user=self.user) return self.thread.participants.exclude(user=self.user)
Expand All @@ -166,10 +167,10 @@ def get_previous(self):
return participation return participation
except: except:
return None return None

def __unicode__(self): def __unicode__(self):
return "%s - %s" % (str(self.user), self.thread.subject) return "%s - %s" % (str(self.user), self.thread.subject)

class Meta: class Meta:
ordering = ['thread'] ordering = ['thread']
verbose_name = _("participant") verbose_name = _("participant")
Expand Down
4 changes: 1 addition & 3 deletions threaded_messages/search_indexes.py
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,5 @@
from haystack import indexes from haystack import indexes
from models import Thread from .models import Thread


class ThreadIndex(indexes.SearchIndex, indexes.Indexable): class ThreadIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True) text = indexes.CharField(document=True, use_template=True)
Expand All @@ -14,5 +14,3 @@ def prepare_participants(self, object):


def get_model(self): def get_model(self):
return Thread return Thread


14 changes: 7 additions & 7 deletions threaded_messages/templatetags/inbox.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class InboxOutput(Node): class InboxOutput(Node):
def __init__(self, varname=None): def __init__(self, varname=None):
self.varname = varname self.varname = varname

def render(self, context): def render(self, context):
try: try:
user = context['user'] user = context['user']
Expand All @@ -16,21 +16,21 @@ def render(self, context):
return "" return ""
else: else:
return "%s" % (count) return "%s" % (count)

def do_print_inbox_count(parser, token): def do_print_inbox_count(parser, token):
""" """
A templatetag to show the unread-count for a logged in user. A templatetag to show the unread-count for a logged in user.
Returns the number of unread messages in the user's inbox. Returns the number of unread messages in the user's inbox.
Usage:: Usage::
{% load inbox %} {% load inbox %}
{% inbox_count %} {% inbox_count %}
{# or assign the value to a variable: #} {# or assign the value to a variable: #}
{% inbox_count as my_var %} {% inbox_count as my_var %}
{{ my_var }} {{ my_var }}
""" """
bits = token.contents.split() bits = token.contents.split()
if len(bits) > 1: if len(bits) > 1:
Expand All @@ -42,5 +42,5 @@ def do_print_inbox_count(parser, token):
else: else:
return InboxOutput() return InboxOutput()


register = Library() register = Library()
register.tag('inbox_count', do_print_inbox_count) register.tag('inbox_count', do_print_inbox_count)
19 changes: 8 additions & 11 deletions threaded_messages/tests.py
Original file line number Original file line Diff line number Diff line change
@@ -1,10 +1,7 @@
import datetime
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User
from models import Message from .utils import strip_mail
from utils import strip_mail
from django.utils.html import strip_tags

class UtilsTest(TestCase): class UtilsTest(TestCase):
def test_strip_quotes(self): def test_strip_quotes(self):
body = """nyan nyan nyan nyan nyan body = """nyan nyan nyan nyan nyan
Expand All @@ -28,15 +25,15 @@ def test_strip_quotes(self):
> >
>> >>
>""" >"""

body_stripped = """nyan nyan nyan nyan nyan body_stripped = """nyan nyan nyan nyan nyan
nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan
nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan
""" """

self.assertEquals(body_stripped.strip(), strip_mail(body).strip()) self.assertEquals(body_stripped.strip(), strip_mail(body).strip())

def test_single_line_quotes(self): def test_single_line_quotes(self):
body = 'asfasf\n\nOn Thu, Dec 15, 2011 at 12:42 PM, Fabrizio S. <messaging@email.gidsy.com>wrote:\n\n> [image: Gidsy] New message\n> Hi Fabrizio, Andrew M. sent you a message\n>\n> *blabla*\n> gasg\n>\n> View and reply<http://email.gidsy.com/wf/click?c=e36x8iH5CyW6UFPc7U%2FiBSpwHwOcqQc55u6Od0IAvnJWLQwR0RdOslgfJYtFkOT0&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh0>\n>\n> Sincerely,\n> the *Gidsy team*<http://email.gidsy.com/wf/click?c=nun%2FbaehJTxhIK1KvYwhU5Tg16XMq0b2DKd6IxvO%2F%2Bw%3D&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh1>\n>\n> This email was intended for fabrizio@gidsy.com. If you do not want to\n> receive emails like this from staging.gidsy.com<http://email.gidsy.com/wf/click?c=e36x8iH5CyW6UFPc7U%2FiBY72qxV4NIiQfC%2BfF%2BpSEec%3D&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh2>anymore, then please change your Email\n> notification settings <http://notice-email-setting/>.\n>\n> Copyright \ufffd 2011 Gidsy.com, All rights reserved.\n>\n' body = 'asfasf\n\nOn Thu, Dec 15, 2011 at 12:42 PM, Fabrizio S. <messaging@email.gidsy.com>wrote:\n\n> [image: Gidsy] New message\n> Hi Fabrizio, Andrew M. sent you a message\n>\n> *blabla*\n> gasg\n>\n> View and reply<http://email.gidsy.com/wf/click?c=e36x8iH5CyW6UFPc7U%2FiBSpwHwOcqQc55u6Od0IAvnJWLQwR0RdOslgfJYtFkOT0&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh0>\n>\n> Sincerely,\n> the *Gidsy team*<http://email.gidsy.com/wf/click?c=nun%2FbaehJTxhIK1KvYwhU5Tg16XMq0b2DKd6IxvO%2F%2Bw%3D&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh1>\n>\n> This email was intended for fabrizio@gidsy.com. If you do not want to\n> receive emails like this from staging.gidsy.com<http://email.gidsy.com/wf/click?c=e36x8iH5CyW6UFPc7U%2FiBY72qxV4NIiQfC%2BfF%2BpSEec%3D&rp=7%2Bq%2FuBUXPhfnWd079jPZDJw1s3xtQcNITJcDWjO98HB8tJ6%2BYeP23y9SaOFiXvnpboQhDnEJRnrEZfRP9WnHQiL7q9Y0Plign2S9mx7i8%2Bk%3D&u=icfx5E9JS66UPX7QM9UvGw%2Fh2>anymore, then please change your Email\n> notification settings <http://notice-email-setting/>.\n>\n> Copyright \ufffd 2011 Gidsy.com, All rights reserved.\n>\n'


Expand All @@ -49,8 +46,8 @@ def test_strip_signature(self):


body_stripped = "signature test" body_stripped = "signature test"
self.assertEquals(body_stripped.strip(), strip_mail(body).strip()) self.assertEquals(body_stripped.strip(), strip_mail(body).strip())


def test_no_signature(self): def test_no_signature(self):
pass pass
#TODO: add support for stripped html #TODO: add support for stripped html
Expand Down
8 changes: 4 additions & 4 deletions threaded_messages/urls.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
url(r'^undelete/(?P<thread_id>[\d]+)/$', undelete, name='messages_undelete'), url(r'^undelete/(?P<thread_id>[\d]+)/$', undelete, name='messages_undelete'),
url(r'^batch-update/$', batch_update, name='messages_batch_update'), url(r'^batch-update/$', batch_update, name='messages_batch_update'),
url(r'^trash/$', trash, name='messages_trash'), url(r'^trash/$', trash, name='messages_trash'),

url(r"^recipient-search/$", recipient_search, name="recipient_search"), url(r"^recipient-search/$", recipient_search, name="recipient_search"),
url(r'^message-reply/(?P<thread_id>[\d]+)/$', message_ajax_reply, name="message_reply"), url(r'^message-reply/(?P<thread_id>[\d]+)/$', message_ajax_reply, name="message_reply"),

# modal composing # modal composing
url(r'^modal-compose/(?P<recipient>[\w.+-_]+)/$', compose, { url(r'^modal-compose/(?P<recipient>[\w.+-_]+)/$', compose, {
"template_name":"django_messages/modal_compose.html", "template_name":"django_messages/modal_compose.html",
"form_class": ComposeForm "form_class": ComposeForm
}, name='modal_messages_compose_to'), }, name='modal_messages_compose_to'),

url(r'^modal-compose/$', compose, { url(r'^modal-compose/$', compose, {
"template_name":"django_messages/modal_compose.html", "template_name":"django_messages/modal_compose.html",
"form_class": ComposeForm "form_class": ComposeForm
Expand Down
Loading

0 comments on commit 0b01a1c

Please sign in to comment.