diff --git a/scrum/fixtures/test_data.json b/scrum/fixtures/test_data.json index 9b17384..2a5771b 100644 --- a/scrum/fixtures/test_data.json +++ b/scrum/fixtures/test_data.json @@ -12,7 +12,8 @@ "model":"scrum.project", "fields":{ "name":"MDN", - "slug":"mdn" + "slug":"mdn", + "team":1 } }, { diff --git a/scrum/models.py b/scrum/models.py index 9f4e9c8..f9dc821 100644 --- a/scrum/models.py +++ b/scrum/models.py @@ -15,7 +15,6 @@ from django.core.cache import cache from django.core.validators import RegexValidator from django.db import models, transaction -#from django.db.models.query import QuerySet from django.db.models.query_utils import Q from django.db.models.signals import pre_save, post_save from django.dispatch import receiver @@ -29,7 +28,7 @@ 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_story_data, parse_bz_url, parse_whiteboard) log = logging.getLogger(__name__) @@ -200,6 +199,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. @@ -229,6 +231,9 @@ class Project(DBBugsMixin, BugsListMixin, models.Model): _date_cached = None + class Meta: + ordering = ('name',) + def __unicode__(self): return self.name @@ -615,6 +620,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) @@ -669,7 +686,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): @@ -700,7 +717,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: @@ -787,11 +804,39 @@ 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: + # already in the sprint + return + if instance.project is None: + new_proj = instance.project_from_product + if new_proj is None: + return + # add to ready backlog + log.debug('Adding %s to %s', instance, new_proj) + instance.project = new_proj + + try: + newsprint_obj = Sprint.objects.get(team=instance.project.team, + slug=newsprint) + except Sprint.DoesNotExist: + return + + 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: diff --git a/scrum/test_data/bugzilla_data.json b/scrum/test_data/bugzilla_data.json index f3d114a..6b61a4d 100644 --- a/scrum/test_data/bugzilla_data.json +++ b/scrum/test_data/bugzilla_data.json @@ -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", @@ -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", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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":"willkg@mozilla.com", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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":"willkg@mozilla.com", @@ -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", @@ -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":"willkg@mozilla.com", @@ -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":"--", @@ -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", @@ -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", diff --git a/scrum/tests.py b/scrum/tests.py index 8b61c9c..490fd37 100644 --- a/scrum/tests.py +++ b/scrum/tests.py @@ -145,6 +145,62 @@ 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_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=2012-01-15' + 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='2012-01-15') + + 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() + newsprint = Sprint.objects.create( + name='New Sprint', + slug='newsprint', + start_date=date.today(), + end_date=date.today() + timedelta(days=10), + team=self.s.team + ) + b = Bug.objects.get(id=778465) + b.whiteboard += ' s=newsprint' + b.save() + b = Bug.objects.get(id=778465) + eq_(b.sprint, newsprint) + + 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 """ diff --git a/scrum/utils.py b/scrum/utils.py index 1de51d5..73f4223 100644 --- a/scrum/utils.py +++ b/scrum/utils.py @@ -54,21 +54,25 @@ def get_setting_or_env(name, default=None): def parse_whiteboard(wb): + wb = wb.strip() + if wb: + return dict(i.split('=', 1) for i in wb.split() if '=' in i) + return {} + + +def get_story_data(wb): wb_dict = { 'points': 0, 'user': '', 'component': '', } - wb = wb.strip() - if wb: - data = dict(i.split('=', 1) for i in wb.split() if '=' in i) - for k, v in data.iteritems(): - if v: - cast = int if k == 'p' else str - try: - wb_dict[TAG_2_ATTR[k]] = cast(v) - except (KeyError, ValueError): - continue + for k, v in parse_whiteboard(wb).iteritems(): + if v: + cast = int if k == 'p' else str + try: + wb_dict[TAG_2_ATTR[k]] = cast(v) + except (KeyError, ValueError): + continue return wb_dict diff --git a/settings/base.py b/settings/base.py index 228f94d..6c8e452 100644 --- a/settings/base.py +++ b/settings/base.py @@ -31,7 +31,6 @@ # http://packages.python.org/Markdown/extensions/index.html MARKDOWN_EXTENSIONS = [ - 'nl2br', 'fenced_code', 'tables', 'smart_strong',