Commit bca4cd7b authored by Damien Lespiau's avatar Damien Lespiau
Browse files

series: Compute a state for series along with a summary patch statuses

We really want to be able to tell if we're finished with a series or
not. For instance this can be used to not display 'DONE' series in the
default list of series. It can also be used to filter out series done
with from the list of reviews a user has to do.

We take the opportunity to incorporate an 'incomplete' state, which
represents series with a revision that didn't receive all of its
Signed-off-by: default avatarDamien Lespiau <>
parent f918f53d
......@@ -884,7 +884,67 @@ def _patch_change_send_notification(old_patch, new_patch):
def _patch_change_callback(sender, instance, **kwargs):
def _revision_is_done(revision, summary):
for entry in summary:
if entry[2]: # state__action_required
return False
return True
def _revision_update_state(revision):
# the order_by() clears the default ordering (from the Meta class) which
# would be used in the GROUP BY clause otherwise. See:
summary = revision.patches.values_list('state', 'state__name',
'state__ordering') \
.annotate(count=models.Count('state')) \
summary = list(summary)
summary.sort(key=lambda e: e[3])
revision.state_summary = [{
'name': s[1],
'final': not s[2],
'count': s[4],
} for s in summary]
# revision not yet complete
revision_complete = revision.patches.count() == revision.n_patches
if not revision_complete:
revision.state = RevisionState.INCOMPLETE
# initial state
elif len(summary) == 1 and \
summary[0][0] == get_default_initial_patch_state().pk:
revision.state = RevisionState.INITIAL
# done: all patches are in a 'final' state, ie. a state that doesn't
# require any more action
elif _revision_is_done(revision, summary):
revision.state = RevisionState.DONE
# in progress
revision.state = RevisionState.IN_PROGRESS
def _patch_change_update_revision_state(new_patch):
# gather all the revisions we need to update (a patch can be part of more
# than one revision)
revisions = new_patch.seriesrevision_set.all()
# we shouldn't hit this since we're careful to not call this function on
# brand new patches that haven't been linked to a revision yet
if len(revisions) == 0:
for rev in revisions:
def _patch_pre_change_callback(sender, instance, **kwargs):
# we only want notification of modified patches
if is None:
......@@ -905,7 +965,32 @@ def _patch_change_callback(sender, instance, **kwargs):
_patch_change_log_event(orig_patch, instance)
_patch_change_send_notification(orig_patch, instance)
models.signals.pre_save.connect(_patch_change_callback, sender=Patch)
def _patch_post_change_callback(sender, instance, created, **kwargs):
# We filter out brand new patches because the SeriesRevisionPatch m2m table
# isn't populated at that point and so we can't query for the
# SeriesRevision <-> Patch relationship.
if created:
def _series_revision_patch_post_change_callback(sender, instance, created,
# We only hook into that many to many table to cover the case when the
# patches are first inserted and the SeriesRevision <-> Patch link wasn't
# established until now.
if not created:
models.signals.pre_save.connect(_patch_pre_change_callback, sender=Patch)
models.signals.post_save.connect(_patch_post_change_callback, sender=Patch)
def _on_revision_complete(sender, revision, **kwargs):
......@@ -23,7 +23,7 @@ from django.test import TestCase
from patchwork.models import (Patch, Series, SeriesRevision, Project,
SERIES_DEFAULT_NAME, EventLog, User, Person,
State, RevisionState)
from patchwork.tests.utils import read_mail
from patchwork.tests.utils import defaults, TestSeries
......@@ -83,6 +83,14 @@ class SeriesTest(TestCase):
logs = EventLog.objects.all()
self.assertEquals(logs.count(), 1)
def check_revision_summary(self, revision, expected):
summary = revision.state_summary
for entry in summary:
state = entry['name']
count = entry['count']
self.assertEquals(expected[state], count)
class GeneratedSeriesTest(SeriesTest):
project = defaults.project
......@@ -652,6 +660,54 @@ class FullSeriesUpdateTest(GeneratedSeriesTest):
self._test_internal((3, 7), ('Awesome series', 'Awesome series v2'))
class SeriesStateTest(GeneratedSeriesTest):
def testPartialRevision(self):
series, mails = self._create_series(3)
# insert cover letter and 2 patches
for i in range(0, 3):
revision = SeriesRevision.objects.all()[0]
self.assertEqual(revision.state, RevisionState.INCOMPLETE)
self.check_revision_summary(revision, {'New': 2})
def testInitialInsert(self):
series, mails = self._create_series(3)
revision = SeriesRevision.objects.all()[0]
self.assertEqual(revision.state, RevisionState.INITIAL)
self.check_revision_summary(revision, {'New': 3})
def testInProgress(self):
series, mails = self._create_series(3)
patch = Patch.objects.all()[1]
patch.state = State.objects.get(name='Under Review')
revision = SeriesRevision.objects.all()[0]
self.assertEqual(revision.state, RevisionState.IN_PROGRESS)
self.check_revision_summary(revision, {'New': 2, 'Under Review': 1})
def testDone(self):
series, mails = self._create_series(3)
patches = Patch.objects.all()
for i in (0, 1):
patch = patches[i]
patch.state = State.objects.get(name='Accepted')
patch = patches[2]
patch.state = State.objects.get(name='Rejected')
revision = SeriesRevision.objects.all()[0]
self.assertEqual(revision.state, RevisionState.DONE)
self.check_revision_summary(revision, {'Accepted': 2, 'Rejected': 1})
# series-new-revision event tests
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment