[damned-lies] Update module maintainers from doap files
- From: Claude Paroz <claudep src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [damned-lies] Update module maintainers from doap files
- Date: Sat, 23 Oct 2010 09:33:16 +0000 (UTC)
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]