def validate_ticket(self, req, ticket): if req.args.get('action') == 'resolve' and req.args.get( 'action_resolve_resolve_resolution') == 'fixed': links = TicketLinks(self.env, ticket) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': yield None, 'Ticket #%s is blocking this ticket' % i
def _build_graph(self, req, tkt_id): links = TicketLinks(self.env, tkt_id) g = graphviz.Graph() node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph g[tkt_id] links = sorted(links.walk(), key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt['status'] == 'closed' and 'green' or 'red' node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = tkt['summary'] for n in link.blocking: node > g[n] return g
def _prepare_links(self, tkt, db): links = TicketLinks(self.env, tkt, db) links.blocking = set( int(n) for n in self.NUMBERS_RE.findall(tkt['blocking'] or '')) links.blocked_by = set( int(n) for n in self.NUMBERS_RE.findall(tkt['blockedby'] or '')) return links
def post_process_request(self, req, template, data, content_type): if req.path_info.startswith('/ticket/'): # In case of an invalid ticket, the data is invalid if not data: return template, data, content_type tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/disable_resolve.js') break # Add link to depgraph if needed if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph('ticket', tkt.id)) for change in data.get('changes', {}): if not change.has_key('fields'): continue for field, field_data in change['fields'].iteritems(): if field in self.fields: if field_data['new'].strip(): new = set( [int(n) for n in field_data['new'].split(',')]) else: new = set() if field_data['old'].strip(): old = set( [int(n) for n in field_data['old'].split(',')]) else: old = set() add = new - old sub = old - new elms = tag() if add: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(add)]))) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(sub)]))) elms.append(u' removed') field_data['rendered'] = elms #add a link to generate a dependency graph for all the tickets in the milestone if req.path_info.startswith('/milestone/'): if not data: return template, data, content_type milestone = data['milestone'] add_ctxtnav(req, 'Depgraph', req.href.depgraph('milestone', milestone.name)) return template, data, content_type
def ticket_deleted(self, tkt): db = self.env.get_db_cnx() links = TicketLinks(self.env, tkt, db) links.blocking = set() links.blocked_by = set() links.save('trac', 'Ticket #%s deleted'%tkt.id, when=None, db=db) db.commit()
def ticket_changed(self, tkt, comment, author, old_values): db = self.env.get_db_cnx() links = TicketLinks(self.env, tkt, db) links.blocking = set(self.NUMBERS_RE.findall(tkt['blocking'] or '')) links.blocked_by = set(self.NUMBERS_RE.findall(tkt['blockedby'] or '')) links.save(author, comment, tkt.time_changed, db) db.commit()
def post_process_request(self, req, template, data, content_type): if req.path_info.startswith('/ticket/'): tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/disable_resolve.js') break data['mastertickets'] = { 'field_values': { 'blocking': linkify_ids(self.env, req, links.blocking), 'blockedby': linkify_ids(self.env, req, links.blocked_by), }, } # Add link to depgraph if needed if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id)) for change in data.get('changes', []): for field, field_data in change['fields'].iteritems(): if field in self.fields: if field_data['new'].strip(): new = set( [int(n) for n in field_data['new'].split(',')]) else: new = set() if field_data['old'].strip(): old = set( [int(n) for n in field_data['old'].split(',')]) else: old = set() add = new - old sub = old - new elms = tag() if add: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(add)]))) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(sub)]))) elms.append(u' removed') field_data['rendered'] = elms return template, data, content_type
def process_request(self, req): path_info = req.path_info[10:] if not path_info: raise TracError('No ticket specified') tkt_id = path_info.split('/', 1)[0] g = self._build_graph(req, tkt_id) if '/' in path_info or 'format' in req.args: format = req.args.get('format') if format == 'text': req.send(str(g), 'text/plain') elif format == 'debug': import pprint req.send(pprint.pformat(TicketLinks(self.env, tkt_id)), 'text/plain') elif format is not None: req.send(g.render(self.dot_path, format), 'text/plain') if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-o', '%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} tkt = Ticket(self.env, tkt_id) data['tkt'] = tkt data['graph'] = g data['graph_render'] = partial(g.render, self.dot_path) data['use_gs'] = self.use_gs add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id, req.href.ticket(tkt_id)) return 'depgraph.html', data, None
def validate_ticket(self, req, ticket): db = self.env.get_db_cnx() cursor = db.cursor() tid = ticket.id links = self._prepare_links(ticket, db) if req.args.get('action') == 'resolve' and req.args.get( 'action_resolve_resolve_resolution') == 'fixed': for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': yield None, 'Ticket #%s is blocking this ticket' % i # Check that ticket does not have itself as a blocker if tid in links.blocking | links.blocked_by: yield 'blocked_by', 'This ticket is blocking itself' return # Check that there aren't any blocked_by in blocking or their parents blocking = links.blocking.copy() while len(blocking) > 0: if len(links.blocked_by & blocking) > 0: yield 'blocked_by', 'This ticket has circular dependencies' return new_blocking = set() for link in blocking: tmp_tkt = Ticket(self.env, link) new_blocking |= TicketLinks(self.env, tmp_tkt, db).blocking blocking = new_blocking for field in ('blocking', 'blockedby'): try: ids = self.NUMBERS_RE.findall(ticket[field] or '') for tid in ids[:]: cursor.execute('SELECT id FROM ticket WHERE id=%s', (tid, )) row = cursor.fetchone() if row is None: ids.remove(tid) ticket[field] = ', '.join(sorted(ids, key=lambda x: int(x))) except Exception, e: self.log.debug('MasterTickets: Error parsing %s "%s": %s', field, ticket[field], e) yield field, 'Not a valid list of ticket IDs'
def process_request(self, req): realm = req.args['realm'] id_ = req.args['id'] if not which(self.dot_path): raise TracError( _("Path to dot executable is invalid: %(path)s", path=self.dot_path)) # Urls to generate the depgraph for a ticket is /depgraph/ticketnum # Urls to generate the depgraph for a milestone is # /depgraph/milestone/milestone_name # List of tickets to generate the depgraph. if realm == 'milestone': # We need to query the list of tickets in the milestone query = Query(self.env, constraints={'milestone': [id_]}, max=0) tkt_ids = [fields['id'] for fields in query.execute(req)] else: tid = as_int(id_, None) if tid is None: raise TracError( tag_("%(id)s is not a valid ticket id.", id=html.tt(id_))) tkt_ids = [tid] # The summary argument defines whether we place the ticket id or # its summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if req.path_info.endswith('/depgraph.png') or 'format' in req.args: format_ = req.args.get('format') if format_ == 'text': # In case g.__str__ returns unicode, convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format_ == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format_ is not None: if format_ in self.acceptable_formats: req.send(g.render(self.dot_path, format_), 'text/plain') else: raise TracError( _("The %(format)s format is not allowed.", format=format_)) if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} # Add a context link to enable/disable labels in nodes. if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if realm == 'milestone': add_ctxtnav(req, 'Back to Milestone: %s' % id_, req.href.milestone(id_)) data['milestone'] = id_ else: data['ticket'] = id_ add_ctxtnav(req, 'Back to Ticket #%s' % id_, req.href.ticket(id_)) data['graph'] = g data['graph_render'] = functools.partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None
def process_request(self, req): path_info = req.path_info[10:] if not path_info: raise TracError('No ticket specified') #list of tickets to generate the depgraph for tkt_ids = [] milestone = None split_path = path_info.split('/', 2) #Urls to generate the depgraph for a ticket is /depgraph/ticketnum #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name if split_path[0] == 'milestone': #we need to query the list of tickets in the milestone milestone = split_path[1] query = Query(self.env, constraints={'milestone': [milestone]}, max=0) tkt_ids = [fields['id'] for fields in query.execute()] else: #the list is a single ticket tkt_ids = [int(split_path[0])] #the summary argument defines whether we place the ticket id or #it's summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if path_info.endswith('/depgraph.png') or 'format' in req.args: format = req.args.get('format') if format == 'text': #in case g.__str__ returns unicode, we need to convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format is not None: req.send(g.render(self.dot_path, format), 'text/plain') if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} #add a context link to enable/disable labels in nodes if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if milestone is None: tkt = Ticket(self.env, tkt_ids[0]) data['tkt'] = tkt add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id, req.href.ticket(tkt.id)) else: add_ctxtnav(req, 'Back to Milestone %s' % milestone, req.href.milestone(milestone)) data['milestone'] = milestone data['graph'] = g data['graph_render'] = partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None