app/taggable/taggable.py
changeset 3085 ded7a67e7e0a
parent 3083 f384c0a42920
child 3091 a48f4e860f7b
equal deleted inserted replaced
3084:cac43a6cb986 3085:ded7a67e7e0a
     1 from google.appengine.ext import db
     1 from google.appengine.ext import db
     2 
     2 
     3 import string
     3 import string
     4 import soc.models.linkable
     4 import soc.models.linkable
     5     
     5 
     6 class Tag(db.Model):
     6 class Tag(db.Model):
     7   "Google AppEngine model for store of tags."
     7   """Google AppEngine model for store of tags.
       
     8   """
     8 
     9 
     9   tag = db.StringProperty(required=True)
    10   tag = db.StringProperty(required=True)
    10   "The actual string value of the tag."
    11   "The actual string value of the tag."
    11 
    12 
    12   added = db.DateTimeProperty(auto_now_add=True)
    13   added = db.DateTimeProperty(auto_now_add=True)
    25                                required=False,
    26                                required=False,
    26                                collection_name='task_type_tags')
    27                                collection_name='task_type_tags')
    27   "Each tag is scoped under some linkable model."
    28   "Each tag is scoped under some linkable model."
    28 
    29 
    29   @classmethod
    30   @classmethod
    30   def __key_name(cls, tag_name):
    31   def __key_name(cls, scope_path, tag_name):
    31     return cls.__name__ + '_' + tag_name
    32     """Create the key_name from program key_name as scope_path and tag_name.
    32     
    33     """
       
    34 
       
    35     return scope_path + '/' + tag_name
       
    36 
    33   def remove_tagged(self, key):
    37   def remove_tagged(self, key):
    34     def remove_tagged_txn():
    38     def remove_tagged_txn():
    35       if key in self.tagged:
    39       if key in self.tagged:
    36         self.tagged.remove(key)
    40         self.tagged.remove(key)
    37         self.tagged_count -= 1
    41         self.tagged_count -= 1
    48         self.tagged.append(key)
    52         self.tagged.append(key)
    49         self.tagged_count += 1
    53         self.tagged_count += 1
    50         self.put()
    54         self.put()
    51     db.run_in_transaction(add_tagged_txn)
    55     db.run_in_transaction(add_tagged_txn)
    52     self.__class__.expire_cached_tags()
    56     self.__class__.expire_cached_tags()
    53     
    57 
    54   def clear_tagged(self):
    58   def clear_tagged(self):
    55     def clear_tagged_txn():
    59     def clear_tagged_txn():
    56       if self.auto_delete:
    60       if self.auto_delete:
    57         self.delete()
    61         self.delete()
    58       else:
    62       else:
    59         self.tagged = []
    63         self.tagged = []
    60         self.tagged_count = 0
    64         self.tagged_count = 0
    61         self.put()
    65         self.put()
    62     db.run_in_transaction(clear_tagged_txn)
    66     db.run_in_transaction(clear_tagged_txn)
    63     self.__class__.expire_cached_tags()
    67     self.__class__.expire_cached_tags()
    64         
    68 
    65   @classmethod
    69   @classmethod
    66   def get_by_name(cls, tag_name):
    70   def get_by_name(cls, tag_name):
    67     return cls.get_by_key_name(cls.__key_name(tag_name))
    71     """Get the list of tag objects that has the given tag_name.
    68     
    72     """
    69   @classmethod
    73 
    70   def get_tags_for_key(cls, key):
    74     tags = db.Query(cls).filter('tag =', tag_name).fetch(1000)
    71     "Set the tags for the datastore object represented by key."
       
    72     tags = db.Query(cls).filter('tagged =', key).fetch(1000)
       
    73     return tags
    75     return tags
    74     
    76 
    75   @classmethod
    77   @classmethod
    76   def get_or_create(cls, tag_name):
    78   def get_by_scope_and_name(cls, scope, tag_name):
    77     "Get the Tag object that has the tag value given by tag_value."
    79     """Get a tag by scope and name.
    78     tag_key_name = cls.__key_name(tag_name)
    80 
       
    81     There may be only one such tag.
       
    82     """
       
    83 
       
    84     return db.Query(cls).filter(
       
    85         'scope =', scope).filter('tag =', tag_name).get()
       
    86 
       
    87   @classmethod
       
    88   def get_tags_for_key(cls, key, limit=1000):
       
    89     """Get the tags for the datastore object represented by key.
       
    90     """
       
    91 
       
    92     tags = db.Query(cls).filter('tagged =', key).fetch(limit)
       
    93     return tags
       
    94 
       
    95   @classmethod
       
    96   def get_or_create(cls, scope, tag_name, order=0):
       
    97     """Get the Tag object that has the tag value given by tag_value.
       
    98     """
       
    99 
       
   100     tag_key_name = cls.__key_name(scope.key().name(), tag_name)
    79     existing_tag = cls.get_by_key_name(tag_key_name)
   101     existing_tag = cls.get_by_key_name(tag_key_name)
    80     if existing_tag is None:
   102     if existing_tag is None:
    81       # The tag does not yet exist, so create it.
   103       # the tag does not yet exist, so create it.
       
   104       if not order:
       
   105         order = cls.get_highest_order(scope=scope) + 1
    82       def create_tag_txn():
   106       def create_tag_txn():
    83         new_tag = cls(key_name=tag_key_name, tag=tag_name)
   107         new_tag = cls(key_name=tag_key_name, tag=tag_name,
       
   108                       scope=scope, order=order)
    84         new_tag.put()
   109         new_tag.put()
    85         return new_tag
   110         return new_tag
    86       existing_tag = db.run_in_transaction(create_tag_txn)
   111       existing_tag = db.run_in_transaction(create_tag_txn)
    87     return existing_tag
   112     return existing_tag
    88     
   113 
    89   @classmethod
   114   @classmethod
    90   def get_tags_by_frequency(cls, limit=1000):
   115   def get_tags_by_frequency(cls, limit=1000):
    91     """Return a list of Tags sorted by the number of objects to 
   116     """Return a list of Tags sorted by the number of objects to which they
    92     which they have been applied, most frequently-used first. 
   117     have been applied, most frequently-used first. If limit is given, return
    93     If limit is given, return only that many tags; otherwise,
   118     only return only that many tags; otherwise, return all.
    94     return all."""
   119     """
       
   120 
    95     tag_list = db.Query(cls).filter('tagged_count >', 0).order(
   121     tag_list = db.Query(cls).filter('tagged_count >', 0).order(
    96         "-tagged_count").fetch(limit)
   122         "-tagged_count").fetch(limit)
    97             
   123 
    98     return tag_list
   124     return tag_list
    99 
   125 
   100   @classmethod
   126   @classmethod
   101   def get_tags_by_name(cls, limit=1000, ascending=True):
   127   def get_tags_by_name(cls, limit=1000, ascending=True):
   102     """Return a list of Tags sorted alphabetically by the name of the tag.
   128     """Return a list of Tags sorted alphabetically by the name of the tag.
   103     If a limit is given, return only that many tags; otherwise, return all.
   129     If a limit is given, return only that many tags; otherwise, return all.
   104     If ascending is True, sort from a-z; otherwise, sort from z-a."""
   130     If ascending is True, sort from a-z; otherwise, sort from z-a.
       
   131     """
   105 
   132 
   106     from google.appengine.api import memcache
   133     from google.appengine.api import memcache
   107 
   134 
   108     cache_name = cls.__name__ + '_tags_by_name'
   135     cache_name = cls.__name__ + '_tags_by_name'
   109     if ascending:
   136     if ascending:
   125         tags = tags[:limit]
   152         tags = tags[:limit]
   126 
   153 
   127     return tags
   154     return tags
   128 
   155 
   129   @classmethod
   156   @classmethod
       
   157   def copy_tag(cls, scope, tag_name, new_tag_name):
       
   158     """Copy a tag with a given scope and tag_name to another tag with
       
   159     new tag_name.
       
   160     """
       
   161     tag = cls.get_by_scope_and_name(scope, tag_name)
       
   162 
       
   163     if tag:
       
   164       tag_key_name = cls.__key_name(scope.key().name(), new_tag_name)
       
   165       existing_tag = cls.get_by_key_name(tag_key_name)
       
   166 
       
   167       if existing_tag is None:
       
   168         new_tag = cls(key_name=tag_key_name, tag=new_tag_name, scope=scope, 
       
   169                       added=tag.added, tagged=tag.tagged,
       
   170                       tagged_count=tag.tagged_count)
       
   171         new_tag.put()
       
   172         tag.delete()
       
   173 
       
   174         return new_tag
       
   175 
       
   176       return existing_tag
       
   177 
       
   178     return None
       
   179 
       
   180   @classmethod
       
   181   def delete_tag(cls, scope, tag_name):
       
   182     """Delete a tag with a given scope and tag_name.
       
   183     """
       
   184 
       
   185     tag = cls.get_by_scope_and_name(scope, tag_name)
       
   186 
       
   187     if tag:
       
   188       tag.delete()
       
   189       return True
       
   190 
       
   191     return False
       
   192 
       
   193   @classmethod
   130   def popular_tags(cls, limit=5):
   194   def popular_tags(cls, limit=5):
       
   195     """Get the most popular tags from memcache, or if they are not defined
       
   196     there, it retrieves them from datastore and sets in memcache.
       
   197     """
       
   198 
   131     from google.appengine.api import memcache
   199     from google.appengine.api import memcache
   132 
   200 
   133     tags = memcache.get(cls.__name__ + '_popular_tags')
   201     tags = memcache.get(cls.__name__ + '_popular_tags')
   134     if tags is None:
   202     if tags is None:
   135       tags = cls.get_tags_by_frequency(limit)
   203       tags = cls.get_tags_by_frequency(limit)
   137 
   205 
   138     return tags
   206     return tags
   139 
   207 
   140   @classmethod
   208   @classmethod
   141   def expire_cached_tags(cls):
   209   def expire_cached_tags(cls):
       
   210     """Expire all tag lists which exist in memcache.
       
   211     """
       
   212 
   142     from google.appengine.api import memcache
   213     from google.appengine.api import memcache
   143 
   214 
   144     memcache.delete(cls.__name__ + '_popular_tags')
   215     memcache.delete(cls.__name__ + '_popular_tags')
   145     memcache.delete(cls.__name__ + '_tags_by_name_asc')
   216     memcache.delete(cls.__name__ + '_tags_by_name_asc')
   146     memcache.delete(cls.__name__ + '_tags_by_name_desc')
   217     memcache.delete(cls.__name__ + '_tags_by_name_desc')
   149     """Returns the string representation of the entity's tag name.
   220     """Returns the string representation of the entity's tag name.
   150     """
   221     """
   151 
   222 
   152     return self.tag
   223     return self.tag
   153 
   224 
       
   225 
   154 def tag_property(tag_name):
   226 def tag_property(tag_name):
   155   """Decorator that creates and returns a tag property to be used 
   227   """Decorator that creates and returns a tag property to be used
   156   in Google AppEngine model.
   228   in Google AppEngine model.
   157 
   229 
   158   Args:
   230   Args:
   159     tag_name: name of the tag to be created.
   231     tag_name: name of the tag to be created.
   160   """
   232   """
   168       self._tags[tag_name] = self._tag_model[
   240       self._tags[tag_name] = self._tag_model[
   169           tag_name].get_tags_for_key(self.key())
   241           tag_name].get_tags_for_key(self.key())
   170     return self._tags[tag_name]
   242     return self._tags[tag_name]
   171 
   243 
   172   def set_tags(self, seed):
   244   def set_tags(self, seed):
   173     """Set a list of Tag objects for all Tags that apply to 
   245     """Set a list of Tag objects for all Tags that apply to
   174     the specified entity.
   246     the specified entity.
   175     """
   247     """
   176 
   248 
   177     import types
   249     import types
   178     if type(seed['tags']) is types.UnicodeType:
   250     if type(seed['tags']) is types.UnicodeType:
   210 
   282 
   211 
   283 
   212 class Taggable(object):
   284 class Taggable(object):
   213   """A mixin class that is used for making GAE Model classes taggable.
   285   """A mixin class that is used for making GAE Model classes taggable.
   214 
   286 
   215   This is an extended version of Taggable-mixin which allows for 
   287   This is an extended version of Taggable-mixin which allows for
   216   multiple tag properties in the same AppEngine Model class.
   288   multiple tag properties in the same AppEngine Model class.
   217   """
   289   """
   218     
   290 
   219   def __init__(self, **kwargs):
   291   def __init__(self, **kwargs):
   220     """The constructor class for Taggable, that creates a dictionary of tags.
   292     """The constructor class for Taggable, that creates a dictionary of tags.
   221 
   293 
   222     The difference from the original taggable in terms of interface is
   294     The difference from the original taggable in terms of interface is
   223     that, tag class is not used as the default tag model, since we don't
   295     that, tag class is not used as the default tag model, since we don't
   237 
   309 
   238     self.tag_separator = ", "
   310     self.tag_separator = ", "
   239 
   311 
   240   def tags_string(self, tag_name, ret_list=False):
   312   def tags_string(self, tag_name, ret_list=False):
   241     """Create a formatted string version of this entity's tags.
   313     """Create a formatted string version of this entity's tags.
   242     
   314 
   243     Args:
   315     Args:
   244       tag_name: the name of the tag which must be formatted
   316       tag_name: the name of the tag which must be formatted
   245       ret_list: if False sends a string, otherwise sends a Python list
   317       ret_list: if False sends a string, otherwise sends a Python list
   246     """
   318     """
   247 
   319