admin.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 djadhere.utils import get_active_filter
  18. from adhesions.models import Adhesion
  19. from .models import Service, ServiceType, IPPrefix, IPResource, Route, Tunnel, ServiceAllocation, Antenna, AntennaAllocation, Allocation
  20. from .utils import notify_allocation
  21. ### Filters
  22. class ResourceInUseFilter(admin.SimpleListFilter):
  23. title = 'disponibilité'
  24. parameter_name = 'available'
  25. def lookups(self, request, model_admin):
  26. return (
  27. (1, 'Disponible'),
  28. (0, 'Non disponible'),
  29. )
  30. def queryset(self, request, queryset):
  31. available_filter = Q(reserved=False, in_use=False)
  32. if self.value() == '0': # non disponible
  33. return queryset.exclude(available_filter)
  34. if self.value() == '1': # disponible
  35. return queryset.filter(available_filter)
  36. class RouteFilter(admin.SimpleListFilter):
  37. title = 'route'
  38. parameter_name = 'route'
  39. def lookups(self, request, model_admin):
  40. return ServiceAllocation.objects.filter(active=True).values_list('route__pk', 'route__name').distinct()
  41. def queryset(self, request, queryset):
  42. try:
  43. route = int(self.value())
  44. except (TypeError, ValueError):
  45. pass
  46. else:
  47. allocations = ServiceAllocation.objects.filter(active=True, route__pk=route).values_list('pk', flat=True)
  48. queryset = queryset.filter(service_allocation__in=allocations)
  49. return queryset
  50. class AntennaPrefixFilter(admin.SimpleListFilter):
  51. title = 'préfix'
  52. parameter_name = 'prefix'
  53. def lookups(self, request, model_admin):
  54. resources = AntennaAllocation.objects.filter(active=True).values_list('resource__pk', flat=True)
  55. prefixes = IPPrefix.objects.filter(ipresource__in=resources).values_list('pk', 'prefix').distinct()
  56. return prefixes
  57. def queryset(self, request, queryset):
  58. try:
  59. prefix = int(self.value())
  60. except (TypeError, ValueError):
  61. pass
  62. else:
  63. allocations = AntennaAllocation.objects.filter(active=True, resource__prefixes__pk=prefix).values_list('pk', flat=True)
  64. queryset = queryset.filter(allocation__in=allocations)
  65. return queryset
  66. class ActiveTunnelFilter(admin.SimpleListFilter):
  67. title = 'status'
  68. parameter_name = 'active'
  69. def lookups(self, request, model_admin):
  70. return (
  71. ('1', 'Actif'),
  72. ('0', 'Désactivé'),
  73. )
  74. def queryset(self, request, queryset):
  75. query = Q(ended__isnull=True)
  76. if self.value() == '0':
  77. return queryset.exclude(query)
  78. if self.value() == '1':
  79. return queryset.filter(query)
  80. return queryset
  81. ### Inlines
  82. class AllocationInlineFormSet(BaseInlineFormSet):
  83. def save_new(self, form, commit=True):
  84. obj = super().save_new(form, commit)
  85. if type(obj) == ServiceAllocation:
  86. notify_allocation(self.request, obj)
  87. return obj
  88. def save_existing(self, form, instance, commit=True):
  89. old = type(instance).objects.get(pk=instance.pk)
  90. if type(instance) == ServiceAllocation:
  91. notify_allocation(self.request, instance, old)
  92. return super().save_existing(form, instance, commit)
  93. class AllocationInline(admin.TabularInline):
  94. formset = AllocationInlineFormSet
  95. extra = 0
  96. verbose_name_plural = 'Allocations'
  97. show_change_link = True
  98. def get_formset(self, request, obj=None, **kwargs):
  99. formset = super().get_formset(request, obj, **kwargs)
  100. formset.request = request
  101. return formset
  102. def get_max_num(self, request, obj=None, **kwargs):
  103. existing = obj.allocations.count() if obj else 0
  104. # pour simplifier la validation, on ajoute qu’une allocation à la fois
  105. # il faudrait surcharger la méthode clean du formset pour supprimer cette limite
  106. return existing + 1
  107. def has_delete_permission(self, request, obj=None):
  108. return False
  109. class ServiceAllocationInline(AllocationInline):
  110. model = ServiceAllocation
  111. fields = ('id', 'service', 'resource', 'route', 'start', 'end')
  112. raw_id_fields = ('service', 'resource',)
  113. class AntennaAllocationInline(AllocationInline):
  114. model = AntennaAllocation
  115. fields = ('id', 'antenna', 'resource', 'start', 'end')
  116. raw_id_fields = ('antenna', 'resource',)
  117. ### Actions
  118. def ends_resource(resource, request, queryset):
  119. now = timezone.now()
  120. queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
  121. # TODO: send mail
  122. ends_resource.short_description = 'Terminer les allocations sélectionnées'
  123. ### ModelAdmin
  124. class ServiceAdmin(admin.ModelAdmin):
  125. list_display = ('id', 'get_adhesion_link', 'get_adherent_link', 'service_type', 'label', 'active')
  126. list_select_related = ('adhesion', 'adhesion__user', 'adhesion__user__profile', 'adhesion__corporation', 'service_type')
  127. list_filter = (
  128. 'active',
  129. ('service_type', admin.RelatedOnlyFieldListFilter),
  130. )
  131. inlines = (ServiceAllocationInline,)
  132. search_fields = ('=id', 'service_type__name', 'label', 'notes',)
  133. raw_id_fields = ('adhesion',)
  134. get_adhesion_link = lambda self, service: service.adhesion.get_adhesion_link()
  135. get_adhesion_link.short_description = Adhesion.get_adhesion_link.short_description
  136. get_adherent_link = lambda self, service: service.adhesion.get_adherent_link()
  137. get_adherent_link.short_description = Adhesion.get_adherent_link.short_description
  138. def get_actions(self, request):
  139. actions = super().get_actions(request)
  140. if 'delete_selected' in actions:
  141. del actions['delete_selected']
  142. return actions
  143. def has_delete_permission(self, request, obj=None):
  144. return False
  145. class IPPrefixAdmin(admin.ModelAdmin):
  146. readonly_fields = ('prefix',)
  147. def has_delete_permission(self, request, obj=None):
  148. # Interdiction de supprimer le préfix s’il est assigné à une route
  149. return obj and obj.tunnel_set.exists()
  150. # pour embêcher de by-passer le check has_delete_permission, on désactive l’action delete
  151. def get_actions(self, request):
  152. actions = super().get_actions(request)
  153. if 'delete_selected' in actions:
  154. del actions['delete_selected']
  155. return actions
  156. class IPResourceAdmin(admin.ModelAdmin):
  157. list_display = ('__str__', 'available_display', 'last_use',)
  158. list_filter = (
  159. 'category',
  160. ResourceInUseFilter,
  161. ('prefixes', admin.RelatedOnlyFieldListFilter),
  162. RouteFilter,
  163. )
  164. fields = ('ip', 'reserved', 'notes')
  165. readonly_fields = ('ip',)
  166. search_fields = ('=ip',)
  167. def get_inline_instances(self, request, obj=None):
  168. if obj:
  169. if obj.category == 0:
  170. inlines = (ServiceAllocationInline,)
  171. elif obj.category == 1:
  172. inlines = (AntennaAllocationInline,)
  173. else:
  174. inlines = ()
  175. return [inline(self.model, self.admin_site) for inline in inlines]
  176. def get_queryset(self, request):
  177. qs = super().get_queryset(request)
  178. qs = qs.annotate(last_use=models.Case(
  179. models.When(category=0, then=models.Max('service_allocation__end')),
  180. models.When(category=1, then=models.Max('antenna_allocation__end')),
  181. ))
  182. return qs
  183. def available_display(self, obj):
  184. return not obj.reserved and not obj.in_use
  185. available_display.short_description = 'Disponible'
  186. available_display.boolean = True
  187. def last_use(self, obj):
  188. return obj.last_use
  189. last_use.short_description = 'Dernière utilisation'
  190. last_use.admin_order_field = 'last_use'
  191. def get_actions(self, request):
  192. actions = super().get_actions(request)
  193. if 'delete_selected' in actions:
  194. del actions['delete_selected']
  195. return actions
  196. def has_add_permission(self, request, obj=None):
  197. return False
  198. def has_delete_permission(self, request, obj=None):
  199. return False
  200. class RouteAdmin(admin.ModelAdmin):
  201. list_display = ('name',)
  202. def get_readonly_fields(self, request, obj=None):
  203. if obj:
  204. return ('name',)
  205. else:
  206. return ()
  207. def get_actions(self, request):
  208. actions = super().get_actions(request)
  209. if 'delete_selected' in actions:
  210. del actions['delete_selected']
  211. return actions
  212. def has_delete_permission(self, request, obj=None):
  213. return False
  214. class TunnelAdmin(RouteAdmin):
  215. list_display = ('name', 'description', 'created', 'active')
  216. list_filter = (
  217. ActiveTunnelFilter,
  218. )
  219. def active(self, obj):
  220. return not obj.ended
  221. active.short_description = 'Actif'
  222. active.boolean = True
  223. class ServiceTypeAdmin(admin.ModelAdmin):
  224. fields = ('name',)
  225. readonly_fields = ('name',)
  226. def get_actions(self, request):
  227. actions = super().get_actions(request)
  228. if 'delete_selected' in actions:
  229. del actions['delete_selected']
  230. return actions
  231. def has_add_permission(self, request, obj=None):
  232. return False
  233. def has_delete_permission(self, request, obj=None):
  234. return False
  235. class AntennaAdmin(geo_admin.OSMGeoAdmin):
  236. list_display = ('id', 'label', 'ip_display')
  237. inlines = (AntennaAllocationInline,)
  238. list_filter = (
  239. AntennaPrefixFilter,
  240. )
  241. def get_queryset(self, request):
  242. qs = super().get_queryset(request)
  243. if connection.vendor == 'postgresql':
  244. qs = qs.annotate(
  245. ip=StringAgg( # concaténation des IP avec des virgules directement par postgresql
  246. Cast( # casting en TextField car StringApp oppère sur des string mais les ip sont des inet
  247. models.Case( # seulement les IP des allocations actives
  248. models.When(
  249. get_active_filter('allocation'),
  250. then='allocation__resource__ip'
  251. ),
  252. ),
  253. models.TextField()
  254. ),
  255. delimiter=', '
  256. )
  257. )
  258. return qs
  259. def ip_display(self, obj):
  260. if connection.vendor == 'postgresql':
  261. return obj.ip
  262. else:
  263. # peu efficace car génère une requête par ligne
  264. allocations = obj.allocations.filter(active=True)
  265. return ', '.join(allocations.values_list('resource__ip', flat=True)) or '-'
  266. ip_display.short_description = 'IP'
  267. def get_actions(self, request):
  268. actions = super().get_actions(request)
  269. if 'delete_selected' in actions:
  270. del actions['delete_selected']
  271. return actions
  272. def has_delete_permission(self, request, obj=None):
  273. return False
  274. def get_urls(self):
  275. urls = super().get_urls()
  276. from djgeojson.views import GeoJSONLayerView
  277. my_urls = [
  278. url(r'^map/$', self.admin_site.admin_view(self.map_view, cacheable=True), name='antenna-map'),
  279. url(r'^map/data.json$', self.admin_site.admin_view(GeoJSONLayerView.as_view(model=Antenna, geometry_field='position')), name='antenna-map-data'),
  280. ]
  281. return my_urls + urls
  282. def map_view(self, request):
  283. return TemplateResponse(request, 'services/antenna_map.html', {
  284. 'json_url': reverse('admin:antenna-map-data'),
  285. })
  286. def map_data_view(self, request):
  287. geojson = serialize('geojson', Antenna.objects.all(), geometry_field='point', fields=('position',))
  288. return HttpResponse(geojson, content_type='application/json')
  289. admin.site.register(ServiceType, ServiceTypeAdmin)
  290. admin.site.register(Service, ServiceAdmin)
  291. admin.site.register(IPPrefix, IPPrefixAdmin)
  292. admin.site.register(IPResource, IPResourceAdmin)
  293. admin.site.register(Route, RouteAdmin)
  294. admin.site.register(Tunnel, TunnelAdmin)
  295. geo_admin.site.register(Antenna, AntennaAdmin)