From 798a73b8bfb41f742e78e481ab9c961556e117b3 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 29 Mar 2011 11:58:39 +0800 Subject: models: Add PatchChangeNotification and record patch state changes Add a PatchChangeNotification model to keep track of changes to a patch's state. Hook this up to Patch's pre_save signal. Requires a DB schema upgrade. Signed-off-by: Jeremy Kerr --- apps/patchwork/models.py | 44 ++++++++ apps/patchwork/tests/__init__.py | 1 + apps/patchwork/tests/notifications.py | 117 +++++++++++++++++++++ lib/sql/grant-all.mysql.sql | 1 + lib/sql/grant-all.postgres.sql | 6 +- .../migration/011-patch-change-notifications.sql | 12 +++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 apps/patchwork/tests/notifications.py create mode 100644 lib/sql/migration/011-patch-change-notifications.sql diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py index f21d073..17a68db 100644 --- a/apps/patchwork/models.py +++ b/apps/patchwork/models.py @@ -64,6 +64,7 @@ class Project(models.Model): name = models.CharField(max_length=255, unique=True) listid = models.CharField(max_length=255, unique=True) listemail = models.CharField(max_length=200) + send_notifications = models.BooleanField() def __unicode__(self): return self.name @@ -406,3 +407,46 @@ class EmailOptout(models.Model): def __unicode__(self): return self.email + +class PatchChangeNotification(models.Model): + patch = models.ForeignKey(Patch, primary_key = True) + last_modified = models.DateTimeField(default = datetime.datetime.now) + orig_state = models.ForeignKey(State) + +def _patch_change_callback(sender, instance, **kwargs): + # we only want notification of modified patches + if instance.pk is None: + return + + if instance.project is None or not instance.project.send_notifications: + return + + try: + orig_patch = Patch.objects.get(pk = instance.pk) + except Patch.DoesNotExist: + return + + # If there's no interesting changes, abort without creating the + # notification + if orig_patch.state == instance.state: + return + + notification = None + try: + notification = PatchChangeNotification.objects.get(patch = instance) + except PatchChangeNotification.DoesNotExist: + pass + + if notification is None: + notification = PatchChangeNotification(patch = instance, + orig_state = orig_patch.state) + + elif notification.orig_state == instance.state: + # If we're back at the original state, there is no need to notify + notification.delete() + return + + notification.last_modified = datetime.datetime.now() + notification.save() + +models.signals.pre_save.connect(_patch_change_callback, sender = Patch) diff --git a/apps/patchwork/tests/__init__.py b/apps/patchwork/tests/__init__.py index 0b56fc1..8ae271a 100644 --- a/apps/patchwork/tests/__init__.py +++ b/apps/patchwork/tests/__init__.py @@ -27,3 +27,4 @@ from patchwork.tests.confirm import * from patchwork.tests.registration import * from patchwork.tests.user import * from patchwork.tests.mail_settings import * +from patchwork.tests.notifications import * diff --git a/apps/patchwork/tests/notifications.py b/apps/patchwork/tests/notifications.py new file mode 100644 index 0000000..c4df1b0 --- /dev/null +++ b/apps/patchwork/tests/notifications.py @@ -0,0 +1,117 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2011 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.test import TestCase +from django.core.urlresolvers import reverse +from django.db.utils import IntegrityError +from patchwork.models import Patch, State, PatchChangeNotification +from patchwork.tests.utils import defaults, create_maintainer + +class PatchNotificationModelTest(TestCase): + """Tests for the creation & update of the PatchChangeNotification model""" + + def setUp(self): + self.project = defaults.project + self.project.send_notifications = True + self.project.save() + self.submitter = defaults.patch_author_person + self.submitter.save() + self.patch = Patch(project = self.project, msgid = 'testpatch', + name = 'testpatch', content = '', + submitter = self.submitter) + + def tearDown(self): + self.patch.delete() + self.submitter.delete() + self.project.delete() + + def testPatchCreation(self): + """Ensure we don't get a notification on create""" + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testPatchUninterestingChange(self): + """Ensure we don't get a notification for "uninteresting" changes""" + self.patch.save() + self.patch.archived = True + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testPatchChange(self): + """Ensure we get a notification for interesting patch changes""" + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.patch, self.patch) + self.assertEqual(notification.orig_state, oldstate) + + def testNotificationCancelled(self): + """Ensure we cancel notifications that are no longer valid""" + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + + self.patch.state = oldstate + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testNotificationUpdated(self): + """Ensure we update notifications when the patch has a second change, + but keep the original patch details""" + self.patch.save() + oldstate = self.patch.state + newstates = State.objects.exclude(pk = oldstate.pk)[:2] + + self.patch.state = newstates[0] + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.orig_state, oldstate) + orig_timestamp = notification.last_modified + + self.patch.state = newstates[1] + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.orig_state, oldstate) + self.assertTrue(notification.last_modified > orig_timestamp) + + def testProjectNotificationsDisabled(self): + """Ensure we don't see notifications created when a project is + configured not to send them""" + self.project.send_notifications = False + self.project.save() + + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + diff --git a/lib/sql/grant-all.mysql.sql b/lib/sql/grant-all.mysql.sql index c272e1e..1bff526 100644 --- a/lib/sql/grant-all.mysql.sql +++ b/lib/sql/grant-all.mysql.sql @@ -23,6 +23,7 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_bundle TO 'www-data'@localhost GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_bundle_patches TO 'www-data'@localhost; GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_patch TO 'www-data'@localhost; GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_emailoptout TO 'www-data'@localhost; +GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_patchchangenotification TO 'www-data'@localhost; -- allow the mail user (in this case, 'nobody') to add patches GRANT INSERT, SELECT ON patchwork_patch TO 'nobody'@localhost; diff --git a/lib/sql/grant-all.postgres.sql b/lib/sql/grant-all.postgres.sql index 9b6c862..72abb57 100644 --- a/lib/sql/grant-all.postgres.sql +++ b/lib/sql/grant-all.postgres.sql @@ -23,7 +23,8 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_bundle, patchwork_bundlepatch, patchwork_patch, - patchwork_emailoptout + patchwork_emailoptout, + patchwork_patchchangenotification TO "www-data"; GRANT SELECT, UPDATE ON auth_group_id_seq, @@ -45,7 +46,8 @@ GRANT SELECT, UPDATE ON patchwork_state_id_seq, patchwork_emailconfirmation_id_seq, patchwork_userprofile_id_seq, - patchwork_userprofile_maintainer_projects_id_seq + patchwork_userprofile_maintainer_projects_id_seq, + patchwork_patchchangenotification_id_seq TO "www-data"; -- allow the mail user (in this case, 'nobody') to add patches diff --git a/lib/sql/migration/011-patch-change-notifications.sql b/lib/sql/migration/011-patch-change-notifications.sql new file mode 100644 index 0000000..0a9b9b7 --- /dev/null +++ b/lib/sql/migration/011-patch-change-notifications.sql @@ -0,0 +1,12 @@ +BEGIN; +CREATE TABLE "patchwork_patchchangenotification" ( + "patch_id" integer NOT NULL PRIMARY KEY REFERENCES "patchwork_patch" ("id") DEFERRABLE INITIALLY DEFERRED, + "last_modified" timestamp with time zone NOT NULL, + "orig_state_id" integer NOT NULL REFERENCES "patchwork_state" ("id") DEFERRABLE INITIALLY DEFERRED +) +; +ALTER TABLE "patchwork_project" ADD COLUMN + "send_notifications" boolean NOT NULL DEFAULT False; +ALTER TABLE "patchwork_project" ALTER COLUMN + "send_notifications" DROP DEFAULT; +COMMIT; -- cgit v1.2.3