summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askbot/media/js/group_messaging.js8
-rw-r--r--askbot/templates/group_messaging/senders_list.html7
-rw-r--r--askbot/templates/group_messaging/threads_list.html9
-rw-r--r--group_messaging/models.py95
-rw-r--r--group_messaging/tests.py221
-rw-r--r--group_messaging/views.py23
6 files changed, 253 insertions, 110 deletions
diff --git a/askbot/media/js/group_messaging.js b/askbot/media/js/group_messaging.js
index 08d9056e..63980569 100644
--- a/askbot/media/js/group_messaging.js
+++ b/askbot/media/js/group_messaging.js
@@ -265,15 +265,19 @@ NewThreadComposer.prototype.onAfterShow = function() {
NewThreadComposer.prototype.onSendErrorInternal = function(data) {
var missingUsers = data['missing_users']
+ var errors = [];
if (missingUsers) {
var errorTpl = ngettext(
'user {{str}} does not exist',
'users {{str}} do not exist',
missingUsers.length
)
- error = errorTpl.replace('{{str}}', joinAsPhrase(missingUsers));
- this._toInputError.html(error);
+ errors.push(errorTpl.replace('{{str}}', joinAsPhrase(missingUsers)));
}
+ if (data['self_message']) {
+ errors.push(gettext('cannot send message to yourself'));
+ }
+ this._toInputError.html(errors.join(', '));
};
NewThreadComposer.prototype.getInputData = function() {
diff --git a/askbot/templates/group_messaging/senders_list.html b/askbot/templates/group_messaging/senders_list.html
index 4bee2626..31432dc4 100644
--- a/askbot/templates/group_messaging/senders_list.html
+++ b/askbot/templates/group_messaging/senders_list.html
@@ -1,9 +1,12 @@
{% if senders %}
<ul class="senders-list">
<li>{% trans %}Messages by sender:{% endtrans %}</li>
- <li><a class="selected" data-sender-id="-1">{% trans %}all users{% endtrans %}</a></li>
+ <li><a class="selected" data-sender-id="-1">{% trans %}inbox{% endtrans %}</a></li>
+ <li><a data-sender-id="{{ request_user_id }}">{% trans %}sent{% endtrans %}</a></li>
{% for sender in senders %}
- <li><a data-sender-id="{{ sender.id }}">{{ sender.username|escape }}</a></li>
+ {% if sender.id != request_user_id %}
+ <li><a data-sender-id="{{ sender.id }}">{{ sender.username|escape }}</a></li>
+ {% endif %}
{% endfor %}
{# -2 is deleted messages #}
<li><a class="trash" data-sender-id="-2">{% trans %}trash{% endtrans %}</a></li>
diff --git a/askbot/templates/group_messaging/threads_list.html b/askbot/templates/group_messaging/threads_list.html
index 43492402..335abcb9 100644
--- a/askbot/templates/group_messaging/threads_list.html
+++ b/askbot/templates/group_messaging/threads_list.html
@@ -9,7 +9,14 @@
>
<td class="delete-or-restore"></td>
<td class="senders">{{ thread_data['senders_info']|escape }}</td>
- <td class="subject">{{ thread.headline|escape }}</td>
+ <td class="subject">
+ {{ thread.headline|escape }}
+ {% if thread_data['responses_count'] > 0 %}
+ <span class="messages-count">
+ ({{ thread_data['responses_count'] + 1 }})
+ </span>
+ {% endif %}
+ </td>
<td class="timestamp">{{ thread.last_active_at|timesince }}</td>
</tr>
{% endfor %}
diff --git a/group_messaging/models.py b/group_messaging/models.py
index 12fe3620..a34e4690 100644
--- a/group_messaging/models.py
+++ b/group_messaging/models.py
@@ -17,6 +17,18 @@ parse_message = lambda v: v
GROUP_NAME_TPL = '_personal_%s'
+def get_recipient_names(recipient_groups):
+ """returns list of user names if groups are private,
+ or group names, otherwise"""
+ names = set()
+ for group in recipient_groups:
+ if group.name.startswith('_personal_'):
+ names.add(group.user_set.all()[0].username)
+ else:
+ names.add(group.name)
+ return names
+
+
def get_personal_group_by_user_id(user_id):
return Group.objects.get(name=GROUP_NAME_TPL % user_id)
@@ -103,13 +115,44 @@ class MessageMemo(models.Model):
class MessageManager(models.Manager):
"""model manager for the :class:`Message`"""
+ def get_sent_threads(self, sender=None):
+ """returns list of threads for the "sent" mailbox
+ this function does not deal with deleted=True
+ """
+ responses = self.filter(sender=sender)
+ responded_to = models.Q(descendants__in=responses, root=None)
+ seen_filter = models.Q(
+ memos__status=MessageMemo.SEEN,
+ memos__user=sender
+ )
+ seen_responses = self.filter(responded_to & seen_filter)
+ unseen_responses = self.filter(responded_to & ~models.Q(memos__user=sender))
+ return (
+ self.get_threads(sender=sender) \
+ | seen_responses.distinct() \
+ | unseen_responses.distinct()
+ ).distinct()
+
def get_threads(self, recipient=None, sender=None, deleted=False):
- user_groups = recipient.groups.all()
- user_thread_filter = models.Q(
- root=None,
- message_type=Message.STORED,
- recipients__in=user_groups
- )
+ """returns query set of first messages in conversations,
+ based on recipient, sender and whether to
+ load deleted messages or not"""
+
+ if sender and sender == recipient:
+ raise ValueError('sender cannot be the same as recipient')
+
+ filter_kwargs = {
+ 'root': None,
+ 'message_type': Message.STORED
+ }
+ if recipient:
+ filter_kwargs['recipients__in'] = recipient.groups.all()
+ else:
+ #todo: possibly a confusing hack - for this branch -
+ #sender but no recipient in the args - we need "sent" origin threads
+ recipient = sender
+
+ user_thread_filter = models.Q(**filter_kwargs)
filter = user_thread_filter
if sender:
@@ -161,7 +204,6 @@ class MessageManager(models.Manager):
)
return message
-
def create_thread(self, sender=None, recipients=None, text=None):
"""creates a stored message and adds recipients"""
message = self.create(
@@ -170,6 +212,9 @@ class MessageManager(models.Manager):
senders_info=sender.username,
text=text,
)
+ names = get_recipient_names(recipients)
+ message.add_recipient_names_to_senders_info(recipients)
+ message.save()
message.add_recipients(recipients)
message.send_email_alert()
return message
@@ -184,11 +229,14 @@ class MessageManager(models.Manager):
#recipients are parent's recipients + sender
#creator of response gets memo in the "read" status
recipients = set(parent.recipients.all())
- senders_group = get_personal_group(parent.sender)
- recipients.add(senders_group)
+
+ if sender != parent.sender:
+ senders_group = get_personal_group(parent.sender)
+ parent.add_recipients([senders_group])
+ recipients.add(senders_group)
+
message.add_recipients(recipients)
#add author of the parent as a recipient to parent
- parent.add_recipients([senders_group])
#update headline
message.root.headline = text[:MAX_HEADLINE_LENGTH]
#mark last active timestamp for the root message
@@ -257,6 +305,12 @@ class Message(models.Model):
objects = MessageManager()
+ def add_recipient_names_to_senders_info(self, recipient_groups):
+ names = get_recipient_names(recipient_groups)
+ old_names = set(self.senders_info.split(','))
+ names |= old_names
+ self.senders_info = ','.join(names)
+
def add_recipients(self, recipients):
"""adds recipients to the message
and updates the sender lists for all recipients
@@ -323,7 +377,24 @@ class Message(models.Model):
self.senders_info = (','.join(senders_names))[:64]
self.save()
- def unarchive(self):
+ def unarchive(self, user=None):
"""unarchive message for all recipients"""
- memos = self.memos.filter(status=MessageMemo.ARCHIVED)
+ archived_filter = {'status': MessageMemo.ARCHIVED}
+ if user:
+ archived_filter['user'] = user
+ memos = self.memos.filter(**archived_filter)
memos.update(status=MessageMemo.SEEN)
+
+ def set_status_for_user(self, status, user):
+ """set specific status to the message for the user"""
+ memo, created = MessageMemo.objects.get_or_create(user=user, message=self)
+ memo.status = status
+ memo.save()
+
+ def archive(self, user):
+ """mark message as archived"""
+ self.set_status_for_user(MessageMemo.ARCHIVED, user)
+
+ def mark_as_seen(self, user):
+ """mark message as seen"""
+ self.set_status_for_user(MessageMemo.SEEN, user)
diff --git a/group_messaging/tests.py b/group_messaging/tests.py
index 9cc69fb8..f0a2dc5c 100644
--- a/group_messaging/tests.py
+++ b/group_messaging/tests.py
@@ -26,23 +26,43 @@ def create_user(name):
user.groups.add(group)
return user
-class ModelTests(TestCase):
- """test cases for the `private_messaging` models"""
+class GroupMessagingTests(TestCase):
+ """base class for the test cases in this app"""
def setUp(self):
self.sender = create_user('sender')
self.recipient = create_user('recipient')
- def create_thread(self, recipients):
+ def create_thread(self, sender, recipient_groups):
return Message.objects.create_thread(
- sender=self.sender, recipients=recipients,
+ sender=sender, recipients=recipient_groups,
text=MESSAGE_TEXT
)
- def create_thread_for_user(self, user):
- group = get_personal_group(user)
- return self.create_thread([group])
+ def create_thread_for_user(self, sender, recipient):
+ group = get_personal_group(recipient)
+ return self.create_thread(sender, [group])
+ def setup_three_message_thread(self, original_poster=None, responder=None):
+ """talk in this order: sender, recipient, sender"""
+ original_poster = original_poster or self.sender
+ responder = responder or self.recipient
+
+ root_message = self.create_thread_for_user(original_poster, responder)
+ response = Message.objects.create_response(
+ sender=responder,
+ text='some response',
+ parent=root_message
+ )
+ response2 = Message.objects.create_response(
+ sender=original_poster,
+ text='some response2',
+ parent=response
+ )
+ return root_message, response, response2
+
+
+class ViewsTests(GroupMessagingTests):
def get_view_context(self, view_class, data=None, user=None, method='GET'):
spec = ['REQUEST', 'user']
@@ -54,25 +74,87 @@ class ModelTests(TestCase):
request.user = user
return view_class().get_context(request)
- def setup_three_message_thread(self):
- """talk in this order: sender, recipient, sender"""
- root_message = self.create_thread_for_user(self.recipient)
+ def test_new_response_marks_thread_heading_as_new(self):
+ root = self.create_thread_for_user(self.sender, self.recipient)
+ response = Message.objects.create_response(
+ sender=self.recipient,
+ text='some response',
+ parent=root
+ )
+ #response must show as "new" to the self.sender
+ context = self.get_view_context(
+ ThreadsList,
+ data={'sender_id': '-1'},
+ user=self.sender
+ )
+ self.assertEqual(context['threads_data'][root.id]['status'], 'new')
+ #"visit" the thread
+ last_visit_time = LastVisitTime.objects.create(
+ user=self.sender,
+ message=root
+ )
+ time.sleep(1.5)
+
+ #response must show as "seen"
+ context = self.get_view_context(
+ ThreadsList,
+ data={'sender_id': '-1'},
+ user=self.sender
+ )
+ self.assertEqual(context['threads_data'][root.id]['status'], 'seen')
+ #self.recipient makes another response
+ response = Message.objects.create_response(
+ sender=self.recipient,
+ text='some response',
+ parent=response
+ )
+ #thread must be "new" again
+ context = self.get_view_context(
+ ThreadsList,
+ data={'sender_id': '-1'},
+ user=self.sender
+ )
+ self.assertEqual(context['threads_data'][root.id]['status'], 'new')
+
+ def test_answer_to_deleted_thread_undeletes_thread(self):
+ #setup: message, reply, responder deletes thread
+ root_message = self.create_thread_for_user(self.sender, self.recipient)
response = Message.objects.create_response(
sender=self.recipient,
text='some response',
parent=root_message
)
+ memo1, created = MessageMemo.objects.get_or_create(
+ message=root_message,
+ user=self.recipient,
+ status=MessageMemo.ARCHIVED
+ )
+ #OP sends reply to reply
response2 = Message.objects.create_response(
sender=self.sender,
text='some response2',
parent=response
)
- return root_message, response, response2
+
+ context = self.get_view_context(
+ ThreadsList,
+ data={'sender_id': '-1'},
+ user=self.recipient
+ )
+
+ self.assertEqual(len(context['threads']), 1)
+ thread_id = context['threads'][0].id
+ thread_data = context['threads_data'][thread_id]
+ self.assertEqual(thread_data['status'], 'new')
+
+
+class ModelsTests(GroupMessagingTests):
+ """test cases for the `private_messaging` models"""
def test_create_thread_for_user(self):
"""the basic create thread with one recipient
tests that the recipient is there"""
- message = self.create_thread_for_user(self.recipient)
+ message = self.create_thread_for_user(self.sender, self.recipient)
#message type is stored
self.assertEqual(message.message_type, Message.STORED)
#recipient is in the list of recipients
@@ -100,7 +182,7 @@ class ModelTests(TestCase):
member of the group has updated the sender list"""
group = Group.objects.create(name='somegroup')
self.recipient.groups.add(group)
- message = self.create_thread([group])
+ message = self.create_thread(self.sender, [group])
senders = SenderList.objects.get_senders_for_user(self.recipient)
self.assertEqual(set(senders), set([self.sender]))
@@ -108,7 +190,7 @@ class ModelTests(TestCase):
"""create a thread with one response,
then load thread for the user
test that only the root message is retrieved"""
- root_message = self.create_thread_for_user(self.recipient)
+ root_message = self.create_thread_for_user(self.sender, self.recipient)
response = Message.objects.create_response(
sender=self.recipient,
text='some response',
@@ -131,7 +213,7 @@ class ModelTests(TestCase):
self.assertEqual(recipients, expected_recipients)
def test_get_threads(self):
- root_message = self.create_thread_for_user(self.recipient)
+ root_message = self.create_thread_for_user(self.sender, self.recipient)
threads = set(Message.objects.get_threads(recipient=self.sender))
self.assertEqual(threads, set([]))
threads = set(Message.objects.get_threads(recipient=self.recipient))
@@ -147,37 +229,6 @@ class ModelTests(TestCase):
threads = set(Message.objects.get_threads(recipient=self.recipient))
self.assertEqual(threads, set([root_message]))
- def test_answer_to_deleted_thread_undeletes_thread(self):
- #setup: message, reply, responder deletes thread
- root_message = self.create_thread_for_user(self.recipient)
- response = Message.objects.create_response(
- sender=self.recipient,
- text='some response',
- parent=root_message
- )
- memo1, created = MessageMemo.objects.get_or_create(
- message=root_message,
- user=self.recipient,
- status=MessageMemo.ARCHIVED
- )
- #OP sends reply to reply
- response2 = Message.objects.create_response(
- sender=self.sender,
- text='some response2',
- parent=response
- )
-
- context = self.get_view_context(
- ThreadsList,
- data={'sender_id': '-1'},
- user=self.recipient
- )
-
- self.assertEqual(len(context['threads']), 1)
- thread_id = context['threads'][0].id
- thread_data = context['threads_data'][thread_id]
- self.assertEqual(thread_data['status'], 'new')
-
def test_deleting_thread_is_user_specific(self):
"""when one user deletes thread, that same thread
should not end up deleted by another user
@@ -205,7 +256,7 @@ class ModelTests(TestCase):
self.assertEquals(threads.count(), 1)
def test_user_specific_inboxes(self):
- self.create_thread_for_user(self.recipient)
+ self.create_thread_for_user(self.sender, self.recipient)
threads = Message.objects.get_threads(
recipient=self.recipient, sender=self.sender
@@ -216,50 +267,8 @@ class ModelTests(TestCase):
)
self.assertEqual(threads.count(), 0)
- def test_new_response_marks_thread_heading_as_new(self):
- root = self.create_thread_for_user(self.recipient)
- response = Message.objects.create_response(
- sender=self.recipient,
- text='some response',
- parent=root
- )
- #response must show as "new" to the self.sender
- context = self.get_view_context(
- ThreadsList,
- data={'sender_id': '-1'},
- user=self.sender
- )
- self.assertEqual(context['threads_data'][root.id]['status'], 'new')
- #"visit" the thread
- last_visit_time = LastVisitTime.objects.create(
- user=self.sender,
- message=root
- )
- time.sleep(1.5)
-
- #response must show as "seen"
- context = self.get_view_context(
- ThreadsList,
- data={'sender_id': '-1'},
- user=self.sender
- )
- self.assertEqual(context['threads_data'][root.id]['status'], 'seen')
- #self.recipient makes another response
- response = Message.objects.create_response(
- sender=self.recipient,
- text='some response',
- parent=response
- )
- #thread must be "new" again
- context = self.get_view_context(
- ThreadsList,
- data={'sender_id': '-1'},
- user=self.sender
- )
- self.assertEqual(context['threads_data'][root.id]['status'], 'new')
-
def test_response_updates_thread_headline(self):
- root = self.create_thread_for_user(self.recipient)
+ root = self.create_thread_for_user(self.sender, self.recipient)
response = Message.objects.create_response(
sender=self.recipient,
text='some response',
@@ -268,8 +277,38 @@ class ModelTests(TestCase):
self.assertEqual(root.headline, 'some response')
def test_email_alert_sent(self):
- root = self.create_thread_for_user(self.recipient)
+ root = self.create_thread_for_user(self.sender, self.recipient)
from django.core.mail import outbox
self.assertEqual(len(outbox), 1)
self.assertEqual(len(outbox[0].recipients()), 1)
self.assertEqual(outbox[0].recipients()[0], self.recipient.email)
+
+ def test_get_sent_threads(self):
+ root1, re11, re12 = self.setup_three_message_thread()
+ root2, re21, re22 = self.setup_three_message_thread(
+ original_poster=self.recipient, responder=self.sender
+ )
+ root3, re31, re32 = self.setup_three_message_thread()
+
+ #mark root2 as seen
+ root2.mark_as_seen(self.sender)
+ #mark root3 as deleted
+ root3.archive(self.sender)
+
+ threads = Message.objects.get_sent_threads(sender=self.sender)
+ self.assertEqual(threads.count(), 2)
+ self.assertEqual(set(threads), set([root1, root2]))#root3 is deleted
+
+ def test_recipient_lists_are_in_senders_info(self):
+ thread = self.create_thread_for_user(self.sender, self.recipient)
+ self.assertTrue(self.recipient.username in thread.senders_info)
+
+ def test_self_response_not_in_senders_inbox(self):
+ root = self.create_thread_for_user(self.sender, self.recipient)
+ response = Message.objects.create_response(
+ sender=self.sender,
+ text='some response',
+ parent=root
+ )
+ threads = Message.objects.get_threads(recipient=self.sender)
+ self.assertEqual(threads.count(), 0)
diff --git a/group_messaging/views.py b/group_messaging/views.py
index 3d973dbe..cd1a74d3 100644
--- a/group_messaging/views.py
+++ b/group_messaging/views.py
@@ -12,6 +12,7 @@ import copy
import datetime
from coffin.template.loader import get_template
from django.contrib.auth.models import User
+from django.db import models
from django.forms import IntegerField
from django.http import HttpResponse
from django.http import HttpResponseNotAllowed
@@ -105,7 +106,12 @@ class NewThread(InboxView):
if missing:
result['success'] = False
result['missing_users'] = missing
- else:
+
+ if request.user.username in usernames:
+ result['success'] = False
+ result['self_message'] = True
+
+ if result.get('success', True):
recipients = get_personal_groups_for_users(users)
message = Message.objects.create_thread(
sender=request.user,
@@ -157,6 +163,8 @@ class ThreadsList(InboxView):
)
elif sender_id == -1:
threads = Message.objects.get_threads(recipient=request.user)
+ elif sender_id == request.user.id:
+ threads = Message.objects.get_sent_threads(sender=request.user)
else:
sender = User.objects.get(id=sender_id)
threads = Message.objects.get_threads(
@@ -181,6 +189,17 @@ class ThreadsList(InboxView):
thread_data['thread'] = thread
threads_data[thread.id] = thread_data
+ ids = [thread.id for thread in threads]
+ counts = Message.objects.filter(
+ id__in=ids
+ ).annotate(
+ responses_count=models.Count('descendants')
+ ).values('id', 'responses_count')
+ for count in counts:
+ thread_id = count['id']
+ responses_count = count['responses_count']
+ threads_data[thread_id]['responses_count'] = responses_count
+
last_visit_times = LastVisitTime.objects.filter(
user=request.user,
message__in=threads
@@ -243,7 +262,7 @@ class SendersList(InboxView):
"""get data about senders for the user"""
senders = SenderList.objects.get_senders_for_user(request.user)
senders = senders.values('id', 'username')
- return {'senders': senders}
+ return {'senders': senders, 'request_user_id': request.user.id}
class ThreadDetails(InboxView):