"""
Mixins can be used to add certain functionality to the items of a
module. Mixins are used in multiple inheritance. The mixin ensures that
the item will have all needed fields in the database and provides the
proper interface to use the added functionality. Example::
class Comment(BaseItem, Nested, Meta, Owned, Base):
__tablename__ = 'comments'
_modul_id = 99
id = sa.Column(sa.Integer, primary_key=True)
comment = sa.Column('comment', sa.Text)
...
The comment class in the example only defines the two fields `id` and
`comment`. But as it inherits from the `Nested`, `Meta` and
`Owned` mixin it also will have date fields with the creation and date of
the last update, references to the user and group which ownes the Comment.
Further the 'Nested' mixin will ensure the comments can reference each other
to be able to build a hierarchy structure (e.g Threads in the example of the
comments).
.. note::
The Comment Mixin has been removed from ringo and converted in the
the ringo comment extension.
.. important::
As most of the mixins will add additional tables and database fields
to your item it is needed to migrate your database to the new model.
"""
import datetime
import json
import logging
import re
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy import (
Column,
Text,
String,
Integer,
DateTime,
ForeignKey,
Table
)
from sqlalchemy.orm import (
relationship,
backref
)
from formbar.converters import to_date
from ringo.model import Base
from ringo.lib.helpers import get_raw_value
from ringo.lib.alchemy import get_columns_from_instance
log = logging.getLogger(__name__)
re_date = re.compile("\d{4}-\d{2}-\d{2}")
class Mixin(object):
"""Base mixin class"""
@classmethod
def get_mixin_actions(cls):
"""Returns a list of actions specific for the mixin. Overwrite
this method if you want to add custom actions for your mixin.
:returns: List of ActionItems
"""
return []
[docs]class StateMixin(object):
"""Mixin to add one or more Statemachines to an item. The
statemachines are stored in a internal '_statemachines' dictionary.
The current state is stored as integer value per item. This field
must be created manually. The name of the field which stores the
value for the current state must be the keyname of the
'_statemachines' dictionary.
Example Mixin usage::
class FooMixin(StateMixin):
# Mixin inherited from the StateMixin to add the Foobar
# state machine
# Attach the statemachines to an internal dictionary
_statemachines = {'foo_state_id': FooStatemachine}
# Configue a field in the model which saves the current
# state per state machine
foo_state_id = sa.Column(sa.Integer, default=1)
# Optional. Create a property to access the statemachine
# like an attribute. This gets usefull if you want to access
# the state in overview lists.
@property
def foo_state(self):
state = self.get_statemachine('foo_state_id')
return state.get_state()
"""
"""Mapping of statemachines to attributes. The key in the dictionary
must be the name of the field which stores the integer value of the
current state of the statemachine."""
_statemachines = {}
@classmethod
def list_statemachines(cls):
"""Returns a list keys of configured statemachines"""
return cls._statemachines.keys()
def change_state(self, request, key, old_state_id, new_state_id):
"""Changes the state of the given statemachine from old to new.
The statemachine is identified by the key in the _statemachines
dictionary"""
log.debug("%s -> %s" % (old_state_id, new_state_id))
sm = self.get_statemachine(key, old_state_id, request)
sm.set_state(new_state_id)
# clear cached statemachines
setattr(self, '_cache_statemachines', {})
def get_statemachine(self, key, state_id=None, request=None):
"""Returns a statemachine instance for the given key
:key: Name of the key of the statemachine
:state_id: initial state of the statemachine
:returns: Statemachine instance
"""
if not hasattr(self, '_cache_statemachines'):
cache = {}
else:
cache = getattr(self, '_cache_statemachines')
if key not in cache:
cache[key] = self._statemachines[key](self, key, state_id, request)
setattr(self, '_cache_statemachines', cache)
else:
# As the statemachine is cached we need to overwrite the
# internale _request parameter to have the current request
# available in the state.
cache[key]._request = request
return cache[key]
class Blob(object):
"""Mixin to add a data fields to store form data as JSON in a single
field. The mixin will overwrite the way how to get the form
definiton and how to get values from the item."""
data = Column(Text, default="{}")
def __getattr__(self, name):
"""This function tries to get the given attribute of the item if
it can not be found using the usual way to get attributes. In
this case we will split the attribute name by "." and try to get
the attribute along the "." separated attribute name."""
data = getattr(self, 'data')
if data:
# In some cases it is needed to be able to trigger getting the
# exapanded value without calling the get_value method. This can
# be achieved by accessing the attribute with a special name.
# Lets say you want to get the expanded value for `foo`. You get
# this by asking for `foo__e_x_p_a_n_d`
if name.endswith("__e_x_p_a_n_d"):
return self.get_value(name.replace("__e_x_p_a_n_d", ""),
expand=True)
elif name.find(".") < 0:
json_data = json.loads(getattr(self, 'data'))
if name in json_data:
json_value = json_data[name]
# Poor mans data type conversion.
if isinstance(json_value, basestring):
# Try to convert the value into a date object if it
# looks like a date.
if re_date.match(json_value):
return to_date(json_value)
return json_value
return get_raw_value(self, name)
def set_values(self, values, use_strict=False, request=None):
"""Will set the given values into Blobform items. This function
overwrites the default behavior of the BaseItem and takes care
that the data will be saved in the data attribute as JSON
string."""
json_data = json.loads(self.get_values().get('data') or "{}")
columns = get_columns_from_instance(self, True)
for key, value in values.iteritems():
# Ignore private form fields
if key.startswith('_'):
continue
if key in columns or key.find(".") > -1:
log.debug("Setting value '%s' for '%s' in DB" % (value, key))
setattr(self, key, value)
else:
if isinstance(value, datetime.date):
value = str(value)
log.debug("Setting value '%s' for '%s' in JSON" % (value, key))
json_data[key] = value
setattr(self, 'data', json.dumps(json_data))
class Blobform(Blob):
"""Enhances the Blob class and make the form configurable. The fid
references the form definiton."""
@declared_attr
def fid(cls):
return Column(Integer, ForeignKey('forms.id'))
@declared_attr
def form(cls):
form = relationship("Form", uselist=False)
return form
class Version(Base):
__tablename__ = 'versions'
id = Column(Integer, primary_key=True)
values = Column(Text, nullable=False)
author = Column(String, nullable=False)
date = Column(DateTime, nullable=False)
class Versioned(object):
"""Mixin to add version functionallity to a modul. This mixin is
used to store different "versions" of an item. On each update of
the item, the serialized values of the item will be saved in the
version table. Item modul will have a "versions" relationship
containing all versions of the values for this item."""
@declared_attr
def versions(cls):
tbl_name = "nm_%s_versions" % cls.__name__.lower()
nm_table = Table(tbl_name, Base.metadata,
Column('iid', Integer, ForeignKey(cls.id)),
Column('vid', Integer, ForeignKey("versions.id")))
versions = relationship(Version, secondary=nm_table, cascade="all")
return versions
@classmethod
def create_handler(cls, request, item):
cls.update_handler(request, item)
@classmethod
def update_handler(cls, request, item):
"""Will add the serialized values of the item into the version
table.
:request: Current request
:item: Item handled in the update.
"""
version = Version()
item_values = item.get_values(serialized=True)
version.values = json.dumps(item_values)
version.author = request.user.login
version.date = datetime.datetime.utcnow()
item.versions.append(version)
def _get_version(self, author, id):
if not author and not id:
return self.versions[-2]
# iterate over the revsersed versions ignoring the last one
for version in self.versions[-2::-1]:
if id and version.id == id:
return version
elif author and version.author == author:
return version
return self.versions[0]
def get_previous_values(self, author=None, id=None):
"""Returns a dictionary parsed from the last log entry of the
item containinge the previous values for each field if it has
been changed."""
if len(self.versions) < 1:
return {}
else:
pvalues = {}
last = self._get_version(author, id)
try:
values = json.loads(last.values)
for field in values:
if field == "data" and isinstance(self, Blob):
blobdata = json.loads(values[field])
for blobfield in blobdata:
pvalues[blobfield] = blobdata[blobfield]
pvalues[field] = values[field]
except:
log.warning(("Could not build previous values dict. "
"Maybe old log format?"))
return pvalues
[docs]class Owned(object):
"""Mixin to add references to a user and a usergroup. This
references are used to build some kind of ownership of the item. The
ownership is used from the permission system.
It is posible to configure inhertinace of the owner and group from
a given parent element. This information is used only while creating
new instances of the modul. If configured, the default group and
owner information will be overwritten. This is done at the very end
of the creation process. See ''save'' method of the BaseItem. You
can configure the inhertiance by setting the name of the relation to
the parent item in the ''_inherit_gid'' and ''_inherit_uid'' class
variable.
"""
_inherit_gid = None
"""Variable to configure a inheritance of the gid. The variable
should be the name of the relation to the parent element from which
the gid will be taken"""
_inherit_uid = None
"""Variable to configure a inheritance of the uid. The variable
should be the name of the relation to the parent element from which
the uid will be taken"""
_owner_cascades = 'save-update, merge'
"""Variable to configure the behavior of the relation to the parent
item if the owner of an itme is deleted. Defaults to 'save-update,
merge'. See SQLAlchemy documentation for more details. This means a
user can be deleted depending on the ForeignKey setting of the child
items. In case the FK allows null values the user can be deleted and
the owner of the child item will be NULL. If the FK must not NULL
the item can not be deleted. Set this variable to 'all, delete' of
you want to delete all items owned by the user too."""
@declared_attr
def uid(cls):
fk_name = "fk_%s_%s_%s" % (cls.__tablename__, 'uid', 'users')
return Column(Integer, ForeignKey('users.id',
use_alter=True,
name=fk_name))
@declared_attr
def owner(cls):
# Defining cascading and backrefs on Userclass does not work and
# seems to be senseless anyway.
if cls.__name__ == "User":
return relationship("User",
primaryjoin="User.id==%s.uid" % cls.__name__)
else:
# The backref is private and only used to place the cascading
# behaviour.
backref_name = "__%s_item" % cls.__name__.lower()
return relationship("User",
primaryjoin="User.id==%s.uid" % cls.__name__,
backref=backref(backref_name,
cascade=cls._owner_cascades))
@declared_attr
def gid(cls):
fk_name = "fk_%s_%s_%s" % (cls.__tablename__, 'gid', 'usergroups')
return Column(Integer, ForeignKey('usergroups.id',
use_alter=True,
name=fk_name))
@declared_attr
def group(cls):
return relationship("Usergroup",
primaryjoin="Usergroup.id==%s.gid" % cls.__name__)
def is_owner(self, user):
"""Returns true if the Item is owned by the given user."""
return user.id == self.uid
def is_member(self, user):
if self.group:
return user.id in [user.id for user in self.group.members]
else:
return False
[docs]class Nested(object):
"""Mixin to make nested (self-reference) Items possible. Each item
can have a parent item and many children. The class will add two
relation attribute to the inheriting class. The parent item is
available under the *parent* attribute. The children items are
available under the *children* attribute."""
@declared_attr
def parent_id(cls):
name = "%s.id" % cls.__tablename__
return Column(Integer, ForeignKey(name))
@declared_attr
def children(cls):
name = cls.__name__
join = "%s.id==%s.parent_id" % (name, name)
return relationship(name,
primaryjoin=join,
backref=backref('parent', remote_side=[cls.id]),
cascade="all")
def get_parents(self):
"""Return a list of all parents of the current item
:returns: List of BaseItems
"""
parents = []
if not self.parent:
return parents
else:
parents.append(self.parent)
parents.extend(self.parent.get_parents())
return parents
def get_children(self):
"""Returns a list of all children und subchildren
:returns: List of BaseItems
"""
childs = []
for child in self.children:
childs.append(child)
childs.extend(child.get_children())
return childs