loko/streetup/assets/models/outdoor.py

177 lines
9.2 KiB
Python

# -----------------------------
# Outdoor Spaces & Assets Models
# -----------------------------
from django.contrib.gis.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from assets.models import AbstractLocation, AbstractGeoAsset, ASSET_STATUS_CHOICES
from common.models import Document
from common.storages import PrivateMediaStorage
private_storage = PrivateMediaStorage()
SITE_TYPE_CHOICES = (
('playground', _('Aire / Plaine de jeux')),
('sports_ground', _('Terrain de sport extérieur')),
('multisports_agora', _('Terrain multisports / City-stadium')),
('skatepark_pumptrack', _('Skatepark / Pumptrack / Bowl')),
('outdoor_fitness', _('Fitness plein air / Street Workout')),
('open_air_theater', _('Théâtre de verdure / Scène extérieure')),
('park_recreation', _('Parc & Espace récréatif')),
('dog_park', _('Espace canin / Canisite')),
('cemetery', _('Cimetière communal')),
('event_plaza', _('Place publique / Zone événementielle')),
('other', _('Autre espace plein air')),
)
OUTDOOR_ASSET_TYPE_CHOICES = (
('play_structure', _('Structure combinée / Tour de jeu')),
('swing', _('Balançoire / Portique')),
('spring_rider', _('Jeu à ressort / Bascule')),
('slide', _('Toboggan')),
('zip_line', _('Tyrolienne')),
('amortizing_surface', _('Sol amortissant / Réception')),
('sports_goal', _('But / Cage / Panneau de basket')),
('fitness_station', _('Agrès de fitness outdoor / Workout')),
('skate_module', _('Module de skate / Rampe / Rail')),
('stage_platform', _('Scène extérieure / Podium')),
('audience_seating', _('Gradins / Bancs de spectateurs')),
('event_power_box', _('Borne foraine / Armoire électrique événementielle')),
('outdoor_lighting', _('Éclairage plein air / Projecteur scénique')),
('bench_table', _('Banc / Table de pique-nique')),
('shade_shelter', _('Kiosque / Voile d\'ombrage / Abri')),
('drinking_fountain', _('Fontaine d\'eau potable')),
('fence_gate', _('Clôture / Portillon de sécurité')),
('info_panel', _('Panneau d\'information / Consignes')),
('other', _('Autre équipement plein air')),
)
NORM_CHOICES = (
('EN_1176', _('EN 1176 - Équipements & sols d\'aires de jeux')),
('EN_16630', _('EN 16630 - Modules de fitness d\'extérieur')),
('EN_14974', _('EN 14974 - Équipements de skatepark')),
('EN_15312', _('EN 15312 - Équipements multisports en accès libre')),
('other', _('Autre norme / Non spécifié')),
)
class OutdoorLocation(AbstractLocation):
"""
Modèle représentant un Espace Public Extérieur (Plaine de jeux, Terrain de sport, Théâtre plein air, Parc, etc.).
Peut comporter une arborescence (ex: Parc municipal > Aire de jeux > Zone toboggan).
"""
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations", verbose_name=_("Localisation parente"))
site_type = models.CharField(max_length=50, choices=SITE_TYPE_CHOICES, default='playground', verbose_name=_("Type de site plein air"))
is_main_location = models.BooleanField(default=False, verbose_name=_("Localisation principale"))
address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse complète"))
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
# Propriété & Gestion
owner = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propriétaire"))
manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="managed_outdoor_locations", verbose_name=_("Gestionnaire référent"))
occupant_entity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Direction / Service gestionnaire"))
# Caractéristiques physiques & aménagements
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface totale (m²)"))
max_capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Jauge / Capacité d'accueil"))
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
has_water_point = models.BooleanField(default=False, verbose_name=_("Point d'eau potable"))
has_power_supply = models.BooleanField(default=False, verbose_name=_("Alimentation électrique disponible"))
is_fenced = models.BooleanField(default=False, verbose_name=_("Site clôturé / Sécurisé"))
class Meta:
verbose_name = _("Espace public d'extérieur")
verbose_name_plural = _("Espaces publics d'extérieur")
ordering = ['code', 'name_fr']
def get_child_locations(self):
return self.child_locations.all()
def get_active_assets(self):
def collect_assets(loc):
assets = list(loc.location_assets.filter(status='active'))
for child in loc.child_locations.all():
assets += collect_assets(child)
return assets
return collect_assets(self)
def get_all_assets(self):
def collect_assets(loc):
if loc.parent_location is None:
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'site').all() | loc.all_outdoor_assets.select_related('location', 'parent_asset', 'site').filter(location__isnull=True))
else:
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'site').all())
for child in loc.child_locations.all():
assets += collect_assets(child)
return assets
seen = set()
unique_assets = []
for a in collect_assets(self):
if a.id not in seen:
seen.add(a.id)
unique_assets.append(a)
return unique_assets
def get_direct_child_assets(self):
return self.location_assets.all()
def get_absolute_url(self):
return reverse('assets:outdoor_location_detail', kwargs={'pk': self.pk})
class OutdoorAsset(AbstractGeoAsset):
"""
Modèle représentant un asset ou équipement physique d'extérieur (Structure de jeu, Sol amortissant, Agrès, Cage, Scène...).
Les assets peuvent former une arborescence parent-enfant.
"""
site = models.ForeignKey(OutdoorLocation, on_delete=models.CASCADE, related_name="all_outdoor_assets", verbose_name=_("Site / Espace principal"))
location = models.ForeignKey(OutdoorLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name="location_assets", verbose_name=_("Localisation spécifique"))
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets", verbose_name=_("Asset parent"))
asset_type = models.CharField(max_length=50, choices=OUTDOOR_ASSET_TYPE_CHOICES, default='play_structure', verbose_name=_("Type d'asset / Équipement"))
# Norme & Sécurité réglementaire
norm = models.CharField(max_length=50, choices=NORM_CHOICES, blank=True, null=True, verbose_name=_("Norme de sécurité appliquée"))
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle visuel/opérationnel"))
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle requis"))
last_annual_inspection = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle annuel certifié"))
next_annual_inspection = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle annuel certifié"))
certifying_body = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Organisme de contrôle certifié"))
# Sols amortissants & hauteurs d'impact
impact_surface_type = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Type de sol d'amortissement"))
critical_fall_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur critique de chute (m)"))
class Meta:
verbose_name = _("Asset / Équipement plein air")
verbose_name_plural = _("Assets / Équipements plein air")
ordering = ['site', 'code']
def get_absolute_url(self):
return reverse('assets:outdoor_assets_detail', kwargs={'asset_model': 'outdoorasset', 'asset_id': self.pk})
def get_child_assets(self):
return self.child_assets.all()
def upload_to_outdoor(instance, filename):
return f"outdoor/{instance.site.code}/{filename}"
class OutdoorLocationDocument(Document):
"""
Documents associés à un espace extérieur (certificats de conformité, registres de sécurité, plans).
"""
site = models.ForeignKey(OutdoorLocation, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Espace extérieur"))
file = models.FileField(upload_to=upload_to_outdoor, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
document_type = models.CharField(max_length=50, default='other', verbose_name=_("Type de document"))
class Meta:
verbose_name = _("Document d'espace extérieur")
verbose_name_plural = _("Documents d'espace extérieur")