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

Last change on this file since 9374 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
Line 
1# -*- coding: utf-8 -*-
2# Copyright (C) 2006 Ashwin Phatak
3# Copyright (C) 2007 Dave Gynn
4# Copyright (C) 2010 Brian Meeker
5
6from trac.core import *
7from trac.config import Option, ListOption
8from trac.db.api import with_transaction
9from trac.perm import IPermissionRequestor
10from trac.ticket import TicketSystem, Ticket
11from trac.ticket.query import QueryModule
12from trac.ticket.notification import TicketNotifyEmail
13from trac.web.api import ITemplateStreamFilter
14from trac.web.chrome import ITemplateProvider, Chrome, \
15                            add_script, add_stylesheet
16from trac.web.main import IRequestFilter
17from trac.util.datefmt import to_datetime, to_utimestamp, utc
18from genshi.filters.transform import Transformer
19from datetime import datetime
20import re
21
22__all__ = ['BatchModifyModule']
23
24class BatchModifyModule(Component):
25    implements(IPermissionRequestor, ITemplateProvider, IRequestFilter,
26               ITemplateStreamFilter)
27
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",
35                default=' ',
36                doc="Connector string for 'list' fields. Defaults to a space.")
37
38    # IPermissionRequestor methods
39    def get_permission_actions(self):
40        yield 'TICKET_BATCH_MODIFY'
41
42    # ITemplateProvider methods
43    def get_htdocs_dirs(self):
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'))]
56
57    def get_templates_dirs(self):
58        from pkg_resources import resource_filename
59        return [resource_filename(__name__, 'templates')]
60
61    # IRequestFilter methods
62    def pre_process_request(self, req, handler):
63        """Look for QueryHandler posts and hijack them"""
64        if req.path_info == '/query' and req.method=='POST' and \
65            req.args.get('batchmod_submit') and self._has_permission(req):
66            self.log.debug('BatchModifyModule: executing')
67           
68            batch_modifier = BatchModifier(self.fields_as_list, 
69                                           self.list_separator_regex, 
70                                           self.list_connector_string)
71            batch_modifier.process_request(req, self.env, self.log)
72            # redirect to original Query
73            # TODO: need better way to fake QueryModule...
74            req.redirect(req.args.get('query_href'))
75        return handler
76
77
78    def post_process_request(self, req, template, content_type):
79        """No-op"""
80        return (template, content_type)
81
82    def post_process_request(self, req, template, data, content_type):
83        """No-op"""
84        return (template, data, content_type)
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
95   
96    def _generate_form(self, req, data):
97        batchFormData = dict(data)
98        batchFormData['query_href']= req.session['query_href'] \
99                                     or req.href.query()
100        batchFormData['notify_enabled'] = self.config.getbool('notification', 
101                                                        'smtp_enabled', False)
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)
108            if field['name'] == 'owner' \
109                and hasattr(ticketSystem, 'eventually_restrict_owner'):
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   
128    def __init__(self, fields_as_list, list_separator_regex, 
129                 list_connector_string):
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):
137        tickets = req.session['query_tickets'].split(' ')
138        comment = req.args.get('batchmod_value_comment', '')
139        modify_changetime = bool(req.args.get(
140                                              'batchmod_modify_changetime',
141                                              False))
142        send_notifications = bool(req.args.get(
143                                              'batchmod_send_notifications',
144                                              False))
145       
146        values = self._get_new_ticket_values(req, env) 
147        self._check_for_resolution(values)
148        self._remove_resolution_if_not_closed(values)
149
150        selectedTickets = req.args.get('selectedTickets')
151        log.debug('BatchModifyPlugin: selected tickets: %s', selectedTickets)
152        selectedTickets = isinstance(selectedTickets, list) \
153                          and selectedTickets or selectedTickets.split(',')
154        if not selectedTickets:
155            raise TracError, 'No tickets selected'
156       
157        self._save_ticket_changes(req, env, log, selectedTickets, tickets, 
158                                  values, comment, modify_changetime, send_notifications)
159
160    def _get_new_ticket_values(self, req, env):
161        """Pull all of the new values out of the post data."""
162        values = {}
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       
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)
174                if name == 'owner' and value == '$USER':
175                    value = user
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'] = ''
191 
192    def _save_ticket_changes(self, req, env, log, selectedTickets, tickets,
193                             new_values, comment, modify_changetime, send_notifications):
194        @with_transaction(env)
195        def _implementation(db):
196            for id in selectedTickets:
197                if id in tickets:
198                    t = Ticket(env, int(id))
199                    new_changetime = datetime.now(utc)
200                   
201                    log_msg = ""
202                    if not modify_changetime:
203                        original_changetime = to_utimestamp(t.time_changed)
204                   
205                    _values = new_values.copy()
206                    for field in [f for f in new_values.keys() \
207                                  if f in self._fields_as_list]:
208                        _values[field] = self._merge_keywords(t.values[field],
209                                                              new_values[field],
210                                                              log)
211                   
212                    t.populate(_values)
213                    t.save_changes(req.authname, comment, when=new_changetime)
214
215                    if send_notifications:
216                        tn = TicketNotifyEmail(env)
217                        tn.notify(t, newticket=0, modtime=new_changetime)
218
219                    if not modify_changetime:
220                        self._reset_changetime(env, original_changetime, t)
221                        log_msg = "(changetime not modified)"
222
223                    log.debug('BatchModifyPlugin: saved changes to #%s %s' % 
224                              (id, log_msg))
225
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        """
231        log.debug('BatchModifyPlugin: existing keywords are %s', 
232                  original_keywords)
233        log.debug('BatchModifyPlugin: new keywords are %s', new_keywords)
234       
235        regexp = re.compile(self._list_separator_regex)
236       
237        new_keywords = [k.strip() for k in regexp.split(new_keywords) if k]
238        combined_keywords = [k.strip() for k
239                             in regexp.split(original_keywords) if k]
240       
241        for keyword in new_keywords:
242            if keyword.startswith('-'):
243                keyword = keyword[1:]
244                while keyword in combined_keywords:
245                    combined_keywords.remove(keyword)
246            else:
247                if keyword not in combined_keywords:
248                    combined_keywords.append(keyword)
249       
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))
258        db.commit()
Note: See TracBrowser for help on using the repository browser.