[RFC 3/3] models: Add callbacks for events
Stephen Finucane
stephen at that.guru
Wed Oct 19 07:12:05 AEDT 2016
When models are modified, fire signal handlers to create the relevant
events.
Signed-off-by: Stephen Finucane <stephen at that.guru>
---
There are a couple of things to note about this:
- We don't create events for object deletion. This means that events
will become out-of-date if the object is deleted or the id is somehow
changed. However, these events are unlikely and we will probably
expire/delete old events at some point, meaing it's unlikely to be an
issue.
- We should add code to expire/delete old events, perhaps as part of
the cron job.
- We should add a notification loop which normalizes and generates
emails for these events. This will replace the existing notification
system.
- As highlighted in the previous patch, we don't raise events for Tag
creation. Doing so would require rework of the relevant models. We
can do this in the future.
- This will be exposed via the REST API, but we may also wish to look
at something a little more realtime. Channels [1] looks like a
promising candidate as it supports Django 1.8+ and is an official
Django incubator project since 1.10
[1] http://channels.readthedocs.io/
---
patchwork/signals.py | 109 +++++++++++++++++++++++++++
patchwork/templatetags/compat.py | 1 +
patchwork/tests/test_events.py | 159 +++++++++++++++++++++++++++++++++++++++
patchwork/tests/utils.py | 15 ++++
4 files changed, 284 insertions(+)
create mode 100644 patchwork/tests/test_events.py
diff --git a/patchwork/signals.py b/patchwork/signals.py
index 6f7f5ea..9919b6c 100644
--- a/patchwork/signals.py
+++ b/patchwork/signals.py
@@ -19,11 +19,15 @@
from datetime import datetime as dt
+from django.db.models.signals import post_save
from django.db.models.signals import pre_save
from django.dispatch import receiver
+from patchwork.models import Check
+from patchwork.models import Event
from patchwork.models import Patch
from patchwork.models import PatchChangeNotification
+from patchwork.models import SeriesRevisionPatch
@receiver(pre_save, sender=Patch)
@@ -61,3 +65,108 @@ def patch_change_callback(sender, instance, **kwargs):
notification.last_modified = dt.now()
notification.save()
+
+
+ at receiver(post_save, sender=Patch)
+def create_patch_created_event(sender, instance, created, **kwargs):
+
+ def create_event(patch):
+ return Event.objects.create(
+ patch=patch,
+ category=Event.CATEGORY_PATCH_CREATED)
+
+ if not created:
+ return
+
+ create_event(instance)
+
+
+ at receiver(pre_save, sender=Patch)
+def create_state_changed_event(sender, instance, **kwargs):
+
+ def create_event(patch, before, after):
+ return Event.objects.create(
+ patch=patch,
+ category=Event.CATEGORY_STATE_CHANGED,
+ before=before.id,
+ after=after.id)
+
+ # only trigger on updated items
+ if not instance.pk:
+ return
+
+ orig_patch = Patch.objects.get(pk=instance.pk)
+
+ if orig_patch.state == instance.state:
+ return
+
+ create_event(instance, orig_patch.state, instance.state)
+
+
+ at receiver(pre_save, sender=Patch)
+def create_delegate_changed_event(sender, instance, **kwargs):
+
+ def create_event(patch, before, after):
+ return Event.objects.create(
+ patch=patch,
+ category=Event.CATEGORY_DELEGATE_CHANGED,
+ before=before.id if before else None,
+ after=after.id if after else None)
+
+ # only trigger on updated items
+ if not instance.pk:
+ return
+
+ orig_patch = Patch.objects.get(pk=instance.pk)
+
+ if orig_patch.delegate == instance.delegate:
+ return
+
+ create_event(instance, orig_patch.delegate, instance.delegate)
+
+
+ at receiver(post_save, sender=SeriesRevisionPatch)
+def create_dependencies_met_event(sender, instance, created, **kwargs):
+
+ def create_event(patch):
+ return Event.objects.create(
+ patch=patch,
+ category=Event.CATEGORY_DEPENDENCIES_MET)
+
+ if not created:
+ return
+
+ # if dependencies not met, don't raise event. There's also no point raising
+ # events for successors since they'll have the same issue
+ predecessors = SeriesRevisionPatch.objects.filter(
+ revision=instance.revision, number__lt=instance.number)
+ if predecessors.count() != instance.number - 1:
+ return
+
+ create_event(instance.patch)
+
+ # if this satisfies dependencies for successor patch, raise events for
+ # those
+ count = instance.number + 1
+ for successor in SeriesRevisionPatch.objects.filter(
+ revision=instance.revision, number__gt=instance.number):
+ if successor.number != count:
+ break
+
+ create_event(successor.patch)
+ count += 1
+
+
+ at receiver(post_save, sender=Check)
+def create_check_created_event(sender, instance, created, **kwargs):
+
+ def create_event(patch, check):
+ return Event.objects.create(
+ patch=patch,
+ category=Event.CATEGORY_CHECK_ADDED,
+ after=check.id) # there's no previous check
+
+ if not created:
+ return
+
+ create_event(instance.patch, instance)
diff --git a/patchwork/templatetags/compat.py b/patchwork/templatetags/compat.py
index b18538f..7b210e8 100644
--- a/patchwork/templatetags/compat.py
+++ b/patchwork/templatetags/compat.py
@@ -26,6 +26,7 @@ from django.template import Library
register = Library()
+
# cycle
#
# The cycle template tag enables auto-escaping by default in 1.8, with
diff --git a/patchwork/tests/test_events.py b/patchwork/tests/test_events.py
new file mode 100644
index 0000000..4c4ef95
--- /dev/null
+++ b/patchwork/tests/test_events.py
@@ -0,0 +1,159 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2015 Stephen Finucane <stephen at that.guru>
+#
+# 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 patchwork.models import Event
+from patchwork.tests import utils
+
+
+def _get_events(patch):
+ # These are sorted by reverse normally, so reverse it once again
+ return Event.objects.filter(patch=patch).order_by('created_on')
+
+
+class PatchCreateTest(TestCase):
+
+ def test_patch_created(self):
+ patch = utils.create_patch()
+
+ # This should raise the CATEGORY_PATCH_CREATED event as it is
+ # not a series patch
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
+ self.assertIsNone(events[0].before)
+ self.assertIsNone(events[0].after)
+
+ def test_patch_dependencies_present(self):
+ """Patch dependencies already exist."""
+ patch = utils.create_series_patch()
+
+ # This should raise both the CATEGORY_PATCH_CREATED and
+ # CATEGORY_DEPENDENCIES_MET events as it is a series patch
+ events = _get_events(patch.patch)
+ self.assertEqual(events.count(), 2)
+ self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
+ self.assertEqual(events[1].category, Event.CATEGORY_DEPENDENCIES_MET)
+ self.assertIsNone(events[1].before)
+ self.assertIsNone(events[1].after)
+
+ def test_patch_dependencies_missing(self):
+ patch = utils.create_series_patch(number=2)
+
+ # This should only raise the CATEGORY_PATCH_CREATED event as
+ # there is a missing dependency (patch 1)
+ events = _get_events(patch.patch)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
+
+ def test_patch_dependencies_resolved(self):
+ series = utils.create_series_revision()
+ patch_3 = utils.create_series_patch(revision=series, number=3)
+ patch_2 = utils.create_series_patch(revision=series, number=2)
+
+ # This should only raise the CATEGORY_PATCH_CREATED event for
+ # both patches as they are both missing dependencies
+ for patch in [patch_2, patch_3]:
+ events = _get_events(patch.patch)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
+
+ patch_1 = utils.create_series_patch(revision=series, number=1)
+
+ # We should now see the CATEGORY_DEPENDENCIES_MET event for all
+ # patches as the dependencies for all have been met
+ for patch in [patch_1, patch_2, patch_3]:
+ events = _get_events(patch.patch)
+ self.assertEqual(events.count(), 2)
+ self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
+ self.assertEqual(events[1].category,
+ Event.CATEGORY_DEPENDENCIES_MET)
+
+
+class PatchChangedTest(TestCase):
+
+ def test_state_changed(self):
+ patch = utils.create_patch()
+ old_state = patch.state
+ new_state = utils.create_state()
+
+ patch.state = new_state
+ patch.save()
+
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 2)
+ # we don't care about the CATEGORY_PATCH_CREATED event here
+ self.assertEqual(events[1].category, Event.CATEGORY_STATE_CHANGED)
+ self.assertEqual(events[1].before, old_state.id)
+ self.assertEqual(events[1].after, new_state.id)
+
+ def test_delegate_changed(self):
+ patch = utils.create_patch()
+ delegate_a = utils.create_user()
+
+ # None -> Delegate A
+
+ patch.delegate = delegate_a
+ patch.save()
+
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 2)
+ # we don't care about the CATEGORY_PATCH_CREATED event here
+ self.assertEqual(events[1].category, Event.CATEGORY_DELEGATE_CHANGED)
+ self.assertIsNone(events[1].before)
+ self.assertEqual(events[1].after, delegate_a.id)
+
+ delegate_b = utils.create_user()
+
+ # Delegate A -> Delegate B
+
+ patch.delegate = delegate_b
+ patch.save()
+
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 3)
+ self.assertEqual(events[2].category, Event.CATEGORY_DELEGATE_CHANGED)
+ self.assertEqual(events[2].before, delegate_a.id)
+ self.assertEqual(events[2].after, delegate_b.id)
+
+ # Delegate B -> None
+
+ patch.delegate = None
+ patch.save()
+
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 4)
+ self.assertEqual(events[3].category, Event.CATEGORY_DELEGATE_CHANGED)
+ self.assertEqual(events[3].before, delegate_b.id)
+ self.assertEqual(events[3].after, None)
+
+
+class CheckCreateTest(TestCase):
+
+ def test_check_created(self):
+ patch = utils.create_patch()
+ check = utils.create_check(patch=patch)
+
+ events = _get_events(patch)
+ self.assertEqual(events.count(), 2)
+ # we don't care about the CATEGORY_PATCH_CREATED event here
+ self.assertEqual(events[1].category, Event.CATEGORY_CHECK_ADDED)
+ self.assertIsNone(events[1].before)
+ self.assertEqual(events[1].after, check.id)
diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py
index 753e1ec..212bfe3 100644
--- a/patchwork/tests/utils.py
+++ b/patchwork/tests/utils.py
@@ -33,6 +33,7 @@ from patchwork.models import Person
from patchwork.models import Project
from patchwork.models import SeriesReference
from patchwork.models import SeriesRevision
+from patchwork.models import SeriesRevisionPatch
from patchwork.models import State
from patchwork.tests import TEST_PATCH_DIR
@@ -242,6 +243,20 @@ def create_series_reference(**kwargs):
return SeriesReference.objects.create(**values)
+def create_series_patch(**kwargs):
+ num = 1 if 'series' not in kwargs else kwargs['series'].patches.count() + 1
+
+ values = {
+ 'revision': create_series_revision() if 'revision' not in kwargs
+ else None,
+ 'number': num,
+ 'patch': create_patch() if 'patch' not in kwargs else None,
+ }
+ values.update(**kwargs)
+
+ return SeriesRevisionPatch.objects.create(**values)
+
+
def _create_submissions(create_func, count=1, **kwargs):
"""Create 'count' Submission-based objects.
--
2.7.4
More information about the Patchwork
mailing list