[damned-lies] Figure stats computed during update and stored in pofile.figures



commit 8d61b672b23c4cd262d1c904226d1b49b740bf14
Author: Claude Paroz <claude 2xlibre net>
Date:   Thu Aug 4 13:14:50 2011 +0200

    Figure stats computed during update and stored in pofile.figures
    
    This is also adding support for detecting images in itstool-generated
    po files.

 common/fields.py                                   |   38 ++++
 docs/DataModel.odg                                 |  Bin 27476 -> 31369 bytes
 stats/fixtures/sample_data.json                    |  178 ++++++++---------
 stats/migrations/0009_add_figure_field.py          |  214 ++++++++++++++++++++
 stats/models.py                                    |   71 ++++---
 stats/tests/__init__.py                            |   20 +-
 stats/tests/fixture_factory.py                     |    6 +-
 .../help_docbook/C/figures/rnusers.nautilus.png    |  Bin 0 -> 844 bytes
 stats/tests/help_docbook/C/rnusers.xml             |    6 +
 stats/utils.py                                     |   85 ++++----
 vertimus/views.py                                  |   11 +-
 11 files changed, 439 insertions(+), 190 deletions(-)
---
diff --git a/common/fields.py b/common/fields.py
index 817472f..4fb0ce8 100644
--- a/common/fields.py
+++ b/common/fields.py
@@ -3,6 +3,7 @@
 
 from django import forms
 from django.core import exceptions
+from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.utils import simplejson
 
@@ -51,9 +52,46 @@ class DictionaryField(models.Field):
         defaults.update(kwargs)
         return super(DictionaryField, self).formfield(**defaults)
 
+
+# From http://djangosnippets.org/snippets/1478/
+
+class JSONField(models.TextField):
+    """JSONField is a generic textfield that neatly serializes/unserializes
+    JSON objects seamlessly"""
+
+    # Used so to_python() is called
+    __metaclass__ = models.SubfieldBase
+
+    def to_python(self, value):
+        """Convert our string value to JSON after we load it from the DB"""
+
+        if value == "":
+            return None
+
+        try:
+            if isinstance(value, basestring):
+                return simplejson.loads(value)
+        except ValueError:
+            pass
+
+        return value
+
+    def get_db_prep_save(self, value, connection):
+        """Convert our JSON object to a string before we save"""
+
+        if value == "":
+            return None
+
+        if isinstance(value, (dict, list)):
+            value = simplejson.dumps(value, cls=DjangoJSONEncoder)
+
+        return super(JSONField, self).get_db_prep_save(value, connection=connection)
+
+
 # rules for South migrations tool (for version >= 0.7)
 try:
     from south.modelsinspector import add_introspection_rules
     add_introspection_rules([], ["^common\.fields\.DictionaryField"])
+    add_introspection_rules([], ["^common\.fields\.JSONField"])
 except ImportError:
     pass
diff --git a/docs/DataModel.odg b/docs/DataModel.odg
index 33e730f..c96e3d2 100644
Binary files a/docs/DataModel.odg and b/docs/DataModel.odg differ
diff --git a/stats/fixtures/sample_data.json b/stats/fixtures/sample_data.json
index 8e85b00..c6c73e2 100644
--- a/stats/fixtures/sample_data.json
+++ b/stats/fixtures/sample_data.json
@@ -9,12 +9,12 @@
    "is_active": true, 
    "is_superuser": false, 
    "is_staff": false, 
-   "last_login": "2011-02-17 22:15:33", 
+   "last_login": "2011-08-04 12:03:53", 
    "groups": [], 
    "user_permissions": [], 
    "password": "!", 
    "email": "bob example org", 
-   "date_joined": "2011-02-17 22:15:33"
+   "date_joined": "2011-08-04 12:03:53"
   }
  }, 
  {
@@ -27,12 +27,12 @@
    "is_active": true, 
    "is_superuser": false, 
    "is_staff": false, 
-   "last_login": "2011-02-17 22:15:33", 
+   "last_login": "2011-08-04 12:03:53", 
    "groups": [], 
    "user_permissions": [], 
    "password": "!", 
    "email": "coord example org", 
-   "date_joined": "2011-02-17 22:15:33"
+   "date_joined": "2011-08-04 12:03:53"
   }
  }, 
  {
@@ -45,12 +45,12 @@
    "is_active": true, 
    "is_superuser": false, 
    "is_staff": false, 
-   "last_login": "2011-02-17 22:15:33", 
+   "last_login": "2011-08-04 12:03:53", 
    "groups": [], 
    "user_permissions": [], 
    "password": "!", 
    "email": "alessio example org", 
-   "date_joined": "2011-02-17 22:15:33"
+   "date_joined": "2011-08-04 12:03:53"
   }
  }, 
  {
@@ -190,6 +190,7 @@
    "name": "gnome-hello", 
    "vcs_root": "git://git.gnome.org/gnome-hello", 
    "bugs_product": "gnome-hello", 
+   "ext_platform": null, 
    "maintainers": [], 
    "bugs_component": "test", 
    "bugs_base": "http://bugzilla.gnome.org";, 
@@ -207,6 +208,7 @@
    "name": "shared-mime-info", 
    "vcs_root": "git://anongit.freedesktop.org/xdg/shared-mime-info", 
    "bugs_product": "shared-mime-info", 
+   "ext_platform": null, 
    "maintainers": [], 
    "bugs_component": "general", 
    "bugs_base": "https://bugs.freedesktop.org/";, 
@@ -224,6 +226,7 @@
    "name": "zenity", 
    "vcs_root": "git://git.gnome.org/zenity", 
    "bugs_product": "zenity", 
+   "ext_platform": null, 
    "maintainers": [], 
    "bugs_component": "general", 
    "bugs_base": "http://bugzilla.gnome.org";, 
@@ -284,8 +287,8 @@
    "dtype": "ui", 
    "pot_method": null, 
    "module": 1, 
-   "linguas_location": null,
-   "red_filter": null,
+   "linguas_location": null, 
+   "red_filter": null, 
    "directory": "po", 
    "description": "UI Translations"
   }
@@ -299,7 +302,7 @@
    "pot_method": null, 
    "module": 2, 
    "linguas_location": null, 
-   "red_filter": null,
+   "red_filter": null, 
    "directory": "po", 
    "description": "UI Translations"
   }
@@ -313,7 +316,7 @@
    "pot_method": null, 
    "module": 3, 
    "linguas_location": null, 
-   "red_filter": null,
+   "red_filter": null, 
    "directory": "po", 
    "description": "UI Translations"
   }
@@ -327,7 +330,7 @@
    "pot_method": null, 
    "module": 1, 
    "linguas_location": null, 
-   "red_filter": null,
+   "red_filter": null, 
    "directory": "help", 
    "description": "User Guide"
   }
@@ -341,7 +344,7 @@
    "pot_method": null, 
    "module": 2, 
    "linguas_location": null, 
-   "red_filter": null,
+   "red_filter": null, 
    "directory": "help", 
    "description": "User Guide"
   }
@@ -355,7 +358,7 @@
    "pot_method": null, 
    "module": 3, 
    "linguas_location": null, 
-   "red_filter": null,
+   "red_filter": null, 
    "directory": "help", 
    "description": "User Guide"
   }
@@ -433,9 +436,9 @@
   "pk": 1, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 47, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -445,9 +448,9 @@
   "pk": 2, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 47, 
    "path": null
@@ -457,9 +460,9 @@
   "pk": 3, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 7, 
+   "figures": null, 
    "fuzzy": 10, 
    "translated": 30, 
    "path": null
@@ -469,9 +472,9 @@
   "pk": 4, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 1, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 20, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -481,9 +484,9 @@
   "pk": 5, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 20, 
    "path": null
@@ -493,9 +496,9 @@
   "pk": 6, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 20, 
    "path": null
@@ -505,9 +508,9 @@
   "pk": 7, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 136, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -517,9 +520,9 @@
   "pk": 8, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 136, 
    "path": null
@@ -529,9 +532,9 @@
   "pk": 9, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 6, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 130, 
    "path": null
@@ -541,9 +544,9 @@
   "pk": 10, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 11, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 259, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -553,9 +556,9 @@
   "pk": 11, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 259, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -565,9 +568,9 @@
   "pk": 12, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 259, 
    "path": null
@@ -577,9 +580,9 @@
   "pk": 13, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 149, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -589,9 +592,9 @@
   "pk": 14, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 4, 
    "translated": 255, 
    "path": null
@@ -601,9 +604,9 @@
   "pk": 15, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 259, 
    "path": null
@@ -613,9 +616,9 @@
   "pk": 16, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 11, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 259, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -625,9 +628,9 @@
   "pk": 17, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 259, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -637,9 +640,9 @@
   "pk": 18, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 259, 
    "path": null
@@ -649,9 +652,9 @@
   "pk": 19, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 626, 
+   "figures": null, 
    "fuzzy": 0, 
    "translated": 0, 
    "path": null
@@ -661,9 +664,9 @@
   "pk": 20, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 2, 
+   "figures": null, 
    "fuzzy": 20, 
    "translated": 598, 
    "path": null
@@ -673,9 +676,9 @@
   "pk": 21, 
   "model": "stats.pofile", 
   "fields": {
-   "updated": "2011-03-23 13:47:59", 
-   "num_figures": 0, 
+   "updated": "2011-08-04 12:03:53", 
    "untranslated": 0, 
+   "figures": null, 
    "fuzzy": 6, 
    "translated": 620, 
    "path": null
@@ -687,10 +690,9 @@
   "fields": {
    "domain": 1, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 1, 
    "language": null, 
    "part_po": null, 
@@ -703,10 +705,9 @@
   "fields": {
    "domain": 1, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 2, 
    "language": 2, 
    "part_po": null, 
@@ -719,10 +720,9 @@
   "fields": {
    "domain": 1, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 3, 
    "language": 3, 
    "part_po": null, 
@@ -735,10 +735,9 @@
   "fields": {
    "domain": 2, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 4, 
    "language": null, 
    "part_po": null, 
@@ -751,10 +750,9 @@
   "fields": {
    "domain": 2, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 5, 
    "language": 2, 
    "part_po": null, 
@@ -767,10 +765,9 @@
   "fields": {
    "domain": 2, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 6, 
    "language": 3, 
    "part_po": null, 
@@ -783,10 +780,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 7, 
    "language": null, 
    "part_po": null, 
@@ -799,10 +795,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 8, 
    "language": 2, 
    "part_po": null, 
@@ -815,10 +810,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 9, 
    "language": 3, 
    "part_po": null, 
@@ -831,10 +825,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 10, 
    "language": null, 
    "part_po": null, 
@@ -847,10 +840,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 11, 
    "language": 2, 
    "part_po": null, 
@@ -863,10 +855,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 12, 
    "language": 3, 
    "part_po": null, 
@@ -879,10 +870,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 13, 
    "language": null, 
    "part_po": null, 
@@ -895,10 +885,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 14, 
    "language": 2, 
    "part_po": null, 
@@ -911,10 +900,9 @@
   "fields": {
    "domain": 3, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 15, 
    "language": 3, 
    "part_po": null, 
@@ -927,10 +915,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 16, 
    "language": null, 
    "part_po": null, 
@@ -943,10 +930,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 17, 
    "language": 2, 
    "part_po": null, 
@@ -959,10 +945,9 @@
   "fields": {
    "domain": 4, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 18, 
    "language": 3, 
    "part_po": null, 
@@ -975,10 +960,9 @@
   "fields": {
    "domain": 5, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 19, 
    "language": null, 
    "part_po": null, 
@@ -991,10 +975,9 @@
   "fields": {
    "domain": 5, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 20, 
    "language": 2, 
    "part_po": null, 
@@ -1007,10 +990,9 @@
   "fields": {
    "domain": 5, 
    "old_translated": 0, 
-   "old_date": "2011-03-23 13:47:59", 
+   "old_date": "2011-08-04 12:03:53", 
    "old_fuzzy": 0, 
    "old_untranslated": 0, 
-   "old_num_figures": 0, 
    "full_po": 21, 
    "language": 3, 
    "part_po": null, 
@@ -1026,4 +1008,4 @@
    "description": "Error regenerating POT file for zenity:\n<pre>intltool-update -g 'zenity' -p\nERROR: xgettext failed to generate PO template file.</pre>"
   }
  }
-]
+]
\ No newline at end of file
diff --git a/stats/migrations/0009_add_figure_field.py b/stats/migrations/0009_add_figure_field.py
new file mode 100644
index 0000000..a860df7
--- /dev/null
+++ b/stats/migrations/0009_add_figure_field.py
@@ -0,0 +1,214 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Deleting field 'PoFile.num_figures'
+        db.delete_column('pofile', 'num_figures')
+
+        # Adding field 'PoFile.figures'
+        db.add_column('pofile', 'figures', self.gf('common.fields.JSONField')(null=True, blank=True), keep_default=False)
+
+        # Deleting field 'Statistics.old_num_figures'
+        db.delete_column('statistics', 'old_num_figures')
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'PoFile.num_figures'
+        db.add_column('pofile', 'num_figures', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False)
+
+        # Deleting field 'PoFile.figures'
+        db.delete_column('pofile', 'figures')
+
+        # Adding field 'Statistics.old_num_figures'
+        db.add_column('statistics', 'old_num_figures', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False)
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'languages.language': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Language', 'db_table': "'language'"},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'locale': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '15'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
+            'plurals': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['teams.Team']", 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "('username',)", 'object_name': 'Person', 'db_table': "'person'", '_ormbases': ['auth.User']},
+            'activation_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'bugzilla_account': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'irc_nick': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '20', 'null': 'True', 'blank': 'True'}),
+            'svn_account': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '20', 'null': 'True', 'blank': 'True'}),
+            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}),
+            'webpage_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'stats.branch': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'module'),)", 'object_name': 'Branch', 'db_table': "'branch'"},
+            'file_hashes': ('common.fields.DictionaryField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Module']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'vcs_subpath': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+            'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        'stats.category': {
+            'Meta': {'unique_together': "(('release', 'branch'),)", 'object_name': 'Category', 'db_table': "'category'"},
+            'branch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Branch']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '30'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Release']"})
+        },
+        'stats.domain': {
+            'Meta': {'ordering': "('-dtype', 'name')", 'object_name': 'Domain', 'db_table': "'domain'"},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'directory': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'dtype': ('django.db.models.fields.CharField', [], {'default': "'ui'", 'max_length': '5'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'linguas_location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+            'module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Module']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'pot_method': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+            'red_filter': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'stats.information': {
+            'Meta': {'object_name': 'Information', 'db_table': "'information'"},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'statistics': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Statistics']"}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+        },
+        'stats.informationarchived': {
+            'Meta': {'object_name': 'InformationArchived', 'db_table': "'information_archived'"},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'statistics': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.StatisticsArchived']"}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+        },
+        'stats.module': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Module', 'db_table': "'module'"},
+            'bugs_base': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+            'bugs_component': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+            'bugs_product': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+            'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'ext_platform': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'maintains_modules'", 'blank': 'True', 'db_table': "'module_maintainer'", 'to': "orm['people.Person']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'vcs_root': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'vcs_type': ('django.db.models.fields.CharField', [], {'max_length': '5'}),
+            'vcs_web': ('django.db.models.fields.URLField', [], {'max_length': '200'})
+        },
+        'stats.pofile': {
+            'Meta': {'object_name': 'PoFile', 'db_table': "'pofile'"},
+            'figures': ('common.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
+            'fuzzy': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'translated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'untranslated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'stats.release': {
+            'Meta': {'ordering': "('status', '-name')", 'object_name': 'Release', 'db_table': "'release'"},
+            'branches': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'releases'", 'symmetrical': 'False', 'through': "orm['stats.Category']", 'to': "orm['stats.Branch']"}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'max_length': '20', 'db_index': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'max_length': '12'}),
+            'string_frozen': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        'stats.statistics': {
+            'Meta': {'unique_together': "(('branch', 'domain', 'language'),)", 'object_name': 'Statistics', 'db_table': "'statistics'"},
+            'branch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Branch']"}),
+            'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Domain']"}),
+            'full_po': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'stat_f'", 'unique': 'True', 'null': 'True', 'to': "orm['stats.PoFile']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['languages.Language']", 'null': 'True'}),
+            'old_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'old_fuzzy': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'old_translated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'old_untranslated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'part_po': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'stat_p'", 'unique': 'True', 'null': 'True', 'to': "orm['stats.PoFile']"})
+        },
+        'stats.statisticsarchived': {
+            'Meta': {'object_name': 'StatisticsArchived', 'db_table': "'statistics_archived'"},
+            'branch': ('django.db.models.fields.TextField', [], {}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'domain': ('django.db.models.fields.TextField', [], {}),
+            'fuzzy': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
+            'module': ('django.db.models.fields.TextField', [], {}),
+            'translated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+            'untranslated': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        'teams.role': {
+            'Meta': {'unique_together': "(('team', 'person'),)", 'object_name': 'Role', 'db_table': "'role'"},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['people.Person']"}),
+            'role': ('django.db.models.fields.CharField', [], {'default': "'translator'", 'max_length': '15'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['teams.Team']"})
+        },
+        'teams.team': {
+            'Meta': {'ordering': "('description',)", 'object_name': 'Team', 'db_table': "'team'"},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mailing_list': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'mailing_list_subscribe': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'members': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'teams'", 'symmetrical': 'False', 'through': "orm['teams.Role']", 'to': "orm['people.Person']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'presentation': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'use_workflow': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'webpage_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['stats']
diff --git a/stats/models.py b/stats/models.py
index 7a43c01..e1a3e80 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2008-2010 Claude Paroz <claude 2xlibre net>.
+# Copyright (c) 2008-2011 Claude Paroz <claude 2xlibre net>.
 # Copyright (c) 2008 Stephane Raimbault <stephane raimbault gmail com>.
 #
 # This file is part of Damned Lies.
@@ -27,12 +27,13 @@ from datetime import datetime
 
 from django.conf import settings
 from django.core.exceptions import ValidationError
+from django.core.urlresolvers import reverse
 from django.utils.translation import ungettext, ugettext as _, ugettext_noop
 from django.utils import dateformat
 from django.utils.datastructures import SortedDict
 from django.db import models, connection
 
-from common.fields import DictionaryField
+from common.fields import DictionaryField, JSONField
 from common.utils import is_site_admin
 from stats import utils, signals
 from stats.doap import update_doap_infos
@@ -394,6 +395,7 @@ class Branch(models.Model):
 
                 # 3. Generate a fresh pot file
                 # ****************************
+                pot_method = dom.pot_method
                 if dom.dtype == 'ui':
                     potfile, errs = dom.generate_pot_file(self)
                 elif dom.dtype == 'doc':
@@ -401,7 +403,8 @@ class Branch(models.Model):
                         potfile, errs = dom.generate_pot_file(self)
                     else:
                         # Standard gnome-doc-utils pot generation
-                        potfile, errs = utils.generate_doc_pot_file(domain_path, dom.potbase(), self.module.name, settings.DEBUG)
+                        potfile, errs, pot_method = utils.generate_doc_pot_file(
+                            domain_path, dom.potbase(), self.module.name)
                 else:
                     print >> sys.stderr, "Unknown domain type '%s', ignoring domain '%s'" % (dom.dtype, dom.name)
                     continue
@@ -444,12 +447,17 @@ class Branch(models.Model):
 
                 # 6. Generate pot stats and update DB
                 # ***********************************
-                pot_stats = utils.po_file_stats(potfile, False)
+                pot_stats = utils.po_file_stats(potfile, msgfmt_checks=False)
+                fig_stats = utils.get_fig_stats(potfile, pot_method, trans_stats=False)
                 errors.extend(pot_stats['errors'])
                 if potfile != previous_pot and not utils.copy_file(potfile, previous_pot):
                     errors.append(('error', ugettext_noop("Can't copy new POT file to public location.")))
 
-                pot_stat.set_translation_stats(previous_pot, untranslated=int(pot_stats['untranslated']), num_figures=int(pot_stats['num_figures']))
+                pot_stat.set_translation_stats(
+                    previous_pot,
+                    untranslated=int(pot_stats['untranslated']),
+                    figstats = fig_stats,
+                )
                 pot_stat.set_errors(errors)
 
                 # Send pot_has_changed signal
@@ -475,11 +483,12 @@ class Branch(models.Model):
                         }
                     utils.run_shell_command(realcmd)
 
-                    langstats = utils.po_file_stats(outpo, msgfmt_checks=True, count_images=(dom.dtype == "doc"))
+                    langstats = utils.po_file_stats(outpo, msgfmt_checks=True)
                     if linguas['langs'] is not None and lang not in linguas['langs']:
                         langstats['errors'].append(("warn-ext", linguas['error']))
+                    fig_stats = None
                     if dom.dtype == "doc":
-                        fig_stats = utils.get_fig_stats(outpo)
+                        fig_stats = utils.get_fig_stats(outpo, pot_method)
                         for fig in fig_stats:
                             trans_path = os.path.join(domain_path, lang, fig['path'])
                             if os.access(trans_path, os.R_OK):
@@ -509,7 +518,7 @@ class Branch(models.Model):
                                                translated = int(langstats['translated']),
                                                fuzzy = int(langstats['fuzzy']),
                                                untranslated = int(langstats['untranslated']),
-                                               num_figures = int(langstats.get('num_figures', 0)))
+                                               figstats=fig_stats)
                     for err in langstats['errors']:
                         stat.information_set.add(Information(type=err[0], description=err[1]))
             # Check if doap file changed
@@ -1174,8 +1183,8 @@ class PoFile(models.Model):
     translated   = models.IntegerField(default=0)
     fuzzy        = models.IntegerField(default=0)
     untranslated = models.IntegerField(default=0)
-    # Number of figures in doc templates
-    num_figures  = models.IntegerField(default=0)
+    # List of figure dict
+    figures      = JSONField(blank=True, null=True)
 
     class Meta:
         db_table = 'pofile'
@@ -1188,7 +1197,7 @@ class PoFile(models.Model):
 
     def fig_count(self):
         """ If stat of a document type, get the number of figures in the document """
-        return self.num_figures
+        return len(self.figures)
 
     def tr_percentage(self):
         if self.pot_size() == 0:
@@ -1219,8 +1228,6 @@ class Statistics(models.Model):
     old_translated   = models.IntegerField(default=0) # obsolete
     old_fuzzy        = models.IntegerField(default=0) # obsolete
     old_untranslated = models.IntegerField(default=0) # obsolete
-    # Number of figures in doc templates
-    old_num_figures = models.IntegerField(default=0) # obsolete
 
     full_po = models.OneToOneField(PoFile, null=True, related_name='stat_f')
     part_po = models.OneToOneField(PoFile, null=True, related_name='stat_p')
@@ -1233,7 +1240,6 @@ class Statistics(models.Model):
 
     def __init__(self, *args, **kwargs):
         models.Model.__init__(self, *args, **kwargs)
-        self.figures = None
         self.modname = None
         self.moddescription = None
         self.partial_po = False # True if part of a multiple po module
@@ -1329,30 +1335,32 @@ class Statistics(models.Model):
         return text
 
     def get_figures(self):
-        """ self.figures is a list of dicts:
+        """ Return an enriched list of figure dicts (used in module_images.html):
             [{'path':, 'hash':, 'fuzzy':, 'translated':, 'translated_file':}, ...] """
-        if self.figures is None and self.domain.dtype == 'doc':
-            self.figures = utils.get_fig_stats(self.po_path())
+        figures = []
+        if self.full_po and self.domain.dtype == 'doc':
             # something like: "http://git.gnome.org/browse/vinagre / plain / help / %s / %s ?h=master"
             url_model = utils.url_join(self.branch.get_vcs_web_url(), self.branch.img_url_prefix,
                                        self.domain.directory, '%s', '%s') + self.branch.img_url_suffix
-            for fig in self.figures:
-                fig['orig_remote_url'] = url_model % ('C', fig['path'])
-                fig['translated_file'] = False
+            for fig in self.full_po.figures:
+                fig2 = fig.copy()
+                fig2['orig_remote_url'] = url_model % ('C', fig['path'])
+                fig2['translated_file'] = False
                 # Check if a translated figure really exists or if the English one is used
                 if (self.language and
                    os.path.exists(os.path.join(self.branch.co_path(), self.domain.directory, self.language.locale, fig['path']))):
-                    fig['trans_remote_url'] = url_model % (self.language.locale, fig['path'])
-                    fig['translated_file'] = True
-        return self.figures
+                    fig2['trans_remote_url'] = url_model % (self.language.locale, fig['path'])
+                    fig2['translated_file'] = True
+                figures.append(fig2)
+        return figures
 
     def fig_stats(self):
         stats = {'fuzzy':0, 'translated':0, 'total':0, 'prc':0}
-        for fig in self.get_figures():
+        for fig in self.full_po.figures:
             stats['total'] += 1
-            if fig['fuzzy']: stats['fuzzy'] += 1
+            if fig.get('fuzzy', 0): stats['fuzzy'] += 1
             else:
-                if fig['translated']: stats['translated'] += 1
+                if fig.get('translated', 0): stats['translated'] += 1
         stats['untranslated'] = stats['total'] - (stats['translated'] + stats['fuzzy'])
         if stats['total'] > 0:
             stats['prc'] = 100*stats['translated']/stats['total']
@@ -1386,7 +1394,7 @@ class Statistics(models.Model):
     def pot_url(self):
         return self.po_url(potfile=True)
 
-    def set_translation_stats(self, po_path, translated=0, fuzzy=0, untranslated=0, num_figures=0):
+    def set_translation_stats(self, po_path, translated=0, fuzzy=0, untranslated=0, figstats=None):
         if not self.full_po:
             self.full_po = PoFile.objects.create(path=po_path)
             self.save()
@@ -1394,7 +1402,7 @@ class Statistics(models.Model):
         self.full_po.translated = translated
         self.full_po.fuzzy = fuzzy
         self.full_po.untranslated = untranslated
-        self.full_po.num_figures = num_figures
+        self.full_po.figures = figstats
         self.full_po.updated = datetime.now()
         self.full_po.save()
         if self.domain.dtype == "ui":
@@ -1413,7 +1421,7 @@ class Statistics(models.Model):
                 else:
                     part_po_path = self.full_po.path[:-3] + ".reduced.po"
                 utils.po_grep(self.full_po.path, part_po_path, self.domain.red_filter)
-                part_stats = utils.po_file_stats(part_po_path, msgfmt_checks=False, count_images=False)
+                part_stats = utils.po_file_stats(part_po_path, msgfmt_checks=False)
                 if part_stats['translated'] + part_stats['fuzzy'] + part_stats['untranslated'] == translated + fuzzy + untranslated:
                     # No possible gain, set part_po = full_po so it is possible to compute complete stats at database level
                     part_po_equals_full_po()
@@ -1622,6 +1630,11 @@ class FakeLangStatistics(object):
             'lang_locale': self.language.locale
         }
 
+    def po_url(self, potfile=False, reduced=False):
+        return reverse(
+            'dynamic_po',
+            args=("%s.%s.%s.%s.po" % (self.branch.module.name, self.domain.name, self.branch.name, self.language.locale),)
+        )
 
 class FakeSummaryStatistics(object):
     """ Statistics class that sums up an entire module stats """
diff --git a/stats/tests/__init__.py b/stats/tests/__init__.py
index 92f6246..b4f97aa 100644
--- a/stats/tests/__init__.py
+++ b/stats/tests/__init__.py
@@ -160,12 +160,14 @@ class ModuleTestCase(TestCase):
         self.assertEquals(var_content.split(), ['rnusers.xml', 'rnlookingforward.xml', '$(NULL)'])
 
     def testGenerateDocPotfile(self):
-        from stats.utils import generate_doc_pot_file
+        from stats.utils import generate_doc_pot_file, get_fig_stats
         # Docbook-style help
         help_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "help_docbook")
-        generate_doc_pot_file(help_path, 'release-notes', 'release-notes', None)
+        generate_doc_pot_file(help_path, 'release-notes', 'release-notes')
         pot_path = os.path.join(help_path, "C", "release-notes.pot")
         self.assertTrue(os.access(pot_path, os.R_OK))
+        res = get_fig_stats(pot_path, image_method='xml2po')
+        self.assertEqual(len(res), 1)
         os.remove(pot_path)
         # TODO: Mallard-style help
 
@@ -173,23 +175,23 @@ class ModuleTestCase(TestCase):
         """ Detect warning if translated figure is identical to original figure """
         self.b.checkout()
         orig_figure = os.path.join(self.b.co_path(), "help", "C", "figures", "gnome-hello-new.png")
-        shutil.copy(orig_figure, os.path.join(self.b.co_path(), "help", "fr", "figures", "gnome-hello-new.png"))
+        shutil.copy(orig_figure, os.path.join(self.b.co_path(), "help", "cs", "figures", "gnome-hello-new.png"))
         self.b.update_stats(force=True, checkout=False)
-        doc_stat = Statistics.objects.get(branch=self.b, domain__name='help', language__locale='fr')
+        doc_stat = Statistics.objects.get(branch=self.b, domain__name='help', language__locale='cs')
         warn_infos = Information.objects.filter(statistics=doc_stat, type='warn-ext')
         self.assertEquals(len(warn_infos), 1);
-        ui_stat = Statistics.objects.get(branch=self.b, domain__name='po', language__locale='fr')
-        self.assertEquals(ui_stat.po_url(), u"/POT/gnome-hello.master/gnome-hello.master.fr.po");
+        ui_stat = Statistics.objects.get(branch=self.b, domain__name='po', language__locale='cs')
+        self.assertEquals(ui_stat.po_url(), u"/POT/gnome-hello.master/gnome-hello.master.cs.po");
         self.assertEquals(ui_stat.pot_url(), u"/POT/gnome-hello.master/gnome-hello.master.pot");
-        self.assertEquals(doc_stat.po_url(), u"/POT/gnome-hello.master/docs/gnome-hello-help.master.fr.po");
+        self.assertEquals(doc_stat.po_url(), u"/POT/gnome-hello.master/docs/gnome-hello-help.master.cs.po");
 
     def testFigureURLs(self):
         """ Test if figure urls are properly constructed """
         self.b.update_stats(force=True)
-        stat = Statistics.objects.get(branch=self.b, domain__dtype='doc', language__locale='fr')
+        stat = Statistics.objects.get(branch=self.b, domain__dtype='doc', language__locale='cs')
         figs = stat.get_figures()
         self.assertEquals(figs[0]['orig_remote_url'], 'http://git.gnome.org/browse/gnome-hello/plain/help/C/figures/gnome-hello-new.png?h=master')
-        self.assertEquals(figs[0]['trans_remote_url'], 'http://git.gnome.org/browse/gnome-hello/plain/help/fr/figures/gnome-hello-new.png?h=master')
+        self.assertEquals(figs[0]['trans_remote_url'], 'http://git.gnome.org/browse/gnome-hello/plain/help/cs/figures/gnome-hello-new.png?h=master')
 
     def testFigureView(self):
         self.b.update_stats(force=True)
diff --git a/stats/tests/fixture_factory.py b/stats/tests/fixture_factory.py
index de416f6..ca7c40c 100644
--- a/stats/tests/fixture_factory.py
+++ b/stats/tests/fixture_factory.py
@@ -109,20 +109,20 @@ class FixtureFactory(TestCase):
         Statistics.objects.create(branch=b1, domain=dom['gnome-hello-ui'], language=None, full_po=PoFile.objects.create(untranslated=47))
         Statistics.objects.create(branch=b1, domain=dom['gnome-hello-ui'], language=l_fr, full_po=PoFile.objects.create(translated=47))
         Statistics.objects.create(branch=b1, domain=dom['gnome-hello-ui'], language=l_it, full_po=PoFile.objects.create(translated=30, fuzzy=10, untranslated=7))
-        Statistics.objects.create(branch=b1, domain=dom['gnome-hello-doc'], language=None, full_po=PoFile.objects.create(untranslated=20, num_figures=1))
+        Statistics.objects.create(branch=b1, domain=dom['gnome-hello-doc'], language=None, full_po=PoFile.objects.create(untranslated=20))
         Statistics.objects.create(branch=b1, domain=dom['gnome-hello-doc'], language=l_fr, full_po=PoFile.objects.create(translated=20))
         Statistics.objects.create(branch=b1, domain=dom['gnome-hello-doc'], language=l_it, full_po=PoFile.objects.create(translated=20))
         # zenity ui 2.30, zenity doc 2.30, zenity ui master, zenity doc master (POT, fr, it)
         Statistics.objects.create(branch=b2, domain=dom['zenity-ui'], language=None, full_po=PoFile.objects.create(untranslated=136))
         Statistics.objects.create(branch=b2, domain=dom['zenity-ui'], language=l_fr, full_po=PoFile.objects.create(translated=136))
         Statistics.objects.create(branch=b2, domain=dom['zenity-ui'], language=l_it, full_po=PoFile.objects.create(translated=130, untranslated=6))
-        Statistics.objects.create(branch=b2, domain=dom['zenity-doc'], language=None, full_po=PoFile.objects.create(untranslated=259, num_figures=11))
+        Statistics.objects.create(branch=b2, domain=dom['zenity-doc'], language=None, full_po=PoFile.objects.create(untranslated=259))
         Statistics.objects.create(branch=b2, domain=dom['zenity-doc'], language=l_fr, full_po=PoFile.objects.create(untranslated=259))
         Statistics.objects.create(branch=b2, domain=dom['zenity-doc'], language=l_it, full_po=PoFile.objects.create(translated=259))
         stat1 = Statistics.objects.create(branch=b3, domain=dom['zenity-ui'], language=None, full_po=PoFile.objects.create(untranslated=149))
         Statistics.objects.create(branch=b3, domain=dom['zenity-ui'], language=l_fr, full_po=PoFile.objects.create(translated=255, fuzzy=4))
         Statistics.objects.create(branch=b3, domain=dom['zenity-ui'], language=l_it, full_po=PoFile.objects.create(translated=259))
-        Statistics.objects.create(branch=b3, domain=dom['zenity-doc'], language=None, full_po=PoFile.objects.create(untranslated=259, num_figures=11))
+        Statistics.objects.create(branch=b3, domain=dom['zenity-doc'], language=None, full_po=PoFile.objects.create(untranslated=259))
         Statistics.objects.create(branch=b3, domain=dom['zenity-doc'], language=l_fr, full_po=PoFile.objects.create(untranslated=259))
         Statistics.objects.create(branch=b3, domain=dom['zenity-doc'], language=l_it, full_po=PoFile.objects.create(translated=259))
         # shared-mime-info ui (POT, fr, it)
diff --git a/stats/tests/help_docbook/C/figures/rnusers.nautilus.png b/stats/tests/help_docbook/C/figures/rnusers.nautilus.png
new file mode 100644
index 0000000..17ddf78
Binary files /dev/null and b/stats/tests/help_docbook/C/figures/rnusers.nautilus.png differ
diff --git a/stats/tests/help_docbook/C/rnusers.xml b/stats/tests/help_docbook/C/rnusers.xml
index 75515b3..8448ee1 100644
--- a/stats/tests/help_docbook/C/rnusers.xml
+++ b/stats/tests/help_docbook/C/rnusers.xml
@@ -6,4 +6,10 @@
 
 <sect1 id="rnusers">
 	<title>What's New for Users</title>
+	<figure id="fig.rnusers.nautilus">
+	 <title><application>Nautilus</application></title>
+	  <screenshot><mediaobject><imageobject>
+		  <imagedata fileref="figures/rnusers.nautilus.png" format="PNG"/>
+		</imageobject></mediaobject></screenshot>
+	</figure>
 </sect1>
diff --git a/stats/utils.py b/stats/utils.py
index 86c587f..89152d0 100644
--- a/stats/utils.py
+++ b/stats/utils.py
@@ -31,11 +31,11 @@ try:
 except ImportError:
     has_toolkit = False
 
-from django.utils.translation import ugettext_noop
+from django.conf import settings
 from django.contrib.sites.models import Site
-from django.core.mail import send_mail
 from django.core.files.base import File
-from django.conf import settings
+from django.core.mail import send_mail
+from django.utils.translation import ugettext_noop
 
 import potdiff
 
@@ -46,6 +46,24 @@ CHANGED_ONLY_FORMATTING = 1
 CHANGED_WITH_ADDITIONS  = 2
 CHANGED_NO_ADDITIONS    = 3
 
+ITSTOOL_PATH = getattr(settings, 'ITSTOOL_PATH', '')
+extract_tools = {
+    'xml2po':  {
+        'command' : "cd \"%(dir)s\" && xml2po %(opts)s -o %(potfile)s -e %(files)s",
+        'mod_var' : "DOC_ID",
+        'incl_var': "DOC_PAGES",
+        'img_grep': "^msgid \"@@image:",
+        'img_regex': re.compile("^msgid \"@@image: \'(?P<path>[^\']*)\'; md5=(?P<hash>[^\"]*)\""),
+    },
+    'itstool': {
+        'command' : "cd \"%%(dir)s\" && %sitstool -o %%(potfile)s %%(files)s" % ITSTOOL_PATH,
+        'mod_var' : "HELP_ID",
+        'incl_var': "HELP_FILES",
+        'img_grep': "^msgid \"external ref=",
+        'img_regex': re.compile("^msgid \"external ref=\'(?P<path>[^\']*)\'; md5=\'(?P<hash>[^\']*)\'\""),
+    },
+}
+
 def sort_object_list(lst, sort_meth):
     """ Sort an object list with sort_meth (which should return a translated string) """
     templist = [(getattr(obj_, sort_meth)().lower(), obj_) for obj_ in lst]
@@ -147,22 +165,8 @@ def check_potfiles(po_path):
                        + "</li>\n</ul>")))
     return errors
 
-def generate_doc_pot_file(vcs_path, potbase, moduleid, verbose):
+def generate_doc_pot_file(vcs_path, potbase, moduleid):
     """ Return the pot file for a document-type domain, and the error if any """
-
-    itstool_path = getattr(settings, 'ITSTOOL_PATH', '')
-    extract_tools = {
-        'xml2po':  {
-            'command' : "cd \"%(dir)s\" && xml2po %(opts)s -o %(potfile)s -e %(files)s",
-            'mod_var' : "DOC_ID",
-            'incl_var': "DOC_PAGES",
-        },
-        'itstool': {
-            'command' : "cd \"%%(dir)s\" && %sitstool -o %%(potfile)s %%(files)s" % itstool_path,
-            'mod_var' : "HELP_ID",
-            'incl_var': "HELP_FILES",
-        },
-    }
     errors = []
 
     doc_id = read_makefile_variable([vcs_path], "HELP_ID")
@@ -192,7 +196,7 @@ def generate_doc_pot_file(vcs_path, potbase, moduleid, verbose):
                     modulename = os.path.basename(xml_files[0])[:-4]
                 else:
                     errors.append(("error", ugettext_noop("DOC_MODULE doesn't point to a real file, probably a macro.")))
-                    return "", errors
+                    return "", errors, tool
         files = [modulename + ".xml"]
         extract_tools[tool]['incl_var'] = "DOC_INCLUDES"
 
@@ -214,9 +218,9 @@ def generate_doc_pot_file(vcs_path, potbase, moduleid, verbose):
         potfile = ""
 
     if not os.access(potfile, os.R_OK):
-        return "", errors
+        return "", errors, tool
     else:
-        return potfile, errors
+        return potfile, errors, tool
 
 def read_makefile_variable(vcs_paths, variable):
     """ vcs_paths is a list of potential path where Makefile.am could be found """
@@ -276,13 +280,12 @@ def pot_diff_status(pota, potb):
     else:
         return CHANGED_NO_ADDITIONS, result_all
 
-def po_file_stats(pofile, msgfmt_checks=True, count_images=True):
+def po_file_stats(pofile, msgfmt_checks=True):
     """ Compute pofile translation statistics, and proceed to some validity checks if msgfmt_checks is True """
     res = {
         'translated' : 0,
         'fuzzy' : 0,
         'untranslated' : 0,
-        'num_figures' : 0,
         'errors' : [],
         }
     c_env = {"LC_ALL": "C", "LANG": "C", "LANGUAGE": "C"}
@@ -345,12 +348,6 @@ def po_file_stats(pofile, msgfmt_checks=True, count_images=True):
         if status != STATUS_OK:
             res['errors'].append(("warn",
                               ugettext_noop("PO file '%s' is not UTF-8 encoded.") % (filename)))
-    # Count number of figures in PO(T) file
-    if count_images:
-        command = "grep '^msgid \"@@image:' \"%s\" | wc -l" % pofile
-        (status, output, errs) = run_shell_command(command)
-        res['num_figures'] = int(output)
-
     return res
 
 def read_linguas_file(full_path):
@@ -397,11 +394,15 @@ def get_doc_linguas(module_path, po_path):
     return {'langs': linguas.split(),
             'error': ugettext_noop("DOC_LINGUAS list doesn't include this language.") }
 
-def get_fig_stats(pofile):
+def get_fig_stats(pofile, image_method, trans_stats=True):
     """ Extract image strings from pofile and return a list of figures dict:
         [{'path':, 'hash':, 'fuzzy':, 'translated':}, ...] """
+    if image_method not in ('xml2po', 'itstool'):
+        return []
     # Extract image strings: beforeline/msgid/msgstr/grep auto output a fourth line
-    command = "msgcat --no-wrap %(pofile)s| grep -A 1 -B 1 '^msgid \"@@image:'" % locals()
+    command = "msgcat --no-wrap %(pofile)s| grep -A 1 -B 1 '%(grep)s'" % {
+        'pofile': pofile, 'grep': extract_tools[image_method]['img_grep']
+    }
     (status, output, errs) = run_shell_command(command)
     if status != STATUS_OK:
         # FIXME: something should be logged here
@@ -409,20 +410,18 @@ def get_fig_stats(pofile):
     lines = output.split('\n')
     while lines[0][0] != "#":
         lines = lines[1:] # skip warning messages at the top of the output
-    re_path = re.compile('^msgid \"@@image: \'([^\']*)\'')
-    re_hash = re.compile('.*md5=(.*)\"')
-    figures = []
 
+    figures = []
     for i, line in islice(enumerate(lines), 0, None, 4):
-        fig = {'path': '', 'hash': ''}
-        fig['fuzzy'] = (line=='#, fuzzy' or line[:8]=='#| msgid')
-        path_match = re_path.match(lines[i+1])
-        if path_match and len(path_match.groups()):
-            fig['path'] = path_match.group(1)
-        hash_match = re_hash.match(lines[i+1])
-        if hash_match and len(hash_match.groups()):
-            fig['hash'] = hash_match.group(1)
-        fig['translated'] = len(lines[i+2])>9 and not fig['fuzzy']
+        # TODO: add image size
+        fig = {"path": '', "hash": ''}
+        m = extract_tools[image_method]['img_regex'].match(lines[i+1])
+        if m:
+            fig["path"] = m.group('path')
+            fig["hash"] = m.group('hash')
+        if trans_stats:
+            fig["fuzzy"] = (line=='#, fuzzy' or line[:8]=='#| msgid')
+            fig["translated"] = len(lines[i+2])>9 and not fig['fuzzy']
         figures.append(fig)
     return figures
 
diff --git a/vertimus/views.py b/vertimus/views.py
index eb86eda..69d84ec 100644
--- a/vertimus/views.py
+++ b/vertimus/views.py
@@ -25,7 +25,7 @@ from django.http import HttpResponseRedirect, Http404
 from django.shortcuts import render, get_object_or_404
 from django.utils.translation import ugettext as _
 
-from stats.models import Statistics, Module, Branch, Domain, Language
+from stats.models import Statistics, FakeLangStatistics, Module, Branch, Domain, Language
 from stats.utils import is_po_reduced
 from vertimus.models import State, Action, ActionArchived
 from vertimus.forms import ActionForm
@@ -62,13 +62,8 @@ def vertimus(request, branch, domain, language, stats=None, level="0"):
     if not stats:
         try:
             stats = Statistics.objects.get(branch=branch, domain=domain, language=language)
-            po_url = stats.po_url()
         except Statistics.DoesNotExist:
-            stats = pot_stats
-            po_url = urlresolvers.reverse('dynamic_po',
-                        args=("%s.%s.%s.%s.po" % (branch.module.name, domain.name, branch.name, language.locale),))
-    else:
-        po_url = stats.po_url()
+            stats = FakeLangStatistics(pot_stats, language)
 
     # Get the state of the translation
     (state, created) = State.objects.get_or_create(
@@ -122,7 +117,7 @@ def vertimus(request, branch, domain, language, stats=None, level="0"):
         'pageSection': 'module',
         'stats': stats,
         'pot_stats': pot_stats,
-        'po_url': po_url,
+        'po_url': stats.po_url(),
         'po_url_reduced': stats.has_reducedstat() and stats.po_url(reduced=True) or '',
         'branch': branch,
         'other_states': other_branch_states,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]