loko/streetup/assets/models/nature.py
2026-07-22 14:48:40 +02:00

501 lines
22 KiB
Python

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")