1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200 | import math
from elixir.statements import Statement
from elixir import *
from sqlalchemy import *
def add_tags(self, taglist, **kwargs):
"""Add a tag or tags to the entity
``taglist``
May be a list of tags, or a string which may contain spaces and
commas to designate multiple tags.
Keyword arguments will be added to the association object for the tagging
relationship if relation(s) were setup for the entity.
"""
if isinstance(taglist, basestring):
taglist = taglist.replace(', ', ' ').replace(',',' ').strip().split(' ')
for newtag in taglist:
tag = Tag.get_by(name=newtag)
if self._tag_relations:
assoc = self._tag_relations[-1]()
for k, v in kwargs.iteritems():
setattr(assoc, k, v)
if tag:
assoc.tag = tag
else:
assoc.tag = Tag(name=newtag)
setattr(assoc, self.__class__.__name__.lower(), self)
self.tags.append(assoc)
else:
if tag:
self.tags.append(tag)
else:
self.tags.append(Tag(name=newtag))
def get_by_tag(cls, tag, **kwargs):
"""Retrieve entities with the provided tag
Additional keywords will be used during the query if a relation was
specified to narrow down the results.
"""
if cls._tag_relations and kwargs:
query = []
for k,v in kwargs.iteritems():
query.append(getattr(cls._tag_relations[-1].c, k)==v)
return entity.select(and_(Tag.c.name==tag, *query))
else:
return entity.select(Tag.c.name==tag)
def tag_sizes(cls, **kwargs):
"""This method returns all the tags and their relative size for a tagcloud
Tagclouds for limited subsets of tagged entities can be queried via keyword
arguments if relation(s) were used for the tagging.
Example:
.. code :: Python
Person.tag_sizes(tagged_by='managers')
"""
if cls._tag_relations and kwargs:
query = []
for k,v in kwargs.iteritems():
query.append(getattr(cls._tag_relations[-1].c, k)==v)
kwargs = {}
kwargs['from_obj'] = [Tag.table.join(cls._tag_table).join(cls.table)]
kwargs['group_by'] = [Tag.table.c.name]
results = select([Tag.table.c.name, func.count(Tag.table.c.name)], *query,
**kwargs).execute()
else:
results = select([Tag.table.c.name, func.count(Tag.table.c.name)],
from_obj=[Tag.table.join(cls._tag_table).join(cls.table)],
group_by=[Tag.table.c.name]).execute()
tag_counts = results.fetchall()
total = sum([tag[1] for tag in tag_counts])
totalcounts = []
for tag in tag_counts:
weight = (math.log(tag[1] or 1) * 4) + 10
totalcounts.append((tag[0], tag[1],weight))
return sorted(totalcounts, cmp=lambda x,y: cmp(x[0], y[0]))
class Tag(Entity):
has_field('name', Unicode)
class Taggable(object):
"""A Taggable Elixir Statement object"""
def __init__(self, entity, lazy=False, backref=None, relations=None):
"""Create a taggable relation on a model
``relations``
A list of additional Fields used for the many-to-many relationship.
Using this will switch the tags table for this relation to use the
TagAssociation objects and require additional checks on the
association object during tag iteration.
When in use, the add_tags will optionally take keyword arguments
corresponding to the relation keywords to attach to a tag
association.
Note: The last object passed in the relations list should be the
tag association object to use. It should be a plain object ready
for SQLAlchemy's mapper.
Example:
.. code :: Python
# Perhaps tags come in from different groups, and should be
# visible by different groups. To designate the group tagging
# a person, a tagged_by is provided.
class PersonTagAssociation(object):
pass
class Person(Entity):
has_field('name', Unicode)
acts_as_taggable(backref='people',
relations=[Field('tagged_by', Unicode, primary_key=True),
PersonTagAssociation])
person = Person(name='Fred Smith')
# Add tags in a category
person.add_tags('employee', tagged_by='manager')
# is equivilant to
assoc = TagAssociation()
assoc.tagged_by = 'manager'
person.tags.append(assoc)
# this wouldn't be much fun making all the TagAssociations....
person.add_tags(['tall', 'fast', 'young'], tagged_by='reviewer')
# Check for a tag
person.has_tag('employee', tagged_by='manager')
# is (mostly) equivilant to
bool([x for x in person.tags if x.tagged_by='manager'])
# Pull out all people with a tag 'honors', tagged by a 'manager'
persons = Person.get_by_tag('honors', tagged_by='manager')
``lazy``
Whether the relation should be eager loaded or not. When false,
the relation will be loaded eagerly, when true it will be loaded
in a lazy fashion upon access.
``backref``
By default, the back-reference in the Tag table will be given the
name of the model table lower-cased. A different name can be
manually specified should a plural form be desired.
"""
self.lazy = lazy
self.entity = entity
self.backref_name = backref
self.relations = relations or []
entity.add_tags = add_tags
entity.get_by_tag = classmethod(get_by_tag)
entity.tag_sizes = classmethod(tag_sizes)
entity._tag_relations = relations
entity._descriptor.relationships['taggable'] = self
def setup(self):
self.create_tables()
self.create_properties()
return True
def create_tables(self):
entity = self.entity
tag_table = Table(entity.table.name + '_tags', entity._descriptor.metadata,
Column('tag_id', Integer,
ForeignKey(Tag.table.name + '.id', ondelete='RESTRICT'),
primary_key=True),
Column(entity.table.name + '_id', Integer,
ForeignKey(entity.table.name + '.id', ondelete='CASCADE'),
primary_key=True),
*self.relations[:-1]
)
entity._tag_table= tag_table
def create_properties(self):
entity = self.entity
kwargs = {}
if self.backref_name:
kwargs['backref'] = backref(self.backref_name)
if self.relations:
entity.mapper.add_property('tags',
relation(self.relations[-1], lazy=self.lazy, cascade="all, delete-orphan"))
mapper(self.relations[-1], entity._tag_table,
properties={
entity.__name__.lower(): relation(entity, lazy=False),
'tag': relation(Tag, lazy=False, **kwargs)
})
else:
tags = relation(Tag, secondary=entity._tag_table, lazy=self.lazy,
**kwargs)
entity.mapper.add_property('tags', tags)
acts_as_taggable = Statement(Taggable)
|