[PATCH 3/4] models: Add callbacks for events

Stephen Finucane stephen at that.guru
Sat Jan 7 10:40:26 AEDT 2017


When models are modified, fire signal handlers to create the relevant
events.

Signed-off-by: Stephen Finucane <stephen at that.guru>
---
Changes since RFC:
- Make use of database-provided foreign keys rather than reimplementing
  them ourselves
---
 patchwork/signals.py           | 114 ++++++++++++++++++++++++++
 patchwork/tests/test_events.py | 180 +++++++++++++++++++++++++++++++++++++++++
 patchwork/tests/utils.py       |  15 ++++
 3 files changed, 309 insertions(+)
 create mode 100644 patchwork/tests/test_events.py

diff --git a/patchwork/signals.py b/patchwork/signals.py
index 6f7f5ea..0547b5b 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 SeriesPatch
 
 
 @receiver(pre_save, sender=Patch)
@@ -61,3 +65,113 @@ 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(
+            project=patch.project,
+            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(
+            project=patch.project,
+            patch=patch,
+            category=Event.CATEGORY_STATE_CHANGED,
+            previous_state=before,
+            current_state=after)
+
+    # 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(
+            project=patch.project,
+            patch=patch,
+            category=Event.CATEGORY_DELEGATE_CHANGED,
+            previous_delegate=before,
+            current_delegate=after)
+
+    # 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=SeriesPatch)
+def create_dependencies_met_event(sender, instance, created, **kwargs):
+
+    def create_event(patch):
+        return Event.objects.create(
+            project=patch.project,
+            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 = SeriesPatch.objects.filter(
+        series=instance.series, 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 SeriesPatch.objects.filter(
+            series=instance.series, 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(
+            project=patch.project,
+            patch=patch,
+            category=Event.CATEGORY_CHECK_ADDED,
+            created_check=check)  # there's no previous check
+
+    if not created:
+        return
+
+    create_event(instance.patch, instance)
diff --git a/patchwork/tests/test_events.py b/patchwork/tests/test_events.py
new file mode 100644
index 0000000..971e7ee
--- /dev/null
+++ b/patchwork/tests/test_events.py
@@ -0,0 +1,180 @@
+# 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
+
+
+BASE_FIELDS = ['previous_state', 'current_state', 'previous_delegate',
+               'current_delegate', 'created_check']
+
+
+def _get_events(patch):
+    # These are sorted by reverse normally, so reverse it once again
+    return Event.objects.filter(patch=patch).order_by('date')
+
+
+class _BaseTestCase(TestCase):
+
+    def assertEventFields(self, event, **fields):
+        for field_name in [x for x in BASE_FIELDS]:
+            field = getattr(event, field_name)
+            if field_name in fields:
+                self.assertEqual(field, fields[field_name])
+            else:
+                self.assertIsNone(field)
+
+
+class PatchCreateTest(_BaseTestCase):
+
+    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.assertEqual(events[0].project, patch.project)
+        self.assertEventFields(events[0])
+
+    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[0].project, patch.patch.project)
+        self.assertEqual(events[1].category, Event.CATEGORY_DEPENDENCIES_MET)
+        self.assertEqual(events[1].project, patch.patch.project)
+        self.assertEventFields(events[0])
+        self.assertEventFields(events[1])
+
+    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)
+        self.assertEventFields(events[0])
+
+    def test_patch_dependencies_resolved(self):
+        series = utils.create_series()
+        patch_3 = utils.create_series_patch(series=series, number=3)
+        patch_2 = utils.create_series_patch(series=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)
+            self.assertEventFields(events[0])
+
+        patch_1 = utils.create_series_patch(series=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)
+            self.assertEventFields(events[0])
+            self.assertEventFields(events[1])
+
+
+class PatchChangedTest(_BaseTestCase):
+
+    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].project, patch.project)
+        self.assertEventFields(events[1], previous_state=old_state,
+                               current_state=new_state)
+
+    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.assertEqual(events[1].project, patch.project)
+        self.assertEventFields(events[1], current_delegate=delegate_a)
+
+        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.assertEventFields(events[2], previous_delegate=delegate_a,
+                               current_delegate=delegate_b)
+
+        # 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.assertEventFields(events[3], previous_delegate=delegate_b)
+
+
+class CheckCreateTest(_BaseTestCase):
+
+    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.assertEqual(events[1].project, patch.project)
+        self.assertEventFields(events[1], created_check=check)
diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py
index d94e889..7ee09c3 100644
--- a/patchwork/tests/utils.py
+++ b/patchwork/tests/utils.py
@@ -33,6 +33,7 @@ from patchwork.models import Patch
 from patchwork.models import Person
 from patchwork.models import Project
 from patchwork.models import Series
+from patchwork.models import SeriesPatch
 from patchwork.models import SeriesReference
 from patchwork.models import State
 from patchwork.tests import TEST_PATCH_DIR
@@ -232,6 +233,20 @@ def create_series(**kwargs):
     return Series.objects.create(**values)
 
 
+def create_series_patch(**kwargs):
+    """Create 'SeriesPatch' object."""
+    num = 1 if 'series' not in kwargs else kwargs['series'].patches.count() + 1
+
+    values = {
+        'series': create_series() if 'series' not in kwargs else None,
+        'number': num,
+        'patch': create_patch() if 'patch' not in kwargs else None,
+    }
+    values.update(**kwargs)
+
+    return SeriesPatch.objects.create(**values)
+
+
 def create_series_reference(**kwargs):
     """Create 'SeriesReference' object."""
     values = {
-- 
2.9.3



More information about the Patchwork mailing list