| 1 | """
|
|---|
| 2 | Import tickets into a Trac database from a correctly formatted comma delimited file.
|
|---|
| 3 |
|
|---|
| 4 | Requires: Trac 0.9 or greater from http://trac.edgewall.com/
|
|---|
| 5 | Python 2.3 from http://www.python.org/
|
|---|
| 6 |
|
|---|
| 7 | Author: Felix Collins felix at keyghost.com
|
|---|
| 8 | Copyright: Felix Collins 2005 - 2006
|
|---|
| 9 |
|
|---|
| 10 | Acknowledgement : This script borrows heavily from sourceforge2trac.py
|
|---|
| 11 |
|
|---|
| 12 | Each line in the CSV file needs to have the following entries
|
|---|
| 13 | type text -- the ticket purpose
|
|---|
| 14 | time integer -- the time it was created
|
|---|
| 15 | changetime integer
|
|---|
| 16 | component text
|
|---|
| 17 | severity text
|
|---|
| 18 | priority text
|
|---|
| 19 | owner text -- who is this ticket assigned to
|
|---|
| 20 | reporter text
|
|---|
| 21 | cc text -- email addresses to notify
|
|---|
| 22 | url text -- url related to this ticket
|
|---|
| 23 | version text --
|
|---|
| 24 | milestone text --
|
|---|
| 25 | status text
|
|---|
| 26 | resolution text
|
|---|
| 27 | summary text -- one-line summary
|
|---|
| 28 | description text -- problem description (long)
|
|---|
| 29 | keywords text
|
|---|
| 30 |
|
|---|
| 31 | Any of the fields may be left blank except for type, status, severity and priority.
|
|---|
| 32 | By default a blank field will be left blank in the ticket
|
|---|
| 33 | If the summary field is blank then no ticket will be entered
|
|---|
| 34 | If the time field is blank then the current time will be used
|
|---|
| 35 |
|
|---|
| 36 | The component, version and milestone fields can use any value.
|
|---|
| 37 | These values will be entered into the component, version and milestone tables respectively.
|
|---|
| 38 |
|
|---|
| 39 | type must be one of: task, defect, enhancement
|
|---|
| 40 | status must be one of: new, assigned, reopened, closed
|
|---|
| 41 | severity must be one of: blocker, critical, major, normal, minor, trivial, enhancement
|
|---|
| 42 | priority must be one of: highest, high, normal, low, lowest
|
|---|
| 43 |
|
|---|
| 44 | In addition to the above entries, custom fields are supported.
|
|---|
| 45 | The name of the custom field should appear in the first row.
|
|---|
| 46 | To enable processing of the custom fields by Trac you will need to put an entry in your trac.ini file
|
|---|
| 47 | The following creates an entry for a field called "estimate".
|
|---|
| 48 | "estimate = text" defines estimate as a single line text entry. See the Trac documentation
|
|---|
| 49 | on custom fields for more help.
|
|---|
| 50 |
|
|---|
| 51 | [ticket-custom]
|
|---|
| 52 | estimate = text
|
|---|
| 53 | estimate.label = First Estimate (hours)
|
|---|
| 54 |
|
|---|
| 55 | Issues
|
|---|
| 56 | This script does not do any checking of character formats. It is therefore
|
|---|
| 57 | quite easy to get illegal characters into the database which will prevent Trac
|
|---|
| 58 | from displaying the ticket. To avoid this stick to alphanumerics if possible.
|
|---|
| 59 | Fixing problems of this nature requires manually removing the tickets from the
|
|---|
| 60 | database with the sqlite3 tool.
|
|---|
| 61 |
|
|---|
| 62 | """
|
|---|
| 63 |
|
|---|
| 64 | from datetime import datetime
|
|---|
| 65 | import trac.env
|
|---|
| 66 | import csv
|
|---|
| 67 | import time
|
|---|
| 68 | import sys
|
|---|
| 69 |
|
|---|
| 70 | #This function allow the script to be called from command line or programatically
|
|---|
| 71 | def Output(Content):
|
|---|
| 72 | if Output.CALLEDFROMCMD == True:
|
|---|
| 73 | print Content
|
|---|
| 74 | else:
|
|---|
| 75 | Output.outputstring = Output.outputstring + Content + "<br>"
|
|---|
| 76 | Output.outputstring = ""
|
|---|
| 77 | Output.CALLEDFROMCMD = False
|
|---|
| 78 |
|
|---|
| 79 | class ImportedCSVData(object):
|
|---|
| 80 | def __init__(self, filename):
|
|---|
| 81 |
|
|---|
| 82 | #This code reads the file and just gets the field names
|
|---|
| 83 | #This is done to support python 2.3
|
|---|
| 84 | self.fileobject = open(filename, "rb")
|
|---|
| 85 | read = csv.reader(self.fileobject)
|
|---|
| 86 | fieldnames = read.next()
|
|---|
| 87 |
|
|---|
| 88 | #must rewind the file object after each use to a known location
|
|---|
| 89 | self.fileobject.seek(0)
|
|---|
| 90 |
|
|---|
| 91 | #work out what the custom fields are
|
|---|
| 92 | standardfields = ["type", "time", "changetime", "component", "severity", "priority", "owner", "reporter",
|
|---|
| 93 | "cc", "url", "version", "milestone", "status", "resolution", "summary",
|
|---|
| 94 | "description", "keywords"]
|
|---|
| 95 | self.customfields = []
|
|---|
| 96 | readstandardfields = []
|
|---|
| 97 | missingfields = []
|
|---|
| 98 | for field in fieldnames:
|
|---|
| 99 | if field not in standardfields:
|
|---|
| 100 | self.customfields.append(field)
|
|---|
| 101 | else:
|
|---|
| 102 | readstandardfields.append(field)
|
|---|
| 103 |
|
|---|
| 104 | if readstandardfields != standardfields:
|
|---|
| 105 | for field in standardfields:
|
|---|
| 106 | if field not in fieldnames:
|
|---|
| 107 | missingfields.append(field)
|
|---|
| 108 | Output("Warning: missing fields in the csv file. Missing: %s" %missingfields)
|
|---|
| 109 |
|
|---|
| 110 | self.reader = csv.DictReader(self.fileobject, fieldnames + missingfields, restval="")
|
|---|
| 111 |
|
|---|
| 112 | #returns the DictReader with the tickets in it
|
|---|
| 113 | #remember that the reader can only be iterated through once without
|
|---|
| 114 | #rewinding the file pointer (ImportedCSVData.reader.seek(0))
|
|---|
| 115 | def getnewtickets(self):
|
|---|
| 116 | return(self.reader)
|
|---|
| 117 |
|
|---|
| 118 | #returns a list of the custom fields defined in the csv file
|
|---|
| 119 | def getcustomfields(self):
|
|---|
| 120 | return(self.customfields)
|
|---|
| 121 |
|
|---|
| 122 | #returns a list of the component types needed for the import
|
|---|
| 123 | def getcomponents(self):
|
|---|
| 124 | components = []
|
|---|
| 125 | for row in self.reader:
|
|---|
| 126 | if row["summary"] != "summary":
|
|---|
| 127 | if row["component"] != "":
|
|---|
| 128 | if row["component"] not in components:
|
|---|
| 129 | components.append(row["component"])
|
|---|
| 130 | self.fileobject.seek(0)
|
|---|
| 131 | return(components)
|
|---|
| 132 |
|
|---|
| 133 | #returns a list of the versions needed for the import
|
|---|
| 134 | def getversions(self):
|
|---|
| 135 | versions = []
|
|---|
| 136 | for row in self.reader:
|
|---|
| 137 | if row["summary"] != "summary":
|
|---|
| 138 | if row["version"] != "":
|
|---|
| 139 | if row["version"] not in versions:
|
|---|
| 140 | versions.append(row["version"])
|
|---|
| 141 | self.fileobject.seek(0)
|
|---|
| 142 | return(versions)
|
|---|
| 143 |
|
|---|
| 144 | #returns a list of the milestones needed for the import
|
|---|
| 145 | def getmilestones(self):
|
|---|
| 146 | milestones = []
|
|---|
| 147 | for row in self.reader:
|
|---|
| 148 | if row["summary"] != "summary":
|
|---|
| 149 | if row["milestone"] != "":
|
|---|
| 150 | if row["milestone"] not in milestones:
|
|---|
| 151 | milestones.append(row["milestone"])
|
|---|
| 152 | self.fileobject.seek(0)
|
|---|
| 153 | return(milestones)
|
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 | class TracDatabase(object):
|
|---|
| 157 | def __init__(self, path):
|
|---|
| 158 | self.env = trac.env.Environment(path)
|
|---|
| 159 | self._db = self.env.get_db_cnx()
|
|---|
| 160 | self._db.autocommit = False
|
|---|
| 161 |
|
|---|
| 162 | def db(self):
|
|---|
| 163 | return self._db
|
|---|
| 164 |
|
|---|
| 165 | #checks whether there are any existing tickets returns a boolean
|
|---|
| 166 | def hasTickets(self):
|
|---|
| 167 | c = self.db().cursor()
|
|---|
| 168 | c.execute('''SELECT count(*) FROM Ticket''')
|
|---|
| 169 | return int(c.fetchall()[0][0]) > 0
|
|---|
| 170 |
|
|---|
| 171 | def setComponentList(self, componentlist):
|
|---|
| 172 | """Remove all components, make new components from the entries in componentlist"""
|
|---|
| 173 | c = self.db().cursor()
|
|---|
| 174 | if self.hasTickets():
|
|---|
| 175 | #if we have tickets then we are importing into a working db
|
|---|
| 176 | #so get the existing components and only create new ones that don't already exist
|
|---|
| 177 | c.execute('SELECT (name) FROM component')
|
|---|
| 178 | rows = c.fetchall()
|
|---|
| 179 | if rows:
|
|---|
| 180 | for r in rows:
|
|---|
| 181 | if r[0] in componentlist:
|
|---|
| 182 | componentlist.remove(r[0])
|
|---|
| 183 | Output("Existing component detected: %s\n" %r[0])
|
|---|
| 184 |
|
|---|
| 185 | else: # no existing tickets therefore start from scratch
|
|---|
| 186 | c.execute("""DELETE FROM component""")
|
|---|
| 187 |
|
|---|
| 188 | for value in componentlist:
|
|---|
| 189 | c.execute("""INSERT INTO component (name) VALUES ("%s")""" % value)
|
|---|
| 190 | Output("New component: %s" % value)
|
|---|
| 191 | self.db().commit()
|
|---|
| 192 |
|
|---|
| 193 | def setVersionList(self, versionlist):
|
|---|
| 194 | """Remove all versions, make new versions from the entries in versionlist"""
|
|---|
| 195 | c = self.db().cursor()
|
|---|
| 196 | if self.hasTickets():
|
|---|
| 197 | #if we have tickets then we are importing into a working db
|
|---|
| 198 | #so get the existing versions and only create new ones that don't already exist
|
|---|
| 199 | c.execute('SELECT (name) FROM version')
|
|---|
| 200 | rows = c.fetchall()
|
|---|
| 201 | if rows:
|
|---|
| 202 | for r in rows:
|
|---|
| 203 | if r[0] in versionlist:
|
|---|
| 204 | versionlist.remove(r[0])
|
|---|
| 205 | Output("Existing version detected: %s\n" %r[0])
|
|---|
| 206 |
|
|---|
| 207 | else: # no existing tickets therefore start from scratch
|
|---|
| 208 | c.execute("""DELETE FROM version""")
|
|---|
| 209 |
|
|---|
| 210 |
|
|---|
| 211 | for value in versionlist:
|
|---|
| 212 | c.execute("""INSERT INTO version (name) VALUES ("%s")""" % value)
|
|---|
| 213 | Output("New version: %s" % value)
|
|---|
| 214 | self.db().commit()
|
|---|
| 215 |
|
|---|
| 216 | def setMilestoneList(self, milestonelist):
|
|---|
| 217 | """Remove all milestones, make new milestones from the entries in milestonelist"""
|
|---|
| 218 | c = self.db().cursor()
|
|---|
| 219 |
|
|---|
| 220 | if self.hasTickets():
|
|---|
| 221 | #if we have tickets then we are importing into a working db
|
|---|
| 222 | #so get the existing milestones and only create new ones that don't already exist
|
|---|
| 223 | c.execute('SELECT (name) FROM milestone')
|
|---|
| 224 | rows = c.fetchall()
|
|---|
| 225 | if rows:
|
|---|
| 226 | for r in rows:
|
|---|
| 227 | if r[0] in milestonelist:
|
|---|
| 228 | milestonelist.remove(r[0])
|
|---|
| 229 | Output("Existing milestone detected: %s\n" %r[0])
|
|---|
| 230 |
|
|---|
| 231 | else: # no existing tickets therefore start from scratch
|
|---|
| 232 | c.execute("""DELETE FROM milestone""")
|
|---|
| 233 |
|
|---|
| 234 |
|
|---|
| 235 | for value in milestonelist:
|
|---|
| 236 | c.execute("""INSERT INTO milestone (name) VALUES ("%s")""" % value)
|
|---|
| 237 | Output("New milestone: %s" % value)
|
|---|
| 238 | self.db().commit()
|
|---|
| 239 |
|
|---|
| 240 | def addTicket(self, ttype, time, changetime, component,
|
|---|
| 241 | severity, priority, owner, reporter, cc,
|
|---|
| 242 | version, milestone, status, resolution,
|
|---|
| 243 | summary, description, keywords):
|
|---|
| 244 | c = self.db().cursor()
|
|---|
| 245 | if status.lower() == 'open':
|
|---|
| 246 | if owner != '':
|
|---|
| 247 | status = 'assigned'
|
|---|
| 248 | else:
|
|---|
| 249 | status = 'new'
|
|---|
| 250 |
|
|---|
| 251 | c.execute('SELECT summary FROM ticket WHERE summary="%s"' %summary)
|
|---|
| 252 | rows = c.fetchall()
|
|---|
| 253 | if rows:
|
|---|
| 254 | Output("Existing ticket with matching summary exists - not overwriting %s" %summary)
|
|---|
| 255 | return 0
|
|---|
| 256 | else:
|
|---|
| 257 |
|
|---|
| 258 | c.execute('INSERT INTO ticket (type, time, changetime, component,\
|
|---|
| 259 | severity, priority, owner, reporter, cc,\
|
|---|
| 260 | version, milestone, status, resolution,\
|
|---|
| 261 | summary, description, keywords)\
|
|---|
| 262 | VALUES ("%s", "%s", "%s", "%s", "%s",\
|
|---|
| 263 | "%s", "%s", "%s", "%s", "%s",\
|
|---|
| 264 | "%s", "%s", "%s", "%s", "%s",\
|
|---|
| 265 | "%s")' %(
|
|---|
| 266 | ttype, time, changetime, component,
|
|---|
| 267 | severity, priority, owner, reporter,
|
|---|
| 268 | cc, version, milestone, status.lower(),
|
|---|
| 269 | resolution, summary, description, keywords))
|
|---|
| 270 | self.db().commit()
|
|---|
| 271 | return self.db().get_last_id(c, 'ticket')
|
|---|
| 272 |
|
|---|
| 273 |
|
|---|
| 274 | def addTicketCustom(self, ticket, name, value):
|
|---|
| 275 | c = self.db().cursor()
|
|---|
| 276 | c.execute("""INSERT INTO ticket_custom (ticket, name, value)
|
|---|
| 277 | VALUES ("%s", "%s", "%s")""" %(
|
|---|
| 278 | ticket, name, value))
|
|---|
| 279 | self.db().commit()
|
|---|
| 280 |
|
|---|
| 281 |
|
|---|
| 282 | def main():
|
|---|
| 283 | import optparse
|
|---|
| 284 | Output.CALLEDFROMCMD = True
|
|---|
| 285 | p = optparse.OptionParser('usage: %prog newtickets.csv /path/to/trac/environment')
|
|---|
| 286 | opt, args = p.parse_args()
|
|---|
| 287 | if len(args) != 2:
|
|---|
| 288 | p.error("Incorrect number of arguments")
|
|---|
| 289 |
|
|---|
| 290 | try:
|
|---|
| 291 | importCSV(args[0], args[1])
|
|---|
| 292 | except Exception, e:
|
|---|
| 293 | Output('Error:%s' %e)
|
|---|
| 294 |
|
|---|
| 295 | def importCSV(f, env):
|
|---|
| 296 |
|
|---|
| 297 | project = ImportedCSVData(f)
|
|---|
| 298 |
|
|---|
| 299 | db = TracDatabase(env)
|
|---|
| 300 | db.setComponentList(project.getcomponents())
|
|---|
| 301 | Output("Imported components")
|
|---|
| 302 | db.setVersionList(project.getversions())
|
|---|
| 303 | Output("Imported versions")
|
|---|
| 304 | db.setMilestoneList(project.getmilestones())
|
|---|
| 305 | Output("Imported milestones")
|
|---|
| 306 |
|
|---|
| 307 | tickets = project.getnewtickets()
|
|---|
| 308 |
|
|---|
| 309 | newid = 1
|
|---|
| 310 | linecount = 0
|
|---|
| 311 | for t in tickets:
|
|---|
| 312 | if t["summary"] != "summary":
|
|---|
| 313 | #Output("processing ticket %s" % t["summary"])
|
|---|
| 314 | linecount += 1
|
|---|
| 315 |
|
|---|
| 316 | #put current time in rows with no time
|
|---|
| 317 | tickettime = t["time"]
|
|---|
| 318 | if tickettime == "":
|
|---|
| 319 | tickettime = int(time.time())
|
|---|
| 320 |
|
|---|
| 321 | #Don't import tickets with no summary
|
|---|
| 322 | if t["summary"] != "":
|
|---|
| 323 |
|
|---|
| 324 | newid = db.addTicket(ttype=t["type"],
|
|---|
| 325 | time=tickettime,
|
|---|
| 326 | changetime=tickettime,
|
|---|
| 327 | component=t["component"],
|
|---|
| 328 | severity=t["severity"],
|
|---|
| 329 | priority=t["priority"],
|
|---|
| 330 | owner=t["owner"],
|
|---|
| 331 | reporter=t["reporter"],
|
|---|
| 332 | cc=t["cc"],
|
|---|
| 333 | version=t["version"],
|
|---|
| 334 | milestone=t["milestone"],
|
|---|
| 335 | status=t["status"],
|
|---|
| 336 | resolution=t["resolution"],
|
|---|
| 337 | summary=t["summary"],
|
|---|
| 338 | description=t["description"],
|
|---|
| 339 | keywords=t["keywords"])
|
|---|
| 340 |
|
|---|
| 341 | if newid != 0:
|
|---|
| 342 | Output('Imported "%s" as ticket #%d' % (t["summary"], newid))
|
|---|
| 343 | #do the custom fields
|
|---|
| 344 | customfields = project.getcustomfields()
|
|---|
| 345 | for field in customfields:
|
|---|
| 346 | if t[field] != "":
|
|---|
| 347 | db.addTicketCustom(newid, field, t[field])
|
|---|
| 348 | else:
|
|---|
| 349 | Output("Warning: Ignored ticket with no summary at line %s" %linecount)
|
|---|
| 350 | Output('Finished importing')
|
|---|
| 351 | return Output.outputstring
|
|---|
| 352 |
|
|---|
| 353 | if __name__ == '__main__':
|
|---|
| 354 | main()
|
|---|