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 _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 ticket_deleted(self, ticket): if self.has_ticket_refs(ticket): self.log.debug("TicketRefsPlugin: ticket are deleting") links = TicketLinks(self.env, ticket) try: links.delete() except Exception, err: self.log.error("TicketRefsPlugin: ticket_deleted %s" % err) self.log.debug("TicketRefsPlugin: ticket are deleted")
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 ticket_created(self, ticket): links = None if self.has_ticket_refs(ticket): self.log.debug("TicketRefsPlugin: ticket are creating") if not links: links = TicketLinks(self.env, ticket) try: links.create() except Exception, err: self.log.error("TicketRefsPlugin: ticket_created %s" % err) self.log.debug("TicketRefsPlugin: ticket are created")
def ticket_changed(self, tkt, comment, author, old_values): db = self.env.get_db_cnx() links = TicketLinks(self.env, tkt, db) old_relations = {'blocking': set([]), 'blockedby': set([])} if "blocking" in old_values: old_relations['blocking'] = extract_ticket_ids(old_values['blocking']) if "blockedby" in old_values: old_relations['blockedby'] = extract_ticket_ids(old_values['blockedby']) links.save(old_relations, author, comment, tkt.time_changed, db) db.commit()
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_ids, label_summary=0): g = graphviz.Graph() g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] links = TicketLinks.walk_tickets(self.env, tkt_ids) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s'%tkt.id node['fillcolor'] = tkt['status'] == 'closed' and self.closed_color or self.opened_color 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 _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph() g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] links = TicketLinks.walk_tickets(self.env, tkt_ids) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt[ 'status'] == 'closed' and self.closed_color or self.opened_color 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 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 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 ticket_changed(self, ticket, comment, author, old_values): links = None need_change = TICKETREF in old_values #com_refs = get_refs_in_comment(comment, [ticket.id]) #if com_refs: # self.log.debug("TicketRefsPlugin: refs in comment %s" % ( # str(com_refs))) # links = TicketLinks(self.env, ticket) # links.add_reference(com_refs) # need_change = True if need_change: self.log.debug("TicketRefsPlugin: ticket are changing") if not links: links = TicketLinks(self.env, ticket) try: print "will call ticketLinks.change" links.change(author, old_values.get(TICKETREF)) except Exception, err: self.log.error("TicketRefsPlugin: ticket_changed %s" % err) self.log.debug("TicketRefsPlugin: ticket are changed")
def validate_ticket(self, req, ticket): db = self.env.get_db_cnx() cursor = db.cursor() id = unicode(ticket.id) links = TicketLinks(self.env, ticket, db) links.blocking = extract_ticket_ids(ticket['blocking'] or '') links.blocked_by = extract_ticket_ids(ticket['blockedby'] or '') # Check that ticket does not have itself as a blocker if id 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 id in ids[:]: cursor.execute('SELECT id FROM ticket WHERE id=%s', (id,)) row = cursor.fetchone() if row is None: ids.remove(id) ticket[field] = ', '.join(sorted(ids, key=lambda x: int(x))) except Exception, e: self.log.debug('TicketRelations: Error parsing %s "%s": %s', field, ticket[field], e) yield field, 'Not a valid list of ticket IDs'
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 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 _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph(log=self.log) g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for tid in tkt_ids: g[tid] if self.show_key: g[-1]['label'] = self.closed_text g[-1]['fillcolor'] = self.closed_color g[-1]['shape'] = 'box' g[-2]['label'] = self.opened_text g[-2]['fillcolor'] = self.opened_color g[-2]['shape'] = 'box' links = TicketLinks.walk_tickets(self.env, tkt_ids, self.full_graph) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt['status'] == 'closed' and \ self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = escape(tkt['summary']) if self.highlight_target and tkt.id in tkt_ids: node['penwidth'] = 3 for n in link.blocking: node > g[n] return g
def _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph(log=self.log) g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] if self.show_key: g[-1]['label'] = self.closed_text g[-1]['fillcolor'] = self.closed_color g[-1]['shape'] = 'box' g[-2]['label'] = self.opened_text g[-2]['fillcolor'] = self.opened_color g[-2]['shape'] = 'box' links = TicketLinks.walk_tickets(self.env, tkt_ids, full=self.full_graph) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt['status'] == 'closed' and self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = escape(tkt['summary']) if self.highlight_target and tkt.id in tkt_ids: node['penwidth'] = 3 for n in link.blocking: node > g[n] return g
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 _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 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 _build_graph(self, req, tkt_ids, label_summary=0, with_clusters=False): g = graphviz.Graph() g.label_summary = label_summary g.attributes.update({ 'rankdir': self.graph_direction, }) node_default = g['node'] node_default.update({ 'style': 'filled', 'fontsize': 11, 'fontname': 'Arial', 'shape': 'box' if label_summary else 'ellipse', 'target': '_blank', }) edge_default = g['edge'] edge_default['style'] = '' width = 20 def q(text): return textwrap.fill(text, width).replace('"', '\\"').replace('\n', '\\n') def create_node(tkt): node = g.get_node(tkt.id) summary = q(tkt['summary']) if label_summary: node['label'] = u'#%s %s' % (tkt.id, summary) else: node['label'] = u'#%s'%tkt.id if tkt['status'] == 'closed': color = tkt['resolution'] in bc_resolutions and self.bad_closed_color or self.closed_color else: color = self.opened_color node['fillcolor'] = color node['URL'] = req.href.ticket(tkt.id) node['alt'] = _('Ticket #%(id)s', id=tkt.id) node['tooltip'] = summary.replace('\\n', ' ') return node ticket_cache = {} if with_clusters: milestone_tkt_ids = sorted(tkt_ids) tkt_ids = [] tickets = {} # { <milestone_name or None>: (<cluster or graph>, <tkt_ids list>), ... } ticket_milestones = {} # { <tkt_id>: <milestone> } m_idx = 0 for milestone, mtkt_ids in itertools.groupby(milestone_tkt_ids, lambda p: p[0]): ids = [p[1] for p in mtkt_ids] if milestone: m_idx += 1 url = req.href.depgraph(get_resource_url(self.env, Resource('milestone', milestone, pid=req.data['project_id']))) tickets[milestone] = (g.create_cluster( u'cluster%s' % m_idx, label=q(milestone), href=url, target='_blank'), ids) else: milestone = None tickets[None] = (g, ids) tkt_ids.extend(ids) for tkt_id in ids: ticket_milestones[tkt_id] = milestone else: # Init nodes for resource tickets on graph top for id in tkt_ids: g[id] bc_resolutions = self.bad_closed_resolutions.syllabus(req.data['syllabus_id']) links = TicketLinks.walk_tickets(self.env, tkt_ids, ticket_cache) links = sorted(links, key=lambda link: link.tkt.id) for link in links: node = create_node(link.tkt) if with_clusters: milestone_from = link.tkt['milestone'] or None stor = tickets[milestone_from][0] stor[link.tkt.id] # include node for n in link.blocking: milestone_to = ticket_milestones[n] if milestone_from == milestone_to: stor.add(node > stor[n]) # save edge in same cluster else: g.add(node > tickets[milestone_to][0][n]) # save edge in global graph else: g[link.tkt.id] for n in link.blocking: g.add(node > g[n]) return g
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