source: batchmodifyplugin/0.12/trunk/batchmod/web_ui.py

Last change on this file was 9374, checked in by CuriousCurmudgeon, 13 years ago

refs #7384

  • Changed the default connector from a comma to a space. This change will need highlighted so users are not surprised when upgrading.
File size: 10.9 KB
RevLine 
[1356]1# -*- coding: utf-8 -*-
2# Copyright (C) 2006 Ashwin Phatak
[2772]3# Copyright (C) 2007 Dave Gynn
[8222]4# Copyright (C) 2010 Brian Meeker
[1356]5
6from trac.core import *
[8120]7from trac.config import Option, ListOption
[8444]8from trac.db.api import with_transaction
[8120]9from trac.perm import IPermissionRequestor
[2772]10from trac.ticket import TicketSystem, Ticket
[1356]11from trac.ticket.query import QueryModule
[8818]12from trac.ticket.notification import TicketNotifyEmail
[2772]13from trac.web.api import ITemplateStreamFilter
[8324]14from trac.web.chrome import ITemplateProvider, Chrome, \
15                            add_script, add_stylesheet
[2772]16from trac.web.main import IRequestFilter
[8818]17from trac.util.datefmt import to_datetime, to_utimestamp, utc
[2772]18from genshi.filters.transform import Transformer
[8818]19from datetime import datetime
[7937]20import re
[1356]21
[2772]22__all__ = ['BatchModifyModule']
[1356]23
24class BatchModifyModule(Component):
[8324]25    implements(IPermissionRequestor, ITemplateProvider, IRequestFilter,
26               ITemplateStreamFilter)
[1356]27
[8324]28    fields_as_list = ListOption("batchmod", "fields_as_list", 
29                default="keywords", 
30                doc="field names modified as a value list(separated by ',')")
31    list_separator_regex = Option("batchmod", "list_separator_regex",
32                default='[,\s]+',
33                doc="separator regex used for 'list' fields")
34    list_connector_string = Option("batchmod", "list_connector_string",
[9374]35                default=' ',
36                doc="Connector string for 'list' fields. Defaults to a space.")
[8120]37
[2772]38    # IPermissionRequestor methods
39    def get_permission_actions(self):
40        yield 'TICKET_BATCH_MODIFY'
[1356]41
42    # ITemplateProvider methods
[2772]43    def get_htdocs_dirs(self):
[8222]44        """Return a list of directories with static resources (such as style
45        sheets, images, etc.)
46   
47        Each item in the list must be a `(prefix, abspath)` tuple. The
48        `prefix` part defines the path in the URL that requests to these
49        resources are prefixed with.
50   
51        The `abspath` is the absolute path to the directory containing the
52        resources on the local file system.
53        """
54        from pkg_resources import resource_filename
55        return [('batchmod', resource_filename(__name__, 'htdocs'))]
[1356]56
57    def get_templates_dirs(self):
58        from pkg_resources import resource_filename
59        return [resource_filename(__name__, 'templates')]
60
[2772]61    # IRequestFilter methods
62    def pre_process_request(self, req, handler):
63        """Look for QueryHandler posts and hijack them"""
[7389]64        if req.path_info == '/query' and req.method=='POST' and \
[8222]65            req.args.get('batchmod_submit') and self._has_permission(req):
[7389]66            self.log.debug('BatchModifyModule: executing')
[8222]67           
[8324]68            batch_modifier = BatchModifier(self.fields_as_list, 
69                                           self.list_separator_regex, 
70                                           self.list_connector_string)
[8222]71            batch_modifier.process_request(req, self.env, self.log)
[8120]72            # redirect to original Query
73            # TODO: need better way to fake QueryModule...
74            req.redirect(req.args.get('query_href'))
[2772]75        return handler
[1356]76
[8222]77
[2772]78    def post_process_request(self, req, template, content_type):
79        """No-op"""
80        return (template, content_type)
[1379]81
[2772]82    def post_process_request(self, req, template, data, content_type):
83        """No-op"""
84        return (template, data, content_type)
[8222]85
86    # ITemplateStreamFilter methods
87    def filter_stream(self, req, method, filename, stream, formdata):
88        """Adds BatchModify form to the query page"""
89        if filename == 'query.html' and self._has_permission(req):
90            self.log.debug('BatchModifyPlugin: rendering template')
91            return stream | Transformer('//div[@id="help"]'). \
92                                before(self._generate_form(req, formdata) )
93        return stream
94
[2772]95   
[8222]96    def _generate_form(self, req, data):
97        batchFormData = dict(data)
[8324]98        batchFormData['query_href']= req.session['query_href'] \
99                                     or req.href.query()
[8826]100        batchFormData['notify_enabled'] = self.config.getbool('notification', 
101                                                        'smtp_enabled', False)
[8222]102       
103        ticketSystem = TicketSystem(self.env)
104        fields = []
105        for field in ticketSystem.get_ticket_fields():
106            if field['name'] not in ('summary', 'reporter', 'description'):
107                fields.append(field)
[8324]108            if field['name'] == 'owner' \
109                and hasattr(ticketSystem, 'eventually_restrict_owner'):
[8222]110                ticketSystem.eventually_restrict_owner(field)
111        fields.sort(key=lambda f: f['name'])
112        batchFormData['fields']=fields
113
114        add_script(req, 'batchmod/js/batchmod.js')
115        add_stylesheet(req, 'batchmod/css/batchmod.css')
116        stream = Chrome(self.env).render_template(req, 'batchmod.html',
117              batchFormData, fragment=True)
118        return stream.select('//form[@id="batchmod_form"]')
119       
120    # Helper methods
121    def _has_permission(self, req):
122        return req.perm.has_permission('TICKET_ADMIN') or \
123                req.perm.has_permission('TICKET_BATCH_MODIFY')
124
125class BatchModifier:
126    """Modifies a batch of tickets"""
127   
[8324]128    def __init__(self, fields_as_list, list_separator_regex, 
129                 list_connector_string):
[8222]130        """Pull all the config values in."""
131        self._fields_as_list = fields_as_list
132        self._list_separator_regex = list_separator_regex
133        self._list_connector_string = list_connector_string
134   
135        # Internal methods
136    def process_request(self, req, env, log):
[1356]137        tickets = req.session['query_tickets'].split(' ')
[8222]138        comment = req.args.get('batchmod_value_comment', '')
[8324]139        modify_changetime = bool(req.args.get(
140                                              'batchmod_modify_changetime',
141                                              False))
[8818]142        send_notifications = bool(req.args.get(
143                                              'batchmod_send_notifications',
144                                              False))
[8222]145       
146        values = self._get_new_ticket_values(req, env) 
147        self._check_for_resolution(values)
148        self._remove_resolution_if_not_closed(values)
[2772]149
150        selectedTickets = req.args.get('selectedTickets')
[8222]151        log.debug('BatchModifyPlugin: selected tickets: %s', selectedTickets)
[8324]152        selectedTickets = isinstance(selectedTickets, list) \
153                          and selectedTickets or selectedTickets.split(',')
[2772]154        if not selectedTickets:
[8222]155            raise TracError, 'No tickets selected'
[2772]156       
[8358]157        self._save_ticket_changes(req, env, log, selectedTickets, tickets, 
[8818]158                                  values, comment, modify_changetime, send_notifications)
[8344]159
160    def _get_new_ticket_values(self, req, env):
161        """Pull all of the new values out of the post data."""
162        values = {}
[9370]163       
164        # Get the current users name.
165        if req.authname and req.authname != 'anonymous':
166            user = req.authname
167        else:
168            user = req.session.get('email') or req.session.get('name') or None
169       
[8344]170        for field in TicketSystem(env).get_ticket_fields():
171            name = field['name']
172            if name not in ('summary', 'reporter', 'description'):
173                value = req.args.get('batchmod_value_' + name)
[9370]174                if name == 'owner' and value == '$USER':
175                    value = user
[8344]176                if value is not None:
177                    values[name] = value
178        return values
179   
180    def _check_for_resolution(self, values):
181        """If a resolution has been set the status is automatically
182        set to closed."""
183        if values.has_key('resolution'):
184            values['status'] = 'closed'
185   
186    def _remove_resolution_if_not_closed(self, values):
187        """If the status is set to something other than closed the
188        resolution should be removed."""
189        if values.has_key('status') and values['status'] is not 'closed':
190            values['resolution'] = ''
[8818]191 
192    def _save_ticket_changes(self, req, env, log, selectedTickets, tickets,
193                             new_values, comment, modify_changetime, send_notifications):
[8446]194        @with_transaction(env)
[8346]195        def _implementation(db):
196            for id in selectedTickets:
197                if id in tickets:
198                    t = Ticket(env, int(id))
[8818]199                    new_changetime = datetime.now(utc)
[8346]200                   
201                    log_msg = ""
202                    if not modify_changetime:
203                        original_changetime = to_utimestamp(t.time_changed)
204                   
[8448]205                    _values = new_values.copy()
206                    for field in [f for f in new_values.keys() \
[8346]207                                  if f in self._fields_as_list]:
[8347]208                        _values[field] = self._merge_keywords(t.values[field],
[8448]209                                                              new_values[field],
[8346]210                                                              log)
211                   
212                    t.populate(_values)
[8818]213                    t.save_changes(req.authname, comment, when=new_changetime)
[1356]214
[8818]215                    if send_notifications:
216                        tn = TicketNotifyEmail(env)
217                        tn.notify(t, newticket=0, modtime=new_changetime)
218
[8346]219                    if not modify_changetime:
[8347]220                        self._reset_changetime(env, original_changetime, t)
[8346]221                        log_msg = "(changetime not modified)"
[7936]222
[8346]223                    log.debug('BatchModifyPlugin: saved changes to #%s %s' % 
224                              (id, log_msg))
[7936]225
[8222]226    def _merge_keywords(self, original_keywords, new_keywords, log):
227        """
228        Prevent duplicate keywords by merging the two lists.
229        Any keywords prefixed with '-' will be removed.
230        """
[8324]231        log.debug('BatchModifyPlugin: existing keywords are %s', 
232                  original_keywords)
[8222]233        log.debug('BatchModifyPlugin: new keywords are %s', new_keywords)
[7531]234       
[8222]235        regexp = re.compile(self._list_separator_regex)
[7967]236       
[8120]237        new_keywords = [k.strip() for k in regexp.split(new_keywords) if k]
[8324]238        combined_keywords = [k.strip() for k
239                             in regexp.split(original_keywords) if k]
[7967]240       
241        for keyword in new_keywords:
242            if keyword.startswith('-'):
243                keyword = keyword[1:]
[8120]244                while keyword in combined_keywords:
[7967]245                    combined_keywords.remove(keyword)
246            else:
[8120]247                if keyword not in combined_keywords:
248                    combined_keywords.append(keyword)
[7967]249       
[8324]250        log.debug('BatchModifyPlugin: combined keywords are %s', 
251                  combined_keywords)
252        return self._list_connector_string.join(combined_keywords)
253   
254    def _reset_changetime(self, env, original_changetime, ticket):
255        db = env.get_db_cnx()
256        db.cursor().execute("UPDATE ticket set changetime=%s where id=%s" 
257                            % (original_changetime, ticket.id))
[8818]258        db.commit()
Note: See TracBrowser for help on using the repository browser.