Update elixir
This commit is contained in:
@@ -38,7 +38,7 @@ from elixir.statements import Statement
|
||||
from elixir.collection import EntityCollection, GlobalEntityCollection
|
||||
|
||||
|
||||
__version__ = '0.7.1'
|
||||
__version__ = '0.8.0dev'
|
||||
|
||||
__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection',
|
||||
'entities',
|
||||
@@ -85,11 +85,6 @@ def drop_all(*args, **kwargs):
|
||||
def setup_all(create_tables=False, *args, **kwargs):
|
||||
'''Setup the table and mapper of all entities in the default entity
|
||||
collection.
|
||||
|
||||
This is called automatically if any entity of the collection is configured
|
||||
with the `autosetup` option and it is first accessed,
|
||||
instanciated (called) or the create_all method of a metadata containing
|
||||
tables from any of those entities is called.
|
||||
'''
|
||||
setup_entities(entities)
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ Default entity collection implementation
|
||||
import sys
|
||||
import re
|
||||
|
||||
from elixir.py23compat import rsplit
|
||||
|
||||
class BaseCollection(list):
|
||||
def __init__(self, entities=None):
|
||||
list.__init__(self)
|
||||
@@ -24,7 +22,7 @@ class BaseCollection(list):
|
||||
root = entity._descriptor.resolve_root
|
||||
if root:
|
||||
full_path = '%s.%s' % (root, full_path)
|
||||
module_path, classname = rsplit(full_path, '.', 1)
|
||||
module_path, classname = full_path.rsplit('.', 1)
|
||||
module = sys.modules[module_path]
|
||||
res = getattr(module, classname, None)
|
||||
if res is None:
|
||||
|
||||
+32
-165
@@ -3,8 +3,6 @@ This module provides the ``Entity`` base class, as well as its metaclass
|
||||
``EntityMeta``.
|
||||
'''
|
||||
|
||||
from py23compat import sorted
|
||||
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
@@ -25,11 +23,6 @@ from elixir import options
|
||||
from elixir.properties import Property
|
||||
|
||||
DEBUG = False
|
||||
try:
|
||||
from sqlalchemy.orm import EXT_PASS
|
||||
SA05orlater = False
|
||||
except ImportError:
|
||||
SA05orlater = True
|
||||
|
||||
__doc_all__ = ['Entity', 'EntityMeta']
|
||||
|
||||
@@ -205,6 +198,7 @@ class EntityDescriptor(object):
|
||||
parent_desc = self.parent._descriptor
|
||||
tablename = parent_desc.table_fullname
|
||||
join_clauses = []
|
||||
fk_columns = []
|
||||
for pk_col in parent_desc.primary_keys:
|
||||
colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
|
||||
{'entity': self.parent.__name__.lower(),
|
||||
@@ -214,12 +208,14 @@ class EntityDescriptor(object):
|
||||
# a real column object when said column is not yet
|
||||
# attached to a table
|
||||
pk_col_name = "%s.%s" % (tablename, pk_col.key)
|
||||
fk = ForeignKey(pk_col_name, ondelete='cascade')
|
||||
col = Column(colname, pk_col.type, fk,
|
||||
primary_key=True)
|
||||
col = Column(colname, pk_col.type, primary_key=True)
|
||||
fk_columns.append(col)
|
||||
self.add_column(col)
|
||||
join_clauses.append(col == pk_col)
|
||||
self.join_condition = and_(*join_clauses)
|
||||
self.add_constraint(
|
||||
ForeignKeyConstraint(fk_columns,
|
||||
parent_desc.primary_keys, ondelete='CASCADE'))
|
||||
elif self.inheritance == 'concrete':
|
||||
# Copy primary key columns from the parent.
|
||||
for col in self.parent._descriptor.columns:
|
||||
@@ -286,7 +282,7 @@ class EntityDescriptor(object):
|
||||
self.add_constraint(
|
||||
ForeignKeyConstraint(
|
||||
[e.parent.key for e in con.elements],
|
||||
[e._get_colspec() for e in con.elements],
|
||||
[e.target_fullname for e in con.elements],
|
||||
name=con.name, #TODO: modify it
|
||||
onupdate=con.onupdate, ondelete=con.ondelete,
|
||||
use_alter=con.use_alter))
|
||||
@@ -370,6 +366,11 @@ class EntityDescriptor(object):
|
||||
|
||||
order = []
|
||||
for colname in order_by:
|
||||
#FIXME: get_column uses self.columns[key] instead of property
|
||||
# names. self.columns correspond to the columns of the table if
|
||||
# the table was already created and to self._columns otherwise,
|
||||
# which is a ColumnCollection indexed on columns.key
|
||||
# See ticket #108.
|
||||
col = self.get_column(colname.strip('-'))
|
||||
if colname.startswith('-'):
|
||||
col = desc(col)
|
||||
@@ -493,17 +494,13 @@ class EntityDescriptor(object):
|
||||
(col.key, self.entity.__name__))
|
||||
else:
|
||||
del self._columns[col.key]
|
||||
# are indexed on col.key
|
||||
self._columns.add(col)
|
||||
|
||||
if col.primary_key:
|
||||
self.has_pk = True
|
||||
|
||||
# Autosetup triggers shouldn't be active anymore at this point, so we
|
||||
# can theoretically access the entity's table safely. But the problem
|
||||
# is that if, for some reason, the trigger removal phase didn't
|
||||
# happen, we'll get an infinite loop. So we just make sure we don't
|
||||
# get one in any case.
|
||||
table = type.__getattribute__(self.entity, 'table')
|
||||
table = self.entity.table
|
||||
if table is not None:
|
||||
if check_duplicate and col.key in table.columns.keys():
|
||||
raise Exception("Column '%s' already exist in table '%s' ! " %
|
||||
@@ -595,6 +592,7 @@ class EntityDescriptor(object):
|
||||
#------------------------
|
||||
# some useful properties
|
||||
|
||||
@property
|
||||
def table_fullname(self):
|
||||
'''
|
||||
Complete name of the table for the related entity.
|
||||
@@ -605,8 +603,8 @@ class EntityDescriptor(object):
|
||||
return "%s.%s" % (schema, self.tablename)
|
||||
else:
|
||||
return self.tablename
|
||||
table_fullname = property(table_fullname)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
if self.entity.table is not None:
|
||||
return self.entity.table.columns
|
||||
@@ -615,8 +613,8 @@ class EntityDescriptor(object):
|
||||
# return the parent entity's columns (for example for order_by
|
||||
# using a column defined in the parent.
|
||||
return self._columns
|
||||
columns = property(columns)
|
||||
|
||||
@property
|
||||
def primary_keys(self):
|
||||
"""
|
||||
Returns the list of primary key columns of the entity.
|
||||
@@ -630,15 +628,15 @@ class EntityDescriptor(object):
|
||||
return self.parent._descriptor.primary_keys
|
||||
else:
|
||||
return [col for col in self.columns if col.primary_key]
|
||||
primary_keys = property(primary_keys)
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
if self.entity.table is not None:
|
||||
return self.entity.table
|
||||
else:
|
||||
return FakeTable(self)
|
||||
table = property(table)
|
||||
|
||||
@property
|
||||
def primary_key_properties(self):
|
||||
"""
|
||||
Returns the list of (mapper) properties corresponding to the primary
|
||||
@@ -653,30 +651,32 @@ class EntityDescriptor(object):
|
||||
for prop in mapper.iterate_properties:
|
||||
if isinstance(prop, ColumnProperty):
|
||||
for col in prop.columns:
|
||||
#XXX: Why is this extra loop necessary? What is this
|
||||
# "proxy_set" supposed to mean?
|
||||
for col in col.proxy_set:
|
||||
col_to_prop[col] = prop
|
||||
pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
|
||||
self._pk_props = [col_to_prop[c] for c in pk_cols]
|
||||
return self._pk_props
|
||||
primary_key_properties = property(primary_key_properties)
|
||||
|
||||
class FakePK(object):
|
||||
def __init__(self, descriptor):
|
||||
self.descriptor = descriptor
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return self.descriptor.primary_keys
|
||||
columns = property(columns)
|
||||
|
||||
class FakeTable(object):
|
||||
def __init__(self, descriptor):
|
||||
self.descriptor = descriptor
|
||||
self.primary_key = FakePK(descriptor)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return self.descriptor.columns
|
||||
columns = property(columns)
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
'''
|
||||
Complete name of the table for the related entity.
|
||||
@@ -687,46 +687,8 @@ class FakeTable(object):
|
||||
return "%s.%s" % (schema, self.descriptor.tablename)
|
||||
else:
|
||||
return self.descriptor.tablename
|
||||
fullname = property(fullname)
|
||||
|
||||
|
||||
class TriggerProxy(object):
|
||||
"""
|
||||
A class that serves as a "trigger" ; accessing its attributes runs
|
||||
the setup_all function.
|
||||
|
||||
Note that the `setup_all` is called on each access of the attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, class_, attrname):
|
||||
self.class_ = class_
|
||||
self.attrname = attrname
|
||||
|
||||
def __getattr__(self, name):
|
||||
elixir.setup_all()
|
||||
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
|
||||
#remove the triggers for this entity. This can happen if the entity is
|
||||
#not in the `entities` list for some reason.
|
||||
proxied_attr = getattr(self.class_, self.attrname)
|
||||
return getattr(proxied_attr, name)
|
||||
|
||||
def __repr__(self):
|
||||
proxied_attr = getattr(self.class_, self.attrname)
|
||||
return "<TriggerProxy (%s)>" % (self.class_.__name__)
|
||||
|
||||
|
||||
class TriggerAttribute(object):
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.attrname = attrname
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
|
||||
#remove the triggers for this entity. This can happen if the entity is
|
||||
#not in the `entities` list for some reason.
|
||||
elixir.setup_all()
|
||||
return getattr(owner, self.attrname)
|
||||
|
||||
def is_entity(cls):
|
||||
"""
|
||||
Scan the bases classes of `cls` to see if any is an instance of
|
||||
@@ -805,13 +767,6 @@ def instrument_class(cls):
|
||||
# setup misc options here (like tablename etc.)
|
||||
desc.setup_options()
|
||||
|
||||
# create trigger proxies
|
||||
# TODO: support entity_name... It makes sense only for autoloaded
|
||||
# tables for now, and would make more sense if we support "external"
|
||||
# tables
|
||||
if desc.autosetup:
|
||||
_install_autosetup_triggers(cls)
|
||||
|
||||
|
||||
class EntityMeta(type):
|
||||
"""
|
||||
@@ -823,11 +778,6 @@ class EntityMeta(type):
|
||||
def __init__(cls, name, bases, dict_):
|
||||
instrument_class(cls)
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls._descriptor.autosetup and not hasattr(cls, '_setup_done'):
|
||||
elixir.setup_all()
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
def __setattr__(cls, key, value):
|
||||
if isinstance(value, Property):
|
||||
if hasattr(cls, '_setup_done'):
|
||||
@@ -839,84 +789,6 @@ class EntityMeta(type):
|
||||
type.__setattr__(cls, key, value)
|
||||
|
||||
|
||||
def _install_autosetup_triggers(cls, entity_name=None):
|
||||
#TODO: move as much as possible of those "_private" values to the
|
||||
# descriptor, so that we don't mess the initial class.
|
||||
warnings.warn("The 'autosetup' option on entities is deprecated. "
|
||||
"Please call setup_all() manually after all your entities have been "
|
||||
"declared.", DeprecationWarning, stacklevel=4)
|
||||
tablename = cls._descriptor.tablename
|
||||
schema = cls._descriptor.table_options.get('schema', None)
|
||||
cls._table_key = sqlalchemy.schema._get_table_key(tablename, schema)
|
||||
|
||||
table_proxy = TriggerProxy(cls, 'table')
|
||||
|
||||
md = cls._descriptor.metadata
|
||||
md.tables[cls._table_key] = table_proxy
|
||||
|
||||
# We need to monkeypatch the metadata's table iterator method because
|
||||
# otherwise it doesn't work if the setup is triggered by the
|
||||
# metadata.create_all().
|
||||
# This is because ManyToMany relationships add tables AFTER the list
|
||||
# of tables that are going to be created is "computed"
|
||||
# (metadata.tables.values()).
|
||||
# see:
|
||||
# - table_iterator method in MetaData class in sqlalchemy/schema.py
|
||||
# - visit_metadata method in sqlalchemy/ansisql.py
|
||||
if SA05orlater:
|
||||
warnings.warn(
|
||||
"The automatic setup via metadata.create_all() through "
|
||||
"the autosetup option doesn't work with SQLAlchemy 0.5 and later!")
|
||||
else:
|
||||
# SA 0.6 does not use table_iterator anymore (it was already deprecated
|
||||
# since SA 0.5.0)
|
||||
original_table_iterator = md.table_iterator
|
||||
if not hasattr(original_table_iterator,
|
||||
'_non_elixir_patched_iterator'):
|
||||
def table_iterator(*args, **kwargs):
|
||||
elixir.setup_all()
|
||||
return original_table_iterator(*args, **kwargs)
|
||||
table_iterator.__doc__ = original_table_iterator.__doc__
|
||||
table_iterator._non_elixir_patched_iterator = \
|
||||
original_table_iterator
|
||||
md.table_iterator = table_iterator
|
||||
|
||||
#TODO: we might want to add all columns that will be available as
|
||||
#attributes on the class itself (in SA 0.4+). This is a pretty
|
||||
#rare usecase, as people will normally hit the query attribute before the
|
||||
#column attributes, but I've seen people hitting this problem...
|
||||
for name in ('c', 'table', 'mapper', 'query'):
|
||||
setattr(cls, name, TriggerAttribute(name))
|
||||
|
||||
cls._has_triggers = True
|
||||
|
||||
|
||||
def _cleanup_autosetup_triggers(cls):
|
||||
if not hasattr(cls, '_has_triggers'):
|
||||
return
|
||||
|
||||
for name in ('table', 'mapper'):
|
||||
setattr(cls, name, None)
|
||||
|
||||
for name in ('c', 'query'):
|
||||
delattr(cls, name)
|
||||
|
||||
desc = cls._descriptor
|
||||
md = desc.metadata
|
||||
|
||||
# the fake table could have already been removed (namely in a
|
||||
# single table inheritance scenario)
|
||||
md.tables.pop(cls._table_key, None)
|
||||
|
||||
# restore original table iterator if not done already
|
||||
if not SA05orlater:
|
||||
if hasattr(md.table_iterator, '_non_elixir_patched_iterator'):
|
||||
md.table_iterator = \
|
||||
md.table_iterator._non_elixir_patched_iterator
|
||||
|
||||
del cls._has_triggers
|
||||
|
||||
|
||||
def setup_entities(entities):
|
||||
'''Setup all entities in the list passed as argument'''
|
||||
|
||||
@@ -928,9 +800,6 @@ def setup_entities(entities):
|
||||
if isinstance(attr, Property):
|
||||
delattr(entity, name)
|
||||
|
||||
if entity._descriptor.autosetup:
|
||||
_cleanup_autosetup_triggers(entity)
|
||||
|
||||
for method_name in (
|
||||
'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
|
||||
'before_table', 'setup_table', 'setup_reltables', 'after_table',
|
||||
@@ -955,8 +824,7 @@ def setup_entities(entities):
|
||||
def cleanup_entities(entities):
|
||||
"""
|
||||
Try to revert back the list of entities passed as argument to the state
|
||||
they had just before their setup phase. It will not work entirely for
|
||||
autosetup entities as we need to remove the autosetup triggers.
|
||||
they had just before their setup phase.
|
||||
|
||||
As of now, this function is *not* functional in that it doesn't revert to
|
||||
the exact same state the entities were before setup. For example, the
|
||||
@@ -968,8 +836,6 @@ def cleanup_entities(entities):
|
||||
"""
|
||||
for entity in entities:
|
||||
desc = entity._descriptor
|
||||
if desc.autosetup:
|
||||
_cleanup_autosetup_triggers(entity)
|
||||
|
||||
if hasattr(entity, '_setup_done'):
|
||||
del entity._setup_done
|
||||
@@ -1007,6 +873,7 @@ class EntityBase(object):
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def update_or_create(cls, data, surrogate=True):
|
||||
pk_props = cls._descriptor.primary_key_properties
|
||||
|
||||
@@ -1016,17 +883,16 @@ class EntityBase(object):
|
||||
record = cls.query.get(pk_tuple)
|
||||
if record is None:
|
||||
if surrogate:
|
||||
raise Exception("cannot create surrogate with pk")
|
||||
raise Exception("Cannot create surrogate with pk")
|
||||
else:
|
||||
record = cls()
|
||||
else:
|
||||
if surrogate:
|
||||
record = cls()
|
||||
else:
|
||||
raise Exception("cannot create non surrogate without pk")
|
||||
raise Exception("Cannot create non surrogate without pk")
|
||||
record.from_dict(data)
|
||||
return record
|
||||
update_or_create = classmethod(update_or_create)
|
||||
|
||||
def from_dict(self, data):
|
||||
"""
|
||||
@@ -1105,10 +971,11 @@ class EntityBase(object):
|
||||
|
||||
# This bunch of session methods, along with all the query methods below
|
||||
# only make sense when using a global/scoped/contextual session.
|
||||
@property
|
||||
def _global_session(self):
|
||||
return self._descriptor.session.registry()
|
||||
_global_session = property(_global_session)
|
||||
|
||||
#FIXME: remove all deprecated methods, possibly all of these
|
||||
def merge(self, *args, **kwargs):
|
||||
return self._global_session.merge(self, *args, **kwargs)
|
||||
|
||||
@@ -1126,6 +993,7 @@ class EntityBase(object):
|
||||
return self._global_session.save_or_update(self, *args, **kwargs)
|
||||
|
||||
# query methods
|
||||
@classmethod
|
||||
def get_by(cls, *args, **kwargs):
|
||||
"""
|
||||
Returns the first instance of this class matching the given criteria.
|
||||
@@ -1133,8 +1001,8 @@ class EntityBase(object):
|
||||
session.query(MyClass).filter_by(...).first()
|
||||
"""
|
||||
return cls.query.filter_by(*args, **kwargs).first()
|
||||
get_by = classmethod(get_by)
|
||||
|
||||
@classmethod
|
||||
def get(cls, *args, **kwargs):
|
||||
"""
|
||||
Return the instance of this class based on the given identifier,
|
||||
@@ -1142,7 +1010,6 @@ class EntityBase(object):
|
||||
session.query(MyClass).get(...)
|
||||
"""
|
||||
return cls.query.get(*args, **kwargs)
|
||||
get = classmethod(get)
|
||||
|
||||
|
||||
class Entity(EntityBase):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from sqlalchemy.orm import reconstructor
|
||||
|
||||
__all__ = [
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
@@ -22,9 +24,4 @@ before_update = create_decorator('before_update')
|
||||
after_update = create_decorator('after_update')
|
||||
before_delete = create_decorator('before_delete')
|
||||
after_delete = create_decorator('after_delete')
|
||||
try:
|
||||
from sqlalchemy.orm import reconstructor
|
||||
except ImportError:
|
||||
def reconstructor(func):
|
||||
raise Exception('The reconstructor method decorator is only '
|
||||
'available with SQLAlchemy 0.5 and later')
|
||||
|
||||
|
||||
@@ -222,12 +222,12 @@ def associable(assoc_entity, plural_name=None, lazy=True):
|
||||
|
||||
# add helper methods
|
||||
def select_by(cls, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
return cls.query.join(attr_name, 'targets') \
|
||||
.filter_by(**kwargs).all()
|
||||
setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
|
||||
|
||||
def select(cls, *args, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
return cls.query.join(attr_name, 'targets') \
|
||||
.filter(*args, **kwargs).all()
|
||||
setattr(entity, 'select_%s' % self.name, classmethod(select))
|
||||
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
'''
|
||||
This extension is DEPRECATED. Please use the orderinglist SQLAlchemy
|
||||
extension instead.
|
||||
|
||||
For details:
|
||||
http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html
|
||||
|
||||
For an Elixir example:
|
||||
http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList
|
||||
or
|
||||
http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155
|
||||
|
||||
|
||||
|
||||
An ordered-list plugin for Elixir to help you make an entity be able to be
|
||||
managed in a list-like way. Much inspiration comes from the Ruby on Rails
|
||||
acts_as_list plugin, which is currently more full-featured than this plugin.
|
||||
|
||||
Once you flag an entity with an `acts_as_list()` statement, a column will be
|
||||
added to the entity called `position` which will be an integer column that is
|
||||
managed for you by the plugin. You can pass an alternative column name to
|
||||
the plugin using the `column_name` keyword argument.
|
||||
|
||||
In addition, your entity will get a series of new methods attached to it,
|
||||
including:
|
||||
|
||||
+----------------------+------------------------------------------------------+
|
||||
| Method Name | Description |
|
||||
+======================+======================================================+
|
||||
| ``move_lower`` | Move the item lower in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_higher`` | Move the item higher in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_bottom`` | Move the item to the bottom of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_top`` | Move the item to the top of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to`` | Move the item to a specific position in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
|
||||
|
||||
Sometimes, your entities that represent list items will be a part of different
|
||||
lists. To implement this behavior, simply pass the `acts_as_list` statement a
|
||||
callable that returns a "qualifier" SQLAlchemy expression. This expression will
|
||||
be added to the generated WHERE clauses used by the plugin.
|
||||
|
||||
Example model usage:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from elixir import *
|
||||
from elixir.ext.list import acts_as_list
|
||||
|
||||
class ToDo(Entity):
|
||||
subject = Field(String(128))
|
||||
owner = ManyToOne('Person')
|
||||
|
||||
def qualify(self):
|
||||
return ToDo.owner_id == self.owner_id
|
||||
|
||||
acts_as_list(qualifier=qualify)
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(String(64))
|
||||
todos = OneToMany('ToDo', order_by='position')
|
||||
|
||||
|
||||
The above example can then be used to manage ordered todo lists for people.
|
||||
Note that you must set the `order_by` property on the `Person.todo` relation in
|
||||
order for the relation to respect the ordering. Here is an example of using
|
||||
this model in practice:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos.append(ToDo(subject='Three'))
|
||||
p.todos.append(ToDo(subject='Two'))
|
||||
p.todos.append(ToDo(subject='One'))
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos[0].move_to_bottom()
|
||||
p.todos[2].move_to_top()
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
assert p.todos[0].subject == 'One'
|
||||
assert p.todos[1].subject == 'Two'
|
||||
assert p.todos[2].subject == 'Three'
|
||||
|
||||
|
||||
For more examples, refer to the unit tests for this plugin.
|
||||
'''
|
||||
|
||||
from elixir.statements import Statement
|
||||
from elixir.events import before_insert, before_delete
|
||||
from sqlalchemy import Column, Integer, select, func, literal, and_
|
||||
import warnings
|
||||
|
||||
__all__ = ['acts_as_list']
|
||||
__doc_all__ = []
|
||||
|
||||
|
||||
def get_entity_where(instance):
|
||||
clauses = []
|
||||
for column in instance.table.primary_key.columns:
|
||||
instance_value = getattr(instance, column.name)
|
||||
clauses.append(column == instance_value)
|
||||
return and_(*clauses)
|
||||
|
||||
|
||||
class ListEntityBuilder(object):
|
||||
|
||||
def __init__(self, entity, qualifier=None, column_name='position'):
|
||||
warnings.warn("The act_as_list extension is deprecated. Please use "
|
||||
"SQLAlchemy's orderinglist extension instead",
|
||||
DeprecationWarning, stacklevel=6)
|
||||
self.entity = entity
|
||||
self.qualifier_method = qualifier
|
||||
self.column_name = column_name
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
if self.entity._descriptor.autoload:
|
||||
for c in self.entity.table.c:
|
||||
if c.name == self.column_name:
|
||||
self.position_column = c
|
||||
if not hasattr(self, 'position_column'):
|
||||
raise Exception(
|
||||
"Could not find column '%s' in autoloaded table '%s', "
|
||||
"needed by entity '%s'." % (self.column_name,
|
||||
self.entity.table.name, self.entity.__name__))
|
||||
else:
|
||||
self.position_column = Column(self.column_name, Integer)
|
||||
self.entity._descriptor.add_column(self.position_column)
|
||||
|
||||
def after_table(self):
|
||||
position_column = self.position_column
|
||||
position_column_name = self.column_name
|
||||
|
||||
qualifier_method = self.qualifier_method
|
||||
if not qualifier_method:
|
||||
qualifier_method = lambda self: None
|
||||
|
||||
def _init_position(self):
|
||||
s = select(
|
||||
[(func.max(position_column)+1).label('value')],
|
||||
qualifier_method(self)
|
||||
).union(
|
||||
select([literal(1).label('value')])
|
||||
)
|
||||
a = s.alias()
|
||||
# we use a second func.max to get the maximum between 1 and the
|
||||
# real max position if any exist
|
||||
setattr(self, position_column_name, select([func.max(a.c.value)]))
|
||||
|
||||
# Note that this method could be rewritten more simply like below,
|
||||
# but because this extension is going to be deprecated anyway,
|
||||
# I don't want to risk breaking something I don't want to maintain.
|
||||
# setattr(self, position_column_name, select(
|
||||
# [func.coalesce(func.max(position_column), 0) + 1],
|
||||
# qualifier_method(self)
|
||||
# ))
|
||||
_init_position = before_insert(_init_position)
|
||||
|
||||
def _shift_items(self):
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column > getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values={
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
_shift_items = before_delete(_shift_items)
|
||||
|
||||
def move_to_bottom(self):
|
||||
# move the items that were above this item up one
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column >= getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values = {
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
|
||||
# move this item to the max position
|
||||
# MySQL does not support the correlated subquery, so we need to
|
||||
# execute the query (through scalar()). See ticket #34.
|
||||
self.table.update(
|
||||
get_entity_where(self),
|
||||
values={
|
||||
position_column : select(
|
||||
[func.max(position_column) + 1],
|
||||
qualifier_method(self)
|
||||
).scalar()
|
||||
}
|
||||
).execute()
|
||||
|
||||
def move_to_top(self):
|
||||
self.move_to(1)
|
||||
|
||||
def move_to(self, position):
|
||||
current_position = getattr(self, position_column_name)
|
||||
|
||||
# determine which direction we're moving
|
||||
if position < current_position:
|
||||
where = and_(
|
||||
position <= position_column,
|
||||
position_column < current_position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = 1
|
||||
elif position > current_position:
|
||||
where = and_(
|
||||
current_position < position_column,
|
||||
position_column <= position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = -1
|
||||
|
||||
# shift the items in between the current and new positions
|
||||
self.table.update(where, values = {
|
||||
position_column : position_column + modifier
|
||||
}).execute()
|
||||
|
||||
# update this item's position to the desired position
|
||||
self.table.update(get_entity_where(self)) \
|
||||
.execute(**{position_column_name: position})
|
||||
|
||||
def move_lower(self):
|
||||
# replace for ex.: p.todos.insert(x + 1, p.todos.pop(x))
|
||||
self.move_to(getattr(self, position_column_name) + 1)
|
||||
|
||||
def move_higher(self):
|
||||
self.move_to(getattr(self, position_column_name) - 1)
|
||||
|
||||
|
||||
# attach new methods to entity
|
||||
self.entity._init_position = _init_position
|
||||
self.entity._shift_items = _shift_items
|
||||
self.entity.move_lower = move_lower
|
||||
self.entity.move_higher = move_higher
|
||||
self.entity.move_to_bottom = move_to_bottom
|
||||
self.entity.move_to_top = move_to_top
|
||||
self.entity.move_to = move_to
|
||||
|
||||
|
||||
acts_as_list = Statement(ListEntityBuilder)
|
||||
@@ -124,16 +124,6 @@ The list of supported arguments are as follows:
|
||||
| | module by setting the ``__session__`` attribute of |
|
||||
| | that module. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``autosetup`` | DEPRECATED. Specify whether that entity will contain |
|
||||
| | automatic setup triggers. |
|
||||
| | That is if this entity will be |
|
||||
| | automatically setup (along with all other entities |
|
||||
| | which were already declared) if any of the following |
|
||||
| | condition happen: some of its attributes are accessed |
|
||||
| | ('c', 'table', 'mapper' or 'query'), instanciated |
|
||||
| | (called) or the create_all method of this entity's |
|
||||
| | metadata is called. Defaults to ``False``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``allowcoloverride``| Specify whether it is allowed to override columns. |
|
||||
| | By default, Elixir forbids you to add a column to an |
|
||||
| | entity's table which already exist in that table. If |
|
||||
@@ -225,7 +215,6 @@ MIGRATION_TO_07_AID = False
|
||||
#
|
||||
options_defaults = dict(
|
||||
abstract=False,
|
||||
autosetup=False,
|
||||
inheritance='single',
|
||||
polymorphic=True,
|
||||
identity=None,
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Some helper functions to get by without Python 2.4
|
||||
|
||||
# set
|
||||
try:
|
||||
set = set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
orig_cmp = cmp
|
||||
# [].sort
|
||||
def sort_list(l, cmp=None, key=None, reverse=False):
|
||||
try:
|
||||
l.sort(cmp, key, reverse)
|
||||
except TypeError, e:
|
||||
if not str(e).startswith('sort expected at most 1 arguments'):
|
||||
raise
|
||||
if cmp is None:
|
||||
cmp = orig_cmp
|
||||
if key is not None:
|
||||
# the cmp=cmp parameter is required to get the original comparator
|
||||
# into the lambda namespace
|
||||
cmp = lambda self, other, cmp=cmp: cmp(key(self), key(other))
|
||||
if reverse:
|
||||
cmp = lambda self, other, cmp=cmp: -cmp(self,other)
|
||||
l.sort(cmp)
|
||||
|
||||
# sorted
|
||||
try:
|
||||
sorted = sorted
|
||||
except NameError:
|
||||
# global name 'sorted' doesn't exist in Python2.3
|
||||
# this provides a poor-man's emulation of the sorted built-in method
|
||||
def sorted(l, cmp=None, key=None, reverse=False):
|
||||
sorted_list = list(l)
|
||||
sort_list(sorted_list, cmp, key, reverse)
|
||||
return sorted_list
|
||||
|
||||
# rsplit
|
||||
try:
|
||||
''.rsplit
|
||||
def rsplit(s, delim, maxsplit):
|
||||
return s.rsplit(delim, maxsplit)
|
||||
|
||||
except AttributeError:
|
||||
def rsplit(s, delim, maxsplit):
|
||||
"""Return a list of the words of the string s, scanning s
|
||||
from the end. To all intents and purposes, the resulting
|
||||
list of words is the same as returned by split(), except
|
||||
when the optional third argument maxsplit is explicitly
|
||||
specified and nonzero. When maxsplit is nonzero, at most
|
||||
maxsplit number of splits - the rightmost ones - occur,
|
||||
and the remainder of the string is returned as the first
|
||||
element of the list (thus, the list will have at most
|
||||
maxsplit+1 elements). New in version 2.4.
|
||||
>>> rsplit('foo.bar.baz', '.', 0)
|
||||
['foo.bar.baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 1)
|
||||
['foo.bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 2)
|
||||
['foo', 'bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 99)
|
||||
['foo', 'bar', 'baz']
|
||||
"""
|
||||
assert maxsplit >= 0
|
||||
|
||||
if maxsplit == 0: return [s]
|
||||
|
||||
# the following lines perform the function, but inefficiently.
|
||||
# This may be adequate for compatibility purposes
|
||||
items = s.split(delim)
|
||||
if maxsplit < len(items):
|
||||
items[:-maxsplit] = [delim.join(items[:-maxsplit])]
|
||||
return items
|
||||
@@ -278,8 +278,9 @@ relationships accept the following optional (keyword) arguments:
|
||||
| | reference the "local"/current entity's table. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``table`` | Use a manually created table. If this argument is |
|
||||
| | used, Elixir won't generate a table for this |
|
||||
| | relationship, and use the one given instead. |
|
||||
| | used, Elixir will not generate a table for this |
|
||||
| | relationship, and use the one given instead. This |
|
||||
| | argument only accepts SQLAlchemy's Table objects. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``order_by`` | Specify which field(s) should be used to sort the |
|
||||
| | results given by accessing the relation field. |
|
||||
@@ -303,13 +304,6 @@ relationships accept the following optional (keyword) arguments:
|
||||
| ``table_kwargs`` | A dictionary holding any other keyword argument you |
|
||||
| | might want to pass to the underlying Table object. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``column_format`` | DEPRECATED. Specify an alternate format string for |
|
||||
| | naming the |
|
||||
| | columns in the mapping table. The default value is |
|
||||
| | defined in ``elixir.options.M2MCOL_NAMEFORMAT``. You |
|
||||
| | will be passed ``tablename``, ``key``, and ``entity`` |
|
||||
| | as arguments to the format string. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
|
||||
|
||||
================
|
||||
@@ -493,6 +487,7 @@ class Relationship(Property):
|
||||
self.property = relation(self.target, **kwargs)
|
||||
self.add_mapper_property(self.name, self.property)
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
if not self._target:
|
||||
if isinstance(self.of_kind, basestring):
|
||||
@@ -501,8 +496,8 @@ class Relationship(Property):
|
||||
else:
|
||||
self._target = self.of_kind
|
||||
return self._target
|
||||
target = property(target)
|
||||
|
||||
@property
|
||||
def inverse(self):
|
||||
if not hasattr(self, '_inverse'):
|
||||
if self.inverse_name:
|
||||
@@ -531,7 +526,6 @@ class Relationship(Property):
|
||||
inverse._inverse = self
|
||||
|
||||
return self._inverse
|
||||
inverse = property(inverse)
|
||||
|
||||
def match_type_of(self, other):
|
||||
return False
|
||||
@@ -612,12 +606,12 @@ class ManyToOne(Relationship):
|
||||
def match_type_of(self, other):
|
||||
return isinstance(other, (OneToMany, OneToOne))
|
||||
|
||||
@property
|
||||
def target_table(self):
|
||||
if isinstance(self.target, EntityMeta):
|
||||
return self.target._descriptor.table
|
||||
else:
|
||||
return class_mapper(self.target).local_table
|
||||
target_table = property(target_table)
|
||||
|
||||
def create_keys(self, pk):
|
||||
'''
|
||||
@@ -634,7 +628,10 @@ class ManyToOne(Relationship):
|
||||
source_desc = self.entity._descriptor
|
||||
if isinstance(self.target, EntityMeta):
|
||||
# make sure the target has all its pk set up
|
||||
#FIXME: this is not enough when specifying target_column manually,
|
||||
# on unique, non-pk col, see tests/test_m2o.py:test_non_pk_forward
|
||||
self.target._descriptor.create_pk_cols()
|
||||
|
||||
#XXX: another option, instead of the FakeTable, would be to create an
|
||||
# EntityDescriptor for the SA class.
|
||||
target_table = self.target_table
|
||||
@@ -655,6 +652,12 @@ class ManyToOne(Relationship):
|
||||
"Couldn't find a foreign key constraint in table "
|
||||
"'%s' using the following columns: %s."
|
||||
% (self.entity.table.name, colnames))
|
||||
else:
|
||||
# in this case we let SA handle everything.
|
||||
# XXX: we might want to try to build join clauses anyway so
|
||||
# that we know whether there is an ambiguity or not, and
|
||||
# suggest using colname if there is one
|
||||
pass
|
||||
if self.field:
|
||||
raise NotImplementedError(
|
||||
"'field' argument not allowed on autoloaded table "
|
||||
@@ -808,8 +811,8 @@ class OneToOne(Relationship):
|
||||
# useless because the remote_side is already setup in the other way
|
||||
# (ManyToOne).
|
||||
if self.entity.table is self.target.table:
|
||||
#FIXME: IF this code is of any use, it will probably break for
|
||||
# autoloaded tables
|
||||
# When using a manual/autoloaded table, it will be assigned
|
||||
# an empty list, which doesn't seem to upset SQLAlchemy
|
||||
kwargs['remote_side'] = self.inverse.foreign_key
|
||||
|
||||
# Contrary to ManyToMany relationships, we need to specify the join
|
||||
@@ -839,7 +842,6 @@ class ManyToMany(Relationship):
|
||||
local_colname=None, remote_colname=None,
|
||||
ondelete=None, onupdate=None,
|
||||
table=None, schema=None,
|
||||
column_format=None,
|
||||
filter=None,
|
||||
table_kwargs=None,
|
||||
*args, **kwargs):
|
||||
@@ -858,14 +860,9 @@ class ManyToMany(Relationship):
|
||||
self.table = table
|
||||
self.schema = schema
|
||||
|
||||
if column_format:
|
||||
warnings.warn("The 'column_format' argument on ManyToMany "
|
||||
"relationships is deprecated. Please use the 'local_colname' "
|
||||
"and/or 'remote_colname' arguments if you want custom "
|
||||
"column names for this table only, or modify "
|
||||
"options.M2MCOL_NAMEFORMAT if you want a custom format for "
|
||||
"all ManyToMany tables", DeprecationWarning, stacklevel=3)
|
||||
self.column_format = column_format or options.M2MCOL_NAMEFORMAT
|
||||
#TODO: this can probably be simplified/moved elsewhere since the
|
||||
#argument disappeared
|
||||
self.column_format = options.M2MCOL_NAMEFORMAT
|
||||
if not hasattr(self.column_format, '__call__'):
|
||||
# we need to store the format in a variable so that the
|
||||
# closure of the lambda is correct
|
||||
@@ -891,13 +888,6 @@ class ManyToMany(Relationship):
|
||||
|
||||
super(ManyToMany, self).__init__(of_kind, *args, **kwargs)
|
||||
|
||||
def get_table(self):
|
||||
warnings.warn("The secondary_table attribute on ManyToMany objects is "
|
||||
"deprecated. You should rather use the table attribute.",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return self.table
|
||||
secondary_table = property(get_table)
|
||||
|
||||
def match_type_of(self, other):
|
||||
return isinstance(other, ManyToMany)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user