class CanConfirmCommitmentWithJSONTest(JSONAgiloTestCase): def setUp(self): self.super() self.teh.disable_sprint_date_normalization() self._create_sprint_with_team_and_small_backlog() self.teh.grant_permission(self.username(), Action.CONFIRM_COMMITMENT) self.smm = SprintModelManager(self.env) def _create_sprint_with_team_and_small_backlog(self, sprint_name=None, team=None): if team is None: self.team = self.teh.create_team(name=self.team_name()) self.teh.create_member(Usernames.team_member, self.team_name()) else: self.team = team self.sprint = self.teh.create_sprint(sprint_name or self.sprint_name(), team=self.team, duration=14, start=datetime.now() - timedelta(hours=2)) self._create_tasks() def _create_tasks(self): self.teh.create_task(sprint=self.sprint.name, remaining_time=13) self.teh.create_story(sprint=self.sprint.name, rd_points=10) # -------------------------------------------------------------------------- # helper methods def username(self): return 'Login User' def team_name(self): return 'Fnord Team' def _commitment(self, sprint, team): cmd = TeamController.GetTeamCommitmentCommand(self.env, sprint=sprint, team=team) commitment = TeamController(self.env).process_command(cmd) return commitment def _metric(self, sprint, team, metric): cmd = TeamController.GetTeamMetricCommand(self.env, sprint=sprint, team=team, metric=metric) result = TeamController(self.env).process_command(cmd) return result def _capacity(self, sprint, team): return self._metric(sprint, team, Key.CAPACITY) def _velocity(self, sprint, team): return self._metric(sprint, team, Key.ESTIMATED_VELOCITY) def _confirm_commitment(self, sprint=None, req=None): if req is None: req = self.teh.mock_request(self.username()) sprint_name = sprint and sprint.name or self.sprint.name args = dict(sprint=sprint_name) return ConfirmCommitmentJSONView(self.env).do_post(req, args) # -------------------------------------------------------------------------- # tests def test_stores_team_commitment_in_metrics(self): self.assert_none(self._commitment(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(13, self._commitment(self.sprint, self.team)) def test_stores_team_velocity_in_metrics(self): self.assert_none(self._velocity(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(10, self._velocity(self.sprint, self.team)) def test_stores_team_capacity_in_metrics(self): # Sprint must start at the same time (or before) the team members # start working or not all of his capacity will be in the sprint self.sprint.start = self.sprint.start.replace(hour=9, minute=0, second=0, microsecond=0, tzinfo=self.team.members[0].timezone()) self.sprint.save() SprintModelManager(self.env).get_cache().invalidate() self.assert_none(self._capacity(self.sprint, self.team)) self._confirm_commitment() self.assert_almost_equals(10*6, self._capacity(self.sprint, self.team), max_delta=0.01) def test_stores_historic_data_change(self): self.teh.create_task(sprint=self.sprint.name, remaining_time=8) aggregator = BurndownDataAggregator(self.env) changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(2, changes) self._confirm_commitment() changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(1, changes) def test_cannot_confirm_without_permission(self): req = self.teh.mock_request(username=Usernames.product_owner) self.assert_raises(PermissionError, lambda: self._confirm_commitment(req=req)) def test_scrum_master_can_commit(self): self.teh.grant_permission(Usernames.scrum_master, Role.SCRUM_MASTER) req = self.teh.mock_request(username=Usernames.scrum_master) self._confirm_commitment(req=req) self.assert_equals(13, self._commitment(self.sprint, self.team)) def _invalidate_backlog_cache(self): BacklogModelManager(self.env).get_cache().invalidate() def test_second_commit_updates_the_first(self): self.assert_none(self._commitment(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(13, self._commitment(self.sprint, self.team)) self.teh.create_task(sprint=self.sprint.name, remaining_time=8) self._invalidate_backlog_cache() self._confirm_commitment() self.assert_equals(21, self._commitment(self.sprint, self.team)) aggregator = BurndownDataAggregator(self.env) changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(1, changes) def test_can_only_commit_for_one_day(self): self.sprint.start = now() - timedelta(days=2) self.smm.save(self.sprint) self.assert_raises(PermissionError, self._confirm_commitment) def test_does_not_fail_with_team_without_members(self): team = self.teh.create_team(name='test_does_not_fail_with_team_without_membersTeam') self._create_sprint_with_team_and_small_backlog('test_does_not_fail_with_team_without_membersSprint', team) self.assert_length(0, self.team.members) self._confirm_commitment() self.assert_equals(0, self._capacity(self.sprint, self.team)) def test_does_not_explode_with_no_team(self): sprint_without_team = self.teh.create_sprint('Fnord') self.assert_raises(PermissionError, lambda: self._confirm_commitment(sprint=sprint_without_team)) def test_does_not_fail_with_unknown_sprint(self): req = self.teh.mock_request(username=self.username()) json_view = ConfirmCommitmentJSONView(self.env) self.assert_method_returns_error_with_empty_data(json_view.do_post, req, dict(sprint='Missing Fnord'))
class CanConfirmCommitmentWithJSONTest(JSONAgiloTestCase): def setUp(self): self.super() self.teh.disable_sprint_date_normalization() self._create_sprint_with_team_and_small_backlog() self.teh.grant_permission(self.username(), Action.CONFIRM_COMMITMENT) self.smm = SprintModelManager(self.env) def _create_sprint_with_team_and_small_backlog(self, sprint_name=None, team=None): if team is None: self.team = self.teh.create_team(name=self.team_name()) self.teh.create_member(Usernames.team_member, self.team_name()) else: self.team = team self.sprint = self.teh.create_sprint(sprint_name or self.sprint_name(), team=self.team, duration=14, start=datetime.now() - timedelta(hours=2)) self._create_tasks() def _create_tasks(self): self.teh.create_task(sprint=self.sprint.name, remaining_time=13) self.teh.create_story(sprint=self.sprint.name, rd_points=10) # -------------------------------------------------------------------------- # helper methods def username(self): return 'Login User' def team_name(self): return 'Fnord Team' def _commitment(self, sprint, team): cmd = TeamController.GetTeamCommitmentCommand(self.env, sprint=sprint, team=team) commitment = TeamController(self.env).process_command(cmd) return commitment def _metric(self, sprint, team, metric): cmd = TeamController.GetTeamMetricCommand(self.env, sprint=sprint, team=team, metric=metric) result = TeamController(self.env).process_command(cmd) return result def _capacity(self, sprint, team): return self._metric(sprint, team, Key.CAPACITY) def _velocity(self, sprint, team): return self._metric(sprint, team, Key.ESTIMATED_VELOCITY) def _confirm_commitment(self, sprint=None, req=None): if req is None: req = self.teh.mock_request(self.username()) sprint_name = sprint and sprint.name or self.sprint.name args = dict(sprint=sprint_name) return ConfirmCommitmentJSONView(self.env).do_post(req, args) # -------------------------------------------------------------------------- # tests def test_stores_team_commitment_in_metrics(self): self.assert_none(self._commitment(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(13, self._commitment(self.sprint, self.team)) def test_stores_team_velocity_in_metrics(self): self.assert_none(self._velocity(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(10, self._velocity(self.sprint, self.team)) def test_stores_team_capacity_in_metrics(self): # Sprint must start at the same time (or before) the team members # start working or not all of his capacity will be in the sprint self.sprint.start = self.sprint.start.replace( hour=9, minute=0, second=0, microsecond=0, tzinfo=self.team.members[0].timezone()) self.sprint.save() SprintModelManager(self.env).get_cache().invalidate() self.assert_none(self._capacity(self.sprint, self.team)) self._confirm_commitment() self.assert_almost_equals(10 * 6, self._capacity(self.sprint, self.team), max_delta=0.01) def test_stores_historic_data_change(self): self.teh.create_task(sprint=self.sprint.name, remaining_time=8) aggregator = BurndownDataAggregator(self.env) changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(2, changes) self._confirm_commitment() changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(1, changes) def test_cannot_confirm_without_permission(self): req = self.teh.mock_request(username=Usernames.product_owner) self.assert_raises(PermissionError, lambda: self._confirm_commitment(req=req)) def test_scrum_master_can_commit(self): self.teh.grant_permission(Usernames.scrum_master, Role.SCRUM_MASTER) req = self.teh.mock_request(username=Usernames.scrum_master) self._confirm_commitment(req=req) self.assert_equals(13, self._commitment(self.sprint, self.team)) def _invalidate_backlog_cache(self): BacklogModelManager(self.env).get_cache().invalidate() def test_second_commit_updates_the_first(self): self.assert_none(self._commitment(self.sprint, self.team)) self._confirm_commitment() self.assert_equals(13, self._commitment(self.sprint, self.team)) self.teh.create_task(sprint=self.sprint.name, remaining_time=8) self._invalidate_backlog_cache() self._confirm_commitment() self.assert_equals(21, self._commitment(self.sprint, self.team)) aggregator = BurndownDataAggregator(self.env) changes = aggregator.changes_for_sprint(self.sprint) self.assert_length(1, changes) def test_can_only_commit_for_one_day(self): self.sprint.start = now() - timedelta(days=2) self.smm.save(self.sprint) self.assert_raises(PermissionError, self._confirm_commitment) def test_does_not_fail_with_team_without_members(self): team = self.teh.create_team( name='test_does_not_fail_with_team_without_membersTeam') self._create_sprint_with_team_and_small_backlog( 'test_does_not_fail_with_team_without_membersSprint', team) self.assert_length(0, self.team.members) self._confirm_commitment() self.assert_equals(0, self._capacity(self.sprint, self.team)) def test_does_not_explode_with_no_team(self): sprint_without_team = self.teh.create_sprint('Fnord') self.assert_raises( PermissionError, lambda: self._confirm_commitment(sprint=sprint_without_team)) def test_does_not_fail_with_unknown_sprint(self): req = self.teh.mock_request(username=self.username()) json_view = ConfirmCommitmentJSONView(self.env) self.assert_method_returns_error_with_empty_data( json_view.do_post, req, dict(sprint='Missing Fnord'))
class SprintAdminPanel(AgiloAdminPanel): """ Administration panel for sprints. """ _type = 'sprints' _label = ('Sprints', _('Sprints')) def __init__(self): # Create an instance of Sprint Manager self.sm = SprintModelManager(self.env) self.tm = TeamModelManager(self.env) def _parse_args(self, req): start = req.args.get('start') if start: start = datefmt.parse_date(start, tzinfo=req.tz) end = req.args.get('end') if end: end = datefmt.parse_date(end, tzinfo=req.tz) duration = req.args.get('duration') if duration: try: duration = int(duration) except ValueError: duration = None return start, end, duration def detail_save_view(self, req, cat, page, name): sprint = self.sm.get(name=name) if not sprint or not sprint.exists: return req.redirect(req.href.admin(cat, page)) new_name = req.args.get('name') # if necessary, rename sprint if sprint.name != new_name: new_sprint = self.sm.get(name=new_name) if new_sprint and new_sprint.exists: add_warning( req, 'A sprint with this name already exists - cannot rename.') return self.detail_view(req, cat, page, name) if '/' in new_name: add_warning(req, 'Please don\'t use "/" in a sprint name.') return self.detail_view(req, cat, page, name) sprint.name = new_name sprint.description = req.args.get('description') sprint.milestone = req.args.get('milestone') team_name = req.args.get('team') team = None if team_name: team = self.tm.get(name=team_name) if not team or not team.exists: add_warning(req, u"Invalid team name, that team doesn't exist.") return self.detail_view(req, cat, page, name) sprint.team = team start, end, duration = self._parse_args(req) if start and start != sprint.start: if (end and duration) or (not end and not duration): add_warning(req, 'Please enter an end date OR a duration.') return self.detail_view(req, cat, page, name) sprint.start = start if end and end != sprint.end: if (start and duration) or (not start and not duration): add_warning(req, 'Please enter a start date OR a duration.') return self.detail_view(req, cat, page, name) sprint.end = end if duration and duration != sprint.duration: if (start and end) or (not start and not end): add_warning(req, 'Please enter an start date OR an end date.') return self.detail_view(req, cat, page, name) sprint.duration = duration self.sm.save(sprint) req.redirect(req.href.admin(cat, page)) def detail_view(self, req, cat, page, name): sprint = self.sm.get(name=name) if not sprint or not sprint.exists: return req.redirect(req.href.admin(cat, page)) data = { 'view': 'detail', 'sprint': sprint, 'teams': self.tm.select(), 'format_datetime': datefmt.format_datetime, 'date_hint': datefmt.get_date_format_hint(), 'datetime_hint': datefmt.get_datetime_format_hint(), 'milestones': [m.name for m in Milestone.select(self.env)], } data.update(req.args) add_script(req, 'common/js/wikitoolbar.js') return 'agilo_admin_sprint.html', data def list_view(self, req, cat, page): data = { 'view': 'list', 'sprints': self.sm.select(), 'format_datetime': datefmt.format_datetime, 'date_hint': datefmt.get_date_format_hint(), 'datetime_hint': datefmt.get_datetime_format_hint(), 'milestones': [m.name for m in Milestone.select(self.env)], } data.update(req.args) return 'agilo_admin_sprint.html', data def list_save_view(self, req, cat, page): name = req.args.get('name') start, end, duration = self._parse_args(req) if req.args.get('add'): if not name: add_warning(req, 'Please enter a sprint name.') return self.list_view(req, cat, page) if '/' in name: add_warning(req, 'Please do not use "/" in a sprint name.') return self.list_view(req, cat, page) sprint = self.sm.create(name=name, save=False) if not sprint: # sprint already exists, redirect to it req.redirect(req.href.admin(cat, page, name)) if not start and not end and not duration: add_warning(req, 'Not enough data to set a sprint.') return self.list_view(req, cat, page) if start: if (end and duration) or (not end and not duration): add_warning(req, 'Please enter an end date OR a duration.') return self.list_view(req, cat, page) sprint.start = start if end: if (start and duration) or (not start and not duration): add_warning(req, 'Please enter an start date OR a duration.') return self.list_view(req, cat, page) sprint.end = end if duration: if (start and end) or (not start and not end): add_warning(req, 'Please enter an start date OR an end date.') return self.list_view(req, cat, page) sprint.duration = duration sprint.milestone = req.args.get('milestone') self.sm.save(sprint) # Remove components if req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('No sprint selected')) if not isinstance(sel, list): sel = [sel] for name in sel: # TODO: relocate not closed ticket to another Sprint sprint = self.sm.get(name=name) if sprint: self.sm.delete(sprint) req.redirect(req.href.admin(cat, page))
class SprintAdminPanel(AgiloAdminPanel): """ Administration panel for sprints. """ _type = 'sprints' _label = ('Sprints', _('Sprints')) def __init__(self): # Create an instance of Sprint Manager self.sm = SprintModelManager(self.env) self.tm = TeamModelManager(self.env) def _parse_args(self, req): start = req.args.get('start') if start: start = datefmt.parse_date(start, tzinfo=req.tz) end = req.args.get('end') if end: end = datefmt.parse_date(end, tzinfo=req.tz) duration = req.args.get('duration') if duration: try: duration = int(duration) except ValueError: duration = None return start, end, duration def detail_save_view(self, req, cat, page, name): sprint = self.sm.get(name=name) if not sprint or not sprint.exists: return req.redirect(req.href.admin(cat, page)) new_name = req.args.get('name') # if necessary, rename sprint if sprint.name != new_name: new_sprint = self.sm.get(name=new_name) if new_sprint and new_sprint.exists: add_warning(req, 'A sprint with this name already exists - cannot rename.') return self.detail_view(req, cat, page, name) if '/' in new_name: add_warning(req, 'Please don\'t use "/" in a sprint name.') return self.detail_view(req, cat, page, name) sprint.name = new_name sprint.description = req.args.get('description') sprint.milestone = req.args.get('milestone') team_name = req.args.get('team') team = None if team_name: team = self.tm.get(name=team_name) if not team or not team.exists: add_warning(req, u"Invalid team name, that team doesn't exist.") return self.detail_view(req, cat, page, name) sprint.team = team start, end, duration = self._parse_args(req) if start and start != sprint.start: if (end and duration) or (not end and not duration): add_warning(req, 'Please enter an end date OR a duration.') return self.detail_view(req, cat, page, name) sprint.start = start if end and end != sprint.end: if (start and duration) or (not start and not duration): add_warning(req, 'Please enter a start date OR a duration.') return self.detail_view(req, cat, page, name) sprint.end = end if duration and duration != sprint.duration: if (start and end) or (not start and not end): add_warning(req, 'Please enter an start date OR an end date.') return self.detail_view(req, cat, page, name) sprint.duration = duration self.sm.save(sprint) req.redirect(req.href.admin(cat, page)) def detail_view(self, req, cat, page, name): sprint = self.sm.get(name=name) if not sprint or not sprint.exists: return req.redirect(req.href.admin(cat, page)) data = { 'view': 'detail', 'sprint': sprint, 'teams': self.tm.select(), 'format_datetime': datefmt.format_datetime, 'date_hint': datefmt.get_date_format_hint(), 'datetime_hint': datefmt.get_datetime_format_hint(), 'milestones': [m.name for m in Milestone.select(self.env)], } data.update(req.args) add_script(req, 'common/js/wikitoolbar.js') return 'agilo_admin_sprint.html', data def list_view(self, req, cat, page): data = { 'view': 'list', 'sprints': self.sm.select(), 'format_datetime' : datefmt.format_datetime, 'date_hint' : datefmt.get_date_format_hint(), 'datetime_hint' : datefmt.get_datetime_format_hint(), 'milestones' : [m.name for m in Milestone.select(self.env)], } data.update(req.args) return 'agilo_admin_sprint.html', data def list_save_view(self, req, cat, page): name = req.args.get('name') start, end, duration = self._parse_args(req) if req.args.get('add'): if not name: add_warning(req, 'Please enter a sprint name.') return self.list_view(req, cat, page) if '/' in name: add_warning(req, 'Please do not use "/" in a sprint name.') return self.list_view(req, cat, page) sprint = self.sm.create(name=name, save=False) if not sprint: # sprint already exists, redirect to it req.redirect(req.href.admin(cat, page, name)) if not start and not end and not duration: add_warning(req, 'Not enough data to set a sprint.') return self.list_view(req, cat, page) if start: if (end and duration) or (not end and not duration): add_warning(req, 'Please enter an end date OR a duration.') return self.list_view(req, cat, page) sprint.start = start if end: if (start and duration) or (not start and not duration): add_warning(req, 'Please enter an start date OR a duration.') return self.list_view(req, cat, page) sprint.end = end if duration: if (start and end) or (not start and not end): add_warning(req, 'Please enter an start date OR an end date.') return self.list_view(req, cat, page) sprint.duration = duration sprint.milestone = req.args.get('milestone') self.sm.save(sprint) # Remove components if req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('No sprint selected')) if not isinstance(sel, list): sel = [sel] for name in sel: # TODO: relocate not closed ticket to another Sprint sprint = self.sm.get(name=name) if sprint: self.sm.delete(sprint) req.redirect(req.href.admin(cat, page))