1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 """Module for managing imports"""
29
30 import logging
31
32 import csv
33 import codecs
34
35 from PyQt4 import QtGui
36 from PyQt4 import QtCore
37 from PyQt4.QtCore import Qt
38 from PyQt4.QtGui import QColor
39
40 from camelot.core.utils import ugettext as _
41
42 from camelot.view.art import Pixmap
43 from camelot.view.model_thread import post
44 from camelot.view.wizard.pages.select import SelectFilePage
45 from camelot.view.controls.editors.one2manyeditor import One2ManyEditor
46 from camelot.view.proxy.collection_proxy import CollectionProxy
47
48
49 logger = logging.getLogger('camelot.view.wizard.importwizard')
50
51
53 """Class representing the data in a single row of the imported file as an
54 object with attributes column_1, column_2, ..., each representing the data
55 in a single column of that row.
56
57 since the imported file might contain less columns than expected, the RowData
58 object returns None for not existing attributes
59 """
60
61 - def __init__(self, row_number, row_data):
62 """:param row_data: a list containing the data
63 [column_1_data, column_2_data, ...] for a single row
64 """
65 self.id = row_number + 1
66 for i, data in enumerate(row_data):
67 self.__setattr__('column_%i' % i, data)
68
71
72
74 """Iterator that reads an encoded stream and reencodes the input to
75 UTF-8."""
76
78 self.reader = codecs.getreader(encoding)(f)
79
82
84 return self.reader.next().encode('utf-8')
85
86
87
89 """A CSV reader which will iterate over lines in the CSV file "f", which is
90 encoded in the given encoding."""
91
92 - def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds):
93 f = UTF8Recoder(f, encoding)
94 self.reader = csv.reader(f, dialect=dialect, **kwds)
95
97 row = self.reader.next()
98 return [unicode(s, 'utf-8') for s in row]
99
102
104 """class that when called returns the data in filename as a list of RowData
105 objects"""
106
108 self.filename = filename
109 self._data = None
110
112 if self._data==None:
113 self._data = []
114 import chardet
115
116 enc = (
117 chardet.detect(open(self.filename).read())['encoding']
118 or 'utf-8'
119 )
120 items = UnicodeReader(open(self.filename), encoding=enc)
121
122 self._data = [
123 RowData(i, row_data)
124 for i, row_data in enumerate(items)
125 ]
126
127 return self._data
128
130 """Decorator that transforms the Admin of the class to be imported to an
131 Admin of the RowData objects to be used when previewing and validating the
132 data to be imported.
133
134 based on the field attributes of the original mode, it will turn the background color pink
135 if the data is invalid for being imported.
136 """
137
138 invalid_color = QColor('Pink')
139
141 """:param object_admin: the object_admin object that will be
142 decorated"""
143 self._object_admin = object_admin
144 self._columns = None
145
147 return getattr(self._object_admin, attr)
148
150 """Creates a validator that validates the data to be imported, the validator will
151 check if the background of the cell is pink, and if it is it will mark that object
152 as invalid.
153 """
154 from camelot.admin.validator.object_validator import ObjectValidator
155
156 class NewObjectValidator(ObjectValidator):
157
158 def objectValidity(self, entity_instance):
159 for _field_name, attributes in self.admin.get_columns():
160 background_color_getter = attributes.get('background_color', None)
161 if background_color_getter:
162 background_color = background_color_getter(entity_instance)
163 if background_color==self.admin.invalid_color:
164 return ['invalid field']
165 return []
166
167 return NewObjectValidator(self, model)
168
171
174
176 if self._columns:
177 return self._columns
178
179 original_columns = self._object_admin.get_columns()
180
181 def create_getter(i):
182 return lambda o:getattr(o, 'column_%i'%i)
183
184 def new_field_attributes(i, original_field_attributes, original_field):
185 from camelot.view.controls import delegates
186 attributes = dict(original_field_attributes)
187 attributes['delegate'] = delegates.PlainTextDelegate
188 attributes['python_type'] = str
189 attributes['original_field'] = original_field
190 attributes['getter'] = create_getter(i)
191
192
193 for attribute in ['background_color', 'tooltip']:
194 attributes[attribute] = None
195
196 if 'from_string' in attributes:
197
198 def get_background_color(o):
199 """If the string is not convertible with from_string, or
200 the result is None when a value is required, set the
201 background to pink"""
202 value = getattr(o, 'column_%i'%i)
203 if not value and (attributes['nullable']==False):
204 return self.invalid_color
205 try:
206 value = attributes['from_string'](value)
207 return None
208 except:
209 return self.invalid_color
210
211 attributes['background_color'] = get_background_color
212
213 return attributes
214
215 new_columns = [
216 (
217 'column_%i' %i,
218 new_field_attributes(i, attributes, original_field)
219 )
220 for i, (original_field, attributes) in enumerate(original_columns)
221 if attributes['editable']
222 ]
223
224 self._columns = new_columns
225
226 return new_columns
227
228
229 -class DataPreviewPage(QtGui.QWizardPage):
230 """DataPreviewPage is the previewing page for the import wizard"""
231
232 - def __init__(self, parent=None, model=None, collection_getter=None):
233 from camelot.view.controls.editors import NoteEditor
234 super(DataPreviewPage, self).__init__(parent)
235 assert model
236 assert collection_getter
237 self.setTitle(_('Data Preview'))
238 self.setSubTitle(_('Please review the data below.'))
239 self._complete = False
240 self.model = model
241 validator = self.model.get_validator()
242 self.connect( validator, validator.validity_changed_signal, self.update_complete)
243 self.connect( model, QtCore.SIGNAL('layoutChanged()'), self.validate_all_rows )
244 post(validator.validate_all_rows)
245 self.collection_getter = collection_getter
246
247 icon = 'tango/32x32/mimetypes/x-office-spreadsheet.png'
248 self.setPixmap(QtGui.QWizard.LogoPixmap, Pixmap(icon).getQPixmap())
249
250 self.previewtable = One2ManyEditor(
251 admin = model.get_admin(),
252 parent = self,
253 create_inline = True,
254 vertical_header_clickable = False,
255 )
256 self._note = NoteEditor()
257 self._note.set_value(None)
258
259 ly = QtGui.QVBoxLayout()
260 ly.addWidget(self.previewtable)
261 ly.addWidget(self._note)
262 self.setLayout(ly)
263
264 self.setCommitPage(True)
265 self.setButtonText(QtGui.QWizard.CommitButton, _('Import'))
266 self.update_complete()
267
271
272 - def update_complete(self, *args):
273 self._complete = (self.model.get_validator().number_of_invalid_rows()==0)
274 self.emit(QtCore.SIGNAL('completeChanged()'))
275 if self._complete:
276 self._note.set_value(None)
277 else:
278 self._note.set_value(_('Please correct the data above before proceeding with the import.<br/>Incorrect cells have a pink background.'))
279
280 - def initializePage(self):
281 """Gets all info needed from SelectFilePage and feeds table"""
282 filename = self.field('datasource').toString()
283 self._complete = False
284 self.emit(QtCore.SIGNAL('completeChanged()'))
285 self.model.set_collection_getter(self.collection_getter(filename))
286 self.previewtable.set_value(self.model)
287 self.validate_all_rows()
288
289 - def validatePage(self):
290 answer = QtGui.QMessageBox.question(self,
291 _('Proceed with import'),
292 _('Importing data cannot be undone,\nare you sure you want to continue'),
293 QtGui.QMessageBox.Cancel,
294 QtGui.QMessageBox.Ok,
295 )
296 if answer==QtGui.QMessageBox.Ok:
297 return True
298 return False
299
300 - def isComplete(self):
301 return self._complete
302
303
304 -class FinalPage(QtGui.QWizardPage):
305 """FinalPage is the final page in the import process"""
306
307 change_maximum_signal = QtCore.SIGNAL('change_maximum')
308 change_value_signal = QtCore.SIGNAL('change_value')
309
310 - def __init__(self, parent=None, model=None, admin=None):
311 """
312 :model: the source model from which to import data
313 :admin: the admin class of the target data
314 """
315 super(FinalPage, self).__init__(parent)
316 self.setTitle(_('Import Progress'))
317 self.model = model
318 self.admin = admin
319 self.setSubTitle(_('Please wait while data is being imported.'))
320
321 icon = 'tango/32x32/mimetypes/x-office-spreadsheet.png'
322 self.setPixmap(QtGui.QWizard.LogoPixmap, Pixmap(icon).getQPixmap())
323 self.setButtonText(QtGui.QWizard.FinishButton, _('Close'))
324 self.progressbar = QtGui.QProgressBar()
325
326 label = QtGui.QLabel(_(
327 'The data will be ready when the progress reaches 100%.'
328 ))
329 label.setWordWrap(True)
330
331 ly = QtGui.QVBoxLayout()
332 ly.addWidget(label)
333 ly.addWidget(self.progressbar)
334 self.setLayout(ly)
335 self.connect(self, self.change_maximum_signal, self.progressbar.setMaximum)
336 self.connect(self, self.change_value_signal, self.progressbar.setValue)
337
338 - def run_import(self):
339 collection = self.model.get_collection_getter()()
340 self.emit(self.change_maximum_signal, len(collection))
341 for i,row in enumerate(collection):
342 new_entity_instance = self.admin.entity()
343 for field_name, attributes in self.model.get_admin().get_columns():
344 setattr(
345 new_entity_instance,
346 attributes['original_field'],
347 attributes['from_string'](getattr(row, field_name))
348 )
349 self.admin.add(new_entity_instance)
350 self.admin.flush(new_entity_instance)
351 self.emit(self.change_value_signal, i)
352
353 - def import_finished(self):
354 self.progressbar.setMaximum(1)
355 self.progressbar.setValue(1)
356 self.emit(QtCore.SIGNAL('completeChanged()'))
357
358 - def isComplete(self):
359 return self.progressbar.value() == self.progressbar.maximum()
360
361 - def initializePage(self):
362 from camelot.view.model_thread import post
363 self.progressbar.setMaximum(1)
364 self.progressbar.setValue(0)
365 self.emit(QtCore.SIGNAL('completeChanged()'))
366 post(self.run_import, self.import_finished, self.import_finished)
367
370
372 """ImportWizard provides a two-step wizard for importing data as objects
373 into Camelot. To create a custom wizard, subclass this ImportWizard and
374 overwrite its class attributes.
375
376 To import a different file format, you probably need a custom
377 collection_getter for this file type.
378 """
379
380 select_file_page = SelectFilePage
381 data_preview_page = DataPreviewPage
382 final_page = FinalPage
383 collection_getter = CsvCollectionGetter
384 window_title = _('Import CSV data')
385
386 - def __init__(self, parent=None, admin=None):
406
407 - def add_pages(self, model, admin):
408 """
409 Add all pages to the import wizard, reimplement this method to add
410 custom pages to the wizard. This method is called in the __init__method, to add
411 all pages to the wizard.
412
413 :param model: the CollectionProxy that will be used to display the to be imported data
414 :param admin: the admin of the destination data
415 """
416 self.addPage(SelectFilePage(parent=self))
417 self.addPage(
418 DataPreviewPage(
419 parent=self,
420 model=model,
421 collection_getter=self.collection_getter
422 )
423 )
424 self.addPage(FinalPage(parent=self, model=model, admin=admin))
425