services.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. from django.core.management.base import BaseCommand, CommandParser, CommandError
  2. from django.db.models import DecimalField, F, Sum, Func, Q
  3. from django.core.validators import validate_ipv4_address, validate_ipv6_address
  4. from django.core.exceptions import ValidationError
  5. from django.utils import timezone
  6. import argparse
  7. from decimal import Decimal
  8. from djadhere.utils import get_active_filter
  9. from adhesions.models import Adhesion
  10. from services.models import Service, ServiceType, IPResource, ResourceAllocation
  11. class IPAction(argparse.Action):
  12. def __call__(self, parser, namespace, values, option_string=None):
  13. args = values.split('/')
  14. if len(args) == 1:
  15. ip, = args
  16. mask = 0
  17. elif len(args) == 2:
  18. ip, mask = args
  19. else:
  20. raise argparse.ArgumentError(self, '"%s" n’est pas une IP valide' % values)
  21. try:
  22. validate_ipv4_address(ip)
  23. protocol = 'ipv4'
  24. except ValidationError:
  25. try:
  26. validate_ipv6_address(ip)
  27. protocol = 'ipv6'
  28. except ValidationError:
  29. raise argparse.ArgumentError(self, '"%s" n’est pas une IP valide' % values)
  30. try:
  31. mask = int(mask)
  32. except ValueError:
  33. raise argparse.ArgumentError(self, 'masque invalide')
  34. if (mask < 0) or \
  35. (protocol == 'ipv4' and mask > 32) or \
  36. (protocol == 'ipv6' and mask > 128):
  37. raise argparse.ArgumentError(self, 'masque invalide')
  38. ip, created = IPResource.objects.get_or_create(ip=ip, mask=mask)
  39. ip_set = getattr(namespace, 'ip')
  40. ip_set.append(ip)
  41. class Command(BaseCommand):
  42. help = 'Gestion des services'
  43. def add_arguments(self, parser):
  44. cmd = self
  45. class SubParser(CommandParser):
  46. def __init__(self, **kwargs):
  47. super().__init__(cmd, **kwargs)
  48. subparsers = parser.add_subparsers(dest='command', parser_class=SubParser)
  49. subparsers.required = True
  50. parser_stats = subparsers.add_parser('stats', help='Afficher les statistiques sur les services')
  51. parser_list = subparsers.add_parser('list', help='Lister les services')
  52. parser_list.add_argument('--type',
  53. choices=ServiceType.objects.all().values_list('name', flat=True),
  54. help='Afficher uniquement les services d’un certain type')
  55. group = parser_list.add_mutually_exclusive_group()
  56. group.add_argument('--active', action='store_true',
  57. help='Afficher uniquement les services actifs')
  58. group.add_argument('--inactive', action='store_true',
  59. help='Afficher uniquement les services inactifs')
  60. parser_show = subparsers.add_parser('show', help='Afficher les informations concernant un service')
  61. parser_show.add_argument('id', type=int)
  62. parser_add = subparsers.add_parser('add', help='Ajouter un nouveau service')
  63. parser_add.add_argument('--adherent', type=int, help='Numéro d’adhérent', required=True)
  64. parser_add.add_argument('--type', required=True,
  65. choices=ServiceType.objects.all().values_list('name', flat=True),
  66. help='Afficher uniquement les services d’un certain type')
  67. parser_add.add_argument('--ip', action=IPAction, default=[], help='Assigner une IP au service')
  68. parser_add.add_argument('--label', default='')
  69. parser_delete = subparsers.add_parser('delete', help='Supprimer un service existant')
  70. parser_delete.add_argument('id', type=int)
  71. def handle(self, *args, **options):
  72. cmd = options.pop('command')
  73. getattr(self, 'handle_{cmd}'.format(cmd=cmd))(*args, **options)
  74. def handle_stats(self, *args, **options):
  75. # TODO: move this code in utils.py or some place like that
  76. as_float_template = '%(function)s(%(expressions)s AS FLOAT)'
  77. total_income = Decimal(0.0)
  78. lines = []
  79. for service_type in ServiceType.objects.all():
  80. services = service_type.services.filter(active=True)
  81. ntotal = services.count()
  82. #services = services.exclude(adherent=None) # TODO: remove infra services here
  83. nadh = services.count()
  84. ninf = ntotal - nadh
  85. services = services.exclude(
  86. Q(contributions__isnull=True)
  87. | Q(contributions__amount=0)
  88. | Q(contributions__period=0)
  89. )
  90. npay = services.count()
  91. ngra = nadh - npay
  92. amount = Func(F('contributions__amount'), function='CAST', template=as_float_template)
  93. period = F('contributions__period')
  94. output_field = DecimalField(max_digits=9, decimal_places=2)
  95. income = services.aggregate(income=Sum(amount / period,
  96. output_field=output_field))['income'] or Decimal(0.0)
  97. total_income += income
  98. lines += [(str(service_type), npay, ninf, ngra, income,)]
  99. self.stdout.write("%-16s%12s%12s%12s%12s%12s" % ('Service', 'npay', 'ninf', 'ngra', 'eur', 'pourcent'))
  100. for service_type, npay, ninf, ngra, income in lines:
  101. if total_income:
  102. percent = income / total_income * 100
  103. else:
  104. percent = 0
  105. self.stdout.write("%-16s%12d%12d%12d%12.2f%12.1f" % (service_type, npay, ninf, ngra, income, percent))
  106. def handle_list(self, *args, **options):
  107. services = Service.objects.all()
  108. if options['type']:
  109. st = ServiceType.objects.get(name=options['type'])
  110. services = services.filter(service_type=st)
  111. if options['active']:
  112. services = services.filter(active=True)
  113. if options['inactive']:
  114. services = services.filter(active=False)
  115. fmt = '{:<8}{:<20}{:<20}{:<20}{:<10}{:<40}'
  116. self.stdout.write(fmt.format('ID', 'Type', 'Label', 'Adhérent', 'Actif', 'IPs'))
  117. for service in services:
  118. adh = service.adhesion.get_adherent_name()
  119. ips = ', '.join(map(lambda a: str(a.resource), service.active_allocations.all())) or '-'
  120. active = '✔' if service.active else '✘'
  121. self.stdout.write(fmt.format(service.pk, str(service.service_type), service.label, adh, active, ips))
  122. def handle_show(self, *args, **options):
  123. try:
  124. service = Service.objects.get(pk=options['id'])
  125. except Service.DoesNotExist:
  126. raise CommandError('Le service n°%d n’existe pas' % options['id'])
  127. self.stdout.write('Service n°%d' % service.pk)
  128. self.stdout.write('\tType de service : %s' % service.service_type)
  129. self.stdout.write('\tAdhérent : ADT%d - %s' % (service.adherent.pk, service.adherent.get_name()))
  130. self.stdout.write('\tActif : %s' % ('oui' if service.active else 'non'))
  131. if service.active_allocations.exists():
  132. self.stdout.write('\tIP :')
  133. for a in service.active_allocations.all():
  134. self.stdout.write('\t\t%s' % a.resource)
  135. else:
  136. self.stdout.write('\tIP : aucune')
  137. def handle_add(self, *args, **options):
  138. for ip in options['ip']:
  139. try:
  140. allocation = ResourceAllocation.objects.filter(resource=ip).get(get_active_filter())
  141. raise CommandError("La ressource %s est déjà utilisé : %s (%s)" % (ip, allocation.service, allocation.service.adherent))
  142. except ResourceAllocation.DoesNotExist:
  143. pass
  144. st = ServiceType.objects.get(name=options['type'])
  145. try:
  146. adherent = Adhesion.objects.get(pk=options['adherent'])
  147. except Adhesion.DoesNotExist:
  148. raise CommandError("L’adhérent %d n’existe pas." % options['adherent'])
  149. if Service.objects.filter(service_type=st, label=options['label']).exists():
  150. raise CommandError("Un service du même type et portant le même label existe déjà.")
  151. service = Service.objects.create(service_type=st, adherent=adherent, label=options['label'])
  152. for ip in options['ip']:
  153. ResourceAllocation.objects.create(service=service, resource=ip, start=timezone.now())
  154. self.stdout.write(self.style.SUCCESS('Service n°%d créé avec succès' % service.pk))
  155. def handle_delete(self, *args, **options):
  156. try:
  157. service = Service.objects.get(pk=options['id'])
  158. except Service.DoesNotExist:
  159. raise CommandError('Le service n°%d n’existe pas' % options['id'])
  160. self.stdout.write('%s – %s' % (service, service.adherent))
  161. value = input('Êtes vous sûr de vouloir supprimer ce service ? [y/N] ')
  162. if value.lower() == 'y':
  163. service.delete()
  164. self.stdout.write(self.style.SUCCESS('Service supprimé'))
  165. else:
  166. self.stdout.write('Service non supprimé')