diff options
Diffstat (limited to 'group_messaging')
-rw-r--r-- | group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py | 142 | ||||
-rw-r--r-- | group_messaging/models.py | 91 | ||||
-rw-r--r-- | group_messaging/tests.py | 20 | ||||
-rw-r--r-- | group_messaging/urls.py | 10 | ||||
-rw-r--r-- | group_messaging/views.py | 116 |
5 files changed, 328 insertions, 51 deletions
diff --git a/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py b/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py new file mode 100644 index 00000000..5e92ef2b --- /dev/null +++ b/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'LastVisitTime' + db.create_table('group_messaging_lastvisittime', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('message', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group_messaging.Message'])), + ('at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('group_messaging', ['LastVisitTime']) + + # Adding unique constraint on 'LastVisitTime', fields ['user', 'message'] + db.create_unique('group_messaging_lastvisittime', ['user_id', 'message_id']) + + # Adding field 'Message.senders_info' + db.add_column('group_messaging_message', 'senders_info', + self.gf('django.db.models.fields.CharField')(default='', max_length=64), + keep_default=False) + + def backwards(self, orm): + # Removing unique constraint on 'LastVisitTime', fields ['user', 'message'] + db.delete_unique('group_messaging_lastvisittime', ['user_id', 'message_id']) + + # Deleting model 'LastVisitTime' + db.delete_table('group_messaging_lastvisittime') + + # Deleting field 'Message.senders_info' + db.delete_column('group_messaging_message', 'senders_info') + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'group_messaging.lastvisittime': { + 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'LastVisitTime'}, + 'at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group_messaging.Message']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'group_messaging.message': { + 'Meta': {'object_name': 'Message'}, + 'active_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'headline': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_active_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'message_type': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['group_messaging.Message']"}), + 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False'}), + 'root': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'descendants'", 'null': 'True', 'to': "orm['group_messaging.Message']"}), + 'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_messages'", 'to': "orm['auth.User']"}), + 'senders_info': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}), + 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'group_messaging.messagememo': { + 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'MessageMemo'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group_messaging.Message']"}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'group_messaging.senderlist': { + 'Meta': {'object_name': 'SenderList'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'unique': 'True'}), + 'senders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}) + } + } + + complete_apps = ['group_messaging']
\ No newline at end of file diff --git a/group_messaging/models.py b/group_messaging/models.py index 838134e7..62f720cf 100644 --- a/group_messaging/models.py +++ b/group_messaging/models.py @@ -1,14 +1,26 @@ +"""models for the ``group_messaging`` app +""" +import datetime from django.db import models from django.contrib.auth.models import Group from django.contrib.auth.models import User MAX_TITLE_LENGTH = 80 +MAX_SENDERS_INFO_LENGTH = 64 #dummy parse message function parse_message = lambda v: v +GROUP_NAME_TPL = '_personal_%s' + def get_personal_group_by_user_id(user_id): - return Group.objects.get(name='_personal_%s' % user_id) + return Group.objects.get(name=GROUP_NAME_TPL % user_id) + + +def get_personal_groups_for_users(users): + """for a given list of users return their personal groups""" + group_names = [(GROUP_NAME_TPL % user.id) for user in users] + return Group.objects.filter(name__in=group_names) def get_personal_group(user): @@ -18,11 +30,23 @@ def get_personal_group(user): def create_personal_group(user): """creates a personal group for the user""" - group = Group(name='_personal_%s' % user.id) + group = Group(name=GROUP_NAME_TPL % user.id) group.save() return group +class LastVisitTime(models.Model): + """just remembers when a user has + last visited a given thread + """ + user = models.ForeignKey(User) + message = models.ForeignKey('Message') + at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'message') + + class SenderListManager(models.Manager): """model manager for the :class:`SenderList`""" @@ -61,7 +85,6 @@ class MessageMemo(models.Model): STATUS_CHOICES = ( (SEEN, 'seen'), (ARCHIVED, 'archived') - ) user = models.ForeignKey(User) message = models.ForeignKey('Message') @@ -99,13 +122,24 @@ class MessageManager(models.Manager): headline = kwargs.get('headline', kwargs['text']) kwargs['headline'] = headline[:MAX_TITLE_LENGTH] kwargs['html'] = parse_message(kwargs['text']) - return super(MessageManager, self).create(**kwargs) + + message = super(MessageManager, self).create(**kwargs) + #creator of message saw it by definition + #crate a "seen" memo for the sender, because we + #don't want to inform the user about his/her own post + sender = kwargs['sender'] + MessageMemo.objects.create( + message=message, user=sender, status=MessageMemo.SEEN + ) + return message + def create_thread(self, sender=None, recipients=None, text=None): """creates a stored message and adds recipients""" message = self.create( message_type=Message.STORED, sender=sender, + senders_info=sender.username, text=text, ) message.add_recipients(recipients) @@ -123,10 +157,13 @@ class MessageManager(models.Manager): recipients = set(parent.recipients.all()) senders_group = get_personal_group(parent.sender) recipients.add(senders_group) - message.add_recipients(recipients, ignore_user=sender) + message.add_recipients(recipients) #add author of the parent as a recipient to parent - #but make sure to mute the message - parent.add_recipients([senders_group], ignore_user=parent.sender) + parent.add_recipients([senders_group]) + #mark last active timestamp for the root message + #so that we know that this thread was most recently + #updated + message.update_root_info() return message @@ -150,47 +187,63 @@ class Message(models.Model): ) sender = models.ForeignKey(User, related_name='sent_messages') + + senders_info = models.CharField( + max_length=MAX_SENDERS_INFO_LENGTH, + default='' + )#comma-separated list of a few names + recipients = models.ManyToManyField(Group) + root = models.ForeignKey( 'self', null=True, blank=True, related_name='descendants' ) + parent = models.ForeignKey( 'self', null=True, blank=True, related_name='children' ) + headline = models.CharField(max_length=MAX_TITLE_LENGTH) + text = models.TextField( null=True, blank=True, help_text='source text for the message, e.g. in markdown format' ) + html = models.TextField( null=True, blank=True, help_text='rendered html of the message' ) + sent_at = models.DateTimeField(auto_now_add=True) last_active_at = models.DateTimeField(auto_now_add=True) active_until = models.DateTimeField(blank=True, null=True) objects = MessageManager() - def add_recipients(self, recipients, ignore_user=None): + def add_recipients(self, recipients): """adds recipients to the message and updates the sender lists for all recipients todo: sender lists may be updated in a lazy way - per user - - `ignore_user` parameter is used to mark a specific user - as not needing to receive a message as new, even if that - user is a member of any of the recipient groups """ - if ignore_user: - #crate a "seen" memo for the sender, because we - #don't want to inform the user about his/her own post - MessageMemo.objects.create( - message=self, user=self.sender, status=MessageMemo.SEEN - ) - self.recipients.add(*recipients) for recipient in recipients: sender_list, created = SenderList.objects.get_or_create(recipient=recipient) sender_list.senders.add(self.sender) + + def update_root_info(self): + """Update the last active at timestamp and + the contributors info, if relevant. + Root object will be saved to the database. + """ + self.root.last_active_at = datetime.datetime.now() + senders_names = self.root.senders_info.split(',') + + if self.sender.username in senders_names: + senders_names.remove(self.sender.username) + senders_names.insert(0, self.sender.username) + + self.root.senders_info = (','.join(senders_names))[:64] + self.root.save() diff --git a/group_messaging/tests.py b/group_messaging/tests.py index 80f6f792..c8401dc1 100644 --- a/group_messaging/tests.py +++ b/group_messaging/tests.py @@ -51,11 +51,19 @@ class ModelTests(TestCase): #sender_group = get_personal_group(self.sender) #maybe add this too expected_recipients = set([recipient_group]) self.assertEqual(recipients, expected_recipients) - self.assertRaises( - MessageMemo.DoesNotExist, - MessageMemo.objects.get, - message=message - ) + #self.assertRaises( + # MessageMemo.DoesNotExist, + # MessageMemo.objects.get, + # message=message + #) + #make sure that the original senders memo to the root + #message is marke ad seen + memos = MessageMemo.objects.filter( + message=message, + user=self.sender + ) + self.assertEquals(memos.count(), 1) + self.assertEqual(memos[0].status, MessageMemo.SEEN) def test_get_senders_for_user(self): """this time send thread to a real group test that @@ -77,11 +85,13 @@ class ModelTests(TestCase): parent=root_message ) self.assertEqual(response.message_type, Message.STORED) + #assert that there is only one "seen" memo for the response memos = MessageMemo.objects.filter(message=response) self.assertEqual(memos.count(), 1) self.assertEqual(memos[0].user, self.recipient) self.assertEqual(memos[0].status, MessageMemo.SEEN) + #assert that recipients are the two people who are part of #this conversation recipients = set(response.recipients.all()) diff --git a/group_messaging/urls.py b/group_messaging/urls.py index eb033751..30002bf3 100644 --- a/group_messaging/urls.py +++ b/group_messaging/urls.py @@ -10,6 +10,11 @@ urlpatterns = patterns('', name='get_threads' ), url( + '^threads/(?P<thread_id>\d+)/$', + views.ThreadDetails().as_view(), + name='thread_details' + ), + url( '^threads/create/$', views.NewThread().as_view(), name='create_thread' @@ -18,5 +23,10 @@ urlpatterns = patterns('', '^senders/$', views.SendersList().as_view(), name='get_senders' + ), + url( + '^post-reply/$', + views.PostReply().as_view(), + name='post_reply' ) ) diff --git a/group_messaging/views.py b/group_messaging/views.py index 9d324d62..289961ff 100644 --- a/group_messaging/views.py +++ b/group_messaging/views.py @@ -8,7 +8,10 @@ in order to render messages within the page. Notice that :mod:`urls` module decorates all these functions and turns them into complete views """ +import copy +import datetime from coffin.template.loader import get_template +from django.contrib.auth.models import User from django.forms import IntegerField from django.http import HttpResponse from django.http import HttpResponseNotAllowed @@ -16,7 +19,9 @@ from django.http import HttpResponseForbidden from django.utils import simplejson from group_messaging.models import Message from group_messaging.models import SenderList +from group_messaging.models import LastVisitTime from group_messaging.models import get_personal_group_by_user_id +from group_messaging.models import get_personal_groups_for_users class InboxView(object): """custom class-based view @@ -35,9 +40,9 @@ class InboxView(object): """ if template_name is None: template_name = self.template_name - template = get_template(self.template_name) + template = get_template(template_name) html = template.render(context) - json = simplejson.dumps({'html': html}) + json = simplejson.dumps({'html': html, 'success': True}) return HttpResponse(json, mimetype='application/json') @@ -78,8 +83,7 @@ class InboxView(object): class NewThread(InboxView): """view for creation of new thread""" - template_name = 'create_thread.html'# contains new thread form - http_method_list = ('GET', 'POST') + http_method_list = ('POST',) def post(self, request): """creates a new thread on behalf of the user @@ -87,18 +91,32 @@ class NewThread(InboxView): need to go back to the thread listing view whose content should be cached in the client' """ - username = IntegerField().clean(request.POST['to_username']) - user = User.objects.get(username=username) - recipient = get_personal_group_by_user_id(user.id) - Message.objects.create_thread( - sender=request.user, - recipients=[recipient], - text=request.POST['text'] - ) - return HttpResponse('', mimetype='application/json') - - -class NewResponse(InboxView): + usernames = request.POST['to_usernames'] + usernames = map(lambda v: v.strip(), usernames.split(',')) + users = User.objects.filter(username__in=usernames) + + missing = copy.copy(usernames) + for user in users: + if user.username in missing: + missing.remove(user.username) + + result = dict() + if missing: + result['success'] = False + result['missing_users'] = missing + else: + recipients = get_personal_groups_for_users(users) + message = Message.objects.create_thread( + sender=request.user, + recipients=recipients, + text=request.POST['text'] + ) + result['success'] = True + result['message_id'] = message.id + return HttpResponse(simplejson.dumps(result), mimetype='application/json') + + +class PostReply(InboxView): """view to create a new response""" http_method_list = ('POST',) @@ -110,25 +128,63 @@ class NewResponse(InboxView): text=request.POST['text'], parent=parent ) + last_visit = LastVisitTime.objects.get( + message=message.root, + user=request.user + ) + last_visit.at = datetime.datetime.now() + last_visit.save() return self.render_to_response( - {'message': message}, template_name='stored_message.htmtl' + {'post': message, 'user': request.user}, + template_name='group_messaging/stored_message.html' ) class ThreadsList(InboxView): """shows list of threads for a given user""" - template_name = 'threads_list.html' + template_name = 'group_messaging/threads_list.html' http_method_list = ('GET',) def get_context(self, request): """returns thread list data""" + #get threads and the last visit time threads = Message.objects.get_threads_for_user(request.user) - threads = threads.values('id', 'headline') - return {'threads': threads} + + sender_id = IntegerField().clean(request.GET.get('sender_id', '-1')) + if sender_id != -1: + threads = threads.filter(sender__id=sender_id) + + #for each thread we need to know if there is something + #unread for the user - to mark "new" threads as bold + threads_data = dict() + for thread in threads: + thread_data = dict() + #determine status + thread_data['status'] = 'new' + #determine the senders info + senders_names = thread.senders_info.split(',') + if request.user.username in senders_names: + senders_names.remove(request.user.username) + thread_data['senders_info'] = ', '.join(senders_names) + thread_data['thread'] = thread + threads_data[thread.id] = thread_data + + last_visit_times = LastVisitTime.objects.filter( + user=request.user, + message__in=threads + ) + for last_visit in last_visit_times: + thread_data = threads_data[last_visit.message_id] + if thread_data['thread'].last_active_at <= last_visit.at: + thread_data['status'] = 'seen' + + #after we have all the data - update the last visit time + last_visit_times.update(at=datetime.datetime.now()) + return {'threads': threads, 'threads_data': threads_data} class SendersList(InboxView): """shows list of senders for a user""" - template_name = 'senders_list.html' + template_name = 'group_messaging/senders_list.html' http_method_names = ('GET',) def get_context(self, request): @@ -140,13 +196,19 @@ class SendersList(InboxView): class ThreadDetails(InboxView): """shows entire thread in the unfolded form""" - template_name = 'thread_details.html' + template_name = 'group_messaging/thread_details.html' http_method_names = ('GET',) - def get_context(self, request): + def get_context(self, request, thread_id=None): """shows individual thread""" - thread_id = IntegerField().clean(request.GET['thread_id']) #todo: assert that current thread is the root - messages = Message.objects.filter(root__id=thread_id) - messages = messages.values('html') - return {'messages': messages} + root = Message.objects.get(id=thread_id) + responses = Message.objects.filter(root__id=thread_id) + last_visit, created = LastVisitTime.objects.get_or_create( + message=root, + user=request.user + ) + if created is False: + last_visit.at = datetime.datetime.now() + last_visit.save() + return {'root_message': root, 'responses': responses, 'request': request} |