class MapperMixin(object): ''' Mixin class for use in a dialog or widget, and manages attribute mapping. ''' def __init__(self, model): ''' :param model: Callable (new instances) or instance (existing instance for updating) of STDM model. ''' if callable(model): self._model = model() self._mode = SAVE else: self._model = model self._mode = UPDATE self._attrMappers = [] self._attr_mapper_collection = {} self._dirtyTracker = ControlDirtyTrackerCollection() self._notifBar = None self.is_valid = False #Initialize notification bar if hasattr(self, "vlNotification"): self._notifBar = NotificationBar(self.vlNotification) #Flag to indicate whether to close the widget or dialog once model has been submitted #self.closeOnSubmit = True def addMapping(self, attributeName, control, isMandatory=False, pseudoname='', valueHandler=None, preloadfunc=None): ''' Specify the mapping configuration. ''' attrMapper = _AttributeMapper(attributeName, control, self._model, pseudoname, isMandatory, valueHandler) self.addMapper(attrMapper, preloadfunc) def addMapper(self, attributeMapper, preloadfunc=None): ''' Add an attributeMapper object to the collection. Preloadfunc specifies a function that can be used to prepopulate the control's value only when the control is on SAVE mode. ''' if self._mode == SAVE and preloadfunc != None: attributeMapper.valueHandler().setValue(preloadfunc) if self._mode == UPDATE: #Set control value based on the model attribute value attributeMapper.bindControl() #Add control to dirty tracker collection after control value has been set self._dirtyTracker.addControl(attributeMapper.control(), attributeMapper.valueHandler()) self._attrMappers.append(attributeMapper) self._attr_mapper_collection[ attributeMapper.attributeName()] = attributeMapper def saveMode(self): ''' Return the mode that the mapper is currently configured in. ''' return self._mode def attribute_mapper(self, attribute_name): """ Returns attribute mapper object corresponding to the the given attribute. :param attribute_name: Name of the attribute :type attribute_name: str :return: Attribute mapper :rtype: _AttributeMapper """ return self._attr_mapper_collection.get(attribute_name, None) def setSaveMode(self, mode): ''' Set the mapper's save mode. ''' self._mode = mode def setModel(self, stdmModel): ''' Set the model to be used by the mapper. ''' self._model = stdmModel def model(self): ''' Returns the model configured for the mapper. ''' return self._model def setNotificationLayout(self, layout): ''' Set the vertical layout instance that will be used to display notification messages. ''' self._notifBar = NotificationBar(layout) def insertNotification(self, message, mtype): ''' There has to be a vertical layout, named 'vlNotification', that enables for notifications to be inserted. ''' if self._notifBar: self._notifBar.insertNotification(message, mtype) def clearNotifications(self): ''' Clears all messages in the notification bar. ''' if self._notifBar: self._notifBar.clear() def checkDirty(self): ''' Asserts whether the dialog contains dirty controls. ''' isDirty = False msgResponse = None if self._dirtyTracker.isDirty(): isDirty = True msg = QApplication.translate( "MappedDialog", "Would you like to save changes before closing?") msgResponse = QMessageBox.information( self, QApplication.translate("MappedDialog", "Save Changes"), msg, QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) return isDirty, msgResponse def closeEvent(self, event): ''' Raised when a request to close the window is received. Check the dirty state of input controls and prompt user to save if dirty. ''' isDirty, userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: # We need to ignore the event so that validation and # saving operations can be executed event.ignore() self.submit() elif userResponse == QMessageBox.No: event.accept() elif userResponse == QMessageBox.Cancel: event.ignore() else: event.accept() def cancel(self): ''' Slot for closing the dialog. Checks the dirty state first before closing. ''' isDirty, userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: self.submit() elif userResponse == QMessageBox.No: self.reject() elif userResponse == QMessageBox.Cancel: pass else: self.reject() def preSaveUpdate(self): ''' Mixin classes can override this method to specify any operations that need to be executed prior to saving or updating the model's values. It should return True prior to saving. ''' return True def postSaveUpdate(self, dbmodel): ''' Executed once a record has been saved or updated. ''' pass def submit(self, collect_model=False, save_and_new=False): """ Slot for saving or updating the model. This will close the dialog on successful submission. :param collect_model: If set to True only returns the model without saving it to the database. :type collect_model: Boolean :param save_and_new: A Boolean indicating it is triggered by save and new button. :type save_and_new: Boolean """ if not self.preSaveUpdate(): return self.clearNotifications() self.is_valid = True # Validate mandatory fields have been entered by the user. for attrMapper in self._attrMappers: if attrMapper.isMandatory() and \ attrMapper.valueHandler().supportsMandatory(): if attrMapper.valueHandler().value() == \ attrMapper.valueHandler().default(): #Notify user msg = QApplication.translate("MappedDialog", "'%s' is a required field.")\ %unicode(attrMapper.pseudoName()) self._notifBar.insertWarningNotification(msg) self.is_valid = False if not self.is_valid: return # Bind model once all attributes are valid for attrMapper in self._attrMappers: attrMapper.set_model(self.model()) attrMapper.bindModel() if not collect_model: self._persistModel(save_and_new) def _persistModel(self, save_and_new): """ Saves the model to the database and shows a success message. :param save_and_new: A Boolean indicating it is triggered by save and new button. :type save_and_new: Boolean """ try: # Persist the model to its corresponding store. if self._mode == SAVE: self._model.save() if not save_and_new: QMessageBox.information( self, QApplication.translate("MappedDialog", "Record Saved"), QApplication.translate( "MappedDialog", "New record has been successfully saved.")) else: self._model.update() QMessageBox.information( self, QApplication.translate("MappedDialog", "Record Updated"), QApplication.translate( "MappedDialog", "Record has been successfully updated.")) except Exception as ex: QMessageBox.critical( self, QApplication.translate("MappedDialog", "Data Operation Error"), QApplication.translate( "MappedDialog", u'The data could not be saved due to ' u'the error: \n{}'.format(ex.args[0]))) self.is_valid = False # Close the dialog if isinstance(self, QDialog) and self.is_valid: self.postSaveUpdate(self._model) if not save_and_new: self.accept() def clear(self): """ Clears the form values. """ for attrMapper in self._attrMappers: attrMapper.valueHandler().clear()
class MapperMixin(object): ''' Mixin class for use in a dialog or widget, and manages attribute mapping. ''' def __init__(self, model, entity): ''' :param model: Callable (new instances) or instance (existing instance for updating) of STDM model. ''' if callable(model): self._model = model() self._mode = SAVE else: self._model = model self._mode = UPDATE self.entity = entity self._attrMappers = [] self._attr_mapper_collection = {} self._dirtyTracker = ControlDirtyTrackerCollection() self._notifBar = None self.is_valid = False self.saved_model = None # Get document objects self.entity_model = entity_model(entity) self.entity_model_obj = self.entity_model() #Initialize notification bar if hasattr(self, "vlNotification"): self._notifBar = NotificationBar(self.vlNotification) #Flag to indicate whether to close the widget or dialog once model has been submitted #self.closeOnSubmit = True def addMapping(self, attributeName, control, isMandatory=False, pseudoname='', valueHandler=None, preloadfunc=None): ''' Specify the mapping configuration. ''' attrMapper = _AttributeMapper(attributeName, control, self._model, pseudoname, isMandatory, valueHandler) self.addMapper(attrMapper, preloadfunc) def addMapper(self, attributeMapper, preloadfunc=None): ''' Add an attributeMapper object to the collection. Preloadfunc specifies a function that can be used to prepopulate the control's value only when the control is on SAVE mode. ''' if self._mode == SAVE and preloadfunc != None: attributeMapper.valueHandler().setValue(preloadfunc) if self._mode == UPDATE: #Set control value based on the model attribute value attributeMapper.bindControl() #Add control to dirty tracker collection after control value has been set self._dirtyTracker.addControl(attributeMapper.control(), attributeMapper.valueHandler()) self._attrMappers.append(attributeMapper) self._attr_mapper_collection[ attributeMapper.attributeName()] = attributeMapper def saveMode(self): ''' Return the mode that the mapper is currently configured in. ''' return self._mode def is_update_mode(self): """ :return: Returns True if the form is in UPDATE mode, otherwise False if in SAVE mode when creating a new record. :rtype: bool """ if self._mode == UPDATE: return True return False def attribute_mapper(self, attribute_name): """ Returns attribute mapper object corresponding to the the given attribute. :param attribute_name: Name of the attribute :type attribute_name: str :return: Attribute mapper :rtype: _AttributeMapper """ return self._attr_mapper_collection.get(attribute_name, None) def setSaveMode(self, mode): ''' Set the mapper's save mode. ''' self._mode = mode def setModel(self, stdmModel): ''' Set the model to be used by the mapper. ''' self._model = stdmModel def model(self): ''' Returns the model configured for the mapper. ''' return self._model def setNotificationLayout(self, layout): ''' Set the vertical layout instance that will be used to display notification messages. ''' self._notifBar = NotificationBar(layout) def insertNotification(self, message, mtype): ''' There has to be a vertical layout, named 'vlNotification', that enables for notifications to be inserted. ''' if self._notifBar: self._notifBar.insertNotification(message, mtype) def clearNotifications(self): ''' Clears all messages in the notification bar. ''' if self._notifBar: self._notifBar.clear() def checkDirty(self): ''' Asserts whether the dialog contains dirty controls. ''' isDirty = False msgResponse = None if self._dirtyTracker.isDirty(): isDirty = True msg = QApplication.translate( "MappedDialog", "Would you like to save changes before closing?") msgResponse = QMessageBox.information( self, QApplication.translate("MappedDialog", "Save Changes"), msg, QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) return isDirty, msgResponse def cancel(self): ''' Slot for closing the dialog. Checks the dirty state first before closing. ''' isDirty, userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: self.submit() elif userResponse == QMessageBox.No: self.reject() elif userResponse == QMessageBox.Cancel: pass else: self.reject() def preSaveUpdate(self): ''' Mixin classes can override this method to specify any operations that need to be executed prior to saving or updating the model's values. It should return True prior to saving. ''' return True def postSaveUpdate(self, dbmodel): ''' Executed once a record has been saved or updated. ''' self.saved_model = dbmodel self._post_save(self.saved_model) def _post_save(self, model): """ Enables sub-classes to incorporate additional logic after form data has been saved. """ pass def validate_all(self): """ Validate the entire form. :return: :rtype: """ errors = [] for attrMapper in self._attrMappers: error = self.validate(attrMapper) if error is not None: self._notifBar.insertWarningNotification(error) errors.append(error) return errors def validate(self, attrMapper, update=False): """ Validate attribute. :param attrMapper: The attribute :type attrMapper: _AttributeMapper :param update: Whether the validation is on update or new entry :type update: Boolean :return: List of error messages or None :rtype: list or NoneType """ error = None field = attrMapper.pseudoName() column_name = attrMapper.attributeName() if column_name in self.entity.columns.keys(): column = self.entity.columns[column_name] else: return if column.unique: column_obj = getattr(self.entity_model, column_name, None) if not update: result = self.entity_model_obj.queryObject().filter( column_obj == attrMapper.valueHandler().value()).first() else: id_obj = getattr(self.entity_model, 'id', None) result = self.entity_model_obj.queryObject().filter( column_obj == attrMapper.valueHandler().value()).filter( id_obj != self.model().id).first() if result is not None: msg = QApplication.translate("MappedDialog", "field value should be unique.") error = u'{} {}'.format(field, msg) if column.mandatory: if attrMapper.valueHandler().value() == \ attrMapper.valueHandler().default(): # Notify user msg = QApplication.translate("MappedDialog", "is a required field.") error = u'{} {}'.format(field, msg) return error def _custom_validate(self): # Sub-classes can implement custom validation logic. return True def submit(self, collect_model=False, save_and_new=False): """ Slot for saving or updating the model. This will close the dialog on successful submission. :param collect_model: If set to True only returns the model without saving it to the database. :type collect_model: Boolean :param save_and_new: A Boolean indicating it is triggered by save and new button. :type save_and_new: Boolean """ if not self.preSaveUpdate(): return self.clearNotifications() self.is_valid = True # Validate mandatory fields have been entered by the user. errors = [] for attrMapper in self._attrMappers: if self._mode == 'SAVE': error = self.validate(attrMapper) else: # update mode error = self.validate(attrMapper, True) if error is not None: self._notifBar.insertWarningNotification(error) errors.append(error) if len(errors) > 0 or not self._custom_validate(): self.is_valid = False if not self.is_valid: return # Bind model once all attributes are valid for attrMapper in self._attrMappers: control = attrMapper.valueHandler().control if isinstance(control, AutoGeneratedLineEdit): if control.column.prefix_source == control.column.columns_name: if attrMapper.valueHandler().value() is None: control.on_load_foreign_key_browser() attrMapper.set_model(self.model()) attrMapper.bindModel() if not collect_model: self._persistModel(save_and_new) def _persistModel(self, save_and_new): """ Saves the model to the database and shows a success message. :param save_and_new: A Boolean indicating it is triggered by save and new button. :type save_and_new: Boolean """ try: # Persist the model to its corresponding store. if self._mode == SAVE: self._model.save() if not save_and_new: QMessageBox.information( self, QApplication.translate("MappedDialog", "Record Saved"), QApplication.translate( "MappedDialog", "New record has been successfully saved.")) else: self._model.update() QMessageBox.information( self, QApplication.translate("MappedDialog", "Record Updated"), QApplication.translate( "MappedDialog", "Record has been successfully updated.")) STDMDb.instance().session.flush() for attrMapper in self._attrMappers: control = attrMapper.valueHandler().control if isinstance(control, ExpressionLineEdit): value = control.on_expression_triggered() print attrMapper._attrName, value setattr(self.model(), attrMapper._attrName, value) self._model.update() # STDMDb.instance().session.flush() except Exception as ex: QMessageBox.critical( self, QApplication.translate("MappedDialog", "Data Operation Error"), QApplication.translate( "MappedDialog", u'The data could not be saved due to ' u'the error: \n{}'.format(ex.args[0]))) self.is_valid = False # Close the dialog if isinstance(self, QDialog) and self.is_valid: self.postSaveUpdate(self._model) if not save_and_new: self.accept() def clear(self): """ Clears the form values. """ for attrMapper in self._attrMappers: attrMapper.valueHandler().clear()
class MapperMixin(object): ''' Mixin class for use in a dialog or widget, and does the heavy lifting when it comes to managing attribute mapping. ''' def __init__(self,model): ''' :param model: Callable (new instances) or instance (existing instance for updating) of STDM model. ''' if callable(model): self._model = model() self._mode = SAVE else: self._model = model self._mode = UPDATE self._attrMappers = [] self._dirtyTracker = ControlDirtyTrackerCollection() self._notifBar = None #Initialize notification bar if hasattr(self,"vlNotification"): self._notifBar = NotificationBar(self.vlNotification) #Flag to indicate whether to close the widget or dialog once model has been submitted #self.closeOnSubmit = True def addMapping(self,attributeName,control,isMandatory = False,pseudoname = "",valueHandler = None,preloadfunc = None): ''' Specify the mapping configuration. ''' attrMapper = _AttributeMapper(attributeName,control,self._model,pseudoname,isMandatory,valueHandler) self.addMapper(attrMapper,preloadfunc) def addMapper(self,attributeMapper,preloadfunc = None): ''' Add an attributeMapper object to the collection. Preloadfunc specifies a function that can be used to prepopulate the control's value only when the control is on SAVE mode. ''' if self._mode == SAVE and preloadfunc != None: attributeMapper.valueHandler().setValue(preloadfunc) if self._mode == UPDATE: #Set control value based on the model attribute value attributeMapper.bindControl() #Add control to dirty tracker collection after control value has been set self._dirtyTracker.addControl(attributeMapper.control(), attributeMapper.valueHandler()) self._attrMappers.append(attributeMapper) def saveMode(self): ''' Return the mode that the mapper is currently configured in. ''' return self._mode def setSaveMode(self,mode): ''' Set the mapper's save mode. ''' self._mode = mode def setModel(self,stdmModel): ''' Set the model to be used by the mapper. ''' self._model = stdmModel def model(self): ''' Returns the model configured for the mapper. ''' return self._model def setNotificationLayout(self,layout): ''' Set the vertical layout instance that will be used to display notification messages. ''' self._notifBar = NotificationBar(layout) def insertNotification(self,message,mtype): ''' There has to be a vertical layout, named 'vlNotification', that enables for notifications to be inserted. ''' if self._notifBar: self._notifBar.insertNotification(message, mtype) def clearNotifications(self): ''' Clears all messages in the notification bar. ''' if self._notifBar: self._notifBar.clear() def checkDirty(self): ''' Asserts whether the dialog contains dirty controls. ''' isDirty = False msgResponse = None if self._dirtyTracker.isDirty(): isDirty = True msg = QApplication.translate("MappedDialog","Would you like to save changes before closing?") msgResponse = QMessageBox.information(self, QApplication.translate("MappedDialog","Save Changes"), msg, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel) return isDirty,msgResponse def closeEvent(self,event): ''' Raised when a request to close the window is received. Check the dirty state of input controls and prompt user to save if dirty. ''' isDirty,userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: #We need to ignore the event so that validation and saving operations can be executed event.ignore() self.submit() elif userResponse == QMessageBox.No: event.accept() elif userResponse == QMessageBox.Cancel: event.ignore() else: event.accept() def cancel(self): ''' Slot for closing the dialog. Checks the dirty state first before closing. ''' isDirty,userResponse = self.checkDirty() if isDirty: if userResponse == QMessageBox.Yes: self.submit() elif userResponse == QMessageBox.No: self.reject() elif userResponse == QMessageBox.Cancel: pass else: self.reject() def preSaveUpdate(self): ''' Mixin classes can override this method to specify any operations that need to be executed prior to saving or updating the model's values. It should return True prior to saving. ''' return True def postSaveUpdate(self,dbmodel): ''' Executed once a record has been saved or updated. ''' pass def submit(self): ''' Slot for saving or updating the model. This will close the dialog on successful submission. ''' if not self.preSaveUpdate(): return self.clearNotifications() isValid= True #Validate mandatory fields have been entered by the user. for attrMapper in self._attrMappers: if attrMapper.isMandatory() and attrMapper.valueHandler().supportsMandatory(): if attrMapper.valueHandler().value() == attrMapper.valueHandler().default(): #Notify user msg = "{0} is a required field.".format(attrMapper.pseudoName()) self._notifBar.insertWarningNotification(msg) isValid = False else: attrMapper.bindModel() else: attrMapper.bindModel() if not isValid: return self._persistModel() def _persistModel(self): #Persist the model to its corresponding store. if self._mode == SAVE: self._model.save() QMessageBox.information(self, QApplication.translate("MappedDialog","Record Saved"), \ QApplication.translate("MappedDialog","New record has been successfully saved")) else: self._model.update() QMessageBox.information(self, QApplication.translate("MappedDialog","Record Updated"), \ QApplication.translate("MappedDialog","Record has been successfully updated")) #Close the dialog if isinstance(self, QDialog): self.postSaveUpdate(self._model) self.accept()