models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. from django.db import models
  2. from django.contrib.gis.db import models as geo_models
  3. from django.db.models import Q
  4. from django.core.validators import MaxValueValidator
  5. from django.utils import timezone
  6. from django.contrib.auth.models import Group
  7. from django.contrib.contenttypes.fields import GenericRelation
  8. from django.core.exceptions import ValidationError
  9. from django.urls import reverse
  10. from django.utils import timezone
  11. from django.utils.html import format_html, mark_safe
  12. from django.core.exceptions import PermissionDenied
  13. from django.core.validators import RegexValidator
  14. from ipaddress import ip_network
  15. from djadhere.utils import get_active_filter, is_overlapping
  16. from adhesions.models import Adhesion
  17. from banking.models import RecurringPayment
  18. def ipprefix_validator(value):
  19. try:
  20. ip_network(value)
  21. except ValueError:
  22. raise ValidationError('%s n’est pas un préfixe valide' % value)
  23. class IPPrefix(models.Model):
  24. prefix = models.CharField(max_length=128, verbose_name='Préfixe', validators=[ipprefix_validator], unique=True)
  25. class Meta:
  26. ordering = ['prefix']
  27. verbose_name = 'Réseau'
  28. verbose_name_plural = 'Réseaux'
  29. def __str__(self):
  30. return self.prefix
  31. class IPResourceManager(models.Manager):
  32. def get_queryset(self):
  33. qs = super().get_queryset()
  34. # On rajoute une super annotation « in_use » pour savoir si l’IP est dispo ou non :-)
  35. qs = qs.annotate(
  36. in_use_by_service=models.Exists(
  37. ServiceAllocation.objects.filter(resource=models.OuterRef('pk'), active=True)
  38. ),
  39. in_use_by_antenna=models.Exists(
  40. ServiceAllocation.objects.filter(resource=models.OuterRef('pk'), active=True)
  41. )
  42. )
  43. qs = qs.annotate(
  44. in_use=models.Case(
  45. models.When(Q(in_use_by_service=True) | Q(in_use_by_antenna=True), then=True),
  46. default=False,
  47. output_field=models.BooleanField()
  48. )
  49. )
  50. return qs
  51. class ActiveAllocationManager(models.Manager):
  52. def get_queryset(self):
  53. qs = super().get_queryset()
  54. qs = qs.annotate(
  55. active=models.Case(
  56. models.When(get_active_filter(), then=True),
  57. default=False,
  58. output_field=models.BooleanField()
  59. )
  60. )
  61. return qs
  62. class ActiveServiceManager(models.Manager):
  63. def get_queryset(self):
  64. qs = super().get_queryset()
  65. qs = qs.annotate(
  66. active=models.Case(
  67. models.When(get_active_filter('allocation'), then=True),
  68. default=False,
  69. output_field=models.BooleanField()
  70. )
  71. ).distinct()
  72. return qs
  73. class IPResource(models.Model):
  74. CATEGORIES = (
  75. (0, 'IP Public'),
  76. (1, 'IP Antenne'),
  77. )
  78. ip = models.GenericIPAddressField(verbose_name='IP', primary_key=True)
  79. prefixes = models.ManyToManyField(IPPrefix, verbose_name='préfixes')
  80. reserved = models.BooleanField(default=False, verbose_name='réservée')
  81. category = models.IntegerField(choices=CATEGORIES, verbose_name='catégorie')
  82. notes = models.TextField(blank=True, default='')
  83. last_ping = models.DateTimeField(null=True, blank=True, verbose_name='Dernière réponse au ping')
  84. objects = IPResourceManager()
  85. @property
  86. def allocations(self):
  87. if self.category == 0:
  88. return self.service_allocations
  89. if self.category == 1:
  90. return self.antenna_allocations
  91. class Meta:
  92. ordering = ['ip']
  93. verbose_name = 'IP'
  94. verbose_name_plural = 'IP'
  95. def __str__(self):
  96. return str(self.ip)
  97. class ServiceType(models.Model):
  98. name = models.CharField(max_length=64, verbose_name='Nom', unique=True)
  99. class Meta:
  100. ordering = ['name']
  101. verbose_name = 'type de service'
  102. verbose_name_plural = 'types de service'
  103. def __str__(self):
  104. return self.name
  105. class Service(models.Model):
  106. adhesion = models.ForeignKey(Adhesion, verbose_name='Adhérent', related_name='services')
  107. service_type = models.ForeignKey(ServiceType, related_name='services',
  108. verbose_name='Type de service')
  109. label = models.CharField(blank=True, default='', max_length=128)
  110. notes = models.TextField(blank=True, default='')
  111. created = models.DateTimeField(auto_now_add=True)
  112. contribution = models.OneToOneField(RecurringPayment, on_delete=models.CASCADE)
  113. objects = ActiveServiceManager()
  114. def save(self, *args, **kwargs):
  115. if not hasattr(self, 'contribution'):
  116. self.contribution = RecurringPayment.objects.create()
  117. super().save(*args, **kwargs)
  118. def clean(self):
  119. super().clean()
  120. # Vérification de l’unicité par type de service du label
  121. if self.label != '' and Service.objects.exclude(pk=self.pk).filter(service_type=self.service_type, label=self.label):
  122. raise ValidationError("Un service du même type existe déjà avec ce label.")
  123. def is_active(self):
  124. return self.active
  125. is_active.boolean = True
  126. is_active.short_description = 'Actif'
  127. def get_absolute_url(self):
  128. return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=(self.pk,))
  129. def __str__(self):
  130. s = '#%d %s' % (self.pk, self.service_type)
  131. if self.label:
  132. s += ' ' + self.label
  133. return s
  134. class Antenna(models.Model):
  135. MODE_UNKNOWN = 0
  136. MODE_AP = 1
  137. MODE_STA = 2
  138. MODE_CHOICES = (
  139. (MODE_UNKNOWN, 'Inconnu'),
  140. (MODE_AP, 'AP'),
  141. (MODE_STA, 'Station'),
  142. )
  143. label = models.CharField(max_length=128, blank=True, default='')
  144. mode = models.IntegerField(choices=MODE_CHOICES, default=MODE_UNKNOWN)
  145. ssid = models.CharField(max_length=64, blank=True, default='', verbose_name='SSID')
  146. mac = models.CharField(
  147. blank=True,
  148. default='',
  149. max_length=17,
  150. validators=[
  151. RegexValidator(r'^([0-9a-fA-F]{2}([:-]?|$)){6}$'),
  152. ],
  153. verbose_name='Adresse MAC')
  154. contact = models.ForeignKey(Adhesion, null=True, blank=True)
  155. notes = models.TextField(blank=True)
  156. position = geo_models.PointField(null=True, blank=True)
  157. orientation = models.IntegerField(verbose_name='Orientation (°)', null=True, blank=True)
  158. def clean(self):
  159. super().clean()
  160. if self.orientation:
  161. self.orientation = self.orientation % 360
  162. def get_absolute_url(self):
  163. return reverse('admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=(self.pk,))
  164. def get_absolute_link(self):
  165. name = 'Antenne n°%d' % self.pk
  166. if self.label:
  167. name += ' : %s' % self.label
  168. link = format_html('<a href="{}">{}</a>', self.get_absolute_url(), name)
  169. if self.allocations.filter(active=True).exists():
  170. link += ' ('
  171. link += ', '.join(map(
  172. lambda alloc: format_html('<a href="http://{}">{}</a>', alloc.resource, alloc.resource),
  173. self.allocations.filter(active=True).all()
  174. ))
  175. link += ')'
  176. return mark_safe(link)
  177. class Meta:
  178. verbose_name = 'antenne'
  179. def __str__(self):
  180. name = 'Antenne %d' % self.pk
  181. if self.label:
  182. name += ' (%s)' % self.label
  183. return name
  184. class Route(models.Model):
  185. name = models.CharField(max_length=64, unique=True)
  186. def __str__(self):
  187. return self.name
  188. class Tunnel(Route):
  189. description = models.CharField(max_length=128, blank=True)
  190. created = models.DateTimeField(default=timezone.now, verbose_name='Date de création')
  191. ended = models.DateTimeField(null=True, blank=True, verbose_name='Date de désactivation')
  192. port = models.IntegerField(null=True, blank=True)
  193. local_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP locale')
  194. remote_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP distante')
  195. networks = models.ManyToManyField(IPPrefix, blank=True, verbose_name='Réseaux')
  196. notes = models.TextField(blank=True, default='')
  197. def clean(self):
  198. super().clean()
  199. if self.ended:
  200. # Vérification de la cohérence des champs created et ended
  201. if self.created > self.ended:
  202. raise ValidationError({'ended': "La date de désactivation doit être postérieur "
  203. "à la date de création du tunnel."})
  204. elif self.port:
  205. # Vérification de l’unicité d’un tunnel actif avec un port donné
  206. if Tunnel.objects.exclude(pk=self.pk).filter(port=self.port, ended__isnull=True).exists():
  207. raise ValidationError({'port': "Ce numéro de port est déjà utilisé par un autre tunnel."})
  208. class Allocation(models.Model):
  209. start = models.DateTimeField(verbose_name='Début de la période d’allocation', default=timezone.now)
  210. end = models.DateTimeField(null=True, blank=True, verbose_name='Fin de la période d’allocation')
  211. notes = models.TextField(blank=True, default='')
  212. objects = ActiveAllocationManager()
  213. def clean(self):
  214. super().clean()
  215. # Vérification de la cohérence des champs start et end
  216. if self.end and self.start > self.end:
  217. raise ValidationError("La date de début de l’allocation doit être antérieur "
  218. "à la date de fin de l’allocation.")
  219. if self.resource_id:
  220. if self.resource.reserved and (not self.end or self.end > timezone.now()):
  221. raise ValidationError("L’IP sélectionnée est réservée")
  222. # Vérification de l’abscence de chevauchement de la période d’allocation
  223. allocations = type(self).objects.filter(resource__pk=self.resource.pk)
  224. if is_overlapping(self, allocations):
  225. raise ValidationError("La période d’allocation de cette ressource chevauche "
  226. "avec une période d’allocation précédente.")
  227. class Meta:
  228. abstract = True
  229. ordering = ['-start']
  230. def __str__(self):
  231. return str(self.resource)
  232. class ServiceAllocation(Allocation):
  233. resource = models.ForeignKey(IPResource, verbose_name='Ressource', related_name='service_allocations',
  234. related_query_name='service_allocation', limit_choices_to={'category': 0})
  235. service = models.ForeignKey(Service, related_name='allocations', related_query_name='allocation')
  236. route = models.ForeignKey(Route, verbose_name='Route', related_name='allocations', related_query_name='allocation')
  237. class Meta:
  238. verbose_name = 'allocation'
  239. verbose_name_plural = 'allocations'
  240. class AntennaAllocation(Allocation):
  241. resource = models.ForeignKey(IPResource, verbose_name='Ressource', related_name='antenna_allocations',
  242. related_query_name='antenna_allocation', limit_choices_to={'category': 1})
  243. antenna = models.ForeignKey(Antenna, related_name='allocations', related_query_name='allocation')
  244. class Meta:
  245. verbose_name = 'allocation'
  246. verbose_name_plural = 'allocations'