class TicketAdminPanel(Component): implements(IAdminPanelProvider, IAdminCommandProvider) abstract = True _label = (N_('(Undefined)'), N_('(Undefined)')) # i18n note: use gettext() whenever refering to the above as text labels, # and don't use it whenever using them as field names (after # a call to `.lower()`) # IAdminPanelProvider methods def get_admin_panels(self, req): if 'TICKET_ADMIN' in req.perm('admin', 'ticket/' + self._type): # in global scope show only products # in local scope everything but products parent = getattr(self.env, 'parent', None) if (parent is None and self._type == 'products') or \ (parent and self._type != 'products'): yield ('ticket', _('Ticket System'), self._type, gettext(self._label[1])) def render_admin_panel(self, req, cat, page, version): # Trap AssertionErrors and convert them to TracErrors try: return self._render_admin_panel(req, cat, page, version) except AssertionError, e: raise TracError(e)
class TicketAdminPanel(Component): implements(IAdminPanelProvider, IAdminCommandProvider) abstract = True _label = (N_('(Undefined)'), N_('(Undefined)')) # i18n note: use gettext() whenever refering to the above as text labels, # and don't use it whenever using them as field names (after # a call to `.lower()`) # IAdminPanelProvider methods def get_admin_panels(self, req): if 'TICKET_ADMIN' in req.perm: yield ('ticket', _('Ticket System'), self._type, gettext(self._label[1])) def render_admin_panel(self, req, cat, page, version): req.perm.require('TICKET_ADMIN') # Trap AssertionErrors and convert them to TracErrors try: return self._render_admin_panel(req, cat, page, version) except AssertionError, e: raise TracError(e)
def get_radio_fields(self): """Default radio fields""" from trac.ticket import model radios = [(20, {'name': 'status', 'label': N_('Status'), 'cls': model.Status}), (80, {'name': 'resolution', 'label': N_('Resolution'), 'cls': model.Resolution})] return radios
class TicketTypeAdminPanel(AbstractEnumAdminPanel): _type = 'type' _enum_cls = model.Type _label = N_("Ticket Type"), N_("Ticket Types") _command_type = 'ticket_type' _command_help = { 'list': 'Show possible %s', 'add': 'Add a %s', 'change': 'Change a %s', 'remove': 'Remove a %s', 'order': 'Move a %s up or down in the list', }
class PermissionError(TracBaseError): """Insufficient permissions to perform the operation.""" title = N_("Forbidden") def __init__(self, action=None, resource=None, env=None, msg=None): self.action = action self.resource = resource self.env = env if self.action: if self.resource and self.resource.id: msg = _( "%(perm)s privileges are required to perform " "this operation on %(resource)s. You don't have the " "required permissions.", perm=self.action, resource=get_resource_name(self.env, self.resource)) else: msg = _( "%(perm)s privileges are required to perform this " "operation. You don't have the required " "permissions.", perm=self.action) elif msg is None: msg = _("Insufficient privileges to perform this operation.") super(PermissionError, self).__init__(msg) @property def message(self): return self.args[0]
class PermissionError(StandardError): """Insufficient permissions to perform the operation. :since 1.0.5: the `msg` attribute is deprecated and will be removed in 1.3.1. Use the `message` property instead. """ title = N_("Forbidden") def __init__(self, action=None, resource=None, env=None, msg=None): self.action = action self.resource = resource self.env = env if self.action: if self.resource: msg = _( "%(perm)s privileges are required to perform " "this operation on %(resource)s. You don't have the " "required permissions.", perm=self.action, resource=get_resource_name(self.env, self.resource)) else: msg = _( "%(perm)s privileges are required to perform this " "operation. You don't have the required " "permissions.", perm=self.action) elif msg is None: msg = _("Insufficient privileges to perform this operation.") self.msg = msg super(PermissionError, self).__init__(msg) @property def message(self): return self.args[0]
class TicketAdminPanel(Component): implements(IAdminPanelProvider, IAdminCommandProvider) abstract = True _type = 'undefined' _label = N_("(Undefined)"), N_("(Undefined)") _view_perms = ['TICKET_ADMIN'] # i18n note: use gettext() whenever referring to the above as text labels, # and don't use it whenever using them as field names (after # a call to `.lower()`) # IAdminPanelProvider methods def get_admin_panels(self, req): if all(perm in req.perm('admin', 'ticket/' + self._type) for perm in self._view_perms): yield ('ticket', _('Ticket System'), self._type, gettext(self._label[1])) def render_admin_panel(self, req, cat, page, path_info): # Trap AssertionErrors and convert them to TracErrors try: return self._render_admin_panel(req, cat, page, path_info) except AssertionError as e: raise TracError(e) def _save_config(self, req): """Try to save the config, and display either a success notice or a failure warning. """ try: self.config.save() except EnvironmentError as e: self.log.error("Error writing to trac.ini: %s", exception_to_unicode(e)) add_warning(req, _("Error writing to trac.ini, make sure it is " "writable by the web server. Your changes " "have not been saved.")) else: add_notice(req, _("Your changes have been saved.")) def _render_admin_panel(self, req, cat, page, path_info): raise NotImplemented("Class inheriting from TicketAdminPanel has not " "implemented the _render_admin_panel method.")
def get_select_fields(self): """Product select fields""" return [(35, { 'name': 'product', 'label': N_('Product'), 'cls': Product, 'optional': True })]
class ConfigurationError(TracError): """Exception raised when a value in the configuration file is not valid.""" title = N_("Configuration Error") def __init__(self, message=None, title=None, show_traceback=False): if message is None: message = _("Look in the Trac log for more information.") super().__init__(message, title, show_traceback)
class TracNotImplementedError(TracError, NotImplementedError): """Raised when a `NotImplementedError` is trapped. This exception is for internal use and should not be raised by plugins. Plugins should raise `NotImplementedError`. :since: 1.0.11 """ title = N_("Not Implemented Error")
def get_select_fields(self): """Default select and radio fields""" from trac.ticket import model selects = [(10, {'name': 'type', 'label': N_('Type'), 'cls': model.Type}), (30, {'name':'priority', 'label': N_('Priority'), 'cls': model.Priority}), (40, {'name': 'milestone', 'label': N_('Milestone'), 'cls': model.Milestone, 'optional': True}), (50, {'name': 'component', 'label': N_('Component'), 'cls': model.Component}), (60, {'name': 'version', 'label': N_('Version'), 'cls': model.Version, 'optional': True}), (70, {'name': 'severity', 'label': N_('Severity'), 'cls': model.Severity})] return selects
class PermissionError(TracBaseError, StandardError): """Insufficient permissions to complete the operation""" title = N_("Forbidden") def __init__(self, action=None, resource=None, env=None, msg=None): self.action = action self.resource = resource self.env = env if self.action: if self.resource: msg = _("%(perm)s privileges are required to perform " "this operation on %(resource)s. You don't have the " "required permissions.", perm=self.action, resource=get_resource_name(self.env, self.resource)) else: msg = _("%(perm)s privileges are required to perform this " "operation. You don't have the required " "permissions.", perm=self.action) elif msg is None: msg = _("Insufficient privileges to perform this operation.") self.msg = msg StandardError.__init__(self, msg)
class VersionAdminPanel(TicketAdminPanel): _type = 'versions' _label = N_("Version"), N_("Versions") # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, version): # Detail view? if version: ver = model.Version(self.env, version) if req.method == 'POST': if req.args.get('save'): ver.name = req.args.get('name') if req.args.get('time'): ver.time = user_time(req, parse_date, req.args.get('time'), hint='datetime') else: ver.time = None # unset ver.description = req.args.get('description') ver.update() add_notice(req, _("Your changes have been saved.")) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) data = {'view': 'detail', 'version': ver} else: default = self.config.get('ticket', 'default_version') if req.method == 'POST': # Add Version if req.args.get('add') and req.args.get('name'): ver = model.Version(self.env) ver.name = req.args.get('name') if req.args.get('time'): ver.time = user_time(req, parse_date, req.args.get('time'), hint='datetime') ver.insert() add_notice(req, _('The version "%(name)s" has been ' 'added.', name=ver.name)) req.redirect(req.href.admin(cat, page)) # Remove versions elif req.args.get('remove'): sel = req.args.getlist('sel') if not sel: raise TracError(_("No version selected")) with self.env.db_transaction: for name in sel: model.Version(self.env, name).delete() if name == default: self.config.set('ticket', 'default_version', '') self._save_config(req) add_notice(req, _("The selected versions have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default version elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default version to %s", name) self.config.set('ticket', 'default_version', name) self._save_config(req) req.redirect(req.href.admin(cat, page)) # Clear default version elif req.args.get('clear'): self.log.info("Clearing default version") self.config.set('ticket', 'default_version', '') self._save_config(req) req.redirect(req.href.admin(cat, page)) data = {'view': 'list', 'versions': list(model.Version.select(self.env)), 'default': default} Chrome(self.env).add_jquery_ui(req) data.update({'datetime_hint': get_datetime_format_hint(req.lc_time)}) return 'admin_versions.html', data # IAdminCommandProvider methods def get_admin_commands(self): locale = get_console_locale(self.env) hints = { 'datetime': get_datetime_format_hint(locale), 'iso8601': get_datetime_format_hint('iso8601'), } yield ('version list', '', "Show versions", None, self._do_list) yield ('version add', '<name> [time]', "Add version", None, self._do_add) yield ('version rename', '<name> <newname>', "Rename version", self._complete_name, self._do_rename) yield ('version remove', '<name>', "Remove version", self._complete_name, self._do_remove) yield ('version time', '<name> <time>', """Set version date The <time> must be specified in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format. Alternatively, "now" can be used to set the version date to the current time. To remove the date from a version, specify an empty string (""). """ % hints, self._complete_name, self._do_time) def get_version_list(self): return [v.name for v in model.Version.select(self.env)] def _complete_name(self, args): if len(args) == 1: return self.get_version_list() def _do_list(self): print_table([(v.name, format_date(v.time, console_date_format) if v.time else None) for v in model.Version.select(self.env)], [_("Name"), _("Time")]) def _do_add(self, name, time=None): version = model.Version(self.env) version.name = name version.time = parse_date(time, hint='datetime', locale=get_console_locale(self.env)) \ if time else None version.insert() def _do_rename(self, name, newname): version = model.Version(self.env, name) version.name = newname version.update() def _do_remove(self, name): model.Version(self.env, name).delete() def _do_time(self, name, time): version = model.Version(self.env, name) version.time = parse_date(time, hint='datetime', locale=get_console_locale(self.env)) \ if time else None version.update()
class MilestoneAdminPanel(TicketAdminPanel): _type = 'milestones' _label = N_("Milestone"), N_("Milestones") _view_perms = TicketAdminPanel._view_perms + ['MILESTONE_VIEW'] # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, milestone_name): perm_cache = req.perm('admin', 'ticket/' + self._type) # Detail view if milestone_name: milestone = model.Milestone(self.env, milestone_name) milestone_module = MilestoneModule(self.env) if req.method == 'POST': if 'save' in req.args: perm_cache.require('MILESTONE_MODIFY') if milestone_module.save_milestone(req, milestone): req.redirect(req.href.admin(cat, page)) elif 'cancel' in req.args: req.redirect(req.href.admin(cat, page)) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) data = {'view': 'detail', 'milestone': milestone, 'default_due': milestone_module.get_default_due(req)} # List view else: ticket_default = self.config.get('ticket', 'default_milestone') retarget_default = self.config.get('milestone', 'default_retarget_to') if req.method == 'POST': # Add milestone if 'add' in req.args and req.args.get('name'): perm_cache.require('MILESTONE_CREATE') name = req.args.get('name') try: model.Milestone(self.env, name=name) except ResourceNotFound: milestone = model.Milestone(self.env) milestone.name = name MilestoneModule(self.env).save_milestone(req, milestone) req.redirect(req.href.admin(cat, page)) else: add_warning(req, _('Milestone "%(name)s" already ' 'exists, please choose another ' 'name.', name=name)) # Remove milestone elif 'remove' in req.args: save = False perm_cache.require('MILESTONE_DELETE') sel = req.args.getlist('sel') if not sel: raise TracError(_("No milestone selected")) with self.env.db_transaction: for name in sel: milestone = model.Milestone(self.env, name) milestone.move_tickets(None, req.authname, "Milestone deleted") milestone.delete() if name == ticket_default: self.config.set('ticket', 'default_milestone', '') save = True if name == retarget_default: self.config.set('milestone', 'default_retarget_to', '') save = True if save: self._save_config(req) add_notice(req, _("The selected milestones have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default milestone elif 'apply' in req.args: save = False name = req.args.get('ticket_default') if name and name != ticket_default: self.log.info("Setting default ticket " "milestone to %s", name) self.config.set('ticket', 'default_milestone', name) save = True retarget = req.args.get('retarget_default') if retarget and retarget != retarget_default: self.log.info("Setting default retargeting " "milestone to %s", retarget) self.config.set('milestone', 'default_retarget_to', retarget) save = True if save: self._save_config(req) req.redirect(req.href.admin(cat, page)) # Clear default milestone elif 'clear' in req.args: self.log.info("Clearing default ticket milestone " "and default retarget milestone") self.config.set('ticket', 'default_milestone', '') self.config.set('milestone', 'default_retarget_to', '') self._save_config(req) req.redirect(req.href.admin(cat, page)) # Get ticket count num_tickets = dict(self.env.db_query(""" SELECT milestone, COUNT(milestone) FROM ticket WHERE milestone != '' GROUP BY milestone """)) query_href = lambda name: req.href.query([('group', 'status'), ('milestone', name)]) data = {'view': 'list', 'milestones': model.Milestone.select(self.env), 'query_href': query_href, 'num_tickets': lambda m: num_tickets.get(m.name, 0), 'ticket_default': ticket_default, 'retarget_default': retarget_default} Chrome(self.env).add_jquery_ui(req) data.update({ 'datetime_hint': get_datetime_format_hint(req.lc_time), }) return 'admin_milestones.html', data # IAdminCommandProvider methods def get_admin_commands(self): locale = get_console_locale(self.env) hints = { 'datetime': get_datetime_format_hint(locale), 'iso8601': get_datetime_format_hint('iso8601'), } yield ('milestone list', '', "Show milestones", None, self._do_list) yield ('milestone add', '<name> [due]', "Add milestone", None, self._do_add) yield ('milestone rename', '<name> <newname>', "Rename milestone", self._complete_name, self._do_rename) yield ('milestone due', '<name> <due>', """Set milestone due date The <due> date must be specified in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format. Alternatively, "now" can be used to set the due date to the current time. To remove the due date from a milestone, specify an empty string (""). """ % hints, self._complete_name, self._do_due) yield ('milestone completed', '<name> <completed>', """Set milestone complete date The <completed> date must be specified in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format. Alternatively, "now" can be used to set the completion date to the current time. To remove the completion date from a milestone, specify an empty string (""). """ % hints, self._complete_name, self._do_completed) yield ('milestone remove', '<name>', "Remove milestone", self._complete_name, self._do_remove) def get_milestone_list(self): return [m.name for m in model.Milestone.select(self.env)] def _complete_name(self, args): if len(args) == 1: return self.get_milestone_list() def _do_list(self): print_table([(m.name, format_date(m.due, console_date_format) if m.due else None, format_datetime(m.completed, console_datetime_format) if m.completed else None) for m in model.Milestone.select(self.env)], [_("Name"), _("Due"), _("Completed")]) def _do_add(self, name, due=None): milestone = model.Milestone(self.env) milestone.name = name if due is not None: milestone.due = parse_date(due, hint='datetime', locale=get_console_locale(self.env)) milestone.insert() def _do_rename(self, name, newname): milestone = model.Milestone(self.env, name) milestone.name = newname milestone.update(author=getuser()) def _do_due(self, name, due): milestone = model.Milestone(self.env, name) milestone.due = parse_date(due, hint='datetime', locale=get_console_locale(self.env)) \ if due else None milestone.update() def _do_completed(self, name, completed): milestone = model.Milestone(self.env, name) milestone.completed = parse_date(completed, hint='datetime', locale=get_console_locale(self.env)) \ if completed else None milestone.update() def _do_remove(self, name): model.Milestone(self.env, name).delete()
class MilestoneAdminPanel(TicketAdminPanel): _type = 'milestones' _label = (N_('Milestone'), N_('Milestones')) # IAdminPanelProvider methods def get_admin_panels(self, req): if 'MILESTONE_VIEW' in req.perm('admin', 'ticket/' + self._type): return TicketAdminPanel.get_admin_panels(self, req) # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, milestone): perm = req.perm('admin', 'ticket/' + self._type) # Detail view? if milestone: mil = model.Milestone(self.env, milestone) if req.method == 'POST': if req.args.get('save'): perm.require('MILESTONE_MODIFY') mil.name = name = req.args.get('name') mil.due = mil.completed = None due = req.args.get('duedate', '') if due: mil.due = user_time(req, parse_date, due, hint='datetime') if req.args.get('completed', False): completed = req.args.get('completeddate', '') mil.completed = user_time(req, parse_date, completed, hint='datetime') 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', '') try: mil.update(author=req.authname) except self.env.db_exc.IntegrityError: raise TracError( _('Milestone "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'milestone': mil} else: default = self.config.get('ticket', 'default_milestone') if req.method == 'POST': # Add Milestone if req.args.get('add') and req.args.get('name'): perm.require('MILESTONE_CREATE') name = req.args.get('name') try: mil = model.Milestone(self.env, name=name) except ResourceNotFound: mil = model.Milestone(self.env) mil.name = name if req.args.get('duedate'): mil.due = user_time(req, parse_date, req.args.get('duedate'), hint='datetime') mil.insert() add_notice( req, _('The milestone "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if mil.name is None: raise TracError(_('Invalid milestone name.')) raise TracError( _('Milestone "%(name)s" already ' 'exists.', name=name)) # Remove milestone elif req.args.get('remove'): perm.require('MILESTONE_DELETE') sel = req.args.get('sel') if not sel: raise TracError(_('No milestone selected')) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: milestone = model.Milestone(self.env, name) milestone.move_tickets(None, req.authname, "Milestone deleted") milestone.delete() add_notice( req, _("The selected milestones have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default milestone elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default milestone to %s", name) self.config.set('ticket', 'default_milestone', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) # Get ticket count milestones = [(milestone, self.env.db_query( """ SELECT COUNT(*) FROM ticket WHERE milestone=%s """, (milestone.name, ))[0][0]) for milestone in model.Milestone.select(self.env)] data = { 'view': 'list', 'milestones': milestones, 'default': default } Chrome(self.env).add_jquery_ui(req) data.update({ 'datetime_hint': get_datetime_format_hint(req.lc_time), }) return 'admin_milestones.html', data # IAdminCommandProvider methods def get_admin_commands(self): hints = { 'datetime': get_datetime_format_hint(get_console_locale(self.env)), 'iso8601': get_datetime_format_hint('iso8601'), } yield ('milestone list', '', "Show milestones", None, self._do_list) yield ('milestone add', '<name> [due]', "Add milestone", None, self._do_add) yield ('milestone rename', '<name> <newname>', "Rename milestone", self._complete_name, self._do_rename) yield ('milestone due', '<name> <due>', """Set milestone due date The <due> date must be specified in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format. Alternatively, "now" can be used to set the due date to the current time. To remove the due date from a milestone, specify an empty string (""). """ % hints, self._complete_name, self._do_due) yield ('milestone completed', '<name> <completed>', """Set milestone complete date The <completed> date must be specified in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format. Alternatively, "now" can be used to set the completion date to the current time. To remove the completion date from a milestone, specify an empty string (""). """ % hints, self._complete_name, self._do_completed) yield ('milestone remove', '<name>', "Remove milestone", self._complete_name, self._do_remove) def get_milestone_list(self): return [m.name for m in model.Milestone.select(self.env)] def _complete_name(self, args): if len(args) == 1: return self.get_milestone_list() def _do_list(self): print_table( [(m.name, format_date(m.due, console_date_format) if m.due else None, format_datetime(m.completed, console_datetime_format) if m.completed else None) for m in model.Milestone.select(self.env)], [_("Name"), _("Due"), _("Completed")]) def _do_add(self, name, due=None): milestone = model.Milestone(self.env) milestone.name = name if due is not None: milestone.due = parse_date(due, hint='datetime', locale=get_console_locale(self.env)) milestone.insert() def _do_rename(self, name, newname): milestone = model.Milestone(self.env, name) milestone.name = newname milestone.update(author=getuser()) def _do_due(self, name, due): milestone = model.Milestone(self.env, name) milestone.due = parse_date(due, hint='datetime', locale=get_console_locale(self.env)) \ if due else None milestone.update() def _do_completed(self, name, completed): milestone = model.Milestone(self.env, name) milestone.completed = parse_date(completed, hint='datetime', locale=get_console_locale(self.env)) \ if completed else None milestone.update() def _do_remove(self, name): model.Milestone(self.env, name).delete(author=getuser())
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled acct_mgr.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `acct_mgr` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. Non-absolute paths are relative to the Environment `conf` directory. """) base_url = Option( 'trac', 'base_url', '', """Reference URL for the Trac deployment. This is the base URL that will be used when producing documents that will be used outside of the web browsing context, like for example when inserting URLs pointing to Trac resources in notification e-mails.""") base_url_for_redirect = BoolOption( 'trac', 'use_base_url_for_redirect', False, """Optionally use `[trac] base_url` for redirects. In some configurations, usually involving running Trac behind a HTTP proxy, Trac can't automatically reconstruct the URL that is used to access it. You may need to use this option to force Trac to use the `base_url` setting also for redirects. This introduces the obvious limitation that this environment will only be usable when accessible from that URL, as redirects are frequently used. """) secure_cookies = BoolOption( 'trac', 'secure_cookies', False, """Restrict cookies to HTTPS connections. When true, set the `secure` flag on all cookies so that they are only sent to the server on HTTPS connections. Use this if your Trac instance is only accessible through HTTPS. """) anonymous_session_lifetime = IntOption( 'trac', 'anonymous_session_lifetime', '90', """Lifetime of the anonymous session, in days. Set the option to 0 to disable purging old anonymous sessions. (''since 1.0.17'')""") project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option( 'project', 'url', '', """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. """) project_footer = Option( 'project', 'footer', N_('Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>'), """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = ChoiceOption('logging', 'log_type', log.LOG_TYPES + log.LOG_TYPE_ALIASES, """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""", case_sensitive=False) log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = ChoiceOption('logging', 'log_level', log.LOG_LEVELS + log.LOG_LEVEL_ALIASES, """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`). """, case_sensitive=False) log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: `Trac[$(module)s] $(levelname)s: $(message)s` In addition to regular key names supported by the [http://docs.python.org/library/logging.html Python logger library] one could use: - `$(path)s` the path for the current environment - `$(basename)s` the last path component of the current environment - `$(project)s` the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the !ConfigParser itself. Example: `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` """) def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. :param path: the absolute path to the Trac environment :param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. :param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = os.path.normpath(os.path.normcase(path)) self.log = None self.config = None if create: self.create(options) for setup_participant in self.setup_participants: setup_participant.environment_created() else: self.verify() self.setup_config() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.path) @lazy def name(self): """The environment name. :since: 1.2 """ return os.path.basename(self.path) @property def env(self): """Property returning the `Environment` object, which is often required for functions and methods that take a `Component` instance. """ # The cached decorator requires the object have an `env` attribute. return self @property def system_info(self): """List of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = [] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) return sorted(set(info), key=lambda args: (args[0] != 'Trac', args[0].lower())) def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. :since 1.3.1: deprecated and will be removed in 1.5.1. Use system_info property instead. """ return self.system_info # ISystemInfoProvider methods def get_system_info(self): yield 'Trac', self.trac_version yield 'Python', sys.version yield 'setuptools', setuptools.__version__ if pytz is not None: yield 'pytz', pytz.__version__ if hasattr(self, 'webfrontend_version'): yield self.webfrontend, self.webfrontend_version def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @lazy def _component_rules(self): _rules = {} for name, value in self.components_section.options(): name = name.rstrip('.*').lower() _rules[name] = as_bool(value) return _rules def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns `False`, the component does not get activated. If it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package except # in trac.test or trac.tests are enabled return component_name.startswith('trac.') and \ not component_name.startswith('trac.test.') and \ not component_name.startswith('trac.tests.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True super(Environment, self).enable_component(cls) @contextmanager def component_guard(self, component, reraise=False): """Traps any runtime exception raised when working with a component and logs the error. :param component: the component responsible for any error that could happen inside the context :param reraise: if `True`, an error is logged but not suppressed. By default, errors are suppressed. """ try: yield except TracError as e: self.log.warning("Component %s failed with %s", component, exception_to_unicode(e)) if reraise: raise except Exception as e: self.log.error("Component %s failed with %s", component, exception_to_unicode(e, traceback=True)) if reraise: raise def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" try: tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] if tag != _VERSION: raise Exception( _("Unknown Trac environment type '%(type)s'", type=tag)) except Exception as e: raise TracError( _("No Trac environment found at %(path)s\n" "%(e)s", path=self.path, e=e)) @lazy def db_exc(self): """Return an object (typically a module) containing all the backend-specific exception types as attributes, named according to the Python Database API (http://www.python.org/dev/peps/pep-0249/). To catch a database exception, use the following pattern:: try: with env.db_transaction as db: ... except env.db_exc.IntegrityError as e: ... """ return DatabaseManager(self).get_exceptions() @property def db_query(self): """Return a context manager (`~trac.db.api.QueryContextManager`) which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still defined, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager (`~trac.db.api.TransactionContextManager`) which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. You should *not* call `commit()` yourself within such block, as this will force a commit even if that transaction is part of a larger transaction. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" from trac.versioncontrol.api import RepositoryManager RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: log.shutdown(self.log) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values. If options contains ('inherit', 'file'), default values will not be loaded; they are expected to be provided by that file or other options. :raises TracError: if the base directory of `path` does not exist. :raises TracError: if `path` exists and is not empty. """ base_dir = os.path.dirname(self.path) if not os.path.exists(base_dir): raise TracError( _( "Base directory '%(env)s' does not exist. Please create it " "and retry.", env=base_dir)) if os.path.exists(self.path) and os.listdir(self.path): raise TracError(_("Directory exists and is not empty.")) # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.htdocs_dir) os.mkdir(self.log_dir) os.mkdir(self.plugins_dir) os.mkdir(self.templates_dir) # Create a few files create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n') create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(self.conf_dir) config = Configuration(self.config_file_path) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the sample configuration create_file(self.config_file_path + '.sample') self._update_sample_config() # Create the database DatabaseManager(self).init_db() @lazy def database_version(self): """Returns the current version of the database. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('database_version') @lazy def database_initial_version(self): """Returns the version of the database at the time of creation. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('initial_database_version') @lazy def trac_version(self): """Returns the version of Trac. :since: 1.2 """ from trac import core, __version__ return get_pkginfo(core).get('version', __version__) def setup_config(self): """Load the configuration file.""" self.config = Configuration(self.config_file_path, {'envname': self.name}) if not self.config.exists: raise TracError( _("The configuration file is not found at " "%(path)s", path=self.config_file_path)) self.setup_log() plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) @lazy def config_file_path(self): """Path of the trac.ini file.""" return os.path.join(self.conf_dir, 'trac.ini') @lazy def log_file_path(self): """Path to the log file.""" if not os.path.isabs(self.log_file): return os.path.join(self.log_dir, self.log_file) return self.log_file def _get_path_to_dir(self, *dirs): path = self.path for dir in dirs: path = os.path.join(path, dir) return os.path.realpath(path) @lazy def attachments_dir(self): """Absolute path to the attachments directory. :since: 1.3.1 """ return self._get_path_to_dir('files', 'attachments') @lazy def conf_dir(self): """Absolute path to the conf directory. :since: 1.0.11 """ return self._get_path_to_dir('conf') @lazy def files_dir(self): """Absolute path to the files directory. :since: 1.3.2 """ return self._get_path_to_dir('files') @lazy def htdocs_dir(self): """Absolute path to the htdocs directory. :since: 1.0.11 """ return self._get_path_to_dir('htdocs') @lazy def log_dir(self): """Absolute path to the log directory. :since: 1.0.11 """ return self._get_path_to_dir('log') @lazy def plugins_dir(self): """Absolute path to the plugins directory. :since: 1.0.11 """ return self._get_path_to_dir('plugins') @lazy def templates_dir(self): """Absolute path to the templates directory. :since: 1.0.11 """ return self._get_path_to_dir('templates') def setup_log(self): """Initialize the logging sub-system.""" self.log, log_handler = \ self.create_logger(self.log_type, self.log_file_path, self.log_level, self.log_format) self.log.addHandler(log_handler) self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, self.trac_version) def create_logger(self, log_type, log_file, log_level, log_format): log_id = 'Trac.%s' % hashlib.sha1(self.path).hexdigest() if log_format: log_format = log_format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', self.name) \ .replace('%(project)s', self.project_name) return log.logger_handler_factory(log_type, log_file, log_level, log_id, format=log_format) def get_known_users(self, as_dict=False): """Returns information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. By default this function returns a iterator that yields one tuple for every user, of the form (username, name, email), ordered alpha-numerically by username. When `as_dict` is `True` the function returns a dictionary mapping username to a (name, email) tuple. :since 1.2: the `as_dict` parameter is available. """ return self._known_users_dict if as_dict else iter(self._known_users) @cached def _known_users(self): return self.db_query(""" SELECT DISTINCT s.sid, n.value, e.value FROM session AS s LEFT JOIN session_attribute AS n ON (n.sid=s.sid AND n.authenticated=1 AND n.name = 'name') LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=1 AND e.name = 'email') WHERE s.authenticated=1 ORDER BY s.sid """) @cached def _known_users_dict(self): return {u[0]: (u[1], u[2]) for u in self._known_users} def invalidate_known_users_cache(self): """Clear the known_users cache.""" del self._known_users del self._known_users_dict def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): self.log.warning( "Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): upgraders.append(participant) if not upgraders: return if backup: try: self.backup(backup_dest) except Exception as e: raise BackupError(e) for participant in upgraders: self.log.info("upgrading %s...", participant) with self.component_guard(participant, reraise=True): participant.upgrade_environment() # Database schema may have changed, so close all connections dbm = DatabaseManager(self) if dbm.connection_uri != 'sqlite::memory:': dbm.shutdown() self._update_sample_config() del self.database_version return True @lazy def href(self): """The application root path""" return Href(urlsplit(self.abs_href.base).path) @lazy def abs_href(self): """The application URL""" if not self.base_url: self.log.warning("base_url option not set in configuration, " "generated links may be incorrect") return Href(self.base_url) def _update_sample_config(self): filename = os.path.join(self.config_file_path + '.sample') if not os.path.isfile(filename): return config = Configuration(filename) config.set_defaults() try: config.save() except EnvironmentError as e: self.log.warning("Couldn't write sample configuration file (%s)%s", e, exception_to_unicode(e, traceback=True)) else: self.log.info( "Wrote sample configuration file with the new " "settings and their default values: %s", filename)
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled webadmin.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `webadmin` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. (''since 0.11'')""") base_url = Option( 'trac', 'base_url', '', """Reference URL for the Trac deployment. This is the base URL that will be used when producing documents that will be used outside of the web browsing context, like for example when inserting URLs pointing to Trac resources in notification e-mails.""") base_url_for_redirect = BoolOption( 'trac', 'use_base_url_for_redirect', False, """Optionally use `[trac] base_url` for redirects. In some configurations, usually involving running Trac behind a HTTP proxy, Trac can't automatically reconstruct the URL that is used to access it. You may need to use this option to force Trac to use the `base_url` setting also for redirects. This introduces the obvious limitation that this environment will only be usable when accessible from that URL, as redirects are frequently used. ''(since 0.10.5)''""") secure_cookies = BoolOption( 'trac', 'secure_cookies', False, """Restrict cookies to HTTPS connections. When true, set the `secure` flag on all cookies so that they are only sent to the server on HTTPS connections. Use this if your Trac instance is only accessible through HTTPS. (''since 0.11.2'')""") project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option( 'project', 'url', '', """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. (''since 0.11.3'')""") project_footer = Option( 'project', 'footer', N_('Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>'), """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = Option( 'logging', 'log_type', 'none', """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""") log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = Option( 'logging', 'log_level', 'DEBUG', """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""") log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: Trac[$(module)s] $(levelname)s: $(message)s In addition to regular key names supported by the Python logger library (see http://docs.python.org/library/logging.html), one could use: - $(path)s the path for the current environment - $(basename)s the last path component of the current environment - $(project)s the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the ConfigParser itself. Example: `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` ''(since 0.10.5)''""") def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. :param path: the absolute path to the Trac environment :param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. :param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = path self.systeminfo = [] self._href = self._abs_href = None if create: self.create(options) else: self.verify() self.setup_config() if create: for setup_participant in self.setup_participants: setup_participant.environment_created() def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = self.systeminfo[:] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) info.sort(key=lambda (name, version): (name != 'Trac', name.lower())) return info # ISystemInfoProvider methods def get_system_info(self): from trac import core, __version__ as VERSION yield 'Trac', get_pkginfo(core).get('version', VERSION) yield 'Python', sys.version yield 'setuptools', setuptools.__version__ from trac.util.datefmt import pytz if pytz is not None: yield 'pytz', pytz.__version__ def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @property def _component_rules(self): try: return self._rules except AttributeError: self._rules = {} for name, value in self.components_section.options(): if name.endswith('.*'): name = name[:-2] self._rules[name.lower()] = value.lower() in ('enabled', 'on') return self._rules def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns `False`, the component does not get activated. If it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) # Disable the pre-0.11 WebAdmin plugin # Please note that there's no recommendation to uninstall the # plugin because doing so would obviously break the backwards # compatibility that the new integration administration # interface tries to provide for old WebAdmin extensions if component_name.startswith('webadmin.'): self.log.info("The legacy TracWebAdmin plugin has been " "automatically disabled, and the integrated " "administration interface will be used " "instead.") return False rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package are enabled return component_name.startswith('trac.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" with open(os.path.join(self.path, 'VERSION'), 'r') as fd: assert fd.read(26) == 'Trac Environment Version 1' def get_db_cnx(self): """Return a database connection from the connection pool :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead `db_transaction` for obtaining the `db` database connection which can be used for performing any query (SELECT/INSERT/UPDATE/DELETE):: with env.db_transaction as db: ... `db_query` for obtaining a `db` database connection which can be used for performing SELECT queries only:: with env.db_query as db: ... """ return DatabaseManager(self).get_connection() def with_transaction(self, db=None): """Decorator for transaction functions :deprecated:""" return with_transaction(self, db) def get_read_db(self): """Return a database connection for read purposes :deprecated: See `trac.db.api.get_read_db` for detailed documentation.""" return DatabaseManager(self).get_connection(readonly=True) @property def db_query(self): """Return a context manager which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: self.log.removeHandler(self._log_handler) self._log_handler.flush() self._log_handler.close() del self._log_handler def get_repository(self, reponame=None, authname=None): """Return the version control repository with the given name, or the default repository if `None`. The standard way of retrieving repositories is to use the methods of `RepositoryManager`. This method is retained here for backward compatibility. :param reponame: the name of the repository :param authname: the user name for authorization (not used anymore, left here for compatibility with 0.11) """ return RepositoryManager(self).get_repository(reponame) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values. If options contains ('inherit', 'file'), default values will not be loaded; they are expected to be provided by that file or other options. """ # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.get_log_dir()) os.mkdir(self.get_htdocs_dir()) os.mkdir(os.path.join(self.path, 'plugins')) # Create a few files create_file(os.path.join(self.path, 'VERSION'), 'Trac Environment Version 1\n') create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(os.path.join(self.path, 'conf')) create_file(os.path.join(self.path, 'conf', 'trac.ini.sample')) config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the database DatabaseManager(self).init_db() def get_version(self, db=None, initial=False): """Return the current version of the database. If the optional argument `initial` is set to `True`, the version of the database used at the time of creation will be returned. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since: 0.11 :since 0.13: deprecation warning: the `db` parameter is no longer used and will be removed in version 0.14 """ rows = self.db_query(""" SELECT value FROM system WHERE name='%sdatabase_version' """ % ('initial_' if initial else '')) return rows and int(rows[0][0]) def setup_config(self): """Load the configuration file.""" self.config = Configuration( os.path.join(self.path, 'conf', 'trac.ini'), {'envname': os.path.basename(self.path)}) self.setup_log() from trac.loader import load_components plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) def get_templates_dir(self): """Return absolute path to the templates directory.""" return os.path.join(self.path, 'templates') def get_htdocs_dir(self): """Return absolute path to the htdocs directory.""" return os.path.join(self.path, 'htdocs') def get_log_dir(self): """Return absolute path to the log directory.""" return os.path.join(self.path, 'log') def setup_log(self): """Initialize the logging sub-system.""" from trac.log import logger_handler_factory logtype = self.log_type logfile = self.log_file if logtype == 'file' and not os.path.isabs(logfile): logfile = os.path.join(self.get_log_dir(), logfile) format = self.log_format if format: format = format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', os.path.basename(self.path)) \ .replace('%(project)s', self.project_name) self.log, self._log_handler = logger_handler_factory(logtype, logfile, self.log_level, self.path, format=format) from trac import core, __version__ as VERSION self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, get_pkginfo(core).get('version', VERSION)) def get_known_users(self, cnx=None): """Generator that yields information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. This function generates one tuple for every user, of the form (username, name, email) ordered alpha-numerically by username. :param cnx: the database connection; if ommitted, a new connection is retrieved :since 0.13: deprecation warning: the `cnx` parameter is no longer used and will be removed in version 0.14 """ for username, name, email in self.db_query(""" SELECT DISTINCT s.sid, n.value, e.value FROM session AS s LEFT JOIN session_attribute AS n ON (n.sid=s.sid and n.authenticated=1 AND n.name = 'name') LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=1 AND e.name = 'email') WHERE s.authenticated=1 ORDER BY s.sid """): yield username, name, email def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" with self.db_query as db: for participant in self.setup_participants: if participant.environment_needs_upgrade(db): self.log.warn("Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] with self.db_query as db: for participant in self.setup_participants: if participant.environment_needs_upgrade(db): upgraders.append(participant) if not upgraders: return if backup: self.backup(backup_dest) for participant in upgraders: self.log.info("%s.%s upgrading...", participant.__module__, participant.__class__.__name__) with self.db_transaction as db: participant.upgrade_environment(db) # Database schema may have changed, so close all connections DatabaseManager(self).shutdown() return True @property def href(self): """The application root path""" if not self._href: self._href = Href(urlsplit(self.abs_href.base)[2]) return self._href @property def abs_href(self): """The application URL""" if not self._abs_href: if not self.base_url: self.log.warn("base_url option not set in configuration, " "generated links may be incorrect") self._abs_href = Href('') else: self._abs_href = Href(self.base_url) return self._abs_href
class ConfigurationError(TracError): """Exception raised when a value in the configuration file is not valid.""" title = N_('Configuration Error')
class ComponentAdminPanel(TicketAdminPanel): _type = 'components' _label = (N_('Component'), N_('Components')) # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, component): # Detail view? if component: comp = model.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') try: comp.update() except self.env.db_exc.IntegrityError: raise TracError( _('Component "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'component': comp} else: default = self.config.get('ticket', 'default_component') if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: comp = model.Component(self.env, name=name) except ResourceNotFound: comp = model.Component(self.env) comp.name = name if req.args.get('owner'): comp.owner = req.args.get('owner') comp.insert() add_notice( req, _('The component "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if comp.name is None: raise TracError(_("Invalid component name.")) raise TracError( _('Component "%(name)s" already ' 'exists.', name=name)) # Remove components elif req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('No component selected')) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: model.Component(self.env, name).delete() add_notice( req, _("The selected components have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default component elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default component to %s", name) self.config.set('ticket', 'default_component', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) data = { 'view': 'list', 'components': list(model.Component.select(self.env)), 'default': default } owners = TicketSystem(self.env).get_allowed_owners() if owners is not None: owners.insert(0, '') data.update({'owners': owners}) return 'admin_components.html', data # IAdminCommandProvider methods def get_admin_commands(self): yield ('component list', '', 'Show available components', None, self._do_list) yield ('component add', '<name> [owner]', 'Add a new component', self._complete_add, self._do_add) yield ('component rename', '<name> <newname>', 'Rename a component', self._complete_remove_rename, self._do_rename) yield ('component remove', '<name>', 'Remove/uninstall a component', self._complete_remove_rename, self._do_remove) yield ('component chown', '<name> <owner>', 'Change component ownership', self._complete_chown, self._do_chown) def get_component_list(self): return [c.name for c in model.Component.select(self.env)] def get_user_list(self): return TicketSystem(self.env).get_allowed_owners() def _complete_add(self, args): if len(args) == 2: return self.get_user_list() def _complete_remove_rename(self, args): if len(args) == 1: return self.get_component_list() def _complete_chown(self, args): if len(args) == 1: return self.get_component_list() elif len(args) == 2: return self.get_user_list() def _do_list(self): print_table([(c.name, c.owner) for c in model.Component.select(self.env)], [_('Name'), _('Owner')]) def _do_add(self, name, owner=None): component = model.Component(self.env) component.name = name component.owner = owner component.insert() def _do_rename(self, name, newname): component = model.Component(self.env, name) component.name = newname component.update() def _do_remove(self, name): model.Component(self.env, name).delete() def _do_chown(self, name, owner): component = model.Component(self.env, name) component.owner = owner component.update()
class PriorityAdminPanel(AbstractEnumAdminPanel): _type = 'priority' _enum_cls = model.Priority _label = (N_('Priority'), N_('Priorities'))
def fields(self): """Return the list of fields available for tickets.""" from trac.ticket import model fields = TicketFieldList() # Basic text fields fields.append({ 'name': 'summary', 'type': 'text', 'label': N_('Summary') }) fields.append({ 'name': 'reporter', 'type': 'text', 'label': N_('Reporter') }) # Owner field, by default text but can be changed dynamically # into a drop-down depending on configuration (restrict_owner=true) fields.append({'name': 'owner', 'type': 'text', 'label': N_('Owner')}) # Description fields.append({ 'name': 'description', 'type': 'textarea', 'format': 'wiki', 'label': N_('Description') }) # Default select and radio fields selects = [('type', N_('Type'), model.Type), ('status', N_('Status'), model.Status), ('priority', N_('Priority'), model.Priority), ('milestone', N_('Milestone'), model.Milestone), ('component', N_('Component'), model.Component), ('version', N_('Version'), model.Version), ('severity', N_('Severity'), model.Severity), ('resolution', N_('Resolution'), model.Resolution)] for name, label, cls in selects: options = [val.name for val in cls.select(self.env)] if not options: # Fields without possible values are treated as if they didn't # exist continue field = { 'name': name, 'type': 'select', 'label': label, 'value': getattr(self, 'default_' + name, ''), 'options': options } if name in ('status', 'resolution'): field['type'] = 'radio' field['optional'] = True elif name in self.allowed_empty_fields: field['optional'] = True fields.append(field) # Advanced text fields fields.append({ 'name': 'keywords', 'type': 'text', 'format': 'list', 'label': N_('Keywords') }) fields.append({ 'name': 'cc', 'type': 'text', 'format': 'list', 'label': N_('Cc') }) # Date/time fields fields.append({ 'name': 'time', 'type': 'time', 'format': 'relative', 'label': N_('Created') }) fields.append({ 'name': 'changetime', 'type': 'time', 'format': 'relative', 'label': N_('Modified') }) for field in self.custom_fields: if field['name'] in [f['name'] for f in fields]: self.log.warning('Duplicate field name "%s" (ignoring)', field['name']) continue fields.append(field) return fields
class SeverityAdminPanel(AbstractEnumAdminPanel): _type = 'severity' _enum_cls = model.Severity _label = N_("Severity"), N_("Severities")
class ResolutionAdminPanel(AbstractEnumAdminPanel): _type = 'resolution' _enum_cls = model.Resolution _label = (N_('Resolution'), N_('Resolutions'))
class ResolutionAdminPanel(AbstractEnumAdminPanel): _type = 'resolution' _enum_cls = model.Resolution _label = N_("Resolution"), N_("Resolutions")
class SeverityAdminPanel(AbstractEnumAdminPanel): _type = 'severity' _enum_cls = model.Severity _label = (N_('Severity'), N_('Severities'))
def get_macro_description(self, name): return 'messages', \ N_("Provide a description list for the known InterWiki " "prefixes.")
class ComponentAdminPanel(TicketAdminPanel): _type = 'components' _label = N_("Component"), N_("Components") # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, component): # Detail view? if component: comp = model.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') comp.update() add_notice(req, _("Your changes have been saved.")) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) data = {'view': 'detail', 'component': comp} else: default = self.config.get('ticket', 'default_component') if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): comp = model.Component(self.env) comp.name = req.args.get('name') comp.owner = req.args.get('owner') comp.insert() add_notice(req, _('The component "%(name)s" has been ' 'added.', name=comp.name)) req.redirect(req.href.admin(cat, page)) # Remove components elif req.args.get('remove'): sel = req.args.getlist('sel') if not sel: raise TracError(_("No component selected")) with self.env.db_transaction: for name in sel: model.Component(self.env, name).delete() if name == default: self.config.set('ticket', 'default_component', '') self._save_config(req) add_notice(req, _("The selected components have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default component elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default component to %s", name) self.config.set('ticket', 'default_component', name) self._save_config(req) req.redirect(req.href.admin(cat, page)) # Clear default component elif req.args.get('clear'): self.log.info("Clearing default component") self.config.set('ticket', 'default_component', '') self._save_config(req) req.redirect(req.href.admin(cat, page)) data = {'view': 'list', 'components': list(model.Component.select(self.env)), 'default': default} owners = TicketSystem(self.env).get_allowed_owners() if owners is not None: owners.insert(0, '') data.update({'owners': owners}) return 'admin_components.html', data # IAdminCommandProvider methods def get_admin_commands(self): yield ('component list', '', "Show components", None, self._do_list) yield ('component add', '<name> [owner]', "Add component", self._complete_add, self._do_add) yield ('component rename', '<name> <newname>', "Rename component", self._complete_name, self._do_rename) yield ('component remove', '<name>', "Remove component", self._complete_name, self._do_remove) yield ('component chown', '<name> <owner>', "Change component owner", self._complete_chown, self._do_chown) def get_component_list(self): return [c.name for c in model.Component.select(self.env)] def get_user_list(self): return TicketSystem(self.env).get_allowed_owners() def _complete_add(self, args): if len(args) == 2: return self.get_user_list() def _complete_name(self, args): if len(args) == 1: return self.get_component_list() def _complete_chown(self, args): if len(args) == 1: return self.get_component_list() elif len(args) == 2: return self.get_user_list() def _do_list(self): print_table([(c.name, c.owner) for c in model.Component.select(self.env)], [_("Name"), _("Owner")]) def _do_add(self, name, owner=None): component = model.Component(self.env) component.name = name component.owner = owner component.insert() def _do_rename(self, name, newname): component = model.Component(self.env, name) component.name = newname component.update() def _do_remove(self, name): model.Component(self.env, name).delete() def _do_chown(self, name, owner): component = model.Component(self.env, name) component.owner = owner component.update()
def get_macro_description(self, name): return 'messages', N_("Provide a list of known InterTrac prefixes.")
class PriorityAdminPanel(AbstractEnumAdminPanel): _type = 'priority' _enum_cls = model.Priority _label = N_("Priority"), N_("Priorities")
def fields(self): """Return the list of fields available for crashes.""" from trac.ticket import model fields = [] # Basic text fields fields.append({'name': 'summary', 'type': 'text', 'label': N_('Summary')}) fields.append({'name': 'reporter', 'type': 'text', 'label': N_('Reporter')}) # Owner field, by default text but can be changed dynamically # into a drop-down depending on configuration (restrict_owner=true) field = {'name': 'owner', 'label': N_('Owner')} field['type'] = 'text' fields.append(field) simple_string_fields = [ ('uuid', N_('Crash identifier') ), ('applicationname', N_('Application') ), ('applicationfile', N_('Application file') ), ('uploadhostname', N_('Upload FQDN') ), ('uploadusername', N_('Upload username') ), ('crashhostname', N_('Crash FQDN') ), ('crashusername', N_('Crash username') ), ('productname', N_('Product name') ), ('productcodename', N_('Product code name') ), ('productversion', N_('Product version') ), ('producttargetversion', N_('Product target version') ), ('buildtype', N_('Build type') ), ('buildpostfix', N_('Build postfix') ), ('machinetype', N_('Machine type') ), ('systemname', N_('System name') ), ('osversion', N_('OS version') ), ('osrelease', N_('OS release') ), ('osmachine', N_('OS machine') ), ('minidumpfile', N_('Minidump file') ), ('minidumpreporttextfile', N_('Minidump text report') ), ('minidumpreportxmlfile', N_('Minidump XML report') ), ('minidumpreporthtmlfile', N_('Minidump HTML report') ), ('coredumpfile', N_('Coredump file') ), ('coredumpreporttextfile', N_('Coredump text report') ), ('coredumpreportxmlfile', N_('Coredump XML report') ), ('coredumpreporthtmlfile', N_('Coredump HTML report') ), ] for (name, label) in simple_string_fields: fields.append({'name': name, 'type': 'text', 'label': label}) # Description fields.append({'name': 'description', 'type': 'textarea', 'label': N_('Description')}) # Default select and radio fields selects = [('type', N_('Type'), model.Type), ('status', N_('Status'), model.Status), ('priority', N_('Priority'), model.Priority), ('milestone', N_('Milestone'), model.Milestone), ('component', N_('Component'), model.Component), ('version', N_('Version'), model.Version), ('severity', N_('Severity'), model.Severity), ('resolution', N_('Resolution'), model.Resolution)] for name, label, cls in selects: options = [val.name for val in cls.select(self.env)] if not options: # Fields without possible values are treated as if they didn't # exist continue field = {'name': name, 'type': 'select', 'label': label, 'value': getattr(self, 'default_' + name, ''), 'options': options} if name in ('status', 'resolution'): field['type'] = 'radio' field['optional'] = True elif name in ('milestone', 'version'): field['optional'] = True fields.append(field) # Advanced text fields fields.append({'name': 'keywords', 'type': 'text', 'format': 'list', 'label': N_('Keywords')}) fields.append({'name': 'cc', 'type': 'text', 'format': 'list', 'label': N_('Cc')}) # Date/time fields fields.append({'name': 'crashtime', 'type': 'time', 'label': N_('Crash time')}) fields.append({'name': 'reporttime', 'type': 'time', 'label': N_('Report time')}) fields.append({'name': 'uploadtime', 'type': 'time', 'label': N_('Upload time')}) fields.append({'name': 'changetime', 'type': 'time', 'label': N_('Modified')}) fields.append({'name': 'closetime', 'type': 'time', 'label': N_('Closed')}) for field in self.get_custom_fields(): if field['name'] in [f['name'] for f in fields]: self.log.warning('Duplicate field name "%s" (ignoring)', field['name']) continue if field['name'] in self.reserved_field_names: self.log.warning('Field name "%s" is a reserved name ' '(ignoring)', field['name']) continue if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']): self.log.warning('Invalid name for custom field: "%s" ' '(ignoring)', field['name']) continue field['custom'] = True fields.append(field) return fields