class AbstractDocumentModel(AbstractDocumentManagementModel): """Base document model class """ name = fields.char_field(blank=False, null=False, max_length=constants.NAME_FIELD_MAX_LENGTH) language = fields.char_field(blank=False, null=False, default=settings.LANGUAGE_CODE) # deletion of client will result in deletion of its associated documents client = fields.foreign_key_field(Client, on_delete=CASCADE) mime_type = fields.char_field(blank=False, null=False, default=constants.MIME_TYPE_UNKNOWN, choices=constants.MIME_TYPE_CHOICES) document_type = fields.char_field( blank=False, null=False, default=constants.DOCUMENT_TYPE_REFERENCE, choices=constants.DOCUMENT_TYPE_CHOICES) title = fields.char_field(blank=True, null=True, max_length=constants.TITLE_FIELD_MAX_LENGTH) description = fields.description_field( blank=True, null=True, max_length=constants.DESCRIPTION_FIELD_MAX_LENGTH) category = fields.foreign_key_field(Category, blank=True, null=True, on_delete=SET_NULL) tags = fields.many_to_many_field(Tag, related_name='documents', through='DocumentTag', through_fields=('document', 'tag')) annotations = fields.many_to_many_field(Annotation, related_name='documents', through='DocumentAnnotation', through_fields=('document', 'annotation')) #@ TODO: this is not a symetrical relationship -> it is unidirectional using from_document documents = fields.many_to_many_field('self', symmetrical=False, related_name='related_documents', through='DocumentAssociation', through_fields=('from_document', 'to_document')) class Meta(AbstractDocumentManagementModel.Meta): """Meta class definition""" abstract = True unique_together = ('client', 'name') def __str__(self): """pretty format instance as string""" return self.name
class DocumentAssociation(AbstractDocumentManagementModel): """Document association model class. A document may be associated with 0 or more other documents Document(1) -------> Document(0..*) """ from_document = fields.foreign_key_field(Document, related_name='from_document', on_delete=CASCADE) to_document = fields.foreign_key_field(Document, related_name='to_document', on_delete=CASCADE) client = fields.foreign_key_field(Client, on_delete=CASCADE) # relationship purpose purpose = fields.char_field( blank=False, null=False, default=constants.DOCUMENT_ASSOCIATION_PURPOSE_UNKNOWN, choices=constants.DOCUMENT_ASSOCIATION_PURPOSE_CHOICES) class Meta(AbstractDocumentManagementModel.Meta): """Model meta class definition""" db_table = db_table(app_label, _document_association) verbose_name = _(_document_association_verbose) verbose_name_plural = _(pluralize(_document_association_verbose)) unique_together = ('from_document', 'to_document', 'purpose') def __str__(self): """pretty format instance as string""" return '({},{},{})'.format(str(self.from_document), str(self.to_document), self.purpose)
class Annotation(AbstractDocumentManagementModel): """Annotation class definition Allows to annotate another model instance. """ name = fields.char_field(blank=False, null=False, max_length=constants.NAME_FIELD_MAX_LENGTH) annotation = fields.annotation_field( blank=False, null=False, max_length=constants.ANNOTATION_FIELD_MAX_LENGTH) # deletion of client will result in deletion of associated annotations client = fields.foreign_key_field(Client, on_delete=CASCADE) class Meta(AbstractDocumentManagementModel.Meta): """Meta class definition""" abstract = False db_table = db_table(app_label, _annotation) verbose_name = _(_annotation_verbose) verbose_name_plural = _(pluralize(_annotation_verbose)) unique_together = ('client', 'name') def __str__(self): """generate pretty string representation""" return self.name
class ClientUser(AbstractDocumentManagementModel): """Client user model class. Capture client user attributes. A client may be associated with 0 or more users: Client(1) ------> Annotation(0..*) A user can only be associated with a single client """ # when client or user are deleted, the associated ClientUser instance # is deleted. client = fields.foreign_key_field(Client, on_delete=CASCADE) user = fields.one_to_one_field(to_class=User, on_delete=CASCADE) phone = fields.phone_number_field(blank=True, null=True) description = fields.description_field(blank=True, null=True, max_length=constants.DESCRIPTION_FIELD_MAX_LENGTH) def has_logged_in(self): """Determine if user has ever logged in""" return self.user.last_login is not None # pylint: disable=no-member class Meta(AbstractDocumentManagementModel.Meta): """ClientUser meta class""" db_table = db_table(app_label, _client_user) verbose_name = _(_client_user_verbose) verbose_name_plural = _(pluralize(_client_user_verbose)) def __str__(self): """pretty format instance as string""" return "{},{}".format(self.client, self.user)
class AnalysisResults(AbstractResultsModel): """ Analysis results model class """ documents = fields.foreign_key_field(DocumentAssociation, on_delete=CASCADE, blank=True, null=True) class Meta(AbstractResultsModel.Meta): """Meta class definition""" db_table = db_table(app_label, _analysis_results) verbose_name = _(_analysis_results_verbose) verbose_name_plural = _(pluralize(_analysis_results_verbose)) def save(self, force_insert=False, force_update=False, using=None, update_fields=None): """save the instance """ self.full_clean() # auto assign a name if self.documents and not self.name: # pylint: disable=no-member self.name = '{}:{}'.format(self.documents.from_document.name, self.documents.to_document.name) return super(AnalysisResults, self).save(force_insert, force_update, using, update_fields)
class AbstractModel(models.Model): """An abstract base class for application models. """ id = fields.auto_field() uuid = fields.uuid_field( blank=False, null=False, ) version = fields.integer_field(blank=False, null=False) is_enabled = fields.boolean_field(blank=False, null=False, default=True) is_deleted = fields.boolean_field(blank=False, null=False, default=False) creation_time = fields.datetime_field(blank=False, null=False, auto_now_add=True) update_time = fields.datetime_field(blank=False, null=False, auto_now=True) # user who created the instance creation_user = fields.user_field(related_name=related_name_base + "creation_user") # user who triggered the instance update update_user = fields.user_field(related_name=related_name_base + "update_user") # user on whose behalf change is made effective_user = fields.user_field(related_name=related_name_base + "effective_user") site = fields.foreign_key_field(Site, related_name=related_name_base + "site") objects = BaseModelManager() class Meta: """Meta class declaration.""" abstract = True get_latest_by = "update_time" def save(self, force_insert=False, force_update=False, using=None, update_fields=None): """Save an instance. """ self.full_clean() self.version += 1 super(AbstractModel, self).save(force_insert=False, force_update=False, using=None, update_fields=None) def __str__(self): return '{0} object {1.id!s} {1.uuid!s} {1.version!s}'.format( instance_class_name(self), self)
class DocumentTag(AbstractDocumentManagementModel): """Document tag association model class. A document may be associated with 0 or more tags: Document(1) -------> Tag(0..*) """ document = fields.foreign_key_field(Document, on_delete=CASCADE) tag = fields.foreign_key_field(Tag, on_delete=CASCADE) client = fields.foreign_key_field(Client, on_delete=CASCADE) class Meta(AbstractDocumentManagementModel.Meta): """Model meta class definition""" db_table = db_table(app_label, _document_tag) verbose_name = _(_document_tag_verbose) verbose_name_plural = _(pluralize(_document_tag_verbose)) unique_together = ('document', 'tag') def __str__(self): """pretty format instance as string""" return '({},{})'.format(str(self.document), str(self.tag))
class AbstractResultsModel(AbstractAnalyticsModel): """Base results model class """ input = fields.json_field(blank=False, null=False) output = fields.json_field(blank=False, null=False) name = fields.char_field(blank=True, null=True, max_length=constants.NAME_FIELD_MAX_LENGTH) # deletion of client will result in deletion of its associated documents client = fields.foreign_key_field(Client, on_delete=CASCADE) description = fields.description_field( blank=True, null=True, max_length=constants.DESCRIPTION_FIELD_MAX_LENGTH) class Meta(AbstractAnalyticsModel.Meta): """Meta class definition""" abstract = True def __str__(self): """pretty format instance as string""" return self.name if self.name else super().__str__()
class AbstractClassification(AbstractDocumentManagementModel): """Abstract classification class definition Allows to associate a classification with an instance. """ name = fields.char_field(blank=False, null=False, max_length=constants.NAME_FIELD_MAX_LENGTH) # deletion of client will result in deletion of associated classifications client = fields.foreign_key_field(Client, on_delete=CASCADE) # deletion of parent will result in parent being set to null parent = fields.foreign_key_field('self', blank=True, null=True, related_name='children', on_delete=SET_NULL) description = fields.description_field( blank=True, null=True, max_length=constants.DESCRIPTION_FIELD_MAX_LENGTH) # the classification population (i.e. AuxiliaryDocument, Reference Document) target = fields.char_field(blank=False, null=False, default=constants.CLASSIFICATION_TARGET_REFERENCE_DOCUMENT, choices=constants.CLASSIFICATION_TARGET_CHOICES) # the industry or other top level classification such as finance, insurance, shipping, general domain = fields.char_field(blank=False, null=False, default=constants.CLASSIFICATION_DOMAIN_GENERAL, choices=constants.CLASSIFICATION_DOMAIN_CHOICES) class Meta(AbstractDocumentManagementModel.Meta): """Meta class definition""" abstract = True unique_together = ('client', 'name') def clean(self): """Model wide validation""" # pylint: disable=no-member if self.parent: if not self.parent.id: raise ValidationError(_('Invalid parent - has not been saved.')) # verify that parent exists - if not exception will be thrown self.__class__.objects.get(pk=self.parent.id) # check the entire hierarchy for name and id duplicates depth = 0 max_depth = 10 current = self name_cache = dict() id_cache = dict() while current and depth < max_depth: current_name = current.name current_id = current.id if current_name in name_cache: raise ValidationError(_(f'Duplicate name {current_name} in hierarchy.')) if current_id in id_cache: raise ValidationError(_(f'Duplicate id {current_id} in hierarchy.')) name_cache[current_name] = current id_cache[current_id] = current if current.parent: current = self.__class__.objects.get(pk=current.parent.id) depth += 1 else: current = current.parent if depth >= max_depth: raise ValidationError(_(f'Max depth of {max_depth} exceeded.')) super(AbstractClassification, self).clean() def __str__(self): """generate pretty string representation""" full_path = [self.name] current = self.parent while current is not None: full_path.append(current.name) current = current.parent # pylint: disable=no-member return ' -> '.join(full_path[::-1])