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

251 lines
8.3 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.

import secrets
from django.contrib.gis.db import models
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.template.defaultfilters import truncatechars
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
class RoadAuthorization(models.Model):
AUTH_TYPE = (
(2, _('Affichage (panneau, ...)')),
(21, _('Terrasse')),
(22, _('Mobilier urbain')),
(23, _("Œuvre d'art")),
(32, _('Nouvel Emplacement PMR')),
(33, _('Nouvelle Zone de livraison')),
(34, _('Tournage audiovisuel')),
(100, _('Evénement')),
(51, _('Plante grimpante')),
(52, _("Pied d'arbre")),
)
AUTH_STATUS = (
(1, _('À traiter')),
(2, _('En cours')),
(3, _('Autorisé')),
(4, _('Refusé')),
(5, _('Clôturé')),
)
LANGUAGE_CHOICES = [('fr', 'Français'), ('nl', 'Nederlands'), ('en', 'English')]
code = models.CharField(max_length=50, default='')
status = models.IntegerField(default=1, choices=AUTH_STATUS)
language = models.CharField(
max_length=5,
default='fr',
choices=LANGUAGE_CHOICES,
verbose_name=_('Langue'),
)
is_email_verified = models.BooleanField(
default=False,
verbose_name=_("Email vérifié"),
help_text=_("Indique si l'adresse email du demandeur a été vérifiée."),
)
type = models.IntegerField(_("Type d'occupation"), choices=AUTH_TYPE)
description = models.TextField(_('Description de la demande'))
applicant_name = models.CharField(_('Nom'), max_length=100)
applicant_email = models.EmailField(_('Email'))
applicant_address = models.CharField(_('Adresse'), max_length=300)
applicant_enterprise = models.CharField(_('Entreprise'), max_length=100, null=True, blank=True)
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
agent = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
verbose_name=_('Agent traitant'),
)
agent_comment = models.TextField(_('Commentaire'), null=True, blank=True)
location = models.CharField(_('Localisation – Adresse'), max_length=500)
geom = models.MultiPolygonField(_('Localisation'), srid=4326)
@property
def end_date(self):
return self.dates.all().latest('end_date').end_date
@property
def date_status(self):
"""
Returns:
1 – older than 2 years
2 – past (< 2 years)
3 – future
4 – active (ongoing right now)
"""
datestatus = 1
now = datetime.now().astimezone()
for d in self.dates.all():
end = d.end_date
if end is None:
# No end date → treat as open-ended (active if already started)
if d.begin_date <= now:
return 4
else:
datestatus = max(3, datestatus)
elif end >= now and d.begin_date <= now:
return 4
elif d.begin_date >= now:
datestatus = max(3, datestatus)
elif end > (now - relativedelta(years=2)):
datestatus = max(2, datestatus)
return datestatus
@property
def truncated_description(self):
return truncatechars(self.description, 100)
class Meta:
ordering = ['status', '-code']
verbose_name = _('Autorisation de voirie')
verbose_name_plural = _('Autorisations de voirie')
# Maps AUTH_TYPE values to their 2-digit code prefix
TYPE_PREFIX = {
2: '01',
21: '02',
22: '03',
23: '04',
32: '05',
33: '06',
34: '07',
100: '08',
51: '09',
52: '10',
}
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.code and self.pk:
year = self.creation_date.year if self.creation_date else datetime.now().year
prefix = self.TYPE_PREFIX.get(self.type, '00')
# Count same-type authorizations created this year (excl. self) that already have a code
count = RoadAuthorization.objects.filter(
type=self.type,
creation_date__year=year,
).exclude(pk=self.pk).exclude(code='').count()
seq = count + 1
seq_str = f"{seq:03d}" if seq < 1000 else str(seq)
self.code = f"AUTH-{year}-{prefix}{seq_str}"
RoadAuthorization.objects.filter(pk=self.pk).update(code=self.code)
def __str__(self):
return f"{self.code} – {self.truncated_description}" if self.code else self.truncated_description
def validate_file_size(file):
limit_mb = 15
if file.size > limit_mb * 1024 * 1024:
raise ValidationError(_("La taille maximale du fichier est de %(limit)s Mo.") % {'limit': limit_mb})
class RoadAuthorizationDocument(models.Model):
file = models.FileField(
verbose_name=_('Fichier'),
upload_to='authorizations_documents/',
validators=[validate_file_size],
)
description = models.CharField(_('Description'), max_length=200)
authorization = models.ForeignKey(
RoadAuthorization,
on_delete=models.CASCADE,
related_name='documents',
)
class Meta:
verbose_name = _('Document')
verbose_name_plural = _('Documents')
def __str__(self):
return self.description
class RoadAuthorizationDate(models.Model):
authorization = models.ForeignKey(
RoadAuthorization,
on_delete=models.CASCADE,
related_name='dates',
)
begin_date = models.DateTimeField(_("Début de l'occupation"))
end_date = models.DateTimeField(_("Fin de l'occupation"), null=True, blank=True)
class Meta:
verbose_name = _("Date d'occupation")
verbose_name_plural = _("Dates d'occupation")
ordering = ['begin_date']
def __str__(self):
begin_str = f"{self.begin_date:%d/%m/%Y %H:%M}" if self.begin_date else "—"
end_str = f"{self.end_date:%d/%m/%Y %H:%M}" if self.end_date else "—"
return f"{begin_str} → {end_str}"
# ---------------------------------------------------------------------------
# Email verification
# ---------------------------------------------------------------------------
class VerifiedEmail(models.Model):
"""Whitelist des adresses email ayant été vérifiées."""
email = models.EmailField(unique=True, verbose_name=_("Email"))
verified_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Email vérifié")
verbose_name_plural = _("Emails vérifiés")
def __str__(self):
return self.email
class EmailVerificationToken(models.Model):
"""Code à 6 chiffres de vérification d'email lié à une demande d'autorisation en attente."""
authorization = models.OneToOneField(
RoadAuthorization,
on_delete=models.CASCADE,
related_name='verification_token',
)
code = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
attempts = models.PositiveSmallIntegerField(default=0)
TOKEN_VALIDITY_MINUTES = 10
MAX_ATTEMPTS = 5
class Meta:
verbose_name = "Token de vérification"
verbose_name_plural = "Tokens de vérification"
def is_expired(self):
return timezone.now() > self.expires_at
def is_locked(self):
return self.attempts >= self.MAX_ATTEMPTS
@classmethod
def create_for_authorization(cls, authorization):
code = f"{secrets.randbelow(1_000_000):06d}"
expires_at = timezone.now() + timedelta(minutes=cls.TOKEN_VALIDITY_MINUTES)
# Supprimer tout token existant pour cette autorisation
cls.objects.filter(authorization=authorization).delete()
return cls.objects.create(
authorization=authorization,
code=code,
expires_at=expires_at,
)
class Meta:
verbose_name = _("Code de vérification")
verbose_name_plural = _("Codes de vérification")
def __str__(self):
return f"Code pour {self.authorization}"