admin.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  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
  7. from django.utils.html import format_html
  8. from django.core.mail import mail_managers
  9. from django.conf.urls import url
  10. from django.template.response import TemplateResponse
  11. from django.core.serializers import serialize
  12. from django.http import HttpResponse
  13. from django.db.models.functions import Cast
  14. from django.contrib.postgres.aggregates import StringAgg
  15. from django.db import connection
  16. from django.core.cache import cache
  17. from django.contrib.humanize.templatetags.humanize import naturaltime
  18. from djgeojson.views import GeoJSONLayerView
  19. from urllib.parse import urlencode
  20. from functools import partial, update_wrapper
  21. from datetime import timedelta
  22. from djadhere.utils import get_active_filter
  23. from adhesions.models import Adhesion
  24. from banking.models import PaymentUpdate
  25. from .models import Service, ServiceType, IPPrefix, IPResource, Route, Tunnel, \
  26. ServiceAllocation, Antenna, AntennaAllocation, Allocation, \
  27. Switch, Port
  28. from .utils import notify_allocation
  29. from .forms import AntennaForm
  30. ### Filters
  31. class ResourceInUseFilter(admin.SimpleListFilter):
  32. title = 'disponibilité'
  33. parameter_name = 'available'
  34. def lookups(self, request, model_admin):
  35. return (
  36. (1, 'Disponible'),
  37. (0, 'Non disponible'),
  38. )
  39. def queryset(self, request, queryset):
  40. available_filter = Q(reserved=False, in_use=False)
  41. if self.value() == '0': # non disponible
  42. return queryset.exclude(available_filter)
  43. if self.value() == '1': # disponible
  44. return queryset.filter(available_filter)
  45. class ResourcePingFilter(admin.SimpleListFilter):
  46. title = 'ping'
  47. parameter_name = 'ping'
  48. def lookups(self, request, model_admin):
  49. return (
  50. ('up', 'Up'),
  51. ('down', 'Down'),
  52. ('unknown', 'Inconnu'),
  53. )
  54. def queryset(self, request, queryset):
  55. known_filter = models.Q(last_time_up__isnull=False, last_check__isnull=False)
  56. if self.value() == 'unknown':
  57. return queryset.exclude(known_filter)
  58. else:
  59. queryset = queryset.filter(known_filter)
  60. if self.value() == 'up':
  61. return queryset.filter(last_check__lte=models.F('last_time_up'))
  62. if self.value() == 'down':
  63. return queryset.filter(last_check__gt=models.F('last_time_up'))
  64. class ActiveServiceFilter(admin.SimpleListFilter):
  65. title = 'actif'
  66. parameter_name = 'active'
  67. def lookups(self, request, model_admin):
  68. return (
  69. (1, 'Actif'),
  70. (0, 'Inactif'),
  71. )
  72. def queryset(self, request, queryset):
  73. if self.value() == '0': # inactif
  74. return queryset.exclude(get_active_filter('allocation'))
  75. if self.value() == '1': # actif
  76. return queryset.filter(get_active_filter('allocation'))
  77. class RouteFilter(admin.SimpleListFilter):
  78. title = 'route'
  79. parameter_name = 'route'
  80. def lookups(self, request, model_admin):
  81. return ServiceAllocation.objects.filter(active=True).values_list('route__pk', 'route__name').distinct()
  82. def queryset(self, request, queryset):
  83. try:
  84. route = int(self.value())
  85. except (TypeError, ValueError):
  86. pass
  87. else:
  88. allocations = ServiceAllocation.objects.filter(active=True, route__pk=route).values_list('pk', flat=True)
  89. queryset = queryset.filter(service_allocation__in=allocations)
  90. return queryset
  91. class AntennaPrefixFilter(admin.SimpleListFilter):
  92. title = 'préfix'
  93. parameter_name = 'prefix'
  94. def lookups(self, request, model_admin):
  95. resources = AntennaAllocation.objects.filter(active=True).values_list('resource__pk', flat=True)
  96. prefixes = IPPrefix.objects.filter(ipresource__in=resources).values_list('pk', 'prefix').distinct()
  97. return prefixes
  98. def queryset(self, request, queryset):
  99. try:
  100. prefix = int(self.value())
  101. except (TypeError, ValueError):
  102. pass
  103. else:
  104. allocations = AntennaAllocation.objects.filter(active=True, resource__prefixes__pk=prefix).values_list('pk', flat=True)
  105. queryset = queryset.filter(allocation__in=allocations)
  106. return queryset
  107. class AntennaPositionFilter(admin.SimpleListFilter):
  108. title = 'géolocalisation'
  109. parameter_name = 'position'
  110. def lookups(self, request, model_admin):
  111. return (
  112. ('1', 'Connue'),
  113. ('0', 'Inconnue'),
  114. )
  115. def queryset(self, request, queryset):
  116. query = Q(position__isnull=True)
  117. if self.value() == '0':
  118. return queryset.filter(query)
  119. if self.value() == '1':
  120. return queryset.exclude(query)
  121. return queryset
  122. class ActiveTunnelFilter(admin.SimpleListFilter):
  123. title = 'status'
  124. parameter_name = 'active'
  125. def lookups(self, request, model_admin):
  126. return (
  127. ('1', 'Actif'),
  128. ('0', 'Désactivé'),
  129. )
  130. def queryset(self, request, queryset):
  131. query = Q(ended__isnull=True)
  132. if self.value() == '0':
  133. return queryset.exclude(query)
  134. if self.value() == '1':
  135. return queryset.filter(query)
  136. return queryset
  137. ### Inlines
  138. class AllocationInlineFormSet(BaseInlineFormSet):
  139. def save_new(self, form, commit=True):
  140. obj = super().save_new(form, commit)
  141. if type(obj) == ServiceAllocation:
  142. notify_allocation(self.request, obj)
  143. return obj
  144. def save_existing(self, form, instance, commit=True):
  145. old = type(instance).objects.get(pk=instance.pk)
  146. if type(instance) == ServiceAllocation:
  147. notify_allocation(self.request, instance, old)
  148. return super().save_existing(form, instance, commit)
  149. class AllocationInline(admin.TabularInline):
  150. formset = AllocationInlineFormSet
  151. extra = 0
  152. show_change_link = True
  153. def get_queryset(self, request):
  154. qs = super().get_queryset(request)
  155. qs = qs.select_related('resource', 'route')
  156. return qs
  157. def get_formset(self, request, obj=None, **kwargs):
  158. formset = super().get_formset(request, obj, **kwargs)
  159. formset.request = request
  160. return formset
  161. def has_delete_permission(self, request, obj=None):
  162. return False
  163. class ActiveAllocationMixin:
  164. verbose_name_plural = 'Allocations actives'
  165. def get_max_num(self, request, obj=None, **kwargs):
  166. existing = obj.allocations.count() if obj else 0
  167. # pour simplifier la validation, on ajoute qu’une allocation à la fois
  168. # il faudrait surcharger la méthode clean du formset pour supprimer cette limite
  169. return existing + 1
  170. def get_queryset(self, request):
  171. return super().get_queryset(request).filter(get_active_filter())
  172. class InactiveAllocationMixin:
  173. verbose_name_plural = 'Anciennes allocations'
  174. max_num = 0
  175. def get_queryset(self, request):
  176. return super().get_queryset(request).exclude(get_active_filter())
  177. class ServiceAllocationMixin:
  178. model = ServiceAllocation
  179. fields = ('id', 'service', 'resource', 'route', 'start', 'end')
  180. raw_id_fields = ('service', 'resource',)
  181. class AntennaAllocationMixin:
  182. model = AntennaAllocation
  183. fields = ('id', 'antenna', 'resource', 'start', 'end')
  184. raw_id_fields = ('antenna', 'resource',)
  185. class ActiveServiceAllocationInline(ServiceAllocationMixin, ActiveAllocationMixin, AllocationInline):
  186. pass
  187. class InactiveServiceAllocationInline(ServiceAllocationMixin, InactiveAllocationMixin, AllocationInline):
  188. pass
  189. class ActiveAntennaAllocationInline(AntennaAllocationMixin, ActiveAllocationMixin, AllocationInline):
  190. pass
  191. class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocationMixin, AllocationInline):
  192. pass
  193. class PortInline(admin.TabularInline):
  194. model = Port
  195. def has_add_permission(self, request):
  196. return False
  197. def has_delete_permission(self, request, obj=None):
  198. return False
  199. class SwitchPortInline(PortInline):
  200. fields = ('port', 'reserved', 'service', 'notes',)
  201. readonly_fields = ('port',)
  202. raw_id_fields = ('service',)
  203. def get_queryset(self, request):
  204. qs = super().get_queryset(request)
  205. qs = qs.select_related('switch', 'service', 'service__service_type')
  206. return qs
  207. class ServicePortInline(PortInline):
  208. fields = ('switch', 'port', 'notes',)
  209. readonly_fields = ('switch', 'port',)
  210. max_num = 0
  211. ### Actions
  212. def ends_resource(resource, request, queryset):
  213. now = timezone.now()
  214. queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
  215. # TODO: send mail
  216. ends_resource.short_description = 'Terminer les allocations sélectionnées'
  217. ### ModelAdmin
  218. class ServiceAdmin(admin.ModelAdmin):
  219. list_display = ('id', 'get_adhesion_link', 'get_adherent_link', 'service_type', 'label', 'is_active',)
  220. list_select_related = ('adhesion', 'adhesion__user', 'adhesion__user__profile', 'adhesion__corporation', 'service_type',)
  221. list_filter = (
  222. ActiveServiceFilter,
  223. ('service_type', admin.RelatedOnlyFieldListFilter),
  224. )
  225. search_fields = ('=id', 'service_type__name', 'label', 'notes',)
  226. fields = ('adhesion', 'service_type', 'label', 'notes', 'get_contribution_link', 'is_active',)
  227. readonly_fields = ('get_contribution_link', 'is_active',)
  228. raw_id_fields = ('adhesion',)
  229. def get_queryset(self, request):
  230. qs = super().get_queryset(request)
  231. qs = qs.prefetch_related('allocations',)
  232. return qs
  233. get_adhesion_link = lambda self, service: service.adhesion.get_adhesion_link()
  234. get_adhesion_link.short_description = Adhesion.get_adhesion_link.short_description
  235. get_adherent_link = lambda self, service: service.adhesion.get_adherent_link()
  236. get_adherent_link.short_description = Adhesion.get_adherent_link.short_description
  237. def get_contribution_link(self, obj):
  238. return format_html(u'<a href="{}">{}</a>', obj.contribution.get_absolute_url(), obj.contribution)
  239. get_contribution_link.short_description = 'Contribution financière'
  240. def get_inline_instances(self, request, obj=None):
  241. inlines = []
  242. if obj and obj.ports.exists():
  243. inlines += [ServicePortInline]
  244. inlines += [ActiveServiceAllocationInline, InactiveServiceAllocationInline]
  245. return [inline(self.model, self.admin_site) for inline in inlines]
  246. def get_actions(self, request):
  247. actions = super().get_actions(request)
  248. if 'delete_selected' in actions:
  249. del actions['delete_selected']
  250. return actions
  251. def has_delete_permission(self, request, obj=None):
  252. if not obj:
  253. return False
  254. one_year_ago = timezone.now() - timedelta(days=365)
  255. contribution = obj.contribution.updates.filter(validated=True).first()
  256. # s’il y avait un paiement actif il y a moins d’un an
  257. if not contribution or contribution.payment_method != PaymentUpdate.STOP or contribution.start > one_year_ago:
  258. return False
  259. # s’il y avait une allocation active il y a moins d’un an
  260. if any(map(lambda a: a.end is None or a.end > one_year_ago, obj.allocations.all())):
  261. return False
  262. return True
  263. class IPPrefixAdmin(admin.ModelAdmin):
  264. readonly_fields = ('prefix',)
  265. def has_delete_permission(self, request, obj=None):
  266. # Interdiction de supprimer le préfix s’il est assigné à une route
  267. return obj and obj.tunnel_set.exists()
  268. # pour embêcher de by-passer le check has_delete_permission, on désactive l’action delete
  269. def get_actions(self, request):
  270. actions = super().get_actions(request)
  271. if 'delete_selected' in actions:
  272. del actions['delete_selected']
  273. return actions
  274. class IPResourceAdmin(admin.ModelAdmin):
  275. list_display = ('__str__', 'available_display', 'last_use', 'ping',)
  276. list_filter = (
  277. 'category',
  278. ResourceInUseFilter,
  279. ResourcePingFilter,
  280. 'reserved',
  281. ('prefixes', admin.RelatedOnlyFieldListFilter),
  282. RouteFilter,
  283. )
  284. search_fields = ('=ip', 'notes',)
  285. def get_fields(self, request, obj=None):
  286. return self.get_readonly_fields(request, obj) + ['notes']
  287. def get_readonly_fields(self, request, obj=None):
  288. fields = ['ip']
  289. if obj and obj.reserved:
  290. fields += ['reserved']
  291. if obj and not obj.in_use:
  292. fields += ['last_use']
  293. if obj and obj.last_time_up and obj.last_check:
  294. fields += ['last_time_up', 'last_check']
  295. if obj and obj.checkmk_label:
  296. fields += ['checkmk']
  297. return fields
  298. def get_inline_instances(self, request, obj=None):
  299. if obj:
  300. if obj.category == 0:
  301. inlines = (ActiveServiceAllocationInline, InactiveServiceAllocationInline,)
  302. elif obj.category == 1:
  303. inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
  304. else:
  305. inlines = ()
  306. return [inline(self.model, self.admin_site) for inline in inlines]
  307. def get_queryset(self, request):
  308. qs = super().get_queryset(request)
  309. qs = qs.annotate(last_use=models.Case(
  310. models.When(category=0, then=models.Max('service_allocation__end')),
  311. models.When(category=1, then=models.Max('antenna_allocation__end')),
  312. default=None,
  313. ))
  314. qs = qs.annotate(downtime=Cast(
  315. models.Case(
  316. models.When(last_check__isnull=False, last_time_up__isnull=False, last_check__lte=models.F('last_time_up'), then=timedelta(days=0)),
  317. models.When(last_check__isnull=False, last_time_up__isnull=False, then=models.F('last_check') - models.F('last_time_up')),
  318. default=None,
  319. ),
  320. models.DurationField(),
  321. ))
  322. return qs
  323. def available_display(self, obj):
  324. return not obj.reserved and not obj.in_use
  325. available_display.short_description = 'Disponible'
  326. available_display.boolean = True
  327. def last_use(self, obj):
  328. if obj.last_use:
  329. return naturaltime(obj.last_use)
  330. else:
  331. return '-'
  332. last_use.short_description = 'Dernière utilisation'
  333. last_use.admin_order_field = 'last_use'
  334. def ping(self, obj):
  335. if obj.downtime:
  336. label = 'down depuis >' + str(obj.downtime)
  337. elif obj.downtime == timedelta(days=0):
  338. label = 'UP'
  339. else:
  340. return None
  341. if obj.checkmk_url:
  342. return format_html('<a href="{}">{}</a>', obj.checkmk_url, label)
  343. else:
  344. return label
  345. ping.short_description = 'ping'
  346. ping.admin_order_field = 'downtime'
  347. def checkmk(self, obj):
  348. return format_html('<a href="{}">{}</a>', obj.checkmk_url, 'voir')
  349. checkmk.short_description = 'CheckMK'
  350. def get_actions(self, request):
  351. actions = super().get_actions(request)
  352. if 'delete_selected' in actions:
  353. del actions['delete_selected']
  354. return actions
  355. def has_add_permission(self, request, obj=None):
  356. return False
  357. def has_delete_permission(self, request, obj=None):
  358. return False
  359. class RouteAdmin(admin.ModelAdmin):
  360. list_display = ('name',)
  361. def get_fields(self, request, obj=None):
  362. if obj:
  363. return ('name', 'get_emails', 'get_sms', 'get_routed_ip', 'get_adh',)
  364. else:
  365. return ('name',)
  366. def get_readonly_fields(self, request, obj=None):
  367. if obj:
  368. return ('get_emails', 'get_sms', 'get_routed_ip', 'get_adh',)
  369. else:
  370. return ()
  371. def get_contacts(self, route):
  372. cache_emails_key = 'route-%d-emails' % route.pk
  373. cache_sms_key = 'route-%d-sms' % route.pk
  374. cache_adh_key = 'route-%d-adh' % route.pk
  375. cache_results = cache.get_many([cache_emails_key, cache_sms_key, cache_adh_key])
  376. if len(cache_results) == 3:
  377. return (cache_results[cache_emails_key], cache_results[cache_sms_key], cache_results[cache_adh_key])
  378. allocations = route.allocations \
  379. .order_by('service__adhesion__pk') \
  380. .distinct('service__adhesion__pk') \
  381. .select_related(
  382. 'service__adhesion__user',
  383. 'service__adhesion__corporation',
  384. )
  385. emails, sms, adh = [], [], []
  386. for allocation in allocations:
  387. adhesion = allocation.service.adhesion
  388. adherent = adhesion.adherent
  389. if adhesion.pk not in adh:
  390. adh.append(adhesion.pk)
  391. if adherent.email:
  392. email = '%s <%s>' % (str(adherent), adherent.email)
  393. if email not in emails:
  394. emails.append(email)
  395. # S’il s’agit d’une raison sociale, on contact aussi les gestionnaires
  396. if adhesion.corporation:
  397. if adhesion.corporation.phone_number:
  398. sms.append(adhesion.corporation.phone_number)
  399. for member in adhesion.corporation.members.all():
  400. if member.email:
  401. email = '%s <%s>' % (str(member), member.email)
  402. if email not in emails:
  403. emails.append(email)
  404. else: # user
  405. if adhesion.user.profile.phone_number:
  406. if adhesion.user.profile.phone_number not in sms:
  407. sms.append(adhesion.user.profile.phone_number)
  408. sms = list(filter(lambda x: x[:2] == '06' or x[:2] == '07' or x[:3] == '+336' or x[:3] == '+337', sms))
  409. cache.set_many({cache_emails_key: emails, cache_sms_key: sms, cache_adh_key: adh}, timeout=3600)
  410. return (emails, sms, adh)
  411. def get_emails(self, route):
  412. emails, _, _ = self.get_contacts(route)
  413. return '\n'.join(emails)
  414. get_emails.short_description = 'Contacts'
  415. def get_sms(self, route):
  416. _, sms, _ = self.get_contacts(route)
  417. return '\n'.join(sms)
  418. get_sms.short_description = 'SMS'
  419. def get_adh(self, route):
  420. _, _, adh = self.get_contacts(route)
  421. return '\n'.join(map(lambda a: 'ADT%d' % a, sorted(adh)))
  422. get_adh.short_description = 'Adhérents'
  423. def get_routed_ip(self, route):
  424. routed_ip = route.allocations.order_by('resource__ip').values_list('resource__ip', flat=True)
  425. return '\n'.join(routed_ip)
  426. get_routed_ip.short_description = 'IP routées'
  427. def get_actions(self, request):
  428. actions = super().get_actions(request)
  429. if 'delete_selected' in actions:
  430. del actions['delete_selected']
  431. return actions
  432. def has_delete_permission(self, request, obj=None):
  433. return False
  434. class TunnelAdmin(admin.ModelAdmin):
  435. list_display = ('name', 'description', 'created', 'active')
  436. list_filter = (
  437. ActiveTunnelFilter,
  438. )
  439. def active(self, obj):
  440. return not obj.ended
  441. active.short_description = 'Actif'
  442. active.boolean = True
  443. class ServiceTypeAdmin(admin.ModelAdmin):
  444. fields = ('name',)
  445. def get_actions(self, request):
  446. actions = super().get_actions(request)
  447. if 'delete_selected' in actions:
  448. del actions['delete_selected']
  449. return actions
  450. def has_delete_permission(self, request, obj=None):
  451. return False
  452. class ActiveAntennaLayer(GeoJSONLayerView):
  453. def get_queryset(self):
  454. return Antenna.objects.filter(get_active_filter('allocation'))
  455. class AntennaAdmin(admin.ModelAdmin):
  456. inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
  457. list_filter = (
  458. AntennaPrefixFilter,
  459. AntennaPositionFilter,
  460. 'mode',
  461. 'ssid',
  462. )
  463. list_display_links = ('id', 'label')
  464. search_fields = ('=id', 'label', 'notes', 'ssid')
  465. raw_id_fields = ('contact',)
  466. form = AntennaForm
  467. def get_queryset(self, request):
  468. qs = super().get_queryset(request)
  469. if connection.vendor == 'postgresql':
  470. qs = qs.annotate(
  471. ip=StringAgg( # concaténation des IP avec des virgules directement par postgresql
  472. Cast( # casting en TextField car StringApp oppère sur des string mais les ip sont des inet
  473. models.Case( # seulement les IP des allocations actives
  474. models.When(
  475. get_active_filter('allocation'),
  476. then='allocation__resource__ip'
  477. ),
  478. ),
  479. models.TextField()
  480. ),
  481. delimiter=', '
  482. )
  483. )
  484. return qs
  485. def get_list_display(self, request):
  486. # ssid_display needs request to access query string and preserve filters
  487. ssid_display = partial(self.ssid_display, request=request)
  488. update_wrapper(ssid_display, self.ssid_display)
  489. return ('id', 'label', 'mode', ssid_display, 'position_display', 'ip_display')
  490. def position_display(self, obj):
  491. return obj.position is not None
  492. position_display.short_description = 'Géolocalisé'
  493. position_display.boolean = True
  494. def ip_display(self, obj):
  495. if connection.vendor == 'postgresql':
  496. return obj.ip
  497. else:
  498. # peu efficace car génère une requête par ligne
  499. allocations = obj.allocations.filter(active=True)
  500. return ', '.join(allocations.values_list('resource__ip', flat=True)) or '-'
  501. ip_display.short_description = 'IP'
  502. def ssid_display(self, obj, request):
  503. if obj.ssid:
  504. qs = request.GET.copy()
  505. qs.update({'ssid': obj.ssid})
  506. ssid_url = reverse('admin:services_antenna_changelist') + '?' + urlencode(qs)
  507. return format_html(u'<a href="{}">{}</a>', ssid_url, obj.ssid)
  508. else:
  509. return None
  510. ssid_display.short_description = 'SSID'
  511. def get_actions(self, request):
  512. actions = super().get_actions(request)
  513. if 'delete_selected' in actions:
  514. del actions['delete_selected']
  515. return actions
  516. def has_delete_permission(self, request, obj=None):
  517. return False
  518. def get_urls(self):
  519. my_urls = [
  520. url(r'^map/$', self.admin_site.admin_view(self.map_view, cacheable=True), name='antenna-map'),
  521. url(r'^map/data.json$', self.admin_site.admin_view(ActiveAntennaLayer.as_view(
  522. model=Antenna,
  523. geometry_field='position',
  524. properties=('label', 'mode', 'ssid', 'orientation', 'absolute_url',),
  525. )), name='antenna-map-data'),
  526. ]
  527. return my_urls + super().get_urls()
  528. def map_view(self, request):
  529. return TemplateResponse(request, 'services/antenna_map.html', {
  530. 'json_url': reverse('admin:antenna-map-data'),
  531. })
  532. def map_data_view(self, request):
  533. geojson = serialize('geojson', Antenna.objects.all(), geometry_field='point', fields=('position',))
  534. return HttpResponse(geojson, content_type='application/json')
  535. class SwitchAdmin(admin.ModelAdmin):
  536. list_display = ('name', 'size',)
  537. fields = ('name', 'size', 'notes',)
  538. inlines = (SwitchPortInline,)
  539. def get_readonly_fields(self, request, obj=None):
  540. if obj:
  541. return ('size',)
  542. else:
  543. return ()
  544. def get_actions(self, request):
  545. actions = super().get_actions(request)
  546. if 'delete_selected' in actions:
  547. del actions['delete_selected']
  548. return actions
  549. def has_delete_permission(self, request, obj=None):
  550. return False
  551. admin.site.register(ServiceType, ServiceTypeAdmin)
  552. admin.site.register(Service, ServiceAdmin)
  553. admin.site.register(IPPrefix, IPPrefixAdmin)
  554. admin.site.register(IPResource, IPResourceAdmin)
  555. admin.site.register(Route, RouteAdmin)
  556. admin.site.register(Tunnel, TunnelAdmin)
  557. admin.site.register(Antenna, AntennaAdmin)
  558. admin.site.register(Switch, SwitchAdmin)