# FogBugz2CSV Migration Tool

import MySQLdb
import xml.sax.saxutils as saxutils

DB_HOSTNAME = 'localhost' # FogBugz's database hostname (i.e.: 'localhost')
DB_USERNAME = '' # FogBugz's database username
DB_PASSWORD = '' # FogBugz's database password
DB_DATABASE = 'fogbugz' # FogBugz's database

# Using FogBugz's 'category' for conversion
# In Trac, 'type' must be one of: task, defect, enhancement
TYPE = {
    '1': 'defect', # Bug in FogBugz, Defect in Trac
    '2': 'enhancement', # Feature in FogBugz, Enhancement in Trac
    '3': 'task', # Inquiry in FogBugz, Task in Trac
    '4': 'task', # Schedule Item in FogBugz, Task in Trac
}

# FogBugz's 'priority' for conversion
# In Trac, 'severity' must be one of: blocker, critical, major, normal, minor, trivial, enhancement
SEVERITY = {
    '1': 'blocker', # Blocker in FogBugz, Blocker in Trac
    '2': 'critical', # Critical in FogBugz, Critical in Trac
    '3': 'major', # Major in FogBugz, Major in Trac
    '4': 'normal', # Normal in FogBugz, Normal in Trac
    '5': 'minor', # Minor in FogBugz, Minor in Trac
    '6': 'trivial', # Trivial in FogBugz, Trivial in Trac
    '7': 'enhancement', # Enhancement in FogBugz, Enhancement in Trac
}

# Not in FogBugz, using 'priority' for conversion
# In Trac, 'priority' must be one of: highest, high, normal, low, lowest
PRIORITY = {
    '1': 'highest', # Blocker in FogBugz, Highest in Trac
    '2': 'high', # Critical in FogBugz, High in Trac
    '3': 'high', # Major in FogBugz, High in Trac
    '4': 'normal', # Normal in FogBugz, Normal in Trac
    '5': 'low', # Minor in FogBugz, Low in Trac
    '6': 'low', # Trivial in FogBugz, Low in Trac
    '7': 'lowest', # Enhancement in FogBugz, Lowest in Trac
}

# Using Fogbugz's 'status' for conversion
# In Trac, 'status' must be one of: new, assigned, reopened, closed
STATUS = {
    '1': 'assigned', # Active in FogBugz, Assigned in Trac
    '2': 'closed', # Resolved (Fixed) in FogBugz, Closed in Trac
    '3': 'closed', # Resolved (Not Reproducible) in FogBugz, Closed in Trac
    '4': 'closed', # Resolved (Duplicate) in FogBugz, Closed in Trac
    '5': 'closed', # Resolved (Postponed) in FogBugz, Closed in Trac
    '6': 'closed', # Resolved (Won't Fix) in FogBugz, Closed in Trac
    '7': 'closed', # Resolved (By Design) in FogBugz, Closed in Trac
    '8': 'closed', # Resolved (Implemented) in FogBugz, Closed in Trac
    '9': 'closed', # Resolved (Won't Impement) in FogBugz, Closed in Trac
    '10': 'closed', # Resolved (Already Exists) in FogBugz, Closed in Trac
    '11': 'closed', # Resolved (Responded) in FogBugz, Closed in Trac
    '12': 'closed', # Resolved (Won't Respond) in FogBugz, Closed in Trac
    '13': 'closed', # Resolved (SPAM) in FogBugz, Closed in Trac
    '14': 'closed', # Resolved (Waiting For Info) in FogBugz, Closed in Trac
    '15': 'closed', # Resolved (Completed) in FogBugz, Closed in Trac
    '16': 'closed', # Resolved (Canceled) in FogBugz, Closed in Trac
}

# Each line in the CSV file needs to have the following entries
# [M] type        text           -- the ticket purpose
# time            integer        -- the time it was created
# changetime      integer
# component       text
# [M] severity    text
# [M] priority    text
# owner           text           -- who is this ticket assigned to
# reporter        text
# cc              text           -- email addresses to notify
# url             text           -- url related to this ticket
# version         text           --
# milestone       text           --
# [M] status      text
# resolution      text
# summary         text           -- one-line summary
# description     text           -- problem description (long)
# keywords        text      

def format_value(value):
    string = str(value)
    string = string.replace("\\'", "'")
    string = string.replace("'", "''")
    string = string.replace("\r", "")
    string = string.replace("\n", "\\n")
    string = string.replace('"', '""')
    string = string.replace('&nbsp;', '')
    string = string.replace('&quot;', '""')
    return '"' + saxutils.unescape(string) + '"'

def get_username(full_name): 
    """ 
    Returns Trac's username
    """ 
    names = full_name.lower().split(' ')
    if len(names) > 1:
        # First letter from the first name and complete surname 
        # i.e.: John Doe will be converted to 'jdoe'
        return names[0][0] + names[1]
    else:
        return full_name

def main():
    db = MySQLdb.connect(
        host=DB_HOSTNAME,
        user=DB_USERNAME,
        passwd=DB_PASSWORD,
        db=DB_DATABASE
    )
    cursor = db.cursor()
    # Getting data from FogBugz's database
    sql = """
    SELECT
    Bug.ixCategory AS type,
    UNIX_TIMESTAMP(Bug.dtOpened) AS time,
    UNIX_TIMESTAMP(NOW()) AS changetime,
    Project.sProject AS component,
    Bug.ixPriority AS severity,
    Bug.ixPriority AS priority,
    Owner.sFullName AS owner,
    Reporter.sFullName AS reporter,
    '' AS cc,
    '' AS url,
    Bug.sVersion AS version,
    FixFor.sFixFor AS milestone,
    Bug.ixStatus as status,
    '' AS resolution,
    Bug.sTitle AS summary,
    (SELECT s FROM BugEvent WHERE BugEvent.ixBug = Bug.ixBug ORDER BY BugEvent.ixBugEvent ASC LIMIT 1) AS description,
    '' AS keywords
    FROM Bug
    LEFT JOIN Project ON Project.ixProject = Bug.ixProject
    LEFT JOIN Person AS Owner ON Owner.ixPerson = Bug.ixPersonAssignedTo
    LEFT JOIN Person AS Reporter ON Reporter.ixPerson = Bug.ixPersonOpenedBy
    LEFT JOIN FixFor ON FixFor.ixFixFor = Bug.ixFixFor
    LEFT JOIN BugEvent ON Bug.ixBugEventLatestText = BugEvent.ixBugEvent
    ORDER BY Bug.ixBug ASC;
    """
    cursor.execute(sql)
    result = cursor.fetchall()
    # CSV header
    print 'type,time,changetime,component,severity,priority,owner,reporter,cc,url,version,milestone,status,resolution,summary,description,keywords'
    for row in result:
        export = (
            TYPE.get(str(row[0])), # type, converting FogBugz's 'category' to Trac's 'type'
            str(row[1]), # time
            str(row[2]), # changetime
            str(row[3]), # component
            SEVERITY.get(str(row[4])), # severity, converting FogBugz's 'priority' to Trac's 'severity'
            PRIORITY.get(str(row[5])), # priority, converting FogBugz's 'priority' to Trac's 'priority'
            get_username(str(row[6])), # owner
            get_username(str(row[7])), # reporter
            str(row[8]), # cc
            str(row[9]), # url
            str(row[10]), # version
            str(row[11]), # milestone
            STATUS.get(str(row[12])), # status, converting FogBugz's 'status' to Trac's 'status'
            str(row[13]), # resolution
            format_value(str(row[14])), # summary (escaped)
            format_value(str(row[15])), # description (escaped)
            format_value(str(row[16])), # keywords
        )
        print ','.join(export) # Don't like csv module, sorry

if __name__ == '__main__':
    main()

