Extend taggable-mixin to support different Tag models.
[Melange.git] / app / taggable-mixin / taggable.py
blob617d4fd9cd08f177ef8e05538f5ac15fd5a9729c
1 from google.appengine.ext import db
2 import string
4 class Tag(db.Model):
5 "Google AppEngine model for store of tags."
7 tag = db.StringProperty(required=True)
8 "The actual string value of the tag."
10 added = db.DateTimeProperty(auto_now_add=True)
11 "The date and time that the tag was first added to the datastore."
13 tagged = db.ListProperty(db.Key)
14 "A List of db.Key values for the datastore objects that have been tagged with this tag value."
16 tagged_count = db.IntegerProperty(default=0)
17 "The number of entities in tagged."
19 @classmethod
20 def __key_name(cls, tag_name):
21 return cls.__name__ + '_' + tag_name
23 def remove_tagged(self, key):
24 def remove_tagged_txn():
25 if key in self.tagged:
26 self.tagged.remove(key)
27 self.tagged_count -= 1
28 self.put()
29 db.run_in_transaction(remove_tagged_txn)
30 self.__class__.expire_cached_tags()
32 def add_tagged(self, key):
33 def add_tagged_txn():
34 if key not in self.tagged:
35 self.tagged.append(key)
36 self.tagged_count += 1
37 self.put()
38 db.run_in_transaction(add_tagged_txn)
39 self.__class__.expire_cached_tags()
41 def clear_tagged(self):
42 def clear_tagged_txn():
43 self.tagged = []
44 self.tagged_count = 0
45 self.put()
46 db.run_in_transaction(clear_tagged_txn)
47 self.__class__.expire_cached_tags()
49 @classmethod
50 def get_by_name(cls, tag_name):
51 return cls.get_by_key_name(cls.__key_name(tag_name))
53 @classmethod
54 def get_tags_for_key(cls, key):
55 "Set the tags for the datastore object represented by key."
56 tags = db.Query(cls).filter('tagged =', key).fetch(1000)
57 return tags
59 @classmethod
60 def get_or_create(cls, tag_name):
61 "Get the Tag object that has the tag value given by tag_value."
62 tag_key_name = cls.__key_name(tag_name)
63 existing_tag = cls.get_by_key_name(tag_key_name)
64 if existing_tag is None:
65 # The tag does not yet exist, so create it.
66 def create_tag_txn():
67 new_tag = cls(key_name=tag_key_name, tag=tag_name)
68 new_tag.put()
69 return new_tag
70 existing_tag = db.run_in_transaction(create_tag_txn)
71 return existing_tag
73 @classmethod
74 def get_tags_by_frequency(cls, limit=1000):
75 """Return a list of Tags sorted by the number of objects to which they have been applied,
76 most frequently-used first. If limit is given, return only that many tags; otherwise,
77 return all."""
78 tag_list = db.Query(cls).filter('tagged_count >', 0).order("-tagged_count").fetch(limit)
80 return tag_list
82 @classmethod
83 def get_tags_by_name(cls, limit=1000, ascending=True):
84 """Return a list of Tags sorted alphabetically by the name of the tag.
85 If a limit is given, return only that many tags; otherwise, return all.
86 If ascending is True, sort from a-z; otherwise, sort from z-a."""
88 from google.appengine.api import memcache
90 cache_name = cls.__name__ + '_tags_by_name'
91 if ascending:
92 cache_name += '_asc'
93 else:
94 cache_name += '_desc'
96 tags = memcache.get(cache_name)
97 if tags is None or len(tags) < limit:
98 order_by = "tag"
99 if not ascending:
100 order_by = "-tag"
102 tags = db.Query(cls).order(order_by).fetch(limit)
103 memcache.add(cache_name, tags, 3600)
104 else:
105 if len(tags) > limit:
106 # Return only as many as requested.
107 tags = tags[:limit]
109 return tags
112 @classmethod
113 def popular_tags(cls, limit=5):
114 from google.appengine.api import memcache
116 tags = memcache.get(cls.__name__ + '_popular_tags')
117 if tags is None:
118 tags = cls.get_tags_by_frequency(limit)
119 memcache.add(cls.__name__ + '_popular_tags', tags, 3600)
121 return tags
123 @classmethod
124 def expire_cached_tags(cls):
125 from google.appengine.api import memcache
127 memcache.delete(cls.__name__ + '_popular_tags')
128 memcache.delete(cls.__name__ + '_tags_by_name_asc')
129 memcache.delete(cls.__name__ + '_tags_by_name_desc')
131 class Taggable:
132 """A mixin class that is used for making Google AppEngine Model classes taggable.
133 Usage:
134 class Post(db.Model, taggable.Taggable):
135 body = db.TextProperty(required = True)
136 title = db.StringProperty()
137 added = db.DateTimeProperty(auto_now_add=True)
138 edited = db.DateTimeProperty()
140 def __init__(self, parent=None, key_name=None, app=None, **entity_values):
141 db.Model.__init__(self, parent, key_name, app, **entity_values)
142 taggable.Taggable.__init__(self)
145 def __init__(self, tag_model = Tag):
146 self.__tags = None
147 self.__tag_model = tag_model
148 self.tag_separator = ","
149 """The string that is used to separate individual tags in a string
150 representation of a list of tags. Used by tags_string() to join the tags
151 into a string representation and tags setter to split a string into
152 individual tags."""
154 def __get_tags(self):
155 "Get a List of Tag objects for all Tags that apply to this object."
156 if self.__tags is None or len(self.__tags) == 0:
157 self.__tags = self.__tag_model.get_tags_for_key(self.key())
158 return self.__tags
160 def __set_tags(self, tags):
161 import types
162 if type(tags) is types.UnicodeType:
163 # Convert unicode to a plain string
164 tags = str(tags)
165 if type(tags) is types.StringType:
166 # Tags is a string, split it on tag_seperator into a list
167 tags = string.split(tags, self.tag_separator)
168 if type(tags) is types.ListType:
169 self.__get_tags()
170 # Firstly, we will check to see if any tags have been removed.
171 # Iterate over a copy of __tags, as we may need to modify __tags
172 for each_tag in self.__tags[:]:
173 if each_tag not in tags:
174 # A tag that was previously assigned to this entity is
175 # missing in the list that is being assigned, so we
176 # disassocaite this entity and the tag.
177 each_tag.remove_tagged(self.key())
178 self.__tags.remove(each_tag)
179 # Secondly, we will check to see if any tags have been added.
180 for each_tag in tags:
181 each_tag = string.strip(each_tag)
182 if len(each_tag) > 0 and each_tag not in self.__tags:
183 # A tag that was not previously assigned to this entity
184 # is present in the list that is being assigned, so we
185 # associate this entity with the tag.
186 tag = self.__tag_model.get_or_create(each_tag)
187 tag.add_tagged(self.key())
188 self.__tags.append(tag)
189 else:
190 raise Exception, "tags must be either a unicode, a string or a list"
192 tags = property(__get_tags, __set_tags, None, None)
194 def tags_string(self):
195 "Create a formatted string version of this entity's tags"
196 to_str = ""
197 for each_tag in self.tags:
198 to_str += each_tag.tag
199 if each_tag != self.tags[-1]:
200 to_str += self.tag_separator
201 return to_str