admin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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.core.urlresolvers 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 urllib.parse import urlencode
  18. from djgeojson.views import GeoJSONLayerView
  19. from functools import partial, update_wrapper
  20. from djadhere.utils import get_active_filter
  21. from adhesions.models import Adhesion
  22. from .models import Service, ServiceType, IPPrefix, IPResource, Route, Tunnel, ServiceAllocation, Antenna, AntennaAllocation, Allocation
  23. from .utils import notify_allocation
  24. from .forms import AntennaForm
  25. ### Filters
  26. class ResourceInUseFilter(admin.SimpleListFilter):
  27. title = 'disponibilité'
  28. parameter_name = 'available'
  29. def lookups(self, request, model_admin):
  30. return (
  31. (1, 'Disponible'),
  32. (0, 'Non disponible'),
  33. )
  34. def queryset(self, request, queryset):
  35. available_filter = Q(reserved=False, in_use=False)
  36. if self.value() == '0': # non disponible
  37. return queryset.exclude(available_filter)
  38. if self.value() == '1': # disponible
  39. return queryset.filter(available_filter)
  40. class ActiveServiceFilter(admin.SimpleListFilter):
  41. title = 'actif'
  42. parameter_name = 'active'
  43. def lookups(self, request, model_admin):
  44. return (
  45. (1, 'Actif'),
  46. (0, 'Inactif'),
  47. )
  48. def queryset(self, request, queryset):
  49. if self.value() == '0': # inactif
  50. return queryset.filter(active=False)
  51. if self.value() == '1': # actif
  52. return queryset.filter(active=True)
  53. class RouteFilter(admin.SimpleListFilter):
  54. title = 'route'
  55. parameter_name = 'route'
  56. def lookups(self, request, model_admin):
  57. return ServiceAllocation.objects.filter(active=True).values_list('route__pk', 'route__name').distinct()
  58. def queryset(self, request, queryset):
  59. try:
  60. route = int(self.value())
  61. except (TypeError, ValueError):
  62. pass
  63. else:
  64. allocations = ServiceAllocation.objects.filter(active=True, route__pk=route).values_list('pk', flat=True)
  65. queryset = queryset.filter(service_allocation__in=allocations)
  66. return queryset
  67. class AntennaPrefixFilter(admin.SimpleListFilter):
  68. title = 'préfix'
  69. parameter_name = 'prefix'
  70. def lookups(self, request, model_admin):
  71. resources = AntennaAllocation.objects.filter(active=True).values_list('resource__pk', flat=True)
  72. prefixes = IPPrefix.objects.filter(ipresource__in=resources).values_list('pk', 'prefix').distinct()
  73. return prefixes
  74. def queryset(self, request, queryset):
  75. try:
  76. prefix = int(self.value())
  77. except (TypeError, ValueError):
  78. pass
  79. else:
  80. allocations = AntennaAllocation.objects.filter(active=True, resource__prefixes__pk=prefix).values_list('pk', flat=True)
  81. queryset = queryset.filter(allocation__in=allocations)
  82. return queryset
  83. class AntennaPositionFilter(admin.SimpleListFilter):
  84. title = 'géolocalisation'
  85. parameter_name = 'position'
  86. def lookups(self, request, model_admin):
  87. return (
  88. ('1', 'Connue'),
  89. ('0', 'Inconnue'),
  90. )
  91. def queryset(self, request, queryset):
  92. query = Q(position__isnull=True)
  93. if self.value() == '0':
  94. return queryset.filter(query)
  95. if self.value() == '1':
  96. return queryset.exclude(query)
  97. return queryset
  98. class ActiveTunnelFilter(admin.SimpleListFilter):
  99. title = 'status'
  100. parameter_name = 'active'
  101. def lookups(self, request, model_admin):
  102. return (
  103. ('1', 'Actif'),
  104. ('0', 'Désactivé'),
  105. )
  106. def queryset(self, request, queryset):
  107. query = Q(ended__isnull=True)
  108. if self.value() == '0':
  109. return queryset.exclude(query)
  110. if self.value() == '1':
  111. return queryset.filter(query)
  112. return queryset
  113. ### Inlines
  114. class AllocationInlineFormSet(BaseInlineFormSet):
  115. def save_new(self, form, commit=True):
  116. obj = super().save_new(form, commit)
  117. if type(obj) == ServiceAllocation:
  118. notify_allocation(self.request, obj)
  119. return obj
  120. def save_existing(self, form, instance, commit=True):
  121. old = type(instance).objects.get(pk=instance.pk)
  122. if type(instance) == ServiceAllocation:
  123. notify_allocation(self.request, instance, old)
  124. return super().save_existing(form, instance, commit)
  125. class AllocationInline(admin.TabularInline):
  126. formset = AllocationInlineFormSet
  127. extra = 0
  128. verbose_name_plural = 'Allocations'
  129. show_change_link = True
  130. def get_formset(self, request, obj=None, **kwargs):
  131. formset = super().get_formset(request, obj, **kwargs)
  132. formset.request = request
  133. return formset
  134. def get_max_num(self, request, obj=None, **kwargs):
  135. existing = obj.allocations.count() if obj else 0
  136. # pour simplifier la validation, on ajoute qu’une allocation à la fois
  137. # il faudrait surcharger la méthode clean du formset pour supprimer cette limite
  138. return existing + 1
  139. def has_delete_permission(self, request, obj=None):
  140. return False
  141. class ServiceAllocationInline(AllocationInline):
  142. model = ServiceAllocation
  143. fields = ('id', 'service', 'resource', 'route', 'start', 'end')
  144. raw_id_fields = ('service', 'resource',)
  145. class AntennaAllocationInline(AllocationInline):
  146. model = AntennaAllocation
  147. fields = ('id', 'antenna', 'resource', 'start', 'end')
  148. raw_id_fields = ('antenna', 'resource',)
  149. ### Actions
  150. def ends_resource(resource, request, queryset):
  151. now = timezone.now()
  152. queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
  153. # TODO: send mail
  154. ends_resource.short_description = 'Terminer les allocations sélectionnées'
  155. ### ModelAdmin
  156. class ServiceAdmin(admin.ModelAdmin):
  157. list_display = ('id', 'get_adhesion_link', 'get_adherent_link', 'service_type', 'label', 'is_active',)
  158. list_select_related = ('adhesion', 'adhesion__user', 'adhesion__user__profile', 'adhesion__corporation', 'service_type')
  159. list_filter = (
  160. ActiveServiceFilter,
  161. ('service_type', admin.RelatedOnlyFieldListFilter),
  162. )
  163. inlines = (ServiceAllocationInline,)
  164. search_fields = ('=id', 'service_type__name', 'label', 'notes',)
  165. fields = ('adhesion', 'service_type', 'label', 'notes', 'get_contribution_link', 'is_active',)
  166. readonly_fields = ('get_contribution_link', 'is_active',)
  167. raw_id_fields = ('adhesion',)
  168. get_adhesion_link = lambda self, service: service.adhesion.get_adhesion_link()
  169. get_adhesion_link.short_description = Adhesion.get_adhesion_link.short_description
  170. get_adherent_link = lambda self, service: service.adhesion.get_adherent_link()
  171. get_adherent_link.short_description = Adhesion.get_adherent_link.short_description
  172. def get_contribution_link(self, obj):
  173. return format_html(u'<a href="{}">{}</a>', obj.contribution.get_absolute_url(), obj.contribution.get_last_validated_update() or 'inconnue')
  174. get_contribution_link.short_description = 'Contribution financière'
  175. def get_actions(self, request):
  176. actions = super().get_actions(request)
  177. if 'delete_selected' in actions:
  178. del actions['delete_selected']
  179. return actions
  180. def has_delete_permission(self, request, obj=None):
  181. return False
  182. class IPPrefixAdmin(admin.ModelAdmin):
  183. readonly_fields = ('prefix',)
  184. def has_delete_permission(self, request, obj=None):
  185. # Interdiction de supprimer le préfix s’il est assigné à une route
  186. return obj and obj.tunnel_set.exists()
  187. # pour embêcher de by-passer le check has_delete_permission, on désactive l’action delete
  188. def get_actions(self, request):
  189. actions = super().get_actions(request)
  190. if 'delete_selected' in actions:
  191. del actions['delete_selected']
  192. return actions
  193. class IPResourceAdmin(admin.ModelAdmin):
  194. list_display = ('__str__', 'available_display', 'last_use', 'last_ping',)
  195. list_filter = (
  196. 'category',
  197. ResourceInUseFilter,
  198. 'reserved',
  199. ('prefixes', admin.RelatedOnlyFieldListFilter),
  200. RouteFilter,
  201. )
  202. fields = ('ip', 'reserved', 'notes')
  203. readonly_fields = ('ip', 'reserved',)
  204. search_fields = ('=ip', 'notes',)
  205. def get_inline_instances(self, request, obj=None):
  206. if obj:
  207. if obj.category == 0:
  208. inlines = (ServiceAllocationInline,)
  209. elif obj.category == 1:
  210. inlines = (AntennaAllocationInline,)
  211. else:
  212. inlines = ()
  213. return [inline(self.model, self.admin_site) for inline in inlines]
  214. def get_queryset(self, request):
  215. qs = super().get_queryset(request)
  216. qs = qs.annotate(last_use=models.Case(
  217. models.When(category=0, then=models.Max('service_allocation__end')),
  218. models.When(category=1, then=models.Max('antenna_allocation__end')),
  219. ))
  220. return qs
  221. def available_display(self, obj):
  222. return not obj.reserved and not obj.in_use
  223. available_display.short_description = 'Disponible'
  224. available_display.boolean = True
  225. def last_use(self, obj):
  226. return obj.last_use
  227. last_use.short_description = 'Dernière utilisation'
  228. last_use.admin_order_field = 'last_use'
  229. def last_ping(self, obj):
  230. return obj.last_ping
  231. last_ping.short_description = 'Dernier Ping'
  232. last_ping.admin_order_field = 'last_ping'
  233. def get_actions(self, request):
  234. actions = super().get_actions(request)
  235. if 'delete_selected' in actions:
  236. del actions['delete_selected']
  237. return actions
  238. def has_add_permission(self, request, obj=None):
  239. return False
  240. def has_delete_permission(self, request, obj=None):
  241. return False
  242. class RouteAdmin(admin.ModelAdmin):
  243. list_display = ('name',)
  244. def get_fields(self, request, obj=None):
  245. if obj:
  246. return ('name', 'get_emails', 'get_sms', 'get_routed_ip', 'get_adh',)
  247. else:
  248. return ('name',)
  249. def get_readonly_fields(self, request, obj=None):
  250. if obj:
  251. return ('get_emails', 'get_sms', 'get_routed_ip', 'get_adh',)
  252. else:
  253. return ()
  254. def get_contacts(self, route):
  255. cache_emails_key = 'route-%d-emails' % route.pk
  256. cache_sms_key = 'route-%d-sms' % route.pk
  257. cache_adh_key = 'route-%d-adh' % route.pk
  258. cache_results = cache.get_many([cache_emails_key, cache_sms_key, cache_adh_key])
  259. if len(cache_results) == 3:
  260. return (cache_results[cache_emails_key], cache_results[cache_sms_key], cache_results[cache_adh_key])
  261. allocations = route.allocations \
  262. .order_by('service__adhesion__pk') \
  263. .distinct('service__adhesion__pk') \
  264. .select_related(
  265. 'service__adhesion__user',
  266. 'service__adhesion__corporation',
  267. )
  268. emails, sms, adh = [], [], []
  269. for allocation in allocations:
  270. adhesion = allocation.service.adhesion
  271. adherent = adhesion.adherent
  272. if adhesion.pk not in adh:
  273. adh.append(adhesion.pk)
  274. if adherent.email:
  275. email = '%s <%s>' % (str(adherent), adherent.email)
  276. if email not in emails:
  277. emails.append(email)
  278. # S’il s’agit d’une raison sociale, on contact aussi les gestionnaires
  279. if adhesion.corporation:
  280. if adhesion.corporation.phone_number:
  281. sms.append(adhesion.corporation.phone_number)
  282. for member in adhesion.corporation.members.all():
  283. if member.email:
  284. email = '%s <%s>' % (str(member), member.email)
  285. if email not in emails:
  286. emails.append(email)
  287. else: # user
  288. if adhesion.user.profile.phone_number:
  289. if adhesion.user.profile.phone_number not in sms:
  290. sms.append(adhesion.user.profile.phone_number)
  291. sms = list(filter(lambda x: x[:2] == '06' or x[:2] == '07' or x[:3] == '+336' or x[:3] == '+337', sms))
  292. cache.set_many({cache_emails_key: emails, cache_sms_key: sms, cache_adh_key: adh}, timeout=3600)
  293. return (emails, sms, adh)
  294. def get_emails(self, route):
  295. emails, _, _ = self.get_contacts(route)
  296. return '\n'.join(emails)
  297. get_emails.short_description = 'Contacts'
  298. def get_sms(self, route):
  299. _, sms, _ = self.get_contacts(route)
  300. return '\n'.join(sms)
  301. get_sms.short_description = 'SMS'
  302. def get_adh(self, route):
  303. _, _, adh = self.get_contacts(route)
  304. return '\n'.join(map(lambda a: 'ADT%d' % a, sorted(adh)))
  305. get_adh.short_description = 'Adhérents'
  306. def get_routed_ip(self, route):
  307. routed_ip = route.allocations.order_by('resource__ip').values_list('resource__ip', flat=True)
  308. return '\n'.join(routed_ip)
  309. get_routed_ip.short_description = 'IP routées'
  310. def get_actions(self, request):
  311. actions = super().get_actions(request)
  312. if 'delete_selected' in actions:
  313. del actions['delete_selected']
  314. return actions
  315. def has_delete_permission(self, request, obj=None):
  316. return False
  317. class TunnelAdmin(admin.ModelAdmin):
  318. list_display = ('name', 'description', 'created', 'active')
  319. list_filter = (
  320. ActiveTunnelFilter,
  321. )
  322. def active(self, obj):
  323. return not obj.ended
  324. active.short_description = 'Actif'
  325. active.boolean = True
  326. class ServiceTypeAdmin(admin.ModelAdmin):
  327. fields = ('name',)
  328. def get_actions(self, request):
  329. actions = super().get_actions(request)
  330. if 'delete_selected' in actions:
  331. del actions['delete_selected']
  332. return actions
  333. def has_delete_permission(self, request, obj=None):
  334. return False
  335. class AntennaAdmin(admin.ModelAdmin):
  336. inlines = (AntennaAllocationInline,)
  337. list_filter = (
  338. AntennaPrefixFilter,
  339. AntennaPositionFilter,
  340. 'mode',
  341. 'ssid',
  342. )
  343. list_display_links = ('id', 'label')
  344. search_fields = ('=id', 'label', 'notes', 'ssid')
  345. raw_id_fields = ('contact',)
  346. form = AntennaForm
  347. def get_queryset(self, request):
  348. qs = super().get_queryset(request)
  349. if connection.vendor == 'postgresql':
  350. qs = qs.annotate(
  351. ip=StringAgg( # concaténation des IP avec des virgules directement par postgresql
  352. Cast( # casting en TextField car StringApp oppère sur des string mais les ip sont des inet
  353. models.Case( # seulement les IP des allocations actives
  354. models.When(
  355. get_active_filter('allocation'),
  356. then='allocation__resource__ip'
  357. ),
  358. ),
  359. models.TextField()
  360. ),
  361. delimiter=', '
  362. )
  363. )
  364. return qs
  365. def get_list_display(self, request):
  366. # ssid_display needs request to access query string and preserve filters
  367. ssid_display = partial(self.ssid_display, request=request)
  368. update_wrapper(ssid_display, self.ssid_display)
  369. return ('id', 'label', 'mode', ssid_display, 'position_display', 'ip_display')
  370. def position_display(self, obj):
  371. return obj.position is not None
  372. position_display.short_description = 'Géolocalisé'
  373. position_display.boolean = True
  374. def ip_display(self, obj):
  375. if connection.vendor == 'postgresql':
  376. return obj.ip
  377. else:
  378. # peu efficace car génère une requête par ligne
  379. allocations = obj.allocations.filter(active=True)
  380. return ', '.join(allocations.values_list('resource__ip', flat=True)) or '-'
  381. ip_display.short_description = 'IP'
  382. def ssid_display(self, obj, request):
  383. if obj.ssid:
  384. qs = request.GET.copy()
  385. qs.update({'ssid': obj.ssid})
  386. ssid_url = reverse('admin:services_antenna_changelist') + '?' + urlencode(qs)
  387. return format_html(u'<a href="{}">{}</a>', ssid_url, obj.ssid)
  388. else:
  389. return None
  390. ssid_display.short_description = 'SSID'
  391. def get_actions(self, request):
  392. actions = super().get_actions(request)
  393. if 'delete_selected' in actions:
  394. del actions['delete_selected']
  395. return actions
  396. def has_delete_permission(self, request, obj=None):
  397. return False
  398. def get_urls(self):
  399. my_urls = [
  400. url(r'^map/$', self.admin_site.admin_view(self.map_view, cacheable=True), name='antenna-map'),
  401. url(r'^map/data.json$', self.admin_site.admin_view(GeoJSONLayerView.as_view(
  402. model=Antenna,
  403. geometry_field='position',
  404. properties=('label', 'mode', 'ssid', 'orientation',),
  405. )), name='antenna-map-data'),
  406. ]
  407. return my_urls + super().get_urls()
  408. def map_view(self, request):
  409. return TemplateResponse(request, 'services/antenna_map.html', {
  410. 'json_url': reverse('admin:antenna-map-data'),
  411. })
  412. def map_data_view(self, request):
  413. geojson = serialize('geojson', Antenna.objects.all(), geometry_field='point', fields=('position',))
  414. return HttpResponse(geojson, content_type='application/json')
  415. admin.site.register(ServiceType, ServiceTypeAdmin)
  416. admin.site.register(Service, ServiceAdmin)
  417. admin.site.register(IPPrefix, IPPrefixAdmin)
  418. admin.site.register(IPResource, IPResourceAdmin)
  419. admin.site.register(Route, RouteAdmin)
  420. admin.site.register(Tunnel, TunnelAdmin)
  421. admin.site.register(Antenna, AntennaAdmin)