2
Task Coach - Your friendly task manager
3
Copyright (C) 2011 Task Coach developers <developers@taskcoach.org>
5
Task Coach is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
10
Task Coach is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public License for more details.
15
You should have received a copy of the GNU General Public License
16
along with this program. If not, see <http://www.gnu.org/licenses/>.
20
from taskcoachlib import meta
21
from taskcoachlib.i18n import _
22
from taskcoachlib.thirdparty import chardet
26
import wx.grid as gridlib
27
import wx.wizard as wiz
30
class CSVDialect(csv.Dialect):
31
def __init__(self, delimiter=',', quotechar='"', doublequote=True, escapechar=''):
32
self.delimiter = delimiter
33
self.quotechar = quotechar
34
self.quoting = csv.QUOTE_MINIMAL
35
self.lineterminator = '\r\n'
36
self.doublequote = doublequote
37
self.escapechar = escapechar
39
csv.Dialect.__init__(self)
42
class CSVImportOptionsPage(wiz.WizardPageSimple):
43
def __init__(self, filename, *args, **kwargs):
44
super(CSVImportOptionsPage, self).__init__(*args, **kwargs)
46
self.delimiter = wx.Choice(self, wx.ID_ANY)
47
self.delimiter.Append(_('Comma'))
48
self.delimiter.Append(_('Tab'))
49
self.delimiter.Append(_('Space'))
50
self.delimiter.Append(_('Colon'))
51
self.delimiter.Append(_('Semicolon'))
52
self.delimiter.Append(_('Pipe'))
53
self.delimiter.SetSelection(0)
55
self.date = wx.Choice(self)
56
self.date.Append(_('DD/MM (day first)'))
57
self.date.Append(_('MM/DD (month first)'))
58
self.date.SetSelection(0)
60
self.quoteChar = wx.Choice(self, wx.ID_ANY)
61
self.quoteChar.Append(_('Simple quote'))
62
self.quoteChar.Append(_('Double quote'))
63
self.quoteChar.SetSelection(1)
65
self.quotePanel = wx.Panel(self, wx.ID_ANY)
66
self.doubleQuote = wx.RadioButton(self.quotePanel, wx.ID_ANY, _('Double it'))
67
self.doubleQuote.SetValue(True)
68
self.escapeQuote = wx.RadioButton(self.quotePanel, wx.ID_ANY, _('Escape with'))
69
self.escapeChar = wx.TextCtrl(self.quotePanel, wx.ID_ANY, '\\', size=(50, -1))
70
self.escapeChar.Enable(False)
71
self.escapeChar.SetMaxLength(1)
73
hsizer = wx.BoxSizer(wx.HORIZONTAL)
74
hsizer.Add(self.doubleQuote, 1, wx.ALL, 3)
75
hsizer.Add(self.escapeQuote, 1, wx.ALL, 3)
76
hsizer.Add(self.escapeChar, 1, wx.ALL, 3)
77
self.quotePanel.SetSizer(hsizer)
79
self.importSelectedRowsOnly = wx.CheckBox(self, wx.ID_ANY, _('Import only the selected rows'))
80
self.importSelectedRowsOnly.SetValue(False)
82
self.hasHeaders = wx.CheckBox(self, wx.ID_ANY, _('First line describes fields'))
83
self.hasHeaders.SetValue(True)
85
self.grid = gridlib.Grid(self)
86
self.grid.SetRowLabelSize(0)
87
self.grid.SetColLabelSize(0)
88
self.grid.CreateGrid(0, 0)
89
self.grid.EnableEditing(False)
90
self.grid.SetSelectionMode(self.grid.wxGridSelectRows)
92
vsizer = wx.BoxSizer(wx.VERTICAL)
93
gridSizer = wx.FlexGridSizer(0, 2)
95
gridSizer.Add(wx.StaticText(self, wx.ID_ANY, _('Delimiter')), 0,
96
wx.ALIGN_CENTRE_VERTICAL | wx.ALL, 3)
97
gridSizer.Add(self.delimiter, 0, wx.ALL, 3)
99
gridSizer.Add(wx.StaticText(self, wx.ID_ANY, _('Date format')), 0,
100
wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3)
101
gridSizer.Add(self.date, 0, wx.ALL, 3)
103
gridSizer.Add(wx.StaticText(self, wx.ID_ANY, _('Quote character')), 0,
104
wx.ALIGN_CENTRE_VERTICAL | wx.ALL, 3)
105
gridSizer.Add(self.quoteChar, 0, wx.ALL, 3)
107
gridSizer.Add(wx.StaticText(self, wx.ID_ANY, _('Escape quote')), 0,
108
wx.ALIGN_CENTRE_VERTICAL | wx.ALL, 3)
109
gridSizer.Add(self.quotePanel, 0, wx.ALL, 3)
111
gridSizer.Add(self.importSelectedRowsOnly, 0, wx.ALL, 3)
112
gridSizer.Add((0, 0))
114
gridSizer.Add(self.hasHeaders, 0, wx.ALL, 3)
115
gridSizer.Add((0, 0))
117
gridSizer.AddGrowableCol(1)
118
vsizer.Add(gridSizer, 0, wx.EXPAND | wx.ALL, 3)
120
vsizer.Add(self.grid, 1, wx.EXPAND | wx.ALL, 3)
122
self.SetSizer(vsizer)
126
self.filename = filename
127
self.encoding = chardet.detect(file(filename, 'rb').read())['encoding']
128
self.OnOptionChanged(None)
130
wx.EVT_CHOICE(self.delimiter, wx.ID_ANY, self.OnOptionChanged)
131
wx.EVT_CHOICE(self.quoteChar, wx.ID_ANY, self.OnOptionChanged)
132
wx.EVT_CHECKBOX(self.importSelectedRowsOnly, wx.ID_ANY, self.OnOptionChanged)
133
wx.EVT_CHECKBOX(self.hasHeaders, wx.ID_ANY, self.OnOptionChanged)
134
wx.EVT_RADIOBUTTON(self.doubleQuote, wx.ID_ANY, self.OnOptionChanged)
135
wx.EVT_RADIOBUTTON(self.escapeQuote, wx.ID_ANY, self.OnOptionChanged)
136
wx.EVT_TEXT(self.escapeChar, wx.ID_ANY, self.OnOptionChanged)
138
def OnOptionChanged(self, event): # pylint: disable=W0613
139
self.escapeChar.Enable(self.escapeQuote.GetValue())
141
if self.filename is None:
142
self.grid.SetRowLabelSize(0)
143
self.grid.SetColLabelSize(0)
144
if self.grid.GetNumberCols():
145
self.grid.DeleteRows(0, self.grid.GetNumberRows())
146
self.grid.DeleteCols(0, self.grid.GetNumberCols())
148
if self.doubleQuote.GetValue():
153
escapechar = self.escapeChar.GetValue().encode('UTF-8')
154
self.dialect = CSVDialect(delimiter={0: ',', 1: '\t', 2: ' ', 3: ':', 4: ';', 5: '|'}[self.delimiter.GetSelection()],
155
quotechar={0: "'", 1: '"'}[self.quoteChar.GetSelection()],
156
doublequote=doublequote, escapechar=escapechar)
158
fp = tempfile.TemporaryFile()
160
fp.write(file(self.filename, 'rU').read().decode(self.encoding).encode('UTF-8'))
163
reader = csv.reader(fp, dialect=self.dialect)
165
if self.hasHeaders.GetValue():
166
self.headers = [header.decode('UTF-8') for header in reader.next()]
168
# In some cases, empty fields are omitted if they're at the end...
171
hsize = max(hsize, len(line))
172
self.headers = [_('Field #%d') % idx for idx in xrange(hsize)]
174
reader = csv.reader(fp, dialect=self.dialect)
176
if self.grid.GetNumberCols():
177
self.grid.DeleteRows(0, self.grid.GetNumberRows())
178
self.grid.DeleteCols(0, self.grid.GetNumberCols())
179
self.grid.InsertCols(0, len(self.headers))
181
self.grid.SetColLabelSize(20)
182
for idx, header in enumerate(self.headers):
183
self.grid.SetColLabelValue(idx, header)
187
self.grid.InsertRows(lineno, 1)
188
for idx, value in enumerate(line):
189
if idx < self.grid.GetNumberCols():
190
self.grid.SetCellValue(lineno, idx, value.decode('UTF-8'))
195
def GetOptions(self):
196
return dict(dialect=self.dialect,
197
dayfirst=self.date.GetSelection() == 0,
198
importSelectedRowsOnly=self.importSelectedRowsOnly.GetValue(),
199
selectedRows=self.GetSelectedRows(),
200
hasHeaders=self.hasHeaders.GetValue(),
201
filename=self.filename,
202
encoding=self.encoding,
205
def GetSelectedRows(self):
206
startRows = [row for row, dummy_column in self.grid.GetSelectionBlockTopLeft()]
207
stopRows = [row for row, dummy_column in self.grid.GetSelectionBlockBottomRight()]
209
for startRow, stopRow in zip(startRows, stopRows):
210
selectedRows.extend(range(startRow, stopRow + 1))
214
if self.filename is not None:
215
self.GetNext().SetOptions(self.GetOptions())
217
return False, _('Please select a file.')
220
class CSVImportMappingPage(wiz.WizardPageSimple):
221
def __init__(self, *args, **kwargs):
222
super(CSVImportMappingPage, self).__init__(*args, **kwargs)
224
# (field name, multiple values allowed)
229
(_('Subject'), False),
230
(_('Description'), True),
231
(_('Category'), True),
232
(_('Priority'), False),
233
(_('Planned start date'), False),
234
(_('Due date'), False),
235
(_('Actual start date'), False),
236
(_('Completion date'), False),
237
(_('Reminder date'), False),
238
(_('Budget'), False),
239
(_('Fixed fee'), False),
240
(_('Hourly fee'), False),
241
(_('Percent complete'), False),
244
self.interior = wx.ScrolledWindow(self)
245
self.interior.EnableScrolling(False, True)
246
self.interior.SetScrollRate(10, 10)
248
sizer = wx.BoxSizer()
249
sizer.Add(self.interior, 1, wx.EXPAND)
252
def SetOptions(self, options):
253
self.options = options
255
if self.interior.GetSizer():
256
self.interior.GetSizer().Clear(True)
258
for child in self.interior.GetChildren():
259
self.interior.RemoveChild(child)
262
gsz = wx.FlexGridSizer(0, 2, 4, 2)
264
gsz.Add(wx.StaticText(self.interior, wx.ID_ANY, _('Column header in CSV file')))
265
gsz.Add(wx.StaticText(self.interior, wx.ID_ANY, _('%s attribute') % meta.name))
266
gsz.AddSpacer((3, 3))
267
gsz.AddSpacer((3, 3))
268
tcFieldNames = [field[0] for field in self.fields]
269
for fieldName in options['fields']:
270
gsz.Add(wx.StaticText(self.interior, wx.ID_ANY, fieldName), flag=wx.ALIGN_CENTER_VERTICAL)
272
choice = wx.Choice(self.interior, wx.ID_ANY)
273
for tcFieldName in tcFieldNames:
274
choice.Append(tcFieldName)
275
choice.SetSelection(self.findFieldName(fieldName, tcFieldNames))
276
self.choices.append(choice)
278
gsz.Add(choice, flag=wx.ALIGN_CENTER_VERTICAL)
280
gsz.AddGrowableCol(1)
281
self.interior.SetSizer(gsz)
284
def findFieldName(self, fieldName, fieldNames):
285
def fieldNameIndex(fieldName, fieldNames):
286
return fieldNames.index(fieldName) if fieldName in fieldNames else 0
288
index = fieldNameIndex(fieldName, fieldNames)
289
return index if index else fieldNameIndex(fieldName[:6], [fieldName[:6] for fieldName in fieldNames])
295
for index, (fieldName, canMultiple) in enumerate(self.fields):
297
for choice in self.choices:
298
if choice.GetSelection() == index:
300
if choice.GetSelection() != 0:
302
if count > 1 and not canMultiple:
303
wrongFields.append(fieldName)
305
if countNotNone == 0:
306
return False, _('No field mapping.')
308
if len(wrongFields) == 1:
309
return False, _('The "%s" field cannot be selected several times.') % wrongFields[0]
312
return False, _('The fields %s cannot be selected several times.') % ', '.join(['"%s"' % fieldName for fieldName in wrongFields])
316
def GetOptions(self):
317
options = dict(self.options)
318
options['mappings'] = [self.fields[choice.GetSelection()][0] for choice in self.choices]
322
class CSVImportWizard(wiz.Wizard):
323
def __init__(self, filename, *args, **kwargs):
324
kwargs['style'] = wx.RESIZE_BORDER | wx.DEFAULT_DIALOG_STYLE
325
super(CSVImportWizard, self).__init__(*args, **kwargs)
327
self.optionsPage = CSVImportOptionsPage(filename, self)
328
self.mappingPage = CSVImportMappingPage(self)
329
self.optionsPage.SetNext(self.mappingPage)
330
self.mappingPage.SetPrev(self.optionsPage)
332
self.SetPageSize((600, -1)) # I know this is obsolete but it's the only one that works...
334
wiz.EVT_WIZARD_PAGE_CHANGING(self, wx.ID_ANY, self.OnPageChanging)
335
wiz.EVT_WIZARD_PAGE_CHANGED(self, wx.ID_ANY, self.OnPageChanged)
337
def OnPageChanging(self, event):
338
if event.GetDirection():
339
can, msg = event.GetPage().CanGoNext()
341
wx.MessageBox(msg, _('Information'), wx.OK)
344
def OnPageChanged(self, event):
345
if event.GetPage() == self.optionsPage:
349
return super(CSVImportWizard, self).RunWizard(self.optionsPage)
351
def GetOptions(self):
352
return self.mappingPage.GetOptions()