app/taggable-mixin/taggable.py
changeset 2368 e07c425c7135
child 2376 feec28b50f1b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/taggable-mixin/taggable.py	Sat May 30 00:36:52 2009 +0200
@@ -0,0 +1,201 @@
+from google.appengine.ext import db
+import string
+    
+class Tag(db.Model):
+    "Google AppEngine model for store of tags."
+    
+    tag = db.StringProperty(required=True)
+    "The actual string value of the tag."
+    
+    added = db.DateTimeProperty(auto_now_add=True)
+    "The date and time that the tag was first added to the datastore."
+    
+    tagged = db.ListProperty(db.Key)
+    "A List of db.Key values for the datastore objects that have been tagged with this tag value."
+    
+    tagged_count = db.IntegerProperty(default=0)
+    "The number of entities in tagged."
+
+    @staticmethod
+    def __key_name(tag_name):
+        return "tag_%s" % tag_name
+    
+    def remove_tagged(self, key):
+        def remove_tagged_txn():
+            if key in self.tagged:
+                self.tagged.remove(key)
+                self.tagged_count -= 1
+                self.put()
+        db.run_in_transaction(remove_tagged_txn)
+        Tag.expire_cached_tags()
+
+    def add_tagged(self, key):
+        def add_tagged_txn():
+            if key not in self.tagged:
+                self.tagged.append(key)
+                self.tagged_count += 1
+                self.put()
+        db.run_in_transaction(add_tagged_txn)
+        Tag.expire_cached_tags()
+    
+    def clear_tagged(self):
+        def clear_tagged_txn():
+            self.tagged = []
+            self.tagged_count = 0
+            self.put()
+        db.run_in_transaction(clear_tagged_txn)
+        Tag.expire_cached_tags()
+        
+    @classmethod
+    def get_by_name(cls, tag_name):
+        return Tag.get_by_key_name(Tag.__key_name(tag_name))
+    
+    @classmethod
+    def get_tags_for_key(cls, key):
+        "Set the tags for the datastore object represented by key."
+        tags = db.Query(Tag).filter('tagged =', key).fetch(1000)
+        return tags
+    
+    @classmethod
+    def get_or_create(cls, tag_name):
+        "Get the Tag object that has the tag value given by tag_value."
+        tag_key_name = Tag.__key_name(tag_name)
+        existing_tag = Tag.get_by_key_name(tag_key_name)
+        if existing_tag is None:
+            # The tag does not yet exist, so create it.
+            def create_tag_txn():
+                new_tag = Tag(key_name=tag_key_name, tag = tag_name)
+                new_tag.put()
+                return new_tag
+            existing_tag = db.run_in_transaction(create_tag_txn)
+        return existing_tag
+    
+    @classmethod
+    def get_tags_by_frequency(cls, limit=1000):
+        """Return a list of Tags sorted by the number of objects to which they have been applied,
+        most frequently-used first.  If limit is given, return only that many tags; otherwise,
+        return all."""
+        tag_list = db.Query(Tag).filter('tagged_count >', 0).order("-tagged_count").fetch(limit)
+            
+        return tag_list
+
+    @classmethod
+    def get_tags_by_name(cls, limit=1000, ascending=True):
+        """Return a list of Tags sorted alphabetically by the name of the tag.
+        If a limit is given, return only that many tags; otherwise, return all.
+        If ascending is True, sort from a-z; otherwise, sort from z-a."""
+
+        from google.appengine.api import memcache
+
+        cache_name = 'tags_by_name'
+        if ascending:
+            cache_name += '_asc'
+        else:
+            cache_name += '_desc'
+            
+        tags = memcache.get(cache_name)
+        if tags is None or len(tags) < limit:
+            order_by = "tag"
+            if not ascending:
+                order_by = "-tag"
+            
+            tags = db.Query(Tag).order(order_by).fetch(limit)
+            memcache.add(cache_name, tags, 3600)
+        else:
+            if len(tags) > limit:
+                # Return only as many as requested.
+                tags = tags[:limit]
+                
+        return tags
+        
+    
+    @classmethod
+    def popular_tags(cls, limit=5):
+        from google.appengine.api import memcache
+        
+        tags = memcache.get('popular_tags')
+        if tags is None:
+            tags = Tag.get_tags_by_frequency(limit)
+            memcache.add('popular_tags', tags, 3600)
+        
+        return tags
+
+    @classmethod
+    def expire_cached_tags(cls):
+        from google.appengine.api import memcache
+        
+        memcache.delete('popular_tags')
+        memcache.delete('tags_by_name_asc')
+        memcache.delete('tags_by_name_desc')
+
+class Taggable:
+    """A mixin class that is used for making Google AppEnigne Model classes taggable.
+        Usage:
+            class Post(db.Model, taggable.Taggable):
+                body = db.TextProperty(required = True)
+                title = db.StringProperty()
+                added = db.DateTimeProperty(auto_now_add=True)
+                edited = db.DateTimeProperty()
+            
+                def __init__(self, parent=None, key_name=None, app=None, **entity_values):
+                    db.Model.__init__(self, parent, key_name, app, **entity_values)
+                    taggable.Taggable.__init__(self)
+    """
+    
+    def __init__(self):
+        self.__tags = None
+        self.tag_separator = ","
+        """The string that is used to separate individual tags in a string
+        representation of a list of tags.  Used by tags_string() to join the tags
+        into a string representation and tags setter to split a string into
+        individual tags."""
+
+    def __get_tags(self):
+        "Get a List of Tag objects for all Tags that apply to this object."
+        if self.__tags is None or len(self.__tags) == 0:
+            self.__tags = Tag.get_tags_for_key(self.key())
+        return self.__tags
+
+    def __set_tags(self, tags):
+        import types
+        if type(tags) is types.UnicodeType:
+            # Convert unicode to a plain string
+            tags = str(tags)
+        if type(tags) is types.StringType:
+            # Tags is a string, split it on tag_seperator into a list
+            tags = string.split(tags, self.tag_separator)
+        if type(tags) is types.ListType:
+            self.__get_tags()
+            # Firstly, we will check to see if any tags have been removed.
+            # Iterate over a copy of __tags, as we may need to modify __tags
+            for each_tag in self.__tags[:]:
+                if each_tag not in tags:
+                    # A tag that was previously assigned to this entity is
+                    # missing in the list that is being assigned, so we
+                    # disassocaite this entity and the tag.
+                    each_tag.remove_tagged(self.key())
+                    self.__tags.remove(each_tag)
+            # Secondly, we will check to see if any tags have been added.
+            for each_tag in tags:
+                each_tag = string.strip(each_tag)
+                if len(each_tag) > 0 and each_tag not in self.__tags:
+                    # A tag that was not previously assigned to this entity
+                    # is present in the list that is being assigned, so we
+                    # associate this entity with the tag.
+                    tag = Tag.get_or_create(each_tag)
+                    tag.add_tagged(self.key())
+                    self.__tags.append(tag)
+        else:
+            raise Exception, "tags must be either a unicode, a string or a list"
+        
+    tags = property(__get_tags, __set_tags, None, None)
+    
+    def tags_string(self):
+        "Create a formatted string version of this entity's tags"
+        to_str = ""
+        for each_tag in self.tags:
+            to_str += each_tag.tag
+            if each_tag != self.tags[-1]:
+                to_str += self.tag_separator
+        return to_str
+