def sort_columns(self): self.columns = RenameableKeyDict( sorted(self.columns.iteritems(), key=lambda e: e[1].row_index))
def __init__(self, name, profile, create_id_column=True, supports_documents=True, is_global=False, is_proxy=False): """ :param name: Name of the entity. The profile prefix will be appended to this name. :type name: str :param is_global: True if the entity should be applied across all profiles. :type is_global: bool :param profile: Profile that the entity will belong to. :type profile: Profile :param create_id_column: True to append an id serial column which will also be the table's primary key. This will always be True (bug). :type create_id_column: bool :param supports_documents: True if supporting documents can be attached to this entity. :type supports_documents: bool :param is_proxy: Table will not be created in the database as there is an existing table created by another process. This is useful when defining ForeignKey references to tables created outside of the configuration process. :type is_proxy: bool """ QObject.__init__(self, profile) self.profile = profile self.is_global = is_global self.short_name = name name = self._shortname_to_name(name) TableItem.__init__(self, name) self.description = '' self.is_associative = False self.user_editable = True self.columns = RenameableKeyDict() self.updated_columns = OrderedDict() # Added in version 1.7 self.label = '' ''' We will always create an ID column due to a bug in SQLAlchemy-migrate as setting the 'id' column as a primary key does not create a serial column type in the database. ''' self.create_id_column = True # Create primary key if self.create_id_column: LOGGER.debug('Creating primary key for %s entity.', self.name) self._create_serial_column() self.supporting_doc = None self._supports_documents = supports_documents if self._supports_documents: self.supports_documents = True self.is_proxy = is_proxy # Sync this with row index of the viewer self.row_index = -1 self.entity_in_database = False LOGGER.debug('%s entity created.', self.name)
class Entity(QObject, TableItem): """ A wrapper for a database table object. """ TYPE_INFO = 'ENTITY' sql_updater = entity_updater column_added = pyqtSignal(BaseColumn) column_removed = pyqtSignal(unicode) def __init__(self, name, profile, create_id_column=True, supports_documents=True, is_global=False, is_proxy=False): """ :param name: Name of the entity. The profile prefix will be appended to this name. :type name: str :param is_global: True if the entity should be applied across all profiles. :type is_global: bool :param profile: Profile that the entity will belong to. :type profile: Profile :param create_id_column: True to append an id serial column which will also be the table's primary key. This will always be True (bug). :type create_id_column: bool :param supports_documents: True if supporting documents can be attached to this entity. :type supports_documents: bool :param is_proxy: Table will not be created in the database as there is an existing table created by another process. This is useful when defining ForeignKey references to tables created outside of the configuration process. :type is_proxy: bool """ QObject.__init__(self, profile) self.profile = profile self.is_global = is_global self.short_name = name name = self._shortname_to_name(name) TableItem.__init__(self, name) self.description = '' self.is_associative = False self.user_editable = True self.columns = RenameableKeyDict() self.updated_columns = OrderedDict() # Added in version 1.7 self.label = '' ''' We will always create an ID column due to a bug in SQLAlchemy-migrate as setting the 'id' column as a primary key does not create a serial column type in the database. ''' self.create_id_column = True # Create primary key if self.create_id_column: LOGGER.debug('Creating primary key for %s entity.', self.name) self._create_serial_column() self.supporting_doc = None self._supports_documents = supports_documents if self._supports_documents: self.supports_documents = True self.is_proxy = is_proxy # Sync this with row index of the viewer self.row_index = -1 self.entity_in_database = False LOGGER.debug('%s entity created.', self.name) def _shortname_to_name(self, name): # Append profile prefix if not global if not self.is_global: # format the internal name, replace spaces between words # with underscore and make all letters lower case. name = unicode(name).strip() name = name.replace(' ', "_") name = name.lower() # Ensure prefix is not duplicated in the names prfx = self.profile.prefix prefix_idx = name.find(u'{}_'.format(prfx), 0, len(prfx) + 1) # If there is no prefix then append if prefix_idx == -1 and not self.is_global: name = u'{0}_{1}'.format(self.profile.prefix, name) return name def rename(self, shortname): """ Updates the entity short name and name to use the new name. This function is used by the profile object and should not be used directly. To rename an entity, please use :py:class:Profile.rename(original_name, new_name) function. :param shortname: New shortname. The table name will also be derived from this as well. :type shortname: str """ self.short_name = shortname self.name = self._shortname_to_name(shortname) # Rename supporting documents if enabled if self.supports_documents: self.supporting_doc.rename(shortname) @property def supports_documents(self): return self._supports_documents @supports_documents.setter def supports_documents(self, value): # Enable the attachment of supporting documents if flag is specified self._supports_documents = value if value: self.supporting_doc = EntitySupportingDocument(self.profile, self) self.profile.add_entity(self.supporting_doc) def _create_serial_column(self): """ Creates a serial column which becomes the primary key of the table. """ sc = SerialColumn('id', self) self.add_column(sc) def ui_display(self): """ :return: Returns a more friendly name for the entity as this will allow non-ASCII characters to be used to describe the entity in a user-interface context. The 'label' attribute is given preference, otherwise the short_name is used. :rtype: str """ return self.label or self.short_name.replace('_', ' ').title() def add_column(self, column): """ Add a column object to the collection. :param column: Column object. :type column: BaseColumn """ self.columns[column.name] = column self.column_added.emit(column) LOGGER.debug('%s column added to %s entity.', column.name, self.name) self.append_updated_column(column) def column(self, name): """ :param name: Column name. :type name: str :returns: Returns a column object with the given name from the collection, else None if not found. :rtype: BaseColumn """ return self.columns.get(name, None) def rename_column(self, old_name, new_name): """ Renames the column with the given old_name to the new_name. This is applicable only if the column has not yet been created in the database. :param old_name: Existing column name to be renamed. :type old_name: str :param new_name: New column name. :type new_name: str :return: Returns True if the column was successfully renamed, else False. :rtype: bool """ if not old_name in self.columns: LOGGER.debug( '%s column not found, item does ' 'not exist in the collection.', old_name) return False col = self.column(old_name) col.rename(new_name) # Update column collection self.columns.rename(old_name, new_name) def remove_column(self, name): """ Removes a column with the given name from the collection. :param name: Column name. :type name: str :return: True if the column was successfully removed, else False. :rtype: bool """ if not name in self.columns: LOGGER.debug( '%s column not found, item does ' 'not exist in the collection.', name) return False col = self.columns.pop(name) LOGGER.debug('%s column removed from %s entity', name, self.name) col.action = DbItem.DROP # Add column to the collection of updated columns self.append_updated_column(col) self.column_removed.emit(name) return True def append_updated_column(self, col): """ Append a column to the list of column that need to be altered or dropped. :param col: Column instance :type col: BaseColumn """ if col.name in self.updated_columns: del self.updated_columns[col.name] self.updated_columns[col.name] = col #Update action if self.action == DbItem.NONE: self.action = DbItem.ALTER def _constructor_args(self): """ :return: Returns a collection of the constructor keys and corresponding values for cloning the this object. """ constructor_args = {} constructor_args['create_id_column'] = self.create_id_column constructor_args['supports_documents'] = self.supports_documents constructor_args['is_global'] = self.is_global constructor_args['is_proxy'] = self.is_proxy return constructor_args def _copy_columns(self, entity): """ Copies the columns in the current object into the specified entity. :param entity: Target entity of the columns to be copied. :type entity: Entity """ for c in self.columns.values(): new_col = c.clone() entity.add_column(new_col) def _copy_attrs(self, entity): """ Copies the attributes of the current object into the given entity object. :param entity: Target of the copied entity attributes. :type entity: Entity """ entity.description = self.description entity.is_associative = self.is_associative entity.user_editable = self.user_editable #Copy columns #self._copy_columns(entity) def on_delete(self): """ Specify additional action for cleaning up any additional references upon deleting this item. Default implementation does nothing. """ pass def update(self, engine, metadata): """ Update the entity in the database using the 'sql_updater' callable attribute. :param engine: SQLAlchemy engine which contains the database connection properties. :param metadata: Object containing all the schema constructs associated with our database. :type metadata: MetaData """ if self.sql_updater is None: LOGGER.debug('%s entity has no sql_updater callable class.', self.name) return self.sql_updater(engine, metadata) def parents(self): """ :return: Returns a collection of entities which this entity refers to through one or more entity relations (implemented as foreign key constraints in the database). :rtype: list """ entity_relations = self.profile.child_relations(self) return [er.parent for er in entity_relations if er.valid()[0]] def associations(self): """ :return: Returns a collection of entities which are indirectly linked to this entity through association entities. A multiple select column is an example of an object that uses association entities. :rtype: list """ assoc_entities = self.profile.entities_by_type_info( 'ASSOCIATION_ENTITY') rel_entities = [] for ase in assoc_entities: if ase.first_parent.name == self.name: rel_entities.append(ase.first_parent) elif ase.second_parent.name == self.name: rel_entities.append(ase.second_parent) return rel_entities def children(self): """ :return: Returns a collection of entities that refer to this entity as the parent through one or more entity relations (implemented as foreign key constraints in the database). :rtype: list """ entity_relations = self.profile.parent_relations(self) return [er.child for er in entity_relations if er.valid()[0]] def dependencies(self): """ Gets the tables and views that are related to the specified entity. :return: A dictionary containing a list of related entity names and views respectively. :rtype: dict """ parents = self.parents() children = self.children() all_relations = parents + children #Dependent entity names dep_ent = [e.short_name for e in all_relations] #Add views as well dep_views = table_view_dependencies(self.name) return {'entities': dep_ent, 'views': dep_views} def column_children_relations(self, name): """ :param name: Column name :type name: str :return: Returns a list of entity relations which reference the column with the given name as the child column in the entity relation. :rtype: list """ entity_relations = self.profile.child_relations(self) return [er for er in entity_relations if er.child_column == name] def column_parent_relations(self, name): """ :param name: Column name :type name: str :return: Returns a list of entity relations which reference the column with the given name as the parent column in the entity relation. These are basically entity relations in foreign key columns. :rtype: list """ entity_relations = self.profile.parent_relations(self) return [er for er in entity_relations if er.parent_column == name] def columns_by_type_info(self, type_info): """ :param type_info: Column TYPE_INFO :type type_info: str :returns: A list of columns based on the specified TYPE_INFO e.g. VARCHAR, DOUBLE etc. :rtype: Entity """ return [c for c in self.columns.values() if c.TYPE_INFO == type_info] def geometry_columns(self): """ :return: A list of Geometry-type columns. :rtype: list """ return self.columns_by_type_info(GeometryColumn.TYPE_INFO) def has_geometry_column(self): """ :return: True if the entity contains a spatial column, else False. :rtype: True """ return True if len(self.geometry_columns()) > 0 else False def document_types(self): """ :return: Returns a list of document types specified for this entity. An AttributeError is raised if supporting documents are not enabled for this entity. :rtype: list """ if not self.supports_documents: raise AttributeError('Supporting documents are not enabled for ' 'this entity.') return self.supporting_doc.document_types().keys() def document_types_non_hex(self): """ :return: Returns a list of document type String specified for this entity. An AttributeError is raised if supporting documents are not enabled for this entity. :rtype: list """ if not self.supports_documents: raise AttributeError('Supporting documents are not enabled for ' 'this entity.') return self.supporting_doc.document_types_non_hex() def document_path(self): """ :return: Returns a subpath for locating supporting documents using the profile and entity names respectively. This is concatenated to the root path to locate documents for this particular entity. :rtype: str """ if not self.supports_documents: raise AttributeError('Supporting documents are not enabled for ' 'this entity.') return self.supporting_doc.document_path() def virtual_columns(self): """ :return: Returns a list of derived column names such as multi-select columns, supporting document columns separated by document type etc. :rtype: list """ virtual_cols = [] if self.supports_documents: doc_types = self.document_types() for doc_type in doc_types: u_doc_type = unicode(doc_type) text_value = self.supporting_doc._doc_types_value_list.values[ u_doc_type] virtual_cols.append(text_value.value) multi_select_cols = self.columns_by_type_info( MultipleSelectColumn.TYPE_INFO) # Get column names multi_select_cols = [c.name for c in multi_select_cols] virtual_cols.extend(multi_select_cols) return virtual_cols def __eq__(self, other): """ Compares entity using the name. :param other: Entity object. :type other: Entity :return: True if the entity is equal, else False. :rtype: bool """ if other.name != self.name: return False return True # Added in 1.7 def update_column_row_index(self, name, index): if name in self.columns: self.columns[name].row_index = index def sort_columns(self): self.columns = RenameableKeyDict( sorted(self.columns.iteritems(), key=lambda e: e[1].row_index))