models.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. from django.db import models
  4. from django.db.models.signals import post_save, post_delete
  5. from django.dispatch import receiver
  6. from coin.resources.models import IPSubnet
  7. class Offer(models.Model):
  8. """Description of an offer available to subscribers.
  9. Implementation notes: achieving genericity is difficult, especially
  10. because different technologies may have very different configuration
  11. parameters.
  12. Technology-specific configuration (e.g. for VPN) is implemented as a
  13. model having a OneToOne relation to OfferSubscription. In order to
  14. reach the technology-specific configuration model from an
  15. OfferSubscription, the OneToOne relation MUST have a related_name
  16. equal to one of the backends in OFFER_BACKEND_CHOICES (for instance
  17. "openvpn_ldap").
  18. """
  19. # OFFER_BACKEND_CHOICES = (
  20. # ('openvpn_ldap', 'OpenVPN (LDAP)'),
  21. # # Use this if you don't actually want to implement a backend, for
  22. # # instance if you resell somebody else's offers and don't manage
  23. # # technical information yourself.
  24. # ('none', 'None'),
  25. # )
  26. def __init__(self, *args, **kwargs):
  27. from coin.configuration.models import Configuration
  28. super(Offer, self).__init__(*args, **kwargs)
  29. """Génère automatiquement la liste de choix possibles de types
  30. de configurations en fonction des classes enfants de Configuration"""
  31. self._meta.get_field_by_name('configuration_type')[0]._choices = (
  32. Configuration.get_configurations_choices_list())
  33. name = models.CharField(max_length=255, blank=False, null=False,
  34. verbose_name='Nom de l\'offre')
  35. configuration_type = models.CharField(max_length=50,
  36. null=True,
  37. choices = (('',''),),
  38. help_text="Type of configuration to use with this offer")
  39. billing_period = models.IntegerField(blank=False, null=False, default=1,
  40. verbose_name='Période de facturation',
  41. help_text='en mois')
  42. period_fees = models.DecimalField(max_digits=5, decimal_places=2,
  43. blank=False, null=False,
  44. verbose_name='Montant par période de '
  45. 'facturation',
  46. help_text='en €')
  47. initial_fees = models.DecimalField(max_digits=5, decimal_places=2,
  48. blank=False, null=False,
  49. verbose_name='Frais de mise en service',
  50. help_text='en €')
  51. # TODO: really ensure that this field does not change (as it would
  52. # seriously break subscriptions)
  53. # backend = models.CharField(max_length=50, choices=OFFER_BACKEND_CHOICES)
  54. def get_configuration_type_display(self):
  55. """
  56. Renvoi le nom affichable du type de configuration
  57. """
  58. for item in Configuration.get_configurations_choices_list():
  59. if item and self.configuration_type in item:
  60. return item[1]
  61. return self.configuration_type
  62. def __unicode__(self):
  63. return u'%s : %s - %d€ / %im' % (
  64. self.get_configuration_type_display(),
  65. self.name,
  66. self.period_fees,
  67. self.billing_period)
  68. class Meta:
  69. verbose_name = 'offre'
  70. class OfferSubscription(models.Model):
  71. """Only contains administrative details about a subscription, not
  72. technical. Nothing here should end up into the LDAP backend.
  73. Implementation notes: the model actually implementing the backend
  74. (technical configuration for the technology) MUST relate to this class
  75. with a OneToOneField whose related name is a member of
  76. OFFER_BACKEND_CHOICES, for instance:
  77. models.OneToOneField('offers.OfferSubscription', related_name="openvpn_ldap")
  78. """
  79. subscription_date = models.DateField(
  80. null=False,
  81. blank=False,
  82. default=datetime.date.today,
  83. verbose_name='Date de souscription à l\'offre')
  84. # TODO: for data retention, prevent deletion of a subscription object
  85. # while the resign date is recent enough (e.g. one year in France).
  86. resign_date = models.DateField(
  87. null=True,
  88. blank=True,
  89. verbose_name='Date de résiliation')
  90. # TODO: move this to offers?
  91. commitment = models.IntegerField(blank=False, null=False,
  92. verbose_name='Période d\'engagement',
  93. help_text = 'en mois',
  94. default=0)
  95. member = models.ForeignKey('members.Member', verbose_name='Membre')
  96. offer = models.ForeignKey('Offer', verbose_name='Offre')
  97. # @property
  98. # def configuration(self):
  99. # """Returns the configuration object associated to this subscription,
  100. # according to the backend type specified in the offer. Yes, this
  101. # is hand-made genericity. If you can think of a better way, feel
  102. # free to propose something.
  103. # """
  104. # if self.offer.backend == 'none' or not hasattr(self, self.offer.backend):
  105. # return
  106. # return getattr(self, self.offer.backend)
  107. def __unicode__(self):
  108. return u'%s - %s - %s - %s' % (self.member, self.offer.name,
  109. self.subscription_date,
  110. self.offer.configuration_type)
  111. class Meta:
  112. verbose_name = 'abonnement'
  113. @receiver(post_save, sender=IPSubnet)
  114. def subnet_save_event(sender, **kwargs):
  115. """Fires when a subnet is saved (created/modified). We tell the
  116. configuration backend to do whatever it needs to do with it.
  117. We should use a pre_save signal, so that if anything goes wrong in the
  118. backend (exception raised), nothing is actually saved in the database.
  119. But it has a big problem: the configuration backend will not see the
  120. change, since it has not been saved into the database yet.
  121. That's why we use a post_save signal instead. But surprisingly, all
  122. is well: if we raise an exception here, the IPSubnet object will not
  123. be saved in the database. But the backend *does* see the new state of
  124. the database. It looks like the database rollbacks if an exception is
  125. raised. Whatever the reason, this is not a documented feature of
  126. Django signals.
  127. """
  128. subnet = kwargs['instance']
  129. config = subnet.offer_subscription.configuration
  130. if config:
  131. config.save_subnet(subnet, kwargs['created'])
  132. @receiver(post_delete, sender=IPSubnet)
  133. def subnet_delete_event(sender, **kwargs):
  134. """Fires when a subnet is deleted. We tell the configuration backend to
  135. do whatever it needs to do with it.
  136. """
  137. subnet = kwargs['instance']
  138. config = subnet.offer_subscription.configuration
  139. if config:
  140. config.delete_subnet(subnet)