Server : Apache System : Linux iad1-shared-b8-43 6.6.49-grsec-jammy+ #10 SMP Thu Sep 12 23:23:08 UTC 2024 x86_64 User : dh_edsupp ( 6597262) PHP Version : 8.2.26 Disable Function : NONE Directory : /lib/python3/dist-packages/trac/ticket/ |
Upload File : |
# -*- coding: utf-8 -*- # # Copyright (C) 2005-2021 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at https://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at https://trac.edgewall.org/. from trac.admin.api import AdminCommandError, IAdminCommandProvider, \ IAdminPanelProvider, console_date_format, \ console_datetime_format, get_console_locale from trac.core import * from trac.resource import ResourceNotFound from trac.ticket import model from trac.ticket.api import TicketSystem from trac.ticket.roadmap import ( MilestoneModule, get_num_tickets_for_milestone, group_milestones) from trac.util import as_int, getuser from trac.util.datefmt import format_date, format_datetime, \ get_datetime_format_hint, parse_date, user_time from trac.util.text import exception_to_unicode, print_table, printout from trac.util.translation import _, N_, gettext from trac.web.chrome import Chrome, add_ctxtnav, add_notice, add_script, \ add_warning class TicketAdminPanel(Component): implements(IAdminPanelProvider, IAdminCommandProvider) abstract = True _type = 'undefined' _label = N_("(Undefined)"), N_("(Undefined)") # 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 'TICKET_ADMIN' in req.perm('admin', 'ticket/' + self._type): 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) from 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.") 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)) # 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.")) # 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) # 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() class MilestoneAdminPanel(TicketAdminPanel): _type = 'milestones' _label = N_("Milestone"), N_("Milestones") # TicketAdminPanel methods def get_admin_panels(self, req): perm = req.perm('admin', 'ticket/' + self._type) if 'MILESTONE_ADMIN' in perm or \ 'MILESTONE_VIEW' in perm and 'TICKET_ADMIN' in perm: yield ('ticket', _('Ticket System'), self._type, gettext(self._label[1])) 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)) data = { 'view': 'detail', 'milestone': milestone, 'default_due': milestone_module.get_default_due(req), 'retarget_to': milestone_module.default_retarget_to } milestones = [m for m in model.Milestone.select(self.env) if m.name != milestone.name and 'MILESTONE_VIEW' in req.perm(m.resource)] data['milestone_groups'] = \ group_milestones(milestones, 'TICKET_ADMIN' in req.perm) data['num_open_tickets'] = \ get_num_tickets_for_milestone(self.env, milestone, exclude_closed=True) chrome = Chrome(self.env) chrome.add_wiki_toolbars(req) chrome.add_auto_preview(req) add_ctxtnav(req, _("View Milestone"), req.href.milestone(milestone_name)) # 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) 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.")) # Set default ticket milestone and retarget milestone elif 'apply' in req.args: save = False perm_cache.require('TICKET_ADMIN') 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) # Clear default ticket milestone and retarget milestone elif 'clear' in req.args: perm_cache.require('TICKET_ADMIN') 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 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') ver.time = self._get_user_time(req) 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') ver.time = self._get_user_time(req) ver.insert() add_notice(req, _('The version "%(name)s" has been ' 'added.', name=ver.name)) # 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.")) # 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) # 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 @classmethod def _get_user_time(cls, req): time = req.args.get('time') return user_time(req, parse_date, time, hint='datetime') \ if time else None # 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 AbstractEnumAdminPanel(TicketAdminPanel): abstract = True _type = 'unknown' _enum_cls = None # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, path_info): label = [gettext(each) for each in self._label] data = {'label_singular': label[0], 'label_plural': label[1], 'type': self._type} # Detail view? if path_info: enum = self._enum_cls(self.env, path_info) if req.method == 'POST': if req.args.get('save'): enum.name = req.args.get('name') enum.description = req.args.get('description') enum.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.update({'view': 'detail', 'enum': enum}) else: default = self.config.get('ticket', 'default_%s' % self._type) if req.method == 'POST': # Add enum if req.args.get('add') and req.args.get('name'): enum = self._enum_cls(self.env) enum.name = req.args.get('name') enum.insert() add_notice(req, _('The %(field)s value "%(name)s" ' 'has been added.', field=label[0], name=enum.name)) # Remove enums elif req.args.get('remove'): sel = req.args.getlist('sel') if not sel: raise TracError(_("No %s selected") % self._type) with self.env.db_transaction: for name in sel: self._enum_cls(self.env, name).delete() if name == default: self.config.set('ticket', 'default_%s' % self._type, '') self.config.save() add_notice(req, _("The selected %(field)s values have " "been removed.", field=label[0])) # Apply changes elif req.args.get('apply'): # Set default value name = req.args.get('default') if name and name != default: self.log.info("Setting default %s to %s", self._type, name) self.config.set('ticket', 'default_%s' % self._type, name) self._save_config(req) # Change enum values order = {str(int(key[6:])): str(req.args.getint(key)) for key in req.args if key.startswith('value_')} values = {val: True for val in order.values()} if len(order) != len(values): raise TracError(_("Order numbers must be unique")) changed = False with self.env.db_transaction: for enum in self._enum_cls.select(self.env): new_value = order[enum.value] if new_value != enum.value: enum.value = new_value enum.update() changed = True if changed: add_notice(req, _("Your changes have been saved.")) # Clear default elif req.args.get('clear'): self.log.info("Clearing default %s", self._type) self.config.set('ticket', 'default_%s' % self._type, '') self._save_config(req) req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_jquery_ui(req) add_script(req, 'common/js/admin_enums.js') data.update(dict(enums=list(self._enum_cls.select(self.env)), default=default, view='list')) return 'admin_enums.html', data # IAdminCommandProvider methods _command_help = { 'list': "Show possible ticket %s", 'add': "Add a %s value option", 'change': "Change a %s value", 'remove': "Remove a %s value", 'order': "Move a %s value up or down in the list", } def get_admin_commands(self): enum_type = getattr(self, '_command_type', self._type) label = tuple(each.lower() for each in self._label) yield ('%s list' % enum_type, '', self._command_help['list'] % label[1], None, self._do_list) yield ('%s add' % enum_type, '<value>', self._command_help['add'] % label[0], None, self._do_add) yield ('%s change' % enum_type, '<value> <newvalue>', self._command_help['change'] % label[0], self._complete_change_remove, self._do_change) yield ('%s remove' % enum_type, '<value>', self._command_help['remove'] % label[0], self._complete_change_remove, self._do_remove) yield ('%s order' % enum_type, '<value> up|down', self._command_help['order'] % label[0], self._complete_order, self._do_order) def get_enum_list(self): return [e.name for e in self._enum_cls.select(self.env)] def _complete_change_remove(self, args): if len(args) == 1: return self.get_enum_list() def _complete_order(self, args): if len(args) == 1: return self.get_enum_list() elif len(args) == 2: return ['up', 'down'] def _do_list(self): print_table([(e.name,) for e in self._enum_cls.select(self.env)], [_("Possible Values")]) def _do_add(self, name): enum = self._enum_cls(self.env) enum.name = name enum.insert() def _do_change(self, name, newname): enum = self._enum_cls(self.env, name) enum.name = newname enum.update() def _do_remove(self, value): self._enum_cls(self.env, value).delete() def _do_order(self, name, up_down): if up_down not in ('up', 'down'): raise AdminCommandError(_("Invalid up/down value: %(value)s", value=up_down)) direction = -1 if up_down == 'up' else 1 enum1 = self._enum_cls(self.env, name) enum1.value = int(float(enum1.value) + direction) for enum2 in self._enum_cls.select(self.env): if int(float(enum2.value)) == enum1.value: enum2.value = int(float(enum2.value) - direction) break else: return with self.env.db_transaction: enum1.update() enum2.update() class PriorityAdminPanel(AbstractEnumAdminPanel): _type = 'priority' _enum_cls = model.Priority _label = model.Priority.label class ResolutionAdminPanel(AbstractEnumAdminPanel): _type = 'resolution' _enum_cls = model.Resolution _label = model.Resolution.label class SeverityAdminPanel(AbstractEnumAdminPanel): _type = 'severity' _enum_cls = model.Severity _label = model.Severity.label class TicketTypeAdminPanel(AbstractEnumAdminPanel): _type = 'type' _enum_cls = model.Type _label = model.Type.label _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 TicketAdmin(Component): """trac-admin command provider for ticket administration.""" implements(IAdminCommandProvider) # IAdminCommandProvider methods def get_admin_commands(self): yield ('ticket remove', '<ticket#>', 'Remove ticket', None, self._do_remove) yield ('ticket remove_comment', '<ticket#> <comment#>', 'Remove ticket comment', None, self._do_remove_comment) def _do_remove(self, number): number = as_int(number, None) if number is None: raise AdminCommandError(_("<ticket#> must be a number")) with self.env.db_transaction: model.Ticket(self.env, number).delete() printout(_("Ticket #%(num)s and all associated data removed.", num=number)) def _do_remove_comment(self, ticket_number, comment_number): ticket_number = as_int(ticket_number, None) if ticket_number is None: raise AdminCommandError(_('<ticket#> must be a number')) comment_number = as_int(comment_number, None) if comment_number is None: raise AdminCommandError(_('<comment#> must be a number')) with self.env.db_transaction: ticket = model.Ticket(self.env, ticket_number) change = ticket.get_change(comment_number) if not change: raise AdminCommandError(_("Comment %(num)s not found", num=comment_number)) ticket.delete_change(comment_number) printout(_("The ticket comment %(num)s on ticket #%(id)s has been " "deleted.", num=comment_number, id=ticket_number))