# -*- 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 logging
from sqlalchemy import (Table, Column, String, Integer, ForeignKey, Interval,
DateTime, PickleType)
from sqlalchemy.exc import UnboundExecutionError
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import synonym, relationship, validates
from stalker import defaults
from stalker.db import Base
from stalker.db.session import DBSession
from stalker.log import logging_level
from stalker.models import make_plural
logger = logging.getLogger(__name__)
logger.setLevel(logging_level)
def create_secondary_table(
primary_cls_name,
secondary_cls_name,
primary_cls_table_name,
secondary_cls_table_name,
secondary_table_name=None
):
"""creates any secondary table
"""
plural_secondary_cls_name = make_plural(secondary_cls_name)
# use the given class_name and the class_table
if not secondary_table_name:
secondary_table_name = \
primary_cls_name + "_" + plural_secondary_cls_name
# check if the table is already defined
if secondary_table_name not in Base.metadata:
secondary_table = Table(
secondary_table_name, Base.metadata,
Column(
primary_cls_name.lower() + "_id",
Integer,
ForeignKey(primary_cls_table_name + ".id"),
primary_key=True,
),
Column(
secondary_cls_name.lower() + "_id",
Integer,
ForeignKey(secondary_cls_table_name + ".id"),
primary_key=True,
)
)
else:
secondary_table = Base.metadata.tables[secondary_table_name]
return secondary_table
[docs]class TargetEntityTypeMixin(object):
"""Adds target_entity_type attribute to mixed in class.
:param target_entity_type: The target entity type which this class is
designed for. Should be a class or a class name.
For example::
from stalker import SimpleEntity, TargetEntityTypeMixin, Project
class A(SimpleEntity, TargetEntityTypeMixin):
__tablename__ = "As"
__mapper_args__ = {"polymorphic_identity": "A"}
def __init__(self, **kwargs):
super(A, self).__init__(**kwargs)
TargetEntityTypeMixin.__init__(self, **kwargs)
a_obj = A(target_entity_type=Project)
The ``a_obj`` will only be accepted by
:class:`~stalker.models.project.Project` instances. You can not assign it
to any other class which accepts a :class:`~stalker.models.type.Type`
instance.
To control the mixed-in class behaviour add these class variables to the
mixed in class:
__nullable_target__ : controls if the target_entity_type can be
nullable or not. Default is False.
__unique_target__ : controls if the target_entity_type should be
unique, so there is only one object for one type.
Default is False.
"""
__nullable_target__ = False
__unique_target__ = False
@declared_attr
def _target_entity_type(cls):
return Column(
"target_entity_type",
String(128),
nullable=cls.__nullable_target__,
unique=cls.__unique_target__
)
[docs] def __init__(self, target_entity_type=None, **kwargs):
self._target_entity_type = \
self._validate_target_entity_type(target_entity_type)
def _validate_target_entity_type(self, target_entity_type_in):
"""validates the given target_entity_type value
"""
# it can not be None
if target_entity_type_in is None:
raise TypeError("%s.target_entity_type can not be None" %
self.__class__.__name__)
if str(target_entity_type_in) == "":
raise ValueError("%s.target_entity_type can not be empty" %
self.__class__.__name__)
# check if it is a class
if isinstance(target_entity_type_in, type):
target_entity_type_in = target_entity_type_in.__name__
return str(target_entity_type_in)
def _target_entity_type_getter(self):
return self._target_entity_type
@declared_attr
def target_entity_type(cls):
return synonym(
"_target_entity_type",
descriptor=property(
fget=cls._target_entity_type_getter,
doc="""The entity type which this object is valid for.
Usually it is set to the TargetClass directly.
"""
)
)
[docs]class StatusMixin(object):
"""Makes the mixed in object statusable.
This mixin adds status and status_list attributes to the mixed in class.
Any object that needs a status and a corresponding status list can include
this mixin.
When mixed with a class which don't have an __init__ method, the mixin
supplies one, and in this case the parameters below must be defined.
:param status_list: this attribute holds a status list object, which shows
the possible statuses that this entity could be in. This attribute can
not be empty or None. Giving a StatusList object, the
StatusList.target_entity_type should match the current class.
.. versionadded:: 0.1.2.a4
The status_list argument now can be skipped or can be None if there
is an active database connection (stalker.models.DBSession is not
None) and there is a suitable
:class:`~stalker.models.status.StatusList` instance in the database
whom :attr:`~stalker.models.status.StatusList.target_entity_type`
attribute is set to the current mixed-in class name.
:param status: It is a :class:`~stalker.models.status.Status` instance
which shows the current status of the statusable object. Integer values
are also accepted, which shows the index of the desired status in the
``status_list`` attribute of the current statusable object. If a
:class:`~stalker.models.status.Status` instance is supplied, it should
also be present in the ``status_list`` attribute. If set to None then the
first :class:`~stalker.models.status.Status` instance in the
``status_list`` will be used.
.. versionadded:: 0.2.0
Status attribute as Status instance:
It is now possible to set the status of the instance by a
:class:`~stalker.models.status.Status` instance directly. And the
:attr:`~stalker.models.mixins.StatusMixin.status` will return a proper
:class:`~stalker.models.status.Status` instance.
"""
[docs] def __init__(self, status=None, status_list=None, **kwargs):
self.status_list = status_list
self.status = status
# logger.debug('%s.status: %s' % (self.__class__.__name__, status))
@declared_attr
def status_id(cls):
return Column(
'status_id',
Integer,
ForeignKey('Statuses.id'),
nullable=False
# This is set to nullable=True but it is impossible to set the
# status to None by using this Declarative approach.
#
# This is done in that way cause SQLAlchemy was flushing the data
# (AutoFlush) preliminarily while checking if the given Status was
# in the related StatusList, and it was complaining about the
# status can not be null
)
@declared_attr
def status(cls):
return relationship(
'Status',
primaryjoin= \
"%s.status_id==Status.status_id" % cls.__name__,
doc="""The current status of the object.
It is a :class:`~stalker.models.status.Status` instance which
is one of the Statuses stored in the ``status_list`` attribute
of this object.
"""
)
@declared_attr
def status_list_id(cls):
return Column(
'status_list_id',
Integer,
ForeignKey('StatusLists.id'), #, use_alter=True, name="x"),
nullable=False
)
@declared_attr
def status_list(cls):
return relationship(
"StatusList",
primaryjoin= \
"%s.status_list_id==StatusList.status_list_id" %
cls.__name__,
)
@validates("status_list")
def _validate_status_list(self, key, status_list):
"""validates the given status_list_in value
"""
from stalker.models.status import StatusList
if status_list is None:
# check if there is a db setup and try to get the appropriate
# StatusList from the database
# disable autoflush to prevent premature class initialization
with DBSession.no_autoflush:
try:
# try to get a StatusList with the target_entity_type is
# matching the class name
status_list = DBSession.query(StatusList) \
.filter_by(target_entity_type=self.__class__.__name__) \
.first()
except UnboundExecutionError:
# it is not mapped just skip it
pass
# if it is still None
if status_list is None:
# there is no db so raise an error because there is no way
# to get an appropriate StatusList
raise TypeError(
"%s instances can not be initialized without a "
"stalker.models.status.StatusList instance, please pass a "
"suitable StatusList (StatusList.target_entity_type=%s) "
"with the 'status_list' argument" %
(self.__class__.__name__, self.__class__.__name__)
)
else:
# it is not an instance of status_list
if not isinstance(status_list, StatusList):
raise TypeError(
"%s.status_list should be an instance of "
"stalker.models.status.StatusList not %s" %
(self.__class__.__name__,
status_list.__class__.__name__)
)
# check if the entity_type matches to the StatusList.target_entity_type
if self.__class__.__name__ != status_list.target_entity_type:
raise TypeError(
"the given StatusLists' target_entity_type is %s, "
"whereas the entity_type of this object is %s" % \
(status_list.target_entity_type,
self.__class__.__name__))
return status_list
@validates('status')
def _validate_status(self, key, status):
"""validates the given status value
"""
from stalker.models.status import Status, StatusList
if not isinstance(self.status_list, StatusList):
raise TypeError("please set the %s.status_list attribute first" %
self.__class__.__name__)
# it is set to None
if status is None:
with DBSession.no_autoflush:
status = self.status_list.statuses[0]
# it is not an instance of status or int
if not isinstance(status, (Status, int)):
raise TypeError("%s.status must be an instance of "
"stalker.models.status.Status or an integer "
"showing the index of the Status object in the "
"%s.status_list, not %s" %
(self.__class__.__name__,
self.__class__.__name__,
status.__class__.__name__))
if isinstance(status, int):
# if it is not in the correct range:
if status < 0:
raise ValueError("%s.status must be a non-negative integer" %
self.__class__.__name__)
if status >= len(self.status_list.statuses):
raise ValueError("%s.status can not be bigger than the length "
"of the status_list" %
self.__class__.__name__)
# get the tatus instance out of the status_list instance
status = self.status_list[status]
# check if the given status is in the status_list
# logger.debug('self.status_list: %s' % self.status_list)
# logger.debug('given status: %s' % status)
if status not in self.status_list:
raise ValueError("The given Status instance for %s.status is not "
"in the %s.status_list, please supply a status "
"from that list." %
(self.__class__.__name__, self.__class__.__name__)
)
return status
[docs]class ScheduleMixin(object):
"""Adds schedule info to the mixed in class.
Adds schedule information like ``start``, ``end`` and ``duration``. These
attributes will be used in TaskJuggler. Because ``effort`` is only
meaningful if there are some ``resources`` this attribute has been left
special for :class:`~stalker.models.task.Task` class. The ``length`` has
not been implemented because of its rare use.
The preceding order for the attributes is as follows::
start > end > duration
So if all of the parameters are given only the ``start`` and the ``end``
will be used and the ``duration`` will be calculated accordingly. In any
other conditions the missing parameter will be calculated from the
following table:
+-------+-----+----------+----------------------------------------+
| start | end | duration | DEFAULTS |
+=======+=====+==========+========================================+
| | | | start = datetime.datetime.now() |
| | | | |
| | | | duration = datetime.timedelta(days=10) |
| | | | |
| | | | end = start + duration |
+-------+-----+----------+----------------------------------------+
| X | | | duration = datetime.timedelta(days=10) |
| | | | |
| | | | end = start + duration |
+-------+-----+----------+----------------------------------------+
| X | X | | duration = end - start |
+-------+-----+----------+----------------------------------------+
| X | | X | end = start + duration |
+-------+-----+----------+----------------------------------------+
| X | X | X | duration = end - start |
+-------+-----+----------+----------------------------------------+
| | X | X | start = end - duration |
+-------+-----+----------+----------------------------------------+
| | X | | duration = datetime.timedelta(days=10) |
| | | | |
| | | | start = end - duration |
+-------+-----+----------+----------------------------------------+
| | | X | start = datetime.datetime.now() |
| | | | |
| | | | end = start + duration |
+-------+-----+----------+----------------------------------------+
Only the ``start``, ``end`` will be stored. The ``duration`` attribute is
the direct difference of the the ``start`` and ``end`` attributes, so there
is no need to store it. But if will be used in calculation of the start and
end values.
The start and end attributes have a ``computed`` companion. Which are the
return values from TaskJuggler. so for start there is the
``computed_start`` and for end there is the ``computed_end`` attributes.
These values are going to be used in Gantt Charts.
The date attributes can be managed with timezones. Follow the Python idioms
shown in the `documentation of datetime`_
.. _documentation of datetime: http://docs.python.org/library/datetime.html
:param start: the start date of the entity, should be a datetime.datetime
instance, the start is the pin point for the date calculation. In
any condition if the start is available then the value will be
preserved. If start passes the end the end is also changed
to a date to keep the timedelta between dates. The default value is
datetime.datetime.now()
:type start: :class:`datetime.datetime`
:param end: the end of the entity, should be a datetime.datetime instance,
when the start is changed to a date passing the end, then the end is also
changed to a later date so the timedelta between the dates is kept.
:type end: :class:`datetime.datetime` or :class:`datetime.timedelta`
:param duration: The duration of the entity. It is a
:class:`datetime.timedelta` instance. The default value is read from
the :class:`~stalker.config.Config` class. See the table above for the
initialization rules.
:type duration: :class:`datetime.timedelta`
:param timing_resolution: The timing_resolution of the datetime.datetime
object in datetime.timedelta. Uses ``timing_resolution`` settings in the
:class:`stalker.config.Config` class which defaults to 1 hour. Setting
the timing_resolution to less then 5 minutes is not suggested because it
is a limit for TaskJuggler.
:type timing_resolution: datetime.timedelta
"""
# # add this lines for Sphinx
# __tablename__ = "ScheduleMixins"
[docs] def __init__(self,
start=None,
end=None,
duration=None,
timing_resolution=None,
**kwargs):
self.timing_resolution = timing_resolution
self._validate_dates(start, end, duration)
@declared_attr
def _end(cls):
return Column("end", DateTime)
def _end_getter(self):
"""The date that the entity should be delivered.
The end can be set to a datetime.timedelta and in this case it will be
calculated as an offset from the start and converted to
datetime.datetime again. Setting the start to a date passing the end
will also set the end, so the timedelta between them is preserved,
default value is 10 days
"""
return self._end
def _end_setter(self, end_in):
self._validate_dates(self.start, end_in, self.duration)
@declared_attr
def end(cls):
return synonym(
"_end",
descriptor=property(
cls._end_getter,
cls._end_setter
)
)
@declared_attr
def _start(cls):
return Column("start", DateTime)
def _start_getter(self):
"""The date that this entity should start.
Also effects the
:attr:`~stalker.models.mixins.ScheduleMixin.end` attribute value in
certain conditions, if the
:attr:`~stalker.models.mixins.ScheduleMixin.start` is set to a time
passing the :attr:`~stalker.models.mixins.ScheduleMixin.end` it will
also offset the :attr:`~stalker.models.mixins.ScheduleMixin.end` to
keep the :attr:`~stalker.models.mixins.ScheduleMixin.duration` value
fixed. :attr:`~stalker.models.mixins.ScheduleMixin.start` should be an
instance of class:`datetime.datetime` and the default value is
:func:`datetime.datetime.now()`
"""
return self._start
def _start_setter(self, start_in):
self._validate_dates(start_in, self.end, self.duration)
@declared_attr
def start(cls):
return synonym(
"_start",
descriptor=property(
cls._start_getter,
cls._start_setter,
)
)
@declared_attr
def _duration(cls):
return Column('duration', Interval)
def _duration_getter(self):
return self._duration
def _duration_setter(self, duration_in):
if duration_in is not None:
if isinstance(duration_in, datetime.timedelta):
# set the end to None
# to make it recalculated
self._validate_dates(self.start, None, duration_in)
else:
self._validate_dates(self.start, self.end, duration_in)
else:
self._validate_dates(self.start, self.end, duration_in)
@declared_attr
def duration(self):
return synonym(
'_duration',
descriptor=property(
self._duration_getter,
self._duration_setter,
doc="""Duration of the entity.
It is a datetime.timedelta instance. Showing the difference of the
:attr:`.start` and the :attr:`.end`. If edited it changes the :attr:`.end`
attribute value."""
)
)
def _validate_dates(self, start, end, duration):
"""updates the date values
"""
# logger.debug('start : %s' % start)
# logger.debug('end : %s' % end)
# logger.debug('duration : %s' % duration)
if not isinstance(start, datetime.datetime):
start = None
if not isinstance(end, datetime.datetime):
end = None
if not isinstance(duration, datetime.timedelta):
duration = None
# check start
if start is None:
# try to calculate the start from end and duration
if end is None:
# set the defaults
start = datetime.datetime.now()
if duration is None:
# set the defaults
duration = defaults.task_duration
end = start + duration
else:
if duration is None:
duration = defaults.task_duration
start = end - duration
# check end
if end is None:
if duration is None:
duration = defaults.task_duration
end = start + duration
if end < start:
# check duration
if duration is None or duration < datetime.timedelta(1):
duration = datetime.timedelta(1)
end = start + duration
# round the dates to the timing_resolution
self._start = self.round_time(start)
self._end = self.round_time(end)
self._duration = self._end - self._start
@declared_attr
def _timing_resolution(cls):
return Column("timing_resolution", Interval)
def _timing_resolution_getter(self):
"""returns the timing_resolution
"""
return self._timing_resolution
def _timing_resolution_setter(self, res_in):
"""sets the timing_resolution
"""
self._timing_resolution = self._validate_timing_resolution(res_in)
logger.debug('self._timing_resolution: %s' % self._timing_resolution)
# update date values
if self.start and self.end and self.duration:
self._validate_dates(
self.round_time(self.start),
self.round_time(self.end),
None
)
@declared_attr
def timing_resolution(cls):
return synonym(
'_timing_resolution',
descriptor=property(
cls._timing_resolution_getter,
cls._timing_resolution_setter,
doc="""The timing_resolution of this object.
Can be set to any value that is representable with
datetime.timedelta. The default value is 1 hour. Whenever it is
changed the start, end and duration values will be updated.
"""
)
)
def _validate_timing_resolution(self, timing_resolution):
"""validates the given timing_resolution value
"""
if timing_resolution is None:
timing_resolution = defaults.timing_resolution
if not isinstance(timing_resolution, datetime.timedelta):
raise TypeError('%s.timing_resolution should be an instance of '
'datetime.timedelta not, %s' %
(self.__class__.__name__,
timing_resolution.__class__.__name__))
return timing_resolution
@declared_attr
def computed_start(cls):
return Column('computed_start', DateTime)
@declared_attr
def computed_end(cls):
return Column('computed_end', DateTime)
@property
[docs] def computed_duration(self):
"""returns the computed_duration as the difference of computed_start
and computed_end if there are computed_start and computed_end otherwise
returns None
"""
return self.computed_end - self.computed_start \
if self.computed_end and self.computed_start else None
[docs] def round_time(self, dt):
"""Round a datetime object to any time laps in seconds.
Uses class property timing_resolution as the closest number of seconds
to round to, defaults 1 hour.
:param dt: datetime.datetime object, defaults now.
Based on Thierry Husson's answer in `Stackoverflow`_
_`Stackoverflow` : http://stackoverflow.com/a/10854034/1431079
"""
# to be compatible with python 2.6 use the following instead of
# total_seconds()
trs = self.timing_resolution.days * 86400 + \
self.timing_resolution.seconds
# convert to seconds
# FIX: using strftime(%s) is dangerous, it uses system time zone
epoch = datetime.datetime(1970, 1, 1)
diff = dt - epoch
diff_in_seconds = diff.days * 86400 + diff.seconds
return epoch + datetime.timedelta(
seconds=(diff_in_seconds + trs * 0.5) // trs * trs
)
@property
[docs] def total_seconds(self):
"""returns the duration as seconds
"""
return self.duration.days * 86400 + self.duration.seconds
@property
[docs] def computed_total_seconds(self):
"""returns the duration as seconds
"""
return self.computed_duration.days * 86400 + \
self.computed_duration.seconds
[docs]class ProjectMixin(object):
"""Gives the ability to connect to a :class:`~stalker.models.project.Project` to the mixed in object.
:param project: A :class:`~stalker.models.project.Project` instance holding
the project which this object is related to. It can not be None, or
anything other than a :class:`~stalker.models.project.Project` instance.
:type project: :class:`~stalker.models.project.Project`
"""
# # add this lines for Sphinx
# __tablename__ = "ProjectMixins"
@declared_attr
def project_id(cls):
return Column(
"project_id",
Integer,
ForeignKey("Projects.id", use_alter=True, name="project_x_id"),
#ForeignKey("Projects.id"),
# cannot use nullable cause a Project object needs
# insert itself as the project and it needs post_update
# thus nullable should be True
#nullable=False,
)
@declared_attr
def project(cls):
backref = cls.__tablename__.lower()
doc = """The :class:`~stalker.models.project.Project` instance that
this object belongs to.
"""
return relationship(
"Project",
primaryjoin= \
cls.__tablename__ + ".c.project_id==Projects.c.id",
post_update=True, # for project itself
uselist=False,
backref=backref,
doc=doc
)
[docs] def __init__(self,
project=None,
**kwargs):
self.project = project
@validates("project")
def _validate_project(self, key, project):
"""validates the given project value
"""
from stalker.models.project import Project
if project is None:
raise TypeError("%s.project can not be None it must be an "
"instance of stalker.models.project.Project" %
self.__class__.__name__)
if not isinstance(project, Project):
raise TypeError("%s.project should be an instance of "
"stalker.models.project.Project instance not %s" %
(self.__class__.__name__,
project.__class__.__name__))
return project
[docs]class ReferenceMixin(object):
"""Adds reference capabilities to the mixed in class.
References are :class:`stalker.models.entity.Entity` instances or anything
derived from it, which adds information to the attached objects. The aim of
the References are generally to give more info to direct the evolution of
the object.
:param references: A list of :class:`~stalker.models.entity.Entity`
objects.
:type references: list of :class:`~stalker.models.entity.Entity` objects.
"""
# add this lines for Sphinx
# __tablename__ = "ReferenceMixins"
[docs] def __init__(self,
references=None,
**kwargs):
if references is None:
references = []
self.references = references
@declared_attr
def references(cls):
# TODO: there is something wrong here, the documentation and the implementation is not telling the same story
# get secondary table
secondary_table = create_secondary_table(
cls.__name__,
'Link',
cls.__tablename__,
'Links',
cls.__name__ + "_References"
)
# return the relationship
return relationship("Link", secondary=secondary_table)
@validates("references")
def _validate_references(self, key, reference):
"""validates the given reference
"""
from stalker.models.entity import SimpleEntity
# all the elements should be instance of stalker.models.entity.Entity
if not isinstance(reference, SimpleEntity):
raise TypeError("%s.references should be all instances of "
"stalker.models.entity.SimpleEntity not %s"
% (self.__class__.__name__,
reference.__class__.__name__))
return reference
[docs]class ACLMixin(object):
"""A Mixin for adding ACLs to mixed in class.
Access control lists or ACLs are used to determine if the given resource
has the permission to access the given data. It is based on Pyramids
Authorization system but organized to fit in Stalker style.
The ACLMixin adds an attribute called ``permissions`` and a
property called ``__acl__`` to be able to pass the permission data to
Pyramid framework.
"""
@declared_attr
def permissions(cls):
# get the secondary table
secondary_table = create_secondary_table(
cls.__name__, 'Permission', cls.__tablename__, 'Permissions'
)
return relationship('Permission', secondary=secondary_table)
@validates('permissions')
def _validate_permissions(self, key, permission):
"""validates the given permission value
"""
from stalker.models.auth import Permission
if not isinstance(permission, Permission):
raise TypeError("%s.permissions should be all instances of "
"stalker.models.auth.Permission not %s" %
(self.__class__.__name__,
permission.__class__.__name__))
return permission
@property
def __acl__(self):
"""Returns Pyramid friendly ACL list composed by the:
* Permission.access (Ex: 'Allow' or 'Deny')
* The Mixed in class name and the object name (Ex: 'User:eoyilmaz')
* The Action and the target class name (Ex: 'Create_Asset')
Thus a list of tuple is returned as follows::
__acl__ = [
('Allow', 'User:eoyilmaz', 'Create_Asset'),
]
For the last example user eoyilmaz can grant access to views requiring
'Add_Project' permission.
"""
return [(perm.access,
self.__class__.__name__ + ':' + self.name,
perm.action + '_' + perm.class_name)
for perm in self.permissions]
[docs]class CodeMixin(object):
"""Adds code info to the mixed in class.
.. versionadded:: 0.2.0
The code attribute of the SimpleEntity is now introduced as a separate
mixin. To let it be used by the classes it is really needed.
The CodeMixin just adds a new field called ``code``. It is a very simple
attribute and is used for simplifying long names (like Project.name etc.).
Contrary to previous implementations the code attribute is not formatted in
anyway, so care needs to be taken if the code attribute is going to be used
in filesystem as file and directory names.
:param str code: The code attribute is a string, can not be empty or can
not be None.
"""
[docs] def __init__(
self,
code=None,
**kwargs):
logger.debug('code: %s' % code)
self.code = code
@declared_attr
def code(cls):
return Column(
'code',
String(256),
nullable=False,
doc="""The code name of this object.
It accepts strings. Can not be None."""
)
@validates('code')
def _validate_code(self, key, code):
"""validates the given code attribute
"""
logger.debug('validating code value of: %s' % code)
if code is None:
raise TypeError("%s.code cannot be None" % self.__class__.__name__)
if not isinstance(code, (str, unicode)):
raise TypeError('%s.code should be an instance of string or '
'unicode not %s' %
(self.__class__.__name__,
code.__class__.__name__)
)
if code == '':
raise ValueError('%s.code can not be an empty string')
return code
[docs]class WorkingHoursMixin(object):
"""Sets working hours for the mixed in class.
Generally is meaningful for users, departments and studio.
:param working_hours: A :class:`~stalker.models.project.WorkingHours`
instance showing the working hours settings for that project. This data
is stored as a PickleType in the database.
"""
[docs] def __init__(self,
working_hours=None,
**kwargs):
self.working_hours = working_hours
@declared_attr
def working_hours(cls):
return Column(PickleType)
@validates('working_hours')
def _validate_working_hours(self, key, wh):
"""validates the given working hours value
"""
if wh is None:
# use the default one
from stalker import WorkingHours
wh = WorkingHours()
return wh