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 """Proxy representing a collection of entities that live in the model thread.
29
30 The proxy represents them in the gui thread and provides access to the data
31 with zero delay. If the data is not yet present in the proxy, dummy data is
32 returned and an update signal is emitted when the correct data is available.
33 """
34
35 import logging
36 logger = logging.getLogger('camelot.view.proxy.collection_proxy')
37 verbose = False
38
39 import pickle
40 import elixir
41 import datetime
42 from PyQt4 import QtGui
43 from PyQt4 import QtCore
44 from PyQt4.QtCore import Qt
45
46 from sqlalchemy.orm.session import Session
47 from camelot.view import art
48 from camelot.view.fifo import fifo
49 from camelot.view.controls import delegates
50 from camelot.view.remote_signals import get_signal_handler
51 from camelot.view.model_thread import gui_function
52 from camelot.view.model_thread import model_function
53 from camelot.view.model_thread import get_model_thread
57 """A proxy object needs to be constructed within the GUI thread. Construct
58 a delayed proxy when the construction of a proxy is needed within the Model
59 thread. On first occasion the delayed proxy will be converted to a real
60 proxy within the GUI thread
61 """
62
63 @model_function
65 self.args = args
66 self.kwargs = kwargs
67
68 @gui_function
71
74 """Create row data from an object, by fetching its attributes"""
75 row_data = []
76 mt = get_model_thread()
77
78 def create_collection_getter(o, attr):
79 return lambda: getattr(o, attr)
80
81 for i,col in enumerate(columns):
82 field_attributes = col[1]
83 if field_attributes['python_type'] == list:
84 row_data.append(DelayedProxy(field_attributes['admin'],
85 create_collection_getter(obj, col[0]),
86 field_attributes['admin'].getColumns))
87 else:
88 row_data.append(getattr(obj, col[0]))
89 return row_data
90
92 def unicode_or_none(data):
93 if data:
94 if isinstance(data, list):
95 return '.'.join(data)
96 elif isinstance(data, datetime.datetime):
97
98 if data.year >= 1900:
99 return data.strftime('%d/%m/%Y %H:%M')
100 elif isinstance(data, datetime.date):
101 if data.year >= 1900:
102 return data.strftime('%d/%m/%Y')
103 return unicode(data)
104 return data
105
106 return [unicode_or_none(col) for col in row_data]
107
112
113 empty_row_data = EmptyRowData()
116 """The CollectionProxy contains a limited copy of the data in the actual
117 collection, usable for fast visualisation in a QTableView
118 """
119
120 @gui_function
121 - def __init__(self, admin, collection_getter, columns_getter,
122 max_number_of_rows=10, edits=None, flush_changes=True):
123 """@param admin: the admin interface for the items in the collection
124
125 @param collection_getter: a function that takes no arguments and returns
126 the collection that will be visualized. This function will be called inside
127 the model thread, to prevent delays when this function causes the database
128 to be hit.
129
130 @param columns_getter: a function that takes no arguments and returns the
131 columns that will be cached in the proxy. This function will be called
132 inside the model thread.
133 """
134 logger.debug('initialize query table for %s' % (admin.getName()))
135 self.logger = logger
136 QtCore.QAbstractTableModel.__init__(self)
137 self.admin = admin
138 self.form_icon = QtCore.QVariant(QtGui.QIcon(art.icon16('places/folder')))
139 self.validator = admin.createValidator(self)
140 self.collection_getter = collection_getter
141 self.column_count = 0
142 self.flush_changes = flush_changes
143 self.mt = admin.getModelThread()
144
145 self.rows = 0
146 self._columns = []
147 self.max_number_of_rows = max_number_of_rows
148 self.cache = {Qt.DisplayRole:fifo(10*self.max_number_of_rows),
149 Qt.EditRole:fifo(10*self.max_number_of_rows)}
150
151 self.rows_under_request = set()
152
153 self.unflushed_rows = set()
154
155 self.edits = edits or []
156 self.rsh = get_signal_handler()
157 self.rsh.connect(self.rsh,
158 self.rsh.entity_update_signal,
159 self.handleEntityUpdate)
160 self.rsh.connect(self.rsh,
161 self.rsh.entity_delete_signal,
162 self.handleEntityDelete)
163 self.rsh.connect(self.rsh,
164 self.rsh.entity_create_signal,
165 self.handleEntityCreate)
166
167 def get_columns():
168 self._columns = columns_getter()
169 return self._columns
170
171 self.mt.post(get_columns, lambda columns:self.setColumns(columns))
172
173 self.mt.post(self.getRowCount, self.setRowCount)
174 logger.debug('initialization finished')
175 self.item_delegate = None
176
178 """The model has rows that have not been flushed to the database yet,
179 because the row is invalid
180 """
181 return len(self.unflushed_rows) > 0
182
183 @model_function
185 return len(self.collection_getter())
186
187 @gui_function
189 def create_refresh_entity(row):
190 @model_function
191 def refresh_entity():
192 o = self._get_object(row)
193 elixir.session.refresh(o)
194 self.rsh.sendEntityUpdate(self, o)
195 return row, o
196
197 return refresh_entity
198
199 def refresh(row_and_entity):
200 row, entity = row_and_entity
201 self.handleRowUpdate(row)
202 self.rsh.sendEntityUpdate(self, entity)
203
204 self.mt.post(create_refresh_entity(row), refresh)
205
207 def refresh_content(rows):
208 self.cache = {Qt.DisplayRole: fifo(10*self.max_number_of_rows),
209 Qt.EditRole: fifo(10*self.max_number_of_rows)}
210 self.setRowCount(rows)
211
212 self.mt.post(self.getRowCount, refresh_content)
213
215 self.collection_getter = collection_getter
216 self.refresh()
217
219 """Handles the update of a row when this row might be out of date"""
220 self.cache[Qt.DisplayRole].delete_by_row(row)
221 self.cache[Qt.EditRole].delete_by_row(row)
222 sig = 'dataChanged(const QModelIndex &, const QModelIndex &)'
223 self.emit(QtCore.SIGNAL(sig),
224 self.index(row, 0),
225 self.index(row, self.column_count))
226
228 """Handles the entity signal, indicating that the model is out of date"""
229 logger.debug('%s %s received entity update signal' % \
230 (self.__class__.__name__, self.admin.getName()))
231 if sender != self:
232 row = self.cache[Qt.DisplayRole].delete_by_entity(entity)
233 row = self.cache[Qt.EditRole].delete_by_entity(entity)
234 if row:
235 logger.debug('updated row %i' % row)
236 sig = 'dataChanged(const QModelIndex &, const QModelIndex &)'
237 self.emit(QtCore.SIGNAL(sig),
238 self.index(row, 0),
239 self.index(row, self.column_count))
240 else:
241 logger.debug('entity not in cache')
242 else:
243 logger.debug('duplicate update')
244
246 """Handles the entity signal, indicating that the model is out of date"""
247 logger.debug('received entity delete signal')
248 if sender != self:
249 self.refresh()
250
252 """Handles the entity signal, indicating that the model is out of date"""
253 logger.debug('received entity create signal')
254 if sender != self:
255 self.refresh()
256
258 """Callback method to set the number of rows
259 @param rows the new number of rows
260 """
261 self.rows = rows
262 self.emit(QtCore.SIGNAL('layoutChanged()'))
263
265 logger.debug('getItemDelegate')
266 if not self.item_delegate:
267 raise Exception('item delegate not yet available')
268 return self.item_delegate
269
272
274 """Callback method to set the columns
275
276 @param columns a list with fields to be displayed
277 """
278
279 self.column_count = len(columns)
280 self._columns = columns
281
282 self.item_delegate = delegates.GenericDelegate()
283 self.item_delegate.set_columns_desc(columns)
284
285 for i, c in enumerate(columns):
286
287 field_name = c[0]
288 type_ = c[1]['python_type']
289 widget_ = c[1]['widget']
290
291 if verbose:
292 logger.debug("creating delegate for %s \ntype: %s\nwidget: %s\n" \
293 "arguments: %s" % (field_name, type_, widget_, str(c[1])))
294 else:
295 logger.debug('creating delegate for %s' % field_name)
296
297
298 if 'delegate' in c[1]:
299 delegate = c[1]['delegate'](parent=None, **c[1])
300 self.item_delegate.insertColumnDelegate(i, delegate)
301 continue
302 if 'choices' in c[1]:
303 delegate = delegates.ComboBoxColumnDelegate(**c[1])
304 self.item_delegate.insertColumnDelegate(i, delegate)
305 continue
306 if widget_ == 'code':
307 delegate = delegates.CodeColumnDelegate(c[1]['parts'])
308 self.item_delegate.insertColumnDelegate(i, delegate)
309 continue
310 elif widget_ == 'datetime':
311 delegate = delegates.DateTimeColumnDelegate(parent=None, **c[1])
312 self.item_delegate.insertColumnDelegate(i, delegate)
313 elif widget_ == 'virtual_address':
314 delegate = delegates.VirtualAddressColumnDelegate()
315 self.item_delegate.insertColumnDelegate(i, delegate)
316 continue
317 elif widget_ == 'image':
318 delegate = delegates. ImageColumnDelegate()
319 self.item_delegate.insertColumnDelegate(i, delegate)
320 continue
321 elif widget_ == 'many2one':
322 entity_admin = c[1]['admin']
323 delegate = delegates.Many2OneColumnDelegate(**c[1])
324 self.item_delegate.insertColumnDelegate(i, delegate)
325 elif widget_ == 'one2many':
326 delegate = delegates.One2ManyColumnDelegate(**c[1])
327 self.item_delegate.insertColumnDelegate(i, delegate)
328 elif type_ == str:
329 if c[1]['length']:
330 delegate = delegates.PlainTextColumnDelegate(maxlength=c[1]['length'])
331 self.item_delegate.insertColumnDelegate(i, delegate)
332 else:
333 delegate = delegates.RichTextColumnDelegate(**c[1])
334 self.item_delegate.insertColumnDelegate(i, delegate)
335 elif type_ == int:
336 delegate = delegates.IntegerColumnDelegate(0, 100000)
337 self.item_delegate.insertColumnDelegate(i, delegate)
338 elif type_ == datetime.date:
339 delegate = delegates.DateColumnDelegate(format='dd/MM/yyyy',
340 default=c[1].get('default', None),
341 nullable=c[1].get('nullable', False))
342 self.item_delegate.insertColumnDelegate(i, delegate)
343 elif widget_ == 'time':
344 delegate = delegates.TimeColumnDelegate(format=c[1].get('format'),
345 default=c[1].get('default', None),
346 nullable=c[1].get('nullable', False))
347 self.item_delegate.insertColumnDelegate(i, delegate)
348 elif type_ == float:
349 delegate = delegates.FloatColumnDelegate(-100000.0, 100000.0, **c[1])
350 self.item_delegate.insertColumnDelegate(i, delegate)
351 elif type_ == bool:
352 delegate = delegates.BoolColumnDelegate()
353 self.item_delegate.insertColumnDelegate(i, delegate)
354 else:
355 delegate = delegates.PlainTextColumnDelegate()
356 self.item_delegate.insertColumnDelegate(i, delegate)
357 self.emit(QtCore.SIGNAL('layoutChanged()'))
358
361
363 return self.column_count
364
365 @gui_function
367 """In case the columns have not been set yet, don't even try to get
368 information out of them
369 """
370 if (orientation == Qt.Horizontal) and (section >= self.column_count):
371 return QtCore.QAbstractTableModel.headerData(self, section,
372 orientation, role)
373 if role == Qt.DisplayRole:
374 if orientation == Qt.Horizontal:
375 return QtCore.QVariant(self._columns[section][1]['name'])
376 elif orientation == Qt.Vertical:
377
378
379 return QtCore.QVariant()
380 if role == Qt.FontRole:
381 if orientation == Qt.Horizontal:
382 font = QtGui.QApplication.font()
383 if ('nullable' in self._columns[section][1]) and \
384 (self._columns[section][1]['nullable']==False):
385 font.setBold(True)
386 return QtCore.QVariant(font)
387 else:
388 font.setBold(False)
389 return QtCore.QVariant(font)
390
391
392
393 if role == Qt.DecorationRole:
394 if orientation == Qt.Vertical:
395 return self.form_icon
396 return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)
397
398 @gui_function
399 - def data(self, index, role):
400 import datetime
401 if not index.isValid() or \
402 not (0 <= index.row() <= self.rowCount(index)) or \
403 not (0 <= index.column() <= self.columnCount(index)):
404 return QtCore.QVariant()
405 if role in (Qt.DisplayRole, Qt.EditRole):
406 data = self._get_row_data(index.row(), role)
407 try:
408 value = data[index.column()]
409 if isinstance(value, DelayedProxy):
410 value = value()
411 data[index.column()] = value
412 except KeyError:
413 logger.error('Programming error, could not find data of column %s in %s'%(index.column(), str(data)))
414 value = None
415 return QtCore.QVariant(value)
416 elif role == Qt.SizeHintRole:
417 c = self.getColumns()[index.column()]
418 type_ = c[1]['python_type']
419 widget_ = c[1]['widget']
420 if type_ == datetime.date:
421 from camelot.view.controls.editors import DateEditor
422 editor = DateEditor()
423 return QtCore.QVariant(editor.sizeHint())
424 elif widget_ == 'one2many':
425 from camelot.view.controls.editors import One2ManyEditor
426 entity_name = c[0]
427 entity_admin = c[1]['admin']
428 editor = One2ManyEditor(entity_admin, entity_name)
429 sh = editor.sizeHint()
430 return QtCore.QVariant(sh)
431 elif widget_ == 'many2one':
432 from camelot.view.controls.editors import Many2OneEditor
433 entity_admin = c[1]['admin']
434 editor = Many2OneEditor(entity_admin)
435 sh = editor.sizeHint()
436 return QtCore.QVariant(sh)
437 elif role == Qt.ForegroundRole:
438 pass
439 elif role == Qt.BackgroundRole:
440 pass
441 return QtCore.QVariant()
442
443 - def setData(self, index, value, role=Qt.EditRole):
444 """Value should be a function taking no arguments that returns the data to
445 be set
446
447 This function will then be called in the model_thread
448 """
449 if role == Qt.EditRole:
450
451 flushed = (index.row() not in self.unflushed_rows)
452 self.unflushed_rows.add(index.row())
453
454 def make_update_function(row, column, value):
455
456 @model_function
457 def update_model_and_cache():
458 new_value = value()
459 if verbose:
460 logger.debug('set data for col %s;row %s to %s' % (row, column, new_value))
461 else:
462 logger.debug('set data for col %s;row %s' % (row, column))
463
464 o = self._get_object(row)
465 if not o:
466
467
468 try:
469 self.unflushed_rows.remove(row)
470 except KeyError:
471 pass
472 return
473 attribute, field_attributes = self.getColumns()[column]
474 old_value = getattr(o, attribute)
475 if new_value!=old_value and field_attributes['editable']==True:
476
477 model_updated = False
478 try:
479 setattr(o, attribute, new_value)
480 model_updated = True
481 except AttributeError:
482 logger.error("Can't set attribute %s to %s"%(attribute, str(value)))
483 except TypeError:
484
485 pass
486
487 row_data = RowDataFromObject(o, self.getColumns())
488 self.cache[Qt.EditRole].add_data(row, o, row_data)
489 self.cache[Qt.DisplayRole].add_data(row, o, RowDataAsUnicode(row_data))
490 if self.flush_changes and self.validator.isValid(row):
491
492 elixir.session.flush([o])
493 try:
494 self.unflushed_rows.remove(row)
495 except KeyError:
496 pass
497 if model_updated:
498
499
500
501 if not 'Imag' in old_value.__class__.__name__:
502 from camelot.model.memento import BeforeUpdate
503 from camelot.model.authentication import getCurrentPerson
504 history = BeforeUpdate(model=unicode(self.admin.entity.__name__),
505 primary_key=o.id,
506 previous_attributes={attribute:old_value},
507 person = getCurrentPerson())
508 elixir.session.flush([history])
509
510 self.rsh.sendEntityUpdate(self, o)
511 return ((row,0), (row,len(self.getColumns())))
512 elif flushed:
513 try:
514 self.unflushed_rows.remove(row)
515 except KeyError:
516 pass
517
518 return update_model_and_cache
519
520 def emit_changes(region):
521 if region:
522 self.emit(QtCore.SIGNAL('dataChanged(const QModelIndex &, const QModelIndex &)'),
523 self.index(region[0][0],region[0][1]), self.index(region[1][0],region[1][1]))
524
525 self.mt.post(make_update_function(index.row(), index.column(), value), emit_changes)
526
527 return True
528
530 flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
531 if self.getColumns()[index.column()][1]['editable']:
532 flags = flags | Qt.ItemIsEditable
533 return flags
534
535 @model_function
537 """Extend the cache around row"""
538
539 columns = self.getColumns()
540 offset = min(offset, self.rows)
541 limit = min(limit, self.rows-offset)
542 for i,o in enumerate(self.collection_getter()[offset:offset+limit+1]):
543 row_data = RowDataFromObject(o, columns)
544 self.cache[Qt.EditRole].add_data(i+offset, o, row_data)
545 self.cache[Qt.DisplayRole].add_data(i+offset, o, RowDataAsUnicode(row_data))
546 return (offset, limit)
547
548 @model_function
550 """Get the object corresponding to row"""
551 try:
552
553
554 return self.cache[Qt.EditRole].get_entity_at_row(row)
555 except KeyError:
556 pass
557 return self.collection_getter()[row]
558
560 self.rows_under_request.difference_update(set(range(offset, offset+limit)))
561 self.emit(QtCore.SIGNAL('dataChanged(const QModelIndex &, const QModelIndex &)'),
562 self.index(offset,0), self.index(offset+limit,self.column_count))
563
565 """Get the data which is to be visualized at a certain row of the
566 table, if needed, post a refill request the cache to get the object
567 and its neighbours in the cache, meanwhile, return an empty object
568 @param role: Qt.EditRole or Qt.DisplayRole
569 """
570 role_cache = self.cache[role]
571 try:
572 return role_cache.get_data_at_row(row)
573 except KeyError:
574 if row not in self.rows_under_request:
575 offset = max(row-self.max_number_of_rows/2,0)
576 limit = self.max_number_of_rows
577 self.rows_under_request.update(set(range(offset, offset+limit)))
578 self.mt.post(lambda :self._extend_cache(offset, limit),
579 lambda interval:self._cache_extended(*interval))
580 return empty_row_data
581
582 @model_function
584 self.collection_getter().remove(o)
585 self.rows -= 1
586
587 @model_function
589 self.collection_getter().append(o)
590 self.rows += 1
591
592 @model_function
614
615 @gui_function
624
625 return delete_function
626
627 self.mt.post(create_delete_function(row))
628 return True
629
630 @model_function
649
650 @gui_function
651 - def insertRow(self, row, entity_instance_getter):
652
653 def create_insert_function(getter):
654
655 @model_function
656 def insert_function():
657 self.insertEntityInstance(row, getter())
658
659 return insert_function
660
661 self.mt.post(create_insert_function(entity_instance_getter))
662
664 logger.warn('delete CollectionProxy')
665