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):
    269          validator = self.model.get_validator()
    270          post(validator.validate_all_rows, self.update_complete)
    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