# -*- coding: utf-8 -*-
# Stalker a Production Asset Management System
# Copyright (C) 2009-2013 Erkan Ozgur Yilmaz
#
# This file is part of Stalker.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import datetime
import re
import uuid
from sqlalchemy import Table, Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship, validates, reconstructor
import stalker
from stalker.db import Base
from stalker.log import logging_level
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging_level)
[docs]class SimpleEntity(Base):
"""The base class of all the others
The ``SimpleEntity`` is the starting point of the Stalker Object Model, it
starts by adding the basic information about an entity which are
:attr:`.name`, :attr:`.description`, the audit information like
:attr:`.created_by`, :attr:`.updated_by`, :attr:`.date_created`,
:attr:`.date_updated` and a couple of naming attributes like
:attr:`.nice_name` and last but not least the :attr:`.type` attribute which
is very important for entities that needs a type.
.. note::
For derived classes if the
:attr:`~stalker.models.entity.SimpleEntity.type` needed to be
specifically specified, that is it can not be None or nothing else then
a :class:`~stalker.models.type.Type` instance, set the
``strictly_typed`` class attribute to True::
class NewClass(SimpleEntity):
__strictly_typed__ = True
This will ensure that the derived class always have a proper
:attr:`~stalker.models.entity.SimpleEntity.type` attribute and can not
be initialized without one.
Two SimpleEntities considered to be equal if they have the same
:attr:`.name`, the
other attributes doesn't matter.
.. versionadded:: 0.2.0
Name attribute can be skipped. Starting from version 0.2.0 the ``name``
attribute can be skipped. For derived classes use the ``__auto_name__``
class attribute to control auto naming behaviour.
:param string name: A string or unicode value that holds the name of this
entity. It should not contain any white space at the beginning and at
the end of the string. Valid characters are [a-zA-Z0-9\_/S].
Advanced::
For classes derived from the SimpleEntity, if an automatic name is
desired, the ``__auto_name__`` class attribute can be set to True. Then
Stalker will automatically generate a uuid4 sequence for the name
attribute.
:param str description: A string or unicode attribute that holds the
description of this entity object, it could be an empty string, and it
could not again have white spaces at the beginning and at the end of the
string, again any given objects will be converted to strings
:param created_by: The :class:`~stalker.models.auth.User` who has created
this object
:type created_by: :class:`~stalker.models.auth.User`
:param updated_by: The :class:`~stalker.models.auth.User` who has updated
this object lastly. The created_by and updated_by attributes point the
same object if this object is just created.
:param date_created: The date that this object is created.
:type date_created: :class:`datetime.datetime`
:param date_updated: The date that this object is updated lastly. For newly
created entities this is equal to date_created and the date_updated
cannot point a date which is before date_created.
:type date_updated: :class:`datetime.datetime`
:param type: The type of the current SimpleEntity. Used across several
places in Stalker. Can be None. The default value is None.
:type type: :class:`~stalker.models.type.Type`
"""
# auto generate name values
__auto_name__ = True
__strictly_typed__ = False
# TODO: Allow the user to specify the formatting of the name attribute as a Regular Expression (name_formatter)
__name_formatter__ = None
__tablename__ = "SimpleEntities"
id = Column("id", Integer, primary_key=True)
entity_type = Column(String(128), nullable=False)
__mapper_args__ = {
"polymorphic_on": entity_type,
"polymorphic_identity": "SimpleEntity"
}
name = Column(
String(256),
nullable=False,
doc="""Name of this object"""
)
description = Column(
"description",
String,
doc="""Description of this object."""
)
created_by_id = Column(
"created_by_id",
Integer,
ForeignKey("Users.id", use_alter=True, name="xc"),
doc="""The id of the :class:`~stalker.models.auth.User` who has created
this entity."""
)
created_by = relationship(
"User",
backref="entities_created",
primaryjoin="SimpleEntity.created_by_id==User.user_id",
post_update=True,
doc="""The :class:`~stalker.models.auth.User` who has created this object."""
)
updated_by_id = Column(
"updated_by_id",
Integer,
ForeignKey("Users.id", use_alter=True, name="xu"),
doc="""The id of the :class:`~stalker.models.auth.User` who has updated
this entity."""
)
updated_by = relationship(
"User",
backref="entities_updated",
primaryjoin="SimpleEntity.updated_by_id==User.user_id",
post_update=True,
doc="""The :class:`~stalker.models.auth.User` who has updated this object."""
)
date_created = Column(
DateTime,
default=datetime.datetime.now(),
doc="""A :class:`datetime.datetime` instance showing the creation date and time of this object."""
)
date_updated = Column(
DateTime,
default=datetime.datetime.now(),
doc="""A :class:`datetime.datetime` instance showing the update date and time of this object."""
,
)
type_id = Column(
"type_id",
Integer,
ForeignKey("Types.id", use_alter=True, name="y"),
doc="""The id of the :class:`~stalker.models.type.Type` of this entity.
Mainly used by SQLAlchemy to create a Many-to-One relates between
SimpleEntities and Types.
"""
)
type = relationship(
"Type",
primaryjoin="SimpleEntities.c.type_id==Types.c.id",
post_update=True,
doc="""The type of the object.
It is an instance of :class:`~stalker.models.type.Type` with a proper
:attr:`~stalker.models.type.Type.target_entity_type`.
"""
)
generic_data = relationship(
'SimpleEntity',
secondary='SimpleEntity_GenericData',
primaryjoin='SimpleEntities.c.id==SimpleEntity_GenericData.c.simple_entity_id',
secondaryjoin='SimpleEntity_GenericData.c.other_simple_entity_id==SimpleEntities.c.id',
post_update=True,
doc='''This attribute can hold any kind of data which exists in SOM.
'''
)
thumbnail_id = Column(
'thumbnail_id',
Integer,
ForeignKey('Links.id', use_alter=True, name='z')
)
thumbnail = relationship(
'Link',
primaryjoin='SimpleEntities.c.thumbnail_id==Links.c.id',
post_update=True
)
__stalker_version__ = Column("stalker_version", String(256))
[docs] def __init__(
self,
name=None,
description="",
type=None,
created_by=None,
updated_by=None,
date_created=None,
date_updated=None,
thumbnail=None,
**kwargs
): # pylint: disable=W0613
# name and nice_name
self._nice_name = ""
self.name = name
self.description = description
self.created_by = created_by
self.updated_by = updated_by
date_created = date_created
date_updated = date_updated
if date_created is None:
date_created = datetime.datetime.now()
if date_updated is None:
date_updated = datetime.datetime.now()
self.date_created = date_created
self.date_updated = date_updated
self.type = type
self.thumbnail = thumbnail
self.__stalker_version__ = stalker.__version__
@reconstructor
def __init_on_load__(self):
"""initialized the instance variables when the instance created with
SQLAlchemy
"""
self._nice_name = None
def __repr__(self):
"""the representation of the SimpleEntity
"""
return "<%s (%s)>" % (self.name, self.entity_type)
def __eq__(self, other):
"""the equality operator
"""
return isinstance(other, SimpleEntity) and\
self.name == other.name
def __ne__(self, other):
"""the inequality operator
"""
return not self.__eq__(other)
@validates("description")
def _validate_description(self, key, description_in):
"""validates the given description_in value
"""
if description_in is None:
description_in = ""
return str(description_in)
@validates("name")
def _validate_name(self, key, name):
"""validates the given name_in value
"""
if self.__auto_name__:
if name is None or name == '':
# generate a uuid4
name = self.__class__.__name__ + '_' + \
uuid.uuid4().urn.split(':')[2]
# it is None
if name is None:
raise TypeError("%s.name can not be None" %
self.__class__.__name__)
if not isinstance(name, (str, unicode)):
raise TypeError("%s.name should be an instance of string or "
"unicode not %s" %
(self.__class__.__name__,
name.__class__.__name__))
name = self._format_name(str(name))
# it is empty
if name == "":
raise ValueError("%s.name can not be an empty string" %
self.__class__.__name__)
# also set the nice_name
self._nice_name = self._format_nice_name(name)
return name
def _format_name(self, name_in):
"""formats the name_in value
"""
# remove unnecessary characters from the string
name_in = name_in.strip()
#name_in = re.sub(r'([^a-zA-Z0-9\s_\-#]+)', '', name_in).strip()
# remove all the characters which are not alphabetic from the start of
# the string
#name_in = re.sub(r"(^[^a-zA-Z0-9]+)", '', name_in)
# remove multiple spaces
name_in = re.sub(r'[\s]+', ' ', name_in)
return name_in
def _format_nice_name(self, nice_name_in):
"""formats the given nice name
"""
# remove unnecessary characters from the string
nice_name_in = str(nice_name_in).strip()
nice_name_in = re.sub(r'([^a-zA-Z0-9\s_\-]+)', '', nice_name_in).strip()
# remove all the characters which are not alphabetic from the start of
# the string
nice_name_in = re.sub(r"(^[^a-zA-Z0-9]+)", '', nice_name_in)
# remove multiple spaces
nice_name_in = re.sub(r'[\s]+', ' ', nice_name_in)
## replace camel case letters
# nice_name_in = re.sub(r"(.+?[a-z]+)([A-Z])", r"\1_\2", nice_name_in)
# replace white spaces and dashes with under score
nice_name_in = re.sub("([\s\-])+", r"_", nice_name_in)
# remove multiple underscores
nice_name_in = re.sub(r"([_]+)", r"_", nice_name_in)
# turn it to lower case
#nice_name_in = nice_name_in.lower()
return nice_name_in
@property
[docs] def nice_name(self):
"""Nice name of this object.
It has the same value with the name (contextually) but with a different
format like, all the white spaces replaced by underscores ("\_"), all
the CamelCase form will be expanded by underscore (\_) characters and
it is always lower case.
"""
# also set the nice_name
if self._nice_name is None or self._nice_name == "":
self._nice_name = self._format_nice_name(self.name)
return self._nice_name
@validates("created_by")
def _validate_created_by(self, key, created_by_in):
"""validates the given created_by_in attribute
"""
from stalker.models.auth import User
if created_by_in is not None:
if not isinstance(created_by_in, User):
raise TypeError("%s.created_by should be an instance of"
"stalker.models.auth.User" %
self.__class__.__name__)
return created_by_in
@validates("updated_by")
def _validate_updated_by(self, key, updated_by_in):
"""validates the given updated_by_in attribute
"""
from stalker.models.auth import User
if updated_by_in is None:
# set it to what created_by attribute has
updated_by_in = self.created_by
if updated_by_in is not None:
if not isinstance(updated_by_in, User):
raise TypeError("%s.updated_by should be an instance of"
"stalker.models.auth.User" %
self.__class__.__name__)
return updated_by_in
@validates("date_created")
def _validate_date_created(self, key, date_created_in):
"""validates the given date_created_in
"""
if date_created_in is None:
raise TypeError("%s.date_created can not be None" %
self.__class__.__name__)
if not isinstance(date_created_in, datetime.datetime):
raise TypeError("%s.date_created should be an instance of "
"datetime.datetime" % self.__class__.__name__)
return date_created_in
@validates("date_updated")
def _validate_date_updated(self, key, date_updated_in):
"""validates the given date_updated_in
"""
# it is None
if date_updated_in is None:
raise TypeError("%s.date_updated can not be None" %
self.__class__.__name__)
# it is not an instance of datetime.datetime
if not isinstance(date_updated_in, datetime.datetime):
raise TypeError("%s.date_updated should be an instance of "
"datetime.datetime" % self.__class__.__name__)
# lower than date_created
if date_updated_in < self.date_created:
raise ValueError("%s.date_updated could not be set to a date "
"before 'date_created', try setting the "
"'date_created' before" %
self.__class__.__name__)
# TODO: all the attribute check errors should use self.__class__.__name__ as used here
return date_updated_in
@validates("type")
def _validate_type(self, key, type_in):
"""validates the given type value
"""
from stalker.models.type import Type
raise_error = False
if not self.__strictly_typed__:
if type_in is not None:
if not isinstance(type_in, Type):
raise_error = True
else:
if not isinstance(type_in, Type):
raise_error = True
if raise_error:
raise TypeError("%s.type must be an instance of "
"stalker.models.type.Type not %s" %
(self.__class__.__name__, type_in))
return type_in
@validates('thumbnail')
def _validate_thumbnail(self, key, thumb):
"""validates the given thumb value
"""
if thumb is not None:
from stalker import Link
if not isinstance(thumb, Link):
raise TypeError('%s.thumbnail should be a '
'stalker.models.link.Link instance, not %s' %
(self.__class__.__name__,
thumb.__class__.__name__))
return thumb
@property
[docs] def tjp_id(self):
"""returns TaskJuggler compatible id
"""
return "%s_%s" % (self.__class__.__name__, self.id)
@property
[docs] def to_tjp(self):
"""renders a TaskJuggler compliant string used for TaskJuggler
integration. Needs to be overridden in inherited classes.
"""
raise NotImplementedError('This property is not implemented in %s' %
self.__class__.__name__)
[docs]class Entity(SimpleEntity):
"""Another base data class that adds tags and notes to the attributes list.
This is the entity class which is derived from the SimpleEntity and adds
only tags to the list of parameters.
Two Entities considered equal if they have the same name. It doesn't matter
if they have different tags or notes.
:param list tags: A list of :class:`~stalker.models.tag.Tag` objects
related to this entity. tags could be an empty list, or when omitted it
will be set to an empty list.
:param list notes: A list of :class:`~stalker.models.note.Note` instances.
Can be an empty list, or when omitted it will be set to an empty list,
when set to None it will be converted to an empty list.
"""
__auto_name__ = True
__tablename__ = "Entities"
__mapper_args__ = {"polymorphic_identity": "Entity"}
entity_id = Column("id", Integer, ForeignKey("SimpleEntities.id"),
primary_key=True)
tags = relationship(
"Tag",
secondary="Entity_Tags",
backref="entities",
doc="""A list of tags attached to this object.
It is a list of :class:`~stalker.models.tag.Tag` instances which shows
the tags of this object"""
)
notes = relationship(
"Note",
primaryjoin="Entities.c.id==Notes.c.entity_id",
backref="entity",
doc="""All the :class:`~stalker.models.note.Notes`\ s attached to this entity.
It is a list of :class:`~stalker.models.note.Note` instances or an
empty list, setting it None will raise a TypeError.
"""
)
[docs] def __init__(self,
tags=None,
notes=None,
**kwargs):
super(Entity, self).__init__(**kwargs)
if tags is None:
tags = []
if notes is None:
notes = []
self.tags = tags
self.notes = notes
@reconstructor
def __init_on_load__(self):
"""initialized the instance variables when the instance created with
SQLAlchemy
"""
super(Entity, self).__init_on_load__()
@validates("notes")
def _validate_notes(self, key, note):
"""validates the given note value
"""
from stalker.models.note import Note
if not isinstance(note, Note):
raise TypeError("%s.note should be an instance of "
"stalker.models.note.Note not %s" %
(self.__class__.__name__,
note.__class__.__name__))
return note
@validates("tags")
def _validate_tags(self, key, tag):
"""validates the given tag
"""
from stalker.models.tag import Tag
if not isinstance(tag, Tag):
raise TypeError("%s.tag should be an instance of "
"stalker.models.tag.Tag not %s" %
(self.__class__.__name__,
tag.__class__.__name__))
return tag
def __eq__(self, other):
"""the equality operator
"""
return super(Entity, self).__eq__(other) and \
isinstance(other, Entity)
# ENTITY_TAGS
Entity_Tags = Table(
"Entity_Tags", Base.metadata,
Column(
"entity_id",
Integer,
ForeignKey("Entities.id"),
primary_key=True,
),
Column(
"tag_id",
Integer,
ForeignKey("Tags.id"),
primary_key=True,
)
)
# SIMPLEENTITY_GENERICDATA
SimpleEntity_GenericData = Table(
'SimpleEntity_GenericData', Base.metadata,
Column(
'simple_entity_id',
Integer,
ForeignKey('SimpleEntities.id'),
primary_key=True
),
Column(
'other_simple_entity_id',
Integer,
ForeignKey('SimpleEntities.id'),
primary_key=True
)
)