Package Camelot :: Package camelot :: Package view :: Package wizard :: Module importwizard
[frames] | no frames]

Source Code for Module Camelot.camelot.view.wizard.importwizard

  1  #  ============================================================================
 
  2  #
 
  3  #  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
 
  4  #  www.conceptive.be / project-camelot@conceptive.be
 
  5  #
 
  6  #  This file is part of the Camelot Library.
 
  7  #
 
  8  #  This file may be used under the terms of the GNU General Public
 
  9  #  License version 2.0 as published by the Free Software Foundation
 
 10  #  and appearing in the file LICENSE.GPL included in the packaging of
 
 11  #  this file.  Please review the following information to ensure GNU
 
 12  #  General Public Licensing requirements will be met:
 
 13  #  http://www.trolltech.com/products/qt/opensource.html
 
 14  #
 
 15  #  If you are unsure which license is appropriate for your use, please
 
 16  #  review the following information:
 
 17  #  http://www.trolltech.com/products/qt/licensing.html or contact
 
 18  #  project-camelot@conceptive.be.
 
 19  #
 
 20  #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 
 21  #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 
 22  #
 
 23  #  For use of this library in commercial applications, please contact
 
 24  #  project-camelot@conceptive.be
 
 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  #logging.basicConfig(level=logging.DEBUG)
 
 49  logger = logging.getLogger('camelot.view.wizard.importwizard') 
 50  
 
 51  
 
52 -class RowData(object):
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
69 - def __getattr__(self, attr_name):
70 return None
71 72 # see http://docs.python.org/library/csv.html
73 -class UTF8Recoder:
74 """Iterator that reads an encoded stream and reencodes the input to 75 UTF-8.""" 76
77 - def __init__(self, f, encoding):
78 self.reader = codecs.getreader(encoding)(f)
79
80 - def __iter__(self):
81 return self
82
83 - def next(self):
84 return self.reader.next().encode('utf-8')
85 86 87 # see http://docs.python.org/library/csv.html
88 -class UnicodeReader:
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
96 - def next(self):
97 row = self.reader.next() 98 return [unicode(s, 'utf-8') for s in row]
99
100 - def __iter__(self):
101 return self
102
103 -class CsvCollectionGetter(object):
104 """class that when called returns the data in filename as a list of RowData 105 objects""" 106
107 - def __init__(self, filename):
108 self.filename = filename 109 self._data = None
110
111 - def __call__(self):
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
129 -class RowDataAdminDecorator(object):
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
140 - def __init__(self, object_admin):
141 """:param object_admin: the object_admin object that will be 142 decorated""" 143 self._object_admin = object_admin 144 self._columns = None
145
146 - def __getattr__(self, attr):
147 return getattr(self._object_admin, attr)
148
149 - def create_validator(self, model):
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
169 - def get_fields(self):
170 return self.get_columns()
171
172 - def flush(self, obj):
173 pass
174
175 - def get_columns(self):
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 # remove some attributes that might disturb the import wizard 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
268 - def validate_all_rows(self):
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
368 -class DataPreviewCollectionProxy(CollectionProxy):
369 header_icon = None
370
371 -class ImportWizard(QtGui.QWizard):
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):
387 """:param admin: camelot model admin of the destination data""" 388 super(ImportWizard, self).__init__(parent) 389 assert admin 390 # 391 # Set the size of the wizard to 2/3rd of the screen, since we want to get some work done 392 # here, the user needs to verify and possibly correct its data 393 # 394 desktop = QtCore.QCoreApplication.instance().desktop() 395 self.setMinimumSize(desktop.width()*2/3, desktop.height()*2/3) 396 397 row_data_admin = RowDataAdminDecorator(admin) 398 model = DataPreviewCollectionProxy( 399 row_data_admin, 400 lambda:[], 401 row_data_admin.get_columns 402 ) 403 self.setWindowTitle(_(self.window_title)) 404 self.add_pages(model, admin) 405 self.setOption(QtGui.QWizard.NoCancelButton)
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