from django.contrib.gis.db import models from django.contrib.postgres.fields import ArrayField from django.urls import reverse from django.conf import settings from django.utils import timezone from django.utils.translation import gettext_lazy as _ from datetime import datetime from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel, AssetCategory from common.models import Document from common.storages import PrivateMediaStorage private_storage = PrivateMediaStorage() class NatureLocation(AbstractLocation): geom = models.MultiPolygonField(srid=3812, null=True,blank=True) parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations") zone = models.IntegerField(null=True, blank=True) is_highway = models.BooleanField(default=False) category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True) municipality = models.ForeignKey('common.Municipality', on_delete=models.SET_NULL, null=True, blank=True) def _get_direct_assets(self, active_only=False): related_sets = ( self.nature_green_surfaces, self.nature_trees, self.nature_shrubs, self.nature_benches, ) assets = [] for related_manager in related_sets: queryset = related_manager.filter(status='active') if active_only else related_manager.all() assets.extend(list(queryset)) return assets def get_active_assets(self): def collect_assets(location): assets = location._get_direct_assets(active_only=True) for child in location.child_locations.all(): assets += collect_assets(child) return assets return collect_assets(self) def get_all_assets(self): def collect_assets(location): assets = location._get_direct_assets(active_only=False) for child in location.child_locations.all(): assets += collect_assets(child) return assets return collect_assets(self) def get_direct_child_assets(self): return self._get_direct_assets(active_only=False) def get_absolute_url(self): return reverse( 'assets:nature_locations_detail', kwargs={ # 'asset_model': content_type.model, 'location_id': self.id } ) class NatureAssetModel(AbstractAssetModel): color = models.CharField(max_length=200, null=True, blank=True) class Meta: verbose_name = "Nature Asset Model" verbose_name_plural = "Nature Asset Models" BULB_TYPE_CHOICES = [ ('tulipes', _('Tulipes')), ('jacynthes', _('Jacynthes')), ('jonquilles', _('Jonquilles')), ('narcisses', _('Narcisses')), ('crocus', _('Crocus')), ('perce_neige', _('Perce-neige')), ('autres', _('Autres')), ] class NatureGreenSurface(AbstractGeoAsset): geom = models.MultiPolygonField(srid=3812, null=True, blank=True) model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_green_surfaces") location = models.ForeignKey(NatureLocation, related_name="nature_green_surfaces", on_delete=models.SET_NULL, null=True, blank=True) surface_area = models.FloatField(null=True, blank=True) slope = models.FloatField(null=True, blank=True) weighting = models.FloatField(null=True, blank=True) density = models.FloatField(null=True, blank=True) mower_frenquency = models.IntegerField(null=True, blank=True) buffer = models.FloatField(null=True, blank=True) width = models.FloatField(null=True, blank=True) length = models.FloatField(null=True, blank=True) bulb_types = ArrayField( models.CharField(max_length=50, choices=BULB_TYPE_CHOICES), blank=True, default=list, verbose_name=_("Types de bulbes") ) indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturegreensurface', 'asset_id':self.id}) class NatureGreenSurfaceDocument(Document): green_surface = models.ForeignKey(NatureGreenSurface, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Surface") file = models.FileField(upload_to="assets_documents/nature/surfaces/", storage=private_storage, verbose_name="Fichier", max_length=255) thumbnail = models.ImageField(upload_to="assets_documents/nature/surfaces/", storage=private_storage, null=True, blank=True, max_length=255) class NatureTree(AbstractGeoAsset): model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_trees") location = models.ForeignKey(NatureLocation, related_name="nature_trees", on_delete=models.SET_NULL, null=True, blank=True) height = models.FloatField(null=True, blank=True) depth = models.FloatField(null=True, blank=True) protection_buffer = models.FloatField(null=True, blank=True) indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id':self.id}) # Arbustes class NatureShrub(AbstractGeoAsset): model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_shrubs") location = models.ForeignKey(NatureLocation, related_name="nature_shrubs", on_delete=models.SET_NULL, null=True, blank=True) height = models.FloatField(null=True, blank=True) depth = models.FloatField(null=True, blank=True) trimmed = models.BooleanField(default=False) indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'natureshrub', 'asset_id':self.id}) class Meta: verbose_name = "Arbuste" verbose_name_plural = "Arbustes" # Bancs class NatureBench(AbstractGeoAsset): model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_benches") location = models.ForeignKey(NatureLocation, related_name="nature_benches", on_delete=models.SET_NULL, null=True, blank=True) material = models.CharField(max_length=200, null=True, blank=True) length = models.FloatField(null=True, blank=True) width = models.FloatField(null=True, blank=True) height = models.FloatField(null=True, blank=True) indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturebench', 'asset_id':self.id}) # ============================================================================ # Geolocated Comments (independent from assets) # ============================================================================ class GreenSurfacesComment(models.Model): """ Commentaire géolocalisé indépendant pour l'édition des espaces verts. Permet aux utilisateurs d'ajouter des remarques géolocalisées avec photos, sans être lié à un asset spécifique. """ STATUS_CHOICES = [ ('memo', 'Mémo'), ('attention', 'Point d\'attention'), ('todo', 'À faire'), ('ok', 'OK'), ('problem', 'Problème'), ] comment = models.TextField(verbose_name="Commentaire", blank=True, default="") status = models.CharField( max_length=20, choices=STATUS_CHOICES, default='memo', verbose_name="Statut" ) latitude = models.FloatField(verbose_name="Latitude") longitude = models.FloatField(verbose_name="Longitude") geom = models.PointField(srid=3812, null=True, blank=True, verbose_name="Géométrie") address = models.CharField(max_length=1024, null=True, blank=True, verbose_name="Adresse") created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, related_name='green_surfaces_comments', null=True, blank=True, verbose_name="Créé par" ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") updated_at = models.DateTimeField(auto_now=True, verbose_name="Date de modification") class Meta: verbose_name = "Commentaire (Espaces verts)" verbose_name_plural = "Commentaires (Espaces verts)" ordering = ['-created_at'] def __str__(self): return f"Commentaire de {self.created_by} - {self.created_at.strftime('%d/%m/%Y %H:%M')}" def comment_photo_upload_path(instance, filename): """Chemin de stockage pour les photos de commentaires""" now = timezone.now() year = now.strftime("%Y") month = now.strftime("%m") return f"green_surfaces/comments/photos/{year}/{month}/{filename}" class GreenSurfacesCommentPhoto(models.Model): """ Photo associée à un commentaire géolocalisé. Conserve la direction (azimut) dans laquelle la photo a été prise. """ comment = models.ForeignKey( GreenSurfacesComment, related_name='photos', on_delete=models.CASCADE, verbose_name="Commentaire" ) photo = models.ImageField( upload_to=comment_photo_upload_path, storage=private_storage, verbose_name="Photo" ) heading = models.FloatField( null=True, blank=True, verbose_name="Direction (azimut en degrés)", help_text="Direction de la boussole dans laquelle la photo a été prise (0-360°)" ) order = models.PositiveIntegerField( default=0, verbose_name="Ordre", help_text="Ordre d'affichage de la photo" ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") class Meta: verbose_name = "Photo de commentaire" verbose_name_plural = "Photos de commentaires" ordering = ['order', 'created_at'] def __str__(self): direction = f" (direction: {self.heading}°)" if self.heading is not None else "" return f"Photo {self.order + 1} du commentaire #{self.comment.id}{direction}" class NatureRWIZ(AbstractLocation): geom = models.MultiPolygonField(srid=3812, null=True,blank=True) def get_active_assets(self): if not self.geom: return NatureRWIAsset.objects.none() return NatureRWIAsset.objects.filter(geom__intersects=self.geom, status='active') def get_all_assets(self): if not self.geom: return NatureRWIAsset.objects.none() return NatureRWIAsset.objects.filter(geom__intersects=self.geom) def get_absolute_url(self): return reverse( 'assets:zirw_detail', kwargs={ 'location_id': self.id } ) class Meta: verbose_name = _("Zone de gestion intégrée des eaux pluviales (ZIEP)") verbose_name_plural = _("Zones de gestion intégrée des eaux pluviales (ZIEP)") CODE_NAME_CHOICES = [ ('AV', _('Stormwater inlet (Avaloir)')), ('BS', _('Sequenced border (Bordure séquencée)')), ('BI', _('Injection port (Bouche d\'injection)')), ('CA', _('Non-return valve (Clapet anti-retour)')), ('ES', _('Sedimentation (Empierrement sédimentation)')), ('KC', _('Kerb cell')), ('PI', _('Infiltration well (Puits d\'infiltration)')), ('SU', _('Overflow (Surverse)')), ('VI', _('Isolation valve (Vanne d\'isolation)')), ('VR', _('Control valve (Vanne de régulation)')), ('GT', _('Sidewalk gargoyle (Gargouille de trottoir)')), ('FE', _('Trickle of water (Filet d\'eau)')), ('AR', _('Rail drain (Avaloir de rail)')), ('BA', _('Leveled edge (Bordure arasée)')), ('CI', _('Infiltration channel (Caniveau infiltrant)')), ('D', _('Drain')), ('TP', _('Overflowing (Trop plein)')), ('RES', _('Separate sewer system (Réseau égout séparatif)')), ('EVC', _('Hollow green space (Espace vert creux)')), ('OED', _('Decentralized underground structure (Ouvrage enterré décentralisé)')), ('OSI', _('Work on an impermeable surface (Ouvrage sur surface imperméable)')), ('TA', _('Alternative technique (Technique alternative)')), ('OA', _('Supplementary work (Ouvrage annexe)')), ] STRUCTURE_TYPE_CHOICES = [ # Points ('avaloir', _('Stormwater inlet (Avaloir)')), ('bordure sequencee', _('Sequenced border (Bordure séquencée)')), ('bouche injection', _('Injection port (Bouche d\'injection)')), ('clapet anti-retour', _('Non-return valve (Clapet anti-retour)')), ('empierrement sedimentation', _('Sedimentation (Empierrement sédimentation)')), ('kerb cell', _('Kerb cell')), ('puits infiltration', _('Infiltration well (Puits d\'infiltration)')), ('surverse', _('Overflow (Surverse)')), ('vanne isolation', _('Isolation valve (Vanne d\'isolation)')), ('vanne regulation', _('Control valve (Vanne de régulation)')), # Lines ('gargouille de trottoir', _('Sidewalk gargoyle (Gargouille de trottoir)')), ('filet eau', _('Trickle of water (Filet d\'eau)')), ('avaloir de rail', _('Rail drain (Avaloir de rail)')), ('bordure arasee', _('Leveled edge (Bordure arasée)')), ('caniveau infiltrant', _('Infiltration channel (Caniveau infiltrant)')), ('drain', _('Drain')), ('trop plein', _('Overflowing (Trop plein)')), ('reseau egout separatif', _('Separate sewer system (Réseau égout séparatif)')), # Polygons ('bassin sec/infiltration', _('Dry/infiltration basin (Bassin sec/infiltration)')), ('bassin en eau', _('Wet basin (Bassin en eau)')), ('fosse', _('Ditch (Fossé)')), ('jardin de pluie', _('Rain garden (Jardin de pluie)')), ('noue', _('Swale (Noue)')), ('massif drainant', _('Draining structure (Massif drainant)')), ('tranchee drainante', _('Draining trench (Tranchée drainante)')), ('toiture stockante', _('Retention roof (Toiture stockante)')), ('toiture verte', _('Green roof (Toiture verte)')), ('bassin orage', _('Stormwater basin (Bassin d\'orage)')), ('SAUL-vide', _('SAUL-empty (SAUL-vide)')), ('SAUL-terre', _('SAUL-earth (SAUL-terre)')), ('citerne', _('Cistern (Citerne)')), ('espace vert sur dalle', _('Green space on slab (Espace vert sur dalle)')), ('bande filtrante', _('Filter strip (Bande filtrante)')), ('collecteur en beton poreux', _('Porous concrete collector (Collecteur en béton poreux)')), ] PROJECT_MANAGER_CHOICES = [ ('DPV', 'DPV'), ('DITP', 'DITP'), ('DEN', 'DEN'), ('DPO', 'DPO'), ('Beliris', 'Beliris'), ('Commune', 'Commune'), ] CONSTRUCTION_STATUS_CHOICES = [ ('etude', _('Design (Étude)')), ('travaux', _('Construction (Travaux)')), ('reception provisoire', _('Provisional acceptance (Réception provisoire)')), ('reception definitive', _('Final acceptance (Réception définitive)')), ] CONDITION_CHOICES = [ ('bon', _('Good (Bon)')), ('examen a programmer', _('Inspection to schedule (Examen à programmer)')), ('intervention curative necessaire', _('Curative action needed (Intervention curative nécessaire)')), ('refection totale necessaire', _('Total rehabilitation needed (Réfection totale nécessaire)')), ('travaux entretien a realiser', _('Maintenance works to perform (Travaux entretien à réaliser)')), ('travaux maintenance', _('Maintenance works (Travaux maintenance)')), ('travaux maintenance a programmer', _('Maintenance works to schedule (Travaux maintenance à programmer)')), ] CONNECTION_CHOICES = [ ('connecte', _('Connected (Connecté)')), ('non connecte', _('Not connected (Non connecté)')), ] COATING_CATEGORY_CHOICES = [ ('Vegetal', _('Vegetal (Végétal)')), ('Structure enherbee', _('Grassed structure (Structure enherbée)')), ('Materiau non agrege', _('Unbound material (Matériau non agrégé)')), ('Structure a remplissage mineral', _('Mineral-filled structure (Structure à remplissage minéral)')), ('Revetement sureleve', _('Raised coating (Revêtement surélevé)')), ('Modulaire poreux', _('Porous modular (Modulaire poreux)')), ('Continu poreux', _('Porous continuous (Continu poreux)')), ('Impermeable', _('Impermeable (Imperméable)')), ] COATING_TYPE_CHOICES = [ ('Pleine terre', _('Open soil (Pleine terre)')), ('Melange terre-pierre', _('Soil-stone mixture (Mélange terre-pierre)')), ('Paves a joints larges enherbes', _('Grassed wide-joint pavers (Pavés à joints larges enherbés)')), ('Dalles gazon en beton', _('Concrete grass grids (Dalles gazon en béton)')), ('Dalles alveolaires vegetalisees', _('Vegetated cellular tiles (Dalles alvéolaires végétalisées)')), ('Mulch vegetal', _('Woodchips/mulch (Mulch végétal)')), ('Graviers calcaires', _('Limestone gravel (Graviers calcaires)')), ('Graviers non calcaires', _('Non-limestone gravel (Graviers non calcaires)')), ('Terre battue', _('Dirt ground (Terre battue)')), ('Paves a joints larges engravillonnes', _('Gravel wide-joint pavers (Pavés à joints larges engravillonnés)')), ('Dalles alveolaires en gravier', _('Gravel cellular tiles (Dalles alvéolaires en gravier)')), ('Dalles en PEHD remplies de pave', _('HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)')), ('Grilles', _('Grates (Grilles)')), ('Platelages bois', _('Wooden decking (Platelages bois)')), ('Pave en beton poreux', _('Porous concrete pavers (Pavé en béton poreux)')), ('Dalle en beton poreux', _('Porous concrete slab (Dalle en béton poreux)')), ('Enrobes bitumineux poreux', _('Porous asphalt (Enrobés bitumineux poreux)')), ('Betons poreux', _('Porous concrete (Bétons poreux)')), ('Graviers lies a la resine', _('Resin-bound gravel (Graviers liés à la résine)')), ('Graviers concasses lies aux polymeres', _('Polymer-bound crushed gravel (Graviers concassés liés aux polymères)')), ('Revetements EPDM et ESBR', _('EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)')), ('Revetements en beton de ciment non permeables', _('Non-porous cement concrete coatings (Revêtements en béton de ciment non perméables)')), ('Revetements bitumineux non permeables', _('Non-porous asphalt coatings (Revêtements bitumineux non perméables)')), ('Dallages non permeables', _('Non-porous tiling (Dallages non perméables)')), ('Pavages non permeables', _('Non-porous paving (Pavages non perméables)')), ('Autres revetements non permeables', _('Other non-porous coatings (Autres revêtements non perméables)')), ] GEOMETRY_TYPE_CHOICES = [ ('point', _('Point')), ('line', _('Line')), ('polygon', _('Polygon')), ] class NatureRWIAsset(AbstractGeoAsset): geom = models.GeometryField(srid=3812, null=True, blank=True) external_id = models.IntegerField(null=True, blank=True, verbose_name=_("External ID")) code_name = models.CharField(max_length=50, choices=CODE_NAME_CHOICES, null=True, blank=True, verbose_name=_("Code name")) structure_type = models.CharField(max_length=100, choices=STRUCTURE_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Structure type")) geometry_type = models.CharField(max_length=50, choices=GEOMETRY_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Geometry type")) project_manager = models.CharField(max_length=50, choices=PROJECT_MANAGER_CHOICES, null=True, blank=True, verbose_name=_("Project manager")) construction_status = models.CharField(max_length=100, choices=CONSTRUCTION_STATUS_CHOICES, null=True, blank=True, verbose_name=_("Construction status")) commissioning_date = models.DateField(null=True, blank=True, verbose_name=_("Commissioning date")) condition = models.CharField(max_length=100, choices=CONDITION_CHOICES, null=True, blank=True, verbose_name=_("Condition")) administrator = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Administrator")) monitoring = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Monitoring")) comment = models.TextField(null=True, blank=True, verbose_name=_("Comment")) has_filter = models.BooleanField(null=True, blank=True, verbose_name=_("Has filter")) sewer_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Sewer connection")) separate_network_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Separate network connection")) hydro_network_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Hydro network connection")) surface_area = models.FloatField(null=True, blank=True, verbose_name=_("Surface area (m²)")) length = models.FloatField(null=True, blank=True, verbose_name=_("Length (m)")) coating_category = models.CharField(max_length=100, choices=COATING_CATEGORY_CHOICES, null=True, blank=True, verbose_name=_("Coating category")) coating_type = models.CharField(max_length=100, choices=COATING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Coating type")) indexes = [ models.Index(fields=["code"]), ] class Meta: verbose_name = _("RWI Asset") verbose_name_plural = _("RWI Assets") def get_absolute_url(self): return reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': self.id}) class NatureRWIAssetDocument(Document): rwi_asset = models.ForeignKey(NatureRWIAsset, on_delete=models.CASCADE, related_name="documents", verbose_name=_("RWI Asset")) file = models.FileField(upload_to="assets_documents/nature/rwi/", storage=private_storage, verbose_name=_("File"), max_length=255) thumbnail = models.ImageField(upload_to="assets_documents/nature/rwi/", storage=private_storage, null=True, blank=True, max_length=255) class Meta: verbose_name = _("RWI Asset Document") verbose_name_plural = _("RWI Asset Documents")