admin.py 16 KB

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