Permalink
Browse files

Merge branch 'master' of github.com:Gidsy/django-threaded-messages

Conflicts:
	threaded_messages/admin.py
  • Loading branch information...
2 parents 6333c3c + e0f9d7f commit 0b01a1c33be1cae2b3516dc452df32acc89d2173 @philippWassibauer philippWassibauer committed Feb 20, 2012
View
16 threaded_messages/fields.py
@@ -11,7 +11,7 @@
class CommaSeparatedUserInput(widgets.Input):
input_type = 'text'
-
+
def render(self, name, value, attrs=None):
if value is None:
value = ''
@@ -22,37 +22,37 @@ def render(self, name, value, attrs=None):
class CommaSeparatedUserField(forms.Field):
widget = CommaSeparatedUserInput
-
+
def __init__(self, *args, **kwargs):
recipient_filter = kwargs.pop('recipient_filter', None)
self._recipient_filter = recipient_filter
super(CommaSeparatedUserField, self).__init__(*args, **kwargs)
-
+
def clean(self, value):
super(CommaSeparatedUserField, self).clean(value)
if not value:
return ''
if isinstance(value, (list, tuple)):
return value
-
+
#test if last char is a seperator, some tokenizers are not that smart
if value[-1] == ',':
value = value[0:-1]
-
+
names = set(value.split(','))
names_set = set([name.strip() for name in names])
users = list(User.objects.filter(username__in=names_set))
unknown_names = names_set ^ set([user.username for user in users])
-
+
recipient_filter = self._recipient_filter
invalid_users = []
if recipient_filter is not None:
for r in users:
if recipient_filter(r) is False:
users.remove(r)
invalid_users.append(r.username)
-
+
if unknown_names or invalid_users:
raise forms.ValidationError(_(u"The following usernames are incorrect: %(users)s") % {'users': ', '.join(list(unknown_names)+invalid_users)})
-
+
return users
View
34 threaded_messages/forms.py
@@ -5,17 +5,17 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop
from django.contrib.auth.models import User
-from models import *
-from fields import CommaSeparatedUserField
-from utils import reply_to_thread
+from .models import *
+from .fields import CommaSeparatedUserField
+from .utils import reply_to_thread, now
if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID:
from sendgrid_parse_api.utils import create_reply_email
notification = None
if "notification" in settings.INSTALLED_APPS:
from notification import models as notification
-
+
class ComposeForm(forms.Form):
"""
A simple default form for private messages.
@@ -24,49 +24,49 @@ class ComposeForm(forms.Form):
subject = forms.CharField(label=_(u"Subject"))
body = forms.CharField(label=_(u"Body"),
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, send=True):
recipients = self.cleaned_data['recipient']
subject = self.cleaned_data['subject']
body = self.cleaned_data['body']
-
+
new_message = Message.objects.create(body=body, sender=sender)
-
+
thread = Thread.objects.create(subject=subject,
latest_msg=new_message,
creator=sender)
thread.all_msgs.add(new_message)
for recipient in recipients:
Participant.objects.create(thread=thread, user=recipient)
-
+
(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()
-
+
thread.save() #save this last, since this updates the search index
-
+
#send notifications
if send and notification:
if sendgrid_settings.THREADED_MESSAGES_USE_SENDGRID:
for r in recipients:
reply_email = create_reply_email(sendgrid_settings.THREADED_MESSAGES_ID, r, thread)
- notification.send(recipients, "received_email",
+ notification.send(recipients, "received_email",
{"thread": thread,
"message": new_message}, sender=sender,
from_email= reply_email.get_from_email(),
headers = {'Reply-To': reply_email.get_reply_to_email()})
else:
- notification.send(recipients, "received_email",
+ notification.send(recipients, "received_email",
{"thread": thread,
"message": new_message}, sender=sender)
-
+
return (thread, new_message)
@@ -75,8 +75,8 @@ class ReplyForm(forms.Form):
A simple default form for private messages.
"""
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):
body = self.cleaned_data['body']
return reply_to_thread(thread, sender, body)
View
41 threaded_messages/models.py
@@ -8,11 +8,11 @@
from django.db.models import F, Q
from django.db.models import Avg, Max, Min, Count
-from listeners import start_listening
+from .listeners import start_listening
start_listening()
class MessageManager(models.Manager):
-
+
def inbox_for(self, user, read=None, only_unreplied=None):
"""
Returns all messages that were received by the given user and are not
@@ -36,9 +36,9 @@ def inbox_for(self, user, read=None, only_unreplied=None):
if only_unreplied == True:
inbox = inbox.filter(Q(replied_at__isnull=True)
|Q(replied_at__lt=F("thread__latest_msg__sent_at")))
-
+
return inbox
-
+
def outbox_for(self, user):
"""
Returns all messages that were sent by the given user and are not
@@ -49,7 +49,7 @@ def outbox_for(self, user):
replied_at__isnull=False,
deleted_at__isnull=True,
)
-
+
def trash_for(self, user):
"""
Returns all messages that were either received or sent by the given
@@ -69,15 +69,16 @@ class Message(models.Model):
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"))
sent_at = models.DateTimeField(_("sent at"), auto_now_add=True)
-
+
def __unicode__(self):
return "%s - %s" % (str(self.sender), self.sent_at)
-
+
def save(self, **kwargs):
if not self.id:
- self.sent_at = datetime.datetime.now()
+ from .utils import now
+ self.sent_at = now()
super(Message, self).save(**kwargs)
-
+
class Meta:
ordering = ['-sent_at']
verbose_name = _("Message")
@@ -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
creator = models.ForeignKey(User, related_name='created_threads', verbose_name=_("creator"))
replied = models.BooleanField(editable=False, default=False)
-
+
def __unicode__(self):
return self.subject
-
+
def get_absolute_url(self):
return ('messages_detail', [self.id])
get_absolute_url = models.permalink(get_absolute_url)
-
+
class Meta:
ordering = ['latest_msg']
verbose_name = _("Thread")
verbose_name_plural = _("Threads")
-
-
+
+
class Participant(models.Model):
"""
Thread manager for each participant
@@ -117,9 +118,9 @@ class Participant(models.Model):
read_at = models.DateTimeField(_("read 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)
-
+
objects = MessageManager()
-
+
def new(self):
"""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:
@@ -132,7 +133,7 @@ def replied(self):
or self.replied_at < self.thread.latest_msg.sent_at:
return True
return False
-
+
def last_other_sender(self):
"""returns the last sender thats not the viewing user. if nobody
besides you sent a message to the thread we take a random one
@@ -145,7 +146,7 @@ def last_other_sender(self):
if others:
return others[0].user
return None
-
+
def others(self):
"""returns the other participants of the thread"""
return self.thread.participants.exclude(user=self.user)
@@ -166,10 +167,10 @@ def get_previous(self):
return participation
except:
return None
-
+
def __unicode__(self):
return "%s - %s" % (str(self.user), self.thread.subject)
-
+
class Meta:
ordering = ['thread']
verbose_name = _("participant")
View
4 threaded_messages/search_indexes.py
@@ -1,5 +1,5 @@
from haystack import indexes
-from models import Thread
+from .models import Thread
class ThreadIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
@@ -14,5 +14,3 @@ def prepare_participants(self, object):
def get_model(self):
return Thread
-
-
View
14 threaded_messages/templatetags/inbox.py
@@ -4,7 +4,7 @@
class InboxOutput(Node):
def __init__(self, varname=None):
self.varname = varname
-
+
def render(self, context):
try:
user = context['user']
@@ -16,21 +16,21 @@ def render(self, context):
return ""
else:
return "%s" % (count)
-
+
def do_print_inbox_count(parser, token):
"""
A templatetag to show the unread-count for a logged in user.
Returns the number of unread messages in the user's inbox.
Usage::
-
+
{% load inbox %}
{% inbox_count %}
-
+
{# or assign the value to a variable: #}
-
+
{% inbox_count as my_var %}
{{ my_var }}
-
+
"""
bits = token.contents.split()
if len(bits) > 1:
@@ -42,5 +42,5 @@ def do_print_inbox_count(parser, token):
else:
return InboxOutput()
-register = Library()
+register = Library()
register.tag('inbox_count', do_print_inbox_count)
View
19 threaded_messages/tests.py
@@ -1,10 +1,7 @@
-import datetime
from django.test import TestCase
-from django.contrib.auth.models import User
-from models import Message
-from utils import strip_mail
-from django.utils.html import strip_tags
-
+
+from .utils import strip_mail
+
class UtilsTest(TestCase):
def test_strip_quotes(self):
body = """nyan nyan nyan nyan nyan
@@ -28,15 +25,15 @@ def test_strip_quotes(self):
>
>>
>"""
-
+
body_stripped = """nyan nyan nyan nyan nyan
nyan nyan nyan nyan nyan
nyan nyan nyan nyan nyan
"""
-
+
self.assertEquals(body_stripped.strip(), strip_mail(body).strip())
-
+
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'
@@ -49,8 +46,8 @@ def test_strip_signature(self):
body_stripped = "signature test"
self.assertEquals(body_stripped.strip(), strip_mail(body).strip())
-
-
+
+
def test_no_signature(self):
pass
#TODO: add support for stripped html
View
8 threaded_messages/urls.py
@@ -15,16 +15,16 @@
url(r'^undelete/(?P<thread_id>[\d]+)/$', undelete, name='messages_undelete'),
url(r'^batch-update/$', batch_update, name='messages_batch_update'),
url(r'^trash/$', trash, name='messages_trash'),
-
+
url(r"^recipient-search/$", recipient_search, name="recipient_search"),
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, {
"template_name":"django_messages/modal_compose.html",
"form_class": ComposeForm
}, name='modal_messages_compose_to'),
-
+
url(r'^modal-compose/$', compose, {
"template_name":"django_messages/modal_compose.html",
"form_class": ComposeForm
View
51 threaded_messages/utils.py
@@ -1,22 +1,24 @@
# -*- coding:utf-8 -*-
+import HTMLParser
+import datetime
import re
-import settings as tm_settings
-from models import Message, Participant
+from lxml.html.clean import Cleaner
+
from django.conf import settings
from django.contrib.sites.models import Site
from django.utils.encoding import force_unicode
from django.utils.text import wrap
from django.utils.translation import ugettext_lazy as _
from django.template import Context, loader
from django.template.loader import render_to_string, get_template
-from django.template import Context
-import HTMLParser
-from lxml.html.clean import Cleaner
-import datetime
+
+import settings as tm_settings
+from .models import Message, Participant
+
if "notification" in settings.INSTALLED_APPS:
from notification import models as notification
-
+
# favour django-mailer but fall back to django.core.mail
if tm_settings.THREADED_MESSAGES_USE_SENDGRID:
import sendgrid_parse_api
@@ -26,6 +28,11 @@
else:
from django.core.mail import send_mail
+try:
+ from django.utils.timezone import now
+except ImportError:
+ now = datetime.datetime.now
+
def open_message_thread(recipients, subject, template,
sender, context={}, send=True, message=None):
@@ -35,53 +42,53 @@ def open_message_thread(recipients, subject, template,
body = t.render(Context({}))
else:
body = message
-
- from forms import ComposeForm #temporary here to remove circular dependence
+
+ from forms import ComposeForm # temporary here to remove circular dependence
compose_form = ComposeForm(data={
"recipient": recipients,
"subject": subject,
"body": body
})
if compose_form.is_valid():
(thread, new_message) = compose_form.save(sender=sender, send=send)
-
+
return (thread, new_message)
-def reply_to_thread(thread,sender, body):
+def reply_to_thread(thread,sender, body):
new_message = Message.objects.create(body=body, sender=sender)
new_message.parent_msg = thread.latest_msg
thread.latest_msg = new_message
thread.all_msgs.add(new_message)
thread.replied = True
thread.save()
new_message.save()
-
+
recipients = []
for participant in thread.participants.all():
participant.deleted_at = None
participant.save()
if sender != participant.user: # dont send emails to the sender!
recipients.append(participant.user)
-
+
sender_part = Participant.objects.get(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()
-
+
if notification:
for r in recipients:
if tm_settings.THREADED_MESSAGES_USE_SENDGRID:
reply_email = sendgrid_parse_api.utils.create_reply_email(tm_settings.THREADED_MESSAGES_ID, r, thread)
- notification.send(recipients, "received_email",
+ notification.send(recipients, "received_email",
{"thread": thread,
"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()})
else:
- notification.send([r], "received_email",
+ notification.send([r], "received_email",
{"thread": thread,
"message": new_message}, sender=sender)
-
+
return (thread, new_message)
@@ -94,14 +101,14 @@ def get_lines(body):
def strip_mail(body):
custom_line_no = None
-
+
lines = get_lines(body)
-
+
has_signature = False
for l in lines:
if l.strip().startswith('>'):
has_signature = True
-
+
# strip signature -- only if there is a signature. otherwise all is stripped
if has_signature:
for l in reversed(lines):
View
64 threaded_messages/views.py
@@ -1,6 +1,8 @@
# -*- coding:utf-8 -*-
-import datetime
-from django.contrib.auth import load_backend, login, BACKEND_SESSION_KEY
+import logging
+import simplejson
+
+from django.contrib.auth import login, BACKEND_SESSION_KEY
from django.http import Http404, HttpResponseRedirect, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
@@ -13,11 +15,12 @@
from django.db.models import Q
from django.conf import settings
from django.template.loader import render_to_string
+
from avatar.templatetags.avatar_tags import avatar_url
+
from .models import *
from .forms import ComposeForm, ReplyForm
-import simplejson
-import logging
+from .utils import now
@login_required
@@ -41,7 +44,7 @@ def inbox(request, template_name='django_messages/inbox.html'):
only_unreplied = True
thread_list = Participant.objects.inbox_for(request.user, read=read, only_unreplied=only_unreplied)
-
+
return render_to_response(template_name, {
'thread_list': thread_list,
'only_read': only_read,
@@ -62,8 +65,8 @@ def search(request, template_name="django_messages/search.html"):
"thread_results": results,
"search_term": search_term,
}, context_instance=RequestContext(request))
-
-
+
+
@login_required
def outbox(request, template_name='django_messages/inbox.html'):
"""
@@ -79,7 +82,7 @@ def outbox(request, template_name='django_messages/inbox.html'):
@login_required
def trash(request, template_name='django_messages/trash.html'):
"""
- Displays a list of deleted messages.
+ Displays a list of deleted messages.
Optional arguments:
``template_name``: name of the template to use
Hint: A Cron-Job could periodicly clean up old messages, which are deleted
@@ -132,25 +135,25 @@ def delete(request, thread_id, success_url=None):
"""
Marks a message as deleted by sender or recipient. The message is not
really removed from the database, because two users must delete a message
- before it's save to remove it completely.
- A cron-job should prune the database and remove old messages which are
+ before it's save to remove it completely.
+ A cron-job should prune the database and remove old messages which are
deleted by both users.
As a side effect, this makes it easy to implement a trash with undelete.
-
+
You can pass ?next=/foo/bar/ via the url to redirect the user to a different
page (e.g. `/foo/bar/`) than ``success_url`` after deletion of the message.
"""
user = request.user
- now = datetime.datetime.now()
+ right_now = now()
thread = get_object_or_404(Thread, id=thread_id)
user_part = get_object_or_404(Participant, user=user, thread=thread)
if request.GET.has_key('next'):
success_url = request.GET['next']
elif success_url is None:
success_url = reverse('messages_inbox')
-
- user_part.deleted_at = now
+
+ user_part.deleted_at = right_now
user_part.save()
messages.success(request, message=_(u"Conversation successfully deleted."))
return HttpResponseRedirect(success_url)
@@ -171,7 +174,7 @@ def undelete(request, thread_id, success_url=None):
elif success_url is None:
success_url = reverse('messages_inbox')
- user_part.deleted_at = now
+ user_part.deleted_at = now()
user_part.save()
messages.success(request, _(u"Conversation successfully recovered."))
return HttpResponseRedirect(success_url)
@@ -181,16 +184,15 @@ def view(request, thread_id, form_class=ReplyForm,
success_url=None, recipient_filter=None, 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
+ The user is only allowed to see the message, if he is either
the sender or the recipient. If the user is not allowed a 404
- is raised.
- If the user is the recipient and the message is unread
+ is raised.
+ If the user is the recipient and the message is unread
``read_at`` is set to the current datetime.
- """
-
+ """
user = request.user
thread = get_object_or_404(Thread, id=thread_id)
-
+
"""
Reply stuff
"""
@@ -205,16 +207,16 @@ def view(request, thread_id, form_class=ReplyForm,
else:
form = form_class()
- now = datetime.datetime.now()
+ right_now = now()
participant = get_object_or_404(Participant, thread=thread, user=request.user)
message_list = []
# in this view we want the last message last
for message in thread.all_msgs.all().order_by("sent_at"):
unread = True
if participant.read_at and message.sent_at <= participant.read_at:
unread = False
- message_list.append((message,unread,))
- participant.read_at = now
+ message_list.append((message, unread,))
+ participant.read_at = right_now
participant.save()
return render_to_response(template_name, {
'thread': thread,
@@ -239,20 +241,20 @@ def batch_update(request, success_url=None):
if participant:
participant = participant[0]
if request.POST.get("action") == "read":
- participant.read_at = datetime.datetime.now()
+ participant.read_at = now()
elif request.POST.get("action") == "delete":
- participant.deleted_at = datetime.datetime.now()
+ participant.deleted_at = now()
elif request.POST.get("action") == "unread":
participant.read_at = None
participant.save()
else:
raise Http404
-
+
else:
# this should only happen when hacked or developer uses wrong, therefore
# return simple message
return HttpResponse("Only Post allowed", code=400)
-
+
if success_url:
return HttpResponseRedirect(success_url)
else:
@@ -262,8 +264,8 @@ def batch_update(request, success_url=None):
return HttpResponseRedirect(referer)
else:
return HttpResponseRedirect(reverse("messages_inbox"))
-
-
+
+
@login_required
def message_ajax_reply(request, thread_id,
@@ -277,7 +279,7 @@ def message_ajax_reply(request, thread_id,
except Exception, e:
logging.exception(e)
return HttpResponse(status=500, content="Message could not be sent")
-
+
return render_to_response(template_name,{
"message": new_message,
}, context_instance=RequestContext(request))

0 comments on commit 0b01a1c

Please sign in to comment.