root / elixir / trunk / elixir / ext / list.py @ 309

Revision 309, 8.1 kB (checked in by ged, 5 years ago)

- Fixed act_as_list extension to work with DBMS that require subselects to be

aliased (patch by Alice McGregor)

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