loko/streetup/assets/admin.py

667 lines
No EOL
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.contrib.gis import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from django import forms
from documents.models import DocumentFolder, DocumentFolderAttachment
from .models import TrafficLightIntersection, TrafficLightIntersectionDocument
from .models import TrafficLightPoleModel, TrafficLightPole, TrafficLightCableModel, TrafficLightCable, TrafficLightElectricalCabinet, TrafficLightElectricalCabinetModel
from .models import TrafficLightLanternModel, TrafficLightLantern, TrafficLightControllerModel, TrafficLightController, TrafficLightDetector, TrafficLightDetectorModel
from .models import TrafficLightContract, TrafficLightRadarModel, TrafficLightRadar
from .models import (
TrafficLightRadarPoleModel, TrafficLightRadarPole, TrafficLightElectricalCabinetContentModel,
TrafficLightElectricalCabinetContent, TrafficLightAccessoryModel, TrafficLightAccessory,
TrafficLightProgramming, TrafficLightControllerHardware, TrafficLightControllerLogFile,
TrafficLightNetworkHardware, TrafficLightVoltageHardware
)
from .models import SignStreet
from .models import AssetCategory, AssetHistory
from .models import GreenSurfacesComment, GreenSurfacesCommentPhoto
from .models import CleanGlassContainer, CleanGlassContainerModel, CleanLitterBin, CleanLitterBinModel, CleanLocation, CleanLocationContract
class AssetAdmin(admin.ModelAdmin):
def get_search_fields(self, request):
search_fields = list(super().get_search_fields(request) or [])
model_fields = {f.name for f in self.model._meta.get_fields()}
for field in ["code", "name_fr", "name_nl"]:
if field in model_fields and field not in search_fields:
search_fields.append(field)
return search_fields
def get_list_filter(self, request):
list_filter = list(super().get_list_filter(request) or [])
model_fields = {f.name for f in self.model._meta.get_fields()}
if "model" in model_fields and "model" not in list_filter:
list_filter.append("model")
return list_filter
class AssetGISAdmin(admin.GISModelAdmin):
gis_widget_kwargs = {
'attrs': {
"default_lon" : 4.3517, # Bruxelles
"default_lat" : 50.8503,
"default_zoom" : 12
},
}
def get_search_fields(self, request):
search_fields = list(super().get_search_fields(request) or [])
model_fields = {f.name for f in self.model._meta.get_fields()}
for field in ["code", "name_fr", "name_nl"]:
if field in model_fields and field not in search_fields:
search_fields.append(field)
return search_fields
def get_list_filter(self, request):
list_filter = list(super().get_list_filter(request) or [])
model_fields = {f.name for f in self.model._meta.get_fields()}
if "model" in model_fields and "model" not in list_filter:
list_filter.append("model")
return list_filter
@admin.register(TrafficLightContract)
class TrafficLightContractAdmin(AssetAdmin):
search_fields = ("intersection__code", "contract__contract_number")
list_display = ("intersection", "contract")
list_filter = ("contract",)
class TrafficLightContractAdminInline(admin.TabularInline):
model = TrafficLightContract
extra = 1
@admin.register(TrafficLightIntersection)
class TrafficLightIntersectionAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "name_fr", "municipality")
list_filter = ("municipality",)
inlines = [TrafficLightContractAdminInline,]
admin.site.register(TrafficLightIntersectionDocument, AssetAdmin)
admin.site.register(TrafficLightPoleModel, AssetAdmin)
admin.site.register(TrafficLightPole, AssetAdmin)
admin.site.register(TrafficLightCableModel, AssetAdmin)
admin.site.register(TrafficLightCable, AssetAdmin)
admin.site.register(TrafficLightLanternModel, AssetAdmin)
admin.site.register(TrafficLightLantern, AssetAdmin)
admin.site.register(TrafficLightControllerModel, AssetAdmin)
admin.site.register(TrafficLightDetector, AssetAdmin)
admin.site.register(TrafficLightDetectorModel, AssetAdmin)
admin.site.register(TrafficLightElectricalCabinetModel, AssetAdmin)
admin.site.register(TrafficLightElectricalCabinet, AssetAdmin)
admin.site.register(TrafficLightRadarModel, AssetAdmin)
admin.site.register(TrafficLightRadar, AssetAdmin)
admin.site.register(TrafficLightRadarPoleModel, AssetAdmin)
admin.site.register(TrafficLightRadarPole, AssetAdmin)
admin.site.register(TrafficLightElectricalCabinetContentModel, AssetAdmin)
admin.site.register(TrafficLightElectricalCabinetContent, AssetAdmin)
admin.site.register(TrafficLightAccessoryModel, AssetAdmin)
admin.site.register(TrafficLightAccessory, AssetAdmin)
admin.site.register(TrafficLightProgramming, AssetAdmin)
admin.site.register(TrafficLightControllerHardware, AssetAdmin)
admin.site.register(TrafficLightControllerLogFile, AssetAdmin)
admin.site.register(TrafficLightNetworkHardware, AssetAdmin)
admin.site.register(TrafficLightVoltageHardware, AssetAdmin)
@admin.register(TrafficLightController)
class TrafficLightControllerAdmin(AssetAdmin):
search_fields = ("intersection__code",)
list_display = ("intersection", "connected_to_control_center", "controller_maintenance_contract")
list_filter = ("connected_to_control_center", "controller_maintenance_contract")
@admin.register(AssetCategory)
class AssetCategoryAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "get_name", "thematic", "parent")
list_filter = ("thematic", "parent")
autocomplete_fields = ("parent",)
filter_horizontal = ("allowed_models",)
change_list_template = "admin/assets/assetcategory/change_list.html"
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if "parent" in form.base_fields:
# Limiter les choix du parent aux catégories sans parent (1 niveau max)
form.base_fields["parent"].queryset = AssetCategory.objects.filter(parent__isnull=True)
return form
def get_urls(self):
from django.urls import path as urlpath
urls = super().get_urls()
custom_urls = [
urlpath(
"import-excel/",
self.admin_site.admin_view(self.import_excel_view),
name="assets_assetcategory_import_excel",
),
urlpath(
"download-template/",
self.admin_site.admin_view(self.download_template_view),
name="assets_assetcategory_download_template",
),
]
return custom_urls + urls
# ------------------------------------------------------------------
def import_excel_view(self, request):
import io
import tempfile
import os
from django.contrib import messages
from django.shortcuts import render, redirect
from django.urls import reverse
from assets.management.commands.import_assets_from_excel import Command as ImportCommand
from assets.management.commands.import_assets_from_excel import COLUMNS, COLUMN_HINTS
column_guide = [
{"key": key, "desc": COLUMN_HINTS.get(key, "")}
for key, _label in COLUMNS
]
context = {
**self.admin_site.each_context(request),
"title": "Import d'assets depuis Excel",
"opts": self.model._meta,
"column_guide": column_guide,
}
if request.method == "POST":
uploaded = request.FILES.get("excel_file")
if not uploaded:
messages.error(request, "Aucun fichier sélectionné.")
return render(request, "admin/assets/assetcategory/import_excel.html", context)
if not uploaded.name.endswith(".xlsx"):
messages.error(request, "Seuls les fichiers .xlsx sont acceptés.")
return render(request, "admin/assets/assetcategory/import_excel.html", context)
dry_run = "dry_run" in request.POST
update = "update" in request.POST
# Save file to a temp path so the command can open it
suffix = ".xlsx"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
for chunk in uploaded.chunks():
tmp.write(chunk)
tmp_path = tmp.name
cmd = ImportCommand()
# Capture stdout / stderr via StringIO-compatible wrapper
class _Writer:
def __init__(self):
self.lines = []
def write(self, msg):
self.lines.append(str(msg))
def flush(self):
pass
out = _Writer()
err = _Writer()
cmd.stdout = out
cmd.stderr = err
import_error = None
try:
cmd._import(tmp_path, dry_run=dry_run, update=update)
except Exception as e:
import_error = str(e)
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
output_lines = out.lines + err.lines
context["output_lines"] = output_lines
context["dry_run"] = dry_run
context["import_error"] = import_error
if import_error:
messages.error(request, f"Erreur lors de l'import : {import_error}")
elif dry_run:
messages.warning(request, "Dry-run terminé – aucune donnée modifiée.")
else:
messages.success(request, "Import terminé avec succès.")
return render(request, "admin/assets/assetcategory/import_excel.html", context)
return render(request, "admin/assets/assetcategory/import_excel.html", context)
# ------------------------------------------------------------------
def download_template_view(self, request):
import io
from django.http import HttpResponse
from assets.management.commands.import_assets_from_excel import generate_template
buf = io.BytesIO()
# generate_template expects a file path, so use a temp file
import tempfile
import os
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
tmp_path = tmp.name
try:
generate_template(tmp_path)
with open(tmp_path, "rb") as f:
data = f.read()
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
response = HttpResponse(
data,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = 'attachment; filename="assets_import_template.xlsx"'
return response
@admin.register(AssetHistory)
class AssetHistoryAdmin(AssetAdmin):
list_display = ("timestamp", "content_type", "object_id", "user", "field_name", "action_type")
list_filter = ("action_type", "content_type", "timestamp")
search_fields = ("user__username", "field_name", "object_id")
readonly_fields = ("content_type", "object_id", "user", "timestamp", "field_name", "old_value", "new_value", "action_type")
date_hierarchy = "timestamp"
def has_add_permission(self, request):
# Prevent manual creation
return False
def has_change_permission(self, request, obj=None):
# Read-only
return False
# ============================================================================
# Public Lighting
# ============================================================================
from .models import (
PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPoleModel,
PublicLightingPole, PublicLightingLightFixtureModel, PublicLightingLightFixture,
PublicLightingElectricalCabinetModel, PublicLightingElectricalCabinet
)
admin.site.register(PublicLightingStreet, AssetAdmin)
admin.site.register(PublicLightingStreetDocument, AssetAdmin)
admin.site.register(PublicLightingPoleModel, AssetAdmin)
admin.site.register(PublicLightingPole, AssetAdmin)
admin.site.register(PublicLightingLightFixtureModel, AssetAdmin)
admin.site.register(PublicLightingLightFixture, AssetAdmin)
admin.site.register(PublicLightingElectricalCabinetModel, AssetAdmin)
admin.site.register(PublicLightingElectricalCabinet, AssetAdmin)
# ============================================================================
# Artworks
# ============================================================================
from .models import (
ArtworkStreet, ArtworkMonument, ArtworkFountain,
ArtworkFountainDocument, ArtworkMonumentDocument
)
admin.site.register(ArtworkStreet, AssetAdmin)
admin.site.register(ArtworkMonument, AssetAdmin)
admin.site.register(ArtworkFountain, AssetAdmin)
admin.site.register(ArtworkFountainDocument, AssetAdmin)
admin.site.register(ArtworkMonumentDocument, AssetAdmin)
from .models import Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract
def make_folder_attachment_inline(thematic_code):
"""
Factory pour créer un inline DocumentFolderAttachment filtré par thématique.
Args:
thematic_code: Le code de la thématique (ex: 'structures', 'control_centers')
"""
class FolderAttachmentForm(forms.ModelForm):
class Meta:
model = DocumentFolderAttachment
fields = ['folder', 'is_default']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filtrer les dossiers par thématique
self.fields['folder'].queryset = DocumentFolder.objects.filter(
thematics__code=thematic_code
).distinct().order_by('name')
class DocumentFolderAttachmentInline(GenericTabularInline):
model = DocumentFolderAttachment
form = FolderAttachmentForm
extra = 1
fields = ('folder', 'is_default')
verbose_name = "Dossier documentaire"
verbose_name_plural = "Dossiers documentaires"
return DocumentFolderAttachmentInline
# Inlines spécifiques par thématique
StructureFolderAttachmentInline = make_folder_attachment_inline('structures')
ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers')
TrafficLightFolderAttachmentInline = make_folder_attachment_inline('traffic_lights')
@admin.register(Structure)
class StructureAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "name_fr", "structure_type")
list_filter = ("structure_type",)
inlines = [StructureFolderAttachmentInline]
@admin.register(StructureLocation)
class StructureLocationAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
admin.site.register(StructureDocument, AssetAdmin)
admin.site.register(StructureAssetModel, AssetAdmin)
admin.site.register(StructureGeoAsset, AssetAdmin)
admin.site.register(StructureContract, AssetAdmin)
from .models import ControlCenter, ControlCenterLocation, ControlCenterDocument, ControlCenterAssetModel, ControlCenterGeoAsset, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
@admin.register(ControlCenter)
class ControlCenterAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
inlines = [ControlCenterFolderAttachmentInline]
@admin.register(ControlCenterLocation)
class ControlCenterLocationAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
admin.site.register(ControlCenterDocument, AssetAdmin)
admin.site.register(ControlCenterAssetModel, AssetAdmin)
admin.site.register(ControlCenterGeoAsset, AssetAdmin)
admin.site.register(ControlCenterContract, AssetAdmin)
admin.site.register(ControlCenterGeoAssetContract, AssetAdmin)
admin.site.register(ControlCenterAssetDocument, AssetAdmin)
@admin.register(SignStreet)
class SignStreetAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "name_fr", "name_nl", "municipality")
list_filter = ("municipality",)
# ============================================================================
# Sign
# ============================================================================
from .models import (
SignStreetDocument, SignPoleModel, SignPole, SignPanelModel,
SignPanelType, SignPanel, ComplementaryPoliceRegulation, SignParkingSpot
)
admin.site.register(SignStreetDocument, AssetAdmin)
admin.site.register(SignPoleModel, AssetAdmin)
admin.site.register(SignPole, AssetAdmin)
admin.site.register(SignPanelModel, AssetAdmin)
admin.site.register(SignPanelType, AssetAdmin)
admin.site.register(SignPanel, AssetAdmin)
admin.site.register(ComplementaryPoliceRegulation, AssetAdmin)
admin.site.register(SignParkingSpot, AssetAdmin)
from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract
admin.site.register(ITSAssetModel, AssetAdmin)
@admin.register(ITSGeoAsset)
class ITSGeoAssetAdmin(AssetAdmin):
search_fields = ("code", "model__name_fr", "location__code")
list_display = ("code", "model", "location")
list_filter = ("model", "location")
@admin.register(ITSGeoAssetContract)
class ITSGeoAssetContractAdmin(AssetAdmin):
search_fields = ("asset__code", "contract__contract_number")
list_display = ("asset", "contract")
list_filter = ("contract",)
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
admin.site.register(ITSLocation, AssetAdmin)
admin.site.register(ITSLocationDocument, AssetAdmin)
admin.site.register(ITSLocationContract, AssetAdmin)
from .models import NatureAssetModel, NatureRWIAsset, NatureRWIAssetDocument
from .models import (
NatureLocation, NatureGreenSurface, NatureGreenSurfaceDocument,
NatureTree, NatureShrub, NatureBench, NatureRWIZ
)
admin.site.register(NatureAssetModel, AssetAdmin)
admin.site.register(NatureLocation, AssetAdmin)
admin.site.register(NatureGreenSurface, AssetAdmin)
admin.site.register(NatureGreenSurfaceDocument, AssetAdmin)
admin.site.register(NatureTree, AssetAdmin)
admin.site.register(NatureShrub, AssetAdmin)
admin.site.register(NatureBench, AssetAdmin)
admin.site.register(NatureRWIZ, AssetAdmin)
class CustomNatureGeoAdmin(AssetGISAdmin):
gis_widget_kwargs = {
'attrs': {
"default_lon" : 4.3517, # Bruxelles
"default_lat" : 50.8503,
"default_zoom" : 12
},
}
@admin.register(NatureRWIAsset)
class NatureRWIAssetAdmin(CustomNatureGeoAdmin):
list_display = ("code", "code_name", "structure_type", "geometry_type", "project_manager", "condition")
list_filter = ("structure_type", "geometry_type", "project_manager", "condition", "construction_status")
search_fields = ("code", "external_id", "administrator", "comment")
change_list_template = "admin/assets/naturerwiasset/change_list.html"
def get_fields(self, request, obj=None):
fields = list(super().get_fields(request, obj))
if obj and obj.geometry_type != 'point':
if 'has_filter' in fields:
fields.remove('has_filter')
return fields
def get_urls(self):
from django.urls import path as urlpath
urls = super().get_urls()
custom_urls = [
urlpath(
"import-rwi/",
self.admin_site.admin_view(self.import_rwi_view),
name="assets_naturerwiasset_import_rwi",
),
]
return custom_urls + urls
def import_rwi_view(self, request):
import io
import os
import tempfile
from django.core.management import call_command
from django.contrib import messages
from django.shortcuts import render
context = {
**self.admin_site.each_context(request),
"title": "Importation du GeoPackage RWI",
"opts": self.model._meta,
}
if request.method == "POST":
dry_run = "dry_run" in request.POST
gpkg_file = request.FILES.get("gpkg_file")
if not gpkg_file:
messages.error(request, "Aucun fichier GeoPackage sélectionné.")
return render(request, "admin/assets/naturerwiasset/import_rwi.html", context)
if not gpkg_file.name.endswith(".gpkg"):
messages.error(request, "Seuls les fichiers .gpkg sont acceptés.")
return render(request, "admin/assets/naturerwiasset/import_rwi.html", context)
# Save uploaded file to a temporary file
with tempfile.NamedTemporaryFile(suffix=".gpkg", delete=False) as tmp:
for chunk in gpkg_file.chunks():
tmp.write(chunk)
tmp_path = tmp.name
stdout_stream = io.StringIO()
stderr_stream = io.StringIO()
import_error = None
try:
if dry_run:
call_command("import_rwi", tmp_path, "--dry-run", stdout=stdout_stream, stderr=stderr_stream)
else:
call_command("import_rwi", tmp_path, stdout=stdout_stream, stderr=stderr_stream)
except Exception as e:
import_error = str(e)
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
output_lines = (stdout_stream.getvalue() + stderr_stream.getvalue()).split("\n")
context["output_lines"] = output_lines
context["dry_run"] = dry_run
context["import_error"] = import_error
if import_error:
messages.error(request, f"Erreur lors de l'importation : {import_error}")
elif dry_run:
messages.warning(request, "Simulation d'importation (dry-run) terminée – aucune donnée n'a été modifiée.")
else:
messages.success(request, "Importation du GeoPackage RWI terminée avec succès !")
return render(request, "admin/assets/naturerwiasset/import_rwi.html", context)
return render(request, "admin/assets/naturerwiasset/import_rwi.html", context)
admin.site.register(NatureRWIAssetDocument, AssetAdmin)
# ============================================================================
# Green Surfaces Comments Admin
# ============================================================================
class GreenSurfacesCommentPhotoInline(admin.TabularInline):
model = GreenSurfacesCommentPhoto
extra = 1
fields = ('photo', 'heading', 'order')
readonly_fields = ('created_at',)
@admin.register(GreenSurfacesComment)
class GreenSurfacesCommentAdmin(AssetAdmin):
list_display = ('id', 'comment_preview', 'created_by', 'created_at', 'address')
list_filter = ('created_at', 'created_by')
search_fields = ('comment', 'address', 'created_by__username')
readonly_fields = ('created_at', 'updated_at', 'geom')
inlines = [GreenSurfacesCommentPhotoInline]
fieldsets = (
('Commentaire', {
'fields': ('comment', 'created_by')
}),
('Localisation', {
'fields': ('latitude', 'longitude', 'geom', 'address')
}),
('Métadonnées', {
'fields': ('created_at', 'updated_at')
}),
)
def comment_preview(self, obj):
return obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment
comment_preview.short_description = 'Commentaire'
@admin.register(GreenSurfacesCommentPhoto)
class GreenSurfacesCommentPhotoAdmin(AssetAdmin):
list_display = ('id', 'comment', 'heading', 'order', 'created_at')
list_filter = ('created_at',)
search_fields = ('comment__comment',)
readonly_fields = ('created_at',)
# ============================================================================
# Roads
# ============================================================================
from .models import (
RoadStreet, RoadStreetDocument, RoadwayModel, Roadway,
RoadSidewalkModel, RoadSidewalk, RoadFurnitureModel, RoadFurniture,
RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
)
admin.site.register(RoadStreet, AssetAdmin)
admin.site.register(RoadStreetDocument, AssetAdmin)
admin.site.register(RoadwayModel, AssetAdmin)
admin.site.register(Roadway, AssetAdmin)
admin.site.register(RoadSidewalkModel, AssetAdmin)
admin.site.register(RoadSidewalk, AssetAdmin)
admin.site.register(RoadFurnitureModel, AssetAdmin)
admin.site.register(RoadFurniture, AssetAdmin)
admin.site.register(RoadwayFunction, AssetAdmin)
admin.site.register(RoadConcreteBlockSet, AssetAdmin)
admin.site.register(RoadDrainModel, AssetAdmin)
admin.site.register(RoadDrain, AssetAdmin)
@admin.register(CleanLocation)
class CleanLocationAdmin(AssetAdmin):
search_fields = ("code", "name_fr", "name_nl")
list_display = ("code", "name_fr", "name_nl", "municipality")
list_filter = ("municipality",)
admin.site.register(CleanLitterBinModel, AssetAdmin)
admin.site.register(CleanLitterBin, AssetAdmin)
admin.site.register(CleanGlassContainerModel, AssetAdmin)
admin.site.register(CleanGlassContainer, AssetAdmin)
admin.site.register(CleanLocationContract, AssetAdmin)
from .models import CleanLitterBinPhoto, CleanLitterBinObservation
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
@admin.register(PublicBuilding)
class PublicBuildingAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "location_type", "parent_location", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating")
list_filter = ("location_type", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
raw_id_fields = ("parent_location", "manager")
@admin.register(BuildingAsset)
class BuildingAssetAdmin(admin.GISModelAdmin):
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "status", "obsolescence")
list_filter = ("asset_type", "status", "obsolescence")
search_fields = ("code", "name_fr", "name_nl", "serial_number")
raw_id_fields = ("building", "location", "parent_asset")
admin.site.register(PublicBuildingDocument, AssetAdmin)