def setUp(self): self.super() self.env = self._testenv.get_trac_environment() self.team = TeamModelManager(self.env).create(name="Team#1") tmm = TeamMemberModelManager(self.env) self.members = (tmm.create(name="Member#1", team=self.team), tmm.create(name="Member#2", team=self.team, default_capacity=[4,4,4,0,0,0,0]), tmm.create(name="Member#3", team=self.team, default_capacity=[0,0,0,2,2,0,0])) for m in self.members: m.save()
def __init__(self): # Create an instance of Sprint Manager self.sm = SprintModelManager(self.env) self.tm = TeamModelManager(self.env)
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))
def setUp(self): self.super() self.tm = TeamModelManager(self.env) self.tmm = TeamMemberModelManager(self.env) self.controller = TeamController(self.env)
class TestTeam(AgiloTestCase): """Tests for the team model class Team""" def setUp(self): self.super() self.tm = TeamModelManager(self.env) self.tmm = TeamMemberModelManager(self.env) self.controller = TeamController(self.env) def set_hours_for_day_on_team_member(self, hours, day, team_member): calendar = team_member.calendar calendar.set_hours_for_day(hours, day) calendar.save() def team_with_no_member(self): team = self.tm.create(name="Team#1") return team def team_with_one_member(self): team = self.team_with_no_member() member = self._add_member_to_team('Member#1', team) return team, member def team_with_two_members(self): team, member1 = self.team_with_one_member() member2 = self._add_member_to_team('Member#2', team) return team, member1, member2 def _add_member_to_team(self, name, team): member = self.tmm.create(name=name, team=team) # just to make sure we have a capacity, 1h/1h for ease of calculating test data member.capacity = [9] * 7 member.save() return member def testCapacityHours(self): """Test the get_capacity_hours() method""" test_team = self.tm.create(name="Team#1") self.assert_true(test_team.exists) test_members = (self.tmm.create(name="Member#1", team=test_team), self.tmm.create(name="Member#2", team=test_team, default_capacity=[4,4,4,0,0,0,0]), self.tmm.create(name="Member#3", team=test_team, default_capacity=[0,0,0,2,2,0,0])) for tm in test_members: self.assert_not_none(tm.team) self.assert_equals(test_team, tm.team) # test the default constructor start_date = parse_date("2008-09-08T08:00:00") test_sprint = self.teh.create_sprint(name="TestSprint", start=start_date, duration=10) # test save and restore for member in test_members: self.assert_true(self.tmm.save(member)) test_sprint.team = test_team test_sprint.save() weekly_hours = (5 * 6) + (4 + 4 + 4) + (2 + 2) self.assert_equals(weekly_hours, test_team.capacity().default_hours_of_capacity_per_week()) sprint_hours = 2 * weekly_hours actual = test_team.capacity().hourly_capacities_in_sprint(test_sprint) capacity = sum(map(lambda each: each.capacity, actual)) self.assert_almost_equals(sprint_hours, capacity, max_delta=.01) def testVelocityForSprint(self): """Tests the set velocity for a sprint as a team metric""" test_team = self.tm.create(name="Team#1") self.assert_true(test_team.save()) self.tmm.create(name="Member#1", team=test_team).save() self.tmm.create(name="Member#2", team=test_team).save() self.tmm.create(name="Member#3", team=test_team).save() sprint = self.teh.create_sprint("TestSprint", team=test_team) us1 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5', Key.SPRINT: sprint.name}) us2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '8', Key.SPRINT: sprint.name}) us3 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '13', Key.SPRINT: sprint.name}) # we have to use the TeamController here cmd_store = TeamController.StoreTeamVelocityCommand(self.env, sprint=sprint, team=test_team, estimated=True) self.assert_equals(26, self.controller.process_command(cmd_store)) # Now close a story and check the actual velocity cmd_get = TeamController.GetStoredTeamVelocityCommand(self.env, sprint=sprint.name, team=test_team, estimated=True) us3[Key.STATUS] = Status.CLOSED us3.save_changes('tester', 'closed US3') self.assert_equals(26, self.controller.process_command(cmd_get)) cmd_store.estimated = False self.assert_equals(13, self.controller.process_command(cmd_store)) cmd_get.estimated = False self.assert_equals(13, self.controller.process_command(cmd_get)) def test_team_commitment(self): """Tests store and retrieval of the team commitment of a sprint""" test_team = self.tm.create(name="Team#1") sprint = self.teh.create_sprint("TestSprint", team=test_team) self.assert_true(test_team.exists) # Set the initial USP/RT ratio to 2 tm = TeamMetrics(self.env, sprint, test_team) tm[Key.RT_USP_RATIO] = 2 tm.save() tm1 = self.tmm.create(name="Member#1", team=test_team) self.tmm.create(name="Member#2", team=test_team) self.tmm.create(name="Member#3", team=test_team) us1 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '5', Key.SPRINT: sprint.name}) us2 = self.teh.create_ticket(Type.USER_STORY, props={Key.STORY_POINTS: '8', Key.SPRINT: sprint.name}) t1 = self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '12', Key.OWNER: tm1.name, Key.SPRINT: sprint.name}) # This task is not explicitly planned for the sprint, but because it is # linked should be calculated t2 = self.teh.create_ticket(Type.TASK, props={Key.REMAINING_TIME: '8', Key.OWNER: tm1.name}) # Make sure there is a remaining time entry on the first day of the sprint RemainingTime(self.env, t1).set_remaining_time(t1[Key.REMAINING_TIME], sprint.start) RemainingTime(self.env, t2).set_remaining_time(t2[Key.REMAINING_TIME], sprint.start) us1.link_to(t1) us1.link_to(t2) us2 = self.teh.load_ticket(ticket=us2) self.assert_equals(Type.USER_STORY, us2.get_type()) self.assert_not_none(us2._calculated) # check the estimated remaining time for us2 self.assert_equals(8 * 2, us2[Key.ESTIMATED_REMAINING_TIME]) cmd_class = TeamController.CalculateAndStoreTeamCommitmentCommand cmd_store_commitment = cmd_class(self.env, sprint=sprint, team=test_team) commitment = TeamController(self.env).process_command(cmd_store_commitment) self.assert_equals(12 + 8 * 2, commitment) cmd_get_commitment = TeamController.GetTeamCommitmentCommand(self.env, sprint=sprint, team=test_team) self.assert_equals(commitment, self.controller.process_command(cmd_get_commitment)) def test_get_team_metrics_command_returns_none_if_no_team_given(self): sprint_without_team = self.teh.create_sprint('FooSprint') cmd = TeamController.GetTeamCommitmentCommand(self.env, sprint=sprint_without_team) commitment = TeamController(self.env).process_command(cmd) self.assert_none(commitment) def test_can_return_hourly_capacity_on_specific_day(self): team, member1, member2 = self.team_with_two_members() capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(today()) self.assert_length(10, capacities) for capacity in capacities[:-1]: self.assert_equals(2, capacity.capacity) def test_last_entry_of_hourly_capacity_is_zero(self): team, member1, member2 = self.team_with_two_members() capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(today()) self.assert_equals(0, capacities[-1].capacity) def test_can_return_empty_list_if_no_capacity_is_there(self): team, member1, member2 = self.team_with_two_members() self.set_hours_for_day_on_team_member(0, today(), member1) self.set_hours_for_day_on_team_member(0, today(), member2) capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(today()) self.assert_length(0, capacities) def test_can_combine_different_timezones(self): team, member1, member2 = self.team_with_two_members() set_user_attribute_in_session(self.env, 'tz', 'GMT', member1.name) set_user_attribute_in_session(self.env, 'tz', 'GMT +1:00', member2.name) capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(today()) self.assert_length(11, capacities) self.assert_equals(1, capacities[0].capacity) self.assert_equals(1, capacities[-2].capacity) def test_can_cut_off_times_that_would_be_on_next_day(self): team, member1, member2 = self.team_with_two_members() set_user_attribute_in_session(self.env, 'tz', 'GMT', member1.name) set_user_attribute_in_session(self.env, 'tz', 'GMT -8:00', member2.name) viewer_timezone = member1.timezone() self.set_hours_for_day_on_team_member(0, yesterday(viewer_timezone), member2) # 15:00 at his place is 23:00 here, so two values should be lost day = today(tz=member1.timezone()) capacities = team.capacity(viewer_timezone).hourly_capacities_for_day(day) self.assert_length(16, capacities) self.assert_equals(1, capacities[0].capacity) self.assert_equals(1, capacities[-2].capacity) last_hour_of_member1 = datetime.combine(day, time(23, tzinfo=member1.timezone())) self.assert_equals(last_hour_of_member1, capacities[-2].when) def test_cuts_off_times_that_would_be_on_previous_day(self): team, member1, member2 = self.team_with_two_members() set_user_attribute_in_session(self.env, 'tz', 'GMT', member1.name) set_user_attribute_in_session(self.env, 'tz', 'GMT +11:00', member2.name) viewer_timezone = utc # don't want to get values from tomorrow self.set_hours_for_day_on_team_member(0, tomorrow(viewer_timezone), member2) # 11:00 at his place is 0:00 here, so two values should be lost capacities = team.capacity(viewer_timezone).hourly_capacities_for_day(today()) self.assert_length(17, capacities) self.assert_equals(1, capacities[0].capacity) self.assert_equals(1, capacities[-2].capacity) start_of_day_for_member1 = midnight(now(tz=viewer_timezone)) self.assert_equals(start_of_day_for_member1, capacities[0].when) def test_cuts_off_times_that_would_be_on_previous_day_for_the_viewer(self): team, member1 = self.team_with_one_member() set_user_attribute_in_session(self.env, 'tz', 'GMT +11:00', member1.name) viewer_timezone = utc # don't want to get values from tomorrow self.set_hours_for_day_on_team_member(0, tomorrow(viewer_timezone), member1) # 11:00 at his place is 0:00 here, so two values should be lost capacities = team.capacity(viewer_timezone).hourly_capacities_for_day(today()) self.assert_length(8, capacities) self.assert_equals(1, capacities[0].capacity) self.assert_equals(1, capacities[-2].capacity) start_of_day_for_member1 = midnight(now(tz=viewer_timezone)) self.assert_equals(start_of_day_for_member1, capacities[0].when) def test_includes_times_from_previous_day_that_get_shifted_to_today_through_the_timezone_difference(self): team, member1, member2 = self.team_with_two_members() set_user_attribute_in_session(self.env, 'tz', 'GMT', member1.name) set_user_attribute_in_session(self.env, 'tz', 'GMT -7:00', member2.name) day = today(tz=member1.timezone()) yesterday = day - timedelta(days=1) self.set_hours_for_day_on_team_member(9, yesterday, member2) # member2s last hour of yesterday should happen on today for member1 capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(day) member1_midnight = datetime.combine(day, time(0, tzinfo=member1.timezone())) self.assert_equals(member1_midnight, capacities[0].when) self.assert_equals(1, capacities[0].capacity) def test_includes_times_from_next_day_that_get_shifted_to_today_through_the_timezone_difference(self): team, member1, member2 = self.team_with_two_members() set_user_attribute_in_session(self.env, 'tz', 'GMT', member1.name) set_user_attribute_in_session(self.env, 'tz', 'GMT +10:00', member2.name) day = today(tz=member1.timezone()) tomorrow = day + timedelta(days=1) self.set_hours_for_day_on_team_member(9, tomorrow, member2) # member2s first hour of tomorrow should happen on today for member1 capacities = team.capacity(member1.timezone()).hourly_capacities_for_day(day) member1_last_hour = datetime.combine(day, time(23, tzinfo=member1.timezone())) self.assert_equals(member1_last_hour, capacities[-2].when) self.assert_equals(1, capacities[-2].capacity) def test_team_with_capacity_on_every_day_has_no_empty_days(self): team, member1 = self.team_with_one_member() member1.capacity = [1] * 7 member1.save() start = now() - timedelta(days=7) end = now() self.assert_length(0, team.capacity().days_without_capacity_in_interval(start, end)) def _create_team_with_weekends_off(self): team, member1 = self.team_with_one_member() member1.capacity = [1] * 5 + [0, 0] member1.save() return team def test_team_not_working_on_weekends_has_no_capacity_on_weekends(self): team = self._create_team_with_weekends_off() # exactly one week so we're sure that this interval covers two non-working days start = now() - timedelta(days=7) end = now() days_without_capacity = team.capacity().days_without_capacity_in_interval(start, end) self.assert_length(2, days_without_capacity) self.assert_equals(0, days_without_capacity[0].hour) self.assert_equals(0, days_without_capacity[0].minute) def test_days_without_capacity_respect_given_timestamp(self): team = self._create_team_with_weekends_off() end = now(utc) start = end - timedelta(days=7) bangkok_tz = get_timezone('GMT +7:00') days_without_capacity = team.capacity(bangkok_tz).days_without_capacity_in_interval(start, end) self.assert_equals(bangkok_tz, days_without_capacity[0].tzinfo) def test_days_without_capacity_include_last_day(self): team = self._create_team_with_weekends_off() end = datetime(2010, 5, 23, 3, 0, 0, tzinfo=localtz) # sunday start = end - timedelta(days=3) days_without_capacity = team.capacity().days_without_capacity_in_interval(start, end) self.assert_length(2, days_without_capacity) def _set_default_capacity_for_member(self, default_capacity, member): member.capacity = [default_capacity] * 7 member.save() def test_can_return_capacity_series_for_interval(self): team, member1 = self.team_with_one_member() self._set_default_capacity_for_member(1, member1) # tomorrow -> date -> 0:00 on date -> removes everything on that day self.assert_length(2*9 + 2, team.capacity().hourly_capacities_in_interval(yesterday(), tomorrow())) def test_return_capacity_information_until_end_of_sprint_even_if_last_values_are_zero(self): self.teh.disable_sprint_date_normalization() team, member = self.team_with_one_member() self._set_default_capacity_for_member(0, member) sprint = self.teh.create_sprint('Foo Sprint') sprint.start = midnight(yesterday()) sprint.end = midnight(day_after_tomorrow()) capacities = team.capacity().hourly_capacities_in_sprint(sprint) # +1 because the last item is from 22:00-23:00 self.assert_length(24*3+1, capacities) def test_can_sum_remaining_capacities_in_sprint(self): self.teh.disable_sprint_date_normalization() team, member = self.team_with_one_member() self._set_default_capacity_for_member(9*2, member) sprint = self.teh.create_sprint(name="SprintWithContingent", team=team, start=midnight(yesterday()), end=midnight(tomorrow())) summed = team.capacity().summed_hourly_capacities_in_sprint(sprint) # 2*9 (no. working hours), +8 (additional hours after zero), +1 (00:00-01:00 on the last day) self.assert_length(9*2+8+1, summed) self.assert_equals(2*(9*2), summed[0].capacity) self.assert_equals(0, summed[-1].capacity) def test_capacity_can_take_timezone_parameter(self): team, member = self.team_with_one_member() self._set_default_capacity_for_member(0, member) set_user_attribute_in_session(self.env, 'tz', 'GMT', member.name) self.set_hours_for_day_on_team_member(9, today(member.timezone()), member) viewer_timezone = get_timezone('GMT -12:00') capacitator = team.capacity(viewer_timezone) # Need to take the member timezone for start and end, to make sure that we really cut off all # workhours he has on the previous day - even though they would be on the current day when # viewed from the viewers timezone. hourly_capacities = capacitator.hourly_capacities_in_interval(today(tz=member.timezone()), tomorrow(tz=member.timezone())) self.assert_length(7, hourly_capacities) def test_contingents_are_removed_from_capacity(self): team, member = self.team_with_one_member() start = parse_date("2008-09-08T08:00:00") sprint = self.teh.create_sprint(name="SprintWithContingent", start=start, duration=10, team=team) self.teh.add_contingent_to_sprint('Contingent', 100, sprint) self._set_default_capacity_for_member(9, member) capacities = team.capacity().hourly_capacities_in_sprint(sprint) capacity = sum(map(lambda each: each.capacity, capacities)) self.assert_almost_equals(108 - 100, capacity, max_delta=.01) def test_will_contain_second_team_member_after_invalidating(self): team, member = self.team_with_one_member() self.assert_contains(member, team.members) member2 = self._add_member_to_team("member2", team) self.assert_not_contains(member2, team.members) team.invalidate_team_member_cache() self.assert_contains(member2, team.members) def test_can_invalidate_empty_cache(self): team = self.team_with_no_member() team.invalidate_team_member_cache() self.assert_length(0, team.members)
def __init__(self, *args, **kwargs): super(AgiloPreferences, self).__init__(*args, **kwargs) self.tmm = TeamModelManager(self.env) self.tmmm = TeamMemberModelManager(self.env)
class AgiloPreferences(Component): implements(IPreferencePanelProvider) def __init__(self, *args, **kwargs): super(AgiloPreferences, self).__init__(*args, **kwargs) self.tmm = TeamModelManager(self.env) self.tmmm = TeamMemberModelManager(self.env) #============================================================================= # IPreferencePanelProvider methods #============================================================================= def get_preference_panels(self, req): """Return a list of available preference panels. The items returned by this function must be tuple of the form `(panel, label)`. """ if req.authname is not None and req.authname != 'anonymous' and \ Role.TEAM_MEMBER in req.perm: yield ('team', _('Team')) def render_preference_panel(self, req, panel): """Process a request for a preference panel. This builds the Panel for the team preferences """ if req.method == 'POST': self._do_save(req) req.redirect(req.href.prefs(panel or None)) # Build the team_member object team_member = None if req.authname not in [None, 'anonymous'] and \ Role.TEAM_MEMBER in req.perm: name = req.authname team_member = self.tmmm.get(name=name) if team_member == None: team_member = self.tmmm.create(name=name) # Build the calendar for the current month calendars = list() ac = AgiloCalendar(day=datetime.today()) for cal in range(2): calendars.append(team_member.calendar.get_hours_for_interval(ac.get_first_day(), ac.get_last_day())) ac = ac.next_month() return 'agilo_prefs_%s.html' % (panel or 'general'), { 'settings': {'session': req.session, 'session_id': req.session.sid}, 'teams': self.tmm.select(), 'team_member': team_member, 'calendars': calendars, } def _do_save(self, req): """Saves the parameters into the object""" # Get the Team Member team_member = self.tmmm.get(name=req.authname) cal = team_member.calendar save_calendar = False for fieldname in sorted(req.args): value = req.args.get(fieldname) if (value is not None) and (team_member is not None): if fieldname == 'team': team = self.tmm.get(name=value) team_member.team = team elif fieldname.startswith('ts_'): if fieldname.startswith('ts_%s_' % team_member.name): ts_member, day = fieldname.rsplit('_', 1) try: cal.set_hours_for_day(float(value), d_ordinal=day) except ValueError: cal.set_hours_for_day(0.0, d_ordinal=day) if not save_calendar: save_calendar = True elif hasattr(team_member, fieldname): try: if float(value) != getattr(team_member, fieldname): setattr(team_member, fieldname, float(value)) except ValueError: setattr(team_member, fieldname, 0.0) team_member.save() # The member's capacity may have changed so we have to invalidate the # burndown ChartGenerator(self.env).invalidate_cache() if save_calendar: cal.save()
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))