40 changed files with 172 additions and 635 deletions
@ -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) |
@ -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 |
Loading…
Reference in new issue