app/taggable/taggable.py
changeset 2679 0ede2f3adbc1
child 2950 ceec88091db8
equal deleted inserted replaced
2678:a525a55833f1 2679:0ede2f3adbc1
       
     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