Commits

Seraphim Mellos  committed cb9bc46

Removed statistics generation code from models and added caching on Stats/BaseStats classes.

  • Participants
  • Parent commits d873050

Comments (0)

Files changed (7)

File transifex/projects/models.py

 SourceEntity = get_model('resources', 'SourceEntity')
 Translation = get_model('resources', 'Translation')
 
-
-# keys used in cache
-# We put it here to have them all in one place for the specific models!
-PROJECTS_CACHE_KEYS = {
-    "word_count": "wcount.%s",
-    "source_strings_count": "sscount.%s"
-}
-
 class DefaultProjectManager(models.Manager):
     """
     This is the defautl manager of the project model (asigned to objects field).
     def get_absolute_url(self):
         return ('project_detail', None, { 'project_slug': self.slug })
 
-    @property
-    def source_strings(self):
-        """
-        Return the list of all the strings, belonging to the Source Language
-        of the Project/Resource.
-        
-        CAUTION! 
-        1. This function returns Translation and not SourceEntity objects!
-        2. The strings may be in different source languages!!!
-        3. The source strings are not grouped based on the string value.
-        """
-        resources = self.resources.all()
-        source_strings = []
-        for resource in resources:
-            source_strings.extend(resource.source_strings)
-        return 
-
-    #TODO: Invalidation for cached value
-    @property
-    def total_entities(self):
-        """Return the total number of source entities to be translated."""
-        cache_key = (PROJECTS_CACHE_KEYS['source_strings_count'] % (self.project.slug,))
-        sc = cache.get(cache_key)
-        if not sc:
-            sc = SourceEntity.objects.filter(
-                resource__in=self.resources.all()).count()
-            cache.set(cache_key, sc)
-        return sc
-
-    # TODO: Invalidation for cached value
-    @property
-    def wordcount(self):
-        """
-        Return the number of words which need translation in this project.
-        
-        The counting of the words uses the Translation objects of the source
-        languages as set of objects.
-        CAUTION: 
-        1. The strings may be in different source languages!!!
-        2. The source strings are not grouped based on the string value.
-        """
-        cache_key = (PROJECTS_CACHE_KEYS['word_count'] % self.project.slug)
-        wc = cache.get(cache_key)
-        if not wc:
-            wc = 0
-            resources = self.resources.all()
-            for resource in resources:
-                wc += resource.wordcount
-            cache.set(cache_key, wc)
-        return wc
-
-    @property
-    def available_languages(self):
-        """
-        Return the languages with at least one Translation of a SourceEntity for
-        all Resources in the specific project instance.
-        """
-        # I put it here due to circular dependency on module
-        resources = self.resources.all()
-        languages = Translation.objects.filter(
-            rsource_entity__resource__in=resources).values_list(
-            'language', flat=True).distinct()
-        # The distinct() below is not important ... I put it just to be sure.
-        return Language.objects.filter(id__in=languages).distinct()
-
-    def translated_strings(self, language):
-        """
-        Return the QuerySet of source entities, translated in this language.
-        
-        This assumes that we DO NOT SAVE empty strings for untranslated entities!
-        """
-        # I put it here due to circular dependency on modules
-        target_language = Language.objects.by_code_or_alias(language)
-        return SourceEntity.objects.filter(resource__in=self.resources.all(),
-            id__in=Translation.objects.filter(language=target_language,
-                source_entity__resource__in=self.resources.all(),
-                rule=5).values_list('source_entity', flat=True))
-
-    def untranslated_strings(self, language):
-        """
-        Return the QuerySet of source entities which are not yet translated in
-        the specific language.
-        
-        This assumes that we DO NOT SAVE empty strings for untranslated entities!
-        """
-        # I put it here due to circular dependency on modules
-        target_language = Language.objects.by_code_or_alias(language)
-        return SourceEntity.objects.filter(
-            resource__in=self.resources.all()).exclude(
-            id__in=Translation.objects.filter(language=target_language,
-                source_entity__resource__in=self.resources.all(), 
-                rule=5).values_list('source_entity', flat=True))
-
-    def num_translated(self, language):
-        """
-        Return the number of translated strings in all Resources of the project.
-        """
-        return self.translated_strings(language).count()
-
-    def num_untranslated(self, language):
-        """
-        Return the number of untranslated strings in all Resources of the project.
-        """
-        return self.untranslated_strings(language).count()
-
-    def trans_percent(self, language):
-        """Return the percent of untranslated strings in this Resource."""
-        t = self.num_translated(language)
-        try:
-            return (t * 100 / self.total_entities)
-        except ZeroDivisionError:
-            return 100
-
-    def untrans_percent(self, language):
-        """Return the percent of untranslated strings in this Resource."""
-        translated_percent = self.trans_percent(language)
-        return (100 - translated_percent)
-
 tagging.register(Project, tag_descriptor_attr='tagsobj')
 log_model(Project)
 

File transifex/resources/handlers.py

 from projects.signals import post_resource_save, post_resource_delete
 from txcommon import notifications as txnotification
 from resources import CACHE_KEYS as RESOURCES_CACHE_KEYS
+from resources.utils import invalidate_object_cache
 from teams.models import Team
 
 def on_ss_save_invalidate_cache(sender, instance, created, **kwargs):
     """Invalidate cache keys related to the SourceEntity updates"""
-    cache.delete(RESOURCES_CACHE_KEYS["word_count"] % (instance.resource.project.slug,
-        instance.resource.slug))
+
     if created:
+        invalidate_object_cache(instance.resource)
         # Number of source strings in resource
-        cache.delete(RESOURCES_CACHE_KEYS["source_strings_count"]% (
-            instance.resource.project.slug,
-            instance.resource.slug))
         for lang in instance.resource.available_languages:
-            team = Team.objects.get_or_none(instance.resource.project, lang)
+            team = Team.objects.get_or_none(instance.resource.project, lang.code)
             if team:
                 # Template lvl cache for team details
                 invalidate_template_cache("team_details",
 def on_ss_delete_invalidate_cache(sender, instance, **kwargs):
     """Invalidate cache keys related to the SourceEntity updates"""
     if instance and instance.resource and instance.resource.project:
-        # Number of words in resource
-        cache.delete(RESOURCES_CACHE_KEYS["word_count"] % (instance.resource.project.slug,
-            instance.resource.slug))
-        # Number of source entities in resource
-        cache.delete(RESOURCES_CACHE_KEYS["source_strings_count"]% (
-            instance.resource.project.slug,
-            instance.resource.slug))
+        invalidate_object_cache(instance.resource)
 
         for lang in instance.resource.available_languages:
-            team = Team.objects.get_or_none(instance.resource.project, lang)
+            team = Team.objects.get_or_none(instance.resource.project, lang.code)
             if team:
                 # Template lvl cache for team details
                 invalidate_template_cache("team_details",
                 instance.resource.project.slug, instance.resource.slug,
                 lang.code)
 
-            # Resource available languages
-            cache.delete(RESOURCES_CACHE_KEYS["lang_trans"] % (
-                lang.code,
-                instance.resource.project.slug,
-                instance.resource.slug))
-
 def on_ts_save_invalidate_cache(sender, instance, created, **kwargs):
     """
     Invalidation for Translation save()
      - Invalidate the translated_strings for this resource/language
      - Invalidate the last_updated property for this language
     """
-    # Update resource available languages
-    langs_key = RESOURCES_CACHE_KEYS["available_langs"] % (
-        instance.source_entity.resource.project.slug,
-        instance.source_entity.resource.slug)
-
-    if cache.get(langs_key) and instance.language not in cache.get(langs_key):
-        cache.delete(langs_key)
-
-    # Update lant_update for specific language
-    cache.set(RESOURCES_CACHE_KEYS["lang_last_update"] % (
-            instance.language.code,
-            instance.source_entity.resource.project.slug,
-            instance.source_entity.resource.slug), instance)
 
     if created:
-        # new translation. Update language percentage
-        cache.delete(RESOURCES_CACHE_KEYS["lang_trans"] % (
-            instance.language.code,
-            instance.source_entity.resource.project.slug,
-            instance.source_entity.resource.slug))
+        invalidate_object_cache(instance.source_entity.resource)
 
         team = Team.objects.get_or_none(instance.source_entity.resource.project,
-            instance.language)
+            instance.language.code)
         if team:
             # Invalidate team details template cache for this lang
             invalidate_template_cache("team_details",
      - Invalidate the translated_stings for the resource/language
      - Invalidate the last_updated property for this language
     """
-    # Invalidate resource details template cache for this lang
-    invalidate_template_cache("resource_details",
-        instance.source_entity.resource.project.slug,
-        instance.source_entity.resource.slug, instance.language.code)
+    invalidate_object_cache(instance.source_entity.resource)
 
     team = Team.objects.get_or_none(instance.source_entity.resource.project,
-        instance.language)
+        instance.language.code)
     if team:
         # Invalidate team details template cache for this lang
         invalidate_template_cache("team_details",
         invalidate_template_cache("release_details",
             rel.id, instance.language.id)
 
-    # Update available langs for this resource
-    langs_key = RESOURCES_CACHE_KEYS["available_langs"] % (
-        instance.source_entity.resource.project.slug,
-        instance.source_entity.resource.slug)
-
-    if cache.get(langs_key) and instance.language not in cache.get(langs_key):
-        cache.delete(langs_key)
-
-    # Update lant_update for specific language
-    cache.set(RESOURCES_CACHE_KEYS["lang_last_update"] % (
-            instance.language.code,
-            instance.source_entity.resource.project.slug,
-            instance.source_entity.resource.slug), instance)
-
-    # Update translated percentage for specific language
-    cache.delete(RESOURCES_CACHE_KEYS["lang_trans"] % (
-            instance.language.code,
-            instance.source_entity.resource.project.slug,
-            instance.source_entity.resource.slug))
-
 def invalidate_template_cache(fragment_name, *variables):
     """
     This function invalidates a template cache named `fragment_name` and with

File transifex/resources/models.py

         """        
         return "%s.%s" % (self.project.slug, self.slug)
 
-
-    @property
-    def source_strings(self):
-        """Return the source language translations, including plurals."""
-        return Translation.objects.filter(source_entity__resource=self,
-                                           language=self.source_language)
-
     @property
     def entities(self):
         """Return the resource's translation entities."""
         return SourceEntity.objects.filter(resource=self)
 
-
-    @property
-    def entities_count(self):
-        """Return the number of source entities."""
-        cache_key = (RESOURCES_CACHE_KEYS['source_strings_count'] % (self.project.slug, self.slug))
-        sc = cache.get(cache_key)
-        if not sc:
-            sc = self.entities.count()
-            cache.set(cache_key, sc)
-        return sc
-
-
-    @property
-    def wordcount(self):
-        """
-        Return the number of words which need translation in this resource.
-        
-        The counting of the words uses the Translation objects of the SOURCE
-        LANGUAGE as set of objects. This function does not count the plural 
-        strings!
-        """
-        cache_key = (RESOURCES_CACHE_KEYS['word_count'] % (self.project.slug, self.slug))
-        wc = cache.get(cache_key)
-        if not wc:
-            wc = 0
-            for ss in self.source_strings:
-                wc += ss.wordcount
-            cache.set(cache_key, wc)
-        return wc
-
-    @property
-    def last_committer(self):
-        """
-        Return the overall last committer for the translation of this resource.
-        """
-        lt = self.last_translation(language=None)
-        if lt:
-            return lt.user
-        return None
-
-    def last_translation(self, language=None):
-        """
-        Return the last translation for this Resource and the specific lang.
-        
-        If None language value then return in all languages avaible the last 
-        translation.
-        """
-        if language:
-            if not isinstance(language, Language):
-                language = Language.objects.by_code_or_alias(language)
-
-            ckey = RESOURCES_CACHE_KEYS["lang_last_update"] % (
-                language.code, self.project.slug, self.slug)
-            val = cache.get(ckey)
-
-            if val: return val
-
-            t = Translation.objects.filter(source_entity__resource=self,
-                language=language).order_by('-last_update').select_related('user')
-
-
-        else:
-            
-            ckey = RESOURCES_CACHE_KEYS["lang_last_update"] % (
-                "None", self.project.slug, self.slug)
-            val = cache.get(ckey)
-
-            if val: return val
-
-            t = Translation.objects.filter(
-                source_entity__resource=self).order_by('-last_update').select_related('user')
-
-        if t:
-            cache.set(ckey, t[0])
-            return t[0]
-        return None
-
     @property
     def available_languages(self):
         """
-        Return the languages with at least one Translation of a SourceEntity for
-        this Resource.
+        Return the languages with at least one Translation of a SourceEntity
+        for this Resource.
         """
-        ckey = RESOURCES_CACHE_KEYS["available_langs"] % (
-            self.project.slug, self.slug)
-        val = cache.get(ckey)
-        if val: return val
-
         languages = Translation.objects.filter(source_entity__resource=self).values_list(
             'language__id', flat=True)
-        cache.set(ckey, Language.objects.filter(id__in=languages).distinct())
         return Language.objects.filter(id__in=languages).distinct()
 
-    def translated_strings(self, language):
-        """
-        Return the QuerySet of source entities, translated in this language.
-        
-        This assumes that we DO NOT SAVE empty strings for untranslated entities!
-        """
-        if not isinstance(language, Language):
-            language = Language.objects.by_code_or_alias(language)
-
-        ckey = RESOURCES_CACHE_KEYS["lang_trans"] % (
-            language.code, self.project.slug, self.slug)
-        val = cache.get(ckey)
-
-        if val:
-            return val
-
-        trans = Translation.objects.filter(language=language,
-                source_entity__resource=self, rule=5).values_list('source_entity', flat=True)
-
-        cache.set(RESOURCES_CACHE_KEYS["lang_trans"] % (
-            language.code, self.project.slug, self.slug), trans)
-
-        return trans
-
-    def untranslated_strings(self, language):
-        """
-        Return the QuerySet of source entities which are not yet translated in
-        the specific language.
-        
-        This assumes that we DO NOT SAVE empty strings for untranslated entities!
-        """
-        if not isinstance(language, Language):
-            language = Language.objects.by_code_or_alias(language)
-
-        return SourceEntity.objects.filter(resource=self).exclude(
-            id__in=self.translated_strings(language))
-
-    def num_translated(self, language):
-        """
-        Return the number of translated strings in this Resource for the language.
-        """
-        return self.translated_strings(language).count()
-
-    def num_untranslated(self, language):
-        """
-        Return the number of untranslated strings in this Resource for the language.
-        """
-        return self.untranslated_strings(language).count()
-
-    def trans_percent(self, language):
-        """Return the percent of untranslated strings in this Resource."""
-        t = self.num_translated(language)
-        try:
-            return (t * 100 / self.entities_count)
-        except ZeroDivisionError:
-            return 100
-
-    def untrans_percent(self, language):
-        """Return the percent of untranslated strings in this Resource."""
-        translated_percent = self.trans_percent(language)
-        return (100 - translated_percent)
-        # With the next approach we lose some data because we cut floating points
-#        u = self.num_untranslated(language)
-#        try:
-#            return (u * 100 / self.entities_count)
-#        except ZeroDivisionError:
-#            return 0
-    
-
-
-    # XXX: Obsolete. Now that filehandlers are implemented the merge_*
-    # methods are no longer needed.
-
-    @transaction.commit_manually
-    def merge_stringset(self, stringset, target_language, metadata=None, is_source=False, user=None, overwrite_translations=True):
-
-        try:
-            strings_added = 0
-            strings_updated = 0
-            for j in stringset.strings:
-                # Check SE existence
-                try:
-                    se = SourceEntity.objects.get(
-                        string = j.source_entity,
-                        context = j.context or "None",
-                        resource = self
-                    )
-                except SourceEntity.DoesNotExist:
-                    # Skip creation of sourceentity object for non-source files.
-                    if not is_source:
-                        continue
-                    # Create the new SE
-                    se = SourceEntity.objects.create(
-                        string = j.source_entity,
-                        context = j.context or "None",
-                        resource = self,
-                        pluralized = j.pluralized,
-                        position = 1,
-                        # FIXME: this has been tested with pofiles only
-                        occurrences = j.occurrences,
-                    )
-
-                # Skip storing empty strings as translations!
-                if not se and not j.translation:
-                    continue
-                tr, created = Translation.objects.get_or_create(
-                    source_entity = se,
-                    language = target_language,
-                    rule = j.rule,
-                    defaults = {
-                        'string' : j.translation,
-                        'user' : user,
-                        },
-                    )
-
-                if created and j.rule==5:
-                    strings_added += 1
-
-                if not created and overwrite_translations:
-                    if tr.string != j.translation:
-                        tr.string = j.translation
-                        tr.save()
-                        strings_updated += 1
-        except Exception, e:
-            logger.error("There was problem while importing the entries "
-                         "into the database. Entity: '%s'. Error: '%s'."
-                         % (j.source_entity, str(e)))
-            transaction.rollback()
-            return 0,0
-        else:
-            self.source_file_metadata = json.dumps(metadata)
-            self.save()
-            transaction.commit()
-            return strings_added, strings_updated
 
 class SourceEntity(models.Model):
     """

File transifex/resources/stats.py

+from django.core.cache import cache
 from languages.models import Language
-
-from txcommon.utils import cached_property
 from resources.models import Resource, Translation, SourceEntity
-
+from resources.utils import *
 
 class StatsBase():
     """A low-level statistics-holding object to inherit from.
     Requires an iterable of entities (e.g. a QuerySet).
     """
 
+    # This object is like an identifier for caching. We only cache object
+    # related stats classes, otherwise if we do it for plain querysets, we'd
+    # have to evaluate them everytime. This object can hold a Resource, Project
+    # or Release
+    object = None
+
     def __init__(self, entities):
         self.entities = entities
 
-    @cached_property
+    @stats_cached_property
     def translations(self):
         """Return all translations for the related entities."""
         return Translation.objects.filter(source_entity__in=self.entities)
 
-    @cached_property
+    @stats_cached_property
     def last_translation(self):
         """Return last translation made, independing on language."""
         t = self.translations.select_related('last_update', 'user'
         if t:
             return t[0]
 
-    @cached_property
+    @stats_cached_property
     def last_update(self):
         """
         Return the time of the last translation made, without depending on 
         if lt:
             return lt.last_update
 
-    @cached_property
+    @stats_cached_property
     def last_committer(self):
         """
         Return the committer of the last translation made, without depending on
         if lt:
             return lt.user
 
-    @cached_property
+    @stats_cached_property
     def total_entities(self):
         """
         Return the total number of SourceEntity objects to be translated.
         self.entities = entities
         self.language = language
 
-    @cached_property
+    @stats_cached_property
     def translations(self):
         return Translation.objects.filter(source_entity__in=self.entities,
             language=self.language)
 
-    @cached_property
+    @stats_cached_property
     def num_translated(self):
         """Return the number of translated entries."""
         trans_ids = self.translations.values_list('source_entity', flat=True)
         return SourceEntity.objects.filter(id__in=trans_ids).values('id').count()
 
-    @cached_property
+    @stats_cached_property
     def num_untranslated(self):
         """Return the number of untranslated entries."""
         trans_ids = self.translations.values_list('source_entity', flat=True)
         return SourceEntity.objects.filter(id__in=self.entities
             ).exclude(id__in=trans_ids).values('id').count()
 
-    @cached_property
+    @stats_cached_property
     def trans_percent(self):
         """Return the percent of translated entries."""
         t = self.num_translated
         except ZeroDivisionError:
             return 100
 
-    @cached_property
+    @stats_cached_property
     def untrans_percent(self):
         """Return the percent of untranslated entries."""
         return (100 - self.trans_percent)
 
-    @cached_property
+    @stats_cached_property
     def resources(self):
         """Return a list of resources related to the given entities."""
         return Resource.objects.filter(source_entities__in=self.entities
         """Return a Stat object for a specific language."""
         return Stats(entities=self.entities, language=language)
 
-    @cached_property
+    @property
     def available_languages(self):
         """
         Return a list of languages with at least one translation for one of 
         language_ids = self.translations.values_list('language__id', flat=True)
         return Language.objects.filter(id__in=language_ids).distinct()
 
-    @cached_property
     def language_stats(self):
         """Yield a Stat object for each available language.
 
             ).distinct()
         for resource in resources:
             sa = StatsBase(resource.entities)
-            sa.resource = resource
+            sa.object = resource
             yield sa
 
     def resource_stats_for_language(self, language):
             ).distinct()
         for resource in resources:
             sa = Stats(resource.entities, language)
-            sa.resource = resource
+            sa.object = resource
             yield sa
 
 
     class attrs.
     """
     def __init__(self, resource):
-        self.resource = resource
+        self.object = resource
         self.entities = SourceEntity.objects.filter(resource=resource)
 
     @property
         strings!
         """
         wc = 0
-        for ss in self.resource.source_strings:
+        for ss in self.object.source_strings:
             wc += ss.wordcount
         return wc
 
     class attrs.
     """
     def __init__(self, project):
-        self.project = project
+        self.object = project
         self.entities = SourceEntity.objects.filter(resource__project=project)
 
 
     class attrs.
     """
     def __init__(self, release):
-        self.release = release
+        self.object = release
         self.entities = SourceEntity.objects.filter(resource__releases=release)
 

File transifex/resources/utils.py

+import inspect
+from django.core.cache import cache
+
+
+def key_from_instance(instance):
+    """
+    Returns a key from an object instance that is unique and can be used as a
+    caching key.
+    """
+    opts = instance._meta
+    return '%s.%s:%s' % (opts.app_label, opts.module_name, instance.pk)
+
+def cache_set(key, value):
+    """
+    Similar to cache.set only it returns the value as well. Usefull when you
+    want to do something like this:
+
+    # return cache.set(key,value)
+    """
+    cache.set(key, value)
+    return value
+
+def stats_cached_property(func):
+    """
+    A method decorator that does the same thing as the @property with added
+    caching for the return value of the method. They key used for the caching
+    is handcrafted and suited only for Stats/StatsBase/StatsList classes so
+    it's not very reusable.
+    """
+    def cached_func(self):
+        if not self.object:
+            return func(self)
+        key = 'cached_property_%s_%s_%s' % \
+            (key_from_instance(self.object), func.__module__, func.__name__)
+        return cache.get(key) or cache_set(key, func(self))
+    return property(cached_func)
+
+def invalidate_object_cache(object):
+    """
+    Invalidate all cached properties of a specific object's stats. The only
+    properties that are actually invalidated are those who belong to a Stats
+    class. #FIXME: find a better way to handle invalidation for all classes.
+    """
+    # !important: If you change the import path to include transifex (eg
+    # transifex.resources.stats) this will BREAK caching since the keys that
+    # are being created don't have the full path of the module. If you need to
+    # change this import statement then you NEED to change the
+    # stats_cached_property as well.
+    from resources.stats import Stats, StatsBase
+
+    keys = []
+    for c in [Stats, StatsBase]:
+        for name, type in inspect.getmembers(Stats):
+            if type.__class__.__name__ == "property":
+                keys.append("_".join([c.__module__, name]))
+
+    for key in keys:
+        cache.delete("cached_property_%s_%s" % (key_from_instance(object), key))

File transifex/templates/projects/resource_list.html

   {% endif %}
     <tr>
         <td>
-          <a class="res_tipsy_enable" href="{{ stat.resource.get_absolute_url }}" style="font-weight:bold" title="Total Strings: {{ stat.resource.entities_count }}<br/>Available Languages: {{ stat.resource.available_languages|length }}">{{ stat.resource.name }}</a>
+          <a class="res_tipsy_enable" href="{% url resource_detail project.slug stat.object.slug %}" style="font-weight:bold" title="Total Strings: {{ stat.total_entities }}<br/>Available Languages: {{ stat.available_languages|length }}">{{ stat.object.name }}</a>
           <sup class="entry_metalink">
              {% if is_maintainer or request.user.is_superuser %}
-               <a href="{% url resource_edit project.slug stat.resource.slug %}">{% trans "edit" %}</a>
-               , <a href="{% url resource_delete project.slug stat.resource.slug %}">{% trans "del" %}</a>
+               <a href="{% url resource_edit project.slug stat.object.slug %}">{% trans "edit" %}</a>
+               , <a href="{% url resource_delete project.slug stat.object.slug %}">{% trans "del" %}</a>
              {% endif %}
           </sup>
         </td>
         <td class="last_updated">
-          <span class="res_tipsy_enable" style="border:0" title="{% if stat.resource.last_committer %}Committed by {{ stat.resource.last_committer }}<br/>{% else %}No committers yet{% endif %}">
-          {% if stat.resource.last_update %}
-              <span class="origin_format" style="display:none">{{ stat.resource.last_update|date:"M d,Y h:i A" }}</span>
-              {{ stat.resource.last_update|timesince }}
-          {% else %}
-              {% trans "no translations yet" %}
-          {% endif %}
+          {% with stat.last_committer as last_committer %}
+            <span class="res_tipsy_enable" style="border:0" title="{% if last_committer %}Committed by {{ last_committer }}<br/>{% else %}No committers yet{% endif %}">
+          {% endwith %}
+          {% with stat.last_update as last_update %}
+            {% if last_update %}
+                <span class="origin_format" style="display:none">{{ last_update|date:"M d,Y h:i A" }}</span>
+                {{ last_update|timesince }}
+            {% else %}
+                {% trans "no translations yet" %}
+            {% endif %}
+          {% endwith %}
           </span>
         </td>
         <td class="priority_level" style="width:100px;text-align:center">
+            {% with stat.object.priority.level as priority_level %}
+            {% with stat.object.priority.display_level as display_level %}
             {% if is_maintainer %}
-              <a id="priority_{{ stat.resource.slug }}" class="resource_priority_trigger" style="cursor:pointer">
-                <span class="priority_sort" style="display:none">{{ stat.resource.priority.level }}</span>
-                <img src="{{ STATIC_URL }}priorities/images/{{ stat.resource.priority.display_level }}.png" style="border:0" title="{{ stat.resource.priority.display_level }}"/>
+              <a id="priority_{{ stat.object.slug }}" class="resource_priority_trigger" style="cursor:pointer">
+                <span class="priority_sort" style="display:none">{{ priority_level }}</span>
+                <img src="{{ STATIC_URL }}priorities/images/{{ priority_display_level }}.png" style="border:0" title="{{ display_level }}"/>
               </a>
             {% else %}
-              <span class="priority_sort" style="display:none">{{ stat.resource.priority.level }}</span>
-              {% ifnotequal stat.resource.priority.level "0" %}
-              <img class="res_tipsy_enable" src="{{ STATIC_URL }}priorities/images/{{ stat.resource.priority.display_level }}.png" style="border:0" title="{{ stat.resource.priority.display_level }}"/>
+              <span class="priority_sort" style="display:none">{{ priority_level }}</span>
+              {% ifnotequal priority_level "0" %}
               {% endifnotequal %}
             {% endif %}
+            {% endwith %}
+            {% endwith %}
         </td>
     </tr>
   {% if forloop.last %}

File transifex/templates/teams/team_detail.html

   {% for stat in statslist %}
 
   <tr id="stat_row_{{forloop.counter}}" title="{% trans 'click for translation' %}">
-  {% cache 604800 team_details team.id stat.resource.id %}
+  {% cache 604800 team_details team.id stat.object.id %}
         <td>
-        {{ stat.resource.name }}
+        {{ stat.object.name }}
         </td>
         <td>
         {% with 200 as barwidth %}
            /* FancyBox Using custom settings */ 
            $.fancybox(
                { 
-                'href': '{% url resource_actions team.project.slug stat.resource.slug team.language.code %}',
+                'href': '{% url resource_actions team.project.slug stat.object.slug team.language.code %}',
                 'hideOnContentClick': false,
                 'onComplete':function() {
                     $(".tipsy_enable").tipsy({'html':true, 'gravity':'s'});
                     if(!$("#download_for_translation").hasClass('disabled')){
                     $("#download_for_translation").click(function(){
                         $.ajax({
-                            url : '{% url lock_and_download_translation stat.resource.project.slug stat.resource.slug team.language.code %}',
+                            url : '{% url lock_and_download_translation project.slug stat.object.slug team.language.code %}',
                             contentType : 'application/json',
                             type : 'POST',
                             beforeSend: function(){