338 lines
12 KiB
Python
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
|