# ============================================================================
#
# Copyright (C) 2007-2016 Conceptive Engineering bvba.
# www.conceptive.be / info@conceptive.be
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Conceptive Engineering nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# ============================================================================
"""
Camelot extends the SQLAlchemy column types with a number of its own column
types. Those field types are automatically mapped to a specific delegate taking
care of the visualisation.
Those fields are stored in the :mod:`camelot.types` module.
"""
import collections
import logging
import string
logger = logging.getLogger('camelot.types')
import six
from sqlalchemy import types
from camelot.core.orm import options
from camelot.core.files.storage import StoredFile, StoredImage, Storage
"""
The `__repr__` method of the types is implemented to be able to use Alembic.
"""
[docs]class PrimaryKey(types.TypeDecorator):
"""Special type that can be used as the column type for a primary key. This
type defererring the definition of the actual type of primary key to
compilation time. This allows the changing of the primary key type through
the whole model by changing the `options.DEFAULT_AUTO_PRIMARYKEY_TYPE`
"""
impl = types.TypeEngine
_type_affinity = types.Integer
def load_dialect_impl(self, dialect):
return options.DEFAULT_AUTO_PRIMARYKEY_TYPE()
@property
def python_type(self):
return options.DEFAULT_AUTO_PRIMARYKEY_TYPE().python_type
def __repr__(self):
return 'PrimaryKey()'
virtual_address = collections.namedtuple('virtual_address',
['type', 'address'])
[docs]class VirtualAddress(types.TypeDecorator):
"""A single field that can be used to enter phone numbers, fax numbers, email
addresses, im addresses. The editor provides soft validation of the data
entered. The address or number is stored as a string in the database.
This column type accepts and returns tuples of strings, the first string is
the :attr:`virtual_address_type`, and the second the address itself.
eg: ``('email','project-camelot@conceptive.be')`` is stored as
``email://project-camelot@conceptive.be``
.. image:: /_static/virtualaddress_editor.png
"""
impl = types.Unicode
virtual_address_types = ['phone', 'fax', 'mobile', 'email', 'im', 'pager',]
@property
def python_type(self):
return virtual_address
def bind_processor(self, dialect):
impl_processor = self.impl.bind_processor(dialect)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
if value[1]:
value = u'://'.join(value)
else:
value = None
return impl_processor(value)
return processor
def result_processor(self, dialect, coltype=None):
impl_processor = self.impl.result_processor(dialect, coltype)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value:
split = value.split('://')
if len(split)>1:
return virtual_address(*split)
return virtual_address(u'phone',u'')
return processor
def __repr__(self):
return 'VirtualAddress()'
class _RegexpTranslator(object):
def __getitem__(self, ch):
if chr(ch) in '<>!':
return None
return ch
if six.PY3:
_translator = _RegexpTranslator()
else:
_translator = string.maketrans('', '')
[docs]class Code(types.TypeDecorator):
"""SQLAlchemy column type to store codes. Where a code is a list of strings
on which a regular expression can be enforced.
This column type accepts and returns a list of strings and stores them as a
string joined with points.
eg: ``['08', 'AB']`` is stored as ``08.AB``
.. image:: /_static/editors/CodeEditor_editable.png
:param parts: a list of input masks specifying the mask for each part,
eg ``['99', 'AA']``. For valid input masks, see the documentation of
:class:`QtWidgets.QLineEdit`.
:param separator: a string that will be used to separate the different parts
in the GUI and in the database
:param length: the size of the underlying string field in the database, if no
length is specified, it will be calculated from the parts
"""
impl = types.Unicode
@property
def python_type(self):
return tuple
def __init__(self, parts=['AB'], separator=u'.', length = None, **kwargs):
self.parts = parts
self.separator = separator
max_length = sum(len(part.translate(_translator)) for part in parts) + len(parts)*len(self.separator)
types.TypeDecorator.__init__( self, length = length or max_length, **kwargs )
def bind_processor(self, dialect):
impl_processor = self.impl.bind_processor(dialect)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
value = self.separator.join(value)
return impl_processor(value)
return processor
def result_processor(self, dialect, coltype=None):
impl_processor = self.impl.result_processor(dialect, coltype)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value:
return value.split(self.separator)
return ['' for _p in self.parts]
return processor
def __repr__(self):
return 'Code()'
class IPAddress(Code):
def __init__(self, **kwargs):
super(IPAddress, self).__init__(parts=['900','900','900','900'])
def __repr__(self):
return 'IPAddress()'
[docs]class Rating(types.TypeDecorator):
"""The rating field is an integer field that is visualized as a number of stars that
can be selected::
class Movie( Entity ):
title = Column( Unicode(60), nullable = False )
rating = Column( camelot.types.Rating() )
.. image:: /_static/editors/StarEditor_editable.png
"""
impl = types.Integer
@property
def python_type(self):
return self.impl.python_type
[docs]class RichText(types.TypeDecorator):
"""RichText fields are unlimited text fields which contain html. The html will be
rendered in a rich text editor.
.. image:: /_static/editors/RichTextEditor_editable.png
"""
impl = types.UnicodeText
@property
def python_type(self):
return self.impl.python_type
def __repr__(self):
return 'RichText()'
[docs]class Language(types.TypeDecorator):
"""The languages are stored as a string in the database of
the form *language*(_*country*), where :
* *language* is a lowercase, two-letter, ISO 639 language code,
* *territory* is an uppercase, two-letter, ISO 3166 country code
This used to be implemented using babel, but this was too slow and
used too much memory, so now it's implemented using QT.
"""
impl = types.Unicode
def __init__(self):
types.TypeDecorator.__init__(self, length=20)
@property
def python_type(self):
return self.impl.python_type
def __repr__(self):
return 'Language()'
color = collections.namedtuple('color',
('red', 'green', 'blue', 'alpha'))
[docs]class Color(types.TypeDecorator):
"""The Color field returns and accepts tuples of the form (r,g,b,a) where
r,g,b,a are integers between 0 and 255. The color is stored as an hexadecimal
string of the form AARRGGBB into the database, where AA is the transparency,
RR is red, GG is green BB is blue::
class MovieType( Entity ):
color = Column( camelot.types.Color() )
.. image:: /_static/editors/ColorEditor_editable.png
The colors are stored in the database as strings.
Use::
QColor(*color)
to convert a color tuple to a QColor.
"""
impl = types.Unicode
def __init__(self):
types.TypeDecorator.__init__(self, length=8)
def bind_processor(self, dialect):
impl_processor = self.impl.bind_processor(dialect)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
assert len(value) == 4
for i in range(4):
assert value[i] >= 0
assert value[i] <= 255
return '%02X%02X%02X%02X'%(value[3], value[0], value[1], value[2])
return impl_processor(value)
return processor
def result_processor(self, dialect, coltype=None):
impl_processor = self.impl.result_processor(dialect, coltype)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value:
return color(int(value[2:4],16), int(value[4:6],16), int(value[6:8],16), int(value[0:2],16))
return processor
@property
def python_type(self):
return color
def __repr__(self):
return 'Color()'
[docs]class Enumeration(types.TypeDecorator):
"""The enumeration field stores integers in the database, but represents them as
strings. This allows efficient storage and querying while preserving readable code.
Typical use of this field would be a status field.
Enumeration fields are visualized as a combo box, where the labels in the combo
box are the capitalized strings::
class Movie(Entity):
title = Column( Unicode(60), nullable = False )
state = Column( camelot.types.Enumeration([(1,'planned'), (2,'recording'), (3,'finished'), (4,'canceled')]),
index = True, nullable = False, default = 'planned' )
.. image:: /_static/editors/ChoicesEditor_editable.png
If None should be a possible value of the enumeration, add (None, None) to the list of
possible enumerations. None will be presented as empty in the GUI.
:param choices: is a list of tuples. each tuple contains an integer and its
associated string. such as ::
choices = [(1,'draft'), (2,'approved')]
"""
impl = types.Integer
def __init__(self, choices=[], **kwargs):
types.TypeDecorator.__init__(self, **kwargs)
self._int_to_string = dict(choices)
self._string_to_int = dict((str_value,int_key) for (int_key,str_value) in choices)
self.choices = [value for (_key,value) in choices]
def bind_processor(self, dialect):
impl_processor = self.impl.bind_processor(dialect)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
try:
value = self._string_to_int[value]
return impl_processor(value)
except KeyError as e:
logger.error('could not process enumeration value %s, possible values are %s'%(value, u', '.join(list(six.iterkeys(self._string_to_int)))), exc_info=e)
raise
else:
impl_processor(value)
return processor
def result_processor(self, dialect, coltype=None):
impl_processor = self.impl.result_processor(dialect, coltype)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
value = impl_processor(value)
try:
return self._int_to_string[value]
except KeyError as e:
logger.error('could not process %s'%value, exc_info=e)
raise
return processor
@property
def python_type(self):
return str
def __repr__(self):
return 'Enumeration()'
[docs]class File(types.TypeDecorator):
"""Sqlalchemy column type to store files. Only the location of the file is stored
This column type accepts and returns a StoredFile. The name of the file is
stored as a string in the database. A subdirectory upload_to can be specified::
class Movie( Entity ):
script = Column( camelot.types.File( upload_to = 'script' ) )
.. image:: /_static/editors/FileEditor_editable.png
Retrieving the actual storage from a File field can be a little cumbersome.
The easy way is taking it from the field attributes, in which it will be
put by default. If no field attributes are available at the location where
the storage is needed, eg in some function doing document processing, one
needs to go through SQLAlchemy to retrieve it.
For an 'task' object with a File field named 'document', the
storage can be retrieved::
from sqlalchemy import orm
task_mapper = orm.object_mapper( task )
document_property = task_mapper.get_property('document')
storage = document_property.columns[0].type.storage
:param max_length: the maximum length of the name of the file that will
be saved in the database.
:param upload_to: a subdirectory in the Storage, in which the the file
should be stored.
:param storage: an alternative storage to use for this field.
"""
impl = types.Unicode
stored_file_implementation = StoredFile
def __init__(self, max_length=100, upload_to=u'', storage=Storage, **kwargs):
self.max_length = max_length
self.storage = storage(upload_to, self.stored_file_implementation)
types.TypeDecorator.__init__(self, length=max_length, **kwargs)
def bind_processor(self, dialect):
impl_processor = self.impl.bind_processor(dialect)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value is not None:
assert isinstance(value, (self.stored_file_implementation))
return impl_processor(value.name)
return impl_processor(value)
return processor
def result_processor(self, dialect, coltype=None):
impl_processor = self.impl.result_processor(dialect, coltype)
if not impl_processor:
impl_processor = lambda x:x
def processor(value):
if value:
value = impl_processor(value)
return self.stored_file_implementation(self.storage, value)
return processor
@property
def python_type(self):
return self.impl.python_type
def __repr__(self):
return 'File()'
[docs]class Image(File):
"""Sqlalchemy column type to store images
This column type accepts and returns a StoredImage, and stores them in the directory
specified by settings.CAMELOT_MEDIA_ROOT. The name of the file is stored as a string in
the database.
The Image field type provides the same functionallity as the File field type, but
the files stored should be images.
.. image:: /_static/editors/ImageEditor_editable.png
"""
stored_file_implementation = StoredImage
def __repr__(self):
return 'Image()'