Edgewall Software

Ticket #3093: web_ui.py

File web_ui.py, 20.1 KB (added by anonymous, 3 years ago)

Modified web_ui.py

Line 
1# -*- coding: iso-8859-1 -*-
2#
3# Copyright (C) 2003-2005 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgstr�jonas@edgewall.com>
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at http://trac.edgewall.com/license.html.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at http://projects.edgewall.com/trac/.
14#
15# Author: Jonas Borgstr�jonas@edgewall.com>
16
17from __future__ import generators
18import os
19import re
20import time
21
22from trac import util
23from trac.attachment import attachment_to_hdf, Attachment
24from trac.core import *
25from trac.env import IEnvironmentSetupParticipant
26from trac.Notify import TicketNotifyEmail
27from trac.ticket import Milestone, Ticket, TicketSystem
28from trac.Timeline import ITimelineEventProvider
29from trac.web import IRequestHandler
30from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
31from trac.wiki import wiki_to_html, wiki_to_oneliner
32
33class NewticketModule(Component):
34
35    implements(IEnvironmentSetupParticipant, INavigationContributor,
36               IRequestHandler)
37
38    # IEnvironmentSetupParticipant methods
39
40    def environment_created(self):
41        """Create the `site_newticket.cs` template file in the environment."""
42        if self.env.path:
43            templates_dir = os.path.join(self.env.path, 'templates')
44            if not os.path.exists(templates_dir):
45                os.mkdir(templates_dir)
46            template_name = os.path.join(templates_dir, 'site_newticket.cs')
47            template_file = file(template_name, 'w')
48            template_file.write("""<?cs
49####################################################################
50# New ticket prelude - Included directly above the new ticket form
51?>
52""")
53
54    def environment_needs_upgrade(self, db):
55        return False
56
57    def upgrade_environment(self, db):
58        pass
59
60    # INavigationContributor methods
61
62    def get_active_navigation_item(self, req):
63        return 'newticket'
64
65    def get_navigation_items(self, req):
66        if not req.perm.has_permission('TICKET_CREATE'):
67            return
68        yield ('mainnav', 'newticket', 
69               util.Markup('<a href="%s" accesskey="7">New Ticket</a>',
70                           self.env.href.newticket()))
71
72    # IRequestHandler methods
73
74    def match_request(self, req):
75        return re.match(r'/newticket/?', req.path_info) is not None
76
77    def process_request(self, req):
78        req.perm.assert_permission('TICKET_CREATE')
79
80        db = self.env.get_db_cnx()
81
82        if req.method == 'POST' and not req.args.has_key('preview'):
83            self._do_create(req, db)
84
85        ticket = Ticket(self.env, db=db)
86        ticket.populate(req.args)
87        ticket.values.setdefault('reporter', util.get_reporter_id(req))
88
89        if ticket.values.has_key('description'):
90            description = wiki_to_html(ticket['description'], self.env, req, db)
91            req.hdf['newticket.description_preview'] = description
92
93        req.hdf['title'] = 'New Ticket'
94        req.hdf['newticket'] = ticket.values
95
96        field_names = [field['name'] for field in ticket.fields
97                       if not field.get('custom')]
98        if 'owner' in field_names:
99            curr_idx = field_names.index('owner')
100            if 'cc' in field_names:
101                insert_idx = field_names.index('cc')
102            else:
103                insert_idx = len(field_names)
104            if curr_idx < insert_idx:
105                ticket.fields.insert(insert_idx, ticket.fields[curr_idx])
106                del ticket.fields[curr_idx]
107
108        for field in ticket.fields:
109            name = field['name']
110            del field['name']
111            if name in ('summary', 'reporter', 'description', 'type', 'status',
112                        'resolution'):
113                field['skip'] = True
114            elif name == 'owner':
115                field['label'] = 'Assign to'
116            elif name == 'milestone':
117                # Don't make completed milestones available for selection
118                options = field['options'][:]
119                for option in field['options']:
120                    milestone = Milestone(self.env, option, db=db)
121                    if milestone.is_completed:
122                        options.remove(option)
123                field['options'] = options
124            req.hdf['newticket.fields.' + name] = field
125
126        add_stylesheet(req, 'common/css/ticket.css')
127        return 'newticket.cs', None
128
129    # Internal methods
130
131    def _do_create(self, req, db):
132        if not req.args.get('summary'):
133            raise TracError('Tickets must contain a summary.')
134
135        ticket = Ticket(self.env, db=db)
136        ticket.values.setdefault('reporter', util.get_reporter_id(req))
137        ticket.populate(req.args)
138        ticket.insert(db=db)
139        db.commit()
140
141        # Notify
142        try:
143            tn = TicketNotifyEmail(self.env)
144            tn.notify(req, ticket, newticket=True)
145        except Exception, e:
146            self.log.exception("Failure sending notification on creation of "
147                               "ticket #%s: %s" % (ticket.id, e))
148
149        # Redirect the user to the newly created ticket
150        req.redirect(self.env.href.ticket(ticket.id))
151
152
153class TicketModule(Component):
154
155    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider)
156
157    # INavigationContributor methods
158
159    def get_active_navigation_item(self, req):
160        return 'tickets'
161
162    def get_navigation_items(self, req):
163        return []
164
165    # IRequestHandler methods
166
167    def match_request(self, req):
168        match = re.match(r'/ticket/([0-9]+)', req.path_info)
169        if match:
170            req.args['id'] = match.group(1)
171            return True
172
173    def process_request(self, req):
174        req.perm.assert_permission('TICKET_VIEW')
175
176        action = req.args.get('action', 'view')
177
178        if not req.args.has_key('id'):
179            req.redirect(self.env.href.wiki())
180
181        db = self.env.get_db_cnx()
182        id = int(req.args.get('id'))
183
184        ticket = Ticket(self.env, id, db=db)
185        reporter_id = util.get_reporter_id(req)
186
187        if req.method == 'POST':
188            if not req.args.has_key('preview'):
189                self._do_save(req, db, ticket)
190            else:
191                # Use user supplied values
192                ticket.populate(req.args)
193                req.hdf['ticket.action'] = action
194                req.hdf['ticket.ts'] = req.args.get('ts')
195                req.hdf['ticket.reassign_owner'] = req.args.get('reassign_owner') \
196                                                   or req.authname
197                req.hdf['ticket.resolve_resolution'] = req.args.get('resolve_resolution')
198                reporter_id = req.args.get('author')
199                comment = req.args.get('comment')
200                if comment:
201                    req.hdf['ticket.comment'] = comment
202                    # Wiki format a preview of comment
203                    req.hdf['ticket.comment_preview'] = wiki_to_html(comment,
204                                                                     self.env,
205                                                                     req, db)
206        else:
207            req.hdf['ticket.reassign_owner'] = req.authname
208            # Store a timestamp in order to detect "mid air collisions"
209            req.hdf['ticket.ts'] = ticket.time_changed
210
211        self._insert_ticket_data(req, db, ticket, reporter_id)
212
213        # If the ticket is being shown in the context of a query, add
214        # links to help navigate in the query result set
215        if 'query_tickets' in req.session:
216            tickets = req.session['query_tickets'].split()
217            if str(id) in tickets:
218                idx = tickets.index(str(ticket.id))
219                if idx > 0:
220                    add_link(req, 'first', self.env.href.ticket(tickets[0]),
221                             'Ticket #%s' % tickets[0])
222                    add_link(req, 'prev', self.env.href.ticket(tickets[idx - 1]),
223                             'Ticket #%s' % tickets[idx - 1])
224                if idx < len(tickets) - 1:
225                    add_link(req, 'next', self.env.href.ticket(tickets[idx + 1]),
226                             'Ticket #%s' % tickets[idx + 1])
227                    add_link(req, 'last', self.env.href.ticket(tickets[-1]),
228                             'Ticket #%s' % tickets[-1])
229                add_link(req, 'up', req.session['query_href'])
230
231        add_stylesheet(req, 'common/css/ticket.css')
232        return 'ticket.cs', None
233
234    # ITimelineEventProvider methods
235
236    def get_timeline_filters(self, req):
237        if req.perm.has_permission('TICKET_VIEW'):
238            yield ('ticket', 'Ticket changes')
239
240    def get_timeline_events(self, req, start, stop, filters):
241        if 'ticket' in filters:
242            format = req.args.get('format')
243            sql = []
244
245            # New tickets
246            sql.append("SELECT time,id,'','new',type,summary,reporter,summary"
247                       " FROM ticket WHERE time>=%s AND time<=%s")
248
249            # Reopened tickets
250            sql.append("SELECT t1.time,t1.ticket,'','reopened',t.type,"
251                       "       t2.newvalue,t1.author,t.summary "
252                       " FROM ticket_change t1"
253                       "   LEFT OUTER JOIN ticket_change t2 ON (t1.time=t2.time"
254                       "     AND t1.ticket=t2.ticket AND t2.field='comment')"
255                       "   LEFT JOIN ticket t on t.id = t1.ticket "
256                       " WHERE t1.field='status' AND t1.newvalue='reopened'"
257                       "   AND t1.time>=%s AND t1.time<=%s")
258
259            # Closed tickets
260            sql.append("SELECT t1.time,t1.ticket,t2.newvalue,'closed',t.type,"
261                       "       t3.newvalue,t1.author,t.summary"
262                       " FROM ticket_change t1"
263                       "   INNER JOIN ticket_change t2 ON t1.ticket=t2.ticket"
264                       "     AND t1.time=t2.time"
265                       "   LEFT OUTER JOIN ticket_change t3 ON t1.time=t3.time"
266                       "     AND t1.ticket=t3.ticket AND t3.field='comment'"
267                       "   LEFT JOIN ticket t on t.id = t1.ticket "
268                       " WHERE t1.field='status' AND t1.newvalue='closed'"
269                       "   AND t2.field='resolution'"
270                       "   AND t1.time>=%s AND t1.time<=%s")
271
272            db = self.env.get_db_cnx()
273            cursor = db.cursor()
274            cursor.execute(" UNION ALL ".join(sql), (start, stop, start, stop,
275                           start, stop))
276            kinds = {'new': 'newticket', 'reopened': 'newticket',
277                     'closed': 'closedticket'}
278            verbs = {'new': 'created', 'reopened': 'reopened',
279                     'closed': 'closed'}
280            for t, id, resolution, status, type, message, author, summary \
281                    in cursor:
282                title = util.Markup('Ticket <em title="%s">#%s</em> (%s) %s by '
283                                    '%s', summary, id, type, verbs[status],
284                                    author)
285                if format == 'rss':
286                    href = self.env.abs_href.ticket(id)
287                    if status != 'new':
288                        message = wiki_to_html(message or '--', self.env, req,
289                                               db)
290                    else:
291                        message = util.escape(message)
292                else:
293                    href = self.env.href.ticket(id)
294                    if status != 'new':
295                        message = util.Markup(': ').join(filter(None, [
296                            resolution,
297                            wiki_to_oneliner(message, self.env, db,
298                                             shorten=True)
299                        ]))
300                    else:
301                        message = util.escape(util.shorten_line(message))
302                yield kinds[status], href, title, t, author, message
303
304    # Internal methods
305
306    def _do_save(self, req, db, ticket):
307        if req.perm.has_permission('TICKET_CHGPROP'):
308            # TICKET_CHGPROP gives permission to edit the ticket
309            if not req.args.get('summary'):
310                raise TracError('Tickets must contain summary.')
311
312            if req.args.has_key('description') or req.args.has_key('reporter'):
313                req.perm.assert_permission('TICKET_ADMIN')
314
315            ticket.populate(req.args)
316        else:
317            req.perm.assert_permission('TICKET_APPEND')
318
319        # Mid air collision?
320        if int(req.args.get('ts')) != ticket.time_changed:
321            raise TracError("Sorry, can not save your changes. "
322                            "This ticket has been modified by someone else "
323                            "since you started", 'Mid Air Collision')
324
325        # Do any action on the ticket?
326        action = req.args.get('action')
327        actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
328        if action not in actions:
329            raise TracError('Invalid action')
330
331        # TODO: this should not be hard-coded like this
332        if action == 'accept':
333            ticket['status'] =  'assigned'
334            ticket['owner'] = req.authname
335        if action == 'resolve':
336            ticket['status'] = 'closed'
337            ticket['resolution'] = req.args.get('resolve_resolution')
338        elif action == 'reassign':
339            ticket['owner'] = req.args.get('reassign_owner')
340            ticket['status'] = 'new'
341        elif action == 'reopen':
342            ticket['status'] = 'reopened'
343            ticket['resolution'] = ''
344
345        now = int(time.time())
346        ticket.save_changes(req.args.get('author', req.authname),
347                            req.args.get('comment'), when=now, db=db)
348        db.commit()
349
350        try:
351            tn = TicketNotifyEmail(self.env)
352            tn.notify(req, ticket, newticket=False, modtime=now)
353        except Exception, e:
354            self.log.exception("Failure sending notification on change to "
355                               "ticket #%s: %s" % (ticket.id, e))
356
357        req.redirect(self.env.href.ticket(ticket.id))
358
359    def _insert_ticket_data(self, req, db, ticket, reporter_id):
360        """Insert ticket data into the hdf"""
361        req.hdf['ticket'] = ticket.values
362        req.hdf['ticket.id'] = ticket.id
363        req.hdf['ticket.href'] = self.env.href.ticket(ticket.id)
364
365        for field in TicketSystem(self.env).get_ticket_fields():
366            if field['type'] in ('radio', 'select'):
367                value = ticket.values.get(field['name'])
368                options = field['options']
369                if value and not value in options:
370                    # Current ticket value must be visible even if its not in the
371                    # possible values
372                    options.append(value)
373                field['options'] = options
374            name = field['name']
375            del field['name']
376            if name in ('summary', 'reporter', 'description', 'type', 'status',
377                        'resolution', 'owner'):
378                field['skip'] = True
379            req.hdf['ticket.fields.' + name] = field
380
381        req.hdf['ticket.reporter_id'] = reporter_id
382        req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary'])
383        req.hdf['ticket.description.formatted'] = wiki_to_html(ticket['description'],
384                                                               self.env, req, db)
385
386        req.hdf['ticket.opened'] = util.format_datetime(ticket.time_created)
387        req.hdf['ticket.opened_delta'] = util.pretty_timedelta(ticket.time_created)
388        if ticket.time_changed != ticket.time_created:
389            req.hdf['ticket.lastmod'] = util.format_datetime(ticket.time_changed)
390            req.hdf['ticket.lastmod_delta'] = util.pretty_timedelta(ticket.time_changed)
391
392        changelog = ticket.get_changelog(db=db)
393        curr_author = None
394        curr_date   = 0
395        changes = []
396        for date, author, field, old, new in changelog:
397            if date != curr_date or author != curr_author:
398                changes.append({
399                    'date': util.format_datetime(date),
400                    'author': author,
401                    'fields': {}
402                })
403                curr_date = date
404                curr_author = author
405            if field == 'comment':
406                changes[-1]['comment'] = wiki_to_html(new, self.env, req, db)
407            elif field == 'description':
408                changes[-1]['fields'][field] = ''
409            else:
410                changes[-1]['fields'][field] = {'old': old,
411                                                'new': new}
412        req.hdf['ticket.changes'] = changes
413
414        # List attached files
415        for idx, attachment in util.enum(Attachment.select(self.env, 'ticket',
416                                                           ticket.id)):
417            hdf = attachment_to_hdf(self.env, db, req, attachment)
418            req.hdf['ticket.attachments.%s' % idx] = hdf
419        if req.perm.has_permission('TICKET_APPEND'):
420            req.hdf['ticket.attach_href'] = self.env.href.attachment('ticket',
421                                                                     ticket.id)
422
423        # Add the possible actions to hdf
424        actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
425        for action in actions:
426            req.hdf['ticket.actions.' + action] = '1'
427
428
429class UpdateDetailsForTimeline(Component):
430    """Provide all details about ticket changes in the Timeline"""
431
432    implements(ITimelineEventProvider)
433
434    # ITimelineEventProvider methods
435
436    def get_timeline_filters(self, req):
437        if not self.config.getbool('timeline', 'ticket_show_details'):
438            return
439        if req.perm.has_permission('TICKET_VIEW'):
440            yield ('ticket_details', 'Ticket details', False)
441
442    def get_timeline_events(self, req, start, stop, filters):
443        if 'ticket_details' in filters:
444            db = self.env.get_db_cnx()
445            cursor = db.cursor()
446            cursor.execute("SELECT tc.time,tc.ticket,t.type,tc.field, "
447                           "       tc.oldvalue,tc.newvalue,tc.author,t.summary "
448                           "FROM ticket_change tc"
449                           "   INNER JOIN ticket t ON t.id = tc.ticket "
450                           "AND tc.time>=%s AND tc.time<=%s ORDER BY tc.time"
451                           % (start, stop))
452            previous_update = None
453            updates = []
454            ticket_change = False
455            for time,id,type,field,oldvalue,newvalue,author,summary in cursor:
456                if not previous_update or (time,id,author) != previous_update[:3]:
457                    if previous_update and not ticket_change:
458                        updates.append((previous_update,field_changes,comment))
459                    ticket_change = False
460                    field_changes = []
461                    comment = ''
462                    previous_update = (time,id,author,type,summary)
463                if field == 'comment':
464                    comment = newvalue
465                elif field == 'status' and newvalue in ['reopened', 'closed']:
466                    ticket_change = True
467                else:
468                    field_changes.append(field)
469            if previous_update and not ticket_change:
470                updates.append((previous_update,field_changes,comment))
471
472            absurls = req.args.get('format') == 'rss' # Kludge
473            for (t,id,author,type,summary),field_changes,comment in updates:
474                if absurls:
475                    href = self.env.abs_href.ticket(id)
476                else:
477                    href = self.env.href.ticket(id) 
478                title = util.Markup('Ticket <em title="%s">#%s</em> (%s) '
479                                    'updated by %s', summary, id, type, author)
480                message = util.Markup()
481                if len(field_changes) > 0:
482                    message = util.Markup(', '.join(field_changes) + \
483                                          ' changed.<br />')
484                message += wiki_to_oneliner(comment, self.env, db,
485                                            shorten=True, absurls=absurls)
486                yield 'editedticket', href, title, t, author, message