def create(self, req, attributes): """ Create a structure milestone object.""" name = attributes.get('summary') try: if StructuredMilestone(self.env, name).exists: raise TracError('Milestone with name %s already exists' % name) except ResourceNotFound: pass milestone = StructuredMilestone(self.env) milestone.name = name req.perm.require('MILESTONE_CREATE', Resource(milestone.resource.realm)) milestone.description = attributes.get('description') milestone.ticket.populate(attributes) def set_date(name, attr_name=None): val = attributes.get(name) if val is None: return val = val and parse_date(val, tzinfo=req.tz) or None if attr_name is not None: setattr(milestone, attr_name, val) milestone.ticket[name] = val and str(to_timestamp(val)) set_date('duedate', 'due') set_date('completedate', 'completed') set_date('started') milestone.ticket.values['reporter'] = attributes.get( 'author') or get_reporter_id(req) milestone.insert() return self._milestone_as_dict(milestone)
def _resolve_milestone(self, name, include_kids, show_completed): def _flatten_and_get_names(mil, include_kids, show_completed): names= [] if mil: mil = isinstance(mil, StructuredMilestone) and [mil,] or mil for m in mil: if show_completed or not m.completed: names.append(m.name) if include_kids: names.extend(_flatten_and_get_names(m.kids, include_kids, show_completed)) return names if name=='nearest': db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute( 'SELECT name FROM milestone WHERE due>%s ORDER BY due LIMIT 1', \ (to_timestamp(datetime.now(utc)),)) row = cursor.fetchone() name=row and row[0] or 'none' elif name=='not_completed_milestones': return _flatten_and_get_names(StructuredMilestone.select(self.env, False), \ include_kids, show_completed) if name=='none': return '' try: mil = StructuredMilestone(self.env, name) names = _flatten_and_get_names(mil, include_kids, show_completed) if not names: names = mil.name return names except ResourceNotFound: return ''
def update(self, req, name, comment, attributes=None): """ Updates a structure milestone object.""" milestone = StructuredMilestone(self.env, name) if not milestone.exists: raise TracError('Milestone with name %s does not exist' % name) req.perm.require('MILESTONE_MODIFY', Resource(milestone.resource.realm)) if attributes is not None: milestone.name = attributes.get('summary') milestone.description = attributes.get('description') milestone.ticket.populate(attributes) def set_date(name, attr_name=None): val = attributes.get(name) if val is None: return val = val and parse_date(val, tzinfo=req.tz) or None if attr_name is not None: setattr(milestone, attr_name, val) milestone.ticket[name] = val and str(to_timestamp(val)) set_date('duedate', 'due') set_date('completedate', 'completed') set_date('started') milestone.save_changes(get_reporter_id(req, 'author'), comment) return self._milestone_as_dict(milestone)
def get(self, req, name): """ Get a structure milestone object.""" milestone = StructuredMilestone(self.env, name) req.perm.require('MILESTONE_VIEW', Resource(milestone.resource.realm)) if not milestone.exists: raise TracError('Milestone with name %s does not exist' % name) return self._milestone_as_dict(milestone)
def delete(self, req, name): """ Deletes structure milestone object.""" milestone = StructuredMilestone(self.env, name) if not milestone.exists: raise TracError('Milestone with name %s does not exist' % name) req.perm.require('MILESTONE_DELETE', Resource(milestone.resource.realm)) milestone.delete() return {'name': milestone.name, 'description': milestone.description}
def _get_job_done(self, mil_names, tkt_type=None, db=None): started_at = to_timestamp(datetime(tzinfo=localtz, \ *(StructuredMilestone(self.env, mil_names[0]).started.timetuple()[:3]))) db = db or self.env.get_db_cnx() cursor = db.cursor() base_sql = None params = list(mil_names) group_by = " GROUP BY t.id, t.type" final_statuses = IttecoEvnSetup(self.env).final_statuses status_params = ("%s," * len(final_statuses))[:-1] params = final_statuses + final_statuses + params if self.count_burndown_on == 'quantity': base_sql = """SELECT MAX(c.time), t.id, t.type, 1 FROM ticket t LEFT JOIN milestone m ON m.name=t.milestone LEFT OUTER JOIN ticket_change c ON t.id=c.ticket AND c.field='status' AND c.newvalue IN (%s) WHERE IN (%s) AND m.name IN (%s)""" % \ ( status_params, status_params, ("%s,"*len(mil_names))[:-1] ) else: base_sql = "SELECT MAX(c.time), t.id, t.type, "+db.cast(db.concat('0','tc.value'),'int')+ \ """FROM ticket t LEFT JOIN milestone m ON m.name=t.milestone LEFT JOIN ticket_custom tc ON t.id=tc.ticket AND tc.name=%%s LEFT OUTER JOIN ticket_change c ON t.id=c.ticket AND c.field='status' AND c.newvalue IN (%s) WHERE t.status IN (%s) AND m.name IN (%s)""" % \ ( status_params, status_params, ("%s,"*len(mil_names))[:-1] ) params = [self.count_burndown_on] + params group_by += ", tc.value" if tkt_type: if isinstance(tkt_type, basestring): base_sql += " AND t.type=%s" params += [tkt_type] else: base_sql += " AND t.type IN (%s)" % ("%s," * len(tkt_type))[:-1] params += list(tkt_type) cursor.execute(base_sql + group_by + " ORDER BY 1", params) data = [(to_datetime((dt < started_at) and started_at or dt), ttype, sum or 0) for dt, tkt_id, ttype, sum in cursor] return data
def _comment_milestone(self, req, mil_id): milestone = StructuredMilestone(self.env, mil_id) if not milestone.exists: raise ResourceNotFound('Milestone %s does not exist.' % mil_id, 'Invalid Milestone Name') req.perm.require('MILESTONE_MODIFY', milestone.resource) changes = TicketModule(self.env).rendered_changelog_entries( req, milestone.ticket) return 'itteco_milestone_comment.html', { 'milestone': milestone, 'changes': changes }, 'text/html'
def _render_link(self, context, name, label, extra=''): try: milestone = StructuredMilestone(self.env, name) except TracError: milestone = None # Note: the above should really not be needed, `Milestone.exists` # should simply be false if the milestone doesn't exist in the db # (related to #4130) href = context.href.milestone(name) if milestone and milestone.exists: if 'MILESTONE_VIEW' in context.perm(milestone.resource): closed = milestone.is_completed and 'closed ' or '' return tag.a(label, class_='%smilestone' % closed, href=href + extra) elif 'MILESTONE_CREATE' in context.perm('milestone', name): return tag.a(label, class_='missing milestone', href=href + extra, rel='nofollow') return tag.a(label, class_='missing milestone')
def milestone(self, req): mil_id = req.args.get('obj_id') if mil_id: req.perm.require('MILESTONE_MODIFY', Resource('milestone', mil_id)) else: req.perm.require('MILESTONE_CREATE') milestone = StructuredMilestone(self.env, mil_id) descriptor = WhiteboardModule(self.env).get_new_ticket_descriptor( [type.name for type in Type.select(self.env)], milestone.ticket.id) data = { 'structured_milestones': StructuredMilestone.select(self.env), 'new_ticket_descriptor': descriptor, 'milestone': milestone, 'action_controls': self._get_action_controls(req, descriptor['ticket']), } return 'itteco_milestone_quick_edit.html', data, 'text/html'
def process_request(self, req): milestone_id = req.args.get('id') req.perm('milestone', milestone_id).require('MILESTONE_VIEW') add_link(req, 'up', req.href.roadmap(), _('Roadmap')) db = self.env.get_db_cnx() # TODO: db can be removed milestone = StructuredMilestone(self.env, milestone_id, db) action = req.args.get('action', 'view') if req.method == 'POST': if req.args.has_key('cancel'): req.redirect(req.href.roadmap()) elif action == 'delete': self._do_delete(req, db, milestone) return self._do_save(req, db, milestone) elif action in ('new', 'edit', 'view'): return self._render_editor(req, db, milestone) elif action == 'delete': return self._render_confirm(req, db, milestone) req.redirect(req.href.roadmap())
def metrics(self, milestone): mil = StructuredMilestone(self.env, milestone) if not mil.is_started: return None scope = self._milestone_scope(mil) scope_types = IttecoEvnSetup(self.env).scope_element types = self._work_types() one_day_delta = timedelta(1) metrics = [{ 'datetime': mil.started, 'burndown': scope, 'burnup': [0] * len(types) }] def clone_item(item, new_time): item = deepcopy(item) item['datetime'] = new_time metrics.append(item) return item for ts, ttype, sum in self._burnup_info(mil): last = metrics[-1] if ts != last['datetime']: if self.fill_idle_days: time_delta = ts - last['datetime'] if time_delta.days > 1: last = clone_item(last, ts - one_day_delta) last = clone_item(last, ts) if ttype in self.initial_plan_element: #this ticket type should influence burndown last['burndown'] -= sum elif ttype not in scope_types: #burnup by ticket type turned around axis X, in order to look like burndown idx = types.index(ttype) last['burnup'][idx] += sum calculated_end_date = None if len(metrics) > 1: # do we have any progress? start = metrics[0] end = metrics[-1] if start['burndown'] != end['burndown']: # do we have any progress to perform approximations? start_ts = to_timestamp(start['datetime']) end_ts = to_timestamp(end['datetime']) calc_ts = start_ts+ \ start['burndown']*(end_ts - start_ts)/ \ (start['burndown']-end['burndown']) end['approximation'] = end['burndown'] calculated_end_date = to_datetime(calc_ts) if mil.due: #if milestone is fully planned, add line of ideal progress if mil.due > metrics[-1]['datetime']: metrics.append({'datetime': mil.due}) metrics[0]['ideal'] = scope metrics[-1]['ideal'] = 0 if calculated_end_date: if calculated_end_date > metrics[-1]['datetime']: metrics.append({ 'datetime': calculated_end_date, 'approximation': 0 }) else: #let's find a correct place on timeline for calculated end date for i in xrange(0, len(metrics)): metric = metrics[i] if metric['datetime'] >= calculated_end_date: if metric['datetime'] > calculated_end_date: metric = {'datetime': calculated_end_date} metrics.insert(i, metric) metric['approximation'] = 0 break return metrics, types
def _chart_settings(self, milestone): burndown_info = self.burndown_info_provider.metrics(milestone) mils =[] def flatten(mil): mils.append(mil) for kid in mil.kids: flatten(kid) flatten(StructuredMilestone(self.env, milestone)) fmt_date = lambda x: format_datetime(x, '%Y-%m-%dT%H:%M:%S') cvs_data = graphs = events = None if burndown_info: metrics, graphs = burndown_info def get_color(tkt_type): tkt_cfg = self.ticket_type_config if tkt_cfg: cfg = tkt_cfg.get(tkt_type) if cfg: return cfg.get('max_color') graphs = [{'name': graph, 'color': get_color(graph)} for graph in graphs] burndown_cvs_data = burnup_cvs_data = [] keys = ['burndown', 'approximation', 'ideal'] milestone_dates= dict([(mil.completed or mil.due, mil) for mil in mils ]) events =[] prev_burndown = metrics[0]['burndown'] prev_burnup = 0 def genitems(metric): yield fmt_date(metric['datetime']) for key in keys: yield str(metric.get(key,'')) for metric in metrics: ts = metric['datetime'] line = ",".join(genitems(metric)) burnup_sum = 0 burnup = metric.get('burnup',[]) for item in burnup: burnup_sum -= item line +=','+str(-1*item) if burnup: line +=','+str(burnup_sum) burndown_cvs_data.append(line) if ts in milestone_dates: mil = milestone_dates[ts] if mil.is_completed: del milestone_dates[ts] burndown = metric['burndown'] events.append({'datetime': fmt_date(mil.completed), 'extended': True, 'text': '"%s" completed\nBurndown delta %d\nBurnup delta %d.' \ % (mil.name, prev_burndown-burndown, prev_burnup-burnup_sum) , 'url': self.env.abs_href('milestone',mil.name)}) burndown_delta =0 prev_burnup = burnup_sum prev_burndown = burndown events.extend([{'datetime': fmt_date(mil.due), 'text': '"%s" is planned to be completed.' % mil.name , 'url': self.env.abs_href('milestone',mil.name)} for mil in milestone_dates.itervalues()]) cvs_data = "<![CDATA["+"\n".join(burndown_cvs_data)+"]]>" data = {'data': cvs_data, 'graphs': graphs, 'events': events} return 'iiteco_chart_settings.xml', data, 'text/xml'
def _do_save(self, req, db, milestone): if milestone.exists: req.perm(milestone.resource).require('MILESTONE_MODIFY') else: req.perm(milestone.resource).require('MILESTONE_CREATE') ticket_module = TicketModule(self.env) ticket_module._populate(req, milestone.ticket, False) if not milestone.exists: reporter_id = get_reporter_id(req, 'author') milestone.ticket.values['reporter'] = reporter_id action = req.args.get('action', 'leave') field_changes, problems = ticket_module.get_ticket_changes( req, milestone.ticket, action) if problems: for problem in problems: add_warning(req, problem) add_warning( req, tag( tag.p('Please review your configuration, ' 'probably starting with'), tag.pre('[trac]\nworkflow = ...\n'), tag.p('in your ', tag.tt('trac.ini'), '.'))) ticket_module._apply_ticket_changes(milestone.ticket, field_changes) old_name = milestone.name new_name = milestone.ticket['summary'] milestone.name = new_name milestone.description = milestone.ticket['description'] due = req.args.get('duedate', '') milestone.due = due and parse_date(due, tzinfo=req.tz) or None milestone.ticket['duedate'] = milestone.due and str( to_timestamp(milestone.due)) or None completed = req.args.get('completedate', '') retarget_to = req.args.get('target') # Instead of raising one single error, check all the constraints and # let the user fix them by going back to edit mode showing the warnings warnings = [] def warn(msg): add_warning(req, msg) warnings.append(msg) # -- check the name if new_name: if new_name != old_name: # check that the milestone doesn't already exists # FIXME: the whole .exists business needs to be clarified # (#4130) and should behave like a WikiPage does in # this respect. try: other_milestone = StructuredMilestone( self.env, new_name, db) warn( _( 'Milestone "%(name)s" already exists, please ' 'choose another name', name=new_name)) except ResourceNotFound: pass else: warn(_('You must provide a name for the milestone.')) # -- check completed date if action in MilestoneSystem(self.env).starting_action: milestone.ticket['started'] = str(to_timestamp(datetime.now(utc))) if action in MilestoneSystem(self.env).completing_action: milestone.completed = datetime.now(utc) if warnings: return self._render_editor(req, db, milestone) # -- actually save changes if milestone.exists: cnum = req.args.get('cnum') replyto = req.args.get('replyto') internal_cnum = cnum if cnum and replyto: # record parent.child relationship internal_cnum = '%s.%s' % (replyto, cnum) now = datetime.now(utc) milestone.save_changes(get_reporter_id(req, 'author'), req.args.get('comment'), when=now, cnum=internal_cnum) # eventually retarget opened tickets associated with the milestone if 'retarget' in req.args and completed: cursor = db.cursor() cursor.execute( "UPDATE ticket SET milestone=%s WHERE " "milestone=%s and status != 'closed'", (retarget_to, old_name)) self.env.log.info('Tickets associated with milestone %s ' 'retargeted to %s' % (old_name, retarget_to)) else: milestone.insert() db.commit() add_notice(req, _('Your changes have been saved.')) jump_to = req.args.get('jump_to', 'roadmap') if jump_to == 'roadmap': req.redirect(req.href.roadmap()) elif jump_to == 'whiteboard': req.redirect( req.href.whiteboard('team_tasks') + '#' + milestone.name) else: req.redirect(req.href.milestone(milestone.name))
def _render_admin_panel(self, req, cat, page, milestone): req.perm.require('TICKET_ADMIN') add_stylesheet(req, 'itteco/css/common.css') add_jscript( req, [ 'stuff/ui/ui.core.js', 'stuff/ui/ui.resizable.js', 'custom_select.js' ], IttecoEvnSetup(self.env).debug ) # Detail view? if milestone: mil = StructuredMilestone(self.env, milestone) if req.method == 'POST': if req.args.get('save'): mil.name = req.args.get('name') mil.due = mil.completed = None due = req.args.get('duedate', '') if due: mil.due = parse_date(due, req.tz) if req.args.get('completed', False): completed = req.args.get('completeddate', '') mil.completed = parse_date(completed, req.tz) if mil.completed > datetime.now(utc): raise TracError(_('Completion date may not be in ' 'the future'), _('Invalid Completion Date')) mil.description = req.args.get('description', '') mil.parent = req.args.get('parent', None) if mil.parent and mil.parent==mil.name: raise TracError(_('Milestone cannot be parent for itself,Please, give it another thought.'), _('Something is wrong with Parent Milestone. Will you check it please?')) if mil.parent and not StructuredMilestone(self.env, mil.parent).exists: raise TracError(_('Milestone should have a valid parent. It does not look like this is the case.'), _('Something is wrong with Parent Milestone. Will you check it please?')) mil.update() req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) add_script(req, 'common/js/wikitoolbar.js') data = {'view': 'detail', 'milestone': mil} else: if req.method == 'POST': # Add Milestone if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: StructuredMilestone(self.env, name) except ResourceNotFound: mil = StructuredMilestone(self.env) mil.name = name if req.args.get('duedate'): mil.due = parse_date(req.args.get('duedate'), req.tz) mil.parent = req.args.get('parent', None) if mil.parent and not StructuredMilestone(self.env, mil.parent).exists: raise TracError(_('Milestone should have a valid parent. It does not look like this is the case'), _('Something is wrong with Parent Milestone. Will you check it please?')) mil.insert() req.redirect(req.href.admin(cat, page)) else: raise TracError(_('Sorry, milestone %s already exists.') % name) # Remove milestone elif req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('Please, select the milestone.')) if not isinstance(sel, list): sel = [sel] db = self.env.get_db_cnx() for name in sel: mil = StructuredMilestone(self.env, name, db=db) mil.delete(db=db) db.commit() req.redirect(req.href.admin(cat, page)) # Set default milestone elif req.args.get('apply'): if req.args.get('default'): name = req.args.get('default') self.config.set('ticket', 'default_milestone', name) self.config.save() req.redirect(req.href.admin(cat, page)) data = { 'view': 'list', 'default': self.config.get('ticket', 'default_milestone'), } # Get ticket count db = self.env.get_db_cnx() cursor = db.cursor() milestones = [] structured_milestones = StructuredMilestone.select(self.env) mil_names = self._get_mil_names(structured_milestones) cursor.execute("SELECT milestone, COUNT(*) FROM ticket " "WHERE milestone IN (%s) GROUP BY milestone" % ("%s,"*len(mil_names))[:-1], mil_names) mil_tkt_quantity = {} for mil, cnt in cursor: mil_tkt_quantity[mil]=cnt data.update({ 'date_hint': get_date_format_hint(), 'milestones': [(mil, 0) for mil in structured_milestones],# we recover this anyway 'structured_milestones': structured_milestones, 'milestone_tickets_quantity': mil_tkt_quantity, 'max_milestone_level': self.milestone_levels and len(self.milestone_levels)-1 or 0, 'datetime_hint': get_datetime_format_hint() }) return 'itteco_admin_milestones.html', data