Edgewall Software

TracSynchronize: csv2trac.py

File csv2trac.py, 13.0 KB (added by Felix Collins, 3 years ago)

Import script for tickets

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