loko/streetup/home/models.py

338 lines
12 KiB
Python

from django.db import models
from django.utils.translation import gettext_lazy as _
# Widget code choices
WIDGET_CODE_CHOICES = [
('intervention_stats', _('Statistiques interventions')),
('contracts_summary', _('Résumé contrats')),
('map_today', _('Carte du jour')),
('operator_shortcuts', _('Raccourcis opérateur')),
('assets_links', _('Liens assets')),
('stock_summary', _('Résumé stock')),
('observer_shortcuts', _('Raccourcis observateur')),
('incident_counter', _('Compteur incidents à traiter')),
('intervention_to_verify', _('Compteur interventions à vérifier')),
('intervention_to_plan', _('Compteur interventions à planifier')),
('intervention_to_approve', _('Compteur interventions à approuver')),
('intervention_checked_to_approve', _('Compteur interventions vérifiées à approuver')),
('intervention_finished', _('Compteur interventions terminées')),
('intervention_on_pause', _('Compteur interventions en pause')),
('stock_actions', _('Actions stock')),
]
# Condition type choices
CONDITION_TYPE_CHOICES = [
('role', _('Rôle')),
('team', _('Équipe')),
('contract', _('Contrat')),
('thematic', _('Thématique')),
('is_intern', _('Statut interne')),
]
class HomeWidget(models.Model):
"""Définit un widget disponible pour la page d'accueil."""
widget_code = models.CharField(
max_length=50,
choices=WIDGET_CODE_CHOICES,
unique=True,
verbose_name=_("Code du widget")
)
default_template_override = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_("Template par défaut (surcharge)"),
help_text=_("Chemin du template optionnel (sinon home/widgets/<widget_code>.html)")
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Actif")
)
class Meta:
verbose_name = _("Widget home")
verbose_name_plural = _("Widgets home")
ordering = ['widget_code']
def __str__(self):
return self.get_widget_code_display()
def get_default_template_name(self):
"""Retourne le nom du template par défaut pour ce widget."""
if self.default_template_override:
return self.default_template_override
return f'home/widgets/{self.widget_code}.html'
def resolve_for_user(self, user_config, user_team, user_contracts):
"""
Résout le widget pour un utilisateur donné.
Args:
user_config: Instance de UserConfig
user_team: Instance de CompanyTeam ou None
user_contracts: QuerySet de Contract
Returns:
dict avec {widget, order, template_name} ou None si non visible
"""
# Get all active condition groups, sorted by priority
condition_groups = self.condition_groups.filter(is_active=True).order_by('priority')
# Iterate through groups until finding the first one that matches
for group in condition_groups:
if group.evaluate(user_config, user_team, user_contracts):
template_name = group.template_override or self.get_default_template_name()
return {
'widget': self,
'order': group.order,
'template_name': template_name,
'col_width': group.col_width,
'col_order': group.col_order,
}
# If no group is defined, the widget is visible to all with order 9999
if not condition_groups.exists():
return {
'widget': self,
'order': 9999,
'template_name': self.get_default_template_name(),
'col_width': 12,
'col_order': 0,
}
# No group matches
return None
class HomeWidgetConditionGroup(models.Model):
"""Groupe de conditions pour un widget (conditions liées par ET)."""
widget = models.ForeignKey(
HomeWidget,
on_delete=models.CASCADE,
related_name='condition_groups',
verbose_name=_("Widget")
)
label = models.CharField(
max_length=255,
verbose_name=_("Libellé"),
help_text=_("Libellé informatif (ex: 'Stock manager - priorité haute')")
)
order = models.PositiveIntegerField(
verbose_name=_("Ordre de la ligne"),
help_text=_("Les widgets ayant le même ordre seront placés dans la même ligne Bootstrap.")
)
col_width = models.PositiveSmallIntegerField(
default=12,
verbose_name=_("Largeur (colonnes Bootstrap)"),
help_text=_("Nombre de colonnes Bootstrap (1-12). 12 = pleine largeur, 6 = demi-largeur, 4 = un tiers, etc.")
)
col_order = models.PositiveIntegerField(
default=0,
verbose_name=_("Ordre dans la ligne"),
help_text=_("Position du widget dans sa ligne (parmi les widgets du même ordre). Les valeurs les plus faibles apparaissent en premier.")
)
priority = models.PositiveIntegerField(
default=0,
verbose_name=_("Priorité"),
help_text=_("Ordre d'évaluation des groupes (le premier qui matche l'emporte)")
)
template_override = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_("Template (surcharge)"),
help_text=_("Surcharge du template pour ce groupe spécifiquement")
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Actif")
)
class Meta:
verbose_name = _("Groupe de conditions")
verbose_name_plural = _("Groupes de conditions")
ordering = ['widget', 'priority']
def __str__(self):
return f"{self.widget.widget_code} - {self.label}"
def evaluate(self, user_config, user_team, user_contracts):
"""
Évalue si ce groupe de conditions matche pour l'utilisateur.
Args:
user_config: Instance de UserConfig
user_team: Instance de CompanyTeam ou None
user_contracts: QuerySet de Contract
Returns:
True si TOUTES les conditions du groupe sont vraies (logique ET)
"""
conditions = self.conditions.filter(is_active=True)
# If no conditions, the group doesn't match
if not conditions.exists():
return False
# All conditions must be true
for condition in conditions:
if not condition.evaluate(user_config, user_team, user_contracts):
return False
return True
def get_summary(self):
"""Retourne une chaîne lisible des conditions du groupe."""
conditions = self.conditions.filter(is_active=True)
if not conditions.exists():
return "Aucune condition"
parts = []
for condition in conditions:
part = ""
if condition.condition_type == 'role' and condition.role:
part = f"role={condition.role.name}"
elif condition.condition_type == 'team' and condition.team:
part = f"team={condition.team.name}"
elif condition.condition_type == 'contract' and condition.contract:
part = f"contract={condition.contract.contract_number}"
elif condition.condition_type == 'thematic' and condition.thematic:
part = f"thematic={condition.thematic.code}"
elif condition.condition_type == 'is_intern':
part = f"is_intern={condition.is_intern}"
if part:
if condition.is_negated:
part = f"not({part})"
parts.append(part)
return " ET ".join(parts) if parts else "Aucune condition"
class HomeWidgetCondition(models.Model):
"""Condition individuelle dans un groupe."""
group = models.ForeignKey(
HomeWidgetConditionGroup,
on_delete=models.CASCADE,
related_name='conditions',
verbose_name=_("Groupe")
)
condition_type = models.CharField(
max_length=20,
choices=CONDITION_TYPE_CHOICES,
verbose_name=_("Type de condition")
)
# Foreign keys pour les différents types de conditions
role = models.ForeignKey(
'common.Role',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Rôle"),
help_text=_("Utilisé si type = 'role'")
)
team = models.ForeignKey(
'contracts.CompanyTeam',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Équipe"),
help_text=_("Utilisé si type = 'team'")
)
contract = models.ForeignKey(
'contracts.Contract',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Contrat"),
help_text=_("Utilisé si type = 'contract'")
)
thematic = models.ForeignKey(
'common.Thematic',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Thématique"),
help_text=_("Utilisé si type = 'thematic'")
)
is_intern = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Statut interne"),
help_text=_("Utilisé si type = 'is_intern'")
)
is_negated = models.BooleanField(
default=False,
verbose_name=_("NOT"),
help_text=_("Inverser le sens de la condition (ex: NOT role=Admin)")
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Actif")
)
class Meta:
verbose_name = _("Condition")
verbose_name_plural = _("Conditions")
def __str__(self):
prefix = "NOT " if self.is_negated else ""
if self.condition_type == 'role' and self.role:
return f"{prefix}Role: {self.role.name}"
elif self.condition_type == 'team' and self.team:
return f"{prefix}Team: {self.team.name}"
elif self.condition_type == 'contract' and self.contract:
return f"{prefix}Contract: {self.contract.contract_number}"
elif self.condition_type == 'thematic' and self.thematic:
return f"{prefix}Thematic: {self.thematic.code}"
elif self.condition_type == 'is_intern':
return f"{prefix}Is intern: {self.is_intern}"
return f"{prefix}{self.condition_type}"
def evaluate(self, user_config, user_team, user_contracts):
"""
Évalue si cette condition est vraie pour l'utilisateur.
Args:
user_config: Instance de UserConfig
user_team: Instance de CompanyTeam ou None
user_contracts: QuerySet de Contract
Returns:
True si la condition est remplie
"""
if self.condition_type == 'role' and self.role:
res = user_config.roles.filter(id=self.role.id).exists()
elif self.condition_type == 'team' and self.team:
if not user_team:
res = False
elif hasattr(user_team, 'filter'):
res = user_team.filter(id=self.team.id).exists()
elif hasattr(user_team, '__iter__'):
res = any(t.id == self.team.id for t in user_team)
else:
res = user_team.id == self.team.id
elif self.condition_type == 'contract' and self.contract:
res = user_contracts.filter(id=self.contract.id).exists()
elif self.condition_type == 'thematic' and self.thematic:
res = user_config.userthematics.filter(
thematic_id=self.thematic.id
).exists()
elif self.condition_type == 'is_intern':
res = user_config.is_intern == self.is_intern
else:
res = False
if self.is_negated:
return not res
return res