|
1 from google.appengine.ext import db |
|
2 import string |
|
3 |
|
4 class Tag(db.Model): |
|
5 "Google AppEngine model for store of tags." |
|
6 |
|
7 tag = db.StringProperty(required=True) |
|
8 "The actual string value of the tag." |
|
9 |
|
10 added = db.DateTimeProperty(auto_now_add=True) |
|
11 "The date and time that the tag was first added to the datastore." |
|
12 |
|
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." |
|
15 |
|
16 tagged_count = db.IntegerProperty(default=0) |
|
17 "The number of entities in tagged." |
|
18 |
|
19 @classmethod |
|
20 def __key_name(cls, tag_name): |
|
21 return cls.__name__ + '_' + tag_name |
|
22 |
|
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() |
|
31 |
|
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() |
|
40 |
|
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() |
|
48 |
|
49 @classmethod |
|
50 def get_by_name(cls, tag_name): |
|
51 return cls.get_by_key_name(cls.__key_name(tag_name)) |
|
52 |
|
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 |
|
58 |
|
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 |
|
72 |
|
73 @classmethod |
|
74 def get_tags_by_frequency(cls, limit=1000): |
|
75 """Return a list of Tags sorted by the number of objects to |
|
76 which they have been applied, most frequently-used first. |
|
77 If limit is given, return only that many tags; otherwise, |
|
78 return all.""" |
|
79 tag_list = db.Query(cls).filter('tagged_count >', 0).order( |
|
80 "-tagged_count").fetch(limit) |
|
81 |
|
82 return tag_list |
|
83 |
|
84 @classmethod |
|
85 def get_tags_by_name(cls, limit=1000, ascending=True): |
|
86 """Return a list of Tags sorted alphabetically by the name of the tag. |
|
87 If a limit is given, return only that many tags; otherwise, return all. |
|
88 If ascending is True, sort from a-z; otherwise, sort from z-a.""" |
|
89 |
|
90 from google.appengine.api import memcache |
|
91 |
|
92 cache_name = cls.__name__ + '_tags_by_name' |
|
93 if ascending: |
|
94 cache_name += '_asc' |
|
95 else: |
|
96 cache_name += '_desc' |
|
97 |
|
98 tags = memcache.get(cache_name) |
|
99 if tags is None or len(tags) < limit: |
|
100 order_by = "tag" |
|
101 if not ascending: |
|
102 order_by = "-tag" |
|
103 |
|
104 tags = db.Query(cls).order(order_by).fetch(limit) |
|
105 memcache.add(cache_name, tags, 3600) |
|
106 else: |
|
107 if len(tags) > limit: |
|
108 # Return only as many as requested. |
|
109 tags = tags[:limit] |
|
110 |
|
111 return tags |
|
112 |
|
113 @classmethod |
|
114 def popular_tags(cls, limit=5): |
|
115 from google.appengine.api import memcache |
|
116 |
|
117 tags = memcache.get(cls.__name__ + '_popular_tags') |
|
118 if tags is None: |
|
119 tags = cls.get_tags_by_frequency(limit) |
|
120 memcache.add(cls.__name__ + '_popular_tags', tags, 3600) |
|
121 |
|
122 return tags |
|
123 |
|
124 @classmethod |
|
125 def expire_cached_tags(cls): |
|
126 from google.appengine.api import memcache |
|
127 |
|
128 memcache.delete(cls.__name__ + '_popular_tags') |
|
129 memcache.delete(cls.__name__ + '_tags_by_name_asc') |
|
130 memcache.delete(cls.__name__ + '_tags_by_name_desc') |
|
131 |
|
132 def __str__(self): |
|
133 """Returns the string representation of the entity's tag name. |
|
134 """ |
|
135 |
|
136 return self.tag |
|
137 |
|
138 def tag_property(tag_name): |
|
139 """Decorator that creates and returns a tag property to be used |
|
140 in Google AppEngine model. |
|
141 |
|
142 Args: |
|
143 tag_name: name of the tag to be created. |
|
144 """ |
|
145 |
|
146 def get_tags(self): |
|
147 """"Get a list of Tag objects for all Tags that apply to the |
|
148 specified entity. |
|
149 """ |
|
150 |
|
151 |
|
152 if self._tags[tag_name] is None or len(self._tags[tag_name]) == 0: |
|
153 self._tags[tag_name] = self._tag_model[ |
|
154 tag_name].get_tags_for_key(self.key()) |
|
155 return self._tags[tag_name] |
|
156 |
|
157 def set_tags(self, seed): |
|
158 """Set a list of Tag objects for all Tags that apply to |
|
159 the specified entity. |
|
160 """ |
|
161 |
|
162 import types |
|
163 if type(seed['tags']) is types.UnicodeType: |
|
164 # Convert unicode to a plain string |
|
165 seed['tags'] = str(seed['tags']) |
|
166 if type(seed['tags']) is types.StringType: |
|
167 # Tags is a string, split it on tag_seperator into a list |
|
168 seed['tags'] = string.split(seed['tags'], self.tag_separator) |
|
169 if type(seed['tags']) is types.ListType: |
|
170 get_tags(self) |
|
171 # Firstly, we will check to see if any tags have been removed. |
|
172 # Iterate over a copy of _tags, as we may need to modify _tags |
|
173 for each_tag in self._tags[tag_name][:]: |
|
174 if each_tag not in seed['tags']: |
|
175 # A tag that was previously assigned to this entity is |
|
176 # missing in the list that is being assigned, so we |
|
177 # disassocaite this entity and the tag. |
|
178 each_tag.remove_tagged(self.key()) |
|
179 self._tags[tag_name].remove(each_tag) |
|
180 # Secondly, we will check to see if any tags have been added. |
|
181 for each_tag in seed['tags']: |
|
182 each_tag = string.strip(each_tag) |
|
183 if len(each_tag) > 0 and each_tag not in self._tags[tag_name]: |
|
184 # A tag that was not previously assigned to this entity |
|
185 # is present in the list that is being assigned, so we |
|
186 # associate this entity with the tag. |
|
187 tag = self._tag_model[tag_name].get_or_create( |
|
188 seed['scope'], each_tag) |
|
189 tag.add_tagged(self.key()) |
|
190 self._tags[tag_name].append(tag) |
|
191 else: |
|
192 raise Exception, "tags must be either a unicode, a string or a list" |
|
193 |
|
194 return property(get_tags, set_tags) |
|
195 |
|
196 |
|
197 class Taggable(object): |
|
198 """A mixin class that is used for making GAE Model classes taggable. |
|
199 |
|
200 This is an extended version of Taggable-mixin which allows for |
|
201 multiple tag properties in the same AppEngine Model class. |
|
202 """ |
|
203 |
|
204 def __init__(self, **kwargs): |
|
205 """The constructor class for Taggable, that creates a dictionary of tags. |
|
206 |
|
207 The difference from the original taggable in terms of interface is |
|
208 that, tag class is not used as the default tag model, since we don't |
|
209 have a default tag property created in this class now. |
|
210 |
|
211 Args: |
|
212 kwargs: keywords containing the name of the tags and arguments |
|
213 containing tag model to be used. |
|
214 """ |
|
215 |
|
216 self._tags = {} |
|
217 self._tag_model = {} |
|
218 |
|
219 for tag_name in kwargs: |
|
220 self._tags[tag_name] = None |
|
221 self._tag_model[tag_name] = kwargs[tag_name] |
|
222 |
|
223 self.tag_separator = ", " |
|
224 |
|
225 def tags_string(self, tag_name): |
|
226 "Create a formatted string version of this entity's tags" |
|
227 to_str = "" |
|
228 for each_tag in tag_name: |
|
229 to_str += each_tag.tag |
|
230 if each_tag != tag_name[-1]: |
|
231 to_str += self.tag_separator |
|
232 return to_str |