app/taggable/taggable.py
changeset 2679 0ede2f3adbc1
child 2950 ceec88091db8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/taggable/taggable.py	Sat Jul 25 01:09:46 2009 +0530
@@ -0,0 +1,232 @@
+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."
+
+  @classmethod
+  def __key_name(cls, tag_name):
+    return cls.__name__ + '_' + 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)
+    self.__class__.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)
+    self.__class__.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)
+    self.__class__.expire_cached_tags()
+        
+  @classmethod
+  def get_by_name(cls, tag_name):
+    return cls.get_by_key_name(cls.__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(cls).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 = cls.__key_name(tag_name)
+    existing_tag = cls.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 = cls(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(cls).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 = cls.__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(cls).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(cls.__name__ + '_popular_tags')
+    if tags is None:
+      tags = cls.get_tags_by_frequency(limit)
+      memcache.add(cls.__name__ + '_popular_tags', tags, 3600)
+
+    return tags
+
+  @classmethod
+  def expire_cached_tags(cls):
+    from google.appengine.api import memcache
+
+    memcache.delete(cls.__name__ + '_popular_tags')
+    memcache.delete(cls.__name__ + '_tags_by_name_asc')
+    memcache.delete(cls.__name__ + '_tags_by_name_desc')
+
+  def __str__(self):
+    """Returns the string representation of the entity's tag name.
+    """
+
+    return self.tag
+
+def tag_property(tag_name):
+  """Decorator that creates and returns a tag property to be used 
+  in Google AppEngine model.
+
+  Args:
+    tag_name: name of the tag to be created.
+  """
+
+  def get_tags(self):
+    """"Get a list of Tag objects for all Tags that apply to the
+    specified entity.
+    """
+
+    
+    if self._tags[tag_name] is None or len(self._tags[tag_name]) == 0:
+      self._tags[tag_name] = self._tag_model[
+          tag_name].get_tags_for_key(self.key())
+    return self._tags[tag_name]
+
+  def set_tags(self, seed):
+    """Set a list of Tag objects for all Tags that apply to 
+    the specified entity.
+    """
+
+    import types
+    if type(seed['tags']) is types.UnicodeType:
+      # Convert unicode to a plain string
+      seed['tags'] = str(seed['tags'])
+    if type(seed['tags']) is types.StringType:
+      # Tags is a string, split it on tag_seperator into a list
+      seed['tags'] = string.split(seed['tags'], self.tag_separator)
+    if type(seed['tags']) is types.ListType:
+      get_tags(self)
+      # 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[tag_name][:]:
+        if each_tag not in seed['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[tag_name].remove(each_tag)
+      # Secondly, we will check to see if any tags have been added.
+      for each_tag in seed['tags']:
+        each_tag = string.strip(each_tag)
+        if len(each_tag) > 0 and each_tag not in self._tags[tag_name]:
+          # 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 = self._tag_model[tag_name].get_or_create(
+              seed['scope'], each_tag)
+          tag.add_tagged(self.key())
+          self._tags[tag_name].append(tag)
+    else:
+      raise Exception, "tags must be either a unicode, a string or a list"
+
+  return property(get_tags, set_tags)
+
+
+class Taggable(object):
+  """A mixin class that is used for making GAE Model classes taggable.
+
+  This is an extended version of Taggable-mixin which allows for 
+  multiple tag properties in the same AppEngine Model class.
+  """
+    
+  def __init__(self, **kwargs):
+    """The constructor class for Taggable, that creates a dictionary of tags.
+
+    The difference from the original taggable in terms of interface is
+    that, tag class is not used as the default tag model, since we don't
+    have a default tag property created in this class now.
+
+    Args:
+      kwargs: keywords containing the name of the tags and arguments
+          containing tag model to be used.
+    """
+
+    self._tags = {}
+    self._tag_model = {}
+
+    for tag_name in kwargs:
+      self._tags[tag_name] = None
+      self._tag_model[tag_name] = kwargs[tag_name]
+
+    self.tag_separator = ", "
+
+  def tags_string(self, tag_name):
+    "Create a formatted string version of this entity's tags"
+    to_str = ""
+    for each_tag in tag_name:
+      to_str += each_tag.tag
+      if each_tag != tag_name[-1]:
+        to_str += self.tag_separator
+    return to_str