From 273396a1887743e4adc8f7b5c13c62d2f35be15b Mon Sep 17 00:00:00 2001 From: Luis Montiel Date: Mon, 9 Jul 2012 12:20:44 -0500 Subject: [PATCH 1/2] added locked attr to NoticeType object, preventing user from disabling/enabling the notification --- notification/admin.py | 2 +- notification/migrations/0001_initial.py | 171 ++++++++++++++++++ .../0002_auto__add_field_noticetype_locked.py | 104 +++++++++++ notification/migrations/__init__.py | 0 notification/models.py | 102 ++++++----- notification/views.py | 50 ++--- 6 files changed, 354 insertions(+), 75 deletions(-) create mode 100644 notification/migrations/0001_initial.py create mode 100644 notification/migrations/0002_auto__add_field_noticetype_locked.py create mode 100644 notification/migrations/__init__.py diff --git a/notification/admin.py b/notification/admin.py index e754e876..6a0f249b 100644 --- a/notification/admin.py +++ b/notification/admin.py @@ -4,7 +4,7 @@ class NoticeTypeAdmin(admin.ModelAdmin): - list_display = ["label", "display", "description", "default"] + list_display = ["label", "display", "description", "default", "locked"] class NoticeSettingAdmin(admin.ModelAdmin): diff --git a/notification/migrations/0001_initial.py b/notification/migrations/0001_initial.py new file mode 100644 index 00000000..ba0aee4c --- /dev/null +++ b/notification/migrations/0001_initial.py @@ -0,0 +1,171 @@ +# encoding: 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 'NoticeType' + db.create_table('notification_noticetype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('label', self.gf('django.db.models.fields.CharField')(max_length=40)), + ('display', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('default', self.gf('django.db.models.fields.IntegerField')()), + )) + db.send_create_signal('notification', ['NoticeType']) + + # Adding model 'NoticeSetting' + db.create_table('notification_noticesetting', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('notice_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['notification.NoticeType'])), + ('medium', self.gf('django.db.models.fields.CharField')(max_length=1)), + ('send', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('notification', ['NoticeSetting']) + + # Adding unique constraint on 'NoticeSetting', fields ['user', 'notice_type', 'medium'] + db.create_unique('notification_noticesetting', ['user_id', 'notice_type_id', 'medium']) + + # Adding model 'Notice' + db.create_table('notification_notice', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('recipient', self.gf('django.db.models.fields.related.ForeignKey')(related_name='recieved_notices', to=orm['auth.User'])), + ('sender', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sent_notices', null=True, to=orm['auth.User'])), + ('message', self.gf('django.db.models.fields.TextField')()), + ('notice_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['notification.NoticeType'])), + ('added', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('unseen', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('archived', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('on_site', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('notification', ['Notice']) + + # Adding model 'NoticeQueueBatch' + db.create_table('notification_noticequeuebatch', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('pickled_data', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('notification', ['NoticeQueueBatch']) + + # Adding model 'ObservedItem' + db.create_table('notification_observeditem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + ('notice_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['notification.NoticeType'])), + ('added', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('signal', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('notification', ['ObservedItem']) + + + def backwards(self, orm): + + # Removing unique constraint on 'NoticeSetting', fields ['user', 'notice_type', 'medium'] + db.delete_unique('notification_noticesetting', ['user_id', 'notice_type_id', 'medium']) + + # Deleting model 'NoticeType' + db.delete_table('notification_noticetype') + + # Deleting model 'NoticeSetting' + db.delete_table('notification_noticesetting') + + # Deleting model 'Notice' + db.delete_table('notification_notice') + + # Deleting model 'NoticeQueueBatch' + db.delete_table('notification_noticequeuebatch') + + # Deleting model 'ObservedItem' + db.delete_table('notification_observeditem') + + + 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'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + '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': '30'}) + }, + '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'}) + }, + 'notification.notice': { + 'Meta': {'ordering': "['-added']", 'object_name': 'Notice'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'on_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recieved_notices'", 'to': "orm['auth.User']"}), + 'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_notices'", 'null': 'True', 'to': "orm['auth.User']"}), + 'unseen': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'notification.noticequeuebatch': { + 'Meta': {'object_name': 'NoticeQueueBatch'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pickled_data': ('django.db.models.fields.TextField', [], {}) + }, + 'notification.noticesetting': { + 'Meta': {'unique_together': "(('user', 'notice_type', 'medium'),)", 'object_name': 'NoticeSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'medium': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'send': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'notification.noticetype': { + 'Meta': {'object_name': 'NoticeType'}, + 'default': ('django.db.models.fields.IntegerField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'display': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '40'}) + }, + 'notification.observeditem': { + 'Meta': {'ordering': "['-added']", 'object_name': 'ObservedItem'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'signal': ('django.db.models.fields.TextField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['notification'] diff --git a/notification/migrations/0002_auto__add_field_noticetype_locked.py b/notification/migrations/0002_auto__add_field_noticetype_locked.py new file mode 100644 index 00000000..e475f032 --- /dev/null +++ b/notification/migrations/0002_auto__add_field_noticetype_locked.py @@ -0,0 +1,104 @@ +# encoding: 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 field 'NoticeType.locked' + db.add_column('notification_noticetype', 'locked', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'NoticeType.locked' + db.delete_column('notification_noticetype', 'locked') + + + 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'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + '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': '30'}) + }, + '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'}) + }, + 'notification.notice': { + 'Meta': {'ordering': "['-added']", 'object_name': 'Notice'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'on_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recieved_notices'", 'to': "orm['auth.User']"}), + 'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_notices'", 'null': 'True', 'to': "orm['auth.User']"}), + 'unseen': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'notification.noticequeuebatch': { + 'Meta': {'object_name': 'NoticeQueueBatch'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pickled_data': ('django.db.models.fields.TextField', [], {}) + }, + 'notification.noticesetting': { + 'Meta': {'unique_together': "(('user', 'notice_type', 'medium'),)", 'object_name': 'NoticeSetting'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'medium': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'send': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'notification.noticetype': { + 'Meta': {'object_name': 'NoticeType'}, + 'default': ('django.db.models.fields.IntegerField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'display': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'notification.observeditem': { + 'Meta': {'ordering': "['-added']", 'object_name': 'ObservedItem'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'notice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['notification.NoticeType']"}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'signal': ('django.db.models.fields.TextField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['notification'] diff --git a/notification/migrations/__init__.py b/notification/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/notification/models.py b/notification/models.py index 8d8b3abe..ce848aed 100644 --- a/notification/models.py +++ b/notification/models.py @@ -28,18 +28,21 @@ class LanguageStoreNotAvailable(Exception): pass + class NoticeType(models.Model): - label = models.CharField(_("label"), max_length=40) display = models.CharField(_("display"), max_length=50) description = models.CharField(_("description"), max_length=100) - + # by default only on for media with sensitivity less than or equal to this number default = models.IntegerField(_("default")) - + locked = models.BooleanField(_("locked"), + default=False, + help_text=_("If the notice type is locked, the user wont be able to enable/disable this notification")) + def __unicode__(self): return self.label - + class Meta: verbose_name = _("notice type") verbose_name_plural = _("notice types") @@ -52,20 +55,21 @@ class Meta: # how spam-sensitive is the medium NOTICE_MEDIA_DEFAULTS = { - "1": 2 # email + "1": 2 # email } + class NoticeSetting(models.Model): """ Indicates, for a given user, whether to send notifications of a given type to a given medium. """ - + user = models.ForeignKey(User, verbose_name=_("user")) notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type")) medium = models.CharField(_("medium"), max_length=1, choices=NOTICE_MEDIA) send = models.BooleanField(_("send")) - + class Meta: verbose_name = _("notice setting") verbose_name_plural = _("notice settings") @@ -87,14 +91,14 @@ def should_send(user, notice_type, medium): class NoticeManager(models.Manager): - + def notices_for(self, user, archived=False, unseen=None, on_site=None, sent=False): """ returns Notice objects for the given user. - + If archived=False, it only include notices not archived. If archived=True, it returns all notices for that user. - + If unseen=None, it includes all notices. If unseen=True, return only unseen notices. If unseen=False, return only seen notices. @@ -111,21 +115,21 @@ def notices_for(self, user, archived=False, unseen=None, on_site=None, sent=Fals if on_site is not None: qs = qs.filter(on_site=on_site) return qs - + def unseen_count_for(self, recipient, **kwargs): """ returns the number of unseen notices for the given user but does not mark them seen """ return self.notices_for(recipient, unseen=True, **kwargs).count() - + def received(self, recipient, **kwargs): """ returns notices the given recipient has recieved. """ kwargs["sent"] = False return self.notices_for(recipient, **kwargs) - + def sent(self, sender, **kwargs): """ returns notices the given sender has sent @@ -135,7 +139,7 @@ def sent(self, sender, **kwargs): class Notice(models.Model): - + recipient = models.ForeignKey(User, related_name="recieved_notices", verbose_name=_("recipient")) sender = models.ForeignKey(User, null=True, related_name="sent_notices", verbose_name=_("sender")) message = models.TextField(_("message")) @@ -144,20 +148,20 @@ class Notice(models.Model): unseen = models.BooleanField(_("unseen"), default=True) archived = models.BooleanField(_("archived"), default=False) on_site = models.BooleanField(_("on site")) - + objects = NoticeManager() - + def __unicode__(self): return self.message - + def archive(self): self.archived = True self.save() - + def is_unseen(self): """ returns value of self.unseen but also changes it to false. - + Use this in a template to mark an unseen notice differently the first time it is shown. """ @@ -166,12 +170,12 @@ def is_unseen(self): self.unseen = False self.save() return unseen - + class Meta: ordering = ["-added"] verbose_name = _("notice") verbose_name_plural = _("notices") - + def get_absolute_url(self): return reverse("notification_notice", args=[str(self.pk)]) @@ -187,7 +191,7 @@ class NoticeQueueBatch(models.Model): def create_notice_type(label, display, description, default=2, verbosity=1): """ Creates a new NoticeType. - + This is intended to be used by other apps as a post_syncdb manangement step. """ try: @@ -251,40 +255,40 @@ def get_formatted_messages(formats, label, context): def send_now(users, label, extra_context=None, on_site=True, sender=None): """ Creates a new notice. - + This is intended to be how other apps create new notices. - + notification.send(user, "friends_invite_sent", { "spam": "eggs", "foo": "bar", ) - + You can pass in on_site=False to prevent the notice emitted from being displayed on the site. """ if extra_context is None: extra_context = {} - + notice_type = NoticeType.objects.get(label=label) - + protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") current_site = Site.objects.get_current() - + notices_url = u"%s://%s%s" % ( protocol, unicode(current_site), reverse("notification_notices"), ) - + current_language = get_language() - + formats = ( "short.txt", "full.txt", "notice.html", "full.html", ) # TODO make formats configurable - + for user in users: recipients = [] # get user language for user from language store defined in @@ -293,11 +297,11 @@ def send_now(users, label, extra_context=None, on_site=True, sender=None): language = get_notification_language(user) except LanguageStoreNotAvailable: language = None - + if language is not None: # activate the user's language activate(language) - + # update context with user specific translations context = Context({ "recipient": user, @@ -307,25 +311,25 @@ def send_now(users, label, extra_context=None, on_site=True, sender=None): "current_site": current_site, }) context.update(extra_context) - + # get prerendered format messages messages = get_formatted_messages(formats, label, context) - + # Strip newlines from subject subject = "".join(render_to_string("notification/email_subject.txt", { "message": messages["short.txt"], }, context).splitlines()) - + body = render_to_string("notification/email_body.txt", { "message": messages["full.txt"], }, context) - + notice = Notice.objects.create(recipient=user, message=messages["notice.html"], notice_type=notice_type, on_site=on_site, sender=sender) if should_send(user, notice_type, "1") and user.email and user.is_active: # Email recipients.append(user.email) send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients) - + # reset environment to original language activate(current_language) @@ -370,7 +374,7 @@ def queue(users, label, extra_context=None, on_site=True, sender=None): class ObservedItemManager(models.Manager): - + def all_for(self, observed, signal): """ Returns all ObservedItems for an observed object, @@ -379,7 +383,7 @@ def all_for(self, observed, signal): content_type = ContentType.objects.get_for_model(observed) observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal) return observed_items - + def get_for(self, observed, observer, signal): content_type = ContentType.objects.get_for_model(observed) observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal) @@ -387,27 +391,27 @@ def get_for(self, observed, observer, signal): class ObservedItem(models.Model): - + user = models.ForeignKey(User, verbose_name=_("user")) - + content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() observed_object = generic.GenericForeignKey("content_type", "object_id") - + notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type")) - + added = models.DateTimeField(_("added"), default=datetime.datetime.now) - + # the signal that will be listened to send the notice signal = models.TextField(verbose_name=_("signal")) - + objects = ObservedItemManager() - + class Meta: ordering = ["-added"] verbose_name = _("observed item") verbose_name_plural = _("observed items") - + def send_notice(self, extra_context=None): if extra_context is None: extra_context = {} @@ -418,7 +422,7 @@ def send_notice(self, extra_context=None): def observe(observed, observer, notice_type_label, signal="post_save"): """ Create a new ObservedItem. - + To be used by applications to register a user as an observer for some object. """ notice_type = NoticeType.objects.get(label=notice_type_label) diff --git a/notification/views.py b/notification/views.py index e60b4483..239c18d1 100644 --- a/notification/views.py +++ b/notification/views.py @@ -26,17 +26,17 @@ def feed_for_user(request): def notices(request): """ The main notices index view. - + Template: :template:`notification/notices.html` - + Context: - + notices A list of :model:`notification.Notice` objects that are not archived and to be displayed on the site. """ notices = Notice.objects.notices_for(request.user, on_site=True) - + return render_to_response("notification/notices.html", { "notices": notices, }, context_instance=RequestContext(request)) @@ -46,14 +46,14 @@ def notices(request): def notice_settings(request): """ The notice settings view. - + Template: :template:`notification/notice_settings.html` - + Context: - + notice_types A list of all :model:`notification.NoticeType` objects. - + notice_settings A dictionary containing ``column_headers`` for each ``NOTICE_MEDIA`` and ``rows`` containing a list of dictionaries: ``notice_type``, a @@ -62,7 +62,7 @@ def notice_settings(request): value is ``True`` or ``False`` depending on a ``request.POST`` variable called ``form_label``, whose valid value is ``on``. """ - notice_types = NoticeType.objects.all() + notice_types = NoticeType.objects.filter(locked=False) settings_table = [] for notice_type in notice_types: settings_row = [] @@ -80,16 +80,16 @@ def notice_settings(request): setting.save() settings_row.append((form_label, setting.send)) settings_table.append({"notice_type": notice_type, "cells": settings_row}) - + if request.method == "POST": next_page = request.POST.get("next_page", ".") return HttpResponseRedirect(next_page) - + notice_settings = { "column_headers": [medium_display for medium_id, medium_display in NOTICE_MEDIA], "rows": settings_table, } - + return render_to_response("notification/notice_settings.html", { "notice_types": notice_types, "notice_settings": notice_settings, @@ -100,16 +100,16 @@ def notice_settings(request): def single(request, id, mark_seen=True): """ Detail view for a single :model:`notification.Notice`. - + Template: :template:`notification/single.html` - + Context: - + notice The :model:`notification.Notice` being viewed - + Optional arguments: - + mark_seen If ``True``, mark the notice as seen if it isn't already. Do nothing if ``False``. Default: ``True``. @@ -131,12 +131,12 @@ def archive(request, noticeid=None, next_page=None): Archive a :model:`notices.Notice` if the requesting user is the recipient or if the user is a superuser. Returns a ``HttpResponseRedirect`` when complete. - + Optional arguments: - + noticeid The ID of the :model:`notices.Notice` to be archived. - + next_page The page to redirect to when done. """ @@ -159,12 +159,12 @@ def delete(request, noticeid=None, next_page=None): Delete a :model:`notices.Notice` if the requesting user is the recipient or if the user is a superuser. Returns a ``HttpResponseRedirect`` when complete. - + Optional arguments: - + noticeid The ID of the :model:`notices.Notice` to be archived. - + next_page The page to redirect to when done. """ @@ -185,9 +185,9 @@ def delete(request, noticeid=None, next_page=None): def mark_all_seen(request): """ Mark all unseen notices for the requesting user as seen. Returns a - ``HttpResponseRedirect`` when complete. + ``HttpResponseRedirect`` when complete. """ - + for notice in Notice.objects.notices_for(request.user, unseen=True): notice.unseen = False notice.save() From b75a36f78fc590f6eaaafd79668e6d36b79c808a Mon Sep 17 00:00:00 2001 From: Luis Montiel Date: Sat, 24 Nov 2012 14:29:13 -0600 Subject: [PATCH 2/2] Fix to prevent re-sending notification to users when sending batch fails --- notification/engine.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/notification/engine.py b/notification/engine.py index d0f4c9f7..81d6db6a 100644 --- a/notification/engine.py +++ b/notification/engine.py @@ -25,7 +25,7 @@ def send_all(): lock = FileLock("send_notices") - + logging.debug("acquiring lock...") try: lock.acquire(LOCK_WAIT_TIMEOUT) @@ -36,10 +36,10 @@ def send_all(): logging.debug("waiting for the lock timed out. quitting.") return logging.debug("acquired.") - - batches, sent = 0, 0 + + batches, sent, tried = 0, 0, 0 start_time = time.time() - + try: # nesting the try statement to be Python 2.4 try: @@ -52,12 +52,22 @@ def send_all(): # call this once per user to be atomic and allow for logging to # accurately show how long each takes. notification.send_now([user], label, extra_context, on_site, sender) + tried = tried + 1 except User.DoesNotExist: + tried = tried + 1 # Ignore deleted users, just warn about them logging.warning("not emitting notice %s to user %s since it does not exist" % (label, user)) + except Exception as e: + logging.critical("Error while sending email to user: %s. Notification: %s" % (user, label)) + logging.critical("Exception: %s" % e.value) + logging.critical("Updating NoticeQueueBatch object to avoid re-sending to same users...") + queued_batch.pickled_data = pickle.dumps(notices[tried:]).encode("base64") + queued_batch.save() + raise e sent += 1 queued_batch.delete() batches += 1 + tried = 0 except: # get the exception exc_class, e, t = sys.exc_info() @@ -72,7 +82,7 @@ def send_all(): logging.debug("releasing lock...") lock.release() logging.debug("released.") - + logging.info("") logging.info("%s batches, %s sent" % (batches, sent,)) logging.info("done in %.2f seconds" % (time.time() - start_time))