Edgewall Software

TracImport: mantis2trac.py

File mantis2trac.py, 29.7 kB (added by Paul Baranowski <paul@…>, 3 years ago)

Mantis to Trac conversion script

Line 
1#!/usr/bin/env python
2
3"""
4Import Mantis bugs into a Trac database.
5
6Requires:  Trac 0.8.X from http://trac.edgewall.com/
7           Python 2.3 from http://www.python.org/
8           MySQL >= 3.23 from http://www.mysql.org/
9
10Version 1.0 (the "it works for me" release)
11Date: August 9, 2005
12Author: Paul Baranowski (paul@paulbaranowski.org)
13
14Based on bugzilla2trac.py by these guys (thank you!):
15Dmitry Yusupov <dmitry_yus@yahoo.com> - bugzilla2trac.py
16Mark Rowe <mrowe@bluewire.net.nz> - original TracDatabase class
17Bill Soudan <bill@soudan.net> - Many enhancements
18
19Example use:
20  python mantis2trac.py --db mantis --tracenv /usr/local/trac-projects/myproj/ --host localhost --user root --clean
21
22Notes:
23  - Private bugs will become public
24  - Some ticket changes will not be preserved since they have no
25    equivalents in Trac.
26  - I consider milestones and versions to be the same thing (actually,
27    I dont really care about the version, because for our project, bugs are
28    only in the 'previous version').
29  - Importing attachments is not implemented (couldnt get it to work,
30    and we didnt have enough attachments to justify spending time on this)
31    "Clean" will not delete your existing attachments.  There is code in here
32    to support adding attachments, but you will have to play with it to
33    make it work.  If you search for the word "attachment" you will find
34    all the code related to this.
35  - Ticket descriptions & comments will be re-wrapped to 70 characters.
36    This may mess up your formatting for your bugs.  If you dont want to do
37    this, search for textwrap.fill() and fix it.
38  - You will probably want to change "report.css" in trac to handle one more
39    level of priorities (default trac has 6 levels of priorities, while Mantis
40    has 7).  When you look at your reports, the color schemes will look wrong.
41   
42    The lines that control the priority color scheme look like this:
43    #tktlist tr.color1-odd  { background: #fdc; border-color: #e88; color: #a22 }
44    #tktlist tr.color1-even { background: #fed; border-color: #e99; color: #a22 }
45   
46    I added a new level 2 ("urgent") with an orange color,
47    and incremented all the rest of the levels:
48    #tktlist tr.color2-odd  { background: #FFE08F; border-color: #e88; color: #a22 }
49    #tktlist tr.color2-even { background: #FFE59F; border-color: #e99; color: #a22 }
50   
51"""
52
53import datetime
54import textwrap
55
56###
57### Conversion Settings -- edit these before running if desired
58###
59
60# Mantis version. 
61#
62# Currently, the following mantis versions are known to work:
63#   0.19.X
64#
65# If you run this script on a version not listed here and it is successful,
66# please report it to the Trac mailing list so we can update the list.
67MANTIS_VERSION = '0.19'
68
69# MySQL connection parameters for the Mantis database.  These can also
70# be specified on the command line.
71MANTIS_DB = 'mantis'
72MANTIS_HOST = 'localhost'
73MANTIS_USER = 'root'
74MANTIS_PASSWORD = ''
75
76# Path to the Trac environment.
77TRAC_ENV = ''
78
79# If true, all existing Trac tickets will be removed
80# prior to import.
81TRAC_CLEAN = True
82
83# Enclose imported ticket description and comments in a {{{ }}}
84# preformat block?  This formats the text in a fixed-point font.
85PREFORMAT_COMMENTS = True
86
87# By default, all bugs are imported from Mantis.  If you add a list
88# of products here, only bugs from those products will be imported.
89# Warning: I have not tested this script where this field is blank!
90PRODUCTS = [ 'campsite' ]
91
92# Trac doesn't have the concept of a product.  Instead, this script can
93# assign keywords in the ticket entry to represent products.
94#
95# ex. PRODUCT_KEYWORDS = { 'product1' : 'PRODUCT1_KEYWORD' }
96PRODUCT_KEYWORDS = {}
97
98# Bug comments that should not be imported.  Each entry in list should
99# be a regular expression.
100IGNORE_COMMENTS = [
101#   '^Created an attachment \(id='
102]
103
104# Ticket changes in Trac have the restriction where the
105# bug ID, field, and time must be unique for all entries in the ticket
106# changes table.
107# Mantis, for unknown reasons, has fields that can change two states
108# in under a second (e.g. "milestone":""->"1.0", "milestone":"1.0"->"2.0").
109# Setting this to true will attempt to fix these cases by adjusting the
110# time for the 2nd change to be one second more than the original time.
111# I dont know why you'd want to turn this off, but I give you the option
112# anyhow. :)
113TIME_ADJUSTMENT_HACK = True
114
115###########################################################################
116### You probably don't need to change any configuration past this line. ###
117###########################################################################
118
119# Mantis status to Trac status translation map.
120#
121# NOTE: bug activity is translated as well, which may cause bug
122# activity to be deleted (e.g. resolved -> closed in Mantis
123# would translate into closed -> closed in Trac, so we just ignore the
124# change).
125#
126# Possible Trac 'status' values: 'new', 'assigned', 'reopened', 'closed'
127STATUS_TRANSLATE = {
128  10 : 'new',      # 10 == 'new' in mantis
129  20 : 'assigned', # 20 == 'feedback'
130  30 : 'new',      # 30 == 'acknowledged'
131  50 : 'assigned', # 50 == 'assigned'
132  40 : 'new',      # 40 == 'confirmed'
133  80 : 'closed',   # 80 == 'resolved'
134  90 : 'closed'    # 90 == 'closed'
135}
136
137# Unused:
138# Translate Mantis statuses into Trac keywords.  This provides a way
139# to retain the Mantis statuses in Trac.  e.g. when a bug is marked
140# 'verified' in Mantis it will be assigned a VERIFIED keyword.
141##STATUS_KEYWORDS = {
142##    'confirmed' : 'CONFIRMED',
143##    'feedback' : 'FEEDBACK',
144##    'acknowledged':'ACKNOWLEDGED'
145##}
146
147# Possible Trac resolutions are 'fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme'
148RESOLUTION_TRANSLATE = {
149    10 : '',          # 10 == 'open' in mantis
150    20 : 'fixed',     # 20 == 'fixed'
151    30 : '',          # 30 == 'reopened' (TODO: 'reopened' needs to be mapped to a status event)
152    40 : 'invalid',   # 40 == 'unable to duplicate'
153    50 : 'wontfix',   # 50 == 'not fixable'
154    60 : 'duplicate', # 60 == 'duplicate'
155    70 : 'invalid',   # 70 == 'not an issue'
156    80 : '',          # 80 == 'suspended'
157    90 : 'wontfix',   # 90 == 'wont fix'
158}
159
160# Mantis severities (which will also become equivalent Trac severities)
161##SEVERITY_LIST = (('block', '80'),
162##                 ('crash', '70'),
163##                 ('major', '60'),
164##                 ('minor', '50'),
165##                 ('tweak', '40'),
166##                 ('text', '30'),
167##                 ('trivial', '20'),
168##                 ('feature', '10'))
169SEVERITY_LIST = (('block', '1'), 
170                 ('crash', '2'), 
171                 ('major', '3'), 
172                 ('minor', '4'),
173                 ('tweak', '5'), 
174                 ('text', '6'), 
175                 ('trivial', '7'), 
176                 ('feature', '8'))
177
178# Translate severity numbers into their text equivalents
179SEVERITY_TRANSLATE = {
180    80 : 'block',
181    70 : 'crash',
182    60 : 'major',
183    50 : 'minor',
184    40 : 'tweak',
185    30 : 'text',
186    20 : 'trivial',
187    10 : 'feature'
188}
189
190# Mantis priorities (which will also become Trac priorities)
191##PRIORITY_LIST = (('immediate', '60'),
192##                 ('urgent', '50'),
193##                 ('high', '40'),
194##                 ('normal', '30'),
195##                 ('low', '20'),
196##                 ('none', '10'))
197PRIORITY_LIST = (('immediate', '1'), 
198                 ('urgent', '2'), 
199                 ('high', '3'), 
200                 ('normal', '4'), 
201                 ('low', '5'), 
202                 ('none', '6'))
203
204# Translate priority numbers into their text equivalent
205PRIORITY_TRANSLATE = {
206    60 : 'immediate', 
207    50 : 'urgent', 
208    40 : 'high',
209    30 : 'normal', 
210    20 : 'low', 
211    10 : 'none'
212}
213
214
215# Some fields in Mantis do not have equivalents in Trac.  Changes in
216# fields listed here will not be imported into the ticket change history,
217# otherwise you'd see changes for fields that don't exist in Trac.
218IGNORED_ACTIVITY_FIELDS = ['', 'project_id', 'reproducibility', 'view_state', 'os', 'os_build', 'duplicate_id']
219
220###
221### Script begins here
222###
223
224import os
225import re
226import sys
227import string
228import StringIO
229
230import MySQLdb
231import MySQLdb.cursors
232import trac.Environment
233
234if not hasattr(sys, 'setdefaultencoding'):
235    reload(sys)
236
237sys.setdefaultencoding('latin1')
238
239# simulated Attachment class for trac.add
240class Attachment:
241    def __init__(self, name, data):
242        self.filename = name
243        self.file = StringIO.StringIO(data.tostring())
244 
245# simple field translation mapping.  if string not in
246# mapping, just return string, otherwise return value
247class FieldTranslator(dict):
248    def __getitem__(self, item):
249        if not dict.has_key(self, item):
250            return item
251           
252        return dict.__getitem__(self, item)
253
254statusXlator = FieldTranslator(STATUS_TRANSLATE)
255
256class TracDatabase(object):
257    def __init__(self, path):
258        self.env = trac.Environment.Environment(path)
259        self._db = self.env.get_db_cnx()
260        self._db.autocommit = False
261        self.loginNameCache = {}
262        self.fieldNameCache = {}
263   
264    def db(self):
265        return self._db
266   
267    def hasTickets(self):
268        c = self.db().cursor()
269        c.execute('''SELECT count(*) FROM Ticket''')
270        return int(c.fetchall()[0][0]) > 0
271
272    def assertNoTickets(self):
273        if self.hasTickets():
274            raise Exception("Will not modify database with existing tickets!")
275   
276    def setSeverityList(self, s):
277        """Remove all severities, set them to `s`"""
278        self.assertNoTickets()
279       
280        c = self.db().cursor()
281        c.execute("""DELETE FROM enum WHERE type='severity'""")
282        for value, i in s:
283            print "inserting severity ", value, " ", i
284            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
285                      "severity", value.encode('utf-8'), i)
286        self.db().commit()
287   
288    def setPriorityList(self, s):
289        """Remove all priorities, set them to `s`"""
290        self.assertNoTickets()
291       
292        c = self.db().cursor()
293        c.execute("""DELETE FROM enum WHERE type='priority'""")
294        for value, i in s:
295            print "inserting priority ", value, " ", i
296            c.execute("""INSERT INTO enum (type, name, value) VALUES (%s, %s, %s)""",
297                      "priority",
298                      value.encode('utf-8'),
299                      i)
300        self.db().commit()
301
302   
303    def setComponentList(self, l, key):
304        """Remove all components, set them to `l`"""
305        self.assertNoTickets()
306       
307        c = self.db().cursor()
308        c.execute("""DELETE FROM component""")
309        for comp in l:
310            print "inserting component '",comp[key],"', owner",  comp['owner']
311            c.execute("""INSERT INTO component (name, owner) VALUES (%s, %s)""",
312                      comp[key].encode('utf-8'), comp['owner'].encode('utf-8'))
313        self.db().commit()
314   
315    def setVersionList(self, v, key):
316        """Remove all versions, set them to `v`"""
317        self.assertNoTickets()
318       
319        c = self.db().cursor()
320        c.execute("""DELETE FROM version""")
321        for vers in v:
322            print "inserting version ", vers[key]
323            c.execute("""INSERT INTO version (name) VALUES (%s)""",
324                      vers[key].encode('utf-8'))
325        self.db().commit()
326       
327    def setMilestoneList(self, m, key):
328        """Remove all milestones, set them to `m`"""
329        self.assertNoTickets()
330       
331        c = self.db().cursor()
332        c.execute("""DELETE FROM milestone""")
333        for ms in m:
334            print "inserting milestone ", ms[key]
335            c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
336                      ms[key].encode('utf-8'))
337        self.db().commit()
338   
339    def addTicket(self, id, time, changetime, component,
340                  severity, priority, owner, reporter, cc,
341                  version, milestone, status, resolution,
342                  summary, description, keywords):
343        c = self.db().cursor()
344       
345        description = textwrap.fill(description, 70)
346        desc = description.encode('utf-8')
347       
348        if PREFORMAT_COMMENTS:
349          desc = '{{{\n%s\n}}}' % desc
350
351        print "inserting ticket %s -- \"%s\"" % (id, summary[0:40].replace("\n", " "))
352        c.execute("""INSERT INTO ticket (id, time, changetime, component,
353                                         severity, priority, owner, reporter, cc,
354                                         version, milestone, status, resolution,
355                                         summary, description, keywords)
356                                 VALUES (%s, %s, %s, %s,
357                                         %s, %s, %s, %s, %s,
358                                         %s, %s, %s, %s,
359                                         %s, %s, %s)""",
360                  id, time.strftime('%s'), changetime.strftime('%s'), component.encode('utf-8'),
361                  severity.encode('utf-8'), priority.encode('utf-8'), owner, reporter, cc,
362                  version, milestone.encode('utf-8'), status.lower(), resolution,
363                  summary.encode('utf-8'), desc, keywords)
364       
365        self.db().commit()
366        return self.db().db.sqlite_last_insert_rowid()
367   
368    def addTicketComment(self, ticket, time, author, value):
369        print " * adding comment \"%s...\"" % value[0:40]
370        comment = textwrap.fill(value, 70)
371        comment = comment.encode('utf-8')
372       
373        if PREFORMAT_COMMENTS:
374          comment = '{{{\n%s\n}}}' % comment
375
376        c = self.db().cursor()
377        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
378                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
379                  ticket, time.strftime('%s'), author, 'comment', '', comment)
380        self.db().commit()
381
382    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
383        print " * adding ticket change \"%s\": \"%s\" -> \"%s\" (%s)" % (field, oldvalue[0:20], newvalue[0:20], time)
384        c = self.db().cursor()
385        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
386                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
387                  ticket, time.strftime('%s'), author, field, oldvalue.encode('utf-8'), newvalue.encode('utf-8'))
388        self.db().commit()
389        # Now actually change the ticket because the ticket wont update itself!
390        sql = "UPDATE ticket SET %s='%s' WHERE id=%s" % (field, newvalue, ticket)
391        c.execute(sql)
392        self.db().commit()       
393       
394    def addAttachment(self, id, attachment, description, author):
395        print 'inserting attachment for ticket %s -- %s' % (id, description)
396        attachment.filename = attachment.filename.encode('utf-8')
397        self.env.create_attachment(self.db(), 'ticket', str(id), attachment, description.encode('utf-8'),
398            author, 'unknown')
399       
400    def getLoginName(self, cursor, userid):
401        if userid not in self.loginNameCache:
402            cursor.execute("SELECT * FROM mantis_user_table WHERE id = %s" % userid)
403            loginName = cursor.fetchall()
404
405            if loginName:
406                loginName = loginName[0]['username']
407            else:
408                print 'warning: unknown mantis userid %d, recording as anonymous' % userid
409                loginName = 'anonymous'
410
411            self.loginNameCache[userid] = loginName
412
413        return self.loginNameCache[userid]
414
415
416def productFilter(fieldName, products):
417    first = True
418    result = ''
419    for product in products:
420        if not first: 
421            result += " or "
422        first = False
423        result += "%s = '%s'" % (fieldName, product)
424    return result
425
426def convert(_db, _host, _user, _password, _env, _force):
427    activityFields = FieldTranslator()
428
429    # account for older versions of mantis
430    if MANTIS_VERSION == '0.19':
431        print 'Using Mantis v%s schema.' % MANTIS_VERSION
432        activityFields['removed'] = 'oldvalue'
433        activityFields['added'] = 'newvalue'
434
435    # init Mantis environment
436    print "Mantis MySQL('%s':'%s':'%s':'%s'): connecting..." % (_db, _host, _user, _password)
437    mysql_con = MySQLdb.connect(host=_host, 
438                user=_user, passwd=_password, db=_db, compress=1, 
439                cursorclass=MySQLdb.cursors.DictCursor)
440    mysql_cur = mysql_con.cursor()
441
442    # init Trac environment
443    print "Trac SQLite('%s'): connecting..." % (_env)
444    trac = TracDatabase(_env)
445
446    # force mode...
447    if _force == 1:
448        print "cleaning all tickets..."
449        c = trac.db().cursor()
450        c.execute("""DELETE FROM ticket_change""")
451        trac.db().commit()
452        c.execute("""DELETE FROM ticket""")
453        trac.db().commit()
454##        c.execute("""DELETE FROM attachment""")
455##        os.system('rm -rf %s' % trac.env.get_attachments_dir())
456##        os.mkdir(trac.env.get_attachments_dir())
457##        trac.db().commit()
458
459    print
460    print '0. Finding project IDs...'
461    sql =  "SELECT id, name FROM mantis_project_table WHERE %s" % productFilter('name', PRODUCTS)
462    mysql_cur.execute(sql)
463    project_list = mysql_cur.fetchall()
464    project_dict = dict()
465    for project_id in project_list:
466        print "Mantis project name '%s' has project ID %s" % (project_id['name'], project_id['id'])
467        project_dict[project_id['id']] = project_id['id']
468       
469    print
470    print "1. import severities..."
471    trac.setSeverityList(SEVERITY_LIST)
472
473    print
474    print "2. import components..."
475    sql = "SELECT category, user_id as owner FROM mantis_project_category_table"
476    if PRODUCTS:
477       sql += " WHERE %s" % productFilter('project_id', project_dict)
478    mysql_cur.execute(sql)
479    components = mysql_cur.fetchall()
480    for component in components:
481            component['owner'] = trac.getLoginName(mysql_cur, component['owner'])
482    trac.setComponentList(components, 'category')
483
484    print
485    print "3. import priorities..."
486    trac.setPriorityList(PRIORITY_LIST)
487
488    print
489    print "4. import versions..."
490    sql = "SELECT DISTINCTROW version FROM mantis_project_version_table"
491    if PRODUCTS:
492       sql += " WHERE %s" % productFilter('project_id', project_dict)
493    mysql_cur.execute(sql)
494    versions = mysql_cur.fetchall()
495    trac.setVersionList(versions, 'version')
496
497    print
498    print "5. import milestones..."
499    sql = "SELECT version FROM mantis_project_version_table"
500    if PRODUCTS:
501       sql += " WHERE %s" % productFilter('project_id', project_dict)
502    mysql_cur.execute(sql)
503    milestones = mysql_cur.fetchall()
504    trac.setMilestoneList(milestones, 'version')
505
506    print
507    print '6. retrieving bugs...'
508    sql = "SELECT * FROM mantis_bug_table "
509    if PRODUCTS:
510       sql += " WHERE %s" % productFilter('project_id', project_dict)
511    sql += " ORDER BY id"
512    mysql_cur.execute(sql)
513    bugs = mysql_cur.fetchall()
514   
515    print
516    print "7. import bugs and bug activity..."
517    totalComments = 0
518    totalTicketChanges = 0
519##    totalAttachments = 0
520    errors = []
521    timeAdjustmentHacks = []
522    for bug in bugs:
523        bugid = bug['id']
524       
525        ticket = {}
526        keywords = []
527        ticket['id'] = bugid
528        ticket['time'] = bug['date_submitted']
529        ticket['changetime'] = bug['last_updated']
530        ticket['component'] = bug['category']
531        ticket['severity'] = SEVERITY_TRANSLATE[bug['severity']]
532        ticket['priority'] = PRIORITY_TRANSLATE[bug['priority']]
533        ticket['owner'] = trac.getLoginName(mysql_cur, bug['handler_id'])
534        ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter_id'])
535        ticket['version'] = bug['version']
536        ticket['milestone'] = bug['version']
537        ticket['summary'] = bug['summary']
538        ticket['status'] = STATUS_TRANSLATE[bug['status']]
539        ticket['cc'] = ''
540        ticket['keywords'] = ''
541
542        # Special case for 'reopened' resolution in mantis -
543        # it maps to a status type in Trac.
544        if (bug['resolution'] == 30):
545            ticket['status'] = 'reopened'
546        ticket['resolution'] = RESOLUTION_TRANSLATE[bug['resolution']]
547       
548        # Compose the description from the three text fields in Mantis:
549        # 'description', 'steps_to_reproduce', 'additional_information'
550        mysql_cur.execute("SELECT * FROM mantis_bug_text_table WHERE id = %s" % bugid) 
551        longdescs = list(mysql_cur.fetchall())
552
553        # check for empty 'longdescs[0]' field...
554        if len(longdescs) == 0:
555            ticket['description'] = ''
556        else:
557            tmpDescr = longdescs[0]['description']
558            if (longdescs[0]['steps_to_reproduce'].strip() != ''):
559               tmpDescr = ('%s\n\nSTEPS TO REPRODUCE:\n%s') % (tmpDescr, longdescs[0]['steps_to_reproduce'])
560            if (longdescs[0]['additional_information'].strip() != ''):
561               tmpDescr = ('%s\n\nADDITIONAL INFORMATION:\n%s') % (tmpDescr, longdescs[0]['additional_information'])
562            ticket['description'] = tmpDescr
563            del longdescs[0]
564
565        # Add the ticket to the Trac database
566        trac.addTicket(**ticket)
567       
568        #
569        # Add ticket comments
570        #
571        mysql_cur.execute("SELECT * FROM mantis_bugnote_table, mantis_bugnote_text_table WHERE bug_id = %s AND mantis_bugnote_table.id = mantis_bugnote_text_table.id ORDER BY date_submitted" % bugid)
572        bug_notes = mysql_cur.fetchall()
573        totalComments += len(bug_notes)
574        for note in bug_notes:
575            trac.addTicketComment(bugid, note['date_submitted'], trac.getLoginName(mysql_cur, note['reporter_id']), note['note'])
576
577        #
578        # Convert ticket changes
579        #
580        mysql_cur.execute("SELECT * FROM mantis_bug_history_table WHERE bug_id = %s ORDER BY date_modified" % bugid)
581        bugs_activity = mysql_cur.fetchall()
582        resolution = ''
583        ticketChanges = []
584        for activity in bugs_activity:
585            field_name = activity['field_name'].lower()
586            # Convert Mantis field names...
587            # The following fields are the same in Mantis and Trac:
588            #  - 'status'
589            #  - 'priority'
590            #  - 'summary'
591            #  - 'resolution'
592            #  - 'severity'
593            #  - 'version'
594            #
595            # Ignore the following changes:
596            #  - project_id
597            #  - reproducibility
598            #  - view_state
599            #  - os
600            #  - os_build
601            #  - duplicate_id
602            #
603            # Convert Mantis -> Trac:
604            #  - 'handler_id' -> 'owner'
605            #  - 'fixed_in_version' -> 'milestone'
606            #  - 'category' -> 'component'
607            #  - 'version' -> 'milestone'
608           
609            ticketChange = {}
610            ticketChange['ticket'] = bugid
611            ticketChange['oldvalue'] = activity['old_value']
612            ticketChange['newvalue'] = activity['new_value']
613            ticketChange['time'] = activity['date_modified']
614            ticketChange['author'] = trac.getLoginName(mysql_cur, activity['user_id'])
615            ticketChange['field'] = field_name
616           
617            if field_name == 'handler_id':
618                ticketChange['field'] = 'owner'
619                ticketChange['oldvalue'] = trac.getLoginName(mysql_cur, int(activity['old_value']))
620                ticketChange['newvalue'] = trac.getLoginName(mysql_cur, int(activity['new_value']))
621            elif field_name == 'fixed_in_version':
622                ticketChange['field'] = 'milestone'
623            elif field_name == 'category':
624                ticketChange['field'] = 'component'
625            elif field_name == 'version':
626                ticketChange['field'] = 'milestone'
627            elif field_name == 'status':
628                ticketChange['oldvalue'] = STATUS_TRANSLATE[int(activity['old_value'])]
629                ticketChange['newvalue'] = STATUS_TRANSLATE[int(activity['new_value'])]
630            elif field_name == 'priority':
631                ticketChange['oldvalue'] = PRIORITY_TRANSLATE[int(activity['old_value'])]
632                ticketChange['newvalue'] = PRIORITY_TRANSLATE[int(activity['new_value'])]
633            elif field_name == 'resolution':
634                ticketChange['oldvalue'] = RESOLUTION_TRANSLATE[int(activity['old_value'])]
635                ticketChange['newvalue'] = RESOLUTION_TRANSLATE[int(activity['new_value'])]
636            elif field_name == 'severity':
637                ticketChange['oldvalue'] = SEVERITY_TRANSLATE[int(activity['old_value'])]
638                ticketChange['newvalue'] = SEVERITY_TRANSLATE[int(activity['new_value'])]           
639               
640            if field_name in IGNORED_ACTIVITY_FIELDS:
641                continue
642
643            # skip changes that have no effect (think translation!)
644            if ticketChange['oldvalue'] == ticketChange['newvalue']:
645                continue
646               
647            ticketChanges.append (ticketChange)
648
649        totalTicketChanges += len(ticketChanges)
650        for ticketChange in ticketChanges:
651            try:
652                trac.addTicketChange (**ticketChange)
653            except:
654                if TIME_ADJUSTMENT_HACK:
655                    addTime = datetime.timedelta(seconds=1)
656                    originalTime = ticketChange['time']
657                    ticketChange['time'] += addTime
658                    try:
659                        trac.addTicketChange(**ticketChange)
660                        noticeStr = " ~ Successfully adjusted time for ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time'])
661                        noticeStr += "\n   Original time: %s" % originalTime
662                        timeAdjustmentHacks.append(noticeStr)
663                    except:
664                        errorStr =  " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time'])
665                        errorStr += "\n          The bug id, field name, and time must be unique"
666                        errors.append(errorStr)
667                        print errorStr
668                else:
669                    errorStr =  " * ERROR: unable to add ticket(#%s) change \"%s\": \"%s\" -> \"%s\" (%s)" % (bugid, ticketChange['field'], ticketChange['oldvalue'], ticketChange['newvalue'], ticketChange['time'])
670                    errorStr += "\n          The bug id, field name, and time must be unique"
671                    errors.append(errorStr)
672                    print errorStr
673               
674
675        #
676        # Add ticket file attachments
677        #
678##        mysql_cur.execute("SELECT * FROM mantis_bug_file_table WHERE bug_id = %s" % bugid)
679##        attachments = mysql_cur.fetchall()
680##        for attachment in attachments:
681##            author = ''
682##            try:
683##                attachmentFile = open(attachment['diskfile'], 'r')
684##                attachmentData = attachmentFile.read()
685##                tracAttachment = Attachment(attachment['filename'], attachmentData)
686##                trac.addAttachment(bugid, tracAttachment, attachment['description'], author)
687##                totalAttachments += 1
688##            except:
689##                errorStr = " * ERROR: couldnt find attachment %s" % attachment['diskfile']
690