[damned-lies] Update module maintainers from doap files



commit 22423bd95cd9743ddf675616d7c6741af5bf4cfb
Author: Claude Paroz <claude 2xlibre net>
Date:   Tue Sep 14 22:42:29 2010 +0200

    Update module maintainers from doap files

 common/fields.py                             |   59 ++++++++
 stats/doap.py                                |   48 +++++++
 stats/migrations/0004_add_file_hashes.py     |  188 ++++++++++++++++++++++++++
 stats/models.py                              |   23 +++-
 stats/tests/__init__.py                      |   16 +++
 stats/tests/git/gnome-hello/gnome-hello.doap |   62 +++++++++
 stats/utils.py                               |   16 ++-
 7 files changed, 410 insertions(+), 2 deletions(-)
---
diff --git a/common/fields.py b/common/fields.py
new file mode 100644
index 0000000..817472f
--- /dev/null
+++ b/common/fields.py
@@ -0,0 +1,59 @@
+#-*- coding: utf-8 -*-
+# From: http://djangosnippets.org/snippets/1979/
+
+from django import forms
+from django.core import exceptions
+from django.db import models
+from django.utils import simplejson
+
+class DictionaryField(models.Field):
+    description = "Dictionary object"
+
+    __metaclass__ = models.SubfieldBase
+
+    def get_internal_type(self):
+        return "TextField"
+
+    def to_python(self, value):
+        if value is None:
+            return None
+        elif value == "":
+            return {}
+        elif isinstance(value, basestring):
+            try:
+                return dict(simplejson.loads(value))
+            except (ValueError, TypeError):
+                raise exceptions.ValidationError(self.error_messages['invalid'])
+
+        if isinstance(value, dict):
+            return value
+        else:
+            return {}
+
+    def get_prep_value(self, value):
+        if not value:
+            return ""
+        elif isinstance(value, basestring):
+            return value
+        else:
+            return simplejson.dumps(value)
+
+    def value_to_string(self, obj):
+        value = self._get_val_from_obj(obj)
+        return self.get_prep_value(value)
+
+    def clean(self, value, model_instance):
+        value = super(DictionaryField, self).clean(value, model_instance)
+        return self.get_prep_value(value)
+
+    def formfield(self, **kwargs):
+        defaults = {'widget': forms.Textarea}
+        defaults.update(kwargs)
+        return super(DictionaryField, self).formfield(**defaults)
+
+# rules for South migrations tool (for version >= 0.7)
+try:
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules([], ["^common\.fields\.DictionaryField"])
+except ImportError:
+    pass
diff --git a/stats/doap.py b/stats/doap.py
new file mode 100644
index 0000000..f011f2e
--- /dev/null
+++ b/stats/doap.py
@@ -0,0 +1,48 @@
+import os
+from xml.etree.ElementTree import parse
+from urllib import unquote
+
+from people.models import Person
+
+def parse_maintainers(doap_path):
+    tree = parse(doap_path)
+    maint_tags = tree.getroot().findall("{http://usefulinc.com/ns/doap#}maintainer";)
+    pers_attrs = [
+        "{http://xmlns.com/foaf/0.1/}name";,
+        "{http://xmlns.com/foaf/0.1/}mbox";,
+        "{http://api.gnome.org/doap-extensions#}userid";
+    ]
+    maintainers = []
+    for maint in maint_tags:
+        name, mbox, uid = [maint[0].find(attr) for attr in pers_attrs]
+        name, mbox, uid = name.text, mbox.items()[0][1].replace("mailto:";, ""), uid.text
+        maintainers.append({'name': name, 'email': unquote(mbox), 'account': uid})
+    return maintainers
+
+
+def update_maintainers(module):
+    """ Should only be called inside an "update-stats" context of a master branch,
+        so there is no need for any extra checkout/locking strategy """
+    doap_path = os.path.join(module.get_head_branch().co_path(), "%s.doap" % module.name)
+    if not os.access(doap_path, os.F_OK):
+        return
+    doap_maintainers = parse_maintainers(doap_path)
+    current_maintainers = dict([(m.email, m) for m in module.maintainers.all()])
+
+    # Using email as unique identifier
+    for maint in doap_maintainers:
+        if maint['email'] in current_maintainers.keys():
+            del current_maintainers[maint['email']]
+        else:
+            # Add new maintainer
+            try:
+                pers = Person.objects.get(email=maint['email'])
+            except Person.DoesNotExist:
+                pers = Person(username=maint['account'], email=maint['email'],
+                              password='!', svn_account=maint['account'], last_name=maint['name'])
+                pers.save()
+            module.maintainers.add(pers)
+
+    for key, maint in current_maintainers.items():
+        # Drop maintainers not in doap file
+        module.maintainers.remove(maint)
diff --git a/stats/migrations/0004_add_file_hashes.py b/stats/migrations/0004_add_file_hashes.py
new file mode 100644
index 0000000..dcb794f
--- /dev/null
+++ b/stats/migrations/0004_add_file_hashes.py
@@ -0,0 +1,188 @@
+# 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):
+        
+        # Adding field 'Branch.file_hashes'
+        db.add_column('branch', 'file_hashes', self.gf('common.fields.DictionaryField')(null=True, blank=True), keep_default=False)
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting field 'Branch.file_hashes'
+        db.delete_column('branch', 'file_hashes')
+    
+    
+    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': {'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', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            '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': {'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': {'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': {'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': {'unique_together': "(('name', 'module'),)", 'object_name': 'Branch', 'db_table': "'branch'"},
+            'file_hashes': ('common.fields.DictionaryField', [], {'null': 'True', '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': {'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'})
+        },
+        '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': {'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'}),
+            '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.release': {
+            'Meta': {'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', 'blank': 'True'}),
+            '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']"}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['stats.Domain']"}),
+            'fuzzy': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['languages.Language']", 'null': 'True'}),
+            'num_figures': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'translated': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'untranslated': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        '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'}),
+            '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': {'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', 'blank': '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 62db4a4..410b6c8 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2008 Claude Paroz <claude 2xlibre net>.
+# Copyright (c) 2008-2010 Claude Paroz <claude 2xlibre net>.
 # Copyright (c) 2008 Stephane Raimbault <stephane raimbault gmail com>.
 #
 # This file is part of Damned Lies.
@@ -32,7 +32,9 @@ from django.utils import dateformat
 from django.utils.datastructures import SortedDict
 from django.db import models, connection
 
+from common.fields import DictionaryField
 from stats import utils, signals
+from stats.doap import update_maintainers
 from people.models import Person
 from languages.models import Language
 
@@ -157,6 +159,7 @@ class Branch(models.Model):
     vcs_subpath = models.CharField(max_length=50, null=True, blank=True)
     module      = models.ForeignKey(Module)
     weight      = models.IntegerField(default=0, help_text="Smaller weight is displayed first")
+    file_hashes = DictionaryField(null=True, blank=True)
     # 'releases' is the backward relation name from Release model
 
     # May be set to False by test suite
@@ -222,6 +225,21 @@ class Branch(models.Model):
             return _(u"This branch is not linked from any release")
         return ""
 
+    def file_changed(self, rel_path):
+        """ This method determine if some file has changed based on its hash
+            Always returns true if this is the first time the path is checked
+        """
+        full_path = os.path.join(self.co_path(), rel_path)
+        if not os.access(full_path, os.R_OK):
+            return False # Raise exception?
+        new_hash = utils.compute_md5(full_path)
+        if self.file_hashes.get(rel_path, None) == new_hash:
+            return False
+        else:
+            self.file_hashes[rel_path] = new_hash
+            self.save(update_statistics=False)
+            return True
+
     def has_string_frozen(self):
         """ Returns true if the branch is contained in at least one string frozen release """
         return self.releases.filter(string_frozen=True).count() and True or False
@@ -458,6 +476,9 @@ class Branch(models.Model):
                                                num_figures = int(langstats['num_figures']))
                     for err in langstats['errors']:
                         stat.information_set.add(Information(type=err[0], description=err[1]))
+            # Check if doap file changed
+            if self.file_changed("%s.doap" % self.module.name):
+                update_maintainers(self.module)
 
     def _exists(self):
         """ Determine if branch (self) already exists (i.e. already checked out) on local FS """
diff --git a/stats/tests/__init__.py b/stats/tests/__init__.py
index 6ab26ce..829d34d 100644
--- a/stats/tests/__init__.py
+++ b/stats/tests/__init__.py
@@ -181,3 +181,19 @@ class ModuleTestCase(TestCase):
 # This file is distributed under the same license as the gnome-hello package.
 # FIRST AUTHOR <EMAIL ADDRESS>, YEAR.""" % date.today().year)
         self.assertContains(response, "Language-Team: Tamil <ta li org>")
+
+    def testBranchFileChanged(self):
+        settings.SCRATCHDIR = os.path.dirname(os.path.abspath(__file__))
+        self.assertTrue(self.mod.get_head_branch().file_changed("gnome-hello.doap"))
+        self.assertFalse(self.mod.get_head_branch().file_changed("gnome-hello.doap"))
+
+    def testUpdateMaintainersFromDoapFile(self):
+        from stats.doap import update_maintainers
+        from people.models import Person
+        settings.SCRATCHDIR = os.path.dirname(os.path.abspath(__file__))
+        # Add a maintainer which will be removed
+        pers = Person(username="toto")
+        pers.save()
+        self.mod.maintainers.add(pers)
+        update_maintainers(self.mod)
+        self.assertEquals(self.mod.maintainers.count(), 6)
diff --git a/stats/tests/git/gnome-hello/gnome-hello.doap b/stats/tests/git/gnome-hello/gnome-hello.doap
new file mode 100644
index 0000000..02eea05
--- /dev/null
+++ b/stats/tests/git/gnome-hello/gnome-hello.doap
@@ -0,0 +1,62 @@
+<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+         xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#";
+         xmlns:foaf="http://xmlns.com/foaf/0.1/";
+         xmlns:gnome="http://api.gnome.org/doap-extensions#";
+         xmlns="http://usefulinc.com/ns/doap#";>
+
+  <name xml:lang="en">gnome-hello</name>
+  <shortdesc xml:lang="en">GNOME demo programme</shortdesc>
+  <mailing-list rdf:resource="http://mail.gnome.org/mailman/listinfo/desktop-devel-list"; />
+  <download-page rdf:resource="http://download.gnome.org/sources/gnome-hello/"; />
+  <category rdf:resource="http://api.gnome.org/doap-extensions#development"; />
+
+  <!-- See also: http://foundation.gnome.org/membership/members.php -->
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Andre Klapper</foaf:name>
+      <foaf:mbox rdf:resource="mailto:a9016009%40gmx.de"; />
+      <gnome:userid>aklapper</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Baptiste Mille-Mathias</foaf:name>
+      <foaf:mbox rdf:resource="mailto:baptiste.millemathias%40gmail.com"; />
+      <gnome:userid>baptistem</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Claude Paroz</foaf:name>
+      <foaf:mbox rdf:resource="mailto:claude%402xlibre.net"; />
+      <gnome:userid>claudep</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Christian Persch</foaf:name>
+      <foaf:mbox rdf:resource="mailto:chpe%40gnome.org"; />
+      <gnome:userid>chpe</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Olav Vitters</foaf:name>
+      <foaf:mbox rdf:resource="mailto:olavi%40bkor.dhs.org"; />
+      <gnome:userid>ovitters</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+
+  <maintainer>
+    <foaf:Person>
+      <foaf:name>Vincent Untz</foaf:name>
+      <foaf:mbox rdf:resource="mailto:vuntz%40gnome.org"; />
+      <gnome:userid>vuntz</gnome:userid>
+    </foaf:Person>
+  </maintainer>
+</Project>
diff --git a/stats/utils.py b/stats/utils.py
index 3d4929a..eec3ee9 100644
--- a/stats/utils.py
+++ b/stats/utils.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 #
 # Copyright (c) 2006-2007 Danilo Segan <danilo gnome org>.
-# Copyright (c) 2008 Claude Paroz <claude 2xlibre net>.
+# Copyright (c) 2008-2010 Claude Paroz <claude 2xlibre net>.
 #
 # This file is part of Damned Lies.
 #
@@ -20,6 +20,7 @@
 # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 import sys, os, re, time
+import hashlib
 from itertools import islice
 from subprocess import Popen, PIPE
 import errno
@@ -380,6 +381,19 @@ def copy_file(file1, file2):
     except:
         return 0
 
+def compute_md5(full_path):
+    m = hashlib.md5()
+    block_size=2**13
+    f = open(full_path)
+    while True:
+        data = f.read(block_size)
+        if not data:
+            break
+        m.update(data)
+    f.close()
+    return m.hexdigest()
+
+
 def notify_list(out_domain, diff):
     """Send notification about string changes described in diff."""
     current_site = Site.objects.get_current()



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