382 lines
No EOL
15 KiB
Python
382 lines
No EOL
15 KiB
Python
# ---------------------
|
||
# 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}" |