From 6802d1e15ff037d81dda99615efd4550f5a7869e Mon Sep 17 00:00:00 2001 From: kdeterme Date: Mon, 5 Oct 2026 10:38:35 +0200 Subject: [PATCH] fix(assets): fix js coord syntax error and add interactive multi-floor plans viewer --- loko/assets/admin.py | 15 +- loko/assets/apps.py | 13 +- loko/assets/forms.py | 3 +- ...uildingdocument_floor_location_and_more.py | 24 + loko/assets/models/buildings.py | 42 +- .../buildings/publicbuilding_detail.html | 473 +++++++++++++++--- loko/assets/urls.py | 1 + loko/assets/views/buildings.py | 178 ++++++- loko/common/models.py | 2 +- 9 files changed, 678 insertions(+), 73 deletions(-) create mode 100644 loko/assets/migrations/0029_publicbuildingdocument_floor_location_and_more.py diff --git a/loko/assets/admin.py b/loko/assets/admin.py index 475f3ab..1996b58 100644 --- a/loko/assets/admin.py +++ b/loko/assets/admin.py @@ -680,12 +680,20 @@ from .models import CleanLitterBinPhoto, CleanLitterBinObservation from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument from .models import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument +class PublicBuildingDocumentInline(admin.TabularInline): + model = PublicBuildingDocument + extra = 1 + fk_name = "building" + fields = ("document_type", "floor_location", "file", "description") + raw_id_fields = ("floor_location",) + @admin.register(PublicBuilding) class PublicBuildingAdmin(admin.GISModelAdmin): list_display = ("code", "name_fr", "location_type", "parent_location", "room_number", "level_number", "usage_type", "building_status", "is_heated", "is_erp", "dpe_rating") list_filter = ("location_type", "floor_type", "is_heated", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible") search_fields = ("code", "name_fr", "name_nl", "address", "owner", "room_number") raw_id_fields = ("parent_location", "manager") + inlines = [PublicBuildingDocumentInline] @admin.register(BuildingAsset) class BuildingAssetAdmin(admin.GISModelAdmin): @@ -694,7 +702,12 @@ class BuildingAssetAdmin(admin.GISModelAdmin): search_fields = ("code", "name_fr", "name_nl", "serial_number", "material", "maintenance_contract_ref", "contractor_name") raw_id_fields = ("building", "location", "parent_asset") -admin.site.register(PublicBuildingDocument, AssetAdmin) +@admin.register(PublicBuildingDocument) +class PublicBuildingDocumentAdmin(admin.ModelAdmin): + list_display = ("id", "building", "floor_location", "document_type", "file", "upload_date") + list_filter = ("document_type",) + search_fields = ("description", "building__code", "building__name_fr", "floor_location__code") + raw_id_fields = ("building", "floor_location") @admin.register(OutdoorLocation) class OutdoorLocationAdmin(admin.GISModelAdmin): diff --git a/loko/assets/apps.py b/loko/assets/apps.py index f443662..829ea8b 100644 --- a/loko/assets/apps.py +++ b/loko/assets/apps.py @@ -54,4 +54,15 @@ class AssetsConfig(AppConfig): return False return True - register('assets', 'InspectionDocument', 'file', guard_inspection_document) + register("assets", "InspectionDocument", "file", guard_inspection_document) + + # Guards for PublicBuilding floor plans and documents + def guard_publicbuilding_floor_plan(obj, user): + return user.is_authenticated + + register("assets", "PublicBuilding", "floor_plan", guard_publicbuilding_floor_plan) + + def guard_publicbuilding_document(obj, user): + return user.is_authenticated + + register("assets", "PublicBuildingDocument", "file", guard_publicbuilding_document) diff --git a/loko/assets/forms.py b/loko/assets/forms.py index a711705..f3075ea 100644 --- a/loko/assets/forms.py +++ b/loko/assets/forms.py @@ -287,7 +287,7 @@ class PublicBuildingForm(forms.ModelForm): 'usage_type', 'building_status', 'construction_year', 'floors_count', 'basement_count', 'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number', - 'room_number', 'ceiling_height_m', 'is_heated', 'fire_compartment', 'floor_type', + 'room_number', 'ceiling_height_m', 'is_heated', 'fire_compartment', 'floor_type', 'floor_plan', 'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible', 'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag', 'lat', 'lon', @@ -315,6 +315,7 @@ class PublicBuildingForm(forms.ModelForm): 'level_number': forms.NumberInput(attrs={'class': 'form-control'}), 'room_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ex: B-204, RDC-01'}), 'ceiling_height_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'floor_plan': forms.FileInput(attrs={'class': 'form-control', 'accept': '.png,.jpg,.jpeg,.svg,.pdf,.webp'}), 'is_heated': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'fire_compartment': forms.TextInput(attrs={'class': 'form-control'}), 'floor_type': forms.Select(attrs={'class': 'form-select'}), diff --git a/loko/assets/migrations/0029_publicbuildingdocument_floor_location_and_more.py b/loko/assets/migrations/0029_publicbuildingdocument_floor_location_and_more.py new file mode 100644 index 0000000..2e1a695 --- /dev/null +++ b/loko/assets/migrations/0029_publicbuildingdocument_floor_location_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.7 on 2026-10-05 08:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0028_buildingasset_contractor_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='publicbuildingdocument', + name='floor_location', + field=models.ForeignKey(blank=True, help_text='Niveau ou local auquel ce document/plan se rapporte.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='floor_documents', to='assets.publicbuilding', verbose_name='Niveau / Étage concerné'), + ), + migrations.AlterField( + model_name='publicbuildingdocument', + name='document_type', + field=models.CharField(choices=[('floor_plan', "Plan d'étage / Plan architectural"), ('as_built', 'Plan de récolement (As-Built)'), ('erp_notice', 'Notice de sécurité / Dossier ERP'), ('dpe', 'Diagnostic énergétique (DPE / PEB)'), ('technical_notice', "Notice technique / Manuel d'exploitation"), ('other', 'Autre document')], default='other', max_length=50, verbose_name='Type de document'), + ), + ] diff --git a/loko/assets/models/buildings.py b/loko/assets/models/buildings.py index 9734b47..28e85ec 100644 --- a/loko/assets/models/buildings.py +++ b/loko/assets/models/buildings.py @@ -275,9 +275,26 @@ class PublicBuilding(AbstractLocation): verbose_name_plural = _("Localisations Bâtiments publics") ordering = ['code', 'name_fr'] + @property + def name(self): + return self.name_fr or self.name_nl or self.code + def get_absolute_url(self): return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk}) + def get_floor_plan_url(self): + """ + Renvoie l'URL du plan d'étage s'il existe (via floor_plan ou document associé de type floor_plan). + """ + if self.floor_plan and self.floor_plan.name: + return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk}) + docs = getattr(self, 'floor_documents', None) + if docs: + first_doc = docs.filter(document_type='floor_plan').first() + if first_doc and first_doc.file and first_doc.file.name: + return reverse('assets:publicbuilding_plan_file', kwargs={'pk': self.pk}) + return None + def get_usage_type_display(self, standard=None): """ Renvoie le libellé du type d'usage adapté dynamiquement à la norme régionale (BE, FR, INT). @@ -422,6 +439,10 @@ class BuildingAsset(AbstractGeoAsset): verbose_name_plural = _("Assets / Équipements de bâtiment") ordering = ['building', 'code'] + @property + def name(self): + return self.name_fr or self.name_nl or self.code + def get_absolute_url(self): return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': self.pk}) @@ -554,13 +575,32 @@ def upload_to_building(instance, filename): return f"buildings/{instance.building.code}/{filename}" +BUILDING_DOCUMENT_TYPE_CHOICES = ( + ('floor_plan', _("Plan d'étage / Plan architectural")), + ('as_built', _("Plan de récolement (As-Built)")), + ('erp_notice', _("Notice de sécurité / Dossier ERP")), + ('dpe', _("Diagnostic énergétique (DPE / PEB)")), + ('technical_notice', _("Notice technique / Manuel d'exploitation")), + ('other', _("Autre document")), +) + + class PublicBuildingDocument(Document): """ Documents associés à un bâtiment public (plans, notices ERP, DPE). """ building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Bâtiment")) + floor_location = models.ForeignKey( + PublicBuilding, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="floor_documents", + verbose_name=_("Niveau / Étage concerné"), + help_text=_("Niveau ou local auquel ce document/plan se rapporte.") + ) file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255) - document_type = models.CharField(max_length=50, default='other', verbose_name=_("Type de document")) + document_type = models.CharField(max_length=50, choices=BUILDING_DOCUMENT_TYPE_CHOICES, default='other', verbose_name=_("Type de document")) class Meta: verbose_name = _("Document de bâtiment") diff --git a/loko/assets/templates/assets/buildings/publicbuilding_detail.html b/loko/assets/templates/assets/buildings/publicbuilding_detail.html index 8fb7eb5..c98a851 100644 --- a/loko/assets/templates/assets/buildings/publicbuilding_detail.html +++ b/loko/assets/templates/assets/buildings/publicbuilding_detail.html @@ -253,37 +253,167 @@ - +
- -
-
- {% translate "Localisation & Carte" %} - {% if center_lat and center_lon %} - - Street View - Street View - - {% endif %} + +
+
+
+ {% if level_specific_mode %} + {% translate "Plan du niveau" %} : {{ active_floor.name|default:building.name }} + {% else %} + {% translate "Localisation & Plans" %} + {% endif %} +
+ +
+
+ + +
+ {% if center_lat and center_lon %} + + Street View + Street View + + {% endif %} +
-
-
-
-
@@ -484,83 +629,281 @@ {% block script %}