When models are modified, fire signal handlers to create the relevant events.
Signed-off-by: Stephen Finucane <step...@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() + + +@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) + + +@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) + + +@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) + + +@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 + + +@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 <step...@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 _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork