177 lines
9.2 KiB
Python
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")
|