681 lines
25 KiB
Python
681 lines
25 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils import timezone
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
|
|
|
|
class Team(models.Model):
|
|
"""Une équipe regroupant des utilisateurs."""
|
|
|
|
name = models.CharField(max_length=100, verbose_name=_("Nom"))
|
|
description = models.TextField(blank=True, verbose_name=_("Description"))
|
|
members = models.ManyToManyField(
|
|
User,
|
|
related_name='notification_teams',
|
|
blank=True,
|
|
verbose_name=_("Membres"),
|
|
)
|
|
created_by = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='notification_teams_created',
|
|
verbose_name=_("Créé par"),
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le"))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Équipe")
|
|
verbose_name_plural = _("Équipes")
|
|
ordering = ['name']
|
|
|
|
|
|
class Notification(models.Model):
|
|
"""Une notification pouvant être envoyée à un ou plusieurs utilisateurs ou équipes."""
|
|
|
|
TYPE_MESSAGE = 'message'
|
|
TYPE_REMINDER = 'reminder'
|
|
TYPE_TASK = 'task'
|
|
TYPE_ALERT = 'alert'
|
|
TYPE_INFO = 'info'
|
|
|
|
TYPES = [
|
|
(TYPE_MESSAGE, _('Message')),
|
|
(TYPE_REMINDER, _('Rappel')),
|
|
(TYPE_TASK, _('Tâche')),
|
|
(TYPE_ALERT, _('Alerte')),
|
|
(TYPE_INFO, _('Information')),
|
|
]
|
|
|
|
# Icon and color classes per type (used in templates)
|
|
TYPE_META = {
|
|
TYPE_MESSAGE: {'icon': 'bi-chat-dots', 'color': 'primary'},
|
|
TYPE_REMINDER: {'icon': 'bi-alarm', 'color': 'warning'},
|
|
TYPE_TASK: {'icon': 'bi-check2-square', 'color': 'success'},
|
|
TYPE_ALERT: {'icon': 'bi-exclamation-triangle', 'color': 'danger'},
|
|
TYPE_INFO: {'icon': 'bi-info-circle', 'color': 'info'},
|
|
}
|
|
|
|
notification_type = models.CharField(
|
|
max_length=20,
|
|
choices=TYPES,
|
|
default=TYPE_INFO,
|
|
verbose_name=_("Type"),
|
|
)
|
|
title = models.CharField(max_length=255, verbose_name=_("Titre"))
|
|
content = models.TextField(blank=True, verbose_name=_("Contenu"))
|
|
sender = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='sent_notifications',
|
|
verbose_name=_("Expéditeur"),
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le"))
|
|
expires_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Expire le"),
|
|
help_text=_("Laisser vide pour ne pas faire expirer la notification."),
|
|
)
|
|
# Optional link to an object in the application
|
|
action_url = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
verbose_name=_("Lien d'action"),
|
|
help_text=_("URL relative ou absolue vers laquelle l'utilisateur est redirigé."),
|
|
)
|
|
action_label = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
verbose_name=_("Libellé du lien"),
|
|
)
|
|
# App that generated the notification
|
|
source_app = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
verbose_name=_("Application source"),
|
|
help_text=_("Nom de l'application qui a généré la notification."),
|
|
)
|
|
is_system = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Notification système"),
|
|
help_text=_("Générée automatiquement par le système."),
|
|
)
|
|
is_single_claim = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Prise en charge unique"),
|
|
help_text=_(
|
|
"Si activé, dès qu'un destinataire marque cette notification comme lue, "
|
|
"elle est automatiquement archivée pour tous les autres destinataires."
|
|
),
|
|
)
|
|
source_discussion = models.ForeignKey(
|
|
'Discussion',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='notifications',
|
|
verbose_name=_("Discussion liée"),
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"[{self.get_notification_type_display()}] {self.title}"
|
|
|
|
@property
|
|
def type_icon(self):
|
|
return self.TYPE_META.get(self.notification_type, {}).get('icon', 'bi-bell')
|
|
|
|
@property
|
|
def type_color(self):
|
|
return self.TYPE_META.get(self.notification_type, {}).get('color', 'secondary')
|
|
|
|
@property
|
|
def is_expired(self):
|
|
if self.expires_at is None:
|
|
return False
|
|
return timezone.now() > self.expires_at
|
|
|
|
class Meta:
|
|
verbose_name = _("Notification")
|
|
verbose_name_plural = _("Notifications")
|
|
ordering = ['-created_at']
|
|
|
|
|
|
class NotificationRecipient(models.Model):
|
|
"""Lie une notification à un utilisateur spécifique (notification personnelle)."""
|
|
|
|
notification = models.ForeignKey(
|
|
Notification,
|
|
on_delete=models.CASCADE,
|
|
related_name='recipients',
|
|
verbose_name=_("Notification"),
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='notification_recipients',
|
|
verbose_name=_("Destinataire"),
|
|
)
|
|
read_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Lu le"))
|
|
archived_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Archivé le"))
|
|
reminder_sent_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Rappel envoyé le"))
|
|
|
|
@property
|
|
def is_read(self):
|
|
return self.read_at is not None
|
|
|
|
@property
|
|
def is_archived(self):
|
|
return self.archived_at is not None
|
|
|
|
def mark_read(self):
|
|
if not self.read_at:
|
|
self.read_at = timezone.now()
|
|
self.save(update_fields=['read_at'])
|
|
# Prise en charge unique : archiver la notification chez tous les autres destinataires
|
|
if self.notification.is_single_claim:
|
|
now = timezone.now()
|
|
NotificationRecipient.objects.filter(
|
|
notification=self.notification,
|
|
read_at__isnull=True,
|
|
).exclude(pk=self.pk).update(
|
|
read_at=now,
|
|
archived_at=now,
|
|
)
|
|
|
|
def archive(self):
|
|
if not self.archived_at:
|
|
if not self.read_at:
|
|
self.read_at = timezone.now()
|
|
self.archived_at = timezone.now()
|
|
self.save(update_fields=['read_at', 'archived_at'])
|
|
|
|
def __str__(self):
|
|
return f"{self.notification} → {self.user.username}"
|
|
|
|
class Meta:
|
|
verbose_name = _("Destinataire de notification")
|
|
verbose_name_plural = _("Destinataires de notification")
|
|
unique_together = [('notification', 'user')]
|
|
ordering = ['-notification__created_at']
|
|
|
|
|
|
class TeamNotificationTarget(models.Model):
|
|
"""Lie une notification à une équipe (notification de groupe)."""
|
|
|
|
notification = models.ForeignKey(
|
|
Notification,
|
|
on_delete=models.CASCADE,
|
|
related_name='team_targets',
|
|
verbose_name=_("Notification"),
|
|
)
|
|
team = models.ForeignKey(
|
|
Team,
|
|
on_delete=models.CASCADE,
|
|
related_name='received_notifications',
|
|
verbose_name=_("Équipe"),
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"{self.notification} → {self.team.name}"
|
|
|
|
class Meta:
|
|
verbose_name = _("Notification d'équipe")
|
|
verbose_name_plural = _("Notifications d'équipe")
|
|
unique_together = [('notification', 'team')]
|
|
|
|
|
|
class NotificationAttachment(models.Model):
|
|
"""Pièce jointe (photo ou document) liée à une notification."""
|
|
|
|
def _upload_to(self, filename):
|
|
return f'notifications/{self.notification_id}/{filename}'
|
|
|
|
notification = models.ForeignKey(
|
|
Notification,
|
|
on_delete=models.CASCADE,
|
|
related_name='attachments',
|
|
verbose_name=_("Notification"),
|
|
)
|
|
file = models.FileField(
|
|
upload_to=_upload_to,
|
|
verbose_name=_("Fichier"),
|
|
)
|
|
name = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
verbose_name=_("Nom du fichier"),
|
|
)
|
|
uploaded_by = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='notification_attachments',
|
|
verbose_name=_("Ajouté par"),
|
|
)
|
|
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Ajouté le"))
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.name and self.file:
|
|
import os
|
|
self.name = os.path.basename(self.file.name)
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def is_image(self):
|
|
import os
|
|
ext = os.path.splitext(self.file.name)[1].lower()
|
|
return ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp')
|
|
|
|
def __str__(self):
|
|
return self.name or str(self.file)
|
|
|
|
class Meta:
|
|
verbose_name = _("Pièce jointe")
|
|
verbose_name_plural = _("Pièces jointes")
|
|
ordering = ['uploaded_at']
|
|
|
|
|
|
class TeamNotificationRead(models.Model):
|
|
"""Suivi de la lecture des notifications d'équipe par utilisateur."""
|
|
|
|
team_notification = models.ForeignKey(
|
|
TeamNotificationTarget,
|
|
on_delete=models.CASCADE,
|
|
related_name='reads',
|
|
verbose_name=_("Notification d'équipe"),
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='team_notification_reads',
|
|
verbose_name=_("Utilisateur"),
|
|
)
|
|
read_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Lu le"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Lecture de notification d'équipe")
|
|
verbose_name_plural = _("Lectures de notifications d'équipe")
|
|
unique_together = [('team_notification', 'user')]
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} lu {self.team_notification}"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Discussion / Messaging
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class Discussion(models.Model):
|
|
"""Fil de discussion, éventuellement lié à n'importe quel objet du système."""
|
|
|
|
title = models.CharField(max_length=255, blank=True, verbose_name=_("Titre"))
|
|
created_by = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='discussions_created',
|
|
verbose_name=_("Créé par"),
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Mis à jour le"))
|
|
participants = models.ManyToManyField(
|
|
User,
|
|
related_name='discussions',
|
|
blank=True,
|
|
verbose_name=_("Participants"),
|
|
)
|
|
is_closed = models.BooleanField(default=False, verbose_name=_("Clôturée"))
|
|
|
|
# Lien générique vers n'importe quel objet du système
|
|
content_type = models.ForeignKey(
|
|
ContentType,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
verbose_name=_("Type d'objet lié"),
|
|
)
|
|
object_id = models.PositiveIntegerField(
|
|
null=True, blank=True, verbose_name=_("ID objet lié")
|
|
)
|
|
content_object = GenericForeignKey('content_type', 'object_id')
|
|
|
|
# URL et libellé pré-calculés pour éviter de résoudre le FK générique en template
|
|
object_url = models.CharField(
|
|
max_length=500, blank=True, verbose_name=_("URL objet lié")
|
|
)
|
|
object_label = models.CharField(
|
|
max_length=255, blank=True, verbose_name=_("Libellé objet lié")
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.title or f"Discussion #{self.pk}"
|
|
|
|
def get_absolute_url(self):
|
|
from django.urls import reverse
|
|
return reverse('notifications:discussion_detail', args=[self.pk])
|
|
|
|
def last_message(self):
|
|
return self.messages.filter(is_deleted=False).order_by('-created_at').first()
|
|
|
|
def unread_count_for(self, user):
|
|
return NotificationRecipient.objects.filter(
|
|
user=user,
|
|
notification__source_discussion=self,
|
|
read_at__isnull=True,
|
|
archived_at__isnull=True,
|
|
).count()
|
|
|
|
@property
|
|
def type_info(self):
|
|
"""Retourne les classes de style, l'icône et le libellé du type d'objet lié."""
|
|
model_name = self.content_type.model.lower() if self.content_type else ''
|
|
lbl = (self.object_label or '').lower()
|
|
title_low = (self.title or '').lower()
|
|
|
|
if model_name == 'intervention' or lbl.startswith('intervention') or title_low.startswith('intervention'):
|
|
return {
|
|
'icon': 'bi-tools',
|
|
'bg_class': 'bg-primary-subtle',
|
|
'text_class': 'text-primary',
|
|
'border_class': 'border-primary-subtle',
|
|
'label': _("Intervention"),
|
|
}
|
|
elif model_name == 'observation' or lbl.startswith('observation') or title_low.startswith('observation'):
|
|
return {
|
|
'icon': 'bi-eye-fill',
|
|
'bg_class': 'bg-warning-subtle',
|
|
'text_class': 'text-warning-emphasis',
|
|
'border_class': 'border-warning-subtle',
|
|
'label': _("Observation"),
|
|
}
|
|
elif model_name == 'preparationorder' or 'préparation' in lbl or 'preparation' in lbl:
|
|
return {
|
|
'icon': 'bi-box-seam-fill',
|
|
'bg_class': 'bg-info-subtle',
|
|
'text_class': 'text-info-emphasis',
|
|
'border_class': 'border-info-subtle',
|
|
'label': _("Ordre de préparation"),
|
|
}
|
|
elif model_name == 'project' or lbl.startswith('projet') or title_low.startswith('projet'):
|
|
return {
|
|
'icon': 'bi-kanban-fill',
|
|
'bg_class': 'bg-success-subtle',
|
|
'text_class': 'text-success-emphasis',
|
|
'border_class': 'border-success-subtle',
|
|
'label': _("Projet"),
|
|
}
|
|
else:
|
|
return {
|
|
'icon': 'bi-chat-dots-fill',
|
|
'bg_class': 'bg-indigo-subtle',
|
|
'text_class': 'text-primary',
|
|
'border_class': 'border-primary-subtle',
|
|
'label': _("Générale"),
|
|
}
|
|
|
|
|
|
class Meta:
|
|
verbose_name = _("Discussion")
|
|
verbose_name_plural = _("Discussions")
|
|
ordering = ['-updated_at']
|
|
|
|
|
|
def _message_attachment_upload(instance, filename):
|
|
return f'discussions/{instance.message.discussion_id}/{filename}'
|
|
|
|
|
|
class Message(models.Model):
|
|
"""Message au sein d'une discussion."""
|
|
|
|
discussion = models.ForeignKey(
|
|
Discussion,
|
|
on_delete=models.CASCADE,
|
|
related_name='messages',
|
|
verbose_name=_("Discussion"),
|
|
)
|
|
author = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='discussion_messages',
|
|
verbose_name=_("Auteur"),
|
|
)
|
|
content = models.TextField(verbose_name=_("Contenu"))
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Envoyé le"))
|
|
is_deleted = models.BooleanField(default=False, verbose_name=_("Supprimé"))
|
|
|
|
def __str__(self):
|
|
author_name = self.author.username if self.author else 'système'
|
|
return f"Message de {author_name} dans {self.discussion}"
|
|
|
|
class Meta:
|
|
verbose_name = _("Message")
|
|
verbose_name_plural = _("Messages")
|
|
ordering = ['created_at']
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Règles de notification (configurable par l'administrateur)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class NotificationRule(models.Model):
|
|
"""
|
|
Règle de notification configurable par un administrateur.
|
|
|
|
Permet de déclencher automatiquement l'envoi de notifications
|
|
à des équipes ou utilisateurs définis lorsqu'un événement particulier
|
|
se produit dans l'application (ex. : annulation d'une intervention).
|
|
|
|
Les modèles de titre et contenu acceptent des variables Python entre
|
|
accolades, par exemple :
|
|
« Intervention {intervention_code} annulée ({cancellation_type_label}) »
|
|
"""
|
|
|
|
# ── Événements supportés ──────────────────────────────────────────────────
|
|
EVENT_INTERVENTION_CANCELED = 'intervention_canceled'
|
|
|
|
TRIGGER_EVENTS = [
|
|
(EVENT_INTERVENTION_CANCELED, _('Intervention annulée')),
|
|
]
|
|
|
|
# Choix de type d'annulation, partagés avec interventions.models
|
|
CANCELLATION_TYPE_CHOICES = [
|
|
('', _('Tous (pas de filtre)')),
|
|
('duplicate', _('Doublon')),
|
|
('to_redirect', _('A rediriger')),
|
|
('not_our_assets', _('Pas sous notre gestion')),
|
|
('non_relevant', _('Non-relevant')),
|
|
('other', _('Autre')),
|
|
]
|
|
|
|
# ── Identification ────────────────────────────────────────────────────────
|
|
name = models.CharField(max_length=200, verbose_name=_("Nom de la règle"))
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
|
|
|
# ── Déclencheur ───────────────────────────────────────────────────────────
|
|
trigger_event = models.CharField(
|
|
max_length=50,
|
|
choices=TRIGGER_EVENTS,
|
|
verbose_name=_("Événement déclencheur"),
|
|
)
|
|
trigger_cancellation_type = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
choices=CANCELLATION_TYPE_CHOICES,
|
|
verbose_name=_("Filtre : type d'annulation"),
|
|
help_text=_(
|
|
"Laisser vide pour déclencher sur tout type d'annulation. "
|
|
"Sélectionner une valeur pour n'agir que sur ce type précis."
|
|
),
|
|
)
|
|
|
|
# ── Contenu de la notification ────────────────────────────────────────────
|
|
notification_type = models.CharField(
|
|
max_length=20,
|
|
choices=Notification.TYPES,
|
|
default=Notification.TYPE_ALERT,
|
|
verbose_name=_("Type de notification"),
|
|
)
|
|
title_template = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("Modèle de titre"),
|
|
help_text=_(
|
|
"Variables disponibles : {intervention_code}, {intervention_title}, "
|
|
"{cancellation_type_label}"
|
|
),
|
|
)
|
|
content_template = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Modèle de contenu"),
|
|
help_text=_(
|
|
"Variables disponibles : {intervention_code}, {intervention_title}, "
|
|
"{cancellation_type_label}, {cancellation_reason}"
|
|
),
|
|
)
|
|
|
|
# ── Destinataires ─────────────────────────────────────────────────────────
|
|
recipient_teams = models.ManyToManyField(
|
|
'contracts.CompanyTeam',
|
|
blank=True,
|
|
related_name='notification_rules',
|
|
verbose_name=_("Équipes destinataires"),
|
|
help_text=_("Les notifications seront envoyées à tous les membres (CompanyMember) de ces équipes."),
|
|
)
|
|
recipient_users = models.ManyToManyField(
|
|
User,
|
|
blank=True,
|
|
related_name='notification_rules',
|
|
verbose_name=_("Utilisateurs destinataires supplémentaires"),
|
|
)
|
|
|
|
# ── Comportement ────────────────────────────────────────────────────────
|
|
single_claim = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Prise en charge unique"),
|
|
help_text=_(
|
|
"Si activé, dès qu'un destinataire marque la notification comme lue (prise en charge), "
|
|
"elle est automatiquement archivée pour tous les autres destinataires."
|
|
),
|
|
)
|
|
|
|
# ── Métadonnées ────────────────────────────────────────────────────────
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créée le"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Mise à jour le"))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Règle de notification")
|
|
verbose_name_plural = _("Règles de notification")
|
|
ordering = ['name']
|
|
|
|
|
|
class MessageAttachment(models.Model):
|
|
"""Pièce jointe liée à un message de discussion."""
|
|
|
|
message = models.ForeignKey(
|
|
Message,
|
|
on_delete=models.CASCADE,
|
|
related_name='attachments',
|
|
verbose_name=_("Message"),
|
|
)
|
|
file = models.FileField(
|
|
upload_to=_message_attachment_upload,
|
|
verbose_name=_("Fichier"),
|
|
)
|
|
name = models.CharField(max_length=255, blank=True, verbose_name=_("Nom du fichier"))
|
|
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Ajouté le"))
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.name and self.file:
|
|
import os
|
|
self.name = os.path.basename(self.file.name)
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def is_image(self):
|
|
import os
|
|
ext = os.path.splitext(self.file.name)[1].lower()
|
|
return ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp')
|
|
|
|
def __str__(self):
|
|
return self.name or str(self.file)
|
|
|
|
class Meta:
|
|
verbose_name = _("Pièce jointe de message")
|
|
verbose_name_plural = _("Pièces jointes de messages")
|
|
ordering = ['uploaded_at']
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Notifications Push (Web Push API / VAPID)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class WebPushSubscription(models.Model):
|
|
"""Abonnement d'un appareil aux notifications push pour un utilisateur."""
|
|
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='webpush_subscriptions',
|
|
verbose_name=_("Utilisateur"),
|
|
)
|
|
endpoint = models.TextField(
|
|
unique=True,
|
|
verbose_name=_("Endpoint Push"),
|
|
help_text=_("URL unique de réception push fournie par le navigateur."),
|
|
)
|
|
p256dh = models.CharField(
|
|
max_length=255,
|
|
verbose_name=_("Clé p256dh"),
|
|
help_text=_("Clé publique du client."),
|
|
)
|
|
auth = models.CharField(
|
|
max_length=100,
|
|
verbose_name=_("Secret auth"),
|
|
help_text=_("Secret d'authentification pour le chiffrement."),
|
|
)
|
|
user_agent = models.CharField(
|
|
max_length=500,
|
|
blank=True,
|
|
verbose_name=_("User Agent"),
|
|
)
|
|
browser_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
verbose_name=_("Navigateur"),
|
|
)
|
|
os_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
verbose_name=_("Système d'exploitation"),
|
|
)
|
|
device_name = models.CharField(
|
|
max_length=150,
|
|
blank=True,
|
|
verbose_name=_("Nom de l'appareil"),
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Enregistré le"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Mis à jour le"))
|
|
|
|
def __str__(self):
|
|
device = self.device_name or self.browser_name or _("Appareil inconnu")
|
|
return f"{self.user.username} ({device})"
|
|
|
|
class Meta:
|
|
verbose_name = _("Abonnement Web Push")
|
|
verbose_name_plural = _("Abonnements Web Push")
|
|
ordering = ['-created_at']
|