251 lines
8.4 KiB
Python
251 lines
8.4 KiB
Python
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 d'occupation de l'espace public")
|
||
verbose_name_plural = _("Autorisations d'occupation de l'espace public")
|
||
|
||
# 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}"
|