def __get__(self, instance, owner): if instance is None: return self order = ListOption.__get__(self, instance, owner) components = [] implementing_classes = [] for impl in self.xtnpt.extensions(instance): implementing_classes.append(impl.__class__.__name__) if self.include_missing or impl.__class__.__name__ in order: components.append(impl) not_found = sorted(set(order) - set(implementing_classes)) if not_found: raise ConfigurationError( tag_("Cannot find implementation(s) of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.code(self.xtnpt.interface.__name__), implementation=tag( (', ' if idx != 0 else None, tag.code(impl)) for idx, impl in enumerate(not_found)), option=tag.code("[%s] %s" % (self.section, self.name)))) def key(impl): name = impl.__class__.__name__ if name in order: return 0, order.index(name) else: return 1, components.index(impl) return sorted(components, key=key)
def expand_macro(self, formatter, name, content): from trac.mimeview.api import Mimeview mime_map = Mimeview(self.env).mime_map mime_type_filter = '' args, kw = parse_args(content) if args: mime_type_filter = args.pop(0).strip().rstrip('*') mime_types = {} for key, mime_type in mime_map.iteritems(): if (not mime_type_filter or mime_type.startswith(mime_type_filter) ) and key != mime_type: mime_types.setdefault(mime_type, []).append(key) return tag.div(class_='mimetypes')( tag.table(class_='wiki')( tag.thead( tag.tr( tag.th(_("MIME Types")), # always use plural tag.th( tag.a("WikiProcessors", href=formatter.context.href.wiki( 'WikiProcessors'))))), tag.tbody( tag.tr( tag.th(tag.code(mime_type), style="text-align: left"), tag.td( tag.code(' '.join(sorted(mime_types[mime_type]))))) for mime_type in sorted(mime_types))))
def _arg_as_int(val, key=None, min=None, max=None): int_val = as_int(val, None, min=min, max=max) if int_val is None: raise MacroError(tag_("Invalid macro argument %(expr)s", expr=tag.code("%s=%s" % (key, val)) if key else tag.code(val))) return int_val
def expand_macro(self, formatter, name, content): from trac.wiki.formatter import system_message content = content.strip() if content else '' name_filter = content.strip('*') def get_macro_descr(): for macro_provider in formatter.wiki.macro_providers: names = list(macro_provider.get_macros() or []) if name_filter and not any( name.startswith(name_filter) for name in names): continue try: name_descriptions = [ (name, macro_provider.get_macro_description(name)) for name in names ] except Exception as e: yield system_message( _("Error: Can't get description for macro %(name)s", name=names[0]), e), names else: for descr, pairs in groupby(name_descriptions, key=lambda p: p[1]): if descr: if isinstance(descr, (tuple, list)): descr = dgettext(descr[0], to_unicode(descr[1])) \ if descr[1] else '' else: descr = to_unicode(descr) or '' if content == '*': descr = format_to_oneliner(self.env, formatter.context, descr, shorten=True) else: descr = format_to_html(self.env, formatter.context, descr) yield descr, [name for name, descr in pairs] return tag.div(class_='trac-macrolist')( (tag.h3(tag.code('[[', names[0], ']]'), id='%s-macro' % names[0]), len(names) > 1 and tag.p( tag.strong(_("Aliases:")), [tag.code(' [[', alias, ']]') for alias in names[1:]]) or None, description or tag.em(_("Sorry, no documentation found"))) for description, names in sorted(get_macro_descr(), key=lambda item: item[1][0]))
def send(self, from_addr, recipients, message): global local_hostname # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port, local_hostname) local_hostname = server.local_hostname except smtplib.socket.error as e: raise ConfigurationError( tag_( "SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError( _("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time_now() server.sendmail(from_addr, recipients, message) t = time_now() - start if t > 5: self.log.warning( "Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit()
def __get__(self, instance, owner): if instance is None: return self value = Option.__get__(self, instance, owner) for impl in self.xtnpt.extensions(instance): if impl.__class__.__name__ == value: return impl raise ConfigurationError( tag_("Cannot find an implementation of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.code(self.xtnpt.interface.__name__), implementation=tag.code(value), option=tag.code("[%s] %s" % (self.section, self.name))))
def send(self, from_addr, recipients, message): # Use native line endings in message message = fix_eol(message, os.linesep) self.log.info("Sending notification through sendmail at %s to %s", self.sendmail_path, recipients) cmdline = [self.sendmail_path, '-i', '-f', from_addr] + recipients self.log.debug("Sendmail command line: %s", cmdline) try: child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds) except OSError as e: raise ConfigurationError( tag_( "Sendmail error (%(error)s). Please modify %(option)s " "in your configuration.", error=to_unicode(e), option=tag.code("[notification] sendmail_path"))) out, err = child.communicate(message) if child.returncode or err: raise Exception("Sendmail failed with (%s, %s), command: '%s'" % (child.returncode, err.strip(), cmdline))
def default_cell(option): default = option.default if default is not None and default != '': return tag.td(tag.code(option.dumps(default)), class_='default') else: return tag.td(_("(no default)"), class_='nodefault')
def _invalid_db_str(db_str): return ConfigurationError( tag_( "Invalid format %(db_str)s for the database connection string. " "Please refer to the %(doc)s for help.", db_str=tag.code(db_str), doc=_doc_db_str()))
def process_request(self, req): parent_realm = req.args.get('realm') path = req.args.get('path') if not parent_realm or not path: raise HTTPBadRequest(_("Bad request")) if parent_realm == 'attachment': raise TracError( tag_("%(realm)s is not a valid parent realm", realm=tag.code(parent_realm))) parent_realm = Resource(parent_realm) action = req.args.get('action', 'view') if action == 'new': parent_id, filename = path.rstrip('/'), None else: last_slash = path.rfind('/') if last_slash == -1: parent_id, filename = path, '' else: parent_id, filename = path[:last_slash], path[last_slash + 1:] parent = parent_realm(id=parent_id) if not resource_exists(self.env, parent): raise ResourceNotFound( _("Parent resource %(parent)s doesn't exist", parent=get_resource_name(self.env, parent))) # Link the attachment page to parent resource parent_name = get_resource_name(self.env, parent) parent_url = get_resource_url(self.env, parent, req.href) add_link(req, 'up', parent_url, parent_name) add_ctxtnav(req, _("Back to %(parent)s", parent=parent_name), parent_url) if not filename: # there's a trailing '/' if req.args.get('format') == 'zip': self._download_as_zip(req, parent) elif action != 'new': return self._render_list(req, parent) attachment = Attachment(self.env, parent.child(self.realm, filename)) if req.method == 'POST': if action == 'new': data = self._do_save(req, attachment) elif action == 'delete': self._do_delete(req, attachment) else: raise HTTPBadRequest(_("Invalid request arguments.")) elif action == 'delete': data = self._render_confirm_delete(req, attachment) elif action == 'new': data = self._render_form(req, attachment) else: data = self._render_view(req, attachment) add_stylesheet(req, 'common/css/code.css') return 'attachment.html', data
def __init__(self, path, log=None, params={}): self.cnx = None if path != ':memory:': if not os.access(path, os.F_OK): raise ConfigurationError( _('Database "%(path)s" not found.', path=path)) dbdir = os.path.dirname(path) if not os.access(path, os.R_OK + os.W_OK) or \ not os.access(dbdir, os.R_OK + os.W_OK): raise ConfigurationError( tag_( "The user %(user)s requires read _and_ write permissions " "to the database file %(path)s and the directory it is " "located in.", user=tag.code(getuser()), path=tag.code(path))) self._active_cursors = weakref.WeakKeyDictionary() timeout = int(params.get('timeout', 10.0)) self._eager = params.get('cursor', 'eager') == 'eager' # eager is default, can be turned off by specifying ?cursor= if isinstance(path, unicode): # needed with 2.4.0 path = path.encode('utf-8') cnx = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES, isolation_level=None, check_same_thread=sqlite_version < (3, 3, 1), timeout=timeout) # load extensions extensions = params.get('extensions', []) if len(extensions) > 0: cnx.enable_load_extension(True) for ext in extensions: cnx.load_extension(ext) cnx.enable_load_extension(False) with closing(cnx.cursor()) as cursor: _set_journal_mode(cursor, params.get('journal_mode')) set_synchronous(cursor, params.get('synchronous')) cnx.isolation_level = 'DEFERRED' ConnectionWrapper.__init__(self, cnx, log)
def _get_valid_default_handler(self, req): # Use default_handler from the Session if it is a valid value. name = req.session.get('default_handler') handler = self._request_handlers.get(name) if handler and not is_valid_default_handler(handler): handler = None if not handler: # Use default_handler from project configuration. handler = self.default_handler if not is_valid_default_handler(handler): raise ConfigurationError( tag_("%(handler)s is not a valid default handler. Please " "update %(option)s through the %(page)s page or by " "directly editing trac.ini.", handler=tag.code(handler.__class__.__name__), option=tag.code("[trac] default_handler"), page=tag.a(_("Basic Settings"), href=req.href.admin('general/basics')))) return handler
def options_table(section, options): if options: return tag.table(class_='wiki')(tag.tbody( tag.tr(tag.td( tag.a(tag.code(option.name), class_='tracini-option', href='#%s-%s-option' % (section, option.name))), tag.td( format_to_html(self.env, formatter.context, option.doc)), default_cell(option), id='%s-%s-option' % (section, option.name), class_='odd' if idx % 2 else 'even') for idx, option in enumerate(options)))
def expand_macro(self, formatter, name, content): content = content.strip() if content else '' name_filter = content.strip('*') items = {} for subscriber in NotificationSystem(self.env).subscribers: name = subscriber.__class__.__name__ if not name_filter or name.startswith(name_filter): items[name] = subscriber.description() return tag.div(class_='trac-subscriberlist')(tag.table(class_='wiki')( tag.thead(tag.tr(tag.th(_("Subscriber")), tag.th(_("Description")))), tag.tbody( tag.tr(tag.td(tag.code(name)), tag.td(items[name]), class_='odd' if idx % 2 else 'even') for idx, name in enumerate(sorted(items)))))
def _provider_failure(self, exc, req, ep, current_filters, all_filters): """Raise a TracError exception explaining the failure of a provider. At the same time, the message will contain a link to the timeline without the filters corresponding to the guilty event provider `ep`. """ self.log.error("Timeline event provider failed: %s", exception_to_unicode(exc, traceback=True)) ep_kinds = {f[0]: f[1] for f in ep.get_timeline_filters(req) or []} ep_filters = set(ep_kinds.keys()) current_filters = set(current_filters) other_filters = set(current_filters) - ep_filters if not other_filters: other_filters = set(all_filters) - ep_filters args = [(a, req.args.get(a)) for a in ('from', 'format', 'max', 'daysback')] href = req.href.timeline(args + [(f, 'on') for f in other_filters]) # TRANSLATOR: ...want to see the 'other kinds of events' from... (link) other_events = tag.a(_('other kinds of events'), href=href) raise TracError( tag( tag.p(tag_( "Event provider %(name)s failed for filters " "%(kinds)s: ", name=tag.code(ep.__class__.__name__), kinds=', '.join('"%s"' % ep_kinds[f] for f in current_filters & ep_filters)), tag.strong(exception_to_unicode(exc)), class_='message'), tag.p( tag_( "You may want to see the %(other_events)s from the " "Timeline or notify your Trac administrator about the " "error (detailed information was written to the log).", other_events=other_events))))
def render_admin_panel(self, req, category, page, path_info): # Retrieve info for all repositories rm = RepositoryManager(self.env) all_repos = rm.get_all_repositories() db_provider = self.env[DbRepositoryProvider] if path_info: # Detail view reponame = path_info if not is_default(path_info) else '' info = all_repos.get(reponame) if info is None: raise TracError( _("Repository '%(repo)s' not found", repo=path_info)) if req.method == 'POST': if req.args.get('cancel'): req.redirect(req.href.admin(category, page)) elif db_provider and req.args.get('save'): # Modify repository changes = {} valid = True for field in db_provider.repository_attrs: value = normalize_whitespace(req.args.get(field)) if (value is not None or field in ('hidden', 'sync_per_request')) \ and value != info.get(field): changes[field] = value if 'dir' in changes and not \ self._check_dir(req, changes['dir']): valid = False if valid and changes: db_provider.modify_repository(reponame, changes) add_notice(req, _('Your changes have been saved.')) name = req.args.get('name') pretty_name = name or '(default)' resync = tag.code('trac-admin "%s" repository resync ' '"%s"' % (self.env.path, pretty_name)) if 'dir' in changes: msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) elif 'type' in changes: msg = tag_( 'You may have to run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) if name and name != path_info and 'alias' not in info: cset_added = tag.code('trac-admin "%s" changeset ' 'added "%s" $REV' % (self.env.path, pretty_name)) msg = tag_( 'You will need to update your ' 'post-commit hook to call ' '%(cset_added)s with the new ' 'repository name.', cset_added=cset_added) add_notice(req, msg) if valid: req.redirect(req.href.admin(category, page)) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) data = {'view': 'detail', 'reponame': reponame} else: # List view if req.method == 'POST': # Add a repository if db_provider and req.args.get('add_repos'): name = req.args.get('name') pretty_name = name or '(default)' if name in all_repos: raise TracError( _('The repository "%(name)s" already ' 'exists.', name=pretty_name)) type_ = req.args.get('type') # Avoid errors when copy/pasting paths dir = normalize_whitespace(req.args.get('dir', '')) if name is None or type_ is None or not dir: add_warning( req, _('Missing arguments to add a ' 'repository.')) elif self._check_dir(req, dir): db_provider.add_repository(name, dir, type_) add_notice( req, _('The repository "%(name)s" has been ' 'added.', name=pretty_name)) resync = tag.code('trac-admin "%s" repository resync ' '"%s"' % (self.env.path, pretty_name)) msg = tag_( 'You should now run %(resync)s to ' 'synchronize Trac with the repository.', resync=resync) add_notice(req, msg) cset_added = tag.code('trac-admin "%s" changeset ' 'added "%s" $REV' % (self.env.path, pretty_name)) doc = tag.a(_("documentation"), href=req.href.wiki('TracRepositoryAdmin') + '#Synchronization') msg = tag_( 'You should also set up a post-commit hook ' 'on the repository to call %(cset_added)s ' 'for each committed changeset. See the ' '%(doc)s for more information.', cset_added=cset_added, doc=doc) add_notice(req, msg) # Add a repository alias elif db_provider and req.args.get('add_alias'): name = req.args.get('name') pretty_name = name or '(default)' alias = req.args.get('alias') if name is not None and alias is not None: try: db_provider.add_alias(name, alias) except self.env.db_exc.IntegrityError: raise TracError( _('The alias "%(name)s" already ' 'exists.', name=pretty_name)) add_notice( req, _('The alias "%(name)s" has been ' 'added.', name=pretty_name)) else: add_warning(req, _('Missing arguments to add an ' 'alias.')) # Refresh the list of repositories elif req.args.get('refresh'): pass # Remove repositories elif db_provider and req.args.get('remove'): sel = req.args.getlist('sel') if sel: for name in sel: db_provider.remove_repository(name) add_notice( req, _('The selected repositories have ' 'been removed.')) else: add_warning(req, _('No repositories were selected.')) req.redirect(req.href.admin(category, page)) data = {'view': 'list'} # Find repositories that are editable db_repos = {} if db_provider is not None: db_repos = dict(db_provider.get_repositories()) # Prepare common rendering data repositories = { reponame: self._extend_info(reponame, info.copy(), reponame in db_repos) for (reponame, info) in all_repos.iteritems() } types = sorted([''] + rm.get_supported_types()) data.update({ 'types': types, 'default_type': rm.default_repository_type, 'repositories': repositories, 'can_add_alias': any('alias' not in info for info in repositories.itervalues()) }) return 'admin_repositories.html', data
def render_admin_panel(self, req, cat, page, path_info): log_type = self.env.log_type log_level = self.env.log_level log_file = self.env.log_file log_dir = self.env.log_dir log_types = [ dict(name='none', label=_("None"), selected=log_type == 'none', disabled=False), dict(name='stderr', label=_("Console"), selected=log_type == 'stderr', disabled=False), dict(name='file', label=_("File"), selected=log_type == 'file', disabled=False), dict(name='syslog', label=_("Syslog"), selected=log_type in ('unix', 'syslog'), disabled=os.name != 'posix'), dict(name='eventlog', label=_("Windows event log"), selected=log_type in ('winlog', 'eventlog', 'nteventlog'), disabled=os.name != 'nt'), ] if req.method == 'POST': changed = False new_type = req.args.get('log_type') if new_type not in [t['name'] for t in log_types]: raise TracError(_("Unknown log type %(type)s", type=new_type), _("Invalid log type")) new_file = req.args.get('log_file', 'trac.log') if not new_file: raise TracError(_("You must specify a log file"), _("Missing field")) new_level = req.args.get('log_level') if new_level not in LOG_LEVELS: raise TracError( _("Unknown log level %(level)s", level=new_level), _("Invalid log level")) # Create logger to be sure the configuration is valid. new_file_path = new_file if not os.path.isabs(new_file_path): new_file_path = os.path.join(self.env.log_dir, new_file) try: logger, handler = \ self.env.create_logger(new_type, new_file_path, new_level, self.env.log_format) except Exception as e: add_warning( req, tag_( "Changes not saved. Logger configuration " "error: %(error)s. Inspect the log for more " "information.", error=tag.code(exception_to_unicode(e)))) self.log.error("Logger configuration error: %s", exception_to_unicode(e, traceback=True)) else: handler.close() if new_type != log_type: self.config.set('logging', 'log_type', new_type) changed = True log_type = new_type if log_type == 'none': self.config.remove('logging', 'log_level') changed = True else: if new_level != log_level: self.config.set('logging', 'log_level', new_level) changed = True log_level = new_level if log_type == 'file': if new_file != log_file: self.config.set('logging', 'log_file', new_file) changed = True log_file = new_file else: self.config.remove('logging', 'log_file') changed = True if changed: _save_config(self.config, req, self.log), req.redirect(req.href.admin(cat, page)) data = { 'type': log_type, 'types': log_types, 'level': log_level, 'levels': LOG_LEVELS, 'file': log_file, 'dir': log_dir } return 'admin_logging.html', {'log': data}
def parse_connection_uri(db_str): """Parse the database connection string. The database connection string for an environment is specified through the `database` option in the `[trac]` section of trac.ini. :return: a tuple containing the scheme and a dictionary of attributes: `user`, `password`, `host`, `port`, `path`, `params`. :since: 1.1.3 """ if not db_str: section = tag.a("[trac]", title=_("TracIni documentation"), class_='trac-target-new', href='https://trac.edgewall.org/wiki/TracIni' '#trac-section') raise ConfigurationError( tag_( "Database connection string is empty. Set the %(option)s " "configuration option in the %(section)s section of " "trac.ini. Please refer to the %(doc)s for help.", option=tag.code("database"), section=section, doc=_doc_db_str())) try: scheme, rest = db_str.split(':', 1) except ValueError: raise _invalid_db_str(db_str) if not rest.startswith('/'): if scheme == 'sqlite' and rest: # Support for relative and in-memory SQLite connection strings host = None path = rest else: raise _invalid_db_str(db_str) else: if not rest.startswith('//'): host = None rest = rest[1:] elif rest.startswith('///'): host = None rest = rest[3:] else: rest = rest[2:] if '/' in rest: host, rest = rest.split('/', 1) else: host = rest rest = '' path = None if host and '@' in host: user, host = host.split('@', 1) if ':' in user: user, password = user.split(':', 1) else: password = None if user: user = urllib.unquote(user) if password: password = unicode_passwd(urllib.unquote(password)) else: user = password = None if host and ':' in host: host, port = host.split(':', 1) try: port = int(port) except ValueError: raise _invalid_db_str(db_str) else: port = None if not path: path = '/' + rest if os.name == 'nt': # Support local paths containing drive letters on Win32 if len(rest) > 1 and rest[1] == '|': path = "%s:%s" % (rest[0], rest[2:]) params = {} if '?' in path: path, qs = path.split('?', 1) qs = qs.split('&') for param in qs: try: name, value = param.split('=', 1) except ValueError: raise _invalid_db_str(db_str) value = urllib.unquote(value) params[name] = value args = zip(('user', 'password', 'host', 'port', 'path', 'params'), (user, password, host, port, path, params)) return scheme, {key: value for key, value in args if value}
def expand_macro(self, formatter, name, content): from trac.config import ConfigSection, Option args, kw = parse_args(content) filters = {} for name, index in (('section', 0), ('option', 1)): pattern = kw.get(name, '').strip() if pattern: filters[name] = fnmatch.translate(pattern) continue prefix = args[index].strip() if index < len(args) else '' if prefix: filters[name] = re.escape(prefix) has_option_filter = 'option' in filters for name in ('section', 'option'): filters[name] = re.compile(filters[name], re.IGNORECASE).match \ if name in filters \ else lambda v: True section_filter = filters['section'] option_filter = filters['option'] section_registry = ConfigSection.get_registry(self.compmgr) option_registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in option_registry.iteritems(): if section_filter(section) and option_filter(key): options.setdefault(section, {})[key] = option if not has_option_filter: for section in section_registry: if section_filter(section): options.setdefault(section, {}) for section in options: options[section] = sorted(options[section].itervalues(), key=lambda option: option.name) sections = [(section, section_registry[section].doc if section in section_registry else '') for section in sorted(options)] def default_cell(option): default = option.default if default is not None and default != '': return tag.td(tag.code(option.dumps(default)), class_='default') else: return tag.td(_("(no default)"), class_='nodefault') def options_table(section, options): if options: return tag.table(class_='wiki')(tag.tbody( tag.tr(tag.td( tag.a(tag.code(option.name), class_='tracini-option', href='#%s-%s-option' % (section, option.name))), tag.td( format_to_html(self.env, formatter.context, option.doc)), default_cell(option), id='%s-%s-option' % (section, option.name), class_='odd' if idx % 2 else 'even') for idx, option in enumerate(options))) return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), options_table(section, options.get(section))) for section, section_doc in sections)
def expand_macro(self, formatter, name, content, args=None): if content is not None: content = content.strip() if not args and not content: raw_actions = self.config.options('ticket-workflow') else: is_macro = args is None if is_macro: kwargs = parse_args(content)[1] file = kwargs.get('file') else: file = args.get('file') if not file and not content: raise ProcessorError("Invalid argument(s).") if file: print(file) text = RepositoryManager(self.env).read_file_by_path(file) if text is None: raise ProcessorError( tag_("The file %(file)s does not exist.", file=tag.code(file))) elif is_macro: text = '\n'.join(line.lstrip() for line in content.split(';')) else: text = content if '[ticket-workflow]' not in text: text = '[ticket-workflow]\n' + text parser = RawConfigParser() try: parser.readfp(io.StringIO(text)) except ParsingError as e: if is_macro: raise MacroError(exception_to_unicode(e)) else: raise ProcessorError(exception_to_unicode(e)) raw_actions = list(parser.items('ticket-workflow')) actions = parse_workflow_config(raw_actions) states = list( {state for action in actions.itervalues() for state in action['oldstates']} | {action['newstate'] for action in actions.itervalues()}) action_labels = [attrs['label'] for attrs in actions.values()] action_names = list(actions) edges = [] for name, action in actions.items(): new_index = states.index(action['newstate']) name_index = action_names.index(name) for old_state in action['oldstates']: old_index = states.index(old_state) edges.append((old_index, new_index, name_index)) args = args or {} width = args.get('width', 800) height = args.get('height', 600) graph = {'nodes': states, 'actions': action_labels, 'edges': edges, 'width': width, 'height': height} graph_id = '%012x' % id(graph) req = formatter.req add_script(req, 'common/js/excanvas.js', ie_if='IE') add_script(req, 'common/js/workflow_graph.js') add_script_data(req, {'graph_%s' % graph_id: graph}) return tag( tag.div('', class_='trac-workflow-graph trac-noscript', id='trac-workflow-graph-%s' % graph_id, style="display:inline-block;width:%spx;height:%spx" % (width, height)), tag.noscript( tag.div(_("Enable JavaScript to display the workflow graph."), class_='system-message')))