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

382 lines
No EOL
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ---------------------
# Clean Models
# ---------------------
from django.contrib.gis.db import models
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel
from common.models import Municipality, Document
from common.storages import PrivateMediaStorage
FIXATION_TYPE_CHOICES = (
('cemented', _('Cemented to ground')),
('bolted', _('Bolted to ground')),
('pole_mounted', _('Pole-mounted')),
('wall_mounted', _('Wall-mounted')),
('freestanding', _('Freestanding')),
('other', _('Other')),
)
GLASS_TYPE_CHOICES = (
('clear', _('Clear glass')),
('colored', _('Coloured glass')),
('mixed', _('Mixed')),
)
private_storage = PrivateMediaStorage()
# Clean Location
class CleanLocation(AbstractLocation):
geom = models.MultiPolygonField(srid=3812, null=True,blank=True)
municipality = models.ForeignKey(Municipality, on_delete=models.SET_NULL, null=True, blank=True, related_name="clean_locations")
road_code = models.CharField(max_length=50, null=True, blank=True)
section_code = models.CharField(max_length=50, null=True, blank=True)
street_level = models.CharField(max_length=50, null=True, blank=True)
def get_active_assets(self):
# Retrieve associated assets
litter_bins = self.litter_bins.filter(status='active')
glass_containers = self.glass_containers.filter(status='active')
return {
"litter_bins": litter_bins,
"glass_containers": glass_containers,
}
def get_all_assets(self):
# Retrieve associated assets
litter_bins = self.litter_bins.all()
glass_containers = self.glass_containers.all()
return {
"litter_bins": litter_bins,
"glass_containers": glass_containers,
}
def get_direct_child_assets(self):
# Retrieve associated assets
litter_bins = self.litter_bins.all()
glass_containers = self.glass_containers.all()
return {
"litter_bins": litter_bins,
"glass_containers": glass_containers,
}
def get_absolute_url(self):
return reverse("assets:clean_location_detail", kwargs={"location_id": self.id})
# ---------------------
# Litter Bin
# ---------------------
class CleanLitterBinModel(AbstractAssetModel):
"""Reference model / template for a litter bin."""
color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour"))
ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code"))
material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material"))
volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)"))
has_cover = models.BooleanField(default=False, verbose_name=_("Cover"))
has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray"))
class Meta:
verbose_name = _("Litter bin model")
verbose_name_plural = _("Litter bin models")
ordering = ["code"]
def __str__(self):
return f"{self.code} - {self.get_name()}"
class CleanLitterBin(AbstractGeoAsset):
"""Street litter bin / waste basket."""
location = models.ForeignKey(
CleanLocation,
related_name="litter_bins",
on_delete=models.PROTECT,
verbose_name=_("Location"),
)
model = models.ForeignKey(
CleanLitterBinModel,
on_delete=models.SET_NULL,
null=True, blank=True,
verbose_name=_("Model"),
)
fixation_type = models.CharField(
max_length=50,
choices=FIXATION_TYPE_CHOICES,
null=True, blank=True,
verbose_name=_("Fixation type"),
)
color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour"))
material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material"))
volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)"))
has_cover = models.BooleanField(default=False, verbose_name=_("Cover"))
has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray"))
has_extinguisher = models.BooleanField(default=False, verbose_name=_("Built-in extinguisher stub (éteignoir)"))
emptying_frequency = models.CharField(
max_length=200, null=True, blank=True,
verbose_name=_("Emptying frequency"),
)
collection_route = models.CharField(
max_length=200, null=True, blank=True,
verbose_name=_("Collection route (tournée)"),
)
external_id = models.IntegerField(
null=True, blank=True,
verbose_name=_("External ID (source system)"),
)
remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks"))
class Meta:
verbose_name = _("Litter bin")
verbose_name_plural = _("Litter bins")
indexes = [
models.Index(fields=["code"]),
]
def get_absolute_url(self):
return reverse("assets:clean_assets_detail", kwargs={"asset_model": "litterbin", "asset_id": self.id})
# ---------------------
# Glass Container
# ---------------------
class CleanGlassContainerModel(AbstractAssetModel):
"""Reference model / template for a glass recycling container."""
color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour"))
ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code"))
material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material"))
capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)"))
is_underground = models.BooleanField(default=False, verbose_name=_("Underground"))
nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings"))
class Meta:
verbose_name = _("Glass container model")
verbose_name_plural = _("Glass container models")
ordering = ["code"]
def __str__(self):
return f"{self.code} - {self.get_name()}"
class CleanGlassContainer(AbstractGeoAsset):
"""Glass recycling container (bottle bank)."""
location = models.ForeignKey(
CleanLocation,
related_name="glass_containers",
on_delete=models.PROTECT,
verbose_name=_("Location"),
)
model = models.ForeignKey(
CleanGlassContainerModel,
on_delete=models.SET_NULL,
null=True, blank=True,
verbose_name=_("Model"),
)
glass_type = models.CharField(
max_length=50,
choices=GLASS_TYPE_CHOICES,
null=True, blank=True,
verbose_name=_("Accepted glass type"),
)
capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)"))
is_underground = models.BooleanField(default=False, verbose_name=_("Underground"))
nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings"))
color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour"))
material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material"))
emptying_frequency = models.CharField(
max_length=200, null=True, blank=True,
verbose_name=_("Emptying frequency"),
)
operator = models.CharField(
max_length=255, null=True, blank=True,
verbose_name=_("Operator / manager"),
)
remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks"))
class Meta:
verbose_name = _("Glass container")
verbose_name_plural = _("Glass containers")
indexes = [
models.Index(fields=["code"]),
]
def get_absolute_url(self):
return reverse("assets:clean_assets_detail", kwargs={"asset_model": "glasscontainer", "asset_id": self.id})
class CleanLocationContract(models.Model):
location = models.ForeignKey(CleanLocation, on_delete=models.CASCADE, related_name="contracts")
contract = models.ForeignKey('contracts.Contract', on_delete=models.CASCADE, related_name="clean_location_contracts")
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
maintenance_type = models.CharField(max_length=50, choices=[('clean', 'Nettoyage'), ('collection', 'Ramassage'), ('other', 'Autre')], default='other')
class Meta:
verbose_name = "Clean Location Contract"
verbose_name_plural = "Clean Location Contracts"
# ---------------------
# Litter Bin Photo
# ---------------------
class CleanLitterBinPhoto(models.Model):
"""Photo associated with a litter bin."""
litter_bin = models.ForeignKey(
CleanLitterBin,
on_delete=models.CASCADE,
related_name="photos",
verbose_name=_("Litter bin"),
)
photo = models.ImageField(
upload_to="clean/litter_bin_photos/",
null=True, blank=True,
verbose_name=_("Photo"),
storage=private_storage,
)
source_url = models.URLField(
max_length=500, null=True, blank=True,
verbose_name=_("Source URL (original)"),
)
order = models.PositiveSmallIntegerField(default=1, verbose_name=_("Order"))
taken_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Taken at"))
caption = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Caption"))
class Meta:
verbose_name = _("Litter bin photo")
verbose_name_plural = _("Litter bin photos")
ordering = ["litter_bin", "order"]
def __str__(self):
return f"Photo {self.order} – {self.litter_bin}"
# ---------------------
# Litter Bin Observation (sol + accessibilité)
# ---------------------
NEIGHBOURHOOD_TYPE_CHOICES = (
('importante_ponctuelle', _('Important – ponctuel')),
('importante_continue', _('Important – continu')),
('faible', _('Faible')),
('autre', _('Autre')),
)
PEDESTRIAN_DENSITY_CHOICES = (
('high', _('Élevée')),
('medium', _('Moyenne')),
('low', _('Faible')),
)
AGENT_ACCESS_CHOICES = (
('normal', _('Normal')),
('difficult', _('Difficile')),
('impossible', _('Impossible')),
)
POINT_OF_INTEREST_CHOICES = (
('bus_stop', _('Arrêt Bus/Tram')),
('park', _('Parc')),
('school', _('École')),
('commerce', _('Commerce')),
('monument', _('Monument')),
('pedestrian_crossing', _('Passage piéton')),
('place', _('Place')),
('none', _('Aucun')),
('other', _('Autre')),
)
class CleanLitterBinObservation(models.Model):
"""Field observation for a litter bin: ground conditions and accessibility."""
litter_bin = models.ForeignKey(
CleanLitterBin,
on_delete=models.CASCADE,
related_name="observations",
verbose_name=_("Litter bin"),
)
observation_datetime = models.DateTimeField(null=True, blank=True, verbose_name=_("Observation date/time"))
observer_note = models.TextField(null=True, blank=True, verbose_name=_("Observer notes"))
# --- Ground / sol ---
sol_pente_trop_forte = models.BooleanField(default=False, verbose_name=_("Slope too steep"))
sol_instable = models.BooleanField(default=False, verbose_name=_("Unstable ground"))
sol_reflechissant = models.BooleanField(default=False, verbose_name=_("Reflective surface"))
sol_glissant = models.BooleanField(default=False, verbose_name=_("Slippery surface"))
sol_fentes = models.BooleanField(default=False, verbose_name=_("Cracks in ground"))
sol_entrave = models.BooleanField(default=False, verbose_name=_("Ground obstacle"))
sol_dalles_podo = models.BooleanField(default=False, verbose_name=_("Tactile paving (dalles podotactiles)"))
sol_pente_transversale = models.FloatField(null=True, blank=True, verbose_name=_("Transversal slope (%)"))
sol_pente_longitudinale = models.FloatField(null=True, blank=True, verbose_name=_("Longitudinal slope (%)"))
sol_remarque = models.TextField(null=True, blank=True, verbose_name=_("Ground remarks"))
# --- Accessibility / accessibilité ---
acc_type_quartier = models.CharField(
max_length=50,
choices=NEIGHBOURHOOD_TYPE_CHOICES,
null=True, blank=True,
verbose_name=_("Neighbourhood type"),
)
acc_densite_flux_pieton = models.CharField(
max_length=50,
choices=PEDESTRIAN_DENSITY_CHOICES,
null=True, blank=True,
verbose_name=_("Pedestrian flow density"),
)
# Stored as a JSON list of strings (e.g. ["bus_stop", "park"])
acc_point_interet = models.JSONField(
default=list, blank=True,
verbose_name=_("Points of interest"),
)
acc_approche_2m = models.BooleanField(default=False, verbose_name=_("Clear 2 m approach"))
acc_voirie_a_traverser = models.BooleanField(default=False, verbose_name=_("Road to cross"))
acc_poubelle_isolee = models.BooleanField(default=False, verbose_name=_("Isolated bin"))
acc_gene_flux_routier = models.BooleanField(default=False, verbose_name=_("Obstructs road traffic"))
acc_gene_flux_pieton = models.BooleanField(default=False, verbose_name=_("Obstructs pedestrian flow"))
acc_chemin_logique = models.BooleanField(default=False, verbose_name=_("Logical path (itinéraire logique)"))
acc_contraste = models.BooleanField(default=False, verbose_name=_("Visual contrast"))
acc_indications_tri = models.BooleanField(default=False, verbose_name=_("Sorting indicators present"))
acc_agent_vehicule = models.CharField(
max_length=20,
choices=AGENT_ACCESS_CHOICES,
null=True, blank=True,
verbose_name=_("Agent access – vehicle"),
)
acc_agent_pied = models.CharField(
max_length=20,
choices=AGENT_ACCESS_CHOICES,
null=True, blank=True,
verbose_name=_("Agent access – on foot"),
)
acc_point_interet_autre = models.CharField(
max_length=255, null=True, blank=True,
verbose_name=_("Other point of interest"),
)
# Integer relevance/obstruction codes stored as JSON list (e.g. [1, 3])
acc_pertinence_placement = models.JSONField(
default=list, blank=True,
verbose_name=_("Placement relevance codes"),
)
acc_entrave = models.JSONField(
default=list, blank=True,
verbose_name=_("Obstruction codes"),
)
acc_point_interet_remarque = models.TextField(null=True, blank=True, verbose_name=_("Point of interest remarks"))
acc_remarque = models.TextField(null=True, blank=True, verbose_name=_("Accessibility remarks"))
class Meta:
verbose_name = _("Litter bin observation")
verbose_name_plural = _("Litter bin observations")
ordering = ["-observation_datetime"]
def __str__(self):
return f"Observation {self.observation_datetime} – {self.litter_bin}"