Browse Source

Make use of django polymorphic for technical configuration genericity. This commit add changes on models and admin.

Fabs 10 years ago
parent
commit
f2d349cfec

+ 0 - 0
coin/configuration/__init__.py


+ 26 - 0
coin/configuration/admin.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from django.contrib import admin
+from coin.configuration.models import Configuration
+from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
+
+class ConfigurationAdmin(PolymorphicParentModelAdmin):
+    base_model = Configuration
+    polymorphic_list = True
+    list_display = ('model_name','configuration_type_name', 'offersubscription')
+    search_fields = ['polymorphic_ctype']
+
+    def get_child_models(self):
+        """
+        Renvoi la liste des modèles enfants de Configuration
+        ex :((VPNConfiguration, VPNConfigurationAdmin),
+            (ADSLConfiguration, ADSLConfigurationAdmin))
+        """
+        return (tuple((x.base_model, x) for x in PolymorphicChildModelAdmin.__subclasses__()))
+
+
+
+    # def offer_subscription_subscritption_date(self, config):
+    #     return config.offersubscription.subscription_date
+    # offer_subscription_subscritption_date.short_description = 'Date'
+
+admin.site.register(Configuration, ConfigurationAdmin)

+ 108 - 0
coin/configuration/migrations/0001_initial.py

@@ -0,0 +1,108 @@
+# -*- 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 model 'Configuration'
+        db.create_table(u'configuration_configuration', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('polymorphic_ctype', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'polymorphic_configuration.configuration_set', null=True, to=orm['contenttypes.ContentType'])),
+            ('offersubscription', self.gf('django.db.models.fields.related.OneToOneField')(blank=True, related_name='configuration', unique=True, null=True, to=orm['offers.OfferSubscription'])),
+        ))
+        db.send_create_signal(u'configuration', ['Configuration'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Configuration'
+        db.delete_table(u'configuration_configuration')
+
+
+    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'configuration.configuration': {
+            'Meta': {'object_name': 'Configuration'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'offersubscription': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'configuration'", 'unique': 'True', 'null': 'True', 'to': u"orm['offers.OfferSubscription']"}),
+            'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'polymorphic_configuration.configuration_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"})
+        },
+        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.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.OneToOneField', [], {'default': 'None', 'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            'configuration_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            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'})
+        },
+        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'})
+        }
+    }
+
+    complete_apps = ['configuration']

+ 0 - 0
coin/configuration/migrations/__init__.py


+ 31 - 0
coin/configuration/models.py

@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+from django.db import models
+from polymorphic import PolymorphicModel
+from coin.offers.models import OfferSubscription
+
+class Configuration(PolymorphicModel):
+
+    offersubscription = models.OneToOneField(OfferSubscription, blank=True, null=True, related_name='configuration')
+
+    @staticmethod
+    def get_configurations_choices_list():
+        """
+        Génère automatiquement la liste de choix possibles en fonction des classes enfants de Configuration
+        """
+        return tuple((x().__class__.__name__,x()._meta.verbose_name) for x in Configuration.__subclasses__())
+    
+    def model_name(self):
+        return self.__class__.__name__
+
+    def configuration_type_name(self):
+        return self._meta.verbose_name
+
+    def get_absolute_url(self):
+        from django.core.urlresolvers import reverse
+        return reverse('%s:details' % self.get_url_namespace(), args=[str(self.id)])
+
+    def get_url_namespace(self):
+        if self.url_namespace:
+            return self.url_namespace
+        else:
+            return self.model_name().lower()

+ 3 - 0
coin/configuration/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 3 - 0
coin/configuration/views.py

@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.

+ 22 - 8
coin/offers/admin.py

@@ -1,4 +1,6 @@
 from django.contrib import admin
+from polymorphic.admin import PolymorphicChildModelAdmin
+
 from coin.offers.models import Offer, OfferSubscription
 from coin.offers.offersubscription_filter import\
             OfferSubscriptionTerminationFilter,\
@@ -13,17 +15,17 @@ class IPSubnetInline(admin.TabularInline):
 
 
 class OfferAdmin(admin.ModelAdmin):
-    list_display = ('type', 'name', 'billing_period', 'period_fees',
-                    'initial_fees', 'backend')
+    list_display = ('configuration_type', 'name', 'billing_period', 'period_fees',
+                    'initial_fees')
     list_display_links = ('name',)
-    list_filter = ('type',)
+    list_filter = ('configuration_type',)
     search_fields = ['name']
 
-    def get_readonly_fields(self, request, obj=None):
-        if obj:
-            return ['backend',]
-        else:
-            return []
+    # def get_readonly_fields(self, request, obj=None):
+    #     if obj:
+    #         return ['backend',]
+    #     else:
+    #         return []
 
 
 class OfferSubscriptionAdmin(admin.ModelAdmin):
@@ -46,5 +48,17 @@ class OfferSubscriptionAdmin(admin.ModelAdmin):
 
     inlines = [ IPSubnetInline ]
 
+    def get_inline_instances(self, request, obj=None):
+        """
+        Si en edition, alors affiche en inline le formulaire de la configuration
+        """
+        ipsubnet_inline = [IPSubnetInline(self.model, self.admin_site)]
+
+        if obj is not None:
+            for item in PolymorphicChildModelAdmin.__subclasses__():
+                if (item.base_model.__name__ == obj.offer.configuration_type):
+                    return [item.inline(self.model, self.admin_site)] + ipsubnet_inline
+        return ipsubnet_inline
+
 admin.site.register(Offer, OfferAdmin)
 admin.site.register(OfferSubscription, OfferSubscriptionAdmin)

+ 123 - 0
coin/offers/migrations/0007_auto__del_field_offer_type__del_field_offer_backend__add_field_offer_c.py

@@ -0,0 +1,123 @@
+# -*- 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):
+        # Deleting field 'Offer.type'
+        db.delete_column(u'offers_offer', 'type')
+
+        # Deleting field 'Offer.backend'
+        db.delete_column(u'offers_offer', 'backend')
+
+        # Adding field 'Offer.configuration_type'
+        db.add_column(u'offers_offer', 'configuration_type',
+                      self.gf('django.db.models.fields.CharField')(max_length=50, null=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+
+        # User chose to not deal with backwards NULL issues for 'Offer.type'
+        raise RuntimeError("Cannot reverse this migration. 'Offer.type' and its values cannot be restored.")
+        
+        # The following code is provided here to aid in writing a correct migration        # Adding field 'Offer.type'
+        db.add_column(u'offers_offer', 'type',
+                      self.gf('django.db.models.fields.CharField')(max_length=50),
+                      keep_default=False)
+
+
+        # User chose to not deal with backwards NULL issues for 'Offer.backend'
+        raise RuntimeError("Cannot reverse this migration. 'Offer.backend' and its values cannot be restored.")
+        
+        # The following code is provided here to aid in writing a correct migration        # Adding field 'Offer.backend'
+        db.add_column(u'offers_offer', 'backend',
+                      self.gf('django.db.models.fields.CharField')(max_length=50),
+                      keep_default=False)
+
+        # Deleting field 'Offer.configuration_type'
+        db.delete_column(u'offers_offer', 'configuration_type')
+
+
+    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.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.OneToOneField', [], {'default': 'None', 'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            'configuration_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
+            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'})
+        },
+        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'})
+        }
+    }
+
+    complete_apps = ['offers']

+ 45 - 24
coin/offers/models.py

@@ -23,17 +23,27 @@ class Offer(models.Model):
     "openvpn_ldap").
 
     """
-    OFFER_BACKEND_CHOICES = (
-        ('openvpn_ldap', 'OpenVPN (LDAP)'),
-        # Use this if you don't actually want to implement a backend, for
-        # instance if you resell somebody else's offers and don't manage
-        # technical information yourself.
-        ('none', 'None'),
-    )
+    # OFFER_BACKEND_CHOICES = (
+    #     ('openvpn_ldap', 'OpenVPN (LDAP)'),
+    #     # Use this if you don't actually want to implement a backend, for
+    #     # instance if you resell somebody else's offers and don't manage
+    #     # technical information yourself.
+    #     ('none', 'None'),
+    # )
+    def __init__(self, *args, **kwargs):
+        from coin.configuration.models import Configuration
+        super(Offer, self).__init__(*args, **kwargs)
+        """Génère automatiquement la liste de choix possibles de types
+        de configurations en fonction des classes enfants de Configuration"""
+        self._meta.get_field_by_name('configuration_type')[0]._choices = (
+            Configuration.get_configurations_choices_list())
+
     name = models.CharField(max_length=255, blank=False, null=False,
                             verbose_name='Nom de l\'offre')
-    type = models.CharField(max_length=50,
-                            help_text="Type of offer, for instance technology used (informative only)")
+    configuration_type = models.CharField(max_length=50,
+                            null=True,
+                            choices = (('',''),),
+                            help_text="Type of configuration to use with this offer")
     billing_period = models.IntegerField(blank=False, null=False, default=1,
                                          verbose_name='Période de facturation',
                                          help_text='en mois')
@@ -48,12 +58,23 @@ class Offer(models.Model):
                                       help_text='en €')
     # TODO: really ensure that this field does not change (as it would
     # seriously break subscriptions)
-    backend = models.CharField(max_length=50, choices=OFFER_BACKEND_CHOICES)
+    # backend = models.CharField(max_length=50, choices=OFFER_BACKEND_CHOICES)
+
+    def get_configuration_type_display(self):
+        """
+        Renvoi le nom affichable du type de configuration
+        """
+        for item in Configuration.get_configurations_choices_list():
+            if item and self.configuration_type in item:
+                return item[1]
+        return self.configuration_type
 
     def __unicode__(self):
-        return u'%s : %s - %d€ / %im [%s]' % (self.type, self.name,
-                                              self.period_fees,
-                                              self.billing_period, self.backend)
+        return u'%s : %s - %d€ / %im' % (
+            self.get_configuration_type_display(),
+            self.name,
+            self.period_fees,
+            self.billing_period)
 
     class Meta:
         verbose_name = 'offre'
@@ -90,21 +111,21 @@ class OfferSubscription(models.Model):
     member = models.ForeignKey('members.Member', verbose_name='Membre')
     offer = models.ForeignKey('Offer', verbose_name='Offre')
 
-    @property
-    def configuration(self):
-        """Returns the configuration object associated to this subscription,
-        according to the backend type specified in the offer.  Yes, this
-        is hand-made genericity.  If you can think of a better way, feel
-        free to propose something.
-        """
-        if self.offer.backend == 'none' or not hasattr(self, self.offer.backend):
-            return
-        return getattr(self, self.offer.backend)
+    # @property
+    # def configuration(self):
+    #     """Returns the configuration object associated to this subscription,
+    #     according to the backend type specified in the offer.  Yes, this
+    #     is hand-made genericity.  If you can think of a better way, feel
+    #     free to propose something.
+    #     """
+    #     if self.offer.backend == 'none' or not hasattr(self, self.offer.backend):
+    #         return
+    #     return getattr(self, self.offer.backend)
 
     def __unicode__(self):
         return u'%s - %s - %s - %s' % (self.member, self.offer.name,
                                        self.subscription_date,
-                                       self.configuration)
+                                       self.offer.configuration_type)
 
     class Meta:
         verbose_name = 'abonnement'

+ 2 - 0
coin/settings.py

@@ -153,6 +153,7 @@ INSTALLED_APPS = (
     'django.contrib.admin',
     # Uncomment the next line to enable admin documentation:
     #'django.contrib.admindocs',
+    'polymorphic',
     'south',
     'ldapdb',  # LDAP as database backend
     'autocomplete_light', #Automagic autocomplete foreingkey form component
@@ -163,6 +164,7 @@ INSTALLED_APPS = (
     'coin.billing',
     'coin.resources',
     'coin.reverse_dns',
+    'coin.configuration',
     'coin.vpn'
 )
 

+ 16 - 6
coin/vpn/admin.py

@@ -1,12 +1,20 @@
 # -*- coding: utf-8 -*-
 from django.contrib import admin
+from polymorphic.admin import PolymorphicChildModelAdmin
 
-from coin.vpn.models import VPNSubscription
-from coin.vpn.forms import VPNSubscriptionForm
+from coin.vpn.models import VPNConfiguration
+# from coin.vpn.forms import VPNConfigurationForm
 
 
-class VPNAdmin(admin.ModelAdmin):
-    list_display = ('administrative_subscription', 'activated', 'login',
+class VPNConfigurationInline(admin.StackedInline):
+    model = VPNConfiguration
+    # fk_name = 'offersubscription'
+    readonly_fields = ['configuration_ptr']
+
+
+class VPNConfigurationAdmin(PolymorphicChildModelAdmin):
+    base_model = VPNConfiguration
+    list_display = ('offersubscription', 'activated', 'login',
                     'ipv4_endpoint', 'ipv6_endpoint', 'comment')
     list_filter = ('activated',)
     search_fields = ('login', 'comment',
@@ -17,7 +25,9 @@ class VPNAdmin(admin.ModelAdmin):
     actions = ("generate_endpoints", "generate_endpoints_v4",
                "generate_endpoints_v6", "activate", "deactivate")
     exclude = ("password",)
-    form = VPNSubscriptionForm
+    inline = VPNConfigurationInline
+
+    # form = VPNConfigurationForm
 
     def get_readonly_fields(self, request, obj=None):
         if obj:
@@ -69,4 +79,4 @@ class VPNAdmin(admin.ModelAdmin):
         self.generate_endpoints_generic(request, queryset, v4=False)
     generate_endpoints_v6.short_description = "Generate IPv6 endpoints"
 
-admin.site.register(VPNSubscription, VPNAdmin)
+# admin.site.register(VPNConfiguration, VPNAdmin)

+ 11 - 11
coin/vpn/forms.py

@@ -4,18 +4,18 @@ from django.forms import ModelForm
 
 from coin.offers.models import OfferSubscription
 from coin.offers.backends import filter_subscriptions
-from coin.vpn.models import VPNSubscription
+from coin.vpn.models import VPNConfiguration
 
 
-class VPNSubscriptionForm(ModelForm):
+# class VPNConfigurationForm(ModelForm):
 
-    class Meta:
-        model = VPNSubscription
+#     class Meta:
+#         model = VPNConfiguration
 
-    def __init__(self, *args, **kwargs):
-        super(VPNSubscriptionForm, self).__init__(*args, **kwargs)
-        if self.instance:
-            query = filter_subscriptions(self.instance.backend_name,
-                                         self.instance)
-            queryset = OfferSubscription.objects.filter(query)
-            self.fields['administrative_subscription'].queryset = queryset
+#     def __init__(self, *args, **kwargs):
+#         super(VPNConfigurationForm, self).__init__(*args, **kwargs)
+#         if self.instance:
+#             query = filter_subscriptions(self.instance.backend_name,
+#                                          self.instance)
+#             queryset = OfferSubscription.objects.filter(query)
+#             self.fields['administrative_subscription'].queryset = queryset

+ 142 - 0
coin/vpn/migrations/0007_auto__del_vpnsubscription__add_vpnconfiguration.py

@@ -0,0 +1,142 @@
+# -*- 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):
+        # Deleting model 'VPNSubscription'
+        db.delete_table(u'vpn_vpnsubscription')
+
+        # Adding model 'VPNConfiguration'
+        db.create_table(u'vpn_vpnconfiguration', (
+            (u'configuration_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['configuration.Configuration'], unique=True, primary_key=True)),
+            ('activated', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('login', self.gf('django.db.models.fields.CharField')(unique=True, max_length=50, blank=True)),
+            ('password', self.gf('django.db.models.fields.CharField')(max_length=256, null=True, blank=True)),
+            ('ipv4_endpoint', self.gf('netfields.fields.InetAddressField')(max_length=39, null=True, blank=True)),
+            ('ipv6_endpoint', self.gf('netfields.fields.InetAddressField')(max_length=39, null=True, blank=True)),
+            ('comment', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
+        ))
+        db.send_create_signal(u'vpn', ['VPNConfiguration'])
+
+
+    def backwards(self, orm):
+        # Adding model 'VPNSubscription'
+        db.create_table(u'vpn_vpnsubscription', (
+            ('comment', self.gf('django.db.models.fields.CharField')(max_length=512, blank=True)),
+            ('administrative_subscription', self.gf('django.db.models.fields.related.OneToOneField')(related_name='openvpn_ldap', unique=True, to=orm['offers.OfferSubscription'])),
+            ('ipv6_endpoint', self.gf('netfields.fields.InetAddressField')(max_length=39, null=True, blank=True)),
+            ('ipv4_endpoint', self.gf('netfields.fields.InetAddressField')(max_length=39, null=True, blank=True)),
+            ('password', self.gf('django.db.models.fields.CharField')(max_length=256, null=True, blank=True)),
+            ('activated', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('login', self.gf('django.db.models.fields.CharField')(max_length=50, unique=True)),
+        ))
+        db.send_create_signal(u'vpn', ['VPNSubscription'])
+
+        # Deleting model 'VPNConfiguration'
+        db.delete_table(u'vpn_vpnconfiguration')
+
+
+    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'configuration.configuration': {
+            'Meta': {'object_name': 'Configuration'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'offersubscription': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'configuration'", 'unique': 'True', 'null': 'True', 'to': u"orm['offers.OfferSubscription']"}),
+            'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'polymorphic_configuration.configuration_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"})
+        },
+        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.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.OneToOneField', [], {'default': 'None', 'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            'configuration_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
+            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'})
+        },
+        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'vpn.ldapvpnconfig': {
+            'Meta': {'object_name': 'LdapVPNConfig', 'managed': 'False'},
+            'dn': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        u'vpn.vpnconfiguration': {
+            'Meta': {'object_name': 'VPNConfiguration', '_ormbases': [u'configuration.Configuration']},
+            'activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'comment': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
+            u'configuration_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['configuration.Configuration']", 'unique': 'True', 'primary_key': 'True'}),
+            'ipv4_endpoint': ('netfields.fields.InetAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'ipv6_endpoint': ('netfields.fields.InetAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'login': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['vpn']

+ 16 - 12
coin/vpn/models.py

@@ -7,18 +7,19 @@ from ldapdb.models.fields import CharField, ListField
 
 from coin.mixins import CoinLdapSyncMixin
 from coin.offers.models import OfferSubscription
-from coin.offers.backends import ValidateBackendType
+from coin.configuration.models import Configuration
+# from coin.offers.backends import ValidateBackendType
 from coin import utils
 from coin import validation
 
 
-class VPNSubscription(CoinLdapSyncMixin, models.Model):
+class VPNConfiguration(CoinLdapSyncMixin, Configuration):
     url_namespace = "vpn"
-    backend_name = "openvpn_ldap"
-    administrative_subscription = models.OneToOneField(
-        'offers.OfferSubscription',
-        related_name=backend_name,
-        validators=[ValidateBackendType(backend_name)])
+    # backend_name = "openvpn_ldap"
+    # administrative_subscription = models.OneToOneField(
+    #     'offers.OfferSubscription',
+    #     related_name=backend_name,
+    #     validators=[ValidateBackendType(backend_name)])
     activated = models.BooleanField(default=False)
     login = models.CharField(max_length=50, unique=True, blank=True,
                              help_text="leave empty for automatic generation")
@@ -42,7 +43,7 @@ class VPNSubscription(CoinLdapSyncMixin, models.Model):
         self.save_subnet(subnet, False)
 
     def get_subnets(self, version):
-        subnets = self.administrative_subscription.ip_subnet.all()
+        subnets = self.offersubscription.ip_subnet.all()
         return [subnet for subnet in subnets if subnet.inet.version == version]
 
     def sync_to_ldap(self, creation):
@@ -73,7 +74,7 @@ class VPNSubscription(CoinLdapSyncMixin, models.Model):
         TODO: this should be factored for other technologies (DSL, etc)
 
         """
-        subnets = self.administrative_subscription.ip_subnet.all()
+        subnets = self.offersubscription.ip_subnet.all()
         updated = False
         if v4 and self.ipv4_endpoint is None:
             subnets_v4 = [s for s in subnets if s.inet.version == 4]
@@ -97,7 +98,7 @@ class VPNSubscription(CoinLdapSyncMixin, models.Model):
         If [delete] is True, then simply delete the faulty endpoints
         instead of raising an exception.
         """
-        subnets = self.administrative_subscription.ip_subnet.all()
+        subnets = self.offersubscription.ip_subnet.all()
         is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets])
         if is_faulty(self.ipv4_endpoint):
             if delete:
@@ -115,8 +116,8 @@ class VPNSubscription(CoinLdapSyncMixin, models.Model):
         # login should not contain any ".", because graphite uses "." as a
         # separator.
         if not self.login:
-            username = self.administrative_subscription.member.ldap_cn
-            vpns = VPNSubscription.objects.filter(administrative_subscription__member__ldap_cn=username)
+            username = self.offersubscription.member.ldap_cn
+            vpns = VPNConfiguration.objects.filter(offersubscription__member__ldap_cn=username)
             # This is the list of existing VPN logins for this user.
             logins = [vpn.login for vpn in vpns]
             # 100 VPNs ought to be enough for anybody.
@@ -138,6 +139,9 @@ class VPNSubscription(CoinLdapSyncMixin, models.Model):
     def __unicode__(self):
         return 'VPN ' + self.login
 
+    class Meta:
+        verbose_name = 'VPN'
+
 
 class LdapVPNConfig(ldapdb.models.Model):
     # TODO: déplacer ligne suivante dans settings.py

+ 10 - 10
coin/vpn/tests.py

@@ -1,7 +1,7 @@
 from django.test import TestCase
 
 from coin.offers.models import Offer, OfferSubscription
-from coin.vpn.models import VPNSubscription
+from coin.vpn.models import VPNConfiguration
 from coin.resources.models import IPPool, IPSubnet
 from coin.members.models import Member
 from coin.members.tests import MemberTestsUtils
@@ -32,7 +32,7 @@ class VPNTestCase(TestCase):
         v4 = IPSubnet(ip_pool=self.v4_pool, offer_subscription=abo)
         v4.full_clean()
         v4.save()
-        vpn = VPNSubscription(administrative_subscription=abo)
+        vpn = VPNConfiguration(administrative_subscription=abo)
         vpn.full_clean()
         vpn.save()
 
@@ -42,35 +42,35 @@ class VPNTestCase(TestCase):
             abo = OfferSubscription(offer=self.offer, member=self.member)
             abo.full_clean()
             abo.save()
-            vpn = VPNSubscription(administrative_subscription=abo)
+            vpn = VPNConfiguration(administrative_subscription=abo)
             vpn.full_clean()
             vpn.save()
 
     def tearDown(self):
         """Properly clean up objects, so that they don't stay in LDAP"""
-        for vpn in VPNSubscription.objects.all():
+        for vpn in VPNConfiguration.objects.all():
             vpn.delete()
         Member.objects.get().delete()
 
     def test_has_ipv4_endpoint(self):
-        vpn = VPNSubscription.objects.all()[0]
+        vpn = VPNConfiguration.objects.all()[0]
         self.assertIsNotNone(vpn.ipv4_endpoint)
 
     def test_has_correct_ipv4_endpoint(self):
         """If there is not endpoint, we consider it to be correct."""
-        vpn = VPNSubscription.objects.all()[0]
+        vpn = VPNConfiguration.objects.all()[0]
         if vpn.ipv4_endpoint is not None:
             abo = vpn.administrative_subscription
             subnet = abo.ip_subnet.get(ip_pool=self.v4_pool)
             self.assertIn(vpn.ipv4_endpoint, subnet.inet)
 
     def test_has_ipv6_endpoint(self):
-        vpn = VPNSubscription.objects.all()[0]
+        vpn = VPNConfiguration.objects.all()[0]
         self.assertIsNotNone(vpn.ipv6_endpoint)
 
     def test_has_correct_ipv6_endpoint(self):
         """If there is not endpoint, we consider it to be correct."""
-        vpn = VPNSubscription.objects.all()[0]
+        vpn = VPNConfiguration.objects.all()[0]
         if vpn.ipv6_endpoint is not None:
             abo = vpn.administrative_subscription
             subnet = abo.ip_subnet.get(ip_pool=self.v6_pool)
@@ -93,10 +93,10 @@ class VPNTestCase(TestCase):
         self.test_has_correct_ipv6_endpoint()
 
     def test_automatic_login(self):
-        vpn = VPNSubscription.objects.all()[0]
+        vpn = VPNConfiguration.objects.all()[0]
         expected_login = vpn.administrative_subscription.member.ldap_cn + "-vpn1"
         self.assertEqual(vpn.login, expected_login)
 
     def test_has_multiple_vpn(self):
-        vpns = VPNSubscription.objects.all()
+        vpns = VPNConfiguration.objects.all()
         self.assertEqual(len(vpns), 6)

+ 3 - 3
coin/vpn/views.py

@@ -2,12 +2,12 @@ from django.contrib.auth.models import User
 from django.shortcuts import render_to_response, get_object_or_404
 from django.views.generic.detail import DetailView
 
-from coin.vpn.models import VPNSubscription
+from coin.vpn.models import VPNConfiguration
 
 
 class VPNView(DetailView):
  def get_object(self):
-        return get_object_or_404(VPNSubscription, pk=self.args[0],
+        return get_object_or_404(VPNConfiguration, pk=self.args[0],
                                  administrative_subscription__member__user=self.request.user)
 
 
@@ -15,7 +15,7 @@ def generate_password(request, vpn_id):
     """This generates a random password, saves it in hashed form, and returns
     it to the user in cleartext.
     """
-    vpn = get_object_or_404(VPNSubscription, pk=vpn_id,
+    vpn = get_object_or_404(VPNConfiguration, pk=vpn_id,
                             administrative_subscription__member__user=request.user)
     # This function has nothing to here, but it's convenient.
     password = User.objects.make_random_password()