root / elixir / tags / 0.7.0 / elixir / ext / list.py

Revision 484, 9.3 kB (checked in by ged, 2 years ago)

update all web links

Line 
1'''
2This extension is DEPRECATED. Please use the orderinglist SQLAlchemy
3extension instead.
4
5For details:
6http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html
7
8For an Elixir example:
9http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList
10or
11http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155
12
13
14
15An ordered-list plugin for Elixir to help you make an entity be able to be
16managed in a list-like way. Much inspiration comes from the Ruby on Rails
17acts_as_list plugin, which is currently more full-featured than this plugin.
18
19Once you flag an entity with an `acts_as_list()` statement, a column will be
20added to the entity called `position` which will be an integer column that is
21managed for you by the plugin.  You can pass an alternative column name to
22the plugin using the `column_name` keyword argument.
23
24In addition, your entity will get a series of new methods attached to it,
25including:
26
27+----------------------+------------------------------------------------------+
28| Method Name          | Description                                          |
29+======================+======================================================+
30| ``move_lower``       | Move the item lower in the list                      |
31+----------------------+------------------------------------------------------+
32| ``move_higher``      | Move the item higher in the list                     |
33+----------------------+------------------------------------------------------+
34| ``move_to_bottom``   | Move the item to the bottom of the list              |
35+----------------------+------------------------------------------------------+
36| ``move_to_top``      | Move the item to the top of the list                 |
37+----------------------+------------------------------------------------------+
38| ``move_to``          | Move the item to a specific position in the list     |
39+----------------------+------------------------------------------------------+
40
41
42Sometimes, your entities that represent list items will be a part of different
43lists. To implement this behavior, simply pass the `acts_as_list` statement a
44callable that returns a "qualifier" SQLAlchemy expression. This expression will
45be added to the generated WHERE clauses used by the plugin.
46
47Example model usage:
48
49.. sourcecode:: python
50
51    from elixir import *
52    from elixir.ext.list import acts_as_list
53
54    class ToDo(Entity):
55        subject = Field(String(128))
56        owner = ManyToOne('Person')
57
58        def qualify(self):
59            return ToDo.owner_id == self.owner_id
60
61        acts_as_list(qualifier=qualify)
62
63    class Person(Entity):
64        name = Field(String(64))
65        todos = OneToMany('ToDo', order_by='position')
66
67
68The above example can then be used to manage ordered todo lists for people.
69Note that you must set the `order_by` property on the `Person.todo` relation in
70order for the relation to respect the ordering. Here is an example of using
71this model in practice:
72
73.. sourcecode:: python
74
75    p = Person.query.filter_by(name='Jonathan').one()
76    p.todos.append(ToDo(subject='Three'))
77    p.todos.append(ToDo(subject='Two'))
78    p.todos.append(ToDo(subject='One'))
79    session.commit(); session.clear()
80
81    p = Person.query.filter_by(name='Jonathan').one()
82    p.todos[0].move_to_bottom()
83    p.todos[2].move_to_top()
84    session.commit(); session.clear()
85
86    p = Person.query.filter_by(name='Jonathan').one()
87    assert p.todos[0].subject == 'One'
88    assert p.todos[1].subject == 'Two'
89    assert p.todos[2].subject == 'Three'
90
91
92For more examples, refer to the unit tests for this plugin.
93'''
94
95from elixir.statements import Statement
96from elixir.events import before_insert, before_delete
97from sqlalchemy import Column, Integer, select, func, literal, and_
98import warnings
99
100__all__ = ['acts_as_list']
101__doc_all__ = []
102
103
104def get_entity_where(instance):
105    clauses = []
106    for column in instance.table.primary_key.columns:
107        instance_value = getattr(instance, column.name)
108        clauses.append(column == instance_value)
109    return and_(*clauses)
110
111
112class ListEntityBuilder(object):
113
114    def __init__(self, entity, qualifier=None, column_name='position'):
115        warnings.warn("The act_as_list extension is deprecated. Please use "
116                      "SQLAlchemy's orderinglist extension instead",
117                      DeprecationWarning, stacklevel=6)
118        self.entity = entity
119        self.qualifier_method = qualifier
120        self.column_name = column_name
121
122    def create_non_pk_cols(self):
123        if self.entity._descriptor.autoload:
124            for c in self.entity.table.c:
125                if c.name == self.column_name:
126                    self.position_column = c
127            if not hasattr(self, 'position_column'):
128                raise Exception(
129                    "Could not find column '%s' in autoloaded table '%s', "
130                    "needed by entity '%s'." % (self.column_name,
131                        self.entity.table.name, self.entity.__name__))
132        else:
133            self.position_column = Column(self.column_name, Integer)
134            self.entity._descriptor.add_column(self.position_column)
135
136    def after_table(self):
137        position_column = self.position_column
138        position_column_name = self.column_name
139
140        qualifier_method = self.qualifier_method
141        if not qualifier_method:
142            qualifier_method = lambda self: None
143
144        def _init_position(self):
145            s = select(
146                [(func.max(position_column)+1).label('value')],
147                qualifier_method(self)
148            ).union(
149                select([literal(1).label('value')])
150            )
151            a = s.alias()
152            # we use a second func.max to get the maximum between 1 and the
153            # real max position if any exist
154            setattr(self, position_column_name, select([func.max(a.c.value)]))
155
156            # Note that this method could be rewritten more simply like below,
157            # but because this extension is going to be deprecated anyway,
158            # I don't want to risk breaking something I don't want to maintain.
159#            setattr(self, position_column_name, select(
160#                [func.coalesce(func.max(position_column), 0) + 1],
161#                qualifier_method(self)
162#            ))
163        _init_position = before_insert(_init_position)
164
165        def _shift_items(self):
166            self.table.update(
167                and_(
168                    position_column > getattr(self, position_column_name),
169                    qualifier_method(self)
170                ),
171                values={
172                    position_column : position_column - 1
173                }
174            ).execute()
175        _shift_items = before_delete(_shift_items)
176
177        def move_to_bottom(self):
178            # move the items that were above this item up one
179            self.table.update(
180                and_(
181                    position_column >= getattr(self, position_column_name),
182                    qualifier_method(self)
183                ),
184                values = {
185                    position_column : position_column - 1
186                }
187            ).execute()
188
189            # move this item to the max position
190            # MySQL does not support the correlated subquery, so we need to
191            # execute the query (through scalar()). See ticket #34.
192            self.table.update(
193                get_entity_where(self),
194                values={
195                    position_column : select(
196                        [func.max(position_column) + 1],
197                        qualifier_method(self)
198                    ).scalar()
199                }
200            ).execute()
201
202        def move_to_top(self):
203            self.move_to(1)
204
205        def move_to(self, position):
206            current_position = getattr(self, position_column_name)
207
208            # determine which direction we're moving
209            if position < current_position:
210                where = and_(
211                    position <= position_column,
212                    position_column < current_position,
213                    qualifier_method(self)
214                )
215                modifier = 1
216            elif position > current_position:
217                where = and_(
218                    current_position < position_column,
219                    position_column <= position,
220                    qualifier_method(self)
221                )
222                modifier = -1
223
224            # shift the items in between the current and new positions
225            self.table.update(where, values = {
226                position_column : position_column + modifier
227            }).execute()
228
229            # update this item's position to the desired position
230            self.table.update(get_entity_where(self)) \
231                      .execute(**{position_column_name: position})
232
233        def move_lower(self):
234            # replace for ex.: p.todos.insert(x + 1, p.todos.pop(x))
235            self.move_to(getattr(self, position_column_name) + 1)
236
237        def move_higher(self):
238            self.move_to(getattr(self, position_column_name) - 1)
239
240
241        # attach new methods to entity
242        self.entity._init_position = _init_position
243        self.entity._shift_items = _shift_items
244        self.entity.move_lower = move_lower
245        self.entity.move_higher = move_higher
246        self.entity.move_to_bottom = move_to_bottom
247        self.entity.move_to_top = move_to_top
248        self.entity.move_to = move_to
249
250
251acts_as_list = Statement(ListEntityBuilder)
Note: See TracBrowser for help on using the browser.