<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -123,7 +123,7 @@ Pootle:
         for dirpath, subdirs, filenames in os.walk(self.po_dir, topdown=False):
             for name in filenames:
                 pofilename = os.path.join(dirpath, name)
-                parsedfile = pootlefile.pootlefile(pofilename=pofilename, generatestats=True)
+                parsedfile = pootlefile.pootlefile(pofilename=pofilename)
                 count += len(parsedfile.units)
         print &quot;stats on %d units&quot; % count
 
@@ -167,9 +167,9 @@ Pootle:
         args = {&quot;pofilename&quot;: pofilename, &quot;submit0&quot;: &quot;true&quot;, &quot;trans0&quot;: &quot;changed&quot;}
         page = self.server.getpage([&quot;zxx&quot;, &quot;benchmark&quot;, &quot;translate.html&quot;], session, args)
         pofile = project.getpofile(pofilename)
-        print str(pofile.transunits[0])
+        print str(pofile.getitem(0))
         # assert fails because of multistring
-        # assert pofile.transunits[0].unquotedmsgstr == &quot;changed&quot;
+        # assert pofile.getitem(0).unquotedmsgstr == &quot;changed&quot;
         print page.templatevars
 
 if __name__ == &quot;__main__&quot;:</diff>
      <filename>benchmark.py</filename>
    </modified>
    <modified>
      <diff>@@ -32,7 +32,8 @@ def processfile(filename):
   pofile = pootlefile.pootlefile(dummyproject, os.path.basename(filename))
   pofile.readpofile()
   conflictitems = []
-  for item, poentry in enumerate(pofile.transunits):
+  for item in pofile.statistics.getstats()[&quot;total&quot;]:
+    poentry = pofile.units[item]
     if poentry.hasplural():
       targets = poentry.target.strings
     else:</diff>
      <filename>conflict2suggest.py</filename>
    </modified>
    <modified>
      <diff>@@ -332,21 +332,19 @@ class ProjectIndex(pagelayout.PootleNavPage):
       mainstats = &quot;&quot;
       mainicon = &quot;file&quot;
     else:
-      if dirfilter or self.editing or self.showassigns or self.showchecks:
+      pofilenames = self.project.browsefiles(dirfilter)
+      projecttotals = self.project.getquickstats(pofilenames)
+      if self.editing or self.showassigns or self.showchecks:
         # we need the complete stats
-        pofilenames = self.project.browsefiles(dirfilter)
         projectstats = self.project.combinestats(pofilenames)
       else:
-        # a common case: plain stats table we can take a shortcut
-        pofilenames = self.project.browsefiles()
-        projectstats = self.project.getquickstats()
+        projectstats = projecttotals
       if self.editing:
         actionlinks = self.getactionlinks(&quot;&quot;, projectstats, [&quot;editing&quot;, &quot;mine&quot;, &quot;review&quot;, &quot;check&quot;, &quot;assign&quot;, &quot;goal&quot;, &quot;quick&quot;, &quot;all&quot;, &quot;zip&quot;, &quot;sdf&quot;], dirfilter)
       else: 
         actionlinks = self.getactionlinks(&quot;&quot;, projectstats, [&quot;editing&quot;, &quot;goal&quot;, &quot;zip&quot;, &quot;sdf&quot;])
-      mainstats = self.getitemstats(&quot;&quot;, projectstats, len(pofilenames))
-      maindata = self.getstats(self.project, projectstats)
-      mainicon = &quot;folder&quot;
+      mainstats = self.getitemstats(&quot;&quot;, pofilenames, len(pofilenames))
+      mainstats[&quot;summary&quot;] = self.describestats(self.project, projecttotals, len(pofilenames))
     if self.showgoals:
       childitems = self.getgoalitems(dirfilter)
     else:
@@ -490,11 +488,11 @@ class ProjectIndex(pagelayout.PootleNavPage):
       if assignwhich == &quot;all&quot;:
         pass
       elif assignwhich == &quot;untranslated&quot;:
-        search.matchnames = [&quot;fuzzy&quot;, &quot;blank&quot;]
+        search.matchnames = [&quot;fuzzy&quot;, &quot;untranslated&quot;]
       elif assignwhich == &quot;unassigned&quot;:
         search.assignedto = [None]
       elif assignwhich == &quot;unassigneduntranslated&quot;:
-        search.matchnames = [&quot;fuzzy&quot;, &quot;blank&quot;]
+        search.matchnames = [&quot;fuzzy&quot;, &quot;untranslated&quot;]
         search.assignedto = [None]
       else:
         raise ValueError(&quot;unexpected assignwhich&quot;)
@@ -683,7 +681,8 @@ class ProjectIndex(pagelayout.PootleNavPage):
         goal[&quot;goal&quot;][&quot;show_adduser&quot;] = True
         goal[&quot;goal&quot;][&quot;otherusers&quot;] = unassignedusers
         goal[&quot;goal&quot;][&quot;adduser_title&quot;] = self.localize(&quot;Add User&quot;)
-    goal[&quot;stats&quot;] = self.getitemstats(&quot;&quot;, projectstats, len(pofilenames))
+    goal[&quot;stats&quot;] = self.getitemstats(&quot;&quot;, pofilenames, len(pofilenames))
+    projectstats = self.project.getquickstats(pofilenames)
     goal[&quot;data&quot;] = self.getstats(self.project, projectstats)
     return goal
 
@@ -694,7 +693,7 @@ class ProjectIndex(pagelayout.PootleNavPage):
       goalfilenames = self.project.getgoalfiles(self.currentgoal, dirfilter=direntry, includedirs=False, expanddirs=True)
       projectstats = self.project.combinestats(goalfilenames)
     else:
-      projectstats = self.project.combinestats(pofilenames)
+      projectstats = self.project.combine_totals(pofilenames)
     basename = os.path.basename(direntry)
     browseurl = self.getbrowseurl(&quot;%s/&quot; % basename, **newargs)
     diritem = {&quot;href&quot;: browseurl, &quot;title&quot;: basename, &quot;icon&quot;: &quot;folder&quot;, &quot;isdir&quot;: True}
@@ -702,10 +701,12 @@ class ProjectIndex(pagelayout.PootleNavPage):
     actionlinks = self.getactionlinks(basename, projectstats, linksrequired=linksrequired)
     diritem[&quot;actions&quot;] = actionlinks
     if self.showgoals and &quot;goal&quot; in self.argdict:
-      diritem[&quot;stats&quot;] = self.getitemstats(basename, projectstats, (len(goalfilenames), len(pofilenames)))
+      diritem[&quot;stats&quot;] = self.getitemstats(basename, goalfilenames, (len(goalfilenames), len(pofilenames)))
+      projectstats = self.project.getquickstats(goalfilenames)
       diritem[&quot;data&quot;] = self.getstats(self.projects, projectstats)
     else:
-      diritem[&quot;stats&quot;] = self.getitemstats(basename, projectstats, len(pofilenames))
+      diritem[&quot;stats&quot;] = self.getitemstats(basename, pofilenames, len(pofilenames))
+      projectstats = self.project.getquickstats(pofilenames)
       diritem[&quot;data&quot;] = self.getstats(self.project, projectstats)
     return diritem
 
@@ -717,7 +718,7 @@ class ProjectIndex(pagelayout.PootleNavPage):
       else:
         linksrequired = [&quot;mine&quot;, &quot;review&quot;, &quot;quick&quot;, &quot;all&quot;, &quot;po&quot;, &quot;xliff&quot;, &quot;update&quot;, &quot;commit&quot;]
     basename = os.path.basename(fileentry)
-    projectstats = self.project.combinestats([fileentry])
+    projectstats = self.project.combine_totals([fileentry])
     browseurl = self.getbrowseurl(basename, **newargs)
     fileitem = {&quot;href&quot;: browseurl, &quot;title&quot;: basename, &quot;icon&quot;: &quot;file&quot;, &quot;isfile&quot;: True}
     actions = self.getactionlinks(basename, projectstats, linksrequired=linksrequired)
@@ -762,7 +763,8 @@ class ProjectIndex(pagelayout.PootleNavPage):
       else:
         actionlink[&quot;sep&quot;] = &quot;&quot;
     fileitem[&quot;actions&quot;] = actions
-    fileitem[&quot;stats&quot;] = self.getitemstats(basename, projectstats, None)
+    fileitem[&quot;stats&quot;] = self.getitemstats(basename, [fileentry], None)
+    projectstats = self.project.getquickstats([fileentry])
     fileitem[&quot;data&quot;] = self.getstats(self.project, projectstats)
     return fileitem
 
@@ -786,7 +788,7 @@ class ProjectIndex(pagelayout.PootleNavPage):
       else:
         action = None
       assignstats = self.project.combineassignstats(assignfilenames, action)
-      assignusers = [username.replace(&quot;assign-&quot;, &quot;&quot;, 1) for username in assignstats.iterkeys()]
+      assignusers = list(assignstats.iterkeys())
       useroptions += [username for username in assignusers if username not in useroptions]
       if len(assignusers) &gt; 1:
         multiusers = &quot;multiple&quot;
@@ -859,34 +861,34 @@ class ProjectIndex(pagelayout.PootleNavPage):
         minelink = self.localize(&quot;Translate My Strings&quot;)
       else:
         minelink = self.localize(&quot;View My Strings&quot;)
-      mystats = projectstats.get(&quot;assign-%s&quot; % self.session.username, [])
+      mystats = projectstats.get('assign', {}).get(self.session.username, [])
       if len(mystats):
         minelink = {&quot;href&quot;: self.makelink(baseactionlink, assignedto=self.session.username), &quot;text&quot;: minelink}
       else:
         minelink = {&quot;title&quot;: self.localize(&quot;No strings assigned to you&quot;), &quot;text&quot;: minelink}
       actionlinks.append(minelink)
       if &quot;quick&quot; in linksrequired and &quot;translate&quot; in self.rights:
-        mytranslatedstats = [statsitem for statsitem in mystats if statsitem in projectstats.get(&quot;translated&quot;, [])]
+        mytranslatedstats = [statsitem for statsitem in mystats if statsitem in projectstats[&quot;units&quot;].get(&quot;translated&quot;, [])]
         quickminelink = self.localize(&quot;Quick Translate My Strings&quot;)
         if len(mytranslatedstats) &lt; len(mystats):
-          quickminelink = {&quot;href&quot;: self.makelink(baseactionlink, assignedto=self.session.username, fuzzy=1, blank=1), &quot;text&quot;: quickminelink}
+          quickminelink = {&quot;href&quot;: self.makelink(baseactionlink, assignedto=self.session.username, fuzzy=1, untranslated=1), &quot;text&quot;: quickminelink}
         else:
           quickminelink = {&quot;title&quot;: self.localize(&quot;No untranslated strings assigned to you&quot;), &quot;text&quot;: quickminelink}
         actionlinks.append(quickminelink)
-    if &quot;review&quot; in linksrequired and projectstats.get(&quot;has-suggestion&quot;, []):
+    if &quot;review&quot; in linksrequired and projectstats.get(&quot;units&quot;, {}).get(&quot;check-hassuggestion&quot;, []):
       if &quot;review&quot; in self.rights:
         reviewlink = self.localize(&quot;Review Suggestions&quot;)
       else:
         reviewlink = self.localize(&quot;View Suggestions&quot;)
-      reviewlink = {&quot;href&quot;: self.makelink(baseactionlink, review=1, **{&quot;has-suggestion&quot;: 1}), &quot;text&quot;: reviewlink}
+      reviewlink = {&quot;href&quot;: self.makelink(baseactionlink, review=1, **{&quot;hassuggestion&quot;: 1}), &quot;text&quot;: reviewlink}
       actionlinks.append(reviewlink)
     if &quot;quick&quot; in linksrequired:
       if &quot;translate&quot; in self.rights:
         quicklink = self.localize(&quot;Quick Translate&quot;)
       else:
         quicklink = self.localize(&quot;View Untranslated&quot;)
-      if len(projectstats.get(&quot;translated&quot;, [])) &lt; len(projectstats.get(&quot;total&quot;, [])):
-        quicklink = {&quot;href&quot;: self.makelink(baseactionlink, fuzzy=1, blank=1), &quot;text&quot;: quicklink}
+      if projectstats.get(&quot;translated&quot;, 0) &lt; projectstats.get(&quot;total&quot;, 0):
+        quicklink = {&quot;href&quot;: self.makelink(baseactionlink, fuzzy=1, untranslated=1), &quot;text&quot;: quicklink}
       else:
         quicklink = {&quot;title&quot;: self.localize(&quot;No untranslated items&quot;), &quot;text&quot;: quicklink}
       actionlinks.append(quicklink)
@@ -928,17 +930,16 @@ class ProjectIndex(pagelayout.PootleNavPage):
       actions[&quot;basic&quot;][-1][&quot;sep&quot;] = &quot;&quot;
     return actions
 
-  def getitemstats(self, basename, projectstats, numfiles):
+  def getitemstats(self, basename, pofilenames, numfiles):
     &quot;&quot;&quot;returns a widget summarizing item statistics&quot;&quot;&quot;
-    statssummary = self.describestats(self.project, projectstats, numfiles)
-    stats = {&quot;summary&quot;: statssummary, &quot;checks&quot;: [], &quot;tracks&quot;: [], &quot;assigns&quot;: []}
+    stats = {&quot;checks&quot;: [], &quot;tracks&quot;: [], &quot;assigns&quot;: []}
     if not basename or basename.endswith(&quot;/&quot;):
       linkbase = basename + &quot;translate.html?&quot;
     else:
       linkbase = basename + &quot;?translate=1&quot;
-    if projectstats:
+    if pofilenames:
       if self.showchecks:
-        stats[&quot;checks&quot;] = self.getcheckdetails(projectstats, linkbase)
+        stats[&quot;checks&quot;] = self.getcheckdetails(pofilenames, linkbase)
       if self.showtracks:
         trackfilter = (self.dirfilter or &quot;&quot;) + basename
         trackpofilenames = self.project.browsefiles(trackfilter)
@@ -949,15 +950,16 @@ class ProjectIndex(pagelayout.PootleNavPage):
           removelinkbase = &quot;?showassigns=1&amp;removeassigns=1&quot;
         else:
           removelinkbase = &quot;?showassigns=1&amp;removeassigns=1&amp;removefilter=%s&quot; % basename
-        stats[&quot;assigns&quot;] = self.getassigndetails(projectstats, linkbase, removelinkbase)
+        stats[&quot;assigns&quot;] = self.getassigndetails(pofilenames, linkbase, removelinkbase)
     return stats
 
   def gettrackdetails(self, projecttracks, linkbase):
     &quot;&quot;&quot;return a list of strings describing the results of tracks&quot;&quot;&quot;
     return [trackmessage for trackmessage in projecttracks]
 
-  def getcheckdetails(self, projectstats, linkbase):
+  def getcheckdetails(self, pofilenames, linkbase):
     &quot;&quot;&quot;return a list of strings describing the results of checks&quot;&quot;&quot;
+    projectstats = self.project.combine_unit_stats(pofilenames)
     total = max(len(projectstats.get(&quot;total&quot;, [])), 1)
     checklinks = []
     keys = projectstats.keys()
@@ -969,31 +971,28 @@ class ProjectIndex(pagelayout.PootleNavPage):
       checkname = checkname.replace(&quot;check-&quot;, &quot;&quot;, 1)
       if total and checkcount:
         stats = self.nlocalize(&quot;%d string (%d%%) failed&quot;, &quot;%d strings (%d%%) failed&quot;, checkcount, checkcount, (checkcount * 100 / total))
-        checklink = {&quot;href&quot;: self.makelink(linkbase, **{checkname:1}), &quot;text&quot;: checkname, &quot;stats&quot;: stats}
+        checklink = {&quot;href&quot;: self.makelink(linkbase, **{str(checkname):1}), &quot;text&quot;: checkname, &quot;stats&quot;: stats}
         checklinks += [checklink]
     return checklinks
 
-  def getassigndetails(self, projectstats, linkbase, removelinkbase):
+  def getassigndetails(self, pofilenames, linkbase, removelinkbase):
     &quot;&quot;&quot;return a list of strings describing the assigned strings&quot;&quot;&quot;
     # TODO: allow setting of action, so goals can only show the appropriate action assigns
-    total = projectstats.get(&quot;total&quot;, [])
     # quick lookup of what has been translated
-    translated = dict.fromkeys(projectstats.get(&quot;translated&quot;, []))
-    totalcount = len(total)
-    totalwords = self.project.countwords(total)
+    projectstats = self.project.combinestats(pofilenames)
+    totalcount = projectstats.get(&quot;total&quot;, 0)
+    totalwords = projectstats.get(&quot;totalsourcewords&quot;, 0)
+    translated = projectstats['units'].get(&quot;translated&quot;, [])
     assignlinks = []
-    keys = projectstats.keys()
+    keys = projectstats['assign'].keys()
     keys.sort()
     for assignname in keys:
-      if not assignname.startswith(&quot;assign-&quot;):
-        continue
-      assigned = projectstats[assignname]
+      assigned = projectstats['assign'][assignname]
       assigncount = len(assigned)
       assignwords = self.project.countwords(assigned)
       complete = [statsitem for statsitem in assigned if statsitem in translated]
       completecount = len(complete)
       completewords = self.project.countwords(complete)
-      assignname = assignname.replace(&quot;assign-&quot;, &quot;&quot;, 1)
       if totalcount and assigncount:
         assignlink = {&quot;href&quot;: self.makelink(linkbase, assignedto=assignname), &quot;text&quot;: assignname}
         percentassigned = assignwords * 100 / max(totalwords, 1)</diff>
      <filename>indexpage.py</filename>
    </modified>
    <modified>
      <diff>@@ -42,6 +42,7 @@ from translate.misc import optrecurse
 from Pootle import __version__ as pootleversion
 from translate import __version__ as toolkitversion
 from jToolkit import __version__ as jtoolkitversion
+from Pootle import statistics
 try:
   from xml.etree import ElementTree
 except ImportError:
@@ -541,6 +542,8 @@ class PootleOptionParser(simplewebserver.WebOptionParser):
     self.add_option('', &quot;--refreshstats&quot;, dest=&quot;action&quot;, action=&quot;store_const&quot;, const=&quot;refreshstats&quot;,
         default=&quot;runwebserver&quot;, help=&quot;refresh the stats files instead of running the webserver&quot;)
     psycomodes=[&quot;none&quot;, &quot;full&quot;, &quot;profile&quot;]
+    self.add_option('', &quot;--statsdb_file&quot;, action=&quot;store&quot;, type=&quot;string&quot;, dest=&quot;statsdb_file&quot;,
+                    default=None, help=&quot;Specifies the location of the SQLite stats db file.&quot;)
     try:
       import psyco
       self.add_option('', &quot;--psyco&quot;, dest=&quot;psyco&quot;, default=None, choices=psycomodes, metavar=&quot;MODE&quot;,
@@ -581,6 +584,7 @@ def main():
   options, args = parser.parse_args()
   options.errorlevel = options.logerrors
   usepsyco(options)
+  statistics.STATS_DB_FILE = options.statsdb_file
   if options.action != &quot;runwebserver&quot;:
     options.servertype = &quot;dummy&quot;
   server = parser.getserver(options)</diff>
      <filename>pootle.py</filename>
    </modified>
    <modified>
      <diff>@@ -26,6 +26,7 @@ from translate.storage import po
 from translate.storage.poheader import tzstring
 from translate.storage import xliff
 from translate.storage import factory
+from translate.filters import checks
 from translate.misc.multistring import multistring
 from Pootle import __version__
 from Pootle import statistics
@@ -34,28 +35,7 @@ from jToolkit import glock
 import time
 import os
 
-class Wrapper(object):
-  &quot;&quot;&quot;An object which wraps an inner object, delegating to the encapsulated methods, etc&quot;&quot;&quot;
-  def __getattr__(self, attrname, *args):
-    if attrname in self.__dict__:
-      return self.__dict__[attrname]
-    return getattr(self.__dict__[&quot;__innerobj__&quot;], attrname, *args)
-
-  def __setattr__(self, attrname, value):
-    if attrname == &quot;__innerobj__&quot;:
-      self.__dict__[attrname] = value
-    elif attrname in self.__dict__:
-      if isinstance(self.__dict__[attrname], property):
-        self.__dict__[attrname].fset(value)
-      else:
-        self.__dict__[attrname] = value
-    elif attrname in self.__class__.__dict__:
-      if isinstance(self.__class__.__dict__[attrname], property):
-        self.__class__.__dict__[attrname].fset(self, value)
-      else:
-        self.__dict__[attrname] = value
-    else:
-      return setattr(self.__dict__[&quot;__innerobj__&quot;], attrname, value)
+_UNIT_CHECKER = checks.UnitChecker()
 
 class LockedFile:
   &quot;&quot;&quot;locked interaction with a filesystem file&quot;&quot;&quot;
@@ -123,7 +103,7 @@ class pootleassigns:
     assignsstring = assignsfile.read()
     assignsfile.close()
     poassigns = {}
-    itemcount = len(getattr(self, &quot;classify&quot;, {}).get(&quot;total&quot;, []))
+    itemcount = len(getattr(self, &quot;stats&quot;, {}).get(&quot;total&quot;, []))
     for line in assignsstring.split(&quot;\n&quot;):
       if not line.strip():
         continue
@@ -250,17 +230,16 @@ class pootleassigns:
             assignitems.extend(actionitems)
     return assignitems
 
-class pootlefile(Wrapper):
+class pootlebase(object):
+  pass
+
+class pootlefile(pootlebase):
   &quot;&quot;&quot;this represents a pootle-managed file and its associated files&quot;&quot;&quot;
-  innerclass = po.pofile
   x_generator = &quot;Pootle %s&quot; % __version__.ver
-  def __init__(self, project=None, pofilename=None, generatestats=True):
+  def __init__(self, project=None, pofilename=None):
     if pofilename:
-      innerclass = factory.getclass(pofilename)
-    innerobj = innerclass()
-    self.__innerobj__ = innerobj
-    self.UnitClass = innerobj.UnitClass
-    
+      self.__class__.__bases__ = (factory.getclass(pofilename),)
+    super(pootlefile, self).__init__()
     self.pofilename = pofilename
     if project is None:
       from Pootle import projects
@@ -275,19 +254,16 @@ class pootlefile(Wrapper):
     self.lockedfile = LockedFile(self.filename)
     # we delay parsing until it is required
     self.pomtime = None
-    self.assigns = pootleassigns(self)
+    self.assigns = None
 
     self.pendingfilename = self.filename + os.extsep + &quot;pending&quot;
     self.pendingfile = None
-    self.statistics = statistics.pootlestatistics(self, generatestats)
+    self.statistics = statistics.pootlestatistics(self)
     self.tmfilename = self.filename + os.extsep + &quot;tm&quot;
     # we delay parsing until it is required
     self.pomtime = None
     self.tracker = timecache.timecache(20*60)
 
-  def __str__(self):
-    return self.__innerobj__.__str__()
-
   def parsestring(cls, storestring):
     newstore = cls()
     newstore.parse(storestring)
@@ -308,39 +284,34 @@ class pootlefile(Wrapper):
   def getheaderplural(self):
     &quot;&quot;&quot;returns values for nplural and plural values.  It tries to see if the 
     file has it specified (in a po header or similar).&quot;&quot;&quot;
-    method = getattr(self.__innerobj__, &quot;getheaderplural&quot;, None)
-    if method and callable(method):
-      return self.__innerobj__.getheaderplural()
-    else:
+    try:
+      return super(pootlefile, self).getheaderplural()
+    except AttributeError:
       return None, None
 
   def updateheaderplural(self, *args, **kwargs):
     &quot;&quot;&quot;updates the file header. If there is an updateheader function in the 
     underlying store it will be delegated there.&quot;&quot;&quot;
-    method = getattr(self.__innerobj__, &quot;updateheaderplural&quot;, None)
-    if method and callable(method):
-      self.__innerobj__.updateheaderplural(*args, **kwargs)
+    try:
+      super(pootlefile, self).updateheaderplural(*args, **kwargs)
+    except AttributeError:
+      pass
 
   def updateheader(self, **kwargs):
     &quot;&quot;&quot;updates the file header. If there is an updateheader function in the 
     underlying store it will be delegated there.&quot;&quot;&quot;
-    method = getattr(self.__innerobj__, &quot;updateheader&quot;, None)
-    if method and callable(method):
-      self.__innerobj__.updateheader(**kwargs)
+    try:
+      super(pootlefile, self).updateheader(**kwargs)
+    except AttributeError:
+      pass
 
   def readpendingfile(self):
     &quot;&quot;&quot;reads and parses the pending file corresponding to this file&quot;&quot;&quot;
     if os.path.exists(self.pendingfilename):
-      pendingmtime = statistics.getmodtime(self.pendingfilename)
-      if pendingmtime == getattr(self, &quot;pendingmtime&quot;, None):
-        return
       inputfile = open(self.pendingfilename, &quot;r&quot;)
-      self.pendingmtime, self.pendingfile = pendingmtime, factory.getobject(inputfile, ignore=&quot;.pending&quot;)
-      if self.pomtime:
-        self.reclassifysuggestions()
+      self.pendingfile = factory.getobject(inputfile, ignore=&quot;.pending&quot;)
     else:
       self.pendingfile = po.pofile()
-      self.savependingfile()
 
   def savependingfile(self):
     &quot;&quot;&quot;saves changes to disk...&quot;&quot;&quot;
@@ -348,7 +319,6 @@ class pootlefile(Wrapper):
     outputfile = open(self.pendingfilename, &quot;w&quot;)
     outputfile.write(output)
     outputfile.close()
-    self.pendingmtime = statistics.getmodtime(self.pendingfilename)
 
   def readtmfile(self):
     &quot;&quot;&quot;reads and parses the tm file corresponding to this file&quot;&quot;&quot;
@@ -361,28 +331,9 @@ class pootlefile(Wrapper):
     else:
       self.tmfile = po.pofile()
 
-  def reclassifysuggestions(self):
-    &quot;&quot;&quot;shortcut to only update classification of has-suggestion for all items&quot;&quot;&quot;
-    suggitems = []
-    sugglocations = {}
-    for thesugg in self.pendingfile.units:
-      locations = tuple(thesugg.getlocations())
-      sugglocations[locations] = thesugg
-    suggitems = [item for item in self.transunits if tuple(item.getlocations()) in sugglocations]
-    havesuggestions = self.statistics.classify[&quot;has-suggestion&quot;]
-    for item, poel in enumerate(self.transunits):
-      if (poel in suggitems) != (item in havesuggestions):
-        if poel in suggitems:
-          havesuggestions.append(item)
-        else:
-          havesuggestions.remove(item)
-        havesuggestions.sort()
-    self.statistics.calcstats()
-    self.statistics.savestats()
-
   def getsuggestions(self, item):
     &quot;&quot;&quot;find all the suggestion items submitted for the given item&quot;&quot;&quot;
-    unit = self.transunits[item]
+    unit = self.getitem(item)
     if isinstance(unit, xliff.xliffunit):
       return unit.getalttrans()
 
@@ -394,7 +345,7 @@ class pootlefile(Wrapper):
 
   def addsuggestion(self, item, suggtarget, username):
     &quot;&quot;&quot;adds a new suggestion for the given item&quot;&quot;&quot;
-    unit = self.transunits[item]
+    unit = self.getitem(item)
     if isinstance(unit, xliff.xliffunit):
       if isinstance(suggtarget, list) and (len(suggtarget) &gt; 0):
         suggtarget = suggtarget[0]
@@ -415,7 +366,7 @@ class pootlefile(Wrapper):
 
   def deletesuggestion(self, item, suggitem):
     &quot;&quot;&quot;removes the suggestion from the pending file&quot;&quot;&quot;
-    unit = self.transunits[item]
+    unit = self.getitem(item)
     if hasattr(unit, &quot;xmlelement&quot;):
       suggestions = self.getsuggestions(item)
       unit.delalttrans(suggestions[suggitem])
@@ -445,7 +396,7 @@ class pootlefile(Wrapper):
   def gettmsuggestions(self, item):
     &quot;&quot;&quot;find all the tmsuggestion items submitted for the given item&quot;&quot;&quot;
     self.readtmfile()
-    unit = self.transunits[item]
+    unit = self.getitem(item)
     locations = unit.getlocations()
     # TODO: review the matching method
     # Can't simply use the location index, because we want multiple matches
@@ -464,9 +415,6 @@ class pootlefile(Wrapper):
     pomtime, filecontents = self.lockedfile.getcontents()
     # note: we rely on this not resetting the filename, which we set earlier, when given a string
     self.parse(filecontents)
-    # we ignore all the headers by using this filtered set
-    self.transunits = [poel for poel in self.units if not (poel.isheader() or poel.isblank())]
-    self.statistics.classifyunits()
     self.pomtime = pomtime
 
   def savepofile(self):
@@ -476,11 +424,17 @@ class pootlefile(Wrapper):
 
   def pofreshen(self):
     &quot;&quot;&quot;makes sure we have a freshly parsed pofile&quot;&quot;&quot;
-    if not os.path.exists(self.filename):
-      # the file has been removed, update the project index (and fail below)
-      self.project.scanpofiles()
-    if self.pomtime != self.lockedfile.readmodtime():
-      self.readpofile()
+    try:
+        if self.pomtime != self.lockedfile.readmodtime():
+          self.readpofile()
+    except OSError, e:
+        # If this exception is not triggered by a bad
+        # symlink, then we have a missing file on our hands...
+        if not os.path.islink(self.filename):
+            # ...and thus we rescan our files to get rid of the missing filename
+            self.project.scanpofiles()
+        else:
+            print &quot;%s is a broken symlink&quot; % (self.filename,)
 
   def getoutput(self):
     &quot;&quot;&quot;returns pofile output&quot;&quot;&quot;
@@ -490,7 +444,7 @@ class pootlefile(Wrapper):
   def updateunit(self, item, newvalues, userprefs, languageprefs):
     &quot;&quot;&quot;updates a translation with a new target value&quot;&quot;&quot;
     self.pofreshen()
-    unit = self.transunits[item]
+    unit = self.getitem(item)
 
     if newvalues.has_key(&quot;target&quot;):
       unit.target = newvalues[&quot;target&quot;]
@@ -506,6 +460,12 @@ class pootlefile(Wrapper):
     if userprefs:
       if getattr(userprefs, &quot;name&quot;, None) and getattr(userprefs, &quot;email&quot;, None):
         headerupdates[&quot;Last_Translator&quot;] = &quot;%s &lt;%s&gt;&quot; % (userprefs.name, userprefs.email)
+    # XXX: If we needed to add a header, the index value in item will be one out after
+    # adding the header.
+    # TODO: remove once we force the PO class to always output headers
+    force_recache = False 
+    if not self.header():
+      force_recache = True
     self.updateheader(add=True, **headerupdates)
     if languageprefs:
       nplurals = getattr(languageprefs, &quot;nplurals&quot;, None)
@@ -513,27 +473,33 @@ class pootlefile(Wrapper):
       if nplurals and pluralequation:
         self.updateheaderplural(nplurals, pluralequation)
     self.savepofile()
+    if force_recache:
+      self.statistics.purge_totals()
     self.statistics.reclassifyunit(item)
 
+  def getitem(self, item):
+    &quot;&quot;&quot;Returns a single unit based on the item number.&quot;&quot;&quot;
+    return self.units[self.statistics.getstats(_UNIT_CHECKER)[&quot;total&quot;][item]]
+
   def iteritems(self, search, lastitem=None):
     &quot;&quot;&quot;iterates through the items in this pofile starting after the given lastitem, using the given search&quot;&quot;&quot;
     # update stats if required
-    self.statistics.getstats()
+    translatables = self.statistics.getstats()[&quot;total&quot;]
     if lastitem is None:
       minitem = 0
     else:
       minitem = lastitem + 1
-    maxitem = len(self.transunits)
+    maxitem = len(translatables)
     validitems = range(minitem, maxitem)
     if search.assignedto or search.assignedaction:
-      assignitems = self.assigns.finditems(search)
+      assignitems = self.getassigns().finditems(search)
       validitems = [item for item in validitems if item in assignitems]
     # loop through, filtering on matchnames if required
     for item in validitems:
       if not search.matchnames:
         yield item
       for name in search.matchnames:
-        if item in self.statistics.classify[name]:
+        if translatables[item] in self.statistics.getstats()[name]:
           yield item
 
   def matchitems(self, newfile, uselocations=False):
@@ -576,15 +542,19 @@ class pootlefile(Wrapper):
         matches.append((oldpo, None))
     return matches
 
+  def getassigns(self):
+    if self.assigns is None:
+        self.assigns = pootleassigns(self)
+    return self.assigns
+
   def mergeitem(self, oldpo, newpo, username, suggest=False):
     &quot;&quot;&quot;merges any changes from newpo into oldpo&quot;&quot;&quot;
     unchanged = oldpo.target == newpo.target
-
     if not suggest and (not oldpo.target or not newpo.target or oldpo.isheader() or newpo.isheader() or unchanged):
       oldpo.merge(newpo)
-    elif not unchanged:
-      #XXX: this is very inefficient!
-      for item, matchpo in enumerate(self.transunits):
+    else:
+      for item in self.statistics.getstats()[&quot;total&quot;]:
+        matchpo = self.units[item]
         if matchpo == oldpo:
           strings = getattr(newpo.target, &quot;strings&quot;, [newpo.target])
           self.addsuggestion(item, strings, username)</diff>
      <filename>pootlefile.py</filename>
    </modified>
    <modified>
      <diff>@@ -480,22 +480,21 @@ class POTree:
 
   def getpofiles(self, languagecode, projectcode, poext=&quot;po&quot;):
     &quot;&quot;&quot;returns a list of po files for the project and language&quot;&quot;&quot;
+    pofilenames = []
+    prefix = os.curdir + os.sep
 
     def addfiles(podir, dirname, fnames):
       &quot;&quot;&quot;adds the files to the set of files for this project&quot;&quot;&quot;
-      basedirname = dirname.replace(podir, &quot;&quot;, 1)
-      while basedirname.startswith(os.sep):
-        basedirname = basedirname.replace(os.sep, &quot;&quot;, 1)
-      ponames = []
+      if dirname == os.curdir:
+        basedirname = &quot;&quot;
+      else:
+        basedirname = dirname.replace(prefix, &quot;&quot;, 1)
       for fname in fnames:
-        #check that it actually exists (to avoid problems with broken symbolic 
+        # check that it actually exists (to avoid problems with broken symbolic 
         # links, for example)
-        if not os.path.exists(os.path.join(dirname, fname)):
-          print &quot;file does not exist:&quot;, os.path.join(dirname, fname)
-          continue
+        fpath = os.path.join(basedirname, fname)
         if fname.endswith(os.extsep+poext):
-          ponames.append(fname)
-      pofilenames.extend([os.path.join(basedirname, poname) for poname in ponames])
+          pofilenames.append(fpath)
 
     def addgnufiles(podir, dirname, fnames):
       &quot;&quot;&quot;adds the files to the set of files for this project&quot;&quot;&quot;
@@ -506,12 +505,14 @@ class POTree:
       ponames = [fn for fn in fnames if fn.endswith(ext) and self.languagematch(languagecode, fn[:-len(ext)])]
       pofilenames.extend([os.path.join(basedirname, poname) for poname in ponames])
 
-    pofilenames = []
     podir = self.getpodir(languagecode, projectcode)
     if self.hasgnufiles(podir, languagecode) == &quot;gnu&quot;:
       os.path.walk(podir, addgnufiles, podir)
     else:
-      os.path.walk(podir, addfiles, podir)
+      pwd = os.path.abspath(os.curdir)
+      os.chdir(podir)
+      os.path.walk(os.curdir, addfiles, None)
+      os.chdir(pwd)
     return pofilenames
 
   def getdefaultrights(self):</diff>
      <filename>potree.py</filename>
    </modified>
    <modified>
      <diff>@@ -33,6 +33,7 @@ from translate.tools import pocompile
 from translate.tools import pogrep
 from translate.search import match
 from translate.search import indexer
+from translate.storage import statsdb, base
 from Pootle import statistics
 from Pootle import pootlefile
 from translate.storage import versioncontrol
@@ -455,13 +456,14 @@ class TranslationProject(object):
   def scanpofiles(self):
     &quot;&quot;&quot;sets the list of pofilenames by scanning the project directory&quot;&quot;&quot;
     self.pofilenames = self.potree.getpofiles(self.languagecode, self.projectcode, poext=self.fileext)
-    for pofilename in self.pofilenames:
-      if not pofilename in self.pofiles:
-        self.pofiles[pofilename] = pootlefile.pootlefile(self, pofilename)
+    filename_set = set(self.pofilenames)
+    pootlefile_set = set(self.pofiles.keys())
+    # add any files that we don't have yet
+    for filename in filename_set.difference(pootlefile_set):
+        self.pofiles[filename] = pootlefile.pootlefile(self, filename)
     # remove any files that have been deleted since initialization
-    for pofilename in self.pofiles.keys():
-      if not pofilename in self.pofilenames:
-        del self.pofiles[pofilename]
+    for filename in pootlefile_set.difference(filename_set):
+        del self.pofiles[filename]
 
   def getuploadpath(self, dirname, localfilename):
     &quot;&quot;&quot;gets the path of a translation file being uploaded securely, creating directories as neccessary&quot;&quot;&quot;
@@ -823,58 +825,60 @@ class TranslationProject(object):
     index = self.getindexer()
     pofile = self.pofiles[pofilename]
     # check if the pomtime in the index == the latest pomtime
-    pomtime = statistics.getmodtime(pofile.filename)
-    pofilenamequery = index.make_query([(&quot;pofilename&quot;, pofilename)], True)
-    pomtimequery = index.make_query([(&quot;pomtime&quot;, str(pomtime))], True)
-    gooditemsquery = index.make_query([pofilenamequery, pomtimequery], True)
-    gooditemsnum = index.get_query_result(gooditemsquery).get_matches_count()
-    # if there is at least one up-to-date indexing item, then the po file
-    # was not changed externally -&gt; no need to update the database
-    if (gooditemsnum &gt; 0) and (not items):
-      # nothing to be done
-      return
-    elif items:
-      # Update only specific items - usually single translation via the web
-      # interface. All other items should still be up-to-date (even with an 
-      # older pomtime).
-      print &quot;updating&quot;, self.languagecode, &quot;index for&quot;, pofilename, &quot;items&quot;, items
-      # delete the relevant items from the database
-      itemsquery = index.make_query([(&quot;itemno&quot;, str(itemno)) for itemno in items], False)
-      index.delete_doc([pofilenamequery, itemsquery])
-    else:
-      # (items is None)
-      # The po file is not indexed - or it was changed externally (see
-      # &quot;pofreshen&quot; in pootlefile.py).
-      print &quot;updating&quot;, self.projectcode, self.languagecode, &quot;index for&quot;, pofilename
-      # delete all items of this file
-      index.delete_doc({&quot;pofilename&quot;: pofilename})
-    pofile.pofreshen()
-    if items is None:
-      # rebuild the whole index
-      items = range(len(pofile.transunits))
-    addlist = []
-    for itemno in items:
-      unit = pofile.transunits[itemno]
-      doc = {&quot;pofilename&quot;: pofilename, &quot;pomtime&quot;: str(pomtime), &quot;itemno&quot;: str(itemno)}
-      if unit.hasplural():
-          sources = unit.source.strings
-          targets = unit.target.strings
-      else:
-          sources = unit.source
-          targets = unit.target
-      orig = &quot;\n&quot;.join(sources)
-      trans = &quot;\n&quot;.join(targets)
-      doc[&quot;msgid&quot;] = orig
-      doc[&quot;msgstr&quot;] = trans
-      addlist.append(doc)
-    if addlist:
-      index.begin_transaction()
-      try:
-        for add_item in addlist:
-            index.index_document(add_item)
-      finally:
-        index.commit_transaction()
-        index.flush(optimize=optimize)
+    try:
+        pomtime = statistics.getmodtime(pofile.filename)
+        pofilenamequery = index.make_query([(&quot;pofilename&quot;, pofilename)], True)
+        pomtimequery = index.make_query([(&quot;pomtime&quot;, str(pomtime))], True)
+        gooditemsquery = index.make_query([pofilenamequery, pomtimequery], True)
+        gooditemsnum = index.get_query_result(gooditemsquery).get_matches_count()
+        # if there is at least one up-to-date indexing item, then the po file
+        # was not changed externally -&gt; no need to update the database
+        if (gooditemsnum &gt; 0) and (not items):
+          # nothing to be done
+          return
+        elif items:
+          # Update only specific items - usually single translation via the web
+          # interface. All other items should still be up-to-date (even with an 
+          # older pomtime).
+          print &quot;updating&quot;, self.languagecode, &quot;index for&quot;, pofilename, &quot;items&quot;, items
+          # delete the relevant items from the database
+          itemsquery = index.make_query([(&quot;itemno&quot;, str(itemno)) for itemno in items], False)
+          index.delete_doc([pofilenamequery, itemsquery])
+        else:
+          # (items is None)
+          # The po file is not indexed - or it was changed externally (see
+          # &quot;pofreshen&quot; in pootlefile.py).
+          print &quot;updating&quot;, self.projectcode, self.languagecode, &quot;index for&quot;, pofilename
+          # delete all items of this file
+          index.delete_doc({&quot;pofilename&quot;: pofilename})
+        pofile.pofreshen()
+        if items is None:
+          # rebuild the whole index
+          items = range(pofile.statistics.getitemslen())
+        addlist = []
+        for itemno in items:
+          unit = pofile.getitem(itemno)
+          doc = {&quot;pofilename&quot;: pofilename, &quot;pomtime&quot;: str(pomtime), &quot;itemno&quot;: str(itemno)}
+          if unit.hasplural():
+              orig = &quot;\n&quot;.join(unit.source.strings)
+              trans = &quot;\n&quot;.join(unit.target.strings)
+          else:
+              orig = unit.source
+              trans = unit.target
+          doc[&quot;msgid&quot;] = orig
+          doc[&quot;msgstr&quot;] = trans
+          addlist.append(doc)
+        if addlist:
+          index.begin_transaction()
+          try:
+            for add_item in addlist:
+                index.index_document(add_item)
+          finally:
+            index.commit_transaction()
+            index.flush(optimize=optimize)
+    except (base.ParseError, IOError, OSError):
+        index.delete_doc({&quot;pofilename&quot;: pofilename})
+        print &quot;Not indexing %s, since it is corrupt&quot; % (pofilename,)
 
   def matchessearch(self, pofilename, search):
     &quot;&quot;&quot;returns whether any items in the pofilename match the search (based on collected stats etc)&quot;&quot;&quot;
@@ -884,9 +888,9 @@ class TranslationProject(object):
     # search.assignedto == [None] means assigned to nobody
     if search.assignedto or search.assignedaction:
       if search.assignedto == [None]:
-        assigns = self.pofiles[pofilename].assigns.getunassigned(search.assignedaction)
+        assigns = self.pofiles[pofilename].getassigns().getunassigned(search.assignedaction)
       else:
-        assigns = self.pofiles[pofilename].assigns.getassigns()
+        assigns = self.pofiles[pofilename].getassigns().getassigns()
         if search.assignedto is not None:
           if search.assignedto not in assigns:
             return False
@@ -900,7 +904,7 @@ class TranslationProject(object):
       postats = self.getpostats(pofilename)
       matches = False
       for name in search.matchnames:
-        if postats[name]:
+        if postats.get(name):
           matches = True
       if not matches:
         return False
@@ -970,7 +974,7 @@ class TranslationProject(object):
             continue
         # TODO: move this to iteritems
         if search.searchtext:
-          unit = pofile.transunits[item]
+          unit = pofile.getitem(item)
           if grepfilter.filterunit(unit):
             yield pofilename, item
         else:
@@ -996,9 +1000,8 @@ class TranslationProject(object):
     assigncount = 0
     if not usercount:
       return assigncount
-    docountwords = lambda pofilename: self.countwords([(pofilename, item) for item in range(self.pofiles[pofilename].statistics.getitemslen())])
     pofilenames = [pofilename for pofilename in self.searchpofilenames(None, search, includelast=True)]
-    wordcounts = [(pofilename, docountwords(pofilename)) for pofilename in pofilenames]
+    wordcounts = [(pofilename, self.getpofile(pofilename).statistics.getquickstats()['totalsourcewords']) for pofilename in pofilenames]
     totalwordcount = sum([wordcount for pofilename, wordcount in wordcounts])
 
     wordsperuser = totalwordcount / usercount
@@ -1007,21 +1010,23 @@ class TranslationProject(object):
     userwords = 0
     for pofilename, wordcount in wordcounts:
       pofile = self.getpofile(pofilename)
+      sourcewordcount = pofile.statistics.getunitstats()['sourcewordcount']
       for item in pofile.iteritems(search, None):
         # TODO: move this to iteritems
         if search.searchtext:
           validitem = False
-          unit = pofile.transunits[item]
+          unit = pofile.getitem(item)
           if grepfilter.filterunit(unit):
             validitem = True
           if not validitem:
             continue
-        itemwordcount = self.countwords([(pofilename, item)])
+        itemwordcount = sourcewordcount[item]
+        #itemwordcount = statsdb.wordcount(str(pofile.getitem(item).source))
         if userwords + itemwordcount &gt; wordsperuser:
           usernum = min(usernum+1, len(assignto)-1)
           userwords = 0
         userwords += itemwordcount
-        pofile.assigns.assignto(item, assignto[usernum], action)
+        pofile.getassigns().assignto(item, assignto[usernum], action)
         assigncount += 1
     return assigncount
 
@@ -1037,12 +1042,12 @@ class TranslationProject(object):
       for item in pofile.iteritems(search, None):
         # TODO: move this to iteritems
         if search.searchtext:
-          unit = pofile.transunits[item]
+          unit = pofile.getitem(item)
           if grepfilter.filterunit(unit):
-            pofile.assigns.unassign(item, assignedto, action)
+            pofile.getassigns().unassign(item, assignedto, action)
             assigncount += 1
         else:
-          pofile.assigns.unassign(item, assignedto, action)
+          pofile.getassigns().unassign(item, assignedto, action)
           assigncount += 1
     return assigncount
 
@@ -1100,7 +1105,6 @@ class TranslationProject(object):
       alltotal += total
     for pofilename in slowfiles:
       self.pofiles[pofilename].statistics.updatequickstats(save=False)
-      self.savequickstats()
       translatedwords, translated, fuzzywords, fuzzy, totalwords, total = self.quickstats[pofilename]
       alltranslatedwords += translatedwords
       alltranslated += translated
@@ -1108,43 +1112,56 @@ class TranslationProject(object):
       allfuzzy += fuzzy
       alltotalwords += totalwords
       alltotal += total
+    if slowfiles:
+      self.savequickstats()
     return {&quot;translatedsourcewords&quot;: alltranslatedwords, &quot;translated&quot;: alltranslated, 
             &quot;fuzzysourcewords&quot;: allfuzzywords, &quot;fuzzy&quot;: allfuzzy, 
             &quot;totalsourcewords&quot;: alltotalwords, &quot;total&quot;: alltotal}
 
   def combinestats(self, pofilenames=None):
     &quot;&quot;&quot;combines translation statistics for the given po files (or all if None given)&quot;&quot;&quot;
-    totalstats = {}
     if pofilenames is None:
       pofilenames = self.pofilenames
+    pofilenames = [pofilename for pofilename in pofilenames 
+                   if pofilename != None and not os.path.isdir(pofilename)]
+    total_stats = self.combine_totals(pofilenames)
+    total_stats['units'] = self.combine_unit_stats(pofilenames)
+    total_stats['assign'] = self.combineassignstats(pofilenames)
+    return total_stats
+
+  def combine_totals(self, pofilenames):
+    totalstats = {}
+    for pofilename in pofilenames:
+      pototals = self.getpototals(pofilename)
+      for name, items in pototals.iteritems():
+        totalstats[name] = totalstats.get(name, 0) + pototals[name]
+    return totalstats
+
+  def combine_unit_stats(self, pofilenames):
+    unit_stats = {}
     for pofilename in pofilenames:
-      if not pofilename or os.path.isdir(pofilename):
-        continue
       postats = self.getpostats(pofilename)
       for name, items in postats.iteritems():
-        totalstats[name] = totalstats.get(name, []) + [(pofilename, item) for item in items]
-    assignstats = self.combineassignstats(pofilenames)
-    totalstats.update(assignstats)
-    return totalstats
+        unit_stats.setdefault(name, []).extend([(pofilename, item) for item in items])
+    return unit_stats
 
   def combineassignstats(self, pofilenames=None, action=None):
     &quot;&quot;&quot;combines assign statistics for the given po files (or all if None given)&quot;&quot;&quot;
-    totalstats = {}
-    if pofilenames is None:
-      pofilenames = self.pofilenames
+    assign_stats = {}
     for pofilename in pofilenames:
       assignstats = self.getassignstats(pofilename, action)
       for name, items in assignstats.iteritems():
-        totalstats[&quot;assign-&quot;+name] = totalstats.get(&quot;assign-&quot;+name, []) + [(pofilename, item) for item in items]
-    return totalstats
+        assign_stats.setdefault(name, []).extend([(pofilename, item) for item in items])
+    return assign_stats
 
   def countwords(self, stats):
     &quot;&quot;&quot;counts the number of words in the items represented by the stats list&quot;&quot;&quot;
     wordcount = 0
     for pofilename, item in stats:
       pofile = self.pofiles[pofilename]
-      if 0 &lt;= item &lt; len(pofile.statistics.sourcewordcounts):
-        wordcount += sum(pofile.statistics.sourcewordcounts[item])
+      if 0 &lt;= item &lt; len(pofile.statistics.getunitstats()['sourcewordcount']):
+        wordcount += pofile.statistics.getunitstats()['sourcewordcount'][item]
+    print &quot;projects::countwords()&quot;
     return wordcount
 
   def getpomtime(self):
@@ -1175,10 +1192,16 @@ class TranslationProject(object):
     &quot;&quot;&quot;calculates translation statistics for the given po file&quot;&quot;&quot;
     return self.pofiles[pofilename].statistics.getstats()
 
+  def getpototals(self, pofilename):
+    &quot;&quot;&quot;calculates translation statistics for the given po file&quot;&quot;&quot;
+    return self.pofiles[pofilename].statistics.getquickstats()
+
   def getassignstats(self, pofilename, action=None):
     &quot;&quot;&quot;calculates translation statistics for the given po file (can filter by action if given)&quot;&quot;&quot;
-    polen = len(self.getpostats(pofilename)[&quot;total&quot;])
-    assigns = self.pofiles[pofilename].assigns.getassigns()
+    polen = self.getpototals(pofilename).get(&quot;total&quot;, 0)
+    # Temporary code to avoid traceback. Was:
+#    polen = len(self.getpostats(pofilename)[&quot;total&quot;])
+    assigns = self.pofiles[pofilename].getassigns().getassigns()
     assignstats = {}
     for username, userassigns in assigns.iteritems():
       allitems = []
@@ -1198,14 +1221,13 @@ class TranslationProject(object):
 
   def getpofilelen(self, pofilename):
     &quot;&quot;&quot;returns number of items in the given pofilename&quot;&quot;&quot;
-    # TODO: needn't parse the file for this ...
     pofile = self.getpofile(pofilename)
-    return len(pofile.transunits)
+    return pofile.statistics.getitemslen()
 
   def getitems(self, pofilename, itemstart, itemstop):
     &quot;&quot;&quot;returns a set of items from the pofile, converted to original and translation strings&quot;&quot;&quot;
     pofile = self.getpofile(pofilename)
-    units = pofile.transunits[max(itemstart,0):itemstop]
+    units = [pofile.units[index] for index in pofile.statistics.getstats()[&quot;total&quot;][max(itemstart,0):itemstop]]
     return units
 
   def updatetranslation(self, pofilename, item, newvalues, session):
@@ -1318,7 +1340,7 @@ class TranslationProject(object):
       if isinstance(pofile, (str, unicode)):
         pofilename = pofile
         pofile = self.getpofile(pofilename)
-        return termmatcher.matches(pofile.transunits[item].source)
+        return termmatcher.matches(pofile.getitem(item).source)
     except Exception, e:
       session.server.errorhandler.logerror(traceback.format_exc())
       return []</diff>
      <filename>projects.py</filename>
    </modified>
    <modified>
      <diff>@@ -3,235 +3,75 @@ from translate.storage import statsdb
 from translate.filters import checks
 from translate.misc.multistring import multistring
 
-def getmodtime(filename, default=None):
-  &quot;&quot;&quot;gets the modificationtime of the given file&quot;&quot;&quot;
-  if os.path.exists(filename):
-    return os.stat(filename)[os.path.stat.ST_MTIME]
-  else:
-    return default
+STATS_DB_FILE = None
 
-class StatsFile:
-  &quot;&quot;&quot;Manages a statistics data file&quot;&quot;&quot;
-  def __init__(self, basefile):
-    self.basefile = basefile
-    self.refname = self.basefile.filename
-    self.filename = self.refname + os.extsep + &quot;stats&quot;
-
-  def read(self):
-    &quot;&quot;&quot;return the contents of the stats file&quot;&quot;&quot;
-    sfile =  open(self.filename, &quot;r&quot;)
-    contents = sfile.read()
-    sfile.close()
-    return contents
-
-  def save(self, statsstring):
-    &quot;&quot;&quot;save the stats data&quot;&quot;&quot;
-    sfile = open(self.filename, &quot;w&quot;)
-    if os.path.exists(self.basefile.pendingfilename):
-      sfile.write(&quot;%d %d\n&quot; % (getmodtime(self.basefile.filename), getmodtime(self.basefile.pendingfilename)))
-    else:
-      sfile.write(&quot;%d\n&quot; % getmodtime(self.basefile.filename))
-    sfile.write(statsstring)
-    sfile.close()
-
-  def hasparent(self):
-    &quot;&quot;&quot;check if the stats file has a parent data file, if not delete it&quot;&quot;&quot;
-    if os.path.exists(self.basefile.filename):
-      return True
-    else:
-      if os.path.exists(self.filename):
-        os.remove(self.filename)
-      return False
+def getmodtime(filename):
+    return statsdb.get_mod_info(filename, errors_return_empty=True, empty_return=None)
 
 class pootlestatistics:
   &quot;&quot;&quot;this represents the statistics known about a file&quot;&quot;&quot;
-  def __init__(self, basefile, generatestats=True):
+  def __init__(self, basefile):
     &quot;&quot;&quot;constructs statistic object for the given file&quot;&quot;&quot;
     # TODO: try and remove circular references between basefile and this class
     self.basefile = basefile
-    self.sfile = StatsFile(self.basefile)
-    self.classify = {}
-    self.sourcewordcounts = []
-    self.targetwordcounts = []
-    if generatestats:
-      self.getstats()
-
-  def getstats(self):
+    self._stats = None
+    self._totals = None
+    self._unitstats = None
+    self.statscache = statsdb.StatsCache(STATS_DB_FILE)
+
+  def getquickstats(self):
+    &quot;&quot;&quot;returns the quick statistics (totals only)&quot;&quot;&quot;
+    if not self._totals:
+      self._totals = self.statscache.filetotals(self.basefile.filename, errors_return_empty=True)
+    return self._totals
+
+  def getstats(self, checker=None):
     &quot;&quot;&quot;reads the stats if neccessary or returns them from the cache&quot;&quot;&quot;
-    if os.path.exists(self.sfile.filename):
-      try:
-        self.readstats()
-      except Exception, e:
-        print &quot;Error reading stats from %s, so recreating (Error was %s)&quot; % (self.sfile.filename, e)
-        self.statspomtime = None
-        raise
-    pomtime = getmodtime(self.basefile.filename)
-    pendingmtime = getmodtime(self.basefile.pendingfilename, None)
-    if hasattr(self, &quot;pendingmtime&quot;):
-      self.basefile.readpendingfile()
-    lastpomtime = getattr(self, &quot;statspomtime&quot;, None)
-    lastpendingmtime = getattr(self, &quot;statspendingmtime&quot;, None)
-    if pomtime is None or pomtime != lastpomtime or pendingmtime != lastpendingmtime:
-      self.calcstats()
-      self.savestats()
-    return self.stats
-
-  def readstats(self):
-    &quot;&quot;&quot;reads the stats from the associated stats file, setting the required variables&quot;&quot;&quot;
-    statsmtime = getmodtime(self.sfile.filename)
-    if statsmtime == getattr(self, &quot;statsmtime&quot;, None):
-      return
-    stats = self.sfile.read()
-    mtimes, statsstring = stats.split(&quot;\n&quot;, 1)
-    mtimes = mtimes.strip().split()
-    if len(mtimes) == 1:
-      frompomtime = int(mtimes[0])
-      frompendingmtime = None
-    elif len(mtimes) == 2:
-      frompomtime = int(mtimes[0])
-      frompendingmtime = int(mtimes[1])
-    stats = {}
-    for line in statsstring.split(&quot;\n&quot;):
-      if not line.strip():
-        continue
-      if not &quot;:&quot; in line:
-        print &quot;invalid stats line in&quot;, self.sfile.filename,line
-        continue
-      name, items = line.split(&quot;:&quot;, 1)
-      if name == &quot;msgidwordcounts&quot; or name == &quot;sourcewordcounts&quot;:
-        stats[&quot;sourcewordcounts&quot;] = [[int(subitem.strip()) for subitem in item.strip().split(&quot;/&quot;)] for item in items.strip().split(&quot;,&quot;) if item]
-      elif name == &quot;msgstrwordcounts&quot; or name == &quot;targetwordcounts&quot;:
-        stats[&quot;targetwordcounts&quot;] = [[int(subitem.strip()) for subitem in item.strip().split(&quot;/&quot;)] for item in items.strip().split(&quot;,&quot;) if item]
-      else:
-        items = [int(item.strip()) for item in items.strip().split(&quot;,&quot;) if item]
-        stats[name.strip()] = items
-    # save all the read times, data simultaneously
-    self.statspomtime, self.statspendingmtime, self.statsmtime, self.stats, self.sourcewordcounts, self.targetwordcounts = frompomtime, frompendingmtime, statsmtime, stats, stats[&quot;sourcewordcounts&quot;], stats[&quot;targetwordcounts&quot;]
-    # if in old-style format (counts instead of items), recalculate
-    totalitems = stats.get(&quot;total&quot;, [])
-    if len(totalitems) == 1 and totalitems[0] != 0:
-      self.calcstats()
-      self.savestats()
-    if (len(stats[&quot;sourcewordcounts&quot;]) &lt; len(totalitems)) or (len(stats[&quot;targetwordcounts&quot;]) &lt; len(totalitems)):
-      self.basefile.pofreshen()
-      self.countwords()
-      self.savestats()
-
-  def savestats(self):
-    &quot;&quot;&quot;saves the current statistics to file&quot;&quot;&quot;
-    if not self.sfile.hasparent():
-      return
-    # assumes self.stats is up to date
-    try:
-      statsstring = &quot;\n&quot;.join([&quot;%s:%s&quot; % (name, &quot;,&quot;.join(map(str,items))) for name, items in self.stats.iteritems()])
-      wordcountsstring = &quot;sourcewordcounts:&quot; + &quot;,&quot;.join([&quot;/&quot;.join(map(str,subitems)) for subitems in self.sourcewordcounts])
-      wordcountsstring += &quot;\ntargetwordcounts:&quot; + &quot;,&quot;.join([&quot;/&quot;.join(map(str,subitems)) for subitems in self.targetwordcounts])
-      self.sfile.save(statsstring + &quot;\n&quot; + wordcountsstring)
-    except IOError:
-      # TODO: log a warning somewhere. we don't want an error as this is an optimization
-      pass
-    self.updatequickstats()
+    if checker == None:
+        checker = self.basefile.checker
+    if not self._stats:
+      self._stats = self.statscache.filestats(self.basefile.filename, checker, errors_return_empty=True)
+    return self._stats
+  
+  def purge_totals(self):
+    &quot;&quot;&quot;Temporary helper to clean up where needed. We might be able to remove 
+    this after the move to cpo.&quot;&quot;&quot;
+    self._totals = None
+    self._stats = None
+    self._unitstats = None
+
+  def getunitstats(self):
+    if not self._unitstats:
+      self._unitstats = self.statscache.unitstats(self.basefile.filename, errors_return_empty=True)
+    return self._unitstats
 
   def updatequickstats(self, save=True):
     &quot;&quot;&quot;updates the project's quick stats on this file&quot;&quot;&quot;
-    translated = self.stats.get(&quot;translated&quot;)
-    fuzzy = self.stats.get(&quot;fuzzy&quot;)
-    translatedwords = sum([sum(self.sourcewordcounts[item]) for item in translated if 0 &lt;= item &lt; len(self.sourcewordcounts)])
-    fuzzywords = sum([sum(self.sourcewordcounts[item]) for item in fuzzy if 0 &lt;= item &lt; len(self.sourcewordcounts)])
-    totalwords = sum([sum(partcounts) for partcounts in self.sourcewordcounts])
+    totals = self.getquickstats()
     self.basefile.project.updatequickstats(self.basefile.pofilename, 
-        translatedwords, len(translated), 
-        fuzzywords, len(fuzzy), 
-        totalwords, len(self.sourcewordcounts),
+        totals.get(&quot;translatedsourcewords&quot;, 0), totals.get(&quot;translated&quot;, 0),
+        totals.get(&quot;fuzzysourcewords&quot;, 0), totals.get(&quot;fuzzy&quot;, 0),
+        totals.get(&quot;totalsourcewords&quot;, 0), totals.get(&quot;total&quot;, 0),
         save)
 
-  def calcstats(self):
-    &quot;&quot;&quot;calculates translation statistics for the given file&quot;&quot;&quot;
-    # handle this being called when self.basefile.statistics is being set and calcstats is called from self.__init__
-    if not hasattr(self.basefile, &quot;statistics&quot;):
-      self.basefile.statistics = self
-    self.basefile.pofreshen()
-    self.stats = dict([(name, items) for name, items in self.classify.iteritems()])
-
-  def classifyunit(self, unit):
-    &quot;&quot;&quot;returns all classify keys that the unit should match&quot;&quot;&quot;
-    classes = [&quot;total&quot;]
-    if unit.isfuzzy():
-      classes.append(&quot;fuzzy&quot;)
-    if unit.gettargetlen() == 0:
-      classes.append(&quot;blank&quot;)
-    if unit.istranslated():
-      classes.append(&quot;translated&quot;)
-    checkresult = self.basefile.checker.run_filters(unit)
-    for checkname, checkmessage in checkresult.iteritems():
-      classes.append(&quot;check-&quot; + checkname)
-    return classes
-
-  def classifyunits(self):
-    &quot;&quot;&quot;makes a dictionary of which units fall into which classifications&quot;&quot;&quot;
-    self.classify = {}
-    self.classify[&quot;fuzzy&quot;] = []
-    self.classify[&quot;blank&quot;] = []
-    self.classify[&quot;translated&quot;] = []
-    self.classify[&quot;has-suggestion&quot;] = []
-    self.classify[&quot;total&quot;] = []
-    for checkname in self.basefile.checker.getfilters().keys():
-      self.classify[&quot;check-&quot; + checkname] = []
-    for item, unit in enumerate(self.basefile.transunits):
-      classes = self.classifyunit(unit)
-      if self.basefile.getsuggestions(item):
-        classes.append(&quot;has-suggestion&quot;)
-      for classname in classes:
-        if classname in self.classify:
-          self.classify[classname].append(item)
-        else:
-          self.classify[classname] = item
-    self.countwords()
-
-  def countwords(self):
-    &quot;&quot;&quot;counts the words in each of the units&quot;&quot;&quot;
-    self.sourcewordcounts = []
-    self.targetwordcounts = []
-    for unit in self.basefile.transunits:
-      if isinstance(unit.source, multistring):
-        sourcestrings = unit.source.strings
-      else:
-        sourcestrings = [unit.source]
-      if isinstance(unit.target, multistring):
-        targetstrings = unit.target.strings
-      else:
-        targetstrings = [unit.target]
-          
-      self.sourcewordcounts.append([statsdb.wordcount(text) for text in sourcestrings])
-      self.targetwordcounts.append([statsdb.wordcount(text) for text in targetstrings])
-
   def reclassifyunit(self, item):
-    &quot;&quot;&quot;updates the classification of a unit in self.classify&quot;&quot;&quot;
-    unit = self.basefile.transunits[item]
-    self.sourcewordcounts[item] = [statsdb.wordcount(text) for text in unit.source.strings]
-    self.targetwordcounts[item] = [statsdb.wordcount(text) for text in unit.target.strings]
-    classes = self.classifyunit(unit)
-    if self.basefile.getsuggestions(item):
-      classes.append(&quot;has-suggestion&quot;)
-    for classname, matchingitems in self.classify.items():
+    &quot;&quot;&quot;Reclassifies all the information in the database and self._stats about 
+    the given unit&quot;&quot;&quot;
+    unit = self.basefile.getitem(item)
+    item = self.getstats()[&quot;total&quot;][item]
+    
+    classes = self.statscache.recacheunit(self.basefile.filename, self.basefile.checker, unit)
+    for classname, matchingitems in self.getstats().items():
       if (classname in classes) != (item in matchingitems):
         if classname in classes:
-          self.classify[classname].append(item)
+          self.getstats()[classname].append(item)
         else:
-          self.classify[classname].remove(item)
-        self.classify[classname].sort()
-    self.calcstats()
-    self.savestats()
+          self.getstats()[classname].remove(item)
+        self.getstats()[classname].sort()
+    # We should be doing better, but for now it is easiet to simply force a 
+    # reload from the database
+    self.purge_totals()
 
   def getitemslen(self):
     &quot;&quot;&quot;gets the number of items in the file&quot;&quot;&quot;
-    # TODO: simplify this, and use wherever its needed
-    if hasattr(self.basefile, &quot;transunits&quot;):
-      return len(self.basefile.transunits)
-    elif hasattr(self, &quot;stats&quot;) and &quot;total&quot; in self.stats:
-      return len(self.stats[&quot;total&quot;])
-    elif hasattr(self, &quot;classify&quot;) and &quot;total&quot; in self.classify:
-      return len(self.classify[&quot;total&quot;])
-    else:
-      # we hadn't read stats...
-      return len(self.getstats()[&quot;total&quot;])
+    return self.getquickstats()[&quot;total&quot;]</diff>
      <filename>statistics.py</filename>
    </modified>
    <modified>
      <diff>@@ -44,7 +44,7 @@
     &lt;/div&gt;
   &lt;/div&gt;
   &lt;div id=&quot;itemstats&quot; class=&quot;item-statistics&quot;&gt;
-    &lt;span py:replace=&quot;XML(item.stats.summary)&quot;&gt;
+    &lt;span py:if=&quot;item.stats.summary&quot; py:replace=&quot;XML(item.stats.summary)&quot;&gt;
       2/2 words (100%) translated &lt;span class=&quot;string-statistics&quot;&gt;[2/2 strings]&lt;/span&gt;
     &lt;/span&gt;
     &lt;span py:for=&quot;check in item.stats.checks&quot; py:strip=&quot;True&quot;&gt;</diff>
      <filename>templates/navbar.html</filename>
    </modified>
    <modified>
      <diff>@@ -87,23 +87,6 @@ msgstr &quot;&quot;'''
         assert unit.source == &quot;test&quot;
         assert unit.target == &quot;rest&quot;
 
-    def test_classify(self):
-        &quot;&quot;&quot;Test basic classification&quot;&quot;&quot;
-        posource = 'msgid &quot;test&quot;\nmsgstr &quot;&quot;\n'
-        pofile = self.poparse(posource)
-        pofile.project.checker = checks.TeeChecker()
-        unit = pofile.units[0]
-        classify = pofile.statistics.classifyunit
-        classes = classify(unit)
-        assert 'blank' in classes
-        unit.target = &quot;Gom&quot;
-        classes = classify(unit)
-        assert 'translated' in classes
-        assert 'blank' not in classes
-        unit.markfuzzy()
-        classes = classify(unit)
-        assert 'fuzzy' in classes
-
     def test_classifyunits(self):
         &quot;Tests basic use of classifyunits.&quot;
         posource = r'''#: test.c
@@ -117,15 +100,11 @@ msgstr &quot;tafel&quot;
 msgid &quot;chair&quot;
 msgstr &quot;&quot;'''
         pofile = self.poparse(posource)
-        pofile.transunits = [poel for poel in pofile.units if not (poel.isheader() or poel.isblank())]
-        pofile.statistics.classifyunits()
-        classify = pofile.statistics.classify
-        print classify
-        for i in pofile.units:
-            print str(i)
-        assert classify['fuzzy'] == [1]
-        assert classify['blank'] == [2]
-        assert len(classify['total']) == 3
+        pofile.savepofile()
+        assert pofile.statistics.getstats()['fuzzy'] == [1]
+        assert pofile.statistics.getstats()['untranslated'] == [2]
+        assert pofile.statistics.getstats()['translated'] == [0]
+        assert pofile.statistics.getstats()['total'] == [0, 1, 2]
 
     def test_updateunit(self):
         &quot;&quot;&quot;Test the updateunit() method.&quot;&quot;&quot;</diff>
      <filename>test_pootlefile.py</filename>
    </modified>
    <modified>
      <diff>@@ -98,9 +98,9 @@ class TranslatePage(pagelayout.PootleNavPage):
       icon=&quot;edit&quot;
     if self.pofilename is not None:
       postats = self.project.getpostats(self.pofilename)
-      blank, fuzzy = postats[&quot;blank&quot;], postats[&quot;fuzzy&quot;]
+      untranslated, fuzzy = postats[&quot;untranslated&quot;], postats[&quot;fuzzy&quot;]
       translated, total = postats[&quot;translated&quot;], postats[&quot;total&quot;]
-      mainstats = self.localize(&quot;%d/%d translated\n(%d blank, %d fuzzy)&quot;, len(translated), len(total), len(blank), len(fuzzy))
+      mainstats = self.localize(&quot;%d/%d translated\n(%d untranslated, %d fuzzy)&quot;, len(translated), len(total), len(untranslated), len(fuzzy))
       pagelinks = self.getpagelinks(&quot;?translate=1&amp;view=1&quot;, rows)
     navbarpath_dict = self.makenavbarpath_dict(self.project, self.session, self.pofilename, dirfilter=self.dirfilter or &quot;&quot;)
     # templatising
@@ -354,7 +354,7 @@ class TranslatePage(pagelayout.PootleNavPage):
     &quot;&quot;&quot;returns any checker filters the user has asked to match...&quot;&quot;&quot;
     matchnames = []
     for checkname in self.argdict:
-      if checkname in [&quot;fuzzy&quot;, &quot;blank&quot;, &quot;translated&quot;, &quot;has-suggestion&quot;]:
+      if checkname in [&quot;fuzzy&quot;, &quot;untranslated&quot;, &quot;translated&quot;]:
         matchnames.append(checkname)
       elif checkname in checker.getfilters():
         matchnames.append(&quot;check-&quot; + checkname)</diff>
      <filename>translatepage.py</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>test_statistics.py</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>8f96ef12462bbb5bcf3b490bb7db5be4fd9b2ece</id>
    </parent>
  </parents>
  <author>
    <name>winterstream</name>
    <email>winterstream@54714841-351b-0410-a198-e36a94b762f5</email>
  </author>
  <url>http://github.com/julen/pootle/commit/49ca02f88f35f99eade40c044f4d6c98f264b63d</url>
  <id>49ca02f88f35f99eade40c044f4d6c98f264b63d</id>
  <committed-date>2008-07-01T04:23:36-07:00</committed-date>
  <authored-date>2008-07-01T04:23:36-07:00</authored-date>
  <message>Merged in changes from branches/Pootle-diet-and-trunk-merge.

Please see the branch if you are interested in the changes that
led to this commit.


git-svn-id: https://translate.svn.sourceforge.net/svnroot/translate/src/trunk/Pootle@7686 54714841-351b-0410-a198-e36a94b762f5</message>
  <tree>33d5ed133e1d790ed8b735f1a21e137c8711acf7</tree>
  <committer>
    <name>winterstream</name>
    <email>winterstream@54714841-351b-0410-a198-e36a94b762f5</email>
  </committer>
</commit>
