Edgewall Software

TracImport: mantis2trac.3.py

File mantis2trac.3.py, 34.8 kB (added by stroganov.a@…, 22 months ago)

1.2: db-stored attachments work, bugfixes

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