You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
9.3 KiB
251 lines
9.3 KiB
'''
|
|
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)
|
|
|