Parcourir la source

Merge branch 'master' of git.illyse.org:coin

Oriane il y a 11 ans
Parent
commit
efa294d503

+ 1 - 2
README.md

@@ -51,8 +51,7 @@ settings:
 
 
 To sync database, the first time run :
 To sync database, the first time run :
 
 
-python manage.py syncdb
-python manage.py migrate
+python manage.py syncdb --migrate
 
 
 Then at each code update :
 Then at each code update :
 
 

+ 2 - 2
coin/billing/admin.py

@@ -43,8 +43,8 @@ class InvoiceAdmin(admin.ModelAdmin):
     fields = (('number', 'date', 'status'),
     fields = (('number', 'date', 'status'),
        ('date_due'),
        ('date_due'),
        ('member'),
        ('member'),
-       'amount')
-    readonly_fields = ('amount',)
+       ('amount','amount_paid'))
+    readonly_fields = ('amount','amount_paid')
     form = autocomplete_light.modelform_factory(Invoice)
     form = autocomplete_light.modelform_factory(Invoice)
 
 
     def get_formsets(self, request, obj=None):
     def get_formsets(self, request, obj=None):

+ 2 - 2
coin/billing/create_subscriptions_invoices.py

@@ -10,7 +10,6 @@ from coin.members.models import Member
 from coin.billing.models import Invoice, InvoiceDetail
 from coin.billing.models import Invoice, InvoiceDetail
 
 
 
 
-
 def create_all_members_invoices_for_a_period(date=datetime.date.today()):
 def create_all_members_invoices_for_a_period(date=datetime.date.today()):
     """
     """
     Pour chaque membre ayant au moins un abonnement actif, génère les factures
     Pour chaque membre ayant au moins un abonnement actif, génère les factures
@@ -122,7 +121,8 @@ def create_member_invoice_for_a_period(member, date):
             # Si durée de 0jours ou dates incohérentes, alors on ajoute pas
             # Si durée de 0jours ou dates incohérentes, alors on ajoute pas
             # (Si la period est de 0jours c'est que la facture existe déjà.)
             # (Si la period est de 0jours c'est que la facture existe déjà.)
             if (period_from<period_to):
             if (period_from<period_to):
-                # Ajout l'item de l'offre correspondant à l'abonnement à la facture
+                # Ajout l'item de l'offre correspondant à l'abonnement
+                # à la facture
                 invoice.details.create(label=offer.name,
                 invoice.details.create(label=offer.name,
                                        amount=offer.period_fees,
                                        amount=offer.period_fees,
                                        quantity=quantity,
                                        quantity=quantity,

+ 94 - 0
coin/billing/migrations/0015_auto__chg_field_payment_amount.py

@@ -0,0 +1,94 @@
+# -*- coding: 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):
+
+        # Changing field 'Payment.amount'
+        db.alter_column(u'billing_payment', 'amount', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=5, decimal_places=2))
+
+    def backwards(self, orm):
+
+        # Changing field 'Payment.amount'
+        db.alter_column(u'billing_payment', 'amount', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=7, decimal_places=2))
+
+    models = {
+        u'billing.invoice': {
+            'Meta': {'object_name': 'Invoice'},
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}),
+            'date_due': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 31, 0, 0)', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['members.Member']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'number': ('django.db.models.fields.CharField', [], {'default': "u'201403-241-114'", 'unique': 'True', 'max_length': '25'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'open'", 'max_length': '50'})
+        },
+        u'billing.invoicedetail': {
+            'Meta': {'object_name': 'InvoiceDetail'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'details'", 'to': u"orm['billing.Invoice']"}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'offersubscription': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['offers.OfferSubscription']", 'null': 'True', 'blank': 'True'}),
+            'period_from': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 1, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'period_to': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 31, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'quantity': ('django.db.models.fields.DecimalField', [], {'default': '1.0', 'null': 'True', 'max_digits': '4', 'decimal_places': '2'}),
+            'tax': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'null': 'True', 'max_digits': '4', 'decimal_places': '2'})
+        },
+        u'billing.payment': {
+            'Meta': {'object_name': 'Payment'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2'}),
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payments'", 'to': u"orm['billing.Invoice']"}),
+            'payment_mean': ('django.db.models.fields.CharField', [], {'default': "'transfer'", 'max_length': '100', 'null': 'True'})
+        },
+        u'members.member': {
+            'Meta': {'object_name': 'Member'},
+            'address': ('django.db.models.fields.TextField', [], {}),
+            'city': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'country': ('django.db.models.fields.CharField', [], {'default': "'France'", 'max_length': '200'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
+            'entry_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'home_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'ldap_cn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'mobile_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            'organization_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'postal_code': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'non_adherent'", 'max_length': '50'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'personne_physique'", 'max_length': '20'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'initial_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'period_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'service': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Service']"})
+        },
+        u'offers.offersubscription': {
+            'Meta': {'object_name': 'OfferSubscription'},
+            'commitment': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['members.Member']"}),
+            'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Offer']"}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'subscription_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'})
+        },
+        u'offers.service': {
+            'Meta': {'object_name': 'Service'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['billing']

+ 32 - 2
coin/billing/models.py

@@ -4,6 +4,8 @@ import calendar
 import random
 import random
 from decimal import Decimal
 from decimal import Decimal
 from django.db import models
 from django.db import models
+from django.db.models.signals import post_save
+from django.dispatch import receiver
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.models import Offer, OfferSubscription
 from coin.members.models import Member
 from coin.members.models import Member
 
 
@@ -40,6 +42,18 @@ class Invoice(models.Model):
         for detail in self.details.all():
         for detail in self.details.all():
             total += detail.total()
             total += detail.total()
         return total.quantize(Decimal('0.01'))
         return total.quantize(Decimal('0.01'))
+    amount.short_description = 'Montant'
+
+    def amount_paid(self):
+        """
+        Calcul le montant payé de la facture en fonction des éléments
+        de paiements
+        """
+        total = Decimal('0.0')
+        for payment in self.payments.all():
+            total += payment.amount
+        return total.quantize(Decimal('0.01'))
+    amount_paid.short_description = 'Montant payé'
 
 
     def has_owner(self, uid):
     def has_owner(self, uid):
         "Check if passed uid (ex gmajax) is owner of the invoice"
         "Check if passed uid (ex gmajax) is owner of the invoice"
@@ -117,10 +131,26 @@ class Payment(models.Model):
                                     default='transfer',
                                     default='transfer',
                                     choices=PAYMENT_MEAN_CHOICES,
                                     choices=PAYMENT_MEAN_CHOICES,
                                     verbose_name='Moyen de paiement')
                                     verbose_name='Moyen de paiement')
-    amount = models.DecimalField(max_digits=7, decimal_places=2, null=True,
+    amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
                                  verbose_name='Montant')
                                  verbose_name='Montant')
     date = models.DateField(default=datetime.date.today)
     date = models.DateField(default=datetime.date.today)
-    invoice = models.ForeignKey(Invoice, verbose_name='Facture')
+    invoice = models.ForeignKey(Invoice, verbose_name='Facture',
+                                related_name='payments')
+
+    def __unicode__(self):
+        return u'Paiment de %0.2f€' % self.amount
 
 
     class Meta:
     class Meta:
         verbose_name = 'paiement'
         verbose_name = 'paiement'
+
+@receiver(post_save, sender=Payment)
+def set_invoice_as_paid_if_needed(sender, instance, **kwargs):
+    """
+    Lorsqu'un paiement est enregistré, vérifie si la facture est alors
+    complétement payée. Dans ce cas elle passe en réglée
+    """
+    if (instance.invoice.amount_paid >= instance.invoice.amount and
+       instance.invoice.status == 'open'):
+       instance.invoice.status = 'closed'
+       instance.invoice.save()
+

+ 2 - 1
coin/billing/urls.py

@@ -4,5 +4,6 @@ from coin.billing import views
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
     '',
     '',
-    url(r'^invoice/(?P<id>.+).pdf$', views.invoice_pdf, name="invoice_pdf")
+    url(r'^invoice/(?P<id>.+).pdf$', views.invoice_pdf, name="invoice_pdf"),
+    url('invoice/create_all_members_invoices_for_a_period', views.gen_invoices)
 )
 )

+ 4 - 0
coin/billing/views.py

@@ -5,7 +5,11 @@ from django.core.exceptions import PermissionDenied
 from coin.billing.models import Invoice
 from coin.billing.models import Invoice
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
 from coin.html2pdf import render_as_pdf
+from coin.billing.create_subscriptions_invoices import create_all_members_invoices_for_a_period
 
 
+def gen_invoices(request):
+    create_all_members_invoices_for_a_period()
+    return HttpResponse('blop')
 
 
 def invoice_pdf(request, id):
 def invoice_pdf(request, id):
     """
     """

+ 104 - 0
coin/members/migrations/0008_auto__add_field_member_user.py

@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as 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 'Member.user'
+        db.add_column(u'members_member', 'user',
+                      self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['auth.User'], null=True, on_delete=models.SET_NULL),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Member.user'
+        db.delete_column(u'members_member', 'user_id')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'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'}),
+            u'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'})
+        },
+        u'members.cryptokey': {
+            'Meta': {'object_name': 'CryptoKey'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.TextField', [], {}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['members.Member']"}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '3'})
+        },
+        u'members.ldapgroup': {
+            'Meta': {'object_name': 'LdapGroup', 'managed': 'False'},
+            'dn': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        u'members.ldapuser': {
+            'Meta': {'object_name': 'LdapUser', 'managed': 'False'},
+            'dn': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        u'members.member': {
+            'Meta': {'object_name': 'Member'},
+            'address': ('django.db.models.fields.TextField', [], {}),
+            'city': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'country': ('django.db.models.fields.CharField', [], {'default': "'France'", 'max_length': '200'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
+            'entry_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'home_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'ldap_cn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'mobile_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            'organization_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'postal_code': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'non_adherent'", 'max_length': '50'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'personne_physique'", 'max_length': '20'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL'})
+        },
+        u'members.membershipfee': {
+            'Meta': {'object_name': 'MembershipFee'},
+            'amount': ('django.db.models.fields.IntegerField', [], {'default': "'20'"}),
+            'end_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2015, 4, 20, 0, 0)'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'membership_fees'", 'to': u"orm['members.Member']"}),
+            'start_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'})
+        }
+    }
+
+    complete_apps = ['members']

+ 95 - 61
coin/members/models.py

@@ -15,8 +15,11 @@ from django.core import exceptions
 from ldapdb.models.fields import CharField, IntegerField, ListField
 from ldapdb.models.fields import CharField, IntegerField, ListField
 from south.modelsinspector import add_ignored_fields
 from south.modelsinspector import add_ignored_fields
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
+from coin.models import CoinLdapSyncModel
+from django.contrib.auth.signals import user_logged_in
+from django.conf import settings
 
 
-class Member(models.Model):
+class Member(CoinLdapSyncModel):
 
 
     MEMBER_TYPE_CHOICES = (
     MEMBER_TYPE_CHOICES = (
         ('personne_physique', 'Personne physique'),
         ('personne_physique', 'Personne physique'),
@@ -28,6 +31,9 @@ class Member(models.Model):
         ('demande_adhesion', "Demande d'adhésion"),
         ('demande_adhesion', "Demande d'adhésion"),
     )
     )
 
 
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, default=None,
+                             verbose_name='Utilisateur Django',
+                             on_delete=models.SET_NULL)
     status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
     status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
                               default='non_adherent')
                               default='non_adherent')
     type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
     type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
@@ -35,11 +41,11 @@ class Member(models.Model):
     first_name = models.CharField(max_length=200, verbose_name=u'Prénom')
     first_name = models.CharField(max_length=200, verbose_name=u'Prénom')
     last_name = models.CharField(max_length=200, verbose_name=u'Nom')
     last_name = models.CharField(max_length=200, verbose_name=u'Nom')
     ldap_cn = models.CharField(max_length=200, blank=True,
     ldap_cn = models.CharField(max_length=200, blank=True,
-        help_text='Clé avec le LDAP. Laisser vide pour la générer'
-                  'automatiquement')
+                               help_text='Clé avec le LDAP. Laisser vide pour '
+                               'la générer automatiquement')
     organization_name = models.CharField(max_length=200, blank=True,
     organization_name = models.CharField(max_length=200, blank=True,
-        verbose_name='Nom de l\'organisme',
-        help_text='Pour une personne morale')
+                                         verbose_name='Nom de l\'organisme',
+                                         help_text='Pour une personne morale')
     email = models.EmailField(max_length=254, verbose_name=u'Courriel')
     email = models.EmailField(max_length=254, verbose_name=u'Courriel')
     home_phone_number = models.CharField(max_length=25, blank=True,
     home_phone_number = models.CharField(max_length=25, blank=True,
                                          verbose_name=u'Téléphone fixe')
                                          verbose_name=u'Téléphone fixe')
@@ -58,7 +64,8 @@ class Member(models.Model):
                                   default=datetime.date.today,
                                   default=datetime.date.today,
                                   verbose_name='Date de première adhésion')
                                   verbose_name='Date de première adhésion')
     resign_date = models.DateField(null=True, blank=True,
     resign_date = models.DateField(null=True, blank=True,
-        verbose_name='Date de départ de l\'association')
+                                   verbose_name='Date de départ de '
+                                   'l\'association')
 
 
     def __unicode__(self):
     def __unicode__(self):
         name = self.first_name + ' ' + self.last_name
         name = self.first_name + ' ' + self.last_name
@@ -84,6 +91,55 @@ class Member(models.Model):
             Q(subscription_date__lte=date),
             Q(subscription_date__lte=date),
             Q(resign_date__isnull=True) | Q(resign_date__gte=date))
             Q(resign_date__isnull=True) | Q(resign_date__gte=date))
 
 
+    def get_automatic_ldap_cn(self):
+        """
+        Calcul le login / ldap_cn automatiquement en fonction du nom et du prénom
+        """
+        # Première lettre de chaque partie du prénom
+        first_name_letters = ''.join(
+            [c[0] for c in self.first_name.split('-')]
+        )
+        # Concaténer avec nom de famille
+        ldap_cn = ('%s%s' % (first_name_letters, self.last_name))
+        # Remplacer ou enlever les caractères non ascii
+        ldap_cn = unicodedata.normalize('NFD', ldap_cn)\
+            .encode('ascii', 'ignore')
+        # Enlever ponctuation et espace
+        ldap_cn = ldap_cn.translate(None, string.punctuation + ' ')
+        # En minuscule
+        ldap_cn = ldap_cn.lower()
+
+        return ldap_cn
+
+    def sync_to_ldap(self, creation):
+        """
+        Update LDAP data when a member is saved
+        """
+
+        assert self.ldap_cn, ('Can\'t sync with LDAP because missing ldap_cn '
+                              'value for the Member : %s' % self)
+
+        if not creation:
+                ldap_user = LdapUser.objects.get(pk=self.ldap_cn)
+
+        if creation:
+            max_uidNumber = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
+
+            ldap_user = LdapUser()
+            ldap_user.pk = self.ldap_cn
+            ldap_user.uid = self.ldap_cn
+            ldap_user.nick_name = self.ldap_cn
+            ldap_user.uidNumber = max_uidNumber + 1
+
+        ldap_user.last_name = self.last_name
+        ldap_user.first_name = self.first_name
+        ldap_user.save()
+
+        if creation:
+            ldap_group = LdapGroup.objects.get(pk='coin')
+            ldap_group.members.append(ldap_user.pk)
+            ldap_group.save()
+
     class Meta:
     class Meta:
         verbose_name = 'membre'
         verbose_name = 'membre'
 
 
@@ -121,7 +177,7 @@ class MembershipFee(models.Model):
 
 
     def __unicode__(self):
     def __unicode__(self):
         return (u'%s - %s - %i€' % (self.member, self.start_date,
         return (u'%s - %s - %i€' % (self.member, self.start_date,
-                                     self.amount))
+                                    self.amount))
 
 
     class Meta:
     class Meta:
         verbose_name = 'cotisation'
         verbose_name = 'cotisation'
@@ -154,6 +210,7 @@ class LdapUser(ldapdb.models.Model):
 
 
 
 
 class LdapGroup(ldapdb.models.Model):
 class LdapGroup(ldapdb.models.Model):
+
     """
     """
     Class for representing an LDAP group entry.
     Class for representing an LDAP group entry.
     """
     """
@@ -172,10 +229,20 @@ class LdapGroup(ldapdb.models.Model):
     class Meta:
     class Meta:
         managed = False  # Indique à South de ne pas gérer le model LdapGroup
         managed = False  # Indique à South de ne pas gérer le model LdapGroup
 
 
-#Indique à South de ne pas gérer les models LdapUser et LdapGroup
+# Indique à South de ne pas gérer les models LdapUser et LdapGroup
 add_ignored_fields(["^ldapdb\.models\.fields"])
 add_ignored_fields(["^ldapdb\.models\.fields"])
 
 
 
 
+@receiver(pre_save, sender=Member)
+def define_ldap_cn(sender, instance, **kwargs):
+    """
+    Lors de la sauvegarde d'un membre. Si le champ ldap_cn n'est pas définit,
+    le calcul automatiquement en fonction du nom et du prénom
+    """
+    if not instance.ldap_cn and self.pk:
+        instance.ldap_cn = instance.get_automatic_ldap_cn()
+
+
 @receiver(pre_save, sender=LdapUser)
 @receiver(pre_save, sender=LdapUser)
 def change_password(sender, instance, **kwargs):
 def change_password(sender, instance, **kwargs):
     """
     """
@@ -202,61 +269,10 @@ def define_display_name(sender, instance, **kwargs):
                                            instance.last_name)
                                            instance.last_name)
 
 
 
 
-@receiver(pre_save, sender=Member)
-def define_ldap_cn(sender, instance, **kwargs):
-    """
-    Lors de la sauvegarde d'un membre. Si le champ ldap_cn n'est pas définit,
-    le calcul automatiquement en fonction du nom et du prénom
-    """
-    if not instance.ldap_cn:
-        # Première lettre de chaque partie du prénom
-        first_name_letters = ''.join(
-            [c[0] for c in instance.first_name.split('-')]
-        )
-        # Concaténer avec nom de famille
-        ldap_cn = ('%s%s' % (first_name_letters, instance.last_name))
-        # Remplacer ou enlever les caractères non ascii
-        ldap_cn = unicodedata.normalize('NFD', ldap_cn)\
-            .encode('ascii', 'ignore')
-        # Enlever ponctuation et espace
-        ldap_cn = ldap_cn.translate(None, string.punctuation + ' ')
-        # En minuscule
-        ldap_cn = ldap_cn.lower()
-
-        instance.ldap_cn = ldap_cn
-
-
-@receiver(post_save, sender=Member)
-def sync_ldap(sender, instance, created, **kwargs):
-    """
-    Update LDAP data when a member is saved
-    """
-    if not created:
-        ldap_user = LdapUser.objects.get(pk=instance.ldap_cn)
-
-    if created:
-        max_uidNumber = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
-
-        ldap_user = LdapUser()
-        ldap_user.pk = instance.ldap_cn
-        ldap_user.uid = instance.ldap_cn
-        ldap_user.nick_name = instance.ldap_cn
-        ldap_user.uidNumber = max_uidNumber + 1
-
-    ldap_user.last_name = instance.last_name
-    ldap_user.first_name = instance.first_name
-    ldap_user.save()
-
-    if created:
-        ldap_group = LdapGroup.objects.get(pk='coin')
-        ldap_group.members.append(ldap_user.pk)
-        ldap_group.save()
-
-
 @receiver(post_delete, sender=Member)
 @receiver(post_delete, sender=Member)
 def remove_ldap_user_from_coin_group_when_deleting_member(sender,
 def remove_ldap_user_from_coin_group_when_deleting_member(sender,
-                                                                 instance,
-                                                                 **kwargs):
+                                                          instance,
+                                                          **kwargs):
     """
     """
     Lorsqu'un membre est supprimé du SI, son utilisateur LDAP correspondant est
     Lorsqu'un membre est supprimé du SI, son utilisateur LDAP correspondant est
     sorti du groupe "coin"
     sorti du groupe "coin"
@@ -266,6 +282,24 @@ def remove_ldap_user_from_coin_group_when_deleting_member(sender,
         ldap_group.members.remove(instance.ldap_cn)
         ldap_group.members.remove(instance.ldap_cn)
         ldap_group.save()
         ldap_group.save()
 
 
+
+@receiver(user_logged_in)
+def define_member_user(sender, request, user, **kwargs):
+    """
+    Lorsqu'un utilisateur se connect avec succes, fait le lien entre le membre
+    et l'utilisateur en définissant le champ user du model membre ayant le 
+    ldap_cn utilisé pour la connexion
+    """
+    member = Member.objects.get(ldap_cn=user.username)
+    if not member.user:
+        member.user = user
+        member.save()
+    elif member.user.username != user.username:
+        raise Exception('Un membre avec cet ldap_cn existe en base de donnée '
+                        'mais l\'utilisateur auquel il est rattaché ne '
+                        'correspond pas.')
+    
+
 #==============================================================================
 #==============================================================================
 # @receiver(pre_save, sender = LdapUser)
 # @receiver(pre_save, sender = LdapUser)
 # def ssha_password(sender, **kwargs):
 # def ssha_password(sender, **kwargs):

+ 126 - 63
coin/members/tests.py

@@ -1,13 +1,16 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 import os
 import os
+from django import db
 from django.test import TestCase, Client
 from django.test import TestCase, Client
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from coin.members.models import Member, LdapUser, LdapGroup
 from coin.members.models import Member, LdapUser, LdapGroup
 import logging
 import logging
+import ldapdb
+from pprint import pprint
 
 
 
 
 class MemberTests(TestCase):
 class MemberTests(TestCase):
-        
+
     def test_when_creating_member_a_ldapuser_is_also_created_with_same_data(self):
     def test_when_creating_member_a_ldapuser_is_also_created_with_same_data(self):
         """
         """
         Test que lors de la création d'un nouveau membre, une entrée 
         Test que lors de la création d'un nouveau membre, une entrée 
@@ -15,26 +18,26 @@ class MemberTests(TestCase):
         les mêmes données.
         les mêmes données.
         Cela concerne le nom et le prénom
         Cela concerne le nom et le prénom
         """
         """
+
         #~ Créé un membre
         #~ Créé un membre
-        
-        first_name = u'Gérard';
-        last_name = u'Majax';
-        ldap_cn =  MemberTestsUtils.get_random_ldap_cn()
-        member = Member(first_name = first_name,
-                        last_name = last_name,
-                        ldap_cn = ldap_cn)
+        first_name = u'Gérard'
+        last_name = u'Majax'
+        ldap_cn = MemberTestsUtils.get_random_ldap_cn()
+        member = Member(first_name=first_name,
+                        last_name=last_name,
+                        ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         #~ Récupère l'utilisateur LDAP et fait les tests
         #~ Récupère l'utilisateur LDAP et fait les tests
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
-        
+
         self.assertEqual(ldap_user.first_name, first_name)
         self.assertEqual(ldap_user.first_name, first_name)
         self.assertEqual(ldap_user.last_name, last_name)
         self.assertEqual(ldap_user.last_name, last_name)
         self.assertEqual(ldap_user.pk, ldap_cn)
         self.assertEqual(ldap_user.pk, ldap_cn)
-        
-        member.delete();
-        ldap_user.delete();
-    
+
+        member.delete()
+        ldap_user.delete()
+
     def test_when_modifiying_member_corresponding_ldap_user_is_also_modified_with_same_data(self):
     def test_when_modifiying_member_corresponding_ldap_user_is_also_modified_with_same_data(self):
         """
         """
         Test que lorsque l'on modifie un membre, l'utilisateur LDAP 
         Test que lorsque l'on modifie un membre, l'utilisateur LDAP 
@@ -42,28 +45,29 @@ class MemberTests(TestCase):
         Cela concerne le no met le prénom
         Cela concerne le no met le prénom
         """
         """
         #~ Créé un membre
         #~ Créé un membre
-        first_name = u'Ronald';
-        last_name = u'Mac Donald';
-        ldap_cn =  MemberTestsUtils.get_random_ldap_cn()
-        member = Member(first_name = first_name, last_name = last_name, ldap_cn = ldap_cn)
+        first_name = u'Ronald'
+        last_name = u'Mac Donald'
+        ldap_cn = MemberTestsUtils.get_random_ldap_cn()
+        member = Member(first_name=first_name,
+                        last_name=last_name, ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         #~  Le modifie
         #~  Le modifie
-        new_first_name = u'José';
-        new_last_name = u'Bové';
+        new_first_name = u'José'
+        new_last_name = u'Bové'
         member.first_name = new_first_name
         member.first_name = new_first_name
         member.last_name = new_last_name
         member.last_name = new_last_name
         member.save()
         member.save()
-        
+
         #~ Récupère l'utilisateur LDAP et fait les tests
         #~ Récupère l'utilisateur LDAP et fait les tests
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
-        
+
         self.assertEqual(ldap_user.first_name, new_first_name)
         self.assertEqual(ldap_user.first_name, new_first_name)
         self.assertEqual(ldap_user.last_name, new_last_name)
         self.assertEqual(ldap_user.last_name, new_last_name)
 
 
-        member.delete();
-        ldap_user.delete();
-    
+        member.delete()
+        ldap_user.delete()
+
     def test_when_creating_member_corresponding_ldap_user_is_in_coin_ldap_group(self):
     def test_when_creating_member_corresponding_ldap_user_is_in_coin_ldap_group(self):
         """
         """
         Test que l'utilisateur Ldap fraichement créé est bien dans le group "coin"
         Test que l'utilisateur Ldap fraichement créé est bien dans le group "coin"
@@ -71,23 +75,23 @@ class MemberTests(TestCase):
         est bien retiré du groupe.
         est bien retiré du groupe.
         """
         """
         #~ Créé un membre
         #~ Créé un membre
-        ldap_cn =  MemberTestsUtils.get_random_ldap_cn()
-        member = Member(first_name = 'Canard', last_name = 'WC', ldap_cn = ldap_cn)
+        ldap_cn = MemberTestsUtils.get_random_ldap_cn()
+        member = Member(first_name='Canard',
+                        last_name='WC', ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         #~ Récupère le group "coin" et test que l'utilisateur y est présent
         #~ Récupère le group "coin" et test que l'utilisateur y est présent
         ldap_group = LdapGroup.objects.get(pk="coin")
         ldap_group = LdapGroup.objects.get(pk="coin")
         self.assertEqual(ldap_cn in ldap_group.members, True)
         self.assertEqual(ldap_cn in ldap_group.members, True)
-        
+
         #~ Supprime l'utilisateur
         #~ Supprime l'utilisateur
-        member.delete();
-        
+        member.delete()
+
         #~ Récupère le group "coin" et test que l'utilisateur n'y est plus
         #~ Récupère le group "coin" et test que l'utilisateur n'y est plus
         ldap_group = LdapGroup.objects.get(pk="coin")
         ldap_group = LdapGroup.objects.get(pk="coin")
         self.assertEqual(ldap_cn in ldap_group.members, False)
         self.assertEqual(ldap_cn in ldap_group.members, False)
-        
-        LdapUser.objects.get(pk=ldap_cn).delete();
 
 
+        LdapUser.objects.get(pk=ldap_cn).delete()
 
 
     def test_change_password_and_auth(self):
     def test_change_password_and_auth(self):
         """
         """
@@ -96,24 +100,24 @@ class MemberTests(TestCase):
         """
         """
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
         password = "1234"
         password = "1234"
-        
+
          #~ Créé un nouveau membre
          #~ Créé un nouveau membre
-        member = Member(first_name = u'Passe-partout', last_name = u'Du fort Boyard', ldap_cn = ldap_cn)
+        member = Member(first_name=u'Passe-partout',
+                        last_name=u'Du fort Boyard', ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         #~ Récupère l'utilisateur LDAP
         #~ Récupère l'utilisateur LDAP
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
-        
+
         #~ Change son mot de passe
         #~ Change son mot de passe
         member.change_password(password)
         member.change_password(password)
 
 
         #~ Test l'authentification
         #~ Test l'authentification
         c = Client()
         c = Client()
         self.assertEqual(c.login(username=ldap_cn, password=password), True)
         self.assertEqual(c.login(username=ldap_cn, password=password), True)
-        
-        member.delete();
-        ldap_user.delete();
 
 
+        member.delete()
+        ldap_user.delete()
 
 
     def test_when_creating_member_ldap_display_name_is_well_defined(self):
     def test_when_creating_member_ldap_display_name_is_well_defined(self):
         """
         """
@@ -123,17 +127,18 @@ class MemberTests(TestCase):
         first_name = u'Gérard'
         first_name = u'Gérard'
         last_name = u'Majax'
         last_name = u'Majax'
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
-        member = Member(first_name = first_name, last_name = last_name, ldap_cn = ldap_cn)
+        member = Member(first_name=first_name,
+                        last_name=last_name, ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         #~ Récupère l'utilisateur LDAP
         #~ Récupère l'utilisateur LDAP
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
         ldap_user = LdapUser.objects.get(pk=ldap_cn)
-        
-        self.assertEqual(ldap_user.display_name, '%s %s' % (first_name, last_name))
-        
-        member.delete();
-        ldap_user.delete();
 
 
+        self.assertEqual(ldap_user.display_name, '%s %s' %
+                         (first_name, last_name))
+
+        member.delete()
+        ldap_user.delete()
 
 
     def test_when_creating_member_ldap_cn_is_well_defined(self):
     def test_when_creating_member_ldap_cn_is_well_defined(self):
         """
         """
@@ -145,28 +150,83 @@ class MemberTests(TestCase):
         random = os.urandom(4).encode('hex')
         random = os.urandom(4).encode('hex')
         first_name = u'Gérard-Étienne'
         first_name = u'Gérard-Étienne'
         last_name = u'Majax de la Boétie-Blop' + random
         last_name = u'Majax de la Boétie-Blop' + random
-               
+
         control = 'gemajaxdelaboetieblop' + random
         control = 'gemajaxdelaboetieblop' + random
-        
-        member = Member(first_name = first_name, last_name = last_name)
+
+        member = Member(first_name=first_name, last_name=last_name)
         member.save()
         member.save()
 
 
         self.assertEqual(member.ldap_cn, control)
         self.assertEqual(member.ldap_cn, control)
-        
-        member.delete();
-        LdapUser.objects.get(pk=member.ldap_cn).delete();
+
+        member.delete()
+        LdapUser.objects.get(pk=member.ldap_cn).delete()
+
+    def test_when_saving_member_and_ldap_fail_dont_save(self):
+        """
+        Test que lors de la sauvegarde d'un membre et que la sauvegarde en LDAP
+        échoue (ici mauvais mot de passe), rien n'est sauvegardé en base
+        """
+        # Fait échouer le LDAP en définissant un mauvais mot de passe
+        for dbconnection in db.connections.all():
+            if (type(dbconnection) is
+                    ldapdb.backends.ldap.base.DatabaseWrapper):
+                dbconnection.settings_dict['PASSWORD'] = 'wrong password test'
+
+        # Créé un membre
+        first_name = u'Du'
+        last_name = u'Pont'
+        ldap_cn = MemberTestsUtils.get_random_ldap_cn()
+        member = Member(first_name=first_name,
+                        last_name=last_name, ldap_cn=ldap_cn)
+
+        # Le sauvegarde en base de donnée
+        # Le save devrait renvoyer une exception parceque le LDAP échoue
+        self.assertRaises(Exception, member.save)
+
+        # On s'assure, malgré l'exception, que le membre n'est pas en base
+        with self.assertRaises(Member.DoesNotExist):
+            Member.objects.get(ldap_cn=ldap_cn)
+
+    def test_when_user_login_member_user_field_is_updated(self):
+        """
+        Test que lorqu'un utilisateur se connect, le champ user du membre
+        correspondant est mis à jour convenablement
+        """
+        # Créé un membre
+        first_name = u'Du'
+        last_name = u'Pond'
+        password = '1234'
+        ldap_cn = MemberTestsUtils.get_random_ldap_cn()
+        member = Member(first_name=first_name,
+                        last_name=last_name, ldap_cn=ldap_cn)
+        member.save()
+        member.change_password(password)
+
+        # Vérifie que user non définit
+        self.assertIsNone(member.user)
+
+        # Connection
+        c = Client()
+        self.assertEqual(c.login(username=ldap_cn, password=password), True)
+
+        # Vérifie que user définit
+        member = Member.objects.get(ldap_cn=ldap_cn)
+        self.assertIsNotNone(member.user)
 
 
 
 
 class MemberAdminTests(TestCase):
 class MemberAdminTests(TestCase):
+
     def setUp(self):
     def setUp(self):
         #~ Client web
         #~ Client web
         self.client = Client()
         self.client = Client()
         #~ Créé un superuser
         #~ Créé un superuser
         self.admin_user_password = '1234'
         self.admin_user_password = '1234'
-        self.admin_user = User.objects.create_superuser('test_admin_user', 'i@mail.com', self.admin_user_password)
+        self.admin_user = User.objects.create_superuser(
+            'test_admin_user', 'i@mail.com', self.admin_user_password)
         #~ Connection
         #~ Connection
-        self.assertEqual(self.client.login(username = self.admin_user.username, password = self.admin_user_password),True)
-        
+        self.assertEqual(self.client.login(
+            username=self.admin_user.username, password=self.admin_user_password), True)
+
     def test_cant_change_ldap_cn_when_editing(self):
     def test_cant_change_ldap_cn_when_editing(self):
         """
         """
         Vérifie que dans l'admin Django, le champ ldap_cn n'est pad modifiable
         Vérifie que dans l'admin Django, le champ ldap_cn n'est pad modifiable
@@ -176,20 +236,23 @@ class MemberAdminTests(TestCase):
         first_name = u'Gérard'
         first_name = u'Gérard'
         last_name = u'Majax'
         last_name = u'Majax'
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
         ldap_cn = MemberTestsUtils.get_random_ldap_cn()
-        member = Member(first_name = first_name, last_name = last_name, ldap_cn = ldap_cn)
+        member = Member(first_name=first_name,
+                        last_name=last_name, ldap_cn=ldap_cn)
         member.save()
         member.save()
-        
+
         edit_page = self.client.get('/admin/members/member/%i/' % member.id)
         edit_page = self.client.get('/admin/members/member/%i/' % member.id)
         self.assertNotContains(edit_page,
         self.assertNotContains(edit_page,
             '''<input id="id_ldap_cn" />''',
             '''<input id="id_ldap_cn" />''',
-            html=True)
-        
-        LdapUser.objects.get(pk=ldap_cn).delete();
+                               html=True)
+
+        LdapUser.objects.get(pk=ldap_cn).delete()
+
 
 
 class MemberTestsUtils(object):
 class MemberTestsUtils(object):
+
     @staticmethod
     @staticmethod
     def get_random_ldap_cn():
     def get_random_ldap_cn():
         """
         """
         Renvoi une clé aléatoire pour un utilisateur LDAP
         Renvoi une clé aléatoire pour un utilisateur LDAP
         """
         """
-        return 'coin_test_' + os.urandom(8).encode('hex');
+        return 'coin_test_' + os.urandom(8).encode('hex')

+ 38 - 0
coin/models.py

@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+from django.db import models
+from django.db import transaction
+
+
+class CoinLdapSyncModel(models.Model):
+
+    """
+    Ce modèle abstrait est à utiliser lorsqu'il s'agit de définir un modèle
+    à synchroniser avec le LDAP. Le modèle doit définir la methode sync_to_ldap
+    qui s'occupe du transfert vers le LDAP.
+    L'avantage de ce modèle est que si cette méthode échoue, la sauvegarde en
+    base de données échoue a son tour et rien n'est sauvegardé afin de conservé
+    l'intégrité.
+    """
+
+    def sync_to_ldap(self, creation):
+        raise NotImplementedError('Using CoinLdapSyncModel require '
+                                  'sync_to_ldap method being implemented')
+
+    @transaction.atomic
+    def save(self, *args, **kwargs):
+        # Détermine si on est dans une création ou une mise à jour
+        creation = (self.pk == None)
+
+        # Sauvegarde en base de donnée (mais sans commit, cf decorator)
+        super(CoinLdapSyncModel, self).save(*args, **kwargs)
+
+        # Sauvegarde dans le LDAP
+        # Si la sauvegarde LDAP échoue, Rollback la sauvegarde en base, sinon
+        # commit
+        try:
+            self.sync_to_ldap(creation)
+        except:
+            raise
+
+    class Meta:
+        abstract = True

+ 6 - 1
coin/reverse_dns/models.py

@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 from django.db import models
 from django.db import models
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from netfields import InetAddressField, NetManager
 from netfields import InetAddressField, NetManager
@@ -26,9 +27,13 @@ class ReverseDNSEntry(models.Model):
     objects = NetManager()
     objects = NetManager()
 
 
     def clean(self):
     def clean(self):
+        if self.reverse:
+            # Check that the reverse ends with a "." (add it if necessary)
+            if not self.reverse.endswith('.'):
+                self.reverse += '.'
         if self.ip:
         if self.ip:
             if not self.ip in self.ip_subnet.inet:
             if not self.ip in self.ip_subnet.inet:
                 raise ValidationError('IP address must be included in the IP subnet.')
                 raise ValidationError('IP address must be included in the IP subnet.')
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return str(self.ip)
+        return u"{} → {}".format(self.ip, self.reverse)

+ 5 - 5
requirements.txt

@@ -1,8 +1,8 @@
-Django==1.6.1
-South==0.8.2
-django-auth-ldap==1.1.4
-psycopg2==2.5.1
-python-ldap==2.4.13
+Django==1.6.2
+South==0.8.4
+django-auth-ldap==1.2.0
+psycopg2==2.5.2
+python-ldap==2.4.15
 wsgiref==0.1.2
 wsgiref==0.1.2
 python-dateutil==2.2
 python-dateutil==2.2
 django-autocomplete-light==2.0.0a8
 django-autocomplete-light==2.0.0a8