admin.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. from django.contrib import admin
  2. from django.db import models
  3. from django.db.models import Q
  4. from django.forms import ModelForm, BaseInlineFormSet
  5. from django.utils import timezone
  6. from django.urls import reverse, path
  7. from django.utils.html import format_html
  8. from django.conf.urls import url
  9. from django.template.response import TemplateResponse
  10. from django.core.serializers import serialize
  11. from django.http import HttpResponse
  12. from django.db.models.functions import Cast
  13. from django.contrib.postgres.aggregates import StringAgg
  14. from django.db import connection, transaction
  15. from django.core.cache import cache
  16. from django.contrib.humanize.templatetags.humanize import naturaltime
  17. from django.contrib.contenttypes.models import ContentType
  18. from django.http import HttpResponseRedirect
  19. from django.core.exceptions import ValidationError
  20. from djgeojson.views import GeoJSONLayerView
  21. from urllib.parse import urlencode
  22. from functools import partial, update_wrapper
  23. from datetime import timedelta
  24. from ipaddress import IPv4Address
  25. from djadhere.utils import get_active_filter
  26. from adhesions.models import Adhesion
  27. from banking.models import PaymentUpdate
  28. from .models import Service, ServiceType, IPPrefix, IPResource, IPResourceState, \
  29. ServiceAllocation, Antenna, AntennaAllocation, Allocation, \
  30. Route, Tunnel, Switch, Port
  31. from .utils.notifications import notify_allocation
  32. from .utils.ip_conversion import ipv4_to_ipv6
  33. from .forms import AntennaForm, StopAllocationForm
  34. ### Filters
  35. class ResourceInUseFilter(admin.SimpleListFilter):
  36. title = 'disponibilité'
  37. parameter_name = 'available'
  38. def lookups(self, request, model_admin):
  39. return (
  40. (1, 'Disponible'),
  41. (0, 'Non disponible'),
  42. )
  43. def queryset(self, request, queryset):
  44. available_filter = Q(reserved=False, in_use=False)
  45. if self.value() == '0': # non disponible
  46. return queryset.exclude(available_filter)
  47. if self.value() == '1': # disponible
  48. return queryset.filter(available_filter)
  49. class ResourcePingFilter(admin.SimpleListFilter):
  50. title = 'ping'
  51. parameter_name = 'ping'
  52. def lookups(self, request, model_admin):
  53. return (
  54. ('up', 'UP'),
  55. ('down', 'DOWN'),
  56. ('down-since', 'DOWN depuis…'),
  57. ('never-up', 'Jamais vu UP'),
  58. )
  59. def queryset(self, request, queryset):
  60. if self.value() == 'up':
  61. return queryset.filter(last_state__state=IPResourceState.STATE_UP)
  62. if self.value() == 'down':
  63. return queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKNOWN
  64. if self.value() == 'down-since':
  65. queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP)
  66. return queryset.filter(last_time_up__isnull=False)
  67. if self.value() == 'never-up':
  68. queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKWON
  69. return queryset.filter(last_time_up__isnull=True)
  70. class ActiveServiceFilter(admin.SimpleListFilter):
  71. title = 'actif'
  72. parameter_name = 'active'
  73. def lookups(self, request, model_admin):
  74. return (
  75. (1, 'Actif'),
  76. (0, 'Inactif'),
  77. )
  78. def queryset(self, request, queryset):
  79. if self.value() == '0': # inactif
  80. return queryset.exclude(get_active_filter('allocation'))
  81. if self.value() == '1': # actif
  82. return queryset.filter(get_active_filter('allocation'))
  83. class RouteFilter(admin.SimpleListFilter):
  84. title = 'route'
  85. parameter_name = 'route'
  86. def lookups(self, request, model_admin):
  87. return ServiceAllocation.objects.filter(active=True).values_list('route__pk', 'route__name').distinct()
  88. def queryset(self, request, queryset):
  89. try:
  90. route = int(self.value())
  91. except (TypeError, ValueError):
  92. pass
  93. else:
  94. allocations = ServiceAllocation.objects.filter(active=True, route__pk=route).values_list('pk', flat=True)
  95. queryset = queryset.filter(service_allocation__in=allocations)
  96. return queryset
  97. class AntennaPrefixFilter(admin.SimpleListFilter):
  98. title = 'préfix'
  99. parameter_name = 'prefix'
  100. def lookups(self, request, model_admin):
  101. resources = AntennaAllocation.objects.filter(active=True).values_list('resource__pk', flat=True)
  102. prefixes = IPPrefix.objects.filter(ipresource__in=resources).values_list('pk', 'prefix').distinct()
  103. return prefixes
  104. def queryset(self, request, queryset):
  105. try:
  106. prefix = int(self.value())
  107. except (TypeError, ValueError):
  108. pass
  109. else:
  110. allocations = AntennaAllocation.objects.filter(active=True, resource__prefixes__pk=prefix).values_list('pk', flat=True)
  111. queryset = queryset.filter(allocation__in=allocations)
  112. return queryset
  113. class AntennaPositionFilter(admin.SimpleListFilter):
  114. title = 'géolocalisation'
  115. parameter_name = 'position'
  116. def lookups(self, request, model_admin):
  117. return (
  118. ('1', 'Connue'),
  119. ('0', 'Inconnue'),
  120. )
  121. def queryset(self, request, queryset):
  122. query = Q(position__isnull=True)
  123. if self.value() == '0':
  124. return queryset.filter(query)
  125. if self.value() == '1':
  126. return queryset.exclude(query)
  127. return queryset
  128. class ActiveTunnelFilter(admin.SimpleListFilter):
  129. title = 'status'
  130. parameter_name = 'active'
  131. def lookups(self, request, model_admin):
  132. return (
  133. ('1', 'Actif'),
  134. ('0', 'Désactivé'),
  135. )
  136. def queryset(self, request, queryset):
  137. query = Q(ended__isnull=True)
  138. if self.value() == '0':
  139. return queryset.exclude(query)
  140. if self.value() == '1':
  141. return queryset.filter(query)
  142. return queryset
  143. ### Inlines
  144. class AllocationInlineFormSet(BaseInlineFormSet):
  145. def save_new(self, form, commit=True):
  146. obj = super().save_new(form, commit)
  147. if type(obj) == ServiceAllocation:
  148. notify_allocation(self.request, obj)
  149. return obj
  150. def save_existing(self, form, instance, commit=True):
  151. old = type(instance).objects.get(pk=instance.pk)
  152. if type(instance) == ServiceAllocation:
  153. notify_allocation(self.request, instance, old)
  154. return super().save_existing(form, instance, commit)
  155. class AllocationInline(admin.TabularInline):
  156. formset = AllocationInlineFormSet
  157. extra = 0
  158. show_change_link = True
  159. ordering = ('-start',)
  160. def resource_link(self, obj):
  161. url = reverse('admin:services_ipresource_change', args=[obj.resource.pk])
  162. return format_html('<a href="{}">{}</a>', url, str(obj.resource))
  163. resource_link.short_description = 'IP'
  164. def get_queryset(self, request):
  165. qs = super().get_queryset(request)
  166. qs = qs.select_related('resource')
  167. return qs
  168. def get_formset(self, request, obj=None, **kwargs):
  169. formset = super().get_formset(request, obj, **kwargs)
  170. formset.request = request
  171. return formset
  172. def has_delete_permission(self, request, obj=None):
  173. return False
  174. class NewAllocationMixin:
  175. verbose_name_plural = 'Nouvelle allocation'
  176. max_num = 1
  177. def get_queryset(self, request):
  178. return super().get_queryset(request).model.objects.none()
  179. class ActiveAllocationMixin:
  180. verbose_name_plural = 'Allocations actives'
  181. max_num = 0
  182. def get_queryset(self, request):
  183. return super().get_queryset(request).filter(get_active_filter())
  184. class InactiveAllocationMixin:
  185. verbose_name_plural = 'Anciennes allocations'
  186. max_num = 0
  187. def get_queryset(self, request):
  188. return super().get_queryset(request).exclude(get_active_filter())
  189. class ServiceAllocationMixin:
  190. model = ServiceAllocation
  191. fields = ('id', 'service', 'resource', 'route', 'start', 'end')
  192. raw_id_fields = ('resource',)
  193. autocomplete_fields = ('service',)
  194. def service_link(self, obj):
  195. url = reverse('admin:services_service_change', args=[obj.service.pk])
  196. return format_html('<a href="{}">{}</a>', url, str(obj.service))
  197. service_link.short_description = 'Service'
  198. def get_queryset(self, request):
  199. qs = super().get_queryset(request)
  200. qs = qs.select_related('route')
  201. return qs
  202. #class AntennaAllocationMixin:
  203. # model = AntennaAllocation
  204. # fields = ('id', 'antenna', 'resource', 'start', 'end')
  205. # raw_id_fields = ('resource',)
  206. # autocomplete_fields = ('antenna',)
  207. #
  208. # def get_queryset(self, request):
  209. # qs = super().get_queryset(request)
  210. # qs = qs.select_related('antenna')
  211. # return qs
  212. class NewServiceAllocationInline(ServiceAllocationMixin, NewAllocationMixin, AllocationInline):
  213. fields = ('id', 'service', 'resource', 'route',)
  214. class ActiveServiceAllocationInline(ServiceAllocationMixin, ActiveAllocationMixin, AllocationInline):
  215. fields = ('id', 'service_link', 'resource_link', 'route', 'start', 'stop',)
  216. readonly_fields = ('service_link', 'start', 'resource_link', 'stop',)
  217. def stop(self, obj):
  218. return format_html('<a href="{}" class="deletelink">Terminer</a>', reverse('admin:stop-allocation', kwargs={'resource': obj.resource.ip}))
  219. stop.short_description = 'Terminer l’allocation'
  220. class InactiveServiceAllocationInline(ServiceAllocationMixin, InactiveAllocationMixin, AllocationInline):
  221. fields = ('id', 'service_link', 'resource_link', 'route', 'start', 'end')
  222. readonly_fields = ('service_link', 'resource_link', 'route', 'start', 'end')
  223. #class ActiveAntennaAllocationInline(AntennaAllocationMixin, ActiveAllocationMixin, AllocationInline):
  224. # pass
  225. #class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocationMixin, AllocationInline):
  226. # pass
  227. class IPResourceStateInline(admin.TabularInline):
  228. model = IPResourceState
  229. verbose_name_plural = 'Historique des derniers changements d’état'
  230. fields = ['date']
  231. readonly_fields = ['date']
  232. ordering = ['-date']
  233. def has_add_permission(self, request):
  234. return False
  235. def has_delete_permission(self, request, obj=None):
  236. return False
  237. class PortInline(admin.TabularInline):
  238. model = Port
  239. max_num = 0
  240. def has_add_permission(self, request):
  241. return False
  242. def has_delete_permission(self, request, obj=None):
  243. return False
  244. class SwitchPortInline(PortInline):
  245. fields = ('port', 'up', 'reserved', 'service', 'notes',)
  246. readonly_fields = ('port', 'up',)
  247. autocomplete_fields = ('service',)
  248. def get_queryset(self, request):
  249. qs = super().get_queryset(request)
  250. qs = qs.select_related('switch', 'service', 'service__service_type')
  251. return qs
  252. class ServicePortInline(PortInline):
  253. fields = ('switch', 'port', 'up', 'notes',)
  254. readonly_fields = ('switch', 'port', 'up',)
  255. ### Forms
  256. class ServiceForm(ModelForm):
  257. def clean_adhesion(self):
  258. if hasattr(self.instance, 'adhesion') \
  259. and self.instance.adhesion.pk != self.cleaned_data['adhesion'].pk \
  260. and not self.instance.is_active():
  261. raise ValidationError('Il n’est pas possible de ré-affecter à un autre adhérent un service inactif (i.e. sans allocations actives).')
  262. return self.cleaned_data['adhesion']
  263. ### ModelAdmin
  264. class ServiceAdmin(admin.ModelAdmin):
  265. list_display = ('id', 'get_adhesion_link', 'get_adherent_link', 'service_type', 'label', 'is_active',)
  266. list_select_related = ('adhesion', 'adhesion__user', 'adhesion__user__profile', 'adhesion__corporation', 'service_type',)
  267. list_filter = (
  268. ActiveServiceFilter,
  269. 'loan_equipment',
  270. ('service_type', admin.RelatedOnlyFieldListFilter),
  271. )
  272. search_fields = ('=id', 'service_type__name', 'label', 'notes',)
  273. fields = ('adhesion', 'service_type', 'label', 'notes', 'loan_equipment', 'get_contribution_link', 'is_active',)
  274. readonly_fields = ('get_contribution_link', 'is_active',)
  275. raw_id_fields = ('adhesion',)
  276. form = ServiceForm
  277. def save_model(self, request, srv, form, change):
  278. if srv.pk and 'adhesion' in form.changed_data:
  279. with transaction.atomic():
  280. old_srv = Service.objects.get(pk=srv.pk)
  281. adhesion = srv.adhesion
  282. srv.adhesion = old_srv.adhesion
  283. label = srv.label
  284. srv.label = '%s (transféré à ADT%d le %s)' % (srv.label, adhesion.pk, timezone.now().strftime('%d/%m/%Y'))
  285. srv.save()
  286. new_srv = Service.objects.create(adhesion=adhesion, service_type=srv.service_type, label=label,
  287. notes=srv.notes, loan_equipment=srv.loan_equipment)
  288. for allocation in srv.active_allocations:
  289. allocation.end = timezone.now()
  290. allocation.save()
  291. ServiceAllocation.objects.create(resource=allocation.resource, service=new_srv, route=allocation.route)
  292. else:
  293. super().save_model(request, srv, form, change)
  294. def get_queryset(self, request):
  295. qs = super().get_queryset(request)
  296. qs = qs.prefetch_related('allocations',)
  297. return qs
  298. get_adhesion_link = lambda self, service: service.adhesion.get_adhesion_link()
  299. get_adhesion_link.short_description = Adhesion.get_adhesion_link.short_description
  300. get_adherent_link = lambda self, service: service.adhesion.get_adherent_link()
  301. get_adherent_link.short_description = Adhesion.get_adherent_link.short_description
  302. def get_contribution_link(self, obj):
  303. return format_html(u'<a href="{}">{}</a>', obj.contribution.get_absolute_url(), obj.contribution.get_current_payment_display())
  304. get_contribution_link.short_description = 'Contribution financière'
  305. def get_inline_instances(self, request, obj=None):
  306. inlines = []
  307. if obj and obj.ports.exists():
  308. inlines += [ServicePortInline]
  309. inlines += [NewServiceAllocationInline]
  310. if obj and obj.active_allocations.exists():
  311. inlines += [ActiveServiceAllocationInline]
  312. if obj and obj.inactive_allocations.exists():
  313. inlines += [InactiveServiceAllocationInline]
  314. return [inline(self.model, self.admin_site) for inline in inlines]
  315. def get_actions(self, request):
  316. actions = super().get_actions(request)
  317. if 'delete_selected' in actions:
  318. del actions['delete_selected']
  319. return actions
  320. def has_delete_permission(self, request, obj=None):
  321. if not obj:
  322. return False
  323. one_year_ago = timezone.now() - timedelta(days=365)
  324. contribution = obj.contribution.updates.filter(validated=True).first()
  325. # s’il y avait un paiement actif il y a moins d’un an
  326. if not contribution or contribution.payment_method != PaymentUpdate.STOP or contribution.start > one_year_ago:
  327. return False
  328. # s’il y avait une allocation active il y a moins d’un an
  329. if any(map(lambda a: a.end is None or a.end > one_year_ago, obj.allocations.all())):
  330. return False
  331. return True
  332. class IPPrefixAdmin(admin.ModelAdmin):
  333. def has_delete_permission(self, request, obj=None):
  334. # Interdiction de supprimer le préfix s’il est assigné à un tunnel
  335. return obj and obj.tunnel_set.exists()
  336. def has_change_permission(self, request, obj=None):
  337. if obj:
  338. return False
  339. else:
  340. return True
  341. # pour embêcher de by-passer le check has_delete_permission, on désactive l’action delete
  342. def get_actions(self, request):
  343. actions = super().get_actions(request)
  344. if 'delete_selected' in actions:
  345. del actions['delete_selected']
  346. return actions
  347. class IPResourceAdmin(admin.ModelAdmin):
  348. list_display = ('__str__', 'available_display', 'route', 'last_use', 'ping',)
  349. list_filter = (
  350. 'category',
  351. ResourceInUseFilter,
  352. ResourcePingFilter,
  353. 'reserved',
  354. ('prefixes', admin.RelatedOnlyFieldListFilter),
  355. RouteFilter,
  356. )
  357. search_fields = ('=ip', 'notes',)
  358. actions = ['contact_ip_owners']
  359. ordering = ['ip']
  360. inlines = [ IPResourceStateInline ]
  361. def get_fields(self, request, obj=None):
  362. return self.get_readonly_fields(request, obj)
  363. def get_readonly_fields(self, request, obj=None):
  364. fields = ['ip', 'ip6']
  365. if obj:
  366. if obj.reserved:
  367. fields += ['reserved']
  368. if not obj.in_use:
  369. fields += ['last_use']
  370. fields += ['last_state']
  371. if obj.last_state.state != IPResourceState.STATE_UP:
  372. fields += ['last_time_up']
  373. if obj.category == IPResource.CATEGORY_PUBLIC:
  374. fields += ['password']
  375. if obj.checkmk_label:
  376. fields += ['checkmk']
  377. if obj.notes:
  378. fields += ['notes']
  379. return fields
  380. def get_inline_instances(self, request, obj=None):
  381. super_inlines = super().get_inline_instances(request, obj)
  382. inlines = []
  383. if obj and obj.category == IPResource.CATEGORY_PUBLIC:
  384. if obj.allocations.filter(get_active_filter()).exists():
  385. inlines += [ActiveServiceAllocationInline]
  386. else:
  387. inlines += [NewServiceAllocationInline]
  388. if obj.allocations.exclude(get_active_filter()).exists():
  389. inlines += [InactiveServiceAllocationInline]
  390. return [inline(self.model, self.admin_site) for inline in inlines] + super_inlines
  391. def get_queryset(self, request):
  392. qs = super().get_queryset(request)
  393. now = timezone.now()
  394. qs = qs.annotate(
  395. last_use=models.Case(
  396. models.When(in_use=True, then=now),
  397. models.When(category=0, then=models.Max('service_allocation__end')),
  398. models.When(category=1, then=models.Max('antenna_allocation__end')),
  399. default=None,
  400. ))
  401. qs = qs.annotate(
  402. downtime=models.Case(
  403. models.When(last_state__state=IPResourceState.STATE_UP, then=models.F('last_state__date')-models.Value(now)),
  404. models.When(last_state__state=IPResourceState.STATE_DOWN, then=models.Value(now)-models.F('last_time_up')),
  405. default=None,
  406. output_field=models.DurationField(),
  407. ))
  408. qs = qs.annotate(
  409. route=models.Case(
  410. models.When(
  411. in_use_by_service=True,
  412. then=models.Subquery(
  413. ServiceAllocation.objects.filter(
  414. Q(resource=models.OuterRef('pk')) & get_active_filter()
  415. ).values('route__name')[:1]
  416. ),
  417. ),
  418. output_field=models.CharField(),
  419. ))
  420. return qs
  421. def available_display(self, obj):
  422. return not obj.reserved and not obj.in_use
  423. available_display.short_description = 'Disponible'
  424. available_display.boolean = True
  425. def last_use(self, obj):
  426. if obj.last_use:
  427. return naturaltime(obj.last_use)
  428. else:
  429. return '-'
  430. last_use.short_description = 'Dernière utilisation'
  431. last_use.admin_order_field = 'last_use'
  432. def ping(self, obj):
  433. if obj.last_state.state == IPResourceState.STATE_UP:
  434. label = 'UP'
  435. else:
  436. if obj.last_time_up:
  437. label = 'dernier ping : ' + naturaltime(obj.last_time_up)
  438. else:
  439. label = 'DOWN'
  440. if obj.checkmk_url:
  441. return format_html('<a href="{}">{}</a>', obj.checkmk_url, label)
  442. else:
  443. return label
  444. ping.short_description = 'ping'
  445. #ping.admin_order_field = 'last_state__date'
  446. ping.admin_order_field = 'downtime'
  447. def route(self, obj):
  448. return obj.route
  449. route.short_description = 'route'
  450. route.admin_order_field = 'route'
  451. def ip6(self, obj):
  452. return ipv4_to_ipv6(IPv4Address(obj.ip))
  453. ip6.short_description = 'Préfixe IPv6'
  454. ip6.admin_order_field = 'ip'
  455. def checkmk(self, obj):
  456. return format_html('<a href="{}">{}</a>', obj.checkmk_url, 'voir')
  457. checkmk.short_description = 'CheckMK'
  458. def contact_ip_owners(self, request, queryset):
  459. selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
  460. services = ServiceAllocation.objects.filter(resource__ip__in=selected) \
  461. .filter(get_active_filter()) \
  462. .values_list('service__adhesion', flat=True)
  463. antennas = AntennaAllocation.objects.filter(resource__ip__in=selected) \
  464. .filter(get_active_filter()) \
  465. .values_list('antenna__contact', flat=True)
  466. pk = ",".join(map(str, set(services) | set(antennas)))
  467. return HttpResponseRedirect(reverse('admin:contact-adherents') + "?pk=%s" % pk)
  468. contact_ip_owners.short_description = 'Contacter les adhérents'
  469. def stop_allocation(self, request, resource):
  470. resource = self.get_object(request, resource)
  471. allocation = resource.allocations.filter(get_active_filter()).first()
  472. if not allocation: # L’IP n’est pas allouée
  473. return HttpResponseRedirect(reverse('admin:services_ipresource_change', args=[resource.pk]))
  474. form = StopAllocationForm(request.POST or None)
  475. if request.method == 'POST' and form.is_valid():
  476. self.message_user(request, 'Allocation stoppée.')
  477. allocation.end = timezone.now()
  478. allocation.save()
  479. notify_allocation(request, allocation)
  480. # Il faudrait rajouter un redirect dans l’URL pour rediriger vers l’IP ou le Service
  481. return HttpResponseRedirect(reverse('admin:services_ipresource_change', args=[resource.pk]))
  482. context = self.admin_site.each_context(request)
  483. context.update({
  484. 'opts': self.model._meta,
  485. 'title': 'Stopper une allocation',
  486. 'object': resource,
  487. 'media': self.media,
  488. 'form': form,
  489. })
  490. return TemplateResponse(request, "admin/services/ipresource/stop_allocation.html", context)
  491. def get_urls(self):
  492. my_urls = [
  493. path('<resource>/stop/', self.admin_site.admin_view(self.stop_allocation), name='stop-allocation'),
  494. ]
  495. return my_urls + super().get_urls()
  496. def get_actions(self, request):
  497. actions = super().get_actions(request)
  498. if 'delete_selected' in actions:
  499. del actions['delete_selected']
  500. return actions
  501. def has_add_permission(self, request, obj=None):
  502. return False
  503. def has_delete_permission(self, request, obj=None):
  504. return False
  505. class IPResourceStateAdmin(admin.ModelAdmin):
  506. list_display = ('ip', 'date', 'state',)
  507. def get_actions(self, request):
  508. actions = super().get_actions(request)
  509. if 'delete_selected' in actions:
  510. del actions['delete_selected']
  511. return actions
  512. def has_add_permission(self, request, obj=None):
  513. return False
  514. def has_change_permission(self, request, obj=None):
  515. return False
  516. def has_delete_permission(self, request, obj=None):
  517. return False
  518. class RouteAdmin(admin.ModelAdmin):
  519. list_display = ('name',)
  520. search_fields = ('name',)
  521. def get_fieldsets(self, request, obj=None):
  522. if obj:
  523. return (
  524. (None, {'fields': ['name']}),
  525. ('Adhérent·e·s', {'fields': ['get_adh'], 'classes': ['collapse']}),
  526. ('E-mails', {'fields': ['get_email'], 'classes': ['collapse']}),
  527. ('SMS', {'fields': ['get_sms'], 'classes': ['collapse']}),
  528. ('IP', {'fields': ['get_ip'], 'classes': ['collapse']}),
  529. )
  530. else:
  531. return (
  532. (None, {'fields': ['name']}),
  533. )
  534. def get_readonly_fields(self, request, obj=None):
  535. if obj:
  536. return ('get_email', 'get_sms', 'get_ip', 'get_adh',)
  537. else:
  538. return ()
  539. def get_email(self, route):
  540. return '\n'.join(route.get_email())
  541. get_email.short_description = 'E-mails'
  542. def get_sms(self, route):
  543. sms_filter = lambda x: x[:2] == '06' or x[:2] == '07' or x[:3] == '+336' or x[:3] == '+337'
  544. return '\n'.join(filter(sms_filter, route.get_tel()))
  545. get_sms.short_description = 'SMS'
  546. def get_ip(self, route):
  547. return '\n'.join(route.get_ip())
  548. get_ip.short_description = 'IP'
  549. def get_adh(self, route):
  550. return '\n'.join(map(lambda adh: '%s %s' % (adh, adh.adherent), route.get_adh()))
  551. get_adh.short_description = 'Adhérent·e·s'
  552. def get_actions(self, request):
  553. actions = super().get_actions(request)
  554. if 'delete_selected' in actions:
  555. del actions['delete_selected']
  556. return actions
  557. def has_delete_permission(self, request, obj=None):
  558. if obj:
  559. if obj.allocations.exists():
  560. return False
  561. return True
  562. return False
  563. class TunnelAdmin(admin.ModelAdmin):
  564. list_display = ('name', 'description', 'created', 'active')
  565. list_filter = (
  566. ActiveTunnelFilter,
  567. )
  568. def get_actions(self, request):
  569. actions = super().get_actions(request)
  570. if 'delete_selected' in actions:
  571. del actions['delete_selected']
  572. return actions
  573. def has_delete_permission(self, request, obj=None):
  574. if obj:
  575. if obj.allocations.exists():
  576. return False
  577. return True
  578. return False
  579. def active(self, obj):
  580. return not obj.ended
  581. active.short_description = 'Actif'
  582. active.boolean = True
  583. class ServiceTypeAdmin(admin.ModelAdmin):
  584. fields = ('name', 'contact')
  585. def get_actions(self, request):
  586. actions = super().get_actions(request)
  587. if 'delete_selected' in actions:
  588. del actions['delete_selected']
  589. return actions
  590. def has_delete_permission(self, request, obj=None):
  591. return False
  592. class ActiveAntennaLayer(GeoJSONLayerView):
  593. def get_queryset(self):
  594. return Antenna.objects.filter(get_active_filter('allocation'))
  595. class AntennaAdmin(admin.ModelAdmin):
  596. #inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
  597. list_filter = (
  598. AntennaPrefixFilter,
  599. AntennaPositionFilter,
  600. 'mode',
  601. 'ssid',
  602. )
  603. list_display_links = ('id', 'label')
  604. search_fields = ('=id', 'label', 'notes', 'ssid')
  605. raw_id_fields = ('contact',)
  606. form = AntennaForm
  607. def get_queryset(self, request):
  608. qs = super().get_queryset(request)
  609. if connection.vendor == 'postgresql':
  610. qs = qs.annotate(
  611. ip=StringAgg( # concaténation des IP avec des virgules directement par postgresql
  612. Cast( # casting en TextField car StringApp oppère sur des string mais les ip sont des inet
  613. models.Case( # seulement les IP des allocations actives
  614. models.When(
  615. get_active_filter('allocation'),
  616. then='allocation__resource__ip'
  617. ),
  618. ),
  619. models.TextField()
  620. ),
  621. delimiter=', '
  622. )
  623. )
  624. return qs
  625. def get_list_display(self, request):
  626. # ssid_display needs request to access query string and preserve filters
  627. ssid_display = partial(self.ssid_display, request=request)
  628. update_wrapper(ssid_display, self.ssid_display)
  629. return ('id', 'label', 'mode', ssid_display, 'position_display', 'ip_display')
  630. def position_display(self, obj):
  631. return obj.position is not None
  632. position_display.short_description = 'Géolocalisé'
  633. position_display.boolean = True
  634. def ip_display(self, obj):
  635. if connection.vendor == 'postgresql':
  636. return obj.ip
  637. else:
  638. # peu efficace car génère une requête par ligne
  639. allocations = obj.allocations.filter(active=True)
  640. return ', '.join(allocations.values_list('resource__ip', flat=True)) or '-'
  641. ip_display.short_description = 'IP'
  642. def ssid_display(self, obj, request):
  643. if obj.ssid:
  644. qs = request.GET.copy()
  645. qs.update({'ssid': obj.ssid})
  646. ssid_url = reverse('admin:services_antenna_changelist') + '?' + urlencode(qs)
  647. return format_html(u'<a href="{}">{}</a>', ssid_url, obj.ssid)
  648. else:
  649. return None
  650. ssid_display.short_description = 'SSID'
  651. def get_actions(self, request):
  652. actions = super().get_actions(request)
  653. if 'delete_selected' in actions:
  654. del actions['delete_selected']
  655. return actions
  656. def has_delete_permission(self, request, obj=None):
  657. return False
  658. def get_urls(self):
  659. my_urls = [
  660. url(r'^map/$', self.admin_site.admin_view(self.map_view, cacheable=True), name='antenna-map'),
  661. url(r'^map/data.json$', self.admin_site.admin_view(ActiveAntennaLayer.as_view(
  662. model=Antenna,
  663. geometry_field='position',
  664. properties=('label', 'mode', 'ssid', 'orientation', 'absolute_url',),
  665. )), name='antenna-map-data'),
  666. ]
  667. return my_urls + super().get_urls()
  668. def map_view(self, request):
  669. return TemplateResponse(request, 'services/antenna_map.html', dict(
  670. self.admin_site.each_context(request),
  671. opts=self.model._meta,
  672. json_url=reverse('admin:antenna-map-data'),
  673. ))
  674. def map_data_view(self, request):
  675. geojson = serialize('geojson', Antenna.objects.all(), geometry_field='point', fields=('position',))
  676. return HttpResponse(geojson, content_type='application/json')
  677. class SwitchAdmin(admin.ModelAdmin):
  678. list_display = ('name', 'ports_count', 'active_ports_count', 'inactive_ports_count', 'unknown_ports_count',)
  679. fields = ('name', 'first_port', 'last_port', 'notes',)
  680. search_fields = ('name', 'notes', 'ports__notes', 'ports__service__label',)
  681. def get_queryset(self, request):
  682. qs = super().get_queryset(request)
  683. qs = qs.annotate(
  684. active_ports_count=models.Count(models.Case(models.When(ports__up=True, then=models.Value('1')))),
  685. unknown_ports_count=models.Count(models.Case(models.When(ports__up__isnull=True, then=models.Value('1')))),
  686. inactive_ports_count=models.Count(models.Case(models.When(ports__up=False, then=models.Value('1')))),
  687. )
  688. return qs
  689. def ports_count(self, switch):
  690. return switch.last_port - switch.first_port + 1
  691. ports_count.short_description = 'Nombre de ports'
  692. def active_ports_count(self, switch):
  693. return switch.active_ports_count
  694. active_ports_count.short_description = 'up'
  695. active_ports_count.admin_order_field = 'active_ports_count'
  696. def inactive_ports_count(self, switch):
  697. return switch.inactive_ports_count
  698. inactive_ports_count.short_description = 'down'
  699. inactive_ports_count.admin_order_field = 'inactive_ports_count'
  700. def unknown_ports_count(self, switch):
  701. return switch.unknown_ports_count
  702. unknown_ports_count.short_description = 'inconnus'
  703. unknown_ports_count.admin_order_field = 'unknown_ports_count'
  704. def get_inline_instances(self, request, obj=None):
  705. if obj:
  706. return [ SwitchPortInline(self.model, self.admin_site) ]
  707. else:
  708. return []
  709. def get_readonly_fields(self, request, obj=None):
  710. if obj:
  711. return ('first_port', 'last_port',)
  712. else:
  713. return ()
  714. def get_actions(self, request):
  715. actions = super().get_actions(request)
  716. if 'delete_selected' in actions:
  717. del actions['delete_selected']
  718. return actions
  719. def has_delete_permission(self, request, obj=None):
  720. return False
  721. admin.site.register(ServiceType, ServiceTypeAdmin)
  722. admin.site.register(Service, ServiceAdmin)
  723. admin.site.register(IPPrefix, IPPrefixAdmin)
  724. admin.site.register(IPResource, IPResourceAdmin)
  725. admin.site.register(Route, RouteAdmin)
  726. admin.site.register(Tunnel, TunnelAdmin)
  727. admin.site.register(Antenna, AntennaAdmin)
  728. admin.site.register(Switch, SwitchAdmin)