source: administrativa/trac_local_changes/web_ui.py@ 1746

Last change on this file since 1746 was 1746, checked in by Александър Шопов, 17 years ago

Локални промени по trac

File size: 28.6 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2006 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgström <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.org/wiki/TracLicense.
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://trac.edgewall.org/log/.
14#
15# Author: Jonas Borgström <jonas@edgewall.com>
16
17import os
18import re
19import time
20from StringIO import StringIO
21
22from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule
23from trac.config import BoolOption, Option
24from trac.core import *
25from trac.env import IEnvironmentSetupParticipant
26from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator
27from trac.ticket.notification import TicketNotifyEmail
28from trac.Timeline import ITimelineEventProvider
29from trac.util import get_reporter_id
30from trac.util.datefmt import format_datetime, pretty_timedelta, http_date
31from trac.util.html import html, Markup
32from trac.util.text import CRLF
33from trac.web import IRequestHandler
34from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
35from trac.wiki import wiki_to_html, wiki_to_oneliner
36from trac.mimeview.api import Mimeview, IContentConverter
37
38
39class InvalidTicket(TracError):
40 """Exception raised when a ticket fails validation."""
41
42
43class TicketModuleBase(Component):
44 # FIXME: temporary place-holder for unified ticket validation until
45 # ticket controller unification is merged
46 abstract = True
47
48 ticket_manipulators = ExtensionPoint(ITicketManipulator)
49
50 def _validate_ticket(self, req, ticket):
51 # Always validate for known values
52 for field in ticket.fields:
53 if 'options' not in field:
54 continue
55 name = field['name']
56 if name in ticket.values and name in ticket._old:
57 value = ticket[name]
58 if value:
59 if value not in field['options']:
60 raise InvalidTicket('"%s" is not a valid value for '
61 'the %s field.' % (value, name))
62 elif not field.get('optional', False):
63 raise InvalidTicket('field %s must be set' % name)
64 # Custom validation rules
65 for manipulator in self.ticket_manipulators:
66 for field, message in manipulator.validate_ticket(req, ticket):
67 if field:
68 raise InvalidTicket("The ticket %s field is invalid: %s" %
69 (field, message))
70 else:
71 raise InvalidTicket("Invalid ticket: %s" % message)
72
73
74class NewticketModule(TicketModuleBase):
75
76 implements(IEnvironmentSetupParticipant, INavigationContributor,
77 IRequestHandler)
78
79 # IEnvironmentSetupParticipant methods
80
81 def environment_created(self):
82 """Create the `site_newticket.cs` template file in the environment."""
83 if self.env.path:
84 templates_dir = os.path.join(self.env.path, 'templates')
85 if not os.path.exists(templates_dir):
86 os.mkdir(templates_dir)
87 template_name = os.path.join(templates_dir, 'site_newticket.cs')
88 template_file = file(template_name, 'w')
89 template_file.write("""<?cs
90####################################################################
91# New ticket prelude - Included directly above the new ticket form
92?>
93""")
94
95 def environment_needs_upgrade(self, db):
96 return False
97
98 def upgrade_environment(self, db):
99 pass
100
101 # INavigationContributor methods
102
103 def get_active_navigation_item(self, req):
104 return 'newticket'
105
106 def get_navigation_items(self, req):
107 if not req.perm.has_permission('TICKET_CREATE'):
108 return
109 yield ('mainnav', 'newticket',
110 html.A('New Ticket', href=req.href.newticket(), accesskey=7))
111
112 # IRequestHandler methods
113
114 def match_request(self, req):
115 return re.match(r'/newticket/?$', req.path_info) is not None
116
117 def process_request(self, req):
118 req.perm.assert_permission('TICKET_CREATE')
119
120 db = self.env.get_db_cnx()
121
122 if req.method == 'POST' and 'owner' in req.args and \
123 not req.perm.has_permission('TICKET_MODIFY'):
124 del req.args['owner']
125
126 if req.method == 'POST' and not req.args.has_key('preview'):
127 self._do_create(req, db)
128
129 ticket = Ticket(self.env, db=db)
130 ticket.populate(req.args)
131 ticket.values['reporter'] = get_reporter_id(req, 'reporter')
132
133 if ticket.values.has_key('description'):
134 description = wiki_to_html(ticket['description'], self.env, req, db)
135 req.hdf['newticket.description_preview'] = description
136
137 req.hdf['title'] = 'New Ticket'
138 req.hdf['newticket'] = ticket.values
139
140 field_names = [field['name'] for field in ticket.fields
141 if not field.get('custom')]
142 if 'owner' in field_names:
143 curr_idx = field_names.index('owner')
144 if 'cc' in field_names:
145 insert_idx = field_names.index('cc')
146 else:
147 insert_idx = len(field_names)
148 if curr_idx < insert_idx:
149 ticket.fields.insert(insert_idx, ticket.fields[curr_idx])
150 del ticket.fields[curr_idx]
151
152 for field in ticket.fields:
153 name = field['name']
154 del field['name']
155 if name in ('summary', 'reporter', 'description', 'type', 'status',
156 'resolution'):
157 field['skip'] = True
158 elif name == 'owner':
159 field['label'] = 'Assign to'
160 if not req.perm.has_permission('TICKET_MODIFY'):
161 field['skip'] = True
162 elif name == 'milestone':
163 # Don't make completed milestones available for selection
164 options = field['options'][:]
165 for option in field['options']:
166 milestone = Milestone(self.env, option, db=db)
167 if milestone.is_completed:
168 options.remove(option)
169 field['options'] = options
170 req.hdf['newticket.fields.' + name] = field
171
172 if req.perm.has_permission('TICKET_APPEND'):
173 req.hdf['newticket.can_attach'] = True
174 req.hdf['newticket.attachment'] = req.args.get('attachment')
175
176 add_stylesheet(req, 'common/css/ticket.css')
177 return 'newticket.cs', None
178
179 # Internal methods
180
181 def _do_create(self, req, db):
182 if not req.args.get('summary'):
183 raise TracError('Tickets must contain a summary.')
184 # ash
185 if ((not req.args.has_key('sezam')) or (req.args.get('sezam')!='GTP')):
186 raise TracError('Попълнете GTP в първото поле — мярка против спама.')
187
188 ticket = Ticket(self.env, db=db)
189 ticket.populate(req.args)
190 ticket.values['reporter'] = get_reporter_id(req, 'reporter')
191 self._validate_ticket(req, ticket)
192
193 ticket.insert(db=db)
194 db.commit()
195
196 # Notify
197 try:
198 tn = TicketNotifyEmail(self.env)
199 tn.notify(ticket, newticket=True)
200 except Exception, e:
201 self.log.exception("Failure sending notification on creation of "
202 "ticket #%s: %s" % (ticket.id, e))
203
204 # Redirect the user to the newly created ticket
205 if req.args.get('attachment'):
206 req.redirect(req.href.attachment('ticket', ticket.id, action='new'))
207 else:
208 req.redirect(req.href.ticket(ticket.id))
209
210
211class TicketModule(TicketModuleBase):
212
213 implements(INavigationContributor, IRequestHandler, ITimelineEventProvider,
214 IContentConverter)
215
216 default_version = Option('ticket', 'default_version', '',
217 """Default version for newly created tickets.""")
218
219 default_type = Option('ticket', 'default_type', 'defect',
220 """Default type for newly created tickets (''since 0.9'').""")
221
222 default_priority = Option('ticket', 'default_priority', 'major',
223 """Default priority for newly created tickets.""")
224
225 default_milestone = Option('ticket', 'default_milestone', '',
226 """Default milestone for newly created tickets.""")
227
228 default_component = Option('ticket', 'default_component', '',
229 """Default component for newly created tickets""")
230
231 timeline_details = BoolOption('timeline', 'ticket_show_details', 'false',
232 """Enable the display of all ticket changes in the timeline
233 (''since 0.9'').""")
234
235 # IContentConverter methods
236
237 def get_supported_conversions(self):
238 yield ('csv', 'Comma-delimited Text', 'csv',
239 'trac.ticket.Ticket', 'text/csv', 8)
240 yield ('tab', 'Tab-delimited Text', 'tsv',
241 'trac.ticket.Ticket', 'text/tab-separated-values', 8)
242 yield ('rss', 'RSS Feed', 'xml',
243 'trac.ticket.Ticket', 'application/rss+xml', 8)
244
245 def convert_content(self, req, mimetype, ticket, key):
246 if key == 'csv':
247 return self.export_csv(ticket, mimetype='text/csv')
248 elif key == 'tab':
249 return self.export_csv(ticket, sep='\t',
250 mimetype='text/tab-separated-values')
251 elif key == 'rss':
252 return self.export_rss(req, ticket)
253
254 # INavigationContributor methods
255
256 def get_active_navigation_item(self, req):
257 return 'tickets'
258
259 def get_navigation_items(self, req):
260 return []
261
262 # IRequestHandler methods
263
264 def match_request(self, req):
265 match = re.match(r'/ticket/([0-9]+)$', req.path_info)
266 if match:
267 req.args['id'] = match.group(1)
268 return True
269
270 def process_request(self, req):
271 req.perm.assert_permission('TICKET_VIEW')
272
273 action = req.args.get('action', 'view')
274
275 db = self.env.get_db_cnx()
276 id = int(req.args.get('id'))
277
278 ticket = Ticket(self.env, id, db=db)
279
280 if req.method == 'POST':
281 if not req.args.has_key('preview'):
282 self._do_save(req, db, ticket)
283 else:
284 # Use user supplied values
285 ticket.populate(req.args)
286 self._validate_ticket(req, ticket)
287
288 req.hdf['ticket.action'] = action
289 req.hdf['ticket.ts'] = req.args.get('ts')
290 req.hdf['ticket.reassign_owner'] = req.args.get('reassign_owner') \
291 or req.authname
292 req.hdf['ticket.resolve_resolution'] = req.args.get('resolve_resolution')
293 comment = req.args.get('comment')
294 if comment:
295 req.hdf['ticket.comment'] = comment
296 # Wiki format a preview of comment
297 req.hdf['ticket.comment_preview'] = wiki_to_html(
298 comment, self.env, req, db)
299 else:
300 req.hdf['ticket.reassign_owner'] = req.authname
301 # Store a timestamp in order to detect "mid air collisions"
302 req.hdf['ticket.ts'] = ticket.time_changed
303
304 self._insert_ticket_data(req, db, ticket,
305 get_reporter_id(req, 'author'))
306
307 mime = Mimeview(self.env)
308 format = req.args.get('format')
309 if format:
310 mime.send_converted(req, 'trac.ticket.Ticket', ticket, format,
311 'ticket_%d' % ticket.id)
312
313 # If the ticket is being shown in the context of a query, add
314 # links to help navigate in the query result set
315 if 'query_tickets' in req.session:
316 tickets = req.session['query_tickets'].split()
317 if str(id) in tickets:
318 idx = tickets.index(str(ticket.id))
319 if idx > 0:
320 add_link(req, 'first', req.href.ticket(tickets[0]),
321 'Ticket #%s' % tickets[0])
322 add_link(req, 'prev', req.href.ticket(tickets[idx - 1]),
323 'Ticket #%s' % tickets[idx - 1])
324 if idx < len(tickets) - 1:
325 add_link(req, 'next', req.href.ticket(tickets[idx + 1]),
326 'Ticket #%s' % tickets[idx + 1])
327 add_link(req, 'last', req.href.ticket(tickets[-1]),
328 'Ticket #%s' % tickets[-1])
329 add_link(req, 'up', req.session['query_href'])
330
331 add_stylesheet(req, 'common/css/ticket.css')
332
333 # Add registered converters
334 for conversion in mime.get_supported_conversions('trac.ticket.Ticket'):
335 conversion_href = req.href.ticket(ticket.id, format=conversion[0])
336 add_link(req, 'alternate', conversion_href, conversion[1],
337 conversion[3])
338
339 return 'ticket.cs', None
340
341 # ITimelineEventProvider methods
342
343 def get_timeline_filters(self, req):
344 if req.perm.has_permission('TICKET_VIEW'):
345 yield ('ticket', 'Ticket changes')
346 if self.timeline_details:
347 yield ('ticket_details', 'Ticket details', False)
348
349 def get_timeline_events(self, req, start, stop, filters):
350 format = req.args.get('format')
351
352 status_map = {'new': ('newticket', 'created'),
353 'reopened': ('newticket', 'reopened'),
354 'closed': ('closedticket', 'closed'),
355 'edit': ('editedticket', 'updated')}
356
357 href = format == 'rss' and req.abs_href or req.href
358
359 def produce((id, t, author, type, summary), status, fields,
360 comment, cid):
361 if status == 'edit':
362 if 'ticket_details' in filters:
363 info = ''
364 if len(fields) > 0:
365 info = ', '.join(['<i>%s</i>' % f for f in \
366 fields.keys()]) + ' changed<br />'
367 else:
368 return None
369 elif 'ticket' in filters:
370 if status == 'closed' and fields.has_key('resolution'):
371 info = fields['resolution']
372 if info and comment:
373 info = '%s: ' % info
374 else:
375 info = ''
376 else:
377 return None
378 kind, verb = status_map[status]
379 if format == 'rss':
380 title = 'Ticket #%s (%s %s): %s' % \
381 (id, type.lower(), verb, summary)
382 else:
383 title = Markup('Ticket <em title="%s">#%s</em> (%s) %s by %s',
384 summary, id, type, verb, author)
385 ticket_href = href.ticket(id)
386 if cid:
387 ticket_href += '#comment:' + cid
388 if status == 'new':
389 message = summary
390 else:
391 message = Markup(info)
392 if comment:
393 if format == 'rss':
394 message += wiki_to_html(comment, self.env, req, db,
395 absurls=True)
396 else:
397 message += wiki_to_oneliner(comment, self.env, db,
398 shorten=True)
399 return kind, ticket_href, title, t, author, message
400
401 # Ticket changes
402 if 'ticket' in filters or 'ticket_details' in filters:
403 db = self.env.get_db_cnx()
404 cursor = db.cursor()
405
406 cursor.execute("SELECT t.id,tc.time,tc.author,t.type,t.summary, "
407 " tc.field,tc.oldvalue,tc.newvalue "
408 " FROM ticket_change tc "
409 " INNER JOIN ticket t ON t.id = tc.ticket "
410 " AND tc.time>=%s AND tc.time<=%s "
411 "ORDER BY tc.time"
412 % (start, stop))
413 previous_update = None
414 for id,t,author,type,summary,field,oldvalue,newvalue in cursor:
415 if not previous_update or (id,t,author) != previous_update[:3]:
416 if previous_update:
417 ev = produce(previous_update, status, fields,
418 comment, cid)
419 if ev:
420 yield ev
421 status, fields, comment, cid = 'edit', {}, '', None
422 previous_update = (id, t, author, type, summary)
423 if field == 'comment':
424 comment = newvalue
425 cid = oldvalue and oldvalue.split('.')[-1]
426 elif field == 'status' and newvalue in ('reopened', 'closed'):
427 status = newvalue
428 else:
429 fields[field] = newvalue
430 if previous_update:
431 ev = produce(previous_update, status, fields, comment, cid)
432 if ev:
433 yield ev
434
435 # New tickets
436 if 'ticket' in filters:
437 cursor.execute("SELECT id,time,reporter,type,summary"
438 " FROM ticket WHERE time>=%s AND time<=%s",
439 (start, stop))
440 for row in cursor:
441 yield produce(row, 'new', {}, None, None)
442
443 # Attachments
444 if 'ticket_details' in filters:
445 def display(id):
446 return Markup('ticket %s', html.EM('#', id))
447 att = AttachmentModule(self.env)
448 for event in att.get_timeline_events(req, db, 'ticket',
449 format, start, stop,
450 display):
451 yield event
452
453 # Internal methods
454
455 def export_csv(self, ticket, sep=',', mimetype='text/plain'):
456 content = StringIO()
457 content.write(sep.join(['id'] + [f['name'] for f in ticket.fields])
458 + CRLF)
459 content.write(sep.join([unicode(ticket.id)] +
460 [ticket.values.get(f['name'], '')
461 .replace(sep, '_').replace('\\', '\\\\')
462 .replace('\n', '\\n').replace('\r', '\\r')
463 for f in ticket.fields]) + CRLF)
464 return (content.getvalue(), '%s;charset=utf-8' % mimetype)
465
466 def export_rss(self, req, ticket):
467 db = self.env.get_db_cnx()
468 changes = []
469 change_summary = {}
470
471 description = wiki_to_html(ticket['description'], self.env, req, db)
472 req.hdf['ticket.description.formatted'] = unicode(description)
473
474 for change in self.grouped_changelog_entries(ticket, db):
475 changes.append(change)
476 # compute a change summary
477 change_summary = {}
478 # wikify comment
479 if 'comment' in change:
480 comment = change['comment']
481 change['comment'] = unicode(wiki_to_html(
482 comment, self.env, req, db, absurls=True))
483 change_summary['added'] = ['comment']
484 for field, values in change['fields'].iteritems():
485 if field == 'description':
486 change_summary.setdefault('changed', []).append(field)
487 else:
488 chg = 'changed'
489 if not values['old']:
490 chg = 'set'
491 elif not values['new']:
492 chg = 'deleted'
493 change_summary.setdefault(chg, []).append(field)
494 change['title'] = '; '.join(['%s %s' % (', '.join(v), k) for k, v \
495 in change_summary.iteritems()])
496 req.hdf['ticket.changes'] = changes
497 return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml')
498
499
500 def _do_save(self, req, db, ticket):
501 if req.perm.has_permission('TICKET_CHGPROP'):
502 # TICKET_CHGPROP gives permission to edit the ticket
503 if not req.args.get('summary'):
504 raise TracError('Tickets must contain summary.')
505
506 if req.args.has_key('description') or req.args.has_key('reporter'):
507 req.perm.assert_permission('TICKET_ADMIN')
508
509 ticket.populate(req.args)
510 else:
511 req.perm.assert_permission('TICKET_APPEND')
512
513 # ash
514 if ((not req.authname) or (req.authname == 'anonymous')) and ((not req.args.has_key('sezam')) or (req.args.get('sezam')!='GTP')):
515 raise TracError('Попълнете GTP в първото поле — мярка против спама.')
516
517 # Mid air collision?
518 if int(req.args.get('ts')) != ticket.time_changed:
519 raise TracError("Sorry, can not save your changes. "
520 "This ticket has been modified by someone else "
521 "since you started", 'Mid Air Collision')
522
523 # Do any action on the ticket?
524 action = req.args.get('action')
525 actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
526 if action not in actions:
527 raise TracError('Invalid action')
528
529 # TODO: this should not be hard-coded like this
530 if action == 'accept':
531 ticket['status'] = 'assigned'
532 ticket['owner'] = req.authname
533 if action == 'resolve':
534 ticket['status'] = 'closed'
535 ticket['resolution'] = req.args.get('resolve_resolution')
536 elif action == 'reassign':
537 ticket['owner'] = req.args.get('reassign_owner')
538 ticket['status'] = 'new'
539 elif action == 'reopen':
540 ticket['status'] = 'reopened'
541 ticket['resolution'] = ''
542
543 self._validate_ticket(req, ticket)
544
545 now = int(time.time())
546 cnum = req.args.get('cnum')
547 replyto = req.args.get('replyto')
548 internal_cnum = cnum
549 if cnum and replyto: # record parent.child relationship
550 internal_cnum = '%s.%s' % (replyto, cnum)
551 if ticket.save_changes(get_reporter_id(req, 'author'),
552 req.args.get('comment'), when=now, db=db,
553 cnum=internal_cnum):
554 db.commit()
555
556 try:
557 tn = TicketNotifyEmail(self.env)
558 tn.notify(ticket, newticket=False, modtime=now)
559 except Exception, e:
560 self.log.exception("Failure sending notification on change to "
561 "ticket #%s: %s" % (ticket.id, e))
562
563 fragment = cnum and '#comment:'+cnum or ''
564 req.redirect(req.href.ticket(ticket.id) + fragment)
565
566 def _insert_ticket_data(self, req, db, ticket, reporter_id):
567 """Insert ticket data into the hdf"""
568 replyto = req.args.get('replyto')
569 req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary'])
570 req.hdf['ticket'] = ticket.values
571 req.hdf['ticket'] = {
572 'id': ticket.id,
573 'href': req.href.ticket(ticket.id),
574 'replyto': replyto
575 }
576
577 # -- Ticket fields
578
579 for field in TicketSystem(self.env).get_ticket_fields():
580 if field['type'] in ('radio', 'select'):
581 value = ticket.values.get(field['name'])
582 options = field['options']
583 if field['name'] == 'milestone' \
584 and not req.perm.has_permission('TICKET_ADMIN'):
585 options = [opt for opt in options if not
586 Milestone(self.env, opt, db=db).is_completed]
587 if value and not value in options:
588 # Current ticket value must be visible even if its not in the
589 # possible values
590 options.append(value)
591 field['options'] = options
592 name = field['name']
593 del field['name']
594 if name in ('summary', 'reporter', 'description', 'type', 'status',
595 'resolution', 'owner'):
596 field['skip'] = True
597 req.hdf['ticket.fields.' + name] = field
598
599 req.hdf['ticket.reporter_id'] = reporter_id
600 req.hdf['ticket.description.formatted'] = wiki_to_html(
601 ticket['description'], self.env, req, db)
602
603 req.hdf['ticket.opened'] = format_datetime(ticket.time_created)
604 req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created)
605 if ticket.time_changed != ticket.time_created:
606 req.hdf['ticket'] = {
607 'lastmod': format_datetime(ticket.time_changed),
608 'lastmod_delta': pretty_timedelta(ticket.time_changed)
609 }
610
611 # -- Ticket Change History
612
613 def quote_original(author, original, link):
614 if not 'comment' in req.args: # i.e. the comment was not yet edited
615 req.hdf['ticket.comment'] = '\n'.join(
616 ['Replying to [%s %s]:' % (link, author)] +
617 ['> %s' % line for line in original.splitlines()] + [''])
618
619 if replyto == 'description':
620 quote_original(ticket['reporter'], ticket['description'],
621 'ticket:%d' % ticket.id)
622 replies = {}
623 changes = []
624 cnum = 0
625 description_lastmod = description_author = None
626 for change in self.grouped_changelog_entries(ticket, db):
627 changes.append(change)
628 # wikify comment
629 comment = ''
630 if 'comment' in change:
631 comment = change['comment']
632 change['comment'] = wiki_to_html(comment, self.env, req, db)
633 if change['permanent']:
634 cnum = change['cnum']
635 # keep track of replies threading
636 if 'replyto' in change:
637 replies.setdefault(change['replyto'], []).append(cnum)
638 # eventually cite the replied to comment
639 if replyto == str(cnum):
640 quote_original(change['author'], comment,
641 'comment:%s' % replyto)
642 if 'description' in change['fields']:
643 change['fields']['description'] = ''
644 description_lastmod = change['date']
645 description_author = change['author']
646
647 req.hdf['ticket'] = {
648 'changes': changes,
649 'replies': replies,
650 'cnum': cnum + 1
651 }
652 if description_lastmod:
653 req.hdf['ticket.description'] = {'lastmod': description_lastmod,
654 'author': description_author}
655
656 # -- Ticket Attachments
657
658 req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db,
659 'ticket', ticket.id)
660 if req.perm.has_permission('TICKET_APPEND'):
661 req.hdf['ticket.attach_href'] = req.href.attachment('ticket',
662 ticket.id)
663
664 # Add the possible actions to hdf
665 actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
666 for action in actions:
667 req.hdf['ticket.actions.' + action] = '1'
668
669 def grouped_changelog_entries(self, ticket, db, when=0):
670 """Iterate on changelog entries, consolidating related changes
671 in a `dict` object.
672 """
673 changelog = ticket.get_changelog(when=when, db=db)
674 autonum = 0 # used for "root" numbers
675 last_uid = current = None
676 for date, author, field, old, new, permanent in changelog:
677 uid = date, author, permanent
678 if uid != last_uid:
679 if current:
680 yield current
681 last_uid = uid
682 current = {
683 'http_date': http_date(date),
684 'date': format_datetime(date),
685 'author': author,
686 'fields': {},
687 'permanent': permanent
688 }
689 if permanent and not when:
690 autonum += 1
691 current['cnum'] = autonum
692 # some common processing for fields
693 if field == 'comment':
694 current['comment'] = new
695 if old:
696 if '.' in old: # retrieve parent.child relationship
697 parent_num, this_num = old.split('.', 1)
698 current['replyto'] = parent_num
699 else:
700 this_num = old
701 current['cnum'] = int(this_num)
702 else:
703 current['fields'][field] = {'old': old, 'new': new}
704 if current:
705 yield current
Note: See TracBrowser for help on using the repository browser.