#-*- coding: utf-8 -*-
import re
import datetime
import platform
from stalker.ext.validatedList import ValidatedList
from stalker.core.errors import CircularDependencyError
from stalker.conf import defaults
########################################################################
class EntityMeta(type):
"""The metaclass for the very basic entity.
Just adds the name of the class as the entity_type class attribute and
creates an attribute called plural_name to hold the auto generated plural
form of the class name. These two attributes can be overriden in the
class itself.
"""
#----------------------------------------------------------------------
def __new__(cls, classname, bases, dict_):
# create the entity_type
dict_["entity_type"] = unicode(classname)
# try to find a plural name for the class if not given
if not dict_.has_key("plural_name"):
plural_name = unicode(classname+"s")
if classname[-1] == "y":
plural_name = unicode(classname[:-1] + "ies")
elif classname[-2] == "ch":
plural_name = unicode(classname + "es")
elif classname[-1] == "f":
plural_name = unicode(classname[:-1] + "ves")
elif classname[-1] == "s":
plural_name = unicode(classname + "es")
dict_["plural_name"] = plural_name
if not dict_.has_key("__strictly_typed__"):
dict_["__strictly_typed__"] = False
return super(EntityMeta, cls).__new__(cls, classname, bases, dict_)
########################################################################
[docs]class SimpleEntity(object):
"""The base class of all the others
The :class:`~stalker.core.models.SimpleEntity` is the starting point of the
Stalker Object Model, it starts by adding the basic information about an
entity which are :attr:`~stalker.core.models.SimpleEntity.name`,
:attr:`~stalker.core.models.SimpleEntity.description`, the audit
informations like :attr:`~stalker.core.models.SimpleEntity.created_by`,
:attr:`~stalker.core.models.SimpleEntity.updated_by`,
:attr:`~stalker.core.models.SimpleEntity.date_created`,
:attr:`~stalker.core.models.SimpleEntity.date_updated` and a couple of
naming attributes like :attr:`~stalker.core.models.SimpleEntity.code` and
:attr:`~stalker.core.models.SimpleEntity.nice_name` and last but not least
the :attr:`~stalker.core.models.SimpleEntity.type` attribute which is very
important for entities that needs a type.
For derived classes if the :attr:`~stalker.core.models.SimpleEntity.type`
needed to be specifically specified, that is it can not be None or nothing
else then a :class:`~stalker.core.models.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.core.models.SimpleEntity.type` attribute and can not
initialize without one.
Two SimpleEntities considered to be equal if they have the same name, the
other attributes doesn't matter.
The formatting rules for the code attribute is as follows:
* only alphanumerics and underscore is allowed \[a-zA-Z0-9_\]
* no number is allowed at the beggining
* no white spaces are allowed
* all the white spaces will be converted to underscore characters
* all the underscores are converted to only one underscore character if
more than one follows each other
Examples:
+-----------------------------------+-----------------------------------+
| Input Value | Formatted Output |
+===================================+===================================+
| testCode | testCode |
+-----------------------------------+-----------------------------------+
| 1testCode | testCode |
+-----------------------------------+-----------------------------------+
| _testCode | testCode |
+-----------------------------------+-----------------------------------+
| 2423$+^^+^'%+%%&_testCode | testCode |
+-----------------------------------+-----------------------------------+
| 2423$+^^+^'%+%%&_testCode_35 | testCode_35 |
+-----------------------------------+-----------------------------------+
| 2423$ +^^+^ '%+%%&_ testCode\_ 35 | testCode_35 |
+-----------------------------------+-----------------------------------+
| SH001 | SH001 |
+-----------------------------------+-----------------------------------+
| My CODE is Ozgur | My_CODE_is_Ozgur |
+-----------------------------------+-----------------------------------+
| this is another code for an asset | this_is_another_code_for_an_asset |
+-----------------------------------+-----------------------------------+
:param string name: A string or unicode value that holds the name of this
entity. It can not be empty, the first letter should be an alphabetic
([a-zA-z]) (not alphanumeric [a-zA-Z0-9]) letter and it should not
contain any white space at the beggining and at the end of the string,
giving an object the object will be converted to string and then the
resulting string will be conditioned.
: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 beggining and at the end of the
string, again any given objects will be converted to strings
:param created_by: The :class:`~stalker.core.models.User` who has created
this object
:type created_by: :class:`~stalker.core.models.User`
:param updated_by: The :class:`~stalker.core.models.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 thedate_updated cannot
point a date which is before date_created.
:type date_updated: :class:`datetime.datetime`
:param str code: The code name of this object. It accepts string or unicode
values and any other kind of objects will be converted to string. Can be
omitted and it will be set to the same value of the nice_name attribute.
If both the name and code arguments are given the code attribute will be
set to the given code argument value, but in any update to name attribute
the code also will be updated to the nice_name attribute. When the code
is directly edited the code will not be formated other than removing any
illegal characters. The default value is the same value of the nice_name.
: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.core.models.Type`
"""
__metaclass__ = EntityMeta
_nice_name = None
#----------------------------------------------------------------------
[docs] def __init__(self,
name=None,
description="",
type=None,
created_by=None,
updated_by=None,
date_created=datetime.datetime.now(),
date_updated=datetime.datetime.now(),
code=None,
**kwargs
):
# code attribute
# just set it to None for now
self._code = ""
# name and nice_name
self._nice_name = ""
self._name = ""
self.name = name
# code
# if the given code argument is not None
# use it to set the code
if code is not None and code is not "":
self._code = self._validate_code(code)
self._description = self._validate_description(description)
self._created_by = self._validate_created_by(created_by)
self._updated_by = self._validate_updated_by(updated_by)
self._date_created = self._validate_date_created(date_created)
self._date_updated = self._validate_date_updated(date_updated)
self._type = self._validate_type(type)
#----------------------------------------------------------------------
def __repr__(self):
"""the representation of the SimpleEntity
"""
return "<%s (%s, %s)>" % (self.entity_type, self.name, self.code)
#----------------------------------------------------------------------
def _validate_description(self, description_in):
"""validates the given description_in value
"""
if description_in is None:
description_in = ""
return str(description_in)
#----------------------------------------------------------------------
def _validate_name(self, name_in):
"""validates the given name_in value
"""
# it is None
if name_in is None:
raise TypeError("the name couldn't be set to None")
name_in = self._condition_name(str(name_in))
# it is empty
if name_in == "":
raise ValueError("the name couldn't be an empty string")
return name_in
#----------------------------------------------------------------------
def _condition_code(self, code_in):
"""formats the given code_in value
"""
# just set it to the uppercase of what nice_name gives
# remove unnecesary characters from the string
code_in = self._validate_name(code_in)
# replace camel case letters
#code_in = re.sub(r"(.+?[a-z]+)([A-Z])", r"\1_\2", code_in)
# replace white spaces with under score
code_in = re.sub("([\s\-])+", r"_", code_in)
# remove multiple underscores
code_in = re.sub(r"([_]+)", r"_", code_in)
return code_in
#----------------------------------------------------------------------
def _condition_name(self, name_in):
"""conditions the name_in value
"""
# remove unnecesary characters from the string
name_in = re.sub("([^a-zA-Z0-9\s_\-]+)", r"", name_in).strip()
# remove all the characters which are not alpabetic
name_in = re.sub("(^[^a-zA-Z]+)", r"", name_in)
return name_in
#----------------------------------------------------------------------
def _condition_nice_name(self, nice_name_in):
"""conditions the given nice name
"""
## remove unnecesary characters from the beginning
#nice_name_in = re.sub("(^[^A-Za-z]+)", r"", nice_name_in)
# remove unnecesary characters from the string
nice_name_in = self._validate_name(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 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
#----------------------------------------------------------------------
[docs] def description():
def fget(self):
return self._description
def fset(self, description_in):
self._description = self._validate_description(description_in)
doc = """Description of this object."""
return locals()
description = property(**description())
#----------------------------------------------------------------------
[docs] def name():
def fget(self):
return self._name
def fset(self, name_in):
assert(isinstance(self, SimpleEntity))
self._name = self._validate_name(name_in)
# also set the nice_name
self._nice_name = self._condition_nice_name(self._name)
# set the code
#self.code = self._nice_name.upper()
self.code = self._name
doc = """Name of this object"""
return locals()
name = property(**name())
#----------------------------------------------------------------------
[docs] def nice_name():
def fget(self):
# also set the nice_name
if self._nice_name is None or self._nice_name == "":
self._nice_name = self._condition_nice_name(self._name)
return self._nice_name
doc = """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.
There is also the ``code`` attribute which is simply the upper case
form of ``nice_name`` if it is not defined differently (i.e set to
another value)."""
return locals()
nice_name = property(**nice_name())
#----------------------------------------------------------------------
def _validate_code(self, code_in):
"""validates the given code value
"""
# check if the code_in is None or empty string
if code_in is None or code_in=="":
# restore the value from nice_name and let it be reformatted
#code_in = self.nice_name.upper()
code_in = self.nice_name
return self._condition_code(str(code_in))
#----------------------------------------------------------------------
def _validate_created_by(self, created_by_in):
"""validates the given created_by_in attribute
"""
if created_by_in is not None:
if not isinstance(created_by_in, User):
raise TypeError("the created_by attribute should be an "
"instance of stalker.core.models.User")
return created_by_in
#----------------------------------------------------------------------
def _validate_updated_by(self, updated_by_in):
"""validates the given updated_by_in attribute
"""
if updated_by_in is None:
# set it to what created_by attribute has
updated_by_in = self._created_by
from stalker.core.models import User
if updated_by_in is not None:
if not isinstance(updated_by_in, User):
raise TypeError("the updated_by attribute should be an "
"instance of stalker.core.models.User")
return updated_by_in
#----------------------------------------------------------------------
def _validate_date_created(self, date_created_in):
"""validates the given date_creaetd_in
"""
if date_created_in is None:
raise TypeError("the date_created could not be None")
if not isinstance(date_created_in, datetime.datetime):
raise TypeError("the date_created should be an instance of "
"datetime.datetime")
return date_created_in
#----------------------------------------------------------------------
def _validate_date_updated(self, date_updated_in):
"""validates the given date_updated_in
"""
# it is None
if date_updated_in is None:
raise TypeError("the date_updated could not be None")
# it is not an instance of datetime.datetime
if not isinstance(date_updated_in, datetime.datetime):
raise TypeError("the date_updated should be an instance of "
"datetime.datetime")
# lower than date_created
if date_updated_in < self.date_created:
raise ValueError("the date_updated could not be set to a date "
"before date_created, try setting the "
"date_created before")
return date_updated_in
#----------------------------------------------------------------------
def _validate_type(self, type_in):
"""validates the given type value
"""
raise_error = False
if not self.__strictly_typed__:
if not type_in is 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.core.models.Type" \
% self.entity_type)
return type_in
#----------------------------------------------------------------------
[docs] def type():
def fget(self):
return self._type
def fset(self, type_in):
self._type = self._validate_type(type_in)
doc = """The type of the object.
It is an instance of :class:`~stalker.core.models.Type` with a proper
:attr:`~stalker.core.models.Type.target_entity_type`.
"""
return locals()
type = property(**type())
#----------------------------------------------------------------------
[docs] def code():
def fget(self):
return self._code
def fset(self, code_in):
self._code = self._validate_code(code_in)
doc = """The code name of this object.
It accepts string or unicode values and any other kind of objects will
be converted to string. In any update to the name attribute the code
also will be updated. If the code is not initialized or given as None,
it will be set to the uppercase version of the nice_name attribute.
Setting the code attribute to None will reset it to the default value.
The default value is the upper case form of the nice_name."""
return locals()
code = property(**code())
#----------------------------------------------------------------------
[docs] def created_by():
def fget(self):
return self._created_by
def fset(self, created_by_in):
self._created_by = self._validate_created_by(created_by_in)
doc = """The :class:`~stalker.core.models.User` who has created this object."""
return locals()
created_by = property(**created_by())
#----------------------------------------------------------------------
[docs] def updated_by():
def fget(self):
return self._updated_by
def fset(self, updated_by_in):
self._updated_by = self._validate_updated_by(updated_by_in)
doc = """The :class:`~stalker.core.models.User` who has updated this object."""
return locals()
updated_by = property(**updated_by())
#----------------------------------------------------------------------
[docs] def date_created():
def fget(self):
return self._date_created
def fset(self, date_created_in):
self._date_created = self._validate_date_created(date_created_in)
doc = """A :class:`datetime.datetime` instance showing the creation date and time of this object."""
return locals()
date_created = property(**date_created())
#----------------------------------------------------------------------
[docs] def date_updated():
def fget(self):
return self._date_updated
def fset(self, date_updated_in):
self._date_updated = self._validate_date_updated(date_updated_in)
doc = """A :class:`datetime.datetime` instance showing the update date and time of this object."""
return locals()
date_updated = property(**date_updated())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return isinstance(other, SimpleEntity) and \
self.name == other.name #and \
#self.description == other.description
#----------------------------------------------------------------------
def __ne__(self, other):
"""the inequality operator
"""
return not self.__eq__(other)
########################################################################
[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.core.models.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.core.models.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.
"""
#----------------------------------------------------------------------
[docs] def __init__(self,
tags=[],
notes=[],
**kwargs
):
super(Entity, self).__init__(**kwargs)
self._tags = self._validate_tags(tags)
self._notes = self._validate_notes(notes)
#----------------------------------------------------------------------
def _validate_notes(self, notes_in):
"""validates the given notes value
"""
if not isinstance(notes_in, list):
raise TypeError("notes should be an instance of list")
from stalker.core.models import Note
for element in notes_in:
if not isinstance(element, Note):
raise TypeError("every element in notes should be an "
"instance of stalker.core.models.Note "
"class")
return ValidatedList(notes_in)
#----------------------------------------------------------------------
def _validate_tags(self, tags_in):
"""validates the given tags_in value
"""
# it is not an instance of list
if not isinstance(tags_in, list):
raise TypeError("the tags attribute should be set to a list")
return ValidatedList(tags_in)
#----------------------------------------------------------------------
[docs] def notes():
def fget(self):
return self._notes
def fset(self, notes_in):
self._notes = self._validate_notes(notes_in)
doc = """All the notes about this entity.
It is a list of :class:`~stalker.core.models.Note` objects or an empty
list, None will be converted to an empty list.
"""
return locals()
notes = property(**notes())
#----------------------------------------------------------------------
tags = property(**tags())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Entity, self).__eq__(other) and \
isinstance(other, Entity)
########################################################################
[docs]class Type(Entity):
"""Everything can have a type.
.. versionadded:: 0.1.1
Types
Type is a generalized version of the previous design that defines types for
specific classes.
The purpose of the :class:`~stalker.core.models.Type` class is just to
define a new type for a specific :class:`~stalker.core.models.Entity`. For
example, you can have a ``Character`` :class:`~stalker.core.models.Asset`
or you can have a ``Commercial`` :class:`~stalker.core.models.Project` or
you can define a :class:`~stalker.core.models.Link` as an ``Image`` etc.,
to create a new :class:`~stalker.core.models.Type` for various classes::
Type(name="Character", target_entity_type="Asset")
Type(name="Commercial", target_entity_type="Project")
Type(name="Image", target_entity_type="Link")
or::
Type(name="Character", target_entity_type=Asset.entity_type)
Type(name="Commercial", target_entity_type=Project.entity_type)
Type(name="Image", target_entity_type=Link.entity_type)
or even better:
Type(name="Character", target_entity_type=Asset)
Type(name="Commercial", target_entity_type=Project)
Type(name="Image", target_entity_type=Link)
By using :class:`~stalker.core.models.Type`\ s, one can able to sort and
group same type of entities.
:class:`~stalker.core.models.Type`\ s are generally used in
:class:`~stalker.core.models.Structure`\ s.
:param string target_entity_type: The string defining the target type of
this :class:`~stalker.core.models.Type`.
"""
#----------------------------------------------------------------------
[docs] def __init__(self, target_entity_type=None, **kwargs):
super(Type, self).__init__(**kwargs)
self._target_entity_type =\
self._validate_target_entity_type(target_entity_type)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Type, self).__eq__(other) and isinstance(other, Type) \
and self.target_entity_type == other.target_entity_type
#----------------------------------------------------------------------
def __ne__(self, other):
"""the inequality operator
"""
return not self.__eq__(other)
#----------------------------------------------------------------------
def _validate_target_entity_type(self, target_entity_type_in):
"""validates the given target_entity_type value
"""
# check if a class is given
if isinstance(target_entity_type_in, type):
target_entity_type_in = target_entity_type_in.__name__
error_string = "target_entity_type must be a string showing the "\
"target class name"
if target_entity_type_in is None \
or not isinstance(target_entity_type_in, str):
raise TypeError(error_string)
if target_entity_type_in == "":
raise ValueError(error_string)
return target_entity_type_in
#----------------------------------------------------------------------
[docs] def target_entity_type():
def fget(self):
return self._target_entity_type
doc = """The target type of this Type instance.
It is a string, showing the name of the target type class. It is a
read-only attribute.
"""
return locals()
target_entity_type = property(**target_entity_type())
########################################################################
[docs]class Status(Entity):
"""Defins object statutes.
No extra parameters, use the *code* attribute to give a short name for the
status.
A Status object can be compared with a string or unicode value and it will
return if the lower case name or lower case code of the status matches the
lower case form of the given string:
>>> from stalker.core.models import Status
>>> a_status = Status(name="On Hold", "OH")
>>> a_status == "on hold"
True
>>> a_status != "complete"
True
>>> a_status == "oh"
True
>>> a_status == "another status"
False
"""
#----------------------------------------------------------------------
[docs] def __init__(self, **kwargs):
super(Status,self).__init__(**kwargs)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
if isinstance(other, (str, unicode)):
return self.name.lower() == other.lower() or \
self.code.lower() == other.lower()
else:
return super(Status, self).__eq__(other) and \
isinstance(other, Status)
########################################################################
[docs]class StatusList(Entity):
"""Type specific list of :class:`~stalker.core.models.Status` instances.
Holds multiple statuses to be used as a choice list for several other
classes.
A StatusList can only be assigned to only one entity type. So a Project can
only have a suitable StatusList object which is designed for Project
entities.
The list of statuses in StatusList can be accessed by using a list like
indexing and it also supports string indexes only for getting the item,
you can not set an item with string indices:
>>> from stalker.core.models import Status, StatusList
>>> status1 = Status(name="Complete", code="CMPLT")
>>> status2 = Status(name="Work in Progress", code="WIP")
>>> status3 = Status(name="Pending Review", code="PRev")
>>> a_status_list = StatusList(name="Asset Status List",
statuses=[status1, status2, status3],
target_entity_type="Asset")
>>> a_status_list[0]
<Status (Complete, CMPLT)>
>>> a_status_list["complete"]
<Status (Complete, CMPLT)>
>>> a_status_list["wip"]
<Status (Work in Progress, WIP)>
:param statuses: this is a list of status objects, so you can prepare
different StatusList objects for different kind of entities
:param target_entity_type: use this parameter to specify the target entity
type that this StatusList is designed for. It accepts classes or names
of classes.
For example::
from stalker.core.models import Status, StatusList, Project
status_list = [
Status(name="Waiting To Start", code="WTS"),
Status(name="On Hold", code="OH"),
Status(name="In Progress", code="WIP"),
Status(name="Waiting Review", code="WREV"),
Status(name="Approved", code="APP"),
Status(name="Completed", code="CMPLT"),
]
project_status_list = StatusList(
name="Project Status List",
statuses=status_list,
target_entity_type="Project"
)
# or
project_status_list = StatusList(
name="Project Status List",
statuses=status_list,
target_entity_type=Project
)
now with the code above you can not assign the ``project_status_list``
object to any other class than a ``Project`` object.
"""
#----------------------------------------------------------------------
[docs] def __init__(self,
statuses=[],
target_entity_type="",
**kwargs
):
super(StatusList,self).__init__(**kwargs)
self._statuses = self._validate_statuses(statuses)
self._target_entity_type = \
self._validate_target_entity_type(target_entity_type)
#----------------------------------------------------------------------
def _validate_statuses(self, statuses):
"""validates the given status_list
"""
if not isinstance(statuses, list):
raise TypeError("statuses should be an instance of list")
if len(statuses) < 1:
raise ValueError("statuses should not be an empty list")
for item in statuses:
self._validate_status(item)
return ValidatedList(statuses)
#----------------------------------------------------------------------
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("target_entity_type can not be None")
if str(target_entity_type_in)=="":
raise ValueError("target_entity_type can not be empty string")
# 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 _validate_status(self, status_in):
"""validates the given status_in
"""
if not isinstance(status_in, Status):
raise TypeError("all elements must be an instance of Status in "
"the given statuses list")
return status_in
#----------------------------------------------------------------------
[docs] def statuses():
def fget(self):
return self._statuses
def fset(self, statuses):
self._statuses = self._validate_statuses(statuses)
doc = """list of :class:`~stalker.core.models.Status` objects,
showing the possible statuses"""
return locals()
statuses = property(**statuses())
#----------------------------------------------------------------------
[docs] def target_entity_type():
def fget(self):
return self._target_entity_type
doc="""the entity type which this StatusList is valid for, usally it
is set to the TargetClass.entity_type class attribute of the target
class::
from stalker.core.models import Status, StatusList, Asset
# now create a StatusList valid only for assets
status1 = Status(name="Waiting To Start", code="WTS")
status2 = Status(name="Work In Progress", code="WIP")
status3 = Status(name="Complete", code="CMPLT")
"""
return locals()
target_entity_type = property(**target_entity_type())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(StatusList, self).__eq__(other) and \
isinstance(other, StatusList) and \
self.statuses == other.statuses and \
self.target_entity_type == other.target_entity_type
#----------------------------------------------------------------------
def __getitem__(self, key):
"""the indexing attributes for getting item
"""
if isinstance(key, (str, unicode)):
for item in self._statuses:
if item==key:
return item
else:
return self._statuses[key]
#----------------------------------------------------------------------
def __setitem__(self, key, value):
"""the indexing attributes for setting item
"""
self._statuses[key] = self._validate_status(value)
#----------------------------------------------------------------------
def __delitem__(self, key):
"""the indexing attributes for deleting item
"""
del self._statuses[key]
#----------------------------------------------------------------------
def __len__(self):
"""the indexing attributes for getting the length
"""
return len(self._statuses)
########################################################################
[docs]class Link(Entity):
"""Holds data about external links.
Links are all about to give some external information to the current entity
(external to the database, so it can be something on the
:class:`~stalker.core.models.Repository` or in the Web). The
link type is defined by the :class:`~stalker.core.models.LinkType` object
and it can be anything like *General*, *File*, *Folder*, *WebPage*,
*Image*, *ImageSequence*, *Movie*, *Text* etc. (you can also use multiple
:class:`~stalker.core.models.Tag` objects to adding more information,
and filtering back). Again it is defined by the needs of the studio.
:param path: The Path to the link, it can be a path to a file in the file
system, or a web page. Setting path to None or an empty string is not
accepted.
:param filename: The file name part of the link url, for file sequences use
"#" in place of the numerator (`Nuke`_ style). Setting filename to None
or an empty string is not accepted.
.. _Nuke: http://www.thefoundry.co.uk
"""
__strictly_typed__ = True
#----------------------------------------------------------------------
[docs] def __init__(self, path="", filename="", **kwargs):
super(Link, self).__init__(**kwargs)
self._path = self._validate_path(path)
self._filename = self._validate_filename(filename)
#self._type = self._validate_type(type)
#----------------------------------------------------------------------
def _validate_path(self, path_in):
"""validates the given path
"""
if path_in is None:
raise TypeError("path can not be None")
if not isinstance(path_in, (str, unicode)):
raise TypeError("path should be an instance of string or unicode")
if path_in=="":
raise ValueError("path can not be an empty string")
return self._format_path(path_in)
#----------------------------------------------------------------------
def _format_path(self, path_in):
"""formats the path to internal format, which is Linux forward slashes
for path seperation
"""
return path_in.replace("\\", "/")
#----------------------------------------------------------------------
def _validate_filename(self, filename_in):
"""validates the given filename
"""
if filename_in is None:
raise TypeError("filename can not be None")
if not isinstance(filename_in, (str, unicode)):
raise TypeError("filename should be an instance of string or "
"unicode")
if filename_in=="":
raise ValueError("filename can not be an empty string")
return filename_in
#----------------------------------------------------------------------
[docs] def path():
def fget(self):
return self._path
def fset(self, path_in):
self._path = self._validate_path(path_in)
doc="""the path part of the url to the link, it can not be None or an
empty string, it should be a string or unicode"""
return locals()
path = property(**path())
#----------------------------------------------------------------------
[docs] def filename():
def fget(self):
return self._filename
def fset(self, filename_in):
self._filename = self._validate_filename(filename_in)
doc="""the filename part of the url to the link, it can not be None or
an empty string, it should be a string or unicode"""
return locals()
filename = property(**filename())
##----------------------------------------------------------------------
#def type():
#def fget(self):
#return self._type
#def fset(self, type_in):
#self._type = self._validate_type(type_in)
#doc="""the type of the link, it should be a
#:class:`~stalker.core.models.LinkType` object and it can not be
#None"""
#return locals()
#type = property(**type())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Link, self).__eq__(other) and \
isinstance(other, Link) and \
self.path == other.path and \
self.filename == other.filename and \
self.type == other.type
# mixin class imports should be placed after StatusList and Link definitions
from stalker.core.mixins import (ReferenceMixin, ScheduleMixin, StatusMixin,
TaskMixin, ProjectMixin)
########################################################################
[docs]class Booking(Entity):
"""Holds information about the time spend on a specific task by a specific user.
"""
pass
########################################################################
[docs]class Department(Entity):
"""The departments that forms the studio itself.
The informations that a Department object holds is like:
* The members of the department
* The lead of the department
* and all the other things those are inherited from the AuditEntity class
Two Department object considered the same if they have the same name, the
the members list nor the lead info is important, a "Modeling" department
should of course be the same with another department which has the name
"Modeling" again.
so creating a department object needs the following parameters:
:param members: it can be an empty list, so one department can be created
without any member in it. But this parameter should be a list of User
objects.
:param lead: this is a User object, that holds the lead information, a lead
could be in this department but it is not forced to be also a member of
the department. So another departments member can be a lead for another
department. Lead attribute can not be empty or None.
"""
#----------------------------------------------------------------------
[docs] def __init__(self, members=[], lead=None, **kwargs):
super(Department, self).__init__(**kwargs)
self._members = self._validate_members(members)
self._lead = self._validate_lead(lead)
#----------------------------------------------------------------------
def _validate_members(self, members):
"""validates the given members attribute
"""
if not isinstance(members, list):
raise TypeError("members should be a list of "
"stalker.core.models.User instances")
if not all([isinstance(member, User) for member in members]):
raise TypeError("every element in the members list should be "
"an instance of stalker.core.models.User"
" class")
return ValidatedList(members, User)
#----------------------------------------------------------------------
def _validate_lead(self, lead):
"""validates the given lead attribute
"""
if lead is not None:
# the lead should be an instance of User class
if not isinstance(lead, User):
raise TypeError("lead should be an instance of "
"stalker.core.models.User")
return lead
#----------------------------------------------------------------------
[docs] def members():
def fget(self):
return self._members
def fset(self, members):
self._members = self._validate_members(members)
doc = """members are a list of users representing the members of this
department"""
return locals()
members = property(**members())
#----------------------------------------------------------------------
[docs] def lead():
def fget(self):
return self._lead
def fset(self, lead):
self._lead = self._validate_lead(lead)
doc = """lead is the lead of this department, it is a User object"""
return locals()
lead = property(**lead())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Department, self).__eq__(other) and \
isinstance(other, Department)
########################################################################
[docs]class PermissionGroup(SimpleEntity):
"""Manages permission in the system.
A PermissionGroup object maps permission for tasks like Create, Read,
Update, Delete operations in the system to available classes in the system.
It reads the :attr:`~stalker.conf.defaults.CORE_MODEL_CLASSES` list to get
the list of available classes which can be created. It then stores a binary
value for each of the class.
A :class:`~stalker.core.models.User` can be in several
:class:`~stalker.core.models.PermissionGroup`\ s. The combined permission
for an object is calculated with an ``OR`` (``^``) operation. So if one of
the :class:`~stalker.core.models.PermissionGroup`\ s of the
:class:`~stalker.core.models.User` is allowing the action then the user is
allowed to do the operation.
The permissions are stored in a dictionary. The key is the class name and
the value is a 4-bit binary integer value like 0b0001.
+-------------------+--------+------+--------+--------+
| 0b | 0 | 0 | 0 | 0 |
+-------------------+--------+------+--------+--------+
| binary identifier | Create | Read | Update | Delete |
| | Bit | Bit | Bit | Bit |
+-------------------+--------+------+--------+--------+
:param dict permissions: A Python dictionary showing the permissions. The
key is the name of the Class and the value is the permission bit.
NOTE TO DEVELOPERS: a Dictionary-Based Collections should be used in
SQLAlchemy.
"""
#----------------------------------------------------------------------
[docs] def __init__(self, **kwargs):
super(PermissionGroup, self).__init__(**kwargs)
pass
########################################################################
[docs]class Message(Entity, StatusMixin):
"""The base of the messaging system in Stalker
Messages are one of the ways to collaborate in Stalker. The model of the
messages is taken from the e-mail system. So it is pretty similiar to an
e-mail message.
:param from: the :class:`~stalker.core.models.User` object sending the
message
:param to: the list of :class:`~stalker.core.models.User`\ s to
receive this message
:param subject: the subject of the message
:param body: the body of the message
:param in_reply_to: the :class:`~stalker.core.models.Message`
object which this message is a reply to.
:param replies: the list of :class:`~stalker.core.models.Message`
objects which are the direct replies of this message
:param attachments: a list of
:class:`~stalker.core.models.SimpleEntity` objects attached to
this message (so anything can be attached to a message)
"""
#----------------------------------------------------------------------
[docs] def __init__(self, **kwargs):
super(Message, self).__init__(**kwargs)
########################################################################
[docs]class Note(SimpleEntity):
"""To leave notes about the connected node
To leave notes in Stalker use the Note class
:param content: the content of the note
"""
#----------------------------------------------------------------------
[docs] def __init__(self, content="", **kwargs):
super(Note, self).__init__(**kwargs)
self._content = self._validate_content(content)
#----------------------------------------------------------------------
def _validate_content(self, content_in):
"""validates the given content
"""
if content_in is not None and \
not isinstance(content_in, (str, unicode)):
raise TypeError("content should be an instance of string or "
"unicode")
return content_in
#----------------------------------------------------------------------
[docs] def content():
def fget(self):
return self._content
def fset(self, content_in):
self._content = self._validate_content(content_in)
doc = """content is a string representing the content of this Note,
can be given as an empty string or can be even None, but anything other
than None or string or unicode will raise a TypeError"""
return locals()
content = property(**content())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Note, self).__eq__(other) and \
isinstance(other, Note) and \
self.content == other.content
########################################################################
[docs]class Project(Entity, ReferenceMixin, StatusMixin, ScheduleMixin, TaskMixin):
"""All the information about a Project in Stalker is hold in this class.
Project is one of the main classes that will direct the others. A project
in Stalker is a gathering point.
It is mixed with :class:`~stalker.core.mixins.ReferenceMixin`,
:class:`~stalker.core.mixins.StatusMixin`,
:class:`~stalker.core.mixins.ScheduleMixin` and
:class:`~stalker.core.mixins.TaskMixin` to give reference, status, schedule
and task abilities. Please read the individual documentation of each of the
mixins.
The :attr:`~stalker.core.models.Project.users` attributes content is
gathered from all the :class:`~stalker.core.models.Task`\ s of the project
itself and from the :class:`~stalker.core.models.Task`\ s of the
:class:`~stalker.core.models.Sequence`\ s stored in the
:attr:`~stalker.core.models.Project.sequences` attribute, the
:class:`~stalker.core.models.Shot`\ s stored in the
:attr:`~stalker.core.models.Sequence.shots` attribute, the
:class:`~stalker.core.models.Asset`\ s stored in the
:attr:`~stalker.core.models.Project.assets`. It is a read only attribute.
:param lead: The lead of the project. Default value is None.
:type lead: :class:`~stalker.core.models.User`
:param list sequences: The sequences of the project, it should be a list of
:class:`~stalker.core.models.Sequence` instances, if set to None it is
converted to an empty list. Default value is an empty list.
:param list assets: The assets used in this project, it should be a list of
:class:`~stalker.core.models.Asset` instances, if set to None it is
converted to an empty list. Default value is an empty list.
:param image_format: The output image format of the project. Default
value is None.
:type image_format: :class:`~stalker.core.models.ImageFormat`
:param float fps: The FPS of the project, it should be a integer or float
number, or a string literal which can be correctly converted to a float.
Default value is 25.0.
:param type: The type of the project. Default value is None.
:type type: :class:`~stalker.core.models.ProjectType`
:param structure: The structure of the project. Default value is None
:type structure: :class:`~stalker.core.models.Structure`
:param repository: The repository that the project files are going to be
stored in. You can not create the project folder structure if the project
doesn't have a connection to a
:class:`~stalker.core.models.Repository`. Default value is
None.
:type repository: :class:`~stalker.core.models.Repository`.
:param bool is_stereoscopic: a bool value, showing if the project is going
to be a stereo 3D project, anything given as the argument will be
converted to True or False. Default value is False.
:param float display_width: the width of the display that the output of the
project is going to be displayed (very unnecessary if you are not using
stereo 3D setup). Should be an int or float value, negative values
converted to the positive values. Default value is 1.
"""
__strictly_typed__ = True
#----------------------------------------------------------------------
[docs] def __init__(self,
lead=None,
repository=None,
structure=None,
sequences=[],
assets=[],
image_format=None,
fps=25.0,
is_stereoscopic=False,
display_width=1.0,
**kwargs):
super(Project, self).__init__(**kwargs)
# call the mixin __init__ methods
ReferenceMixin.__init__(self, **kwargs)
StatusMixin.__init__(self, **kwargs)
ScheduleMixin.__init__(self, **kwargs)
TaskMixin.__init__(self, **kwargs)
self._lead = self._validate_lead(lead)
self._users = []
self._repository = self._validate_repository(repository)
self._structure = self._validate_structure(structure)
self._sequences = self._validate_sequences(sequences)
self._assets = self._validate_assets(assets)
# do not persist this attribute
self._project_duration = self._due_date - self._start_date
self._image_format = self._validate_image_format(image_format)
self._fps = self._validate_fps(fps)
self._is_stereoscopic = bool(is_stereoscopic)
self._display_width = self._validate_display_width(display_width)
#----------------------------------------------------------------------
def _validate_assets(self, assets_in):
"""validates the given assets_in lists
"""
if assets_in is None:
assets_in = []
if not isinstance(assets_in, list):
raise TypeError("assets should be a list of "
"stalker.core.models.Assets instances")
if not all([isinstance(element, Asset)
for element in assets_in]):
raise TypeError("the elements in assets lists should be all "
"stalker.core.models.Asset instances")
return ValidatedList(assets_in)
#----------------------------------------------------------------------
def _validate_display_width(self, display_width_in):
"""validates the given display_width_in value
"""
return abs(float(display_width_in))
#----------------------------------------------------------------------
def _validate_fps(self, fps_in):
"""validates the given fps_in value
"""
fps_in = float(fps_in)
return fps_in
#----------------------------------------------------------------------
def _validate_image_format(self, image_format_in):
"""validates the given image format
"""
if image_format_in is not None and \
not isinstance(image_format_in, ImageFormat):
raise TypeError("the image_format should be an instance of "
"stalker.core.models.ImageFormat")
return image_format_in
#----------------------------------------------------------------------
def _validate_lead(self, lead_in):
"""validates the given lead_in value
"""
if lead_in is not None:
if not isinstance(lead_in, User):
raise TypeError("lead must be an instance of "
"stalker.core.models.User")
return lead_in
#----------------------------------------------------------------------
def _validate_repository(self, repository_in):
"""validates the given repository_in value
"""
if repository_in is not None and \
not isinstance(repository_in, Repository):
raise TypeError("the repsoitory should be an instance of "
"stalker.core.models.Repository")
return repository_in
#----------------------------------------------------------------------
def _validate_sequences(self, sequences_in):
"""validates the given sequences_in value
"""
if sequences_in is None:
sequences_in = []
if not isinstance(sequences_in, list):
raise TypeError("sequences should be a list of "
"stalker.core.models.Sequence instances")
if not all([isinstance(seq, Sequence)
for seq in sequences_in]):
raise TypeError("sequences should be a list of "
"stalker.core.models.Sequence instances")
return ValidatedList(sequences_in, Sequence)
#----------------------------------------------------------------------
def _validate_structure(self, structure_in):
"""validates the given structure_in vlaue
"""
if structure_in is not None:
if not isinstance(structure_in, Structure):
raise TypeError("structure should be an instance of "
"stalker.core.models.Structure")
return structure_in
##----------------------------------------------------------------------
#def _validate_users(self, users_in):
#"""validates the given users_in value
#"""
#if users_in is None:
#users_in = []
#if not isinstance(users_in, list):
#raise TypeError("users should be a list of "
#"stalker.core.models.User instances")
#if not all([isinstance(element, User) \
#for element in users_in]):
#raise TypeError("users should be a list containing instances of "
#":class:`~stalker.core.models.User`")
#return ValidatedList(users_in)
#----------------------------------------------------------------------
[docs] def assets():
def fget(self):
return self._assets
def fset(self, assets_in):
self._assets = self._validate_assets(assets_in)
doc = """the list of assets created in this project"""
return locals()
assets = property(**assets())
#----------------------------------------------------------------------
[docs] def display_width():
def fget(self):
return self._display_width
def fset(self, display_width_in):
self._display_width = \
self._validate_display_width(display_width_in)
doc = """the target display width that this project is going to be
displayed on, meaningfull if this project is a stereoscopic project"""
return locals()
display_width = property(**display_width())
#----------------------------------------------------------------------
[docs] def fps():
def fget(self):
return self._fps
def fset(self, fps_in):
self._fps = self._validate_fps(fps_in)
doc = """the fps of the project, it is a float value, any other types
will be converted to float. The default value is 25.0"""
return locals()
fps = property(**fps())
#----------------------------------------------------------------------
image_format = property(**image_format())
#----------------------------------------------------------------------
[docs] def is_stereoscopic():
def fget(self):
return self._is_stereoscopic
def fset(self, is_stereoscopic_in):
self._is_stereoscopic = bool(is_stereoscopic_in)
doc= """True if the project is a stereoscopic project"""
return locals()
is_stereoscopic = property(**is_stereoscopic())
#----------------------------------------------------------------------
[docs] def lead():
def fget(self):
return self._lead
def fset(self, lead_in):
self._lead = self._validate_lead(lead_in)
doc = """the lead of the project, should be an instance of
:class:`~stalker.core.models.User`, also can set to None"""
return locals()
lead = property(**lead())
#----------------------------------------------------------------------
[docs] def repository():
def fget(self):
return self._repository
def fset(self, repository_in):
self._repository = self._validate_repository(repository_in)
doc = """the repository that this project should reside, should be an
instance of :class:`~stalker.core.models.Repository`, can
not be skipped"""
return locals()
repository = property(**repository())
#----------------------------------------------------------------------
[docs] def sequences():
def fget(self):
return self._sequences
def fset(self, sequences_in):
self._sequences = self._validate_sequences(sequences_in)
doc = """the sequences contained in this project, should be a list
containing all of :class:`~stalker.core.models.Sequence`
instances, when set to None it is converted to an empty list"""
return locals()
sequences = property(**sequences())
#----------------------------------------------------------------------
[docs] def structure():
def fget(self):
return self._structure
def fset(self, structure_in):
self._structure = self._validate_structure(structure_in)
doc = """The structure of the project. Should be an instance of
:class:`~stalker.core.models.Structure` class"""
return locals()
structure = property(**structure())
#----------------------------------------------------------------------
[docs] def users():
def fget(self):
#return self._users
self._users = []
# project tasks
for task in self.tasks:
self._users.extend(task.resources)
# sequence tasks
for seq in self.sequences:
for task in seq.tasks:
self._users.extend(task.resources)
# shot tasks
for shot in seq.shots:
for task in shot.tasks:
self._users.extend(task.resources)
# asset tasks
for asset in self.assets:
for task in asset.tasks:
self._users.extend(task.resources)
self._users = list(set(self._users))
return self._users
doc = """The users assigned to this project.
This is a list of :class:`~stalker.core.models.User` instances. All the
elements are gathered from all the
:class:`~stalker.core.models.Task`\ s of the project itself and from
:class:`~stalker.core.models.Sequence`\ s,
:class:`~stalker.core.models.Shot`\ s,
:class:`~stalker.core.models.Asset`\ s.
"""
return locals()
users = property(**users())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Project, self).__eq__(other) and \
isinstance(other, Project)
########################################################################
[docs]class Repository(Entity):
"""Repository is a class to hold repository server data. A repository is a
network share that all users have access to.
A studio can create several repositories, for example, one for movie
projects and one for commercial projects.
A repository also defines the default paths for linux, windows and mac
fileshares.
:param linux_path: shows the linux path of the repository root, should be a
string
:param osx_path: shows the mac osx path of the repository root, should be a
string
:param windows_path: shows the windows path of the repository root, should
be a string
"""
#----------------------------------------------------------------------
[docs] def __init__(self, linux_path="", windows_path="", osx_path="", **kwargs):
super(Repository, self).__init__(**kwargs)
self._linux_path = self._validate_linux_path(linux_path)
self._windows_path = self._validate_windows_path(windows_path)
self._osx_path = self._validate_osx_path(osx_path)
#----------------------------------------------------------------------
def _validate_linux_path(self, linux_path_in):
"""validates the given linux path
"""
if not isinstance(linux_path_in, (str, unicode)):
raise TypeError("linux_path should be an instance of string or "
"unicode")
return linux_path_in
#----------------------------------------------------------------------
def _validate_osx_path(self, osx_path_in):
"""validates the given osx path
"""
if not isinstance(osx_path_in, (str, unicode)):
raise TypeError("osx_path should be an instance of string or "
"unicode")
return osx_path_in
#----------------------------------------------------------------------
def _validate_windows_path(self, windows_path_in):
"""validates the given windows path
"""
if not isinstance(windows_path_in, (str, unicode)):
raise TypeError("windows_path should be an instance of string or "
"unicode")
return windows_path_in
#----------------------------------------------------------------------
[docs] def linux_path():
def fget(self):
return self._linux_path
def fset(self, linux_path_in):
self._linux_path = self._validate_linux_path(linux_path_in)
doc = """property that helps to set and get linux_path values"""
return locals()
linux_path = property(**linux_path())
#----------------------------------------------------------------------
[docs] def osx_path():
def fget(self):
return self._osx_path
def fset(self, osx_path_in):
self._osx_path = self._validate_osx_path(osx_path_in)
doc = """property that helps to set and get osx_path values"""
return locals()
osx_path = property(**osx_path())
#----------------------------------------------------------------------
[docs] def windows_path():
def fget(self):
return self._windows_path
def fset(self, windows_path_in):
self._windows_path = self._validate_windows_path(windows_path_in)
doc = """property that helps to set and get windows_path values"""
return locals()
windows_path = property(**windows_path())
#----------------------------------------------------------------------
[docs] def path():
def fget(self):
# return the proper value according to the current os
platform_system = platform.system()
if platform_system == "Linux":
return self.linux_path
elif platform_system == "Windows":
return self.windows_path
elif platform_system == "Darwin":
return self.osx_path
doc = """property that helps to get path for the current os"""
return locals()
path = property(**path())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Repository, self).__eq__(other) and \
isinstance(other, Repository) and \
self.linux_path == other.linux_path and \
self.osx_path == other.osx_path and \
self.windows_path == other.windows_path
########################################################################
[docs]class Sequence(Entity,
ReferenceMixin,
StatusMixin,
ScheduleMixin,
TaskMixin,
ProjectMixin):
"""Stores data about Sequences.
Sequences are holders of the :class:`~stalker.core.models.Shot` objects.
They orginize the conceptual data with another level of complexity.
:param lead: The lead of this Seuqence. The default value is None.
:type lead: :class:`~stalker.core.models.User`
"""
#----------------------------------------------------------------------
[docs] def __init__(self,
shots=[],
lead=None,
**kwargs
):
super(Sequence, self).__init__(**kwargs)
# call the mixin __init__ methods
ReferenceMixin.__init__(self, **kwargs)
StatusMixin.__init__(self, **kwargs)
ScheduleMixin.__init__(self, **kwargs)
TaskMixin.__init__(self, **kwargs)
ProjectMixin.__init__(self, **kwargs)
self._lead = self._validate_lead(lead)
self._shots = self._validate_shots(None)
#----------------------------------------------------------------------
def _validate_lead(self, lead_in):
"""validates the given lead_in value
"""
if lead_in is not None:
if not isinstance(lead_in, User):
raise TypeError("lead should be instance of "
"stalker.core.models.User")
return lead_in
#----------------------------------------------------------------------
def _validate_shots(self, shots_in):
"""validates the given shots_in value
"""
if shots_in is None:
shots_in = []
if not isinstance(shots_in, list):
raise TypeError("shots should be list containing "
"stalker.core.models.Shot instances")
for element in shots_in:
if not isinstance(element, Shot):
raise TypeError("every item in the shots list should be an "
"instance of stalker.core.models.Shot")
return ValidatedList(shots_in, Shot)
#----------------------------------------------------------------------
[docs] def lead():
def fget(self):
return self._lead
def fset(self, lead_in):
self._lead = self._validate_lead(lead_in)
doc = """The lead of this sequence object."""
return locals()
lead = property(**lead())
#----------------------------------------------------------------------
[docs] def shots():
def fget(self):
return self._shots
def fset(self, shots_in):
self._shots = self._validate_shots(shots_in)
doc = """The shots of this sequence object."""
return locals()
shots = property(**shots())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Sequence, self).__eq__(other) and \
isinstance(other, Sequence)
########################################################################
[docs]class Shot(Entity, ReferenceMixin, StatusMixin, TaskMixin):
"""Manages Shot related data.
Because most of the shots in different projects are going to have the same
name, which is a kind of a code like SH001, SH012A etc., and in Stalker you
can not have two entities with the same name if their types are also
matching, to guarantee all the shots are going to have different names the
:attr:`~stalker.core.models.Shot.name` attribute of the Shot instances are
automatically set to a randomly generated **uuid4** sequence.
But there is no such rule for the
:attr:`~stalker.core.models.Shot.code` attribute, which should be used to
give shot codes to individual shots.
Two shots with the same :attr:`~stalker.core.models.Shot.code` can not be
assigned to the same :class:`~stalker.core.models.Sequence`.
The :attr:`~stalker.core.models.Shot.cut_out` and
:attr:`~stalker.core.models.Shot.cut_duration` attributes effects each
other. Setting the :attr:`~stalker.core.models.Shot.cut_out` will change
the :attr:`~stalker.core.models.Shot.cut_duration` and setting the
:attr:`~stalker.core.models.Shot.cut_duration` will change the
:attr:`~stalker.core.models.Shot.cut_out` value. The default value of the
:attr:`~stalker.core.models.Shot.cut_out` attribute is calculated from the
:attr:`~stalker.core.models.Shot.cut_in` and
:attr:`~stalker.core.models.Shot.cut_duration` attributes. If both
:attr:`~stalker.core.models.Shot.cut_out` and
:attr:`~stalker.core.models.Shot.cut_duration` arguments are set to None,
the :attr:`~stalker.core.models.Shot.cut_duration` defaults to 100 and
:attr:`~stalker.core.models.Shot.cut_out` will be set to
:attr:`~stalker.core.models.Shot.cut_in` +
:attr:`~stalker.core.models.Shot.cut_duration`. So the priority of the
attributes are as follows:
:attr:`~stalker.core.models.Shot.cut_in` >
:attr:`~stalker.core.models.Shot.cut_duration` >
:attr:`~stalker.core.models.Shot.cut_out`
For still images (which can be also managed by shots) the
:attr:`~stalker.core.models.Shot.cut_in` and
:attr:`~stalker.core.models.Shot.cut_out` can be set to the same value
so the :attr:`~stalker.core.models.Shot.cut_duration` can be set to zero.
:param sequence: The :class:`~stalker.core.models.Sequence` that this shot
blengs to. A shot can only be created with a
:class:`~stalker.core.models.Sequence` instance, so it can not be None.
The shot itself will be added to the
:attr:`~stalker.core.models.Sequence.shots` list of the given sequence.
:type sequence: :class:`~stalker.core.models.Sequence`
:param integer cut_in: The in frame number that this shot starts. The
default value is 1. When the ``cut_in`` is bigger then
``cut_out``, the :attr:`~stalker.core.models.Shot.cut_out` attribute is
set to :attr:`~stalker.core.models.Shot.cut_in` + 1.
:param integer cut_duration: The duration of this shot in frames. It should
be zero or a positive integer value (natural number?) or . The default
value is None.
:param integer cut_out: The out frame number that this shot ends. If it is
given as a value lower then the ``cut_in`` parameter, then the
:attr:`~stalker.core.models.Shot.cut_out` will be set to the same value
with :attr:`~stalker.core.models.Shot.cut_in` and the
:attr:`~stalker.core.models.Shot.cut_duration` attribute will be set to
1. Can be skipped. The default value is None.
"""
# fix creation with __new__
_cut_out = None
#----------------------------------------------------------------------
[docs] def __init__(self,
code=None,
sequence=None,
cut_in=1,
cut_out=None,
cut_duration=None,
**kwargs):
kwargs["name"] = code
super(Shot, self).__init__(**kwargs)
ReferenceMixin.__init__(self, **kwargs)
StatusMixin.__init__(self, **kwargs)
TaskMixin.__init__(self, **kwargs)
# set the code
self._code = self._validate_code(code)
# set the name atribute
import uuid
self._name = uuid.uuid4().hex
self._sequence = self._validate_sequence(sequence)
# add the shot to the sequences shot list
self._sequence.shots.append(self)
self._cut_in = cut_in
self._cut_duration = cut_duration
self._cut_out = cut_out
self._update_cut_info(cut_in, cut_duration, cut_out)
#----------------------------------------------------------------------
def __repr__(self):
"""the representation of the Shot
"""
return "<%s (%s, %s)>" % (self.entity_type, self.code, self.code)
#----------------------------------------------------------------------
def __eq__(self, other):
"""equality operator
"""
# __eq__ always returns false but to be safe the code will be added
# here
return self.code == other.code and isinstance(other, Shot) and \
self.sequence == other.sequence
#----------------------------------------------------------------------
def _update_cut_info(self, cut_in, cut_duration, cut_out):
"""updates the cut_in, cut_duration and cut_out attributes
"""
# validate all the values
self._cut_in = self._validate_cut_in(cut_in)
self._cut_duration = self._validate_cut_duration(cut_duration)
self._cut_out = self._validate_cut_out(cut_out)
if self._cut_in is None:
self._cut_in = 1
if self._cut_out is not None:
if self._cut_in > self._cut_out:
# just update cut_duration
self._cut_duration = 1
#else:
#self._cut_o
if self._cut_duration is None or self._cut_duration <= 0:
self._cut_duration = 1
self._cut_out = self._cut_in + self._cut_duration - 1
##----------------------------------------------------------------------
#def _validate_assets(self, assets_in):
#"""validates the given assets_in value
#"""
#if assets_in is None:
#assets_in = []
#if not isinstance(assets_in, list):
#raise TypeError("assets should be an instance of list")
#for item in assets_in:
#if not isinstance(item, Asset):
#raise TypeError("all the items in the assets list should be"
#"an instance of stalker.core.models.Asset")
#return ValidatedList(assets_in, Asset)
#----------------------------------------------------------------------
def _validate_code(self, code_in):
"""validates the given code value
"""
# check if the code_in is None or empty string
if code_in is None:
raise TypeError("the code can not be None")
if code_in=="":
raise ValueError("the code can not be empty string")
return self._condition_code(str(code_in))
#----------------------------------------------------------------------
def _validate_cut_duration(self, cut_duration_in):
"""validates the given cut_duration value
"""
if cut_duration_in is not None and \
not isinstance(cut_duration_in, int):
raise TypeError("cut_duration should be an instance of int")
#if cut_duration_in is None or cut_duration_in <= 0:
#if self._cut_out is None:
#raise ValueError("cut_duration can not be None or negative"
#"when the cut_out is also undefined")
#else:
#cut_duration_in = self._cut_out - self._cut_in + 1
return cut_duration_in
#----------------------------------------------------------------------
def _validate_cut_in(self, cut_in_in):
"""validates the given cut_in_in value
"""
if cut_in_in is not None:
if not isinstance(cut_in_in, int):
raise TypeError("cut_in should be an instance of int")
return cut_in_in
#----------------------------------------------------------------------
def _validate_cut_out(self, cut_out_in):
"""validates the given cut_out_in value
"""
if cut_out_in is not None:
if not isinstance(cut_out_in, int):
raise TypeError("cut_out should be an instance of int")
return cut_out_in
#----------------------------------------------------------------------
def _validate_sequence(self, sequence_in):
"""validates the given sequence_in value
"""
if not isinstance(sequence_in, Sequence):
raise TypeError("the sequence should be an instance of"
"stalker.core.models.Sequence instance")
for shot in sequence_in.shots:
#assert(isinstance(shot, Shot))
if self.code == shot.code:
raise ValueError("the given sequence already has a shot with "
"a code %s" % self.code)
return sequence_in
##----------------------------------------------------------------------
#def assets():
#def fget(self):
#return self._assets
#def fset(self, assets_in):
#self._assets = self._validate_assets(assets_in)
#doc = """The list of :class:`~stalker.core.models.Asset`\ s used in this shot.
#When it is set to None, it defaults to the default value and the
#default value is an empty list."""
#return locals()
#assets = property(**assets())
#----------------------------------------------------------------------
[docs] def code():
def fget(self):
return self._code
def fset(self, code_in):
self._code = self._validate_code(code_in)
doc = """The code of this :class:`~stalker.core.models.Shot`.
Contrary to the original attribute from the inherited parent
(:attr:`~stalker.core.models.SimpleEntity.code`), the code attribute
can not be set to None or empty string."""
return locals()
code = property(**code())
#----------------------------------------------------------------------
[docs] def cut_duration():
def fget(self):
return self._cut_duration
def fset(self, cut_duration_in):
#self._cut_duration = self._validate_cut_duration(cut_duration_in)
# also set the cut_out
#self._cut_out = self._cut_in + self._cut_duration - 1
self._update_cut_info(self._cut_in, cut_duration_in, self._cut_out)
doc = """The duration of this shot in frames.
It should be a positive integer value. If updated also updates the
:attr:`~stalker.core.models.Shot.cut_duration` attribute. The default
value is 100."""
return locals()
cut_duration = property(**cut_duration())
#----------------------------------------------------------------------
[docs] def cut_in():
def fget(self):
return self._cut_in
def fset(self, cut_in_in):
#self._cut_in = self._validate_cut_in(cut_in_in)
#if self._cut_in > self._cut_out:
## udpate the cut_out value
#self._cut_out = self._cut_in + 1
#self._update_cut_duration()
self._update_cut_info(cut_in_in, self._cut_duration, self._cut_out)
doc = """The in frame number taht this shot starts.
The default value is 1. When the cut_in is bigger then
:attr:`~stalker.core.models.Shot.cut_out`, the
:attr:`~stalker.core.models.Shot.cut_out` value is update to
:attr:`~stalker.core.models.Shot.cut_in` + 1."""
return locals()
cut_in = property(**cut_in())
#----------------------------------------------------------------------
[docs] def cut_out():
def fget(self):
if self._cut_out is None:
self._update_cut_info(self._cut_in, self._cut_duration, None)
return self._cut_out
def fset(self, cut_out_in):
#self._cut_out = self._validate_cut_out(cut_out_in)
#if self._cut_out is None:
#self._cut_out = self._cut_in + self._cut_duration - 1
#self._update_cut_duration()
self._update_cut_info(self._cut_in, self._cut_duration, cut_out_in)
doc = """The out frame number that this shot ends.
When the :attr:`~stalker.core.models.Shot.cut_out` is set to a value
lower than :attr:`~stalker.core.models.Shot.cut_in`,
:attr:`~stalker.core.models.Shot.cut_out` will be updated to
:attr:`~stalker.core.models.Shot.cut_in` + 1. The default value is
:attr:`~stalker.core.models.Shot.cut_in` +
:attr:`~stalker.core.models.Shot.cut_duration`."""
return locals()
cut_out = property(**cut_out())
#----------------------------------------------------------------------
[docs] def name():
def fget(self):
return self._name
def fset(self, name_in):
pass
doc = """Name of this Shot.
Different than other :class:`~stalker.core.models.SimpleEntity`
derivatives, the :class:`~stalker.core.models.Shot` classes
:attr:`~stalker.core.models.Shot.name` attribute is read-only. And the
stored value is a uuid4 sequence.
"""
return locals()
name = property(**name())
#----------------------------------------------------------------------
[docs] def sequence():
def fget(self):
return self._sequence
doc = """The :class:`~stalker.core.models.Sequence` instance that this :class:`~stalker.core.models.Shot` instance belongs to.
It is a read-only attribute."""
return locals()
sequence = property(**sequence())
########################################################################
[docs]class Structure(Entity):
"""A structure object is the place to hold data about how the physical files are arranged in the :class:`~stalker.core.models.Repository`.
Structures are generally owned by :class:`~stalker.core.models.Project`
objects. Whenever a :class:`~stalker.core.models.Project` is physicaly
created, project folders are created by looking at
:attr:`~stalker.core.models.Structure.custom_template` of the
:class:`~stalker.core.models.Structure`, the
:class:`~stalker.core.models.Project` object is generally given to the
:class:`~stalker.core.models.Structure`. So it is possible to use a
variable like "{{project}}" or derived variables like::
{% for seq in project.sequences %}
{{do something here}}
:param templates: A list of
:class:`~stalker.core.models.FilenameTemplate`\ s which
defines a specific template for the given
:attr:`~stalker.core.models.FilenameTemplate.target_entity_type`\ s.
:type templates: list of :class:`~stalker.core.models.FilenameTemplate`\ s
:param str custom_template: A string containing several lines of folder
names. The folders are relative to the
:class:`~stalker.core.models.Project` root. It can also contain a Jinja2
Template code. Which will be rendered to show the list of folders to be
created with the project. The Jinja2 Template is going to have the
{{project}} variable. The important point to be careful about is to list
all the custom folders of the project in a new line in this string. For
example a :class:`~stalker.core.models.Structure` for a
:class:`~stalker.core.models.Project` can have the following
:attr:`~stalker.core.models.Structure.custom_template`::
ASSETS
{% for asset in project.assets %}
{% set asset_root = 'ASSETS/' + asset.code %}
{{asset_root}}
{% for task in asset.tasks %}
{% set task_root = asset_root + '/' + task.code %}
{{task_root}}
SEQUENCES
{% for seq in project.sequences %}}
{% set seq_root = 'SEQUENCES/' + {{seq.code}} %}
{{seq_root}}/Edit
{{seq_root}}/Edit/AnimaticStoryboard
{{seq_root}}/Edit/Export
{{seq_root}}/Storyboard
{% for shot in seq.shots %}
{% set shot_root = seq_root + '/SHOTS/' + shot.code %}
{{shot_root}}
{% for task in shot.tasks %}
{% set task_root = shot_root + '/' + task.code %}
{{task_root}}
The above example has gone far beyond deep than it is needed, where it
started to define paths for :class:`~stalker.core.models.Asset`\ s. Even
it is possible to create a :class:`~stalker.core.models.Project`
structure like that, in general it is unnecessary. Because the above
folders are going to be created but they are probably going to be empty
for a while, because the :class:`~stalker.core.models.Asset`\ s are not
created yet (or in fact no :class:`~stalker.core.models.Version`\ s are
created for the :class:`~stalker.core.models.Task`\ s). Anyway, it is
much suitable and desired to create this details by using
:class:`~stalker.core.models.FilenameTemplate` objects. Which are
spesific to certain
:attr:`~stalker.core.FilenameTemplate.target_entity_type`\ s. And by
using the :attr:`~stalker.core.models.Structure.custom_template`
attribute, Stalker can not place any source or output file of a
:class:`~stalker.core.models.Version` in the
:class:`~stalker.core.models.Repository` where as it can by using
:class:`~stalker.core.models.FilenameTemplate`\ s.
But for certain types of :class:`~stalker.core.models.Task`\ s it is may
be good to previously create the folder structure just because in certain
environments (programs) it is not possible to run a Python code that will
place the file in to the Repository like in Photoshop.
The ``custom_template`` parameter can be None or an empty string if it is
not needed. Be carefull not to pass a variable other than a string or
unicode because it will use the string representation of the given
variable.
A :class:`~stalker.core.models.Structure` can not be created without a
``type`` (__strictly_typed__ = True). By giving a ``type`` to the
:class:`~stalker.core.models.Strucutre`, you can create one structure for
**Commmercials** and another project structure for **Movies** and another
one for **Print** projects etc. and can reuse them with new
:class:`~stalker.core.models.Project`\ s.
"""
__strictly_typed__ = True
#----------------------------------------------------------------------
[docs] def __init__(self, templates=None, custom_template=None, **kwargs):
super(Structure, self).__init__(**kwargs)
self._templates = self._validate_templates(templates)
self._custom_template= self._validate_custom_template(custom_template)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Structure, self).__eq__(other) and \
isinstance(other, Structure) and \
self.templates == other.templates and \
self.custom_template == other.custom_template
#----------------------------------------------------------------------
def _validate_custom_template(self, custom_template_in):
"""validates the given custom_template value
"""
return custom_template_in
#----------------------------------------------------------------------
def _validate_templates(self, templates_in):
"""validates the given templates value
"""
if templates_in is None:
templates_in = []
if not isinstance(templates_in, list):
raise TypeError("templates should be a list of "
"stalker.core.models.FilenameTemplate instances")
if not all(isinstance(element, FilenameTemplate)
for element in templates_in):
raise TypeError("all the elements in the templates should be a "
"list of stalker.core.models.FilenameTemplate "
"instances")
return ValidatedList(templates_in)
#----------------------------------------------------------------------
[docs] def custom_template():
def fget(self):
return self._custom_template
def fset(self, custom_template_in):
self._custom_template =\
self._validate_custom_template(custom_template_in)
doc = """A string value, which is a list of folder names.
It can also contain Jinja2 direction. See the\
:class:`~stalker.core.models.Structure` documentaion for more details.
"""
return locals()
custom_template = property(**custom_template())
#----------------------------------------------------------------------
[docs] def templates():
def fget(self):
return self._templates
def fset(self, templates_in):
self._templates = self._validate_templates(templates_in)
doc = """A list of :class:`~stalker.core.models.FilenameTemplate` instances.
This list shows possible filenaming conventions created along with this
:class:`~stalker.core.models.Structure` instance. It should be a list
of :class:`~stalker.core.models.FilenameTemplate` instances.
"""
return locals()
templates = property(**templates())
########################################################################
[docs]class Tag(SimpleEntity):
"""the tag class
"""
#----------------------------------------------------------------------
[docs] def __init__(self, **kwargs):
super(Tag, self).__init__(**kwargs)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Tag, self).__eq__(other) and isinstance(other, Tag)
#----------------------------------------------------------------------
def __ne__(self, other):
"""the inequality operator
"""
return not self.__eq__(other)
########################################################################
[docs]class Asset(Entity, ReferenceMixin, StatusMixin, TaskMixin, ProjectMixin):
"""The Asset class is the whole idea behind Stalker.
*Assets* are containers of :class:`~stalker.core.models.Task`\ s. And
:class:`~stalker.core.models.Task`\ s are the smallest meaningful part that
should be accomplished to complete the
:class:`~stalker.core.models.Project`.
An example could be given as follows; you can create an asset for one of
the characters in your project. Than you can divide this character asset in
to :class:`~stalker.core.models.Task`\ s. These
:class:`~stalker.core.models.Task`\ s can be defined by the type of the
:class:`~stalker.core.models.Asset`, which is a
:class:`~stalker.core.models.Type` object created specifically for
:class:`~stalker.core.models.Asset` (ie. has its
:attr:`~stalker.core.models.Type.target_entity_type` set to "Asset"),
"""
__strictly_typed__ = True
_shots = None
#----------------------------------------------------------------------
[docs] def __init__(self, **kwargs):
super(Asset, self).__init__(**kwargs)
# call the mixin init methods
ReferenceMixin.__init__(self, **kwargs)
StatusMixin.__init__(self, **kwargs)
TaskMixin.__init__(self, **kwargs)
ProjectMixin.__init__(self, **kwargs)
self._shots = self._validate_shots(None)
#----------------------------------------------------------------------
def _validate_shots(self, shots_in):
"""validates the given shots_in value
"""
if shots_in is None:
shots_in = []
if not isinstance(shots_in, list):
raise TypeError("shots should be set to a list of "
"stalker.core.models.Shot instances")
if not all([isinstance(shot, Shot)
for shot in shots_in]):
raise TypeError("shots should be set to a list of "
"stalker.core.models.Shot objects")
return ValidatedList(shots_in, Shot)
#----------------------------------------------------------------------
[docs] def shots():
def fget(self):
return self._shots
def fset(self, shots_in):
self._shots = self._validate_shots(shots_in)
doc = """The :class:`~stalker.core.models.Shot`\ s that this :class:`~stalker.core.models.Asset` has been used in.
"""
return locals()
shots = property(**shots())
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Asset, self).__eq__(other) and \
isinstance(other, Asset) and self.type == other.type
########################################################################
[docs]class Version(Entity, StatusMixin):
"""The connection to the filesystem.
A :class:`~stalker.core.models.Version` holds information about the
incarnations of the files in the :class:`~stalker.core.models.Repository`.
So if one creates a new version for a file or a sequences of file for a
:class:`~stalker.core.models.Task` then the information is hold in the
:class:`~stalker.core.models.Version` instance.
:param str take: A short string holding the current take name. Can be
any alphanumeric value (a-zA-Z0-0_)
:param int version: An integer value showing the current version number.
:param source_file: A :class:`~stalker.core.models.Link` instance, showing
the source file of this version.
:type source_file: :class:`~stalker.core.models.Link`
:param outputs: A list of :class:`~stalker.core.models.Link` instances,
holding the outputs of the current version.
:type outputs: list of :class:`~stalker.core.models.Link` instances
:param task: A :class:`~stalker.core.models.Task` instance showing the
owner of this Version.
:type task: :class:`~stalker.core.models.Task`
"""
#:param review: A list of :class:`~stalker.core.models.Comment` instances,
#holding all the comments made for this Version.
#:type review: :class:`~stalker.core.models.Stalker`
#:param bool published: A bool value shows if this version is published or
#not.
pass
########################################################################
[docs]class Task(Entity, StatusMixin, ScheduleMixin, ProjectMixin):
"""Manages Task related data.
Tasks are the smallest meaningful part that should be accomplished to
complete the a :class:`~stalker.core.models.Project`.
In Stalker, currently these items supports Tasks:
* :class:`~stalker.core.models.Project`
* :class:`~stalker.core.models.Sequence`
* :class:`~stalker.core.models.Asset`
* :class:`~stalker.core.models.Shot`
If you want to have your own class to be *taskable* user the
:class:`~stalker.core.mixins.TaskMixin` to add the ability to connect a
:class:`~stalker.core.models.Task` to it.
The Task class itself is mixed with
:class:`~stalker.core.mixins.StatusMixin` and
:class:`~stalker.core.mixins.ScheduleMixin`. To be able to give the
:class:`~stalker.core.models.Task` a *Status* and a *start* and *end* time.
:param int priority: It is a number between 0 to 1000 which defines the
priority of the :class:`~stalker.core.models.Task`. The higher the value
the higher its priority. The default value is 500.
:param resources: The :class:`~stalker.core.models.User`\ s assigned to
this :class:`~stalker.core.models.Task`. A
:class:`~stalker.core.models.Task` without any resource can not be
scheduled.
:type resources: list of :class:`~stalker.core.models.User`
:param effort: The total effort that needs to be spend to complete this
Task. Can be used to create an initial bid of how long this task going to
take. The effort is equaly divided to the assigned resources. So if the
effort is 10 days and 2 :attr:`~stalker.core.models.Task.resources` is
assigned then the :attr:`~stalker.core.models.Task.duration` of the task
is going to be 5 days (if both of the resources are free to work). The
default value is stalker.conf.defaults.DEFAULT_TASK_DURATION.
The effort argument defines the
:attr:`~stalker.core.models.Task.duration` of the task. Every resource is
counted equally effective and the
:attr:`~stalker.core.models.Task.duration` will be calculated by the
simple formula:
.. math::
{duration} = \\frac{{effort}}{n_{resources}}
And changing the :attr:`~stalker.core.models.Task.duration` will also
effect the :attr:`~stalker.core.models.Task.effort` spend. The
:attr:`~stalker.core.models.Task.effort` will be calculated with the
formula:
.. math::
{effort} = {duration} \\times {n_{resources}}
:type effort: datetime.timedelta
:param depends: A list of :class:`~stalker.core.models.Task`\ s those this
:class:`~stalker.core.models.Task` is dependening on.
:type depends: list of :class:`~stalker.core.models.Task`
:param bool milestone: A bool (True or False) value showing if this task is
a milestone which doesn't need any resource and effort.
:param part_of: A :class:`~satlker.core.models.SimpleEntity` instance that
has this :class:`~stalker.core.models.Task`. Now this one creates a
little hole in the system, cause not all of the
:class:`~stalker.core.models.SimpleEntity` instances has ``tasks``
attribute. Only classes that has been mixed with
:class:`~stalker.core.mixins.TasksMixin` has the attribute called
``tasks``. But it needed to be done in this way cause Stalker is designed
to be used in a relational database (although you can opt not to use
one), and in the ORM of the database, possibly everything is going to be
derived from the :class:`~stalker.core.models.SimpleEntity` and the
mixed in classes are not going to have individual tables. Anyway you got
the point, it needs to be something that is represented in the database.
:type part_of: :class:`~stalker.core.models.SimpleEntity`.
"""
#.. :param depends: A list of
#:class:`~stalker.core.models.TaskDependencyRelation` objects. Holds
#information about the list of other :class:`~stalker.core.models.Task`\ s
#which the current one is dependent on.
#.. giving information about the dependent tasks. The given list is iterated
#and the :attr:`~stalker.core.models.Task.start_date` attribute is set to
#the latest found :attr:`~stalker.core.models.Task.due_date` attribute of
#the dependent :class:`~stalker.core.models.Task`\ s.
#.. :type depends: list of :class:`~stalker.core.models.TaskDependencyRelation`
#:param parent_task: Another :class:`~stalker.core.models.Task` which is the
#parent of the current :class:`~stalker.core.models.Task`.
#:class:`~stalker.core.models.Task`\ s can be grouped by using parent and
#child relation.
#:type parent_task: :class:`~stalker.core.models.Task`
#:param sub_tasks: A list of other :class:`~stalker.core.models.Task`\ s
#which are the child of the current one. A
#:class:`~stalker.core.models.Task` with other child
#:class:`~stalker.core.models.Task`\ s:
#* can not have any resources
#* can not have any effort set
#* can not have any versions
#The only reason of a :class:`~stalker.core.models.Task` to have other
#:class:`~stalker.core.models.Task`\ s as child is to group them. So it
#is meaningles to let a parent :class:`~stalker.core.models.Task` to have
#any resource or any effort or any verions. The
#:attr:`~stalker.core.models.Task.start_date`,
#:attr:`~stalker.core.models.Task.due_date` and
#:attr:`~stalker.core.models.Task.duration` attributes of a
#:class:`~stalker.core.models.Task` with child classes will be based on
#it childrens date attributes.
#:type child_tasks: :class:`~stalker.core.models.Task`.
#:param versions: A list of :class:`~stalker.core.models.Version` objects
#showing the produced work on the repository. This is the relation between
#database and the repository.
#:type versions: list of :class:`~stalker.core.models.Version`
#"""
_bookings = ValidatedList([], Booking)
_versions = ValidatedList([], Version)
#----------------------------------------------------------------------
[docs] def __init__(self,
depends=None,
effort=None,
resources=None,
milestone=False,
priority=defaults.DEFAULT_TASK_PRIORITY,
part_of=None,
**kwargs):
super(Task, self).__init__(**kwargs)
# call the mixin __init__ methods
StatusMixin.__init__(self, **kwargs)
ScheduleMixin.__init__(self, **kwargs)
ProjectMixin.__init__(self, **kwargs)
self._milestone = self._validate_milestone(milestone)
self._depends = self._validate_depends(depends)
self._resources = []
self.resources = resources
self._effort = None
self.effort = effort
self._priority = self._validate_priority(priority)
self._part_of = None
self.part_of = self._validate_part_of(part_of)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(Task, self).__eq__(other) and isinstance(other, Task)
#----------------------------------------------------------------------
def _validate_bookings(self, bookings_in):
"""validates the given bookings value
"""
if bookings_in is None:
bookings_in = []
if not all([isinstance(element, Booking) for element in bookings_in]):
raise TypeError("all the elements in the bookings should be "
"an instances of stalker.core.models.Booking")
return ValidatedList(bookings_in, Booking)
#----------------------------------------------------------------------
def _validate_complete(self, complete_in):
"""validates the given complete value
"""
return bool(complete_in)
#----------------------------------------------------------------------
def _validate_depends(self, depends_in):
"""validates the given depends value
"""
if depends_in is None:
depends_in = []
if not isinstance(depends_in, list):
raise TypeError("the depends attribute should be an list of"
"stalker.core.models.Task instances")
if not all([isinstance(element, Task) for element in depends_in]):
raise TypeError("all the elements in the depends should be an "
"instance of stalker.core.models.Task")
# check for the circular dependency
for task in depends_in:
_check_circular_dependency(task, self)
return ValidatedList(depends_in, Task)
#----------------------------------------------------------------------
def _validate_effort(self, effort_in):
"""validates the given effort
"""
if not isinstance(effort_in, datetime.timedelta):
effort_in = None
if effort_in is None:
# take it from the duration and resources
num_of_resources = len(self.resources)
if num_of_resources == 0:
num_of_resources = 1
effort_in = self.duration * num_of_resources
return effort_in
#----------------------------------------------------------------------
def _validate_milestone(self, milestone_in):
"""validates the given milestone value
"""
return bool(milestone_in)
#----------------------------------------------------------------------
def _validate_part_of(self, part_of_in):
"""validates the given part_of value
"""
if not isinstance(part_of_in, SimpleEntity):
raise TypeError("part_of should be an instance of "
"stalker.core.models.SimpleEntity or a derived "
"one")
return part_of_in
#----------------------------------------------------------------------
def _validate_priority(self, priority_in):
"""validates the given priority value
"""
try:
priority_in = int(priority_in)
except (ValueError, TypeError):
pass
if not isinstance(priority_in, int):
priority_in = defaults.DEFAULT_TASK_PRIORITY
if priority_in < 0:
priority_in = 0
elif priority_in > 1000:
priority_in = 1000
return priority_in
#----------------------------------------------------------------------
def _validate_resources(self, resources_in):
"""validates the given resources value
"""
if resources_in is None:
resources_in = []
if not isinstance(resources_in, list):
raise TypeError("resources should be a list of "
"stalker.core.models.User instances")
if not all([isinstance(element, User) for element in resources_in]):
raise TypeError("resources should be a list of "
"stalker.core.models.User instances")
if self.milestone:
resources_in = []
return ValidatedList(resources_in, User,
self.__resource_item_validator__)
#----------------------------------------------------------------------
def _validate_versions(self, versions_in):
"""validates the given version value
"""
if versions_in is None:
versions_in = []
if not all([isinstance(element, Version) for element in versions_in]):
raise TypeError("all the elements in the versions list should be "
"stalker.core.models.Version instances")
return ValidatedList(versions_in, Version)
#----------------------------------------------------------------------
[docs] def bookings():
def fget(self):
return self._bookings
def fset(self, bookings_in):
self._bookings = self._validate_bookings(bookings_in)
doc = """A list of :class:`~stalker.core.models.Booking` objects
showing who and when spent how much effort on this task.
"""
return locals()
bookings = property(**bookings())
#----------------------------------------------------------------------
[docs] def complete():
def fget(self):
return self._complete
def fset(self, complete_in):
self._complete = self._validate_complete(complete_in)
doc = """A bool value showing if this task is completed or not.
There is a good article_ about why not to use an attribute called
``percent_complete`` to measure how much the task is completed.
.. _article: http://www.pmhut.com/how-percent-complete-is-that-task-again
"""
return locals()
complete = property(**complete())
#----------------------------------------------------------------------
[docs] def depends():
def fget(self):
return self._depends
def fset(self, depends_in):
self._depends = self._validate_depends(depends_in)
doc = """A list of :class:`~stalker.core.models.Task`\ s that this one is depending on.
A CircularDependencyError will be raised when the task dependency
creates a circlar dependency which means it is not allowed to create
a dependency for this Task which is depending on another one which in
some way depends to this one again.
"""
return locals()
depends = property(**depends())
#----------------------------------------------------------------------
[docs] def duration():
def fget(self):
return self._duration
def fset(self, duration_in):
# just call the fset method of the duration property in the super
ScheduleMixin.duration.fset(self, duration_in)
# then update the effort
num_of_resources = len(self.resources)
if num_of_resources == 0:
num_of_resources = 1
self._effort = self.duration * num_of_resources
doc = """The overriden duration attribute.
It is a datetime.timedelta instance. Showing the difference of the
:attr:`~stalker.core.mixins.ScheduleMixin.start_date` and the
:attr:`~stalker.core.mixins.ScheduleMixin.due_date`. If edited it
changes the :attr:`~stalker.core.mixins.ScheduleMixin.due_date`
attribute value.
"""
return locals()
duration = property(**duration())
#----------------------------------------------------------------------
[docs] def effort():
def fget(self):
return self._effort
def fset(self, effort_in):
self._effort = self._validate_effort(effort_in)
# update the duration
num_of_resources = len(self.resources)
if num_of_resources == 0:
num_of_resources = 1
self.duration = self._effort / num_of_resources
doc = """The total effort that needs to be spend to complete this Task.
Can be used to create an initial bid of how long this task going to
take. The effort is equaly divided to the assigned resources. So if the
effort is 10 days and 2 resources is assigned then the
:attr:`~stalker.core.models.Task.duration` of the task is going to be 5
days (if both of the resources are free to work). The default value is
stalker.conf.defaults.DEFAULT_TASK_DURATION.
The effort defines the :attr:`~stalker.core.models.Task.duration` of
the task. Every resource is counted equally effective and the
:attr:`~stalker.core.models.Task.duration` will be calculated by the
simple formula:
.. math::
{duration} = \\frac{{effort}}{n_{resources}}
And changing the :attr:`~stalker.core.models.Task.duration` will also
effect the :attr:`~stalker.core.models.Task.effort` spend. The
:attr:`~stalker.core.models.Task.effort` will be calculated with the
formula:
.. math::
{effort} = {duration} \\times {n_{resources}}
"""
return locals()
effort = property(**effort())
#----------------------------------------------------------------------
[docs] def milestone():
def fget(self):
return self._milestone
def fset(self, milestone_in):
self._milestone = self._validate_milestone(milestone_in)
if self._milestone:
self._resources = []
doc = """Specifies if this Task is a milestone.
Milestones doesn't need any duration, any effort and any resources. It
is used to create meaningfull dependencies between the critical stages
of the project.
"""
return locals()
milestone = property(**milestone())
#----------------------------------------------------------------------
def __resource_item_validator__(self, resources_added, resources_removed):
"""a callable for more granular control over resources list
"""
# add the task to the resources
for resource in resources_added:
resource._tasks.append(self)
for resource in resources_removed:
resource._tasks.remove(self)
#----------------------------------------------------------------------
[docs] def resources():
def fget(self):
return self._resources
def fset(self, resources_in):
# validate the incoming resources
resources_in = self._validate_resources(resources_in)
# remove the current task from the previous resources tasks list
for resource in self._resources:
try:
resource.tasks.remove(self)
except ValueError:
pass
self._resources = resources_in
# now append the task to every one of the users in the resources_in
for resource in self._resources:
if self not in resource.tasks:
resource._tasks.append(self)
doc = """The list of :class:`stalker.core.models.User`\ s instances
assigned to this Task.
"""
return locals()
resources = property(**resources())
#----------------------------------------------------------------------
[docs] def part_of():
def fget(self):
return self._part_of
def fset(self, part_of_in):
part_of_in = self._validate_part_of(part_of_in)
# remove it from the current part_of attribute
if not self._part_of is None:
self._part_of.tasks.remove(self)
# update the back reference attribute tasks
self._part_of = part_of_in
self._part_of.tasks.append(self)
doc = """The :class:`~satlker.core.models.SimpleEntity derivative that this Task is a part of.
"""
return locals()
part_of = property(**part_of())
#----------------------------------------------------------------------
[docs] def priority():
def fget(self):
return self._priority
def fset(self, priority_in):
self._priority = self._validate_priority(priority_in)
doc = """The priority of the current Task.
It is a number between 0 and 1000 shows how important this task is
among the others.
"""
return locals()
priority = property(**priority())
#----------------------------------------------------------------------
[docs] def versions():
def fget(self):
return self._versions
def fset(self, versions_in):
self._versions = self._validate_versions(versions_in)
doc = """A list of :class:`~stalker.core.models.Version` instances
showing the files created for this task.
"""
return locals()
versions = property(**versions())
#----------------------------------------------------------------------
def _check_circular_dependency(task, check_for_task):
"""checks the circular dependency in task if it has check_for_task in its
depends list
!!!!WARNING NO TEST FOR THIS FUNCTION!!!!
"""
for dependent_task in task.depends:
if dependent_task is check_for_task:
raise CircularDependencyError(
"task %s and %s creates a circular dependency" % \
(task, check_for_task)
)
else:
_check_circular_dependency(dependent_task, check_for_task)
#########################################################################
#class TaskDependencyRelation(object):
#"""Holds information about :class:`~stalker.core.models.Task` dependencies.
#(DEVELOPERS: It could be an association proxy for the Task class)
#A TaskDependencyRelation object basically defines which
#:class:`~stalker.core.models.Task` is dependedt
#to which other :class:`~stalker.core.models.Task` and what is the lag
#between the end of the dependent to the start of the dependee.
#A :class:`~stalker.core.models.Task` can not be set dependent to it self.
#So the the :attr:`~stalker.core.models.TaskDependencyRelation.depends` list
#can not contain the same value with
#:attr:`~stalker.core.models.TaskDependencyRelation.task`.
#:param task: The :class:`~stalker.core.models.Task` that is dependent to
#others.
#:type task: :class:`~stalker.core.models.Task`
#:param depends: A :class:`~stalker.core.models.Task`\ s that the
#:class:`~stalker.core.models.Task` which is held by the
#:attr:`~stakler.core.models.TaskDependencyRelation.task` attribute is
#dependening on. The :attr:`~stalker.core.models.Task.start_date` and the
#:attr:`~stalker.core.models.Task.due_date` attributes of the
#:class:`~stalker.core.models.Task` is updated if it is before the
#``due_date`` of the dependent :class:`~stalker.core.models.Task`.
#:type depends: :class:`~stalker.core.models.Task`
#:param lag: The lag between the end of the dependent task to the start of
#the dependee. It is an instance of timedelta and could be a negative
#value. The default is 0. So the end of the task is start of the other.
#"""
##----------------------------------------------------------------------
#def __init__(self):
#pass
########################################################################
[docs]class FilenameTemplate(Entity):
"""Holds templates for filename conventions.
FilenameTemplate objects help to specify where to place a file related to
its :attr:`~stalker.core.models.FilenameTemplate.target_entity_type`.
The first very important usage of FilenameTemplates is to place asset file
:class:`~stalker.core.models.Version`'s to proper places inside a
:class:`~stalker.core.models.Project`'s
:class:`~stalker.core.models.Structure`.
Secondly, it can be used in the process of injecting files in to the
repository. By creating templates for :class:`~stalker.core.models.Links`.
:param str target_entity_type: The class name that this FilenameTemplate
is designed for. You can also pass the class itself. So both of the
examples below can work::
new_filename_template1 = FilenameTemplate(target_entity_type="Asset")
new_filename_template2 = FilenameTemplate(target_entity_type=Asset)
A TypeError will be raised when it is skipped or it is None and a
ValueErorr will be raised when it is given as and empty string.
:param str path_code: A Jinja2 template code which specifies the path of
the given item. It is relative to the project root. A typical example
could be::
asset_path_code = "ASSETS/{{asset.code}}/{{task.code}}/"
:param str file_code: A Jinja2 template code which specifies the file name
of the given item. It is relative to the
:attr:`~stalker.core.models.FilenameTemplate.path_code`. A typical
example could be::
asset_file_code = "{{asset.code}}_{{version.take}}_{{task.code}}_"\\
"{{version.version}}_{{user.initials}}"
Could be set to an empty string or None, the default value is None.
:param str output_path_code: A Jinja2 template code specifying where to
place the outputs of the applied
:attr:`~stalker.core.models.FilenameTemplate.target_entity_type`. If it
is empty and the
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is True,
then the outputs will naturally be in the same place with the
:attr:`~stalker.core.models.FilenameTemplate.path_code`. If the
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is False
then :attr:`~stalker.core.models.FilenameTemplate.output_path_code` will
be the same code with
:attr:`~stalker.core.models.FilenameTemplate.path_code`.
It can be None, or an empty string, or it can be skipped.
:param str output_file_code: A Jinja2 template code specifying what will be
the file name of the output. If it is skipped or given as None or as an
empty string, it will be the same with the
:attr:`~stalker.core.models.FilenameTemplate.file_code`.
It can be skipped, or can be set to None or an empty string. The default
value is None, and this will set the
:attr:`~stalker.core.models.FilenameTemplate.output_file_code` to the
same value with :attr:`~stalker.core.models.FilenameTemplate.file_code`.
:param bool output_is_relative: A bool value specifying if the
:attr:`~stalker.core.models.FilenameTemplate.output_path_code` is
relative to the :attr:`~stalker.core.models.FilenameTemplate.path_code`.
The default value is True. Can be skipped, any other than a bool value
will be evaluated to a bool value.
Examples:
A template for asset versions can be used like this::
from stalker.core.models import Type, FilenameTemplate, TaskTemplate
# create a couple of variables
path_code = "ASSETS/{{asset_type.name}}/{{task_type.code}}"
file_code =
"{{asset.name}}_{{take.name}}_{{asset_type.name}}_v{{version.version_number}}"
output_path_code = "OUTPUT"
output_file_code = file_code
# create a type for modeling task
modeling = Type(
name="Modeling",
code="MODEL",
description="The modeling step of the asset",
target_entity_type=Task
)
# create a "Character" Type for Asset classes
character = Type(
name="Character",
description="this is the character asset type",
target_entity_type=Asset
)
# now create our FilenameTemplate
char_template = FilenameTemplate(
name="Character",
description="this is the template which explains how to place \
Character assets",
target_entity_type="Asset",
path_code=path_code,
file_code=file_code,
output_path_code=output_path_code,
output_file_code=output_file_code,
output_is_relative=True,
)
# assign this type template to the structure of the project with id=101
myProject = query(Project).filter_by(id=101).first()
# append the type template to the structures' templates
myProject.structure.templates.append(char_template)
# commit everything to the database
session.commit()
Now with the code above, whenever a new
:class:`~stalker.core.models.Version` created for a **Character**
asset, Stalker will automatically place the related file to a certain
folder and with a certain file name defined by the template. For example
the above template should render something like below for Windows::
|- M:\\\PROJECTS --> {{repository.path}}
|- PRENSESIN_UYKUSU --> {{project.code}}
|- ASSETS --> "ASSETS"
|- Character --> {{asset_type.name}}
|- Olum --> {{asset.name}}
|- MODEL --> {{task_type.code}}
|- Olum_MAIN_MODEL_v001.ma --> {{asset.name}}_\
{{take.name}}_{{asset_type.name}}_v{{version.version_number}}
And one of the best side is you can create a version from Linux, Windows or
OSX all the paths will be correctly handled by Stalker.
"""
#----------------------------------------------------------------------
[docs] def __init__(self,
target_entity_type=None,
path_code=None,
file_code=None,
output_path_code=None,
output_file_code=None,
output_is_relative=True,
**kwargs):
super(FilenameTemplate, self).__init__(**kwargs)
self._target_entity_type =\
self._validate_target_entity_type(target_entity_type)
self._path_code = self._validate_path_code(path_code)
self._file_code = self._validate_file_code(file_code)
self._output_is_relative =\
self._validate_output_is_relative(output_is_relative)
self._output_path_code =\
self._validate_output_path_code(output_path_code)
self._output_file_code =\
self._validate_output_file_code(output_file_code)
#----------------------------------------------------------------------
def _validate_path_code(self, path_code_in):
"""validates the given path_code attribute for several conditions
"""
# check if it is None
if path_code_in is None:
path_code_in = u""
path_code_in = unicode(path_code_in)
return path_code_in
#----------------------------------------------------------------------
def _validate_file_code(self, file_code_in):
"""validates the given file_code attribute for several conditions
"""
# check if it is None
if file_code_in is None:
file_code_in = u""
# convert it to unicode
file_code_in = unicode(file_code_in)
return file_code_in
#----------------------------------------------------------------------
def _validate_target_entity_type(self, target_entity_type_in):
"""validates the given target_entity_type attribute for several
conditions
"""
# check if it is None
if target_entity_type_in is None:
raise TypeError("target_entity_type can not be None")
if isinstance(target_entity_type_in, type):
target_entity_type_in = target_entity_type_in.__name__
return target_entity_type_in
#----------------------------------------------------------------------
def _validate_output_path_code(self, output_path_code_in):
"""validates the given output_path_code value
"""
if output_path_code_in == None or output_path_code_in == "":
if self.output_is_relative:
output_path_code_in = ""
else:
output_path_code_in = self.path_code
return output_path_code_in
#----------------------------------------------------------------------
def _validate_output_file_code(self, output_file_code_in):
"""validates the given output_file_code value
"""
if output_file_code_in == None or output_file_code_in == "":
if self.output_is_relative:
output_file_code_in = ""
else:
output_file_code_in = self.file_code
return output_file_code_in
#----------------------------------------------------------------------
def _validate_output_is_relative(self, output_is_relative_in):
"""validates the given output_is_relative value
"""
return bool(output_is_relative_in)
#----------------------------------------------------------------------
[docs] def path_code():
def fget(self):
return self._path_code
def fset(self, path_code_in):
self._path_code = self._validate_path_code(path_code_in)
doc = """this is the property that helps you assign values to
path_code attribute"""
return locals()
path_code = property(**path_code())
#----------------------------------------------------------------------
[docs] def file_code():
def fget(self):
return self._file_code
def fset(self, file_code_in):
self._file_code = self._validate_file_code(file_code_in)
doc = """this is the property that helps you assign values to
file_code attribute"""
return locals()
file_code = property(**file_code())
#----------------------------------------------------------------------
[docs] def target_entity_type():
def fget(self):
return self._target_entity_type
doc = """the target entity type this FilenameTemplate object should
work on, should be a string value or the class itself"""
return locals()
target_entity_type = property(**target_entity_type())
#----------------------------------------------------------------------
[docs] def output_path_code():
def fget(self):
return self._output_path_code
def fset(self, output_path_code_in):
self._output_path_code =\
self._validate_output_path_code(output_path_code_in)
doc = """The output_path_code of this FilenameTemplate object.
Should be a unicode string. None and empty string is also accepted, but
in this case the value is copied from the
:attr:`~stalker.core.models.FilenameTemplate.path_code` if also the
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is
False. If
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is
True then it will left as an empty string.
"""
return locals()
output_path_code = property(**output_path_code())
#----------------------------------------------------------------------
[docs] def output_file_code():
def fget(self):
return self._output_file_code
def fset(self, output_file_code_in):
self._output_file_code =\
self._validate_output_file_code(output_file_code_in)
doc = """The output_file_code of this FilenameTemplate object.
Should be a unicode string. None and empty string is also accepted, but
in this case the value is copied from the
:attr:`~stalker.core.models.FilenameTemplate.file_code` if also the
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is
False. If
:attr:`~stalker.core.models.FilenameTemplate.output_is_relative` is
True then it will left as an empty string.
"""
return locals()
output_file_code = property(**output_file_code())
#----------------------------------------------------------------------
[docs] def output_is_relative():
def fget(self):
return self._output_is_relative
def fset(self, output_is_relative_in):
self._output_is_relative =\
self._validate_output_is_relative(output_is_relative_in)
doc = """
"""
return locals()
output_is_relative = property(**output_is_relative())
#----------------------------------------------------------------------
def __eq__(self, other):
"""checks the equality of the given object to this one
"""
return super(FilenameTemplate, self).__eq__(other) and \
isinstance(other, FilenameTemplate) and \
self.target_entity_type == other.target_entity_type and \
self.path_code == other.path_code and \
self.file_code == other.file_code and \
self.output_path_code == other.output_path_code and \
self.output_file_code == other.output_file_code
########################################################################
[docs]class User(Entity):
"""The user class is designed to hold data about a User in the system.
There are a couple of points to take your attention to:
* The :attr:`~stalker.core.models.User.code` attribute is derived from
the :attr:`~stalker.core.models.User.nice_name` as it is in a
:class:`~stalker.core.models.SimpleEntity`, but the
:attr:`~stalker.core.models.User.nice_name` is derived from the
:attr:`~stalker.core.models.User.login_name` instead of the
:attr:`~stalker.core.models.User.name` attribute, so the
:attr:`~stalker.core.models.User.code` of a
:class:`~stalker.core.models.User` and a
:class:`~stalker.core.models.SimpleEntity` will be different then each
other. The formatting of the :attr:`~stalker.core.models.User.code`
attribute is as follows:
* no underscore character is allowed, so while in the
:class:`~stalker.core.models.SimpleEntity` class the code could have
underscores, in :class:`~stalker.core.models.User` class it is not
allowed.
* all the letters in the code will be converted to lower case.
Other than this two new rules all the previous formatting rules from the
:class:`~stalker.core.models.SimpleEntity` are still in charge.
* The :attr:`~stalker.core.models.User.name` is a synonym of the
:attr:`~stalker.core.models.User.login_name`, so changing one of them
will change the other.
:param email: holds the e-mail of the user, should be in [part1]@[part2]
format
:type email: unicode
:param login_name: it is the login name of the user, it should be all lower
case. Giving a string or unicode that has uppercase letters, it will be
converted to lower case. It can not be an empty string or None and it can
not contain any white space inside. login_name parameter will be copied
over name if both of them is given, if one of them given they will have
the same value which is the formatted login_name value. Setting the name
value also sets the login_name and setting the login_name property also
sets the name, while creating a User object you don't need to specify
both of them, one is enough and if the two is given `login_name` will be
used.
:type login_name: unicode
:param first_name: it is the first name of the user, must be a string or
unicode, middle name also can be added here, so it accepts white-spaces
in the variable, but it will truncate the white spaces at the beginin and
at the end of the variable and it can not be empty or None
:type first_name: unicode
:param last_name: it is the last name of the user, must be a string or
unicode, again it can not contain any white spaces at the beggining and
at the end of the variable and it can be an empty string or None
:type last_name: unicode
:param department: it is the department of the current user. It should be
a Department object. One user can only be listed in one department. A
user is allowed to have no department to make it easy to create a new
user and create the department and assign the user it later.
:type department: :class:`~stalker.core.models.Department`
:param password: it is the password of the user, can contain any character.
Stalker doesn't store the raw passwords of the users. To check a stored
password with a raw password use
:meth:`~stalker.core.models.User.check_password` and to set the password
you can use the :attr:`~stalker.core.models.User.password` property
directly.
:type password: unicode
:param permission_groups: it is a list of permission groups that this user
is belong to
:type permission_groups: :class:`~stalker.core.models.PermissionGroup`
:param tasks: it is a list of Task objects which holds the tasks that this
user has been assigned to
:type tasks: list of :class:`~stalker.core.models.Task`\ s
:param projects_lead: it is a list of Project objects that this user
is the leader of, it is for back refefrencing purposes.
:type projects_lead: list of :class:`~stalker.core.models.Project`\ s
:param sequences_lead: it is a list of Sequence objects that this
user is the leader of, it is for back referencing purposes
:type sequences_lead: list of :class:`~stalker.core.models.Sequence`
:param last_login: it is a datetime.datetime object holds the last login
date of the user (not implemented yet)
:type last_login: datetime.datetime
:param initials: it is the initials of the users name, if nothing given it
will be calculated from the first and last names of the user
:type initials: unicode
"""
#:param projects: it is a list of Project objects which holds the projects
#that this user is a part of. Calculated from all the
#:class:`~stalker.core.models.Task`\ s of the current User.
# fix __new__ errors
_projects = []
#----------------------------------------------------------------------
[docs] def __init__(self,
department=None,
email="",
first_name="",
last_name="",
login_name="",
password="",
permission_groups=[],
#projects=[],
projects_lead=[],
sequences_lead=[],
tasks=[],
last_login=None,
initials="",
**kwargs
):
# use the login_name for name if there are no name attribute present
name = kwargs.get("name")
if name is None:
name = login_name
if login_name == "":
login_name = name
name = login_name
kwargs["name"] = name
super(User, self).__init__(**kwargs)
self._department = self._validate_department(department)
self._email = self._validate_email(email)
self._first_name = self._validate_first_name(first_name)
self._last_name = self._validate_last_name(last_name)
#self._login_name = ""
#self.login_name = login_name
self._login_name = self._validate_login_name(login_name)
self._initials = self._validate_initials(initials)
# to be able to mangle the password do it like this
self._password = None
self.password = password
self._permission_groups = \
self._validate_permission_groups(permission_groups)
#self._projects = self._validate_projects(projects)
self._projects_lead = self._validate_projects_lead(projects_lead)
self._sequences_lead = self._validate_sequences_lead(sequences_lead)
self._tasks = self._validate_tasks(tasks)
self._last_login = self._validate_last_login(last_login)
#----------------------------------------------------------------------
def __repr__(self):
"""return the representation of the current User
"""
return "<User (%s %s ('%s'))>" % \
(self.first_name, self.last_name, self.login_name)
#----------------------------------------------------------------------
def __eq__(self, other):
"""the equality operator
"""
return super(User, self).__eq__(other) and \
isinstance(other, User) and \
self.email == other.email and \
self.login_name == other.login_name and \
self.first_name == other.first_name and \
self.last_name == other.last_name and \
self.name == other.name
#----------------------------------------------------------------------
def __ne__(self, other):
"""the inequality operator
"""
return not self.__eq__(other)
#----------------------------------------------------------------------
def _validate_department(self, department_in):
"""validates the given department value
"""
# check if it is intance of Department object
if department_in is not None:
if not isinstance(department_in, Department):
raise TypeError("department should be instance of "
"stalker.core.models.Department")
return department_in
#----------------------------------------------------------------------
def _validate_email(self, email_in):
"""validates the given email value
"""
# check if email_in is an instance of string or unicode
if not isinstance(email_in, (str, unicode)):
raise TypeError("email should be an instance of string or "
"unicode")
return self._validate_email_format(email_in)
#----------------------------------------------------------------------
def _validate_email_format(self, email_in):
"""formats the email
"""
# split the mail from @ sign
splits = email_in.split("@")
len_splits = len(splits)
# there should be one and only one @ sign
if len_splits > 2:
raise ValueError("check the email formatting, there are more than "
"one @ sign")
if len_splits < 2:
raise ValueError("check the email formatting, there are no @ sign")
if splits[0] == "":
raise ValueError("check the email formatting, the name part is "
"missing")
if splits[1] == "":
raise ValueError("check the email formatting, the domain part is "
"missing")
return email_in
#----------------------------------------------------------------------
def _validate_first_name(self, first_name_in):
"""validates the given first_name attribute
"""
if first_name_in is None:
raise TypeError("first_name cannot be None")
if not isinstance(first_name_in, (str, unicode)):
raise TypeError("first_name should be instance of string or "
"unicode")
if first_name_in == "":
raise ValueError("first_name can not be an empty string")
return self._format_first_name(first_name_in)
#----------------------------------------------------------------------
def _format_first_name(self, first_name_in):
"""formats the given first_name
"""
return first_name_in.strip().title()
#----------------------------------------------------------------------
def _validate_initials(self, initials_in):
"""validates the given initials
"""
initials_in = str(initials_in)
if initials_in == "":
# derive the initials from the first and last name
initials_in = re.sub("[^A-Z]+", "",
self.first_name.title() + " " + \
self.last_name.title()).lower()
return initials_in
#----------------------------------------------------------------------
def _validate_last_login(self, last_login_in):
"""validates the given last_login argument
"""
if not isinstance(last_login_in, datetime.datetime) and \
last_login_in is not None:
raise TypeError("last_login should be an instance of "
"datetime.datetime or None")
return last_login_in
#----------------------------------------------------------------------
def _validate_last_name(self, last_name_in):
"""validates the given last_name attribute
"""
#if last_name_in is None:
#raise ValueError("last_name cannot be none")
if last_name_in is not None:
if not isinstance(last_name_in, (str, unicode)):
raise TypeError("last_name should be instance of string or "
"unicode")
else:
last_name_in = ""
#if last_name_in == "":
#raise ValueError("last_name can not be an empty string")
return self._format_last_name(last_name_in)
#----------------------------------------------------------------------
def _format_last_name(self, last_name_in):
"""formats the given last_name
"""
return last_name_in.strip().title()
#----------------------------------------------------------------------
def _validate_login_name(self, login_name_in):
"""validates the given login_name value
"""
if login_name_in is None:
raise TypeError("login name could not be None")
#if not isinstance(login_name_in, (str, unicode)):
#raise TypeError("login_name should be instance of string or "
#"unicode")
login_name_in = self._format_login_name(str(login_name_in))
if login_name_in == "":
raise ValueError("login name could not be empty string")
return login_name_in
#----------------------------------------------------------------------
def _format_login_name(self, login_name_in):
"""formats the given login_name
"""
# be sure it is a string
login_name_in = str(login_name_in)
# strip white spaces from start and end
login_name_in = login_name_in.strip()
# remove all the spaces
login_name_in = login_name_in.replace(" ","")
# make it lowercase
login_name_in = login_name_in.lower()
# remove any illegal characters
login_name_in = re.sub( "[^\\(a-zA-Z0-9)]+", "", login_name_in)
# remove any number at the begining
login_name_in = re.sub( "^[0-9]+", "", login_name_in)
return login_name_in
#----------------------------------------------------------------------
def _validate_password(self, password_in):
"""validates the given password
"""
if password_in is None:
raise TypeError("password cannot be None")
return password_in
#----------------------------------------------------------------------
def _validate_permission_groups(self, permission_groups_in):
"""check the given permission_group
"""
if permission_groups_in is None:
permission_groups_in = []
if not isinstance(permission_groups_in, list):
raise TypeError("permission_groups should be a list of group "
"objects")
for permission_group in permission_groups_in:
if not isinstance(permission_group, PermissionGroup):
raise TypeError(
"any group in permission_groups should be an instance of"
"stalker.core.models.PermissionGroup"
)
return ValidatedList(permission_groups_in, PermissionGroup)
##----------------------------------------------------------------------
#def _validate_projects(self, projects_in):
#"""validates the given projects attribute
#"""
#if projects_in is None:
#projects_in = []
#if not isinstance(projects_in, list):
#raise TypeError("projects should be a list of "
#"stalker.core.models.Project objects")
#for a_project in projects_in:
#if not isinstance(a_project, Project):
#raise TypeError(
#"any element in projects should be an instance of "
#"stalker.core.models.Project"
#)
#return ValidatedList(projects_in, Project)
#----------------------------------------------------------------------
def _validate_projects_lead(self, projects_lead_in):
"""validates the given projects_lead attribute
"""
if projects_lead_in is None:
projects_lead_in = []
if not isinstance(projects_lead_in, list):
raise TypeError("projects_lead should be a list of "
"stalker.core.models.Project instances")
for a_project in projects_lead_in:
if not isinstance(a_project, Project):
raise TypeError(
"any element in projects_lead should be a"
"stalker.core.models.Project instance")
return ValidatedList(projects_lead_in, Project)
#----------------------------------------------------------------------
def _validate_sequences_lead(self, sequences_lead_in):
"""validates the given sequences_lead attribute
"""
if sequences_lead_in is None:
sequences_lead_in = []
if not isinstance(sequences_lead_in, list):
raise TypeError("sequences_lead should be a list of "
"stalker.core.models.Sequence objects")
for a_sequence in sequences_lead_in:
if not isinstance(a_sequence, Sequence):
raise TypeError(
"any element in sequences_lead should be an instance of "
"stalker.core.models.Sequence class"
)
return ValidatedList(sequences_lead_in, Sequence)
#----------------------------------------------------------------------
def _validate_tasks(self, tasks_in):
"""validates the given taks attribute
"""
if tasks_in is None:
tasks_in = []
if not isinstance(tasks_in, list):
raise TypeError("tasks should be a list of "
"stalker.core.models.Task objects")
for a_task in tasks_in:
if not isinstance(a_task, Task):
raise TypeError(
"any element in tasks should be an instance of "
"stalker.core.models.Task class")
return ValidatedList(tasks_in, Task)
#----------------------------------------------------------------------
[docs] def check_password(self, raw_password):
"""Checks the given raw_password.
Checks the given raw_password with the current Users objects encrypted
password.
"""
from stalker.ext import auth
return auth.check_password(raw_password, self._password)
#----------------------------------------------------------------------
[docs] def department():
def fget(self):
return self._department
def fset(self, department_in):
self._department = self._validate_department(department_in)
doc = """department of the user, it is a
:class:`~stalker.core.models.Department` object"""
return locals()
department = property(**department())
#----------------------------------------------------------------------
[docs] def email():
def fget(self):
return self._email
def fset(self, email_in):
self._email = self._validate_email(email_in)
doc = """email of the user, accepts strings or unicodes
"""
return locals()
email = property(**email())
#----------------------------------------------------------------------
[docs] def first_name():
def fget(self):
return self._first_name
def fset(self, first_name_in):
self._first_name = self._validate_first_name(first_name_in)
doc = """first name of the user, accepts string or unicode"""
return locals()
first_name = property(**first_name())
#----------------------------------------------------------------------
[docs] def initials():
def fget(self):
return self._initials
def fset(self, initials_in):
self._intials = self._validate_initials(initials_in)
doc = """The initials of the user.
If not spesified, it is the upper case form of first letters of the
:attr:`~stalker.core.models.User.first_name` and
:attr:`~stalker.core.models.User.last_name`"""
return locals()
initials = property(**initials())
#----------------------------------------------------------------------
[docs] def last_login():
def fget(self):
return self._last_login
def fset(self, last_login_in):
self._last_login = self._validate_last_login(last_login_in)
doc = """The last login time of this user.
It is an instance of datetime.datetime class."""
return locals()
last_login = property(**last_login())
#----------------------------------------------------------------------
[docs] def last_name():
def fget(self):
return self._last_name
def fset(self, last_name_in):
self._last_name = self._validate_last_name(last_name_in)
doc = """The last name of the user.
It is a string and can be None or empty string"""
return locals()
last_name = property(**last_name())
#----------------------------------------------------------------------
[docs] def login_name():
def fget(self):
return self._name
def fset(self, login_name_in):
self._name = self._validate_login_name(login_name_in)
# set the name attribute
#self._login_name = self._validate_name(login_name_in)
self._login_name = self._name
# also set the code
self._code = self._validate_code(self._name)
doc = """The login name of the user.
It is a string and also sets the :attr:`~stalker.core.models.User.name`
attribute"""
return locals()
login_name = property(**login_name())
#----------------------------------------------------------------------
[docs] def name():
def fget(self):
return self._name
def fset(self, name_in):
# set the login name first
self._login_name = self._validate_login_name(name_in)
self._name = self._login_name
# also set the nice_name
self._nice_name = self._condition_nice_name(self._name)
# and also the code
#self.code = self._nice_name.upper()
self.code = self._name
doc = """The name of this user.
It is the synonym for the
:attr:`~stalker.core.models.User.login_name`\."""
return locals()
name = property(**name())
#----------------------------------------------------------------------
[docs] def password():
def fget(self):
return self._password
def fset(self, password_in):
from stalker.ext import auth
self._password = auth.set_password(password_in)
doc = """The password of the user.
It is scrambled before it is stored."""
return locals()
password = property(**password())
#----------------------------------------------------------------------
[docs] def permission_groups():
def fget(self):
return self._permission_groups
def fset(self, permission_groups_in):
self._permission_groups = \
self._validate_permission_groups(permission_groups_in)
doc = """permission groups that this users is a member of, accepts
:class:`~stalker.core.models.PermissionGroup` object"""
return locals()
permission_groups = property(**permission_groups())
#----------------------------------------------------------------------
[docs] def projects():
def fget(self):
#return self._projects
projects = []
for task in self.tasks:
projects.append(task.project)
return list(set(projects))
doc = """The list of :class:`~stlalker.core.models.Project`\ s those the current user assigned to.
returns a list of :class:`~stalker.core.models.Project` objects.
It is a read-only attribute. To assign a
:class:`~stalker.core.models.User` to a
:class:`~stalker.core.models.Project`, you need to create a new
:class:`~stalker.core.models.Task` with the
:attr:`~stalker.core.models.Task.resources` is set to this
:class:`~stalker.core.models.User` and assign the
:class:`~stalker.core.models.Task` to the
:class:`~stalker.core.models.Project` by setting the
:attr:`~stalker.core.models.Task.project` attribute of the
:class:`~stalker.core.models.Task` to the
:class:`~stalker.core.models.Project`.
"""
return locals()
projects = property(**projects())
#----------------------------------------------------------------------
[docs] def projects_lead():
def fget(self):
return self._projects_lead
def fset(self, projects_lead_in):
self._projects_lead = \
self._validate_projects_lead(projects_lead_in)
doc = """projects lead by this current user, accepts
:class:`~stalker.core.models.Project` object"""
return locals()
projects_lead = property(**projects_lead())
#----------------------------------------------------------------------
[docs] def sequences_lead():
def fget(self):
return self._sequences_lead
def fset(self, sequences_lead_in):
self._sequences_lead = \
self._validate_sequences_lead(sequences_lead_in)
doc = """sequences lead by this user, accpets
:class:`~stalker.core.models.Sequence` objects"""
return locals()
sequences_lead = property(**sequences_lead())
#----------------------------------------------------------------------
[docs] def tasks():
def fget(self):
return self._tasks
def fset(self, tasks_in):
self._tasks = self._validate_tasks(tasks_in)
doc = """tasks assigned to the current user, accepts
:class:`~stalker.core.models.Task` objects"""
return locals()
tasks = property(**tasks())