123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- # -*- coding: utf-8 -*-
- import datetime
- from django.db import models
- from django.db.models.signals import post_save, post_delete
- from django.dispatch import receiver
- from coin.resources.models import IPSubnet
- class Offer(models.Model):
- """Description of an offer available to subscribers.
- Implementation notes: achieving genericity is difficult, especially
- because different technologies may have very different configuration
- parameters.
- Technology-specific configuration (e.g. for VPN) is implemented as a
- model having a OneToOne relation to OfferSubscription. In order to
- reach the technology-specific configuration model from an
- OfferSubscription, the OneToOne relation MUST have a related_name
- equal to one of the backends in OFFER_BACKEND_CHOICES (for instance
- "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'),
- # )
- 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')
- 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')
- period_fees = models.DecimalField(max_digits=5, decimal_places=2,
- blank=False, null=False,
- verbose_name='Montant par période de '
- 'facturation',
- help_text='en €')
- initial_fees = models.DecimalField(max_digits=5, decimal_places=2,
- blank=False, null=False,
- verbose_name='Frais de mise en service',
- 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)
- 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' % (
- self.get_configuration_type_display(),
- self.name,
- self.period_fees,
- self.billing_period)
- class Meta:
- verbose_name = 'offre'
- class OfferSubscription(models.Model):
- """Only contains administrative details about a subscription, not
- technical. Nothing here should end up into the LDAP backend.
- Implementation notes: the model actually implementing the backend
- (technical configuration for the technology) MUST relate to this class
- with a OneToOneField whose related name is a member of
- OFFER_BACKEND_CHOICES, for instance:
- models.OneToOneField('offers.OfferSubscription', related_name="openvpn_ldap")
- """
- subscription_date = models.DateField(
- null=False,
- blank=False,
- default=datetime.date.today,
- verbose_name='Date de souscription à l\'offre')
- # TODO: for data retention, prevent deletion of a subscription object
- # while the resign date is recent enough (e.g. one year in France).
- resign_date = models.DateField(
- null=True,
- blank=True,
- verbose_name='Date de résiliation')
- # TODO: move this to offers?
- commitment = models.IntegerField(blank=False, null=False,
- verbose_name='Période d\'engagement',
- help_text = 'en mois',
- default=0)
- 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)
- def __unicode__(self):
- return u'%s - %s - %s - %s' % (self.member, self.offer.name,
- self.subscription_date,
- self.offer.configuration_type)
- class Meta:
- verbose_name = 'abonnement'
- @receiver(post_save, sender=IPSubnet)
- def subnet_save_event(sender, **kwargs):
- """Fires when a subnet is saved (created/modified). We tell the
- configuration backend to do whatever it needs to do with it.
- We should use a pre_save signal, so that if anything goes wrong in the
- backend (exception raised), nothing is actually saved in the database.
- But it has a big problem: the configuration backend will not see the
- change, since it has not been saved into the database yet.
- That's why we use a post_save signal instead. But surprisingly, all
- is well: if we raise an exception here, the IPSubnet object will not
- be saved in the database. But the backend *does* see the new state of
- the database. It looks like the database rollbacks if an exception is
- raised. Whatever the reason, this is not a documented feature of
- Django signals.
- """
- subnet = kwargs['instance']
- config = subnet.offer_subscription.configuration
- if config:
- config.save_subnet(subnet, kwargs['created'])
- @receiver(post_delete, sender=IPSubnet)
- def subnet_delete_event(sender, **kwargs):
- """Fires when a subnet is deleted. We tell the configuration backend to
- do whatever it needs to do with it.
- """
- subnet = kwargs['instance']
- config = subnet.offer_subscription.configuration
- if config:
- config.delete_subnet(subnet)
|