Skip to content
This repository has been archived by the owner on Feb 21, 2019. It is now read-only.

Add ability to move bugs to sprints via bugzilla whiteboard. #95

Merged
merged 2 commits into from
Oct 5, 2012
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scrum/fixtures/test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"model":"scrum.project",
"fields":{
"name":"MDN",
"slug":"mdn"
"slug":"mdn",
"team":1
}
},
{
Expand Down
64 changes: 59 additions & 5 deletions scrum/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@

from bugzilla.api import BUG_OPEN_STATUSES, bugzilla, is_closed
from scrum.utils import (date_to_js, date_range, get_bz_url_for_bug_ids,
parse_bz_url, parse_whiteboard)
get_date, get_story_data, parse_bz_url,
parse_whiteboard)


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -200,6 +201,9 @@ class Team(DBBugsMixin, BugsListMixin, models.Model):
slug = models.CharField(max_length=50, validators=[validate_slug],
db_index=True, unique=True)

class Meta:
ordering = ('name',)

def get_bugs(self, **kwargs):
"""
Get all bugs from the ready backlogs of the projects.
Expand Down Expand Up @@ -229,6 +233,9 @@ class Project(DBBugsMixin, BugsListMixin, models.Model):

_date_cached = None

class Meta:
ordering = ('name',)

def __unicode__(self):
return self.name

Expand Down Expand Up @@ -615,6 +622,18 @@ class Meta:
def __unicode__(self):
return unicode(self.id)

@property
def project_from_product(self):
prodcomp = BZProduct.objects.filter(name=self.product,
component=self.component)
if prodcomp:
return prodcomp[0].project
else:
prod = BZProduct.objects.filter(name=self.product)
if prod:
return prod[0].project
return None

def fill_from_data(self, data):
for attr_name, value in data.items():
setattr(self, attr_name, value)
Expand Down Expand Up @@ -669,7 +688,7 @@ def basic_status(self):

@property
def scrum_data(self):
return parse_whiteboard(self.whiteboard)
return get_story_data(self.whiteboard)

@property
def has_scrum_data(self):
Expand Down Expand Up @@ -700,7 +719,7 @@ def points_history(self):
})
closed = now_closed
elif fn == 'status_whiteboard':
pts = parse_whiteboard(change['added'])['points']
pts = get_story_data(change['added'])['points']
if pts != cpoints:
cpoints = pts
if not closed:
Expand Down Expand Up @@ -787,11 +806,46 @@ def get_bzproducts_dict(qs):

@receiver(pre_save, sender=Bug)
def update_scrum_data(sender, instance, **kwargs):
scrum_data = parse_whiteboard(instance.whiteboard)
for k, v in scrum_data.items():
for k, v in instance.scrum_data.items():
setattr(instance, 'story_' + k, v)


@receiver(pre_save, sender=Bug)
def move_to_sprint(sender, instance, **kwargs):
wb_data = parse_whiteboard(instance.whiteboard)
if 's' in wb_data:
newsprint = wb_data['s']
if instance.sprint and newsprint == instance.sprint.slug:
return
if instance.project is None:
new_prod = instance.project_from_product
if new_prod is None:
return
log.debug('Adding %s to %s', instance, new_prod)
instance.project = new_prod

newsprint_obj = None
try:
newsprint_obj = Sprint.objects.get(team=instance.project.team,
slug=newsprint)
except Sprint.DoesNotExist:
sprintdate = get_date(newsprint)
if sprintdate:
newsprint_obj = Sprint.objects.create(
team=instance.project.team,
name=newsprint,
slug=newsprint,
start_date=sprintdate,
end_date=sprintdate + timedelta(days=14),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. As I read this, this will create new sprints for s= that don't already exist. Part of me thinks it should just go in the backlog. Otherwise every s= typo will create a new sprint and since anyone can fiddle with the s= column in bugzilla, that seems like the worst of the two possibilities (lost in backlog vs. lots of extra sprints).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I could take that out. I thought it'd be cool if and only if the s= value was a valid ISO date. My worry is that if the sprint isn't yet created, then this code will mostly only add it to the ready backlog, and when the sprint is created, it won't automatically flow into it unless the bug is updated again. I could write some post save logic on Sprints that would search for bugs with the slug in their whiteboard and collect them I guess.

I'm just trying to make this thing as useful as possible for the most people. MDN isn't organized enough to use the sprint planning features yet, so they'd rather still be able to manipulate things from bugzilla. I did think about adding a boolean onto Project that you'd have to switch to enable this. Perhaps that would be better?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I think we (MDN, or just me) would be fine if s= didn't auto-create sprints, and was just ignored until/unless a corresponding sprint was created by an admin. Then, once the corresponding sprint is created (eg. s=2012-10-30), start paying attention to them.

One of the things I sorted out in my thinking yesterday is that we've often used tagging bugs for the next sprint as a way to collaboratively pre-load the next planning meeting (or even the one after next, like a GTD "tickler" file). So, if the sprint was created by an admin PM just before that meeting, and then picked up the tagged bugs, we'd be good.

(Seems like rather than being heavily top-down organized, we have a lot of inputs - from writers (ie. sheppy), readers, translators, devs, PM, etc. Helps to capture all those "oh yeah, next sprint we should..." moments with a quick whiteboard tweak by anyone who happens to be in IRC and can update a bug.)

I had also insisted that collaboratively adding bugs to a sprint after it started was important. I still think that, since MDN devs tend to do a few things every sprint that weren't originally planned, and we'd like the end-state of the sprint to reflect what we really did. But, it's less important than the pre-loading notion. (ie. If it's easier to just import sprint-tagged bugs once at sprint creation, I won't cry)

But, if you're doing it for MDN, I don't think either of those two things (pre-loading, post-loading) from whiteboard data really demands sprint auto-creation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. That sounds good. So when there's an 's=' tag in the whiteboard, but the sprint doesn't yet exist, the bug will go into the "Ready backlog", so it will be in the short list for planning the next sprint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. And if you open the sprint story management page and know there there are some in the ready backlog that have the sprint marked in the WB, you can just shift refresh and they'll move in via magic 💥

if newsprint_obj:
if instance.sprint:
BugSprintLog.objects.removed_from_sprint(instance,
instance.sprint)
instance.sprint = newsprint_obj
BugSprintLog.objects.added_to_sprint(instance, newsprint_obj)


@receiver(pre_save, sender=Sprint)
def process_notes(sender, instance, **kwargs):
if instance.notes:
Expand Down
42 changes: 21 additions & 21 deletions scrum/test_data/bugzilla_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"blocks":[778466],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p=1 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=1",
"creation_time":"2012-07-28T18:54:00Z",
"summary":"get a -dev server set up",
"priority":"P2",
Expand Down Expand Up @@ -72,7 +72,7 @@
"blocks":[],
"severity":"normal",
"depends_on":[778465],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-07-28T18:56:00Z",
"summary":"set up localization infrastructure",
"priority":"P2",
Expand Down Expand Up @@ -147,7 +147,7 @@
"blocks":[780626, 781715, 781717],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-10T01:25:00Z",
"summary":"implement models for firefox mobile feedback",
"priority":"P3",
Expand All @@ -169,7 +169,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.16"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -248,7 +248,7 @@
"blocks":[780626],
"severity":"normal",
"depends_on":[781709, 781721],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-10T01:29:00Z",
"summary":"implement firefox desktop feedback view and templates",
"priority":"P2",
Expand All @@ -270,7 +270,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.16"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -359,7 +359,7 @@
"blocks":[780626],
"severity":"normal",
"depends_on":[781710, 781721],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-10T01:30:00Z",
"summary":"implement firefox mobile feedback view and templates",
"priority":"P3",
Expand All @@ -381,7 +381,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.16"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -433,7 +433,7 @@
"blocks":[780626, 781718],
"severity":"normal",
"depends_on":[781710, 781709],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-10T01:35:00Z",
"summary":"implement indexing code",
"priority":"P2",
Expand All @@ -455,7 +455,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.17"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -491,9 +491,9 @@
{
"changes":[
{
"removed":"u=dev c=infrastructure p=2 s=2012.17",
"removed":"u=dev c=infrastructure p=2",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.16"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand All @@ -518,7 +518,7 @@
"blocks":[780626],
"severity":"normal",
"depends_on":[781717, 784742],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-10T01:42:00Z",
"summary":"implement dashboard",
"priority":"P2",
Expand All @@ -545,7 +545,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.17"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand All @@ -554,9 +554,9 @@
{
"changes":[
{
"removed":"u=dev c=infrastructure p=2 s=2012.17",
"removed":"u=dev c=infrastructure p=2",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.16"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -603,7 +603,7 @@
"blocks":[],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.17",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-20T20:29:00Z",
"summary":"visual notification of prod vs. non-prod environment",
"priority":"P2",
Expand All @@ -620,7 +620,7 @@
{
"removed":"",
"field_name":"status_whiteboard",
"added":"u=dev c=infrastructure p=2 s=2012.17"
"added":"u=dev c=infrastructure p=2"
}
],
"who":"[email protected]",
Expand Down Expand Up @@ -687,7 +687,7 @@
"blocks":[],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p= s=2012.16",
"whiteboard":"u=dev c=infrastructure p=",
"creation_time":"2012-08-21T20:50:00Z",
"summary":"Desktop and mobile AND TABLETS, oh my!",
"priority":"--",
Expand Down Expand Up @@ -734,7 +734,7 @@
"blocks":[],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p=2 s=2012.17",
"whiteboard":"u=dev c=infrastructure p=2",
"creation_time":"2012-08-21T20:56:00Z",
"summary":"upgrade playdoh-lib submodules",
"priority":"P4",
Expand Down Expand Up @@ -776,7 +776,7 @@
"blocks":[781718],
"severity":"normal",
"depends_on":[],
"whiteboard":"u=dev c=infrastructure p=3 s=2012.16",
"whiteboard":"u=dev c=infrastructure p=3",
"creation_time":"2012-08-22T18:44:00Z",
"summary":"implement search infrastructure",
"priority":"P2",
Expand Down
85 changes: 85 additions & 0 deletions scrum/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,91 @@ def test_whiteboard_clearable(self):
eq_(b.story_points, 0)
eq_(b.story_user, '')

def test_whiteboard_add_to_sprint(self):
"""
Specifying `s=SPRINT_SLUG` in the whiteboard should add the bug to
the sprint if it exists.
"""
b = Bug.objects.get(id=778465)
assert b.sprint is None
assert b.project is None
b.whiteboard = 's=2.2'
b.save()
b = Bug.objects.get(id=778465)
eq_(b.sprint, self.s)
eq_(b.project, self.p)

def test_whiteboard_add_to_created_sprint(self):
"""
Specifying `s=YYYY-MM-DD` in the whiteboard should add the bug to
a new sprint if it doesn't exist.
"""
with self.assertRaises(Sprint.DoesNotExist):
Sprint.objects.get(slug='2012-01-01')
b = Bug.objects.get(id=778465)
assert b.sprint is None
assert b.project is None
b.whiteboard = 's=2012-01-01'
b.save()
b = Bug.objects.get(id=778465)
s = Sprint.objects.get(slug='2012-01-01')
eq_(b.sprint, s)
eq_(b.project, self.p)
eq_(s.name, '2012-01-01')
eq_(s.start_date, date(2012, 1, 1))
eq_(s.end_date, date(2012, 1, 15))

def test_whiteboard_invalid_date_no_created(self):
"""
Specifying `s=YYYY-MM-DD` in the whiteboard should add the bug to
a new sprint if it doesn't exist unless the date is invalid.
"""
b = Bug.objects.get(id=778465)
assert b.sprint is None
assert b.project is None
b.whiteboard = 's=2012-30-01' # invalid date
b.save()
with self.assertRaises(Sprint.DoesNotExist):
Sprint.objects.get(slug='2012-30-01')
b = Bug.objects.get(id=778465)
assert b.sprint is None
eq_(b.project, self.p)

def test_whiteboard_add_to_project_if_no_sprint(self):
"""
Specifying a nonexistent sprint not in a date format should only add
to project.
"""
b = Bug.objects.get(id=778465)
assert b.sprint is None
assert b.project is None
b.whiteboard = 's=does-not-exist'
b.save()
b = Bug.objects.get(id=778465)
assert b.sprint is None
eq_(b.project, self.p)
with self.assertRaises(Sprint.DoesNotExist):
Sprint.objects.get(slug='does-not-exist')

def test_whiteboard_change_moves_bug_to_new_sprint(self):
""" Changes to the s= whiteboard tag should move the bug. """
self.test_whiteboard_add_to_sprint()
b = Bug.objects.get(id=778465)
b.whiteboard = 's=2012-01-15'
b.save()
b = Bug.objects.get(id=778465)
eq_(b.sprint, Sprint.objects.get(slug='2012-01-15'))

def test_whiteboard_sprint_moves_logged(self):
""" Bugs added to and removed from sprints should be in the log. """
self.test_whiteboard_change_moves_bug_to_new_sprint()
logs = BugSprintLog.objects.filter(bug_id=778465,
action=BugSprintLog.ADDED)
eq_(logs.count(), 2)
logs = BugSprintLog.objects.filter(bug_id=778465,
action=BugSprintLog.REMOVED)
eq_(logs.count(), 1)

@patch.object(Bug, 'points_history')
def test_points_for_date_default(self, mock_bug):
""" should default to points in whiteboard """
Expand Down
Loading