services.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. import argparse
  6. from decimal import Decimal
  7. from services.models import Service, ServiceType, IPResource
  8. class IPAction(argparse.Action):
  9. def __call__(self, parser, namespace, values, option_string=None):
  10. args = values.split('/')
  11. if len(args) == 1:
  12. ip, = args
  13. mask = 0
  14. elif len(args) == 2:
  15. ip, mask = args
  16. else:
  17. raise argparse.ArgumentError(self, '"%s" n’est pas une IP valide' % values)
  18. try:
  19. validate_ipv4_address(ip)
  20. protocol = 'ipv4'
  21. except ValidationError:
  22. try:
  23. validate_ipv6_address(ip)
  24. protocol = 'ipv6'
  25. except ValidationError:
  26. raise argparse.ArgumentError(self, '"%s" n’est pas une IP valide' % values)
  27. try:
  28. mask = int(mask)
  29. except ValueError:
  30. raise argparse.ArgumentError(self, 'masque invalide')
  31. if (mask < 0) or \
  32. (protocol == 'ipv4' and mask > 32) or \
  33. (protocol == 'ipv6' and mask > 128):
  34. raise argparse.ArgumentError(self, 'masque invalide')
  35. ip, created = IPResource.objects.get_or_create(ip=ip, mask=mask)
  36. ip_set = getattr(namespace, 'ip')
  37. ip_set.append(ip)
  38. class Command(BaseCommand):
  39. help = 'Gestion des services'
  40. def add_arguments(self, parser):
  41. cmd = self
  42. class SubParser(CommandParser):
  43. def __init__(self, **kwargs):
  44. super().__init__(cmd, **kwargs)
  45. subparsers = parser.add_subparsers(dest='command', parser_class=SubParser)
  46. subparsers.required = True
  47. parser_stats = subparsers.add_parser('stats', help='Afficher les statistiques sur les services')
  48. parser_list = subparsers.add_parser('list', help='Lister les services')
  49. parser_list.add_argument('--type',
  50. choices=ServiceType.objects.all().values_list('name', flat=True),
  51. help='Afficher uniquement les services d’un certain type')
  52. group = parser_list.add_mutually_exclusive_group()
  53. group.add_argument('--ongoing', action='store_true',
  54. help='Afficher uniquement les services actifs')
  55. group.add_argument('--forthcoming', action='store_true',
  56. help='Afficher uniquement les services à venir (non configuré ou à date de début dans le futur)')
  57. group.add_argument('--finished', action='store_true',
  58. help='Afficher uniquement les services terminés')
  59. parser_show = subparsers.add_parser('show', help='Afficher les informations concernant un service')
  60. parser_show.add_argument('id', type=int)
  61. parser_add = subparsers.add_parser('add', help='Ajouter un nouveau service')
  62. parser_add.add_argument('--type', required=True,
  63. choices=ServiceType.objects.all().values_list('name', flat=True),
  64. help='Afficher uniquement les services d’un certain type')
  65. parser_add.add_argument('--ip', action=IPAction, default=[], help='Assigner une IP au service')
  66. parser_delete = subparsers.add_parser('delete', help='Supprimer un service existant')
  67. parser_delete.add_argument('id', type=int)
  68. def handle(self, *args, **options):
  69. cmd = options.pop('command')
  70. getattr(self, 'handle_{cmd}'.format(cmd=cmd))(*args, **options)
  71. def handle_stats(self, *args, **options):
  72. # TODO: move this code in utils.py or some file like that
  73. as_float_template = '%(function)s(%(expressions)s AS FLOAT)'
  74. total_income = Decimal(0.0)
  75. lines = []
  76. for service_type in ServiceType.objects.all():
  77. services = service_type.services.filter(Service.get_ongoing_filter())
  78. ntotal = services.count()
  79. services = services.exclude(adherent=None)
  80. nadh = services.count()
  81. ninf = ntotal - nadh
  82. services = services.exclude(
  83. Q(contribution__isnull=True)
  84. | Q(contribution__amount=0)
  85. | Q(contribution__period=0)
  86. )
  87. npay = services.count()
  88. ngra = nadh - npay
  89. amount = Func(F('contribution__amount'), function='CAST', template=as_float_template)
  90. period = F('contribution__period')
  91. output_field = DecimalField(max_digits=9, decimal_places=2)
  92. income = services.aggregate(income=Sum(amount / period,
  93. output_field=output_field))['income'] or Decimal(0.0)
  94. total_income += income
  95. lines += [(str(service_type), npay, ninf, ngra, income,)]
  96. self.stdout.write("%-16s%12s%12s%12s%12s%12s" % ('Service', 'npay', 'ninf', 'ngra', 'eur', 'pourcent'))
  97. for service_type, npay, ninf, ngra, income in lines:
  98. if total_income:
  99. percent = income / total_income * 100
  100. else:
  101. percent = 0
  102. self.stdout.write("%-16s%12d%12d%12d%12.2f%12.1f" % (service_type, npay, ninf, ngra, income, percent))
  103. def handle_list(self, *args, **options):
  104. services = Service.objects.all()
  105. if options['type']:
  106. st = ServiceType.objects.get(name=options['type'])
  107. services = services.filter(service_type=st)
  108. if options['ongoing']:
  109. services = services.filter(Service.get_ongoing_filter())
  110. if options['forthcoming']:
  111. services = services.filter(Service.get_forthcoming_filter())
  112. if options['finished']:
  113. services = services.filter(Service.get_finished_filter())
  114. fmt = '{:<8}{:<20}{:<20}{:<10}{:<40}'
  115. print(fmt.format('ID', 'Type', 'Adhérent', 'En cours', 'IPs'))
  116. for service in services:
  117. adh = str(service.adherent) if service.adherent else '-'
  118. ips = ', '.join(map(str, service.ip_resources.all())) or '-'
  119. ongoing = '✔' if service.is_ongoing else '✘'
  120. print(fmt.format(service.pk, str(service.service_type), adh, ongoing, ips))
  121. def handle_show(self, *args, **options):
  122. try:
  123. service = Service.objects.get(pk=options['id'])
  124. except Service.DoesNotExist:
  125. raise CommandError('Le service n°%d n’existe pas' % options['id'])
  126. self.stdout.write('Service n°%d' % service.pk)
  127. self.stdout.write('\tType de service : %s' % service.service_type)
  128. self.stdout.write('\tAdhérent : %s' % (service.adherent or 'aucun'))
  129. self.stdout.write('\tDébut du service : %s' % (service.start or '-'))
  130. self.stdout.write('\tFin du service : %s' % (service.end or '-'))
  131. self.stdout.write('\tService en cours : %s' % ('oui' if service.is_ongoing else 'non'))
  132. def handle_add(self, *args, **options):
  133. for ip in options['ip']:
  134. services = Service.objects.filter(ip_resources=ip).exclude(Service.get_finished_filter())
  135. if services.exists():
  136. self.stdout.write(self.style.WARNING('Note : la ressource IP "%s" est également affecté aux '
  137. 'services suivants :' % ip))
  138. for service in services:
  139. self.stdout.write(self.style.WARNING('\t%d (%s)' % (service.pk, service.service_type)))
  140. value = input('Continuer ? [y/N] ')
  141. if value.lower() != 'y':
  142. raise CommandError('Opération annulée')
  143. st = ServiceType.objects.get(name=options['type'])
  144. service = Service.objects.create(service_type=st)
  145. service.ip_resources = options['ip']
  146. service.save()
  147. self.stdout.write(self.style.SUCCESS('Service n°%d créé avec succès' % service.pk))
  148. def handle_delete(self, *args, **options):
  149. try:
  150. service = Service.objects.get(pk=options['id'])
  151. except Service.DoesNotExist:
  152. raise CommandError('Le service n°%d n’existe pas' % options['id'])
  153. value = input('Êtes vous sûr de vouloir supprimer ce service ? [y/N] ')
  154. if value.lower() == 'y':
  155. service.delete()
  156. self.stdout.write(self.style.SUCCESS('Service supprimé'))
  157. else:
  158. self.stdout.write('Service non supprimé')