# Ticket deleting plugins from trac import __version__ as TRAC_VERSION from trac import ticket from trac.admin.api import IAdminPanelProvider from trac.core import * from trac.ticket.model import Ticket from trac.web.api import IRequestFilter from trac.web.chrome import add_ctxtnav, add_notice from trac.web.chrome import add_script from trac.web.chrome import add_stylesheet from trac.web.chrome import add_warning from trac.web.chrome import ITemplateProvider from trac.ticket.web_ui import TicketModule from trac.util import sorted from trac.util.datefmt import to_datetime, utc, to_timestamp import re import traceback import pprint from time import strftime, localtime, mktime from datetime import datetime __all__ = ['TicketDeletePlugin'] class TicketDeletePlugin(Component): """A small ticket deletion plugin.""" implements(ITemplateProvider, IAdminPanelProvider, IRequestFilter) ### IRequestFilter methods def pre_process_request(self, req, handler): if isinstance(handler, TicketModule) and 'TICKET_ADMIN' in req.perm: add_script(req, 'ticketdelete/ticketdelete.js') add_stylesheet(req, 'ticketdelete/ticketdelete.css') return handler def post_process_request(self, req, template, data, content_type): return template, data, content_type ### IAdminPanelProvider methods def get_admin_panels(self, req): if 'TICKET_ADMIN' in req.perm: yield ('ticket', 'Ticket System', 'delete', 'Delete Tickets') yield ('ticket', 'Ticket System', 'comments', 'Delete Changes') def render_admin_panel(self, req, cat, page, path_info): req.perm.require('TICKET_ADMIN') data = {} data['href'] = req.href('admin', cat, page) data['page'] = page data['redir'] = 1 data['changes'] = {} data['id'] = 0 if req.method == 'POST': if page == 'delete': if 'ticketid' in req.args: t = self._validate(req, req.args.get('ticketid')) if t: self._delete_ticket(t.id) add_notice(req, "Ticket #%s has been deleted." % t.id) req.redirect(req.href('admin', cat, 'delete')) elif page == 'comments': if 'ticketid' in req.args: ticket_id = req.args.get('ticketid') req.redirect(req.href.admin(cat, page, ticket_id)) else: t = self._validate(req, path_info) if t: data['href'] = req.href('admin', cat, page, path_info) deletions = None if "multidelete" in req.args: deletions = [x.split('_') for x in req.args.getlist('mdelete')] deletions.sort(lambda a,b: cmp(b[1],a[1])) else: buttons = [x[6:] for x in req.args.keys() if x.startswith('delete')] deletions = [buttons[0].split('_')] if deletions: for field, ts in deletions: if ts != '': self.log.debug('TicketDelete: Deleting change to ticket %s at %s (%s)'%(t.id,ts,field)) self._delete_change(t.id, ts, field) add_notice(req, "Change to ticket #%s at %s has been modified" % (t.id, ts)) data['redir'] = 0 if path_info: t = self._validate(req, path_info) if t: if page == 'comments': try: selected = int(req.args.get('cnum')) - 1 except (TypeError, ValueError): selected = None ticket_data = {} for time, author, field, oldvalue, newvalue, perm in t.get_changelog(): c_data = ticket_data.setdefault(to_timestamp(time), {}) c_data.setdefault('fields', {})[field] = {'old': oldvalue, 'new': newvalue} c_data['author'] = author # FIXME: The datetime handling is not working - enable # for traceback c_data['prettytime'] = strftime('%a, %d %b %Y %H:%M:%S',time.timetuple()) # Check the boxes next to change number `selected` time_list = list(sorted(ticket_data.iterkeys())) if selected is not None and selected < len(time_list): ticket_data[time_list[selected]]['checked'] = True data['changes'] = ticket_data data['id'] = t.id # cnum is only in the args dictionary if we navigated from the ticket page if 'cnum' in req.args: add_ctxtnav(req, "Back to Ticket #%s" % t.id, req.href.ticket(t.id)) elif page == 'delete': data['id'] = t.id return 'ticketdelete_admin.html', {'ticketdelete': data} ### ITemplateProvider methods def get_templates_dirs(self): """ Return the absolute path of the directory containing the provided ClearSilver templates. """ from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] def get_htdocs_dirs(self): """ Return a list of directories with static resources (such as style sheets, images, etc.) Each item in the list must be a `(prefix, abspath)` tuple. The `prefix` part defines the path in the URL that requests to these resources are prefixed with. The `abspath` is the absolute path to the directory containing the resources on the local file system. """ from pkg_resources import resource_filename return [('ticketdelete', resource_filename(__name__, 'htdocs'))] ### Internal methods def _get_trac_version(self): md = re.match('(\d+)\.(\d+)',TRAC_VERSION) if md: return (int(md.group(1)),int(md.group(2))) else: return (0,0) def _validate(self, req, arg): """Validate that arg is a string containing a valid ticket ID.""" try: id = int(arg) t = Ticket(self.env, id) return t except TracError: add_warning(req, "Ticket #%s not found. Please try again." % id) except ValueError: add_warning(req, "Ticket ID '%s' is not valid. Please try again." % arg) return False def _delete_ticket(self, id): """Delete the given ticket ID.""" major, minor = self._get_trac_version() if major > 0 or minor >= 10: ticket = Ticket(self.env,id) ticket.delete() else: db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("DELETE FROM ticket WHERE id=%s", (id,)) cursor.execute("DELETE FROM ticket_change WHERE ticket=%s", (id,)) cursor.execute("DELETE FROM attachment WHERE type='ticket' and id=%s", (id,)) cursor.execute("DELETE FROM ticket_custom WHERE ticket=%s", (id,)) db.commit() def _delete_change(self, id, ts, field=None): """Delete the change on the given ticket at the given timestamp.""" db = self.env.get_db_cnx() cursor = db.cursor() ticket = Ticket(self.env,id) if field: if field == 'attachment': cursor.execute("DELETE FROM attachment WHERE type = 'ticket' AND id = %s AND time = %s", (id, ts)) else: custom_fields = [f['name'] for f in ticket.fields if f.get('custom')] if field != "comment" and not [1 for time, author, field2, oldval, newval, _ in ticket.get_changelog() if to_timestamp(time) > int(ts) and field == field2]: oldval = [old for _, _, field2, old, _, _ in ticket.get_changelog(to_datetime(int(ts))) if field2 == field][0] if field in custom_fields: cursor.execute("UPDATE ticket_custom SET value=%s WHERE ticket=%s AND name=%s", (oldval, id, field)) else: cursor.execute("UPDATE ticket SET %s=%%s WHERE id=%%s" % field, (oldval, id)) cursor.execute("DELETE FROM ticket_change WHERE ticket = %s AND time = %s AND field = %s", (id, ts, field)) else: for _, _, field, _, _, _ in ticket.get_changelog(to_datetime(int(ts), utc)): self._delete_change(id, ts, field) db.commit()