From ba1ef99a5e55d2a08bb1ed7176dff6fb9abd9134 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 18 Nov 2012 04:28:39 -0300 Subject: temporarily moved group_messaging files to askbot/deps --- askbot/deps/group_messaging/__init__.py | 17 + .../group_messaging/migrations/0001_initial.py | 177 +++++++++ ...dd_unique_lastvisittime_user_message__add_fi.py | 142 +++++++ askbot/deps/group_messaging/migrations/__init__.py | 0 askbot/deps/group_messaging/models.py | 438 +++++++++++++++++++++ askbot/deps/group_messaging/tests.py | 363 +++++++++++++++++ askbot/deps/group_messaging/urls.py | 37 ++ askbot/deps/group_messaging/views.py | 286 ++++++++++++++ group_messaging/__init__.py | 17 - group_messaging/migrations/0001_initial.py | 177 --------- ...dd_unique_lastvisittime_user_message__add_fi.py | 142 ------- group_messaging/migrations/__init__.py | 0 group_messaging/models.py | 438 --------------------- group_messaging/tests.py | 363 ----------------- group_messaging/urls.py | 37 -- group_messaging/views.py | 286 -------------- 16 files changed, 1460 insertions(+), 1460 deletions(-) create mode 100644 askbot/deps/group_messaging/__init__.py create mode 100644 askbot/deps/group_messaging/migrations/0001_initial.py create mode 100644 askbot/deps/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py create mode 100644 askbot/deps/group_messaging/migrations/__init__.py create mode 100644 askbot/deps/group_messaging/models.py create mode 100644 askbot/deps/group_messaging/tests.py create mode 100644 askbot/deps/group_messaging/urls.py create mode 100644 askbot/deps/group_messaging/views.py delete mode 100644 group_messaging/__init__.py delete mode 100644 group_messaging/migrations/0001_initial.py delete mode 100644 group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py delete mode 100644 group_messaging/migrations/__init__.py delete mode 100644 group_messaging/models.py delete mode 100644 group_messaging/tests.py delete mode 100644 group_messaging/urls.py delete mode 100644 group_messaging/views.py diff --git a/askbot/deps/group_messaging/__init__.py b/askbot/deps/group_messaging/__init__.py new file mode 100644 index 00000000..ed3d73ff --- /dev/null +++ b/askbot/deps/group_messaging/__init__.py @@ -0,0 +1,17 @@ +"""`group_messages` is a django application +which allows users send messages to other users +and groups (instances of :class:`django.contrib.auth.models.Group`) + +The same methods are used are used to send messages +to users as to groups - achieved via special "personal groups". + +By convention - personal groups have names formatted as follows: +_personal_, for example for the user whose `id == 1`, +the group should be named `'_personal_1'`. + +Only one person must be a member of a personal group and +each user must have such group. + +TODO: decouple this application +first step is to package send_mail separately +""" diff --git a/askbot/deps/group_messaging/migrations/0001_initial.py b/askbot/deps/group_messaging/migrations/0001_initial.py new file mode 100644 index 00000000..7d907dc1 --- /dev/null +++ b/askbot/deps/group_messaging/migrations/0001_initial.py @@ -0,0 +1,177 @@ +# -*- 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 'SenderList' + db.create_table('group_messaging_senderlist', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('recipient', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], unique=True)), + )) + db.send_create_signal('group_messaging', ['SenderList']) + + # Adding M2M table for field senders on 'SenderList' + db.create_table('group_messaging_senderlist_senders', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('senderlist', models.ForeignKey(orm['group_messaging.senderlist'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('group_messaging_senderlist_senders', ['senderlist_id', 'user_id']) + + # Adding model 'MessageMemo' + db.create_table('group_messaging_messagememo', ( + ('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'])), + ('status', self.gf('django.db.models.fields.SmallIntegerField')(default=0)), + )) + db.send_create_signal('group_messaging', ['MessageMemo']) + + # Adding unique constraint on 'MessageMemo', fields ['user', 'message'] + db.create_unique('group_messaging_messagememo', ['user_id', 'message_id']) + + # Adding model 'Message' + db.create_table('group_messaging_message', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('message_type', self.gf('django.db.models.fields.SmallIntegerField')(default=0)), + ('sender', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sent_messages', to=orm['auth.User'])), + ('root', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='descendants', null=True, to=orm['group_messaging.Message'])), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['group_messaging.Message'])), + ('headline', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('text', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('sent_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('last_active_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('active_until', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + )) + db.send_create_signal('group_messaging', ['Message']) + + # Adding M2M table for field recipients on 'Message' + db.create_table('group_messaging_message_recipients', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('message', models.ForeignKey(orm['group_messaging.message'], null=False)), + ('group', models.ForeignKey(orm['auth.group'], null=False)) + )) + db.create_unique('group_messaging_message_recipients', ['message_id', 'group_id']) + + def backwards(self, orm): + # Removing unique constraint on 'MessageMemo', fields ['user', 'message'] + db.delete_unique('group_messaging_messagememo', ['user_id', 'message_id']) + + # Deleting model 'SenderList' + db.delete_table('group_messaging_senderlist') + + # Removing M2M table for field senders on 'SenderList' + db.delete_table('group_messaging_senderlist_senders') + + # Deleting model 'MessageMemo' + db.delete_table('group_messaging_messagememo') + + # Deleting model 'Message' + db.delete_table('group_messaging_message') + + # Removing M2M table for field recipients on 'Message' + db.delete_table('group_messaging_message_recipients') + + 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.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']"}), + '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/askbot/deps/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py b/askbot/deps/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/askbot/deps/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/askbot/deps/group_messaging/migrations/__init__.py b/askbot/deps/group_messaging/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/askbot/deps/group_messaging/models.py b/askbot/deps/group_messaging/models.py new file mode 100644 index 00000000..5d43baf8 --- /dev/null +++ b/askbot/deps/group_messaging/models.py @@ -0,0 +1,438 @@ +"""models for the ``group_messaging`` app +""" +import copy +import datetime +import urllib +from askbot.mail import send_mail #todo: remove dependency? +from django.template.loader import get_template +from django.db import models +from django.db.models import signals +from django.conf import settings as django_settings +from django.contrib.auth.models import Group +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.utils.importlib import import_module +from django.utils.translation import ugettext as _ + +MAX_HEADLINE_LENGTH = 80 +MAX_SENDERS_INFO_LENGTH = 64 +MAX_SUBJECT_LINE_LENGTH = 30 + +#dummy parse message function +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) + + +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): + """returns personal group for the user""" + return get_personal_group_by_user_id(user.id) + + +def create_personal_group(user): + """creates a personal group for the user""" + 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`""" + + def get_senders_for_user(self, user=None): + """returns query set of :class:`User`""" + user_groups = user.groups.all() + lists = self.filter(recipient__in=user_groups) + user_ids = lists.values_list( + 'senders__id', flat=True + ).distinct() + return User.objects.filter(id__in=user_ids) + +class SenderList(models.Model): + """a model to store denormalized data + about who sends messages to any given person + sender list is populated automatically + as new messages are created + """ + recipient = models.ForeignKey(Group, unique=True) + senders = models.ManyToManyField(User) + objects = SenderListManager() + + +class MessageMemo(models.Model): + """A bridge between message recipients and messages + these records are only created when user sees a message. + The idea is that using groups as recipients, we can send + messages to massive numbers of users, without cluttering + the database. + + Instead we'll be creating a "seen" message after user + reads the message. + """ + SEEN = 0 + ARCHIVED = 1 + STATUS_CHOICES = ( + (SEEN, 'seen'), + (ARCHIVED, 'archived') + ) + user = models.ForeignKey(User) + message = models.ForeignKey('Message', related_name='memos') + status = models.SmallIntegerField( + choices=STATUS_CHOICES, default=SEEN + ) + + class Meta: + unique_together = ('user', 'message') + + +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): + """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: + filter = filter & models.Q(sender=sender) + + if deleted: + deleted_filter = models.Q( + memos__status=MessageMemo.ARCHIVED, + memos__user=recipient + ) + return self.filter(filter & deleted_filter) + else: + #rather a tricky query (may need to change the idea to get rid of this) + #select threads that have a memo for the user, but the memo is not ARCHIVED + #in addition, select threads that have zero memos for the user + marked_as_non_deleted_filter = models.Q( + memos__status=MessageMemo.SEEN, + memos__user=recipient + ) + #part1 - marked as non-archived + part1 = self.filter(filter & marked_as_non_deleted_filter) + #part2 - messages for the user without an attached memo + part2 = self.filter(filter & ~models.Q(memos__user=recipient)) + return (part1 | part2).distinct() + + def create(self, **kwargs): + """creates a message""" + root = kwargs.get('root', None) + if root is None: + parent = kwargs.get('parent', None) + if parent: + if parent.root: + root = parent.root + else: + root = parent + kwargs['root'] = root + + headline = kwargs.get('headline', kwargs['text']) + kwargs['headline'] = headline[:MAX_HEADLINE_LENGTH] + kwargs['html'] = parse_message(kwargs['text']) + + 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, + ) + now = datetime.datetime.now() + LastVisitTime.objects.create(message=message, user=sender, at=now) + 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 + + def create_response(self, sender=None, text=None, parent=None): + message = self.create( + parent=parent, + message_type=Message.STORED, + sender=sender, + text=text, + ) + #recipients are parent's recipients + sender + #creator of response gets memo in the "read" status + recipients = set(parent.recipients.all()) + + 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 + #update headline + message.root.headline = text[:MAX_HEADLINE_LENGTH] + #mark last active timestamp for the root message + message.root.last_active_at = datetime.datetime.now() + #update senders info - stuff that is shown in the thread heading + message.root.update_senders_info() + #unarchive the thread for all recipients + message.root.unarchive() + message.send_email_alert() + return message + + +class Message(models.Model): + """the message model allowing users to send + messages to other users and groups, via + personal groups. + """ + STORED = 0 + TEMPORARY = 1 + ONE_TIME = 2 + MESSAGE_TYPE_CHOICES = ( + (STORED, 'email-like message, stored in the inbox'), + (ONE_TIME, 'will be shown just once'), + (TEMPORARY, 'will be shown until certain time') + ) + + message_type = models.SmallIntegerField( + choices=MESSAGE_TYPE_CHOICES, + default=STORED, + ) + + 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_HEADLINE_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_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 + todo: sender lists may be updated in a lazy way - per user + """ + 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 get_absolute_url(self, user=None): + """returns absolute url to the thread""" + assert(user != None) + settings = django_settings.GROUP_MESSAGING + func_path = settings['base_url_getter_function'] + path_bits = func_path.split('.') + url_getter = getattr( + import_module('.'.join(path_bits[:-1])), + path_bits[-1] + ) + params = copy.copy(settings['base_url_params']) + params['thread_id'] = self.id + url = url_getter(user) + '?' + urllib.urlencode(params) + #if include_domain_name: #don't need this b/c + # site = Site.objects.get_current() + # url = 'http://' + site.domain + url + return url + + def get_email_subject_line(self): + """forms subject line based on the root message + and prepends 'Re': if message is non-root + """ + subject = self.get_root_message().text[:MAX_SUBJECT_LINE_LENGTH] + if self.root: + subject = _('Re: ') + subject + return subject + + def get_root_message(self): + """returns root message or self + if current message is root + """ + return self.root or self + + def get_recipients_users(self): + """returns query set of users""" + groups = self.recipients.all() + return User.objects.filter( + groups__in=groups + ).exclude( + id=self.sender.id + ).distinct() + + def get_timeline(self): + """returns ordered query set of messages in the thread + with the newest first""" + root = self.get_root_message() + root_qs = Message.objects.filter(id=root.id) + return (root.descendants.all() | root_qs).order_by('-sent_at') + + + def send_email_alert(self): + """signal handler for the message post-save""" + root_message = self.get_root_message() + data = {'messages': self.get_timeline()} + template = get_template('group_messaging/email_alert.html') + body_text = template.render(data) + subject = self.get_email_subject_line() + for user in self.get_recipients_users(): + #todo change url scheme so that all users have the same + #urls within their personal areas of the user profile + #so that we don't need to have loops like this one + thread_url = root_message.get_absolute_url(user) + thread_url = thread_url.replace('&', '&') + #in the template we have a placeholder to be replaced like this: + body_text = body_text.replace('THREAD_URL_HOLE', thread_url) + send_mail( + subject, + body_text, + django_settings.DEFAULT_FROM_EMAIL, + [user.email,], + ) + + + def update_senders_info(self): + """update the contributors info, + meant to be used on a root message only + """ + senders_names = self.senders_info.split(',') + + if self.sender.username in senders_names: + senders_names.remove(self.sender.username) + senders_names.insert(0, self.sender.username) + + self.senders_info = (','.join(senders_names))[:64] + self.save() + + def unarchive(self, user=None): + """unarchive message for all recipients""" + 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/askbot/deps/group_messaging/tests.py b/askbot/deps/group_messaging/tests.py new file mode 100644 index 00000000..bcf764db --- /dev/null +++ b/askbot/deps/group_messaging/tests.py @@ -0,0 +1,363 @@ +import datetime +import time +import urlparse +from bs4 import BeautifulSoup +from django.test import TestCase +from django.contrib.auth.models import User, Group +from group_messaging.models import Message +from group_messaging.models import MessageMemo +from group_messaging.models import SenderList +from group_messaging.models import LastVisitTime +from group_messaging.models import get_personal_group +from group_messaging.models import create_personal_group +from group_messaging.views import ThreadsList +from mock import Mock + +MESSAGE_TEXT = 'test message text' + +def create_user(name): + """creates a user and a personal group, + returns the created user""" + user = User.objects.create_user(name, name + '@example.com') + #note that askbot will take care of three lines below automatically + try: + group = get_personal_group(user) + except Group.DoesNotExist: + group = create_personal_group(user) + group_name = '_personal_%d' % user.id + group, created = Group.objects.get_or_create(name=group_name) + user.groups.add(group) + return user + +def get_html_message(mail_message): + """mail message is an item from the django.core.mail.outbox""" + return mail_message.alternatives[0][0] + +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, sender, recipient_groups): + return Message.objects.create_thread( + sender=sender, recipients=recipient_groups, + text=MESSAGE_TEXT + ) + + 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'] + assert(method in ('GET', 'POST')) + spec.append(method) + request = Mock(spec=spec) + request.REQUEST = data + setattr(request, method, data) + request.user = user + return view_class().get_context(request) + + 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: todo - make a method + last_visit_time, created = LastVisitTime.objects.get_or_create( + user=self.sender, + message=root + ) + last_visit_time.at = datetime.datetime.now() + last_visit_time.save() + 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 + ) + + 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_emailed_message_url_works_for_post_recipient(self): + root = self.create_thread_for_user(self.sender, self.recipient) + from django.core.mail import outbox + html_message = get_html_message(outbox[0]) + link = BeautifulSoup(html_message).find('a', attrs={'class': 'thread-link'}) + url = link['href'].replace('&', '&') + parsed_url = urlparse.urlparse(url) + url_data = urlparse.parse_qsl(parsed_url.query) + self.client.login(user_id=self.recipient.id, method='force') + response = self.client.get(parsed_url.path, url_data) + dom = BeautifulSoup(response.content) + threads = dom.find_all('ul', attrs={'class': 'thread'}) + self.assertEquals(len(threads), 1) + thread_lists = dom.find_all('table', attrs={'class': 'threads-list'}) + self.assertEquals(len(thread_lists), 0) + + def test_sent_thread_is_visited_by_sender(self): + root = self.create_thread_for_user(self.sender, self.recipient) + context = self.get_view_context( + ThreadsList, + data={'sender_id': str(self.sender.id)}, + user=self.sender + ) + thread_data = context['threads_data'][root.id] + self.assertEqual(thread_data['status'], 'seen') + +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.sender, self.recipient) + #message type is stored + self.assertEqual(message.message_type, Message.STORED) + #recipient is in the list of recipients + recipients = set(message.recipients.all()) + recipient_group = get_personal_group(self.recipient) + #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 + #) + #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 + member of the group has updated the sender list""" + group = Group.objects.create(name='somegroup') + self.recipient.groups.add(group) + message = self.create_thread(self.sender, [group]) + senders = SenderList.objects.get_senders_for_user(self.recipient) + self.assertEqual(set(senders), set([self.sender])) + + def test_create_thread_response(self): + """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.sender, self.recipient) + response = Message.objects.create_response( + sender=self.recipient, + text='some response', + 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()) + sender_group = get_personal_group(self.sender) + recipient_group = get_personal_group(self.recipient) + expected_recipients = set([sender_group, recipient_group]) + self.assertEqual(recipients, expected_recipients) + + def test_get_threads(self): + 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)) + self.assertEqual(threads, set([root_message])) + + response = Message.objects.create_response( + sender=self.recipient, + text='some response', + parent=root_message + ) + threads = set(Message.objects.get_threads(recipient=self.sender)) + self.assertEqual(threads, set([root_message])) + threads = set(Message.objects.get_threads(recipient=self.recipient)) + self.assertEqual(threads, set([root_message])) + + def test_deleting_thread_is_user_specific(self): + """when one user deletes thread, that same thread + should not end up deleted by another user + """ + root, response, response2 = self.setup_three_message_thread() + + threads = Message.objects.get_threads(recipient=self.sender) + self.assertEquals(threads.count(), 1) + threads = Message.objects.get_threads(recipient=self.recipient) + self.assertEquals(threads.count(), 1) + + memo1, created = MessageMemo.objects.get_or_create( + message=root, + user=self.recipient, + status=MessageMemo.ARCHIVED + ) + + threads = Message.objects.get_threads(recipient=self.sender) + self.assertEquals(threads.count(), 1) + threads = Message.objects.get_threads(recipient=self.recipient) + self.assertEquals(threads.count(), 0) + threads = Message.objects.get_threads( + recipient=self.recipient, deleted=True + ) + self.assertEquals(threads.count(), 1) + + def test_user_specific_inboxes(self): + self.create_thread_for_user(self.sender, self.recipient) + + threads = Message.objects.get_threads( + recipient=self.recipient, sender=self.sender + ) + self.assertEqual(threads.count(), 1) + threads = Message.objects.get_threads( + recipient=self.sender, sender=self.recipient + ) + self.assertEqual(threads.count(), 0) + + def test_response_updates_thread_headline(self): + root = self.create_thread_for_user(self.sender, self.recipient) + response = Message.objects.create_response( + sender=self.recipient, + text='some response', + parent=root + ) + self.assertEqual(root.headline, 'some response') + + def test_email_alert_sent(self): + 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) + html_message = get_html_message(outbox[0]) + self.assertTrue(root.text in html_message) + soup = BeautifulSoup(html_message) + links = soup.find_all('a', attrs={'class': 'thread-link'}) + self.assertEqual(len(links), 1) + parse_result = urlparse.urlparse(links[0]['href']) + query = urlparse.parse_qs(parse_result.query.replace('&', '&')) + self.assertEqual(query['thread_id'][0], str(root.id)) + + 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) + + def test_sent_message_is_seen_by_the_sender(self): + root = self.create_thread_for_user(self.sender, self.recipient) + time.sleep(1.5) + last_visits = LastVisitTime.objects.filter(message=root, user=self.sender) + self.assertEqual(last_visits.count(), 1) + diff --git a/askbot/deps/group_messaging/urls.py b/askbot/deps/group_messaging/urls.py new file mode 100644 index 00000000..19ee35bb --- /dev/null +++ b/askbot/deps/group_messaging/urls.py @@ -0,0 +1,37 @@ +"""url configuration for the group_messaging application""" +from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import url +from group_messaging import views + +urlpatterns = patterns('', + url( + '^threads/$', + views.ThreadsList().as_view(), + name='get_threads' + ), + url( + '^threads/(?P\d+)/$', + views.ThreadDetails().as_view(), + name='thread_details' + ), + url( + '^threads/(?P\d+)/delete-or-restore/$', + views.DeleteOrRestoreThread().as_view(), + name='delete_or_restore_thread' + ), + url( + '^threads/create/$', + views.NewThread().as_view(), + name='create_thread' + ), + url( + '^senders/$', + views.SendersList().as_view(), + name='get_senders' + ), + url( + '^post-reply/$', + views.PostReply().as_view(), + name='post_reply' + ) +) diff --git a/askbot/deps/group_messaging/views.py b/askbot/deps/group_messaging/views.py new file mode 100644 index 00000000..244762d1 --- /dev/null +++ b/askbot/deps/group_messaging/views.py @@ -0,0 +1,286 @@ +"""semi-views for the `group_messaging` application +These are not really views - rather context generator +functions, to be used separately, when needed. + +For example, some other application can call these +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 django.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 +from django.http import HttpResponseForbidden +from django.utils import simplejson +from group_messaging.models import Message +from group_messaging.models import MessageMemo +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 + to be used for pjax use and for generation + of content in the traditional way, where + the only the :method:`get_context` would be used. + """ + template_name = None #used only for the "GET" method + http_method_names = ('GET', 'POST') + + def render_to_response(self, context, template_name=None): + """like a django's shortcut, except will use + template_name from self, if `template_name` is not given. + Also, response is packaged as json with an html fragment + for the pjax consumption + """ + if template_name is None: + template_name = self.template_name + template = get_template(template_name) + html = template.render(context) + json = simplejson.dumps({'html': html, 'success': True}) + return HttpResponse(json, mimetype='application/json') + + + def get(self, request, *args, **kwargs): + """view function for the "GET" method""" + context = self.get_context(request, *args, **kwargs) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + """view function for the "POST" method""" + pass + + def dispatch(self, request, *args, **kwargs): + """checks that the current request method is allowed + and calls the corresponding view function""" + if request.method not in self.http_method_names: + return HttpResponseNotAllowed() + view_func = getattr(self, request.method.lower()) + return view_func(request, *args, **kwargs) + + def get_context(self, request, *args, **kwargs): + """Returns the context dictionary for the "get" + method only""" + return {} + + def as_view(self): + """returns the view function - for the urls.py""" + def view_function(request, *args, **kwargs): + """the actual view function""" + if request.user.is_authenticated() and request.is_ajax(): + view_method = getattr(self, request.method.lower()) + return view_method(request, *args, **kwargs) + else: + return HttpResponseForbidden() + + return view_function + + +class NewThread(InboxView): + """view for creation of new thread""" + http_method_list = ('POST',) + + def post(self, request): + """creates a new thread on behalf of the user + response is blank, because on the client side we just + need to go back to the thread listing view whose + content should be cached in the client' + """ + 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 + + 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, + 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',) + + def post(self, request): + parent_id = IntegerField().clean(request.POST['parent_id']) + parent = Message.objects.get(id=parent_id) + message = Message.objects.create_response( + sender=request.user, + 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( + {'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 = '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 + sender_id = IntegerField().clean(request.REQUEST.get('sender_id', '-1')) + if sender_id == -2: + threads = Message.objects.get_threads( + recipient=request.user, + deleted=True + ) + 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( + recipient=request.user, + sender=sender + ) + + threads = threads.order_by('-last_active_at') + + #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 + + 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 + ) + 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' + + return { + 'threads': threads, + 'threads_count': threads.count(), + 'threads_data': threads_data, + 'sender_id': sender_id + } + + +class DeleteOrRestoreThread(ThreadsList): + """subclassing :class:`ThreadsList`, because deletion + or restoring of thread needs subsequent refreshing + of the threads list""" + + http_method_list = ('POST',) + + def post(self, request, thread_id=None): + """process the post request: + * delete or restore thread + * recalculate the threads list and return it for display + by reusing the threads list "get" function + """ + #part of the threads list context + sender_id = IntegerField().clean(request.POST['sender_id']) + + #a little cryptic, but works - sender_id==-2 means deleted post + if sender_id == -2: + action = 'restore' + else: + action = 'delete' + + thread = Message.objects.get(id=thread_id) + memo, created = MessageMemo.objects.get_or_create( + user=request.user, + message=thread + ) + if action == 'delete': + memo.status = MessageMemo.ARCHIVED + else: + memo.status = MessageMemo.SEEN + memo.save() + + context = self.get_context(request) + return self.render_to_response(context) + + +class SendersList(InboxView): + """shows list of senders for a user""" + template_name = 'group_messaging/senders_list.html' + http_method_names = ('GET',) + + def get_context(self, request): + """get data about senders for the user""" + senders = SenderList.objects.get_senders_for_user(request.user) + senders = senders.values('id', 'username') + return {'senders': senders, 'request_user_id': request.user.id} + + +class ThreadDetails(InboxView): + """shows entire thread in the unfolded form""" + template_name = 'group_messaging/thread_details.html' + http_method_names = ('GET',) + + def get_context(self, request, thread_id=None): + """shows individual thread""" + #todo: assert that current thread is the root + 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} diff --git a/group_messaging/__init__.py b/group_messaging/__init__.py deleted file mode 100644 index ed3d73ff..00000000 --- a/group_messaging/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""`group_messages` is a django application -which allows users send messages to other users -and groups (instances of :class:`django.contrib.auth.models.Group`) - -The same methods are used are used to send messages -to users as to groups - achieved via special "personal groups". - -By convention - personal groups have names formatted as follows: -_personal_, for example for the user whose `id == 1`, -the group should be named `'_personal_1'`. - -Only one person must be a member of a personal group and -each user must have such group. - -TODO: decouple this application -first step is to package send_mail separately -""" diff --git a/group_messaging/migrations/0001_initial.py b/group_messaging/migrations/0001_initial.py deleted file mode 100644 index 7d907dc1..00000000 --- a/group_messaging/migrations/0001_initial.py +++ /dev/null @@ -1,177 +0,0 @@ -# -*- 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 'SenderList' - db.create_table('group_messaging_senderlist', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('recipient', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], unique=True)), - )) - db.send_create_signal('group_messaging', ['SenderList']) - - # Adding M2M table for field senders on 'SenderList' - db.create_table('group_messaging_senderlist_senders', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('senderlist', models.ForeignKey(orm['group_messaging.senderlist'], null=False)), - ('user', models.ForeignKey(orm['auth.user'], null=False)) - )) - db.create_unique('group_messaging_senderlist_senders', ['senderlist_id', 'user_id']) - - # Adding model 'MessageMemo' - db.create_table('group_messaging_messagememo', ( - ('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'])), - ('status', self.gf('django.db.models.fields.SmallIntegerField')(default=0)), - )) - db.send_create_signal('group_messaging', ['MessageMemo']) - - # Adding unique constraint on 'MessageMemo', fields ['user', 'message'] - db.create_unique('group_messaging_messagememo', ['user_id', 'message_id']) - - # Adding model 'Message' - db.create_table('group_messaging_message', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('message_type', self.gf('django.db.models.fields.SmallIntegerField')(default=0)), - ('sender', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sent_messages', to=orm['auth.User'])), - ('root', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='descendants', null=True, to=orm['group_messaging.Message'])), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['group_messaging.Message'])), - ('headline', self.gf('django.db.models.fields.CharField')(max_length=80)), - ('text', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), - ('html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), - ('sent_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('last_active_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('active_until', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - )) - db.send_create_signal('group_messaging', ['Message']) - - # Adding M2M table for field recipients on 'Message' - db.create_table('group_messaging_message_recipients', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('message', models.ForeignKey(orm['group_messaging.message'], null=False)), - ('group', models.ForeignKey(orm['auth.group'], null=False)) - )) - db.create_unique('group_messaging_message_recipients', ['message_id', 'group_id']) - - def backwards(self, orm): - # Removing unique constraint on 'MessageMemo', fields ['user', 'message'] - db.delete_unique('group_messaging_messagememo', ['user_id', 'message_id']) - - # Deleting model 'SenderList' - db.delete_table('group_messaging_senderlist') - - # Removing M2M table for field senders on 'SenderList' - db.delete_table('group_messaging_senderlist_senders') - - # Deleting model 'MessageMemo' - db.delete_table('group_messaging_messagememo') - - # Deleting model 'Message' - db.delete_table('group_messaging_message') - - # Removing M2M table for field recipients on 'Message' - db.delete_table('group_messaging_message_recipients') - - 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.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']"}), - '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/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 deleted file mode 100644 index 5e92ef2b..00000000 --- a/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- 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/migrations/__init__.py b/group_messaging/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/group_messaging/models.py b/group_messaging/models.py deleted file mode 100644 index 5d43baf8..00000000 --- a/group_messaging/models.py +++ /dev/null @@ -1,438 +0,0 @@ -"""models for the ``group_messaging`` app -""" -import copy -import datetime -import urllib -from askbot.mail import send_mail #todo: remove dependency? -from django.template.loader import get_template -from django.db import models -from django.db.models import signals -from django.conf import settings as django_settings -from django.contrib.auth.models import Group -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.utils.importlib import import_module -from django.utils.translation import ugettext as _ - -MAX_HEADLINE_LENGTH = 80 -MAX_SENDERS_INFO_LENGTH = 64 -MAX_SUBJECT_LINE_LENGTH = 30 - -#dummy parse message function -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) - - -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): - """returns personal group for the user""" - return get_personal_group_by_user_id(user.id) - - -def create_personal_group(user): - """creates a personal group for the user""" - 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`""" - - def get_senders_for_user(self, user=None): - """returns query set of :class:`User`""" - user_groups = user.groups.all() - lists = self.filter(recipient__in=user_groups) - user_ids = lists.values_list( - 'senders__id', flat=True - ).distinct() - return User.objects.filter(id__in=user_ids) - -class SenderList(models.Model): - """a model to store denormalized data - about who sends messages to any given person - sender list is populated automatically - as new messages are created - """ - recipient = models.ForeignKey(Group, unique=True) - senders = models.ManyToManyField(User) - objects = SenderListManager() - - -class MessageMemo(models.Model): - """A bridge between message recipients and messages - these records are only created when user sees a message. - The idea is that using groups as recipients, we can send - messages to massive numbers of users, without cluttering - the database. - - Instead we'll be creating a "seen" message after user - reads the message. - """ - SEEN = 0 - ARCHIVED = 1 - STATUS_CHOICES = ( - (SEEN, 'seen'), - (ARCHIVED, 'archived') - ) - user = models.ForeignKey(User) - message = models.ForeignKey('Message', related_name='memos') - status = models.SmallIntegerField( - choices=STATUS_CHOICES, default=SEEN - ) - - class Meta: - unique_together = ('user', 'message') - - -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): - """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: - filter = filter & models.Q(sender=sender) - - if deleted: - deleted_filter = models.Q( - memos__status=MessageMemo.ARCHIVED, - memos__user=recipient - ) - return self.filter(filter & deleted_filter) - else: - #rather a tricky query (may need to change the idea to get rid of this) - #select threads that have a memo for the user, but the memo is not ARCHIVED - #in addition, select threads that have zero memos for the user - marked_as_non_deleted_filter = models.Q( - memos__status=MessageMemo.SEEN, - memos__user=recipient - ) - #part1 - marked as non-archived - part1 = self.filter(filter & marked_as_non_deleted_filter) - #part2 - messages for the user without an attached memo - part2 = self.filter(filter & ~models.Q(memos__user=recipient)) - return (part1 | part2).distinct() - - def create(self, **kwargs): - """creates a message""" - root = kwargs.get('root', None) - if root is None: - parent = kwargs.get('parent', None) - if parent: - if parent.root: - root = parent.root - else: - root = parent - kwargs['root'] = root - - headline = kwargs.get('headline', kwargs['text']) - kwargs['headline'] = headline[:MAX_HEADLINE_LENGTH] - kwargs['html'] = parse_message(kwargs['text']) - - 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, - ) - now = datetime.datetime.now() - LastVisitTime.objects.create(message=message, user=sender, at=now) - 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 - - def create_response(self, sender=None, text=None, parent=None): - message = self.create( - parent=parent, - message_type=Message.STORED, - sender=sender, - text=text, - ) - #recipients are parent's recipients + sender - #creator of response gets memo in the "read" status - recipients = set(parent.recipients.all()) - - 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 - #update headline - message.root.headline = text[:MAX_HEADLINE_LENGTH] - #mark last active timestamp for the root message - message.root.last_active_at = datetime.datetime.now() - #update senders info - stuff that is shown in the thread heading - message.root.update_senders_info() - #unarchive the thread for all recipients - message.root.unarchive() - message.send_email_alert() - return message - - -class Message(models.Model): - """the message model allowing users to send - messages to other users and groups, via - personal groups. - """ - STORED = 0 - TEMPORARY = 1 - ONE_TIME = 2 - MESSAGE_TYPE_CHOICES = ( - (STORED, 'email-like message, stored in the inbox'), - (ONE_TIME, 'will be shown just once'), - (TEMPORARY, 'will be shown until certain time') - ) - - message_type = models.SmallIntegerField( - choices=MESSAGE_TYPE_CHOICES, - default=STORED, - ) - - 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_HEADLINE_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_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 - todo: sender lists may be updated in a lazy way - per user - """ - 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 get_absolute_url(self, user=None): - """returns absolute url to the thread""" - assert(user != None) - settings = django_settings.GROUP_MESSAGING - func_path = settings['base_url_getter_function'] - path_bits = func_path.split('.') - url_getter = getattr( - import_module('.'.join(path_bits[:-1])), - path_bits[-1] - ) - params = copy.copy(settings['base_url_params']) - params['thread_id'] = self.id - url = url_getter(user) + '?' + urllib.urlencode(params) - #if include_domain_name: #don't need this b/c - # site = Site.objects.get_current() - # url = 'http://' + site.domain + url - return url - - def get_email_subject_line(self): - """forms subject line based on the root message - and prepends 'Re': if message is non-root - """ - subject = self.get_root_message().text[:MAX_SUBJECT_LINE_LENGTH] - if self.root: - subject = _('Re: ') + subject - return subject - - def get_root_message(self): - """returns root message or self - if current message is root - """ - return self.root or self - - def get_recipients_users(self): - """returns query set of users""" - groups = self.recipients.all() - return User.objects.filter( - groups__in=groups - ).exclude( - id=self.sender.id - ).distinct() - - def get_timeline(self): - """returns ordered query set of messages in the thread - with the newest first""" - root = self.get_root_message() - root_qs = Message.objects.filter(id=root.id) - return (root.descendants.all() | root_qs).order_by('-sent_at') - - - def send_email_alert(self): - """signal handler for the message post-save""" - root_message = self.get_root_message() - data = {'messages': self.get_timeline()} - template = get_template('group_messaging/email_alert.html') - body_text = template.render(data) - subject = self.get_email_subject_line() - for user in self.get_recipients_users(): - #todo change url scheme so that all users have the same - #urls within their personal areas of the user profile - #so that we don't need to have loops like this one - thread_url = root_message.get_absolute_url(user) - thread_url = thread_url.replace('&', '&') - #in the template we have a placeholder to be replaced like this: - body_text = body_text.replace('THREAD_URL_HOLE', thread_url) - send_mail( - subject, - body_text, - django_settings.DEFAULT_FROM_EMAIL, - [user.email,], - ) - - - def update_senders_info(self): - """update the contributors info, - meant to be used on a root message only - """ - senders_names = self.senders_info.split(',') - - if self.sender.username in senders_names: - senders_names.remove(self.sender.username) - senders_names.insert(0, self.sender.username) - - self.senders_info = (','.join(senders_names))[:64] - self.save() - - def unarchive(self, user=None): - """unarchive message for all recipients""" - 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 deleted file mode 100644 index bcf764db..00000000 --- a/group_messaging/tests.py +++ /dev/null @@ -1,363 +0,0 @@ -import datetime -import time -import urlparse -from bs4 import BeautifulSoup -from django.test import TestCase -from django.contrib.auth.models import User, Group -from group_messaging.models import Message -from group_messaging.models import MessageMemo -from group_messaging.models import SenderList -from group_messaging.models import LastVisitTime -from group_messaging.models import get_personal_group -from group_messaging.models import create_personal_group -from group_messaging.views import ThreadsList -from mock import Mock - -MESSAGE_TEXT = 'test message text' - -def create_user(name): - """creates a user and a personal group, - returns the created user""" - user = User.objects.create_user(name, name + '@example.com') - #note that askbot will take care of three lines below automatically - try: - group = get_personal_group(user) - except Group.DoesNotExist: - group = create_personal_group(user) - group_name = '_personal_%d' % user.id - group, created = Group.objects.get_or_create(name=group_name) - user.groups.add(group) - return user - -def get_html_message(mail_message): - """mail message is an item from the django.core.mail.outbox""" - return mail_message.alternatives[0][0] - -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, sender, recipient_groups): - return Message.objects.create_thread( - sender=sender, recipients=recipient_groups, - text=MESSAGE_TEXT - ) - - 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'] - assert(method in ('GET', 'POST')) - spec.append(method) - request = Mock(spec=spec) - request.REQUEST = data - setattr(request, method, data) - request.user = user - return view_class().get_context(request) - - 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: todo - make a method - last_visit_time, created = LastVisitTime.objects.get_or_create( - user=self.sender, - message=root - ) - last_visit_time.at = datetime.datetime.now() - last_visit_time.save() - 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 - ) - - 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_emailed_message_url_works_for_post_recipient(self): - root = self.create_thread_for_user(self.sender, self.recipient) - from django.core.mail import outbox - html_message = get_html_message(outbox[0]) - link = BeautifulSoup(html_message).find('a', attrs={'class': 'thread-link'}) - url = link['href'].replace('&', '&') - parsed_url = urlparse.urlparse(url) - url_data = urlparse.parse_qsl(parsed_url.query) - self.client.login(user_id=self.recipient.id, method='force') - response = self.client.get(parsed_url.path, url_data) - dom = BeautifulSoup(response.content) - threads = dom.find_all('ul', attrs={'class': 'thread'}) - self.assertEquals(len(threads), 1) - thread_lists = dom.find_all('table', attrs={'class': 'threads-list'}) - self.assertEquals(len(thread_lists), 0) - - def test_sent_thread_is_visited_by_sender(self): - root = self.create_thread_for_user(self.sender, self.recipient) - context = self.get_view_context( - ThreadsList, - data={'sender_id': str(self.sender.id)}, - user=self.sender - ) - thread_data = context['threads_data'][root.id] - self.assertEqual(thread_data['status'], 'seen') - -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.sender, self.recipient) - #message type is stored - self.assertEqual(message.message_type, Message.STORED) - #recipient is in the list of recipients - recipients = set(message.recipients.all()) - recipient_group = get_personal_group(self.recipient) - #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 - #) - #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 - member of the group has updated the sender list""" - group = Group.objects.create(name='somegroup') - self.recipient.groups.add(group) - message = self.create_thread(self.sender, [group]) - senders = SenderList.objects.get_senders_for_user(self.recipient) - self.assertEqual(set(senders), set([self.sender])) - - def test_create_thread_response(self): - """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.sender, self.recipient) - response = Message.objects.create_response( - sender=self.recipient, - text='some response', - 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()) - sender_group = get_personal_group(self.sender) - recipient_group = get_personal_group(self.recipient) - expected_recipients = set([sender_group, recipient_group]) - self.assertEqual(recipients, expected_recipients) - - def test_get_threads(self): - 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)) - self.assertEqual(threads, set([root_message])) - - response = Message.objects.create_response( - sender=self.recipient, - text='some response', - parent=root_message - ) - threads = set(Message.objects.get_threads(recipient=self.sender)) - self.assertEqual(threads, set([root_message])) - threads = set(Message.objects.get_threads(recipient=self.recipient)) - self.assertEqual(threads, set([root_message])) - - def test_deleting_thread_is_user_specific(self): - """when one user deletes thread, that same thread - should not end up deleted by another user - """ - root, response, response2 = self.setup_three_message_thread() - - threads = Message.objects.get_threads(recipient=self.sender) - self.assertEquals(threads.count(), 1) - threads = Message.objects.get_threads(recipient=self.recipient) - self.assertEquals(threads.count(), 1) - - memo1, created = MessageMemo.objects.get_or_create( - message=root, - user=self.recipient, - status=MessageMemo.ARCHIVED - ) - - threads = Message.objects.get_threads(recipient=self.sender) - self.assertEquals(threads.count(), 1) - threads = Message.objects.get_threads(recipient=self.recipient) - self.assertEquals(threads.count(), 0) - threads = Message.objects.get_threads( - recipient=self.recipient, deleted=True - ) - self.assertEquals(threads.count(), 1) - - def test_user_specific_inboxes(self): - self.create_thread_for_user(self.sender, self.recipient) - - threads = Message.objects.get_threads( - recipient=self.recipient, sender=self.sender - ) - self.assertEqual(threads.count(), 1) - threads = Message.objects.get_threads( - recipient=self.sender, sender=self.recipient - ) - self.assertEqual(threads.count(), 0) - - def test_response_updates_thread_headline(self): - root = self.create_thread_for_user(self.sender, self.recipient) - response = Message.objects.create_response( - sender=self.recipient, - text='some response', - parent=root - ) - self.assertEqual(root.headline, 'some response') - - def test_email_alert_sent(self): - 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) - html_message = get_html_message(outbox[0]) - self.assertTrue(root.text in html_message) - soup = BeautifulSoup(html_message) - links = soup.find_all('a', attrs={'class': 'thread-link'}) - self.assertEqual(len(links), 1) - parse_result = urlparse.urlparse(links[0]['href']) - query = urlparse.parse_qs(parse_result.query.replace('&', '&')) - self.assertEqual(query['thread_id'][0], str(root.id)) - - 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) - - def test_sent_message_is_seen_by_the_sender(self): - root = self.create_thread_for_user(self.sender, self.recipient) - time.sleep(1.5) - last_visits = LastVisitTime.objects.filter(message=root, user=self.sender) - self.assertEqual(last_visits.count(), 1) - diff --git a/group_messaging/urls.py b/group_messaging/urls.py deleted file mode 100644 index 19ee35bb..00000000 --- a/group_messaging/urls.py +++ /dev/null @@ -1,37 +0,0 @@ -"""url configuration for the group_messaging application""" -from django.conf.urls.defaults import patterns -from django.conf.urls.defaults import url -from group_messaging import views - -urlpatterns = patterns('', - url( - '^threads/$', - views.ThreadsList().as_view(), - name='get_threads' - ), - url( - '^threads/(?P\d+)/$', - views.ThreadDetails().as_view(), - name='thread_details' - ), - url( - '^threads/(?P\d+)/delete-or-restore/$', - views.DeleteOrRestoreThread().as_view(), - name='delete_or_restore_thread' - ), - url( - '^threads/create/$', - views.NewThread().as_view(), - name='create_thread' - ), - url( - '^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 deleted file mode 100644 index 244762d1..00000000 --- a/group_messaging/views.py +++ /dev/null @@ -1,286 +0,0 @@ -"""semi-views for the `group_messaging` application -These are not really views - rather context generator -functions, to be used separately, when needed. - -For example, some other application can call these -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 django.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 -from django.http import HttpResponseForbidden -from django.utils import simplejson -from group_messaging.models import Message -from group_messaging.models import MessageMemo -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 - to be used for pjax use and for generation - of content in the traditional way, where - the only the :method:`get_context` would be used. - """ - template_name = None #used only for the "GET" method - http_method_names = ('GET', 'POST') - - def render_to_response(self, context, template_name=None): - """like a django's shortcut, except will use - template_name from self, if `template_name` is not given. - Also, response is packaged as json with an html fragment - for the pjax consumption - """ - if template_name is None: - template_name = self.template_name - template = get_template(template_name) - html = template.render(context) - json = simplejson.dumps({'html': html, 'success': True}) - return HttpResponse(json, mimetype='application/json') - - - def get(self, request, *args, **kwargs): - """view function for the "GET" method""" - context = self.get_context(request, *args, **kwargs) - return self.render_to_response(context) - - def post(self, request, *args, **kwargs): - """view function for the "POST" method""" - pass - - def dispatch(self, request, *args, **kwargs): - """checks that the current request method is allowed - and calls the corresponding view function""" - if request.method not in self.http_method_names: - return HttpResponseNotAllowed() - view_func = getattr(self, request.method.lower()) - return view_func(request, *args, **kwargs) - - def get_context(self, request, *args, **kwargs): - """Returns the context dictionary for the "get" - method only""" - return {} - - def as_view(self): - """returns the view function - for the urls.py""" - def view_function(request, *args, **kwargs): - """the actual view function""" - if request.user.is_authenticated() and request.is_ajax(): - view_method = getattr(self, request.method.lower()) - return view_method(request, *args, **kwargs) - else: - return HttpResponseForbidden() - - return view_function - - -class NewThread(InboxView): - """view for creation of new thread""" - http_method_list = ('POST',) - - def post(self, request): - """creates a new thread on behalf of the user - response is blank, because on the client side we just - need to go back to the thread listing view whose - content should be cached in the client' - """ - 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 - - 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, - 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',) - - def post(self, request): - parent_id = IntegerField().clean(request.POST['parent_id']) - parent = Message.objects.get(id=parent_id) - message = Message.objects.create_response( - sender=request.user, - 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( - {'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 = '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 - sender_id = IntegerField().clean(request.REQUEST.get('sender_id', '-1')) - if sender_id == -2: - threads = Message.objects.get_threads( - recipient=request.user, - deleted=True - ) - 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( - recipient=request.user, - sender=sender - ) - - threads = threads.order_by('-last_active_at') - - #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 - - 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 - ) - 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' - - return { - 'threads': threads, - 'threads_count': threads.count(), - 'threads_data': threads_data, - 'sender_id': sender_id - } - - -class DeleteOrRestoreThread(ThreadsList): - """subclassing :class:`ThreadsList`, because deletion - or restoring of thread needs subsequent refreshing - of the threads list""" - - http_method_list = ('POST',) - - def post(self, request, thread_id=None): - """process the post request: - * delete or restore thread - * recalculate the threads list and return it for display - by reusing the threads list "get" function - """ - #part of the threads list context - sender_id = IntegerField().clean(request.POST['sender_id']) - - #a little cryptic, but works - sender_id==-2 means deleted post - if sender_id == -2: - action = 'restore' - else: - action = 'delete' - - thread = Message.objects.get(id=thread_id) - memo, created = MessageMemo.objects.get_or_create( - user=request.user, - message=thread - ) - if action == 'delete': - memo.status = MessageMemo.ARCHIVED - else: - memo.status = MessageMemo.SEEN - memo.save() - - context = self.get_context(request) - return self.render_to_response(context) - - -class SendersList(InboxView): - """shows list of senders for a user""" - template_name = 'group_messaging/senders_list.html' - http_method_names = ('GET',) - - def get_context(self, request): - """get data about senders for the user""" - senders = SenderList.objects.get_senders_for_user(request.user) - senders = senders.values('id', 'username') - return {'senders': senders, 'request_user_id': request.user.id} - - -class ThreadDetails(InboxView): - """shows entire thread in the unfolded form""" - template_name = 'group_messaging/thread_details.html' - http_method_names = ('GET',) - - def get_context(self, request, thread_id=None): - """shows individual thread""" - #todo: assert that current thread is the root - 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} -- cgit v1.2.3-1-g7c22