546 lignes
18 KiB
Python
546 lignes
18 KiB
Python
from django.conf import settings
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import models
|
||
from django.db.models import Q
|
||
|
||
import reversion
|
||
from dynamic_filenames import FilePattern
|
||
from stdimage.models import StdImageField
|
||
|
||
from benevalibre.accounts.models import User
|
||
from benevalibre.instance.models import (
|
||
AbstractCategory,
|
||
AbstractLevel,
|
||
DefaultCategory,
|
||
DefaultLevel,
|
||
DefaultRole,
|
||
)
|
||
from benevalibre.utils.mixins import HTMLDocString
|
||
from benevalibre.utils.models import AbstractRole, AbstractTaxonomy
|
||
|
||
|
||
@reversion.register()
|
||
class ActivityFieldGroup(HTMLDocString, models.Model):
|
||
"""
|
||
Les groupes de champs d'activité servent à regrouper les champs
|
||
d'activités associatives. Ce sont des informations utiles à fins de
|
||
statistiques inter-associatives.
|
||
|
||
Par conséquent il n'est pas recommandé de modifier ces groupes sans
|
||
concertation préalable avec les fédérations de mouvements associatifs.
|
||
"""
|
||
|
||
class Meta:
|
||
verbose_name = "groupe de champs d'activités associatives"
|
||
verbose_name_plural = "groupes de champs d'activités associatives"
|
||
|
||
name = models.CharField("nom", max_length=255)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
@reversion.register()
|
||
class ActivityField(HTMLDocString, models.Model):
|
||
"""
|
||
Les champs d'activité servent à catégorier les associations.
|
||
Ce sont des informations utiles à fins de statistiques
|
||
inter-associatives.
|
||
"""
|
||
|
||
class Meta:
|
||
verbose_name = "champ d'activité associative"
|
||
verbose_name_plural = "champs d'activités associatives"
|
||
|
||
activity_field_group = models.ForeignKey(
|
||
ActivityFieldGroup,
|
||
verbose_name="groupe de champs d'activités",
|
||
on_delete=models.SET_NULL,
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
name = models.CharField("nom", max_length=255)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class AssociationQuerySet(models.QuerySet):
|
||
def inactive(self):
|
||
return self.filter(is_active=False)
|
||
|
||
def active(self):
|
||
return self.filter(is_active=True)
|
||
|
||
def public(self):
|
||
return self.active().filter(conceal=False)
|
||
|
||
def engaged_by(self, user):
|
||
if user.is_anonymous:
|
||
return self.none()
|
||
return self.filter(engagement__user=user)
|
||
|
||
def viewable_by(self, user):
|
||
if user.is_anonymous:
|
||
return self.public()
|
||
elif user.is_superuser:
|
||
return self.all()
|
||
else:
|
||
return self.public() | self.engaged_by(user)
|
||
|
||
|
||
AssociationManager = models.Manager.from_queryset(AssociationQuerySet)
|
||
|
||
|
||
@reversion.register()
|
||
class Association(models.Model):
|
||
"""
|
||
Les associations sont au cœur du logiciel **Bénévalibre**. Elles sont
|
||
définies par :
|
||
|
||
* un nom ;
|
||
* une description ;
|
||
* un logo ;
|
||
* un champ d'activité.
|
||
|
||
Elles peuvent être « cachée », dans ce cas elles n'apparaissent pas dans la
|
||
liste publique.
|
||
|
||
Elles peuvent modérer l'engagement bénévole, dans ce cas les bénévoles
|
||
souhaitant s'engager pour l'association doivent chacun⋅e⋅s être
|
||
approuvé⋅e⋅s.
|
||
|
||
Elles peuvent modérer la saisie des actions de bénévolat, dans ce cas
|
||
chaque action de bénévolat déclarée doit être approuvée, sauf si celui-ci
|
||
a été saisi par un⋅e bénévole privilégié⋅e.
|
||
"""
|
||
|
||
# Identity
|
||
activity_field = models.ForeignKey(
|
||
ActivityField,
|
||
verbose_name="champ d'activité",
|
||
on_delete=models.SET_NULL,
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
name = models.CharField("nom", max_length=127)
|
||
description = models.TextField("description", blank=True, null=True)
|
||
website_url = models.URLField("adresse du site web", blank=True, null=True)
|
||
|
||
# Logo
|
||
logo_upload_to = FilePattern(filename_pattern='logos/{uuid:x}{ext}')
|
||
logo = StdImageField(
|
||
"logo",
|
||
upload_to=logo_upload_to,
|
||
blank=True,
|
||
null=True,
|
||
variations={'thumbnail': (100, 48), 'medium': (250, 250)},
|
||
)
|
||
|
||
# Parameters
|
||
conceal = models.BooleanField(
|
||
"cachée",
|
||
default=False,
|
||
help_text="Une association cachée n'apparait qu'à ses bénévoles.",
|
||
)
|
||
|
||
moderate_engagement = models.BooleanField(
|
||
"modérer l'inscription des bénévoles",
|
||
default=False,
|
||
help_text=(
|
||
"Une modération est nécessaire avant qu'un⋅e utilisat⋅eur⋅rice "
|
||
"ne devienne bénévole pour l'association."
|
||
),
|
||
)
|
||
|
||
moderate_benevalo = models.BooleanField(
|
||
"modérer la saisie des actions de bénévolats",
|
||
default=False,
|
||
help_text=(
|
||
"Une modération est nécessaire avant qu'un⋅e bénévole "
|
||
"n'enregistre du bénévolat pour l'association."
|
||
),
|
||
)
|
||
|
||
def skip_registration_moderation():
|
||
return not settings.MODERATED_REGISTRATION
|
||
|
||
is_active = models.BooleanField(
|
||
default=skip_registration_moderation, editable=False
|
||
)
|
||
|
||
# Metadata
|
||
objects = AssociationManager()
|
||
|
||
def after_creation(self):
|
||
def id_excluded(dic):
|
||
return {key: val for key, val in dic.items() if key != "id"}
|
||
|
||
# Create default association's categories
|
||
[
|
||
Category.objects.create(association=self, **id_excluded(values))
|
||
for values in DefaultCategory.objects.all().values()
|
||
]
|
||
# Create default association's levels
|
||
[
|
||
Level.objects.create(association=self, **id_excluded(values))
|
||
for values in DefaultLevel.objects.all().values()
|
||
]
|
||
# Create default association's roles
|
||
[
|
||
Role.objects.create(association=self, **id_excluded(values))
|
||
for values in DefaultRole.objects.all().values()
|
||
]
|
||
|
||
def delete(self, *args, **kwargs):
|
||
self.engagement_set.all().delete()
|
||
super().delete(*args, **kwargs)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
def get_benevoles(self):
|
||
return User.objects.filter(engagement__association=self).distinct()
|
||
|
||
# tmp perms
|
||
def is_engaged(self, user):
|
||
return not user.is_anonymous and self.get_engagement(user)
|
||
|
||
def is_waiting_for_moderation(self, user):
|
||
return self.engagement_set.filter(user=user, is_active=False).exists()
|
||
|
||
def need_moderation(self, user):
|
||
return self.moderate_benevalo and not self.can_manage_benevalos(user)
|
||
|
||
def can_self_engage(self, user):
|
||
return (
|
||
not user.is_anonymous
|
||
and self.is_active
|
||
and not self.is_engaged(user)
|
||
and not self.is_waiting_for_moderation(user)
|
||
and not self.conceal
|
||
)
|
||
|
||
def can_self_create_benevalo(self, user):
|
||
return self.is_engaged(user)
|
||
|
||
def can_self_list_benevalo(self, user):
|
||
return not user.is_anonymous and (
|
||
self.can_self_create_benevalo(user)
|
||
or self.benevalo_set.by_user(user).exists()
|
||
)
|
||
|
||
def get_role(self, user):
|
||
if not user.is_anonymous:
|
||
engagement = self.get_engagement(user)
|
||
return engagement.role if engagement else None
|
||
|
||
def get_engagement(self, user):
|
||
if not user.is_anonymous:
|
||
return self.engagement_set.active().for_user(user).first()
|
||
|
||
def can_list_users(self, user):
|
||
role = self.get_role(user)
|
||
return role and (
|
||
role.list_users or role.manage_benevalos or role.manage_association
|
||
)
|
||
|
||
def can_manage_engagements(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.delegates or role.manage_association)
|
||
|
||
def can_manage_benevalos(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.manage_benevalos or role.manage_association)
|
||
|
||
def can_manage_levels(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.manage_levels or role.manage_association)
|
||
|
||
def can_manage_projects(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.manage_projects or role.manage_association)
|
||
|
||
def can_manage_categories(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.manage_categories or role.manage_association)
|
||
|
||
def can_manage_roles(self, user):
|
||
role = self.get_role(user)
|
||
return role and (role.manage_roles and role.manage_association)
|
||
|
||
def can_manage_association(self, user):
|
||
role = self.get_role(user)
|
||
return (role and role.manage_association) or user.is_superuser
|
||
|
||
|
||
class AbstractAssociated(models.Model):
|
||
class Meta:
|
||
abstract = True
|
||
|
||
association = models.ForeignKey(
|
||
Association, verbose_name="association", on_delete=models.CASCADE
|
||
)
|
||
|
||
|
||
@reversion.register()
|
||
class Project(HTMLDocString, AbstractAssociated, AbstractTaxonomy):
|
||
"""
|
||
Les projets permettent pour ceux qui souhaitent de ventiler l'activité
|
||
de l'association sur différents projets, qui vont typiquement durer
|
||
longtemps et mobiliser des actions dans différentes catégories.
|
||
|
||
* Par défaut, si aucun projet n'existe, ce champ est masqué aux bénévoles.
|
||
* La saisie de ce champ par les bénévoles est optionnelle.
|
||
"""
|
||
|
||
class Meta:
|
||
verbose_name = "projet"
|
||
verbose_name_plural = "projets"
|
||
|
||
|
||
@reversion.register()
|
||
class Level(HTMLDocString, AbstractAssociated, AbstractLevel):
|
||
"""
|
||
Les niveaux de bénévolat permettent pour ceux qui souhaitent de
|
||
caractériser le niveau d'expertise que l'action mobilise. C'est utile
|
||
par exemple pour les associations qui souhaitent moduler la
|
||
valorisation en fonction de niveaux d'expertise différents.
|
||
|
||
* Par défaut, si aucun niveau n'existe, ce champ est masqué aux bénévoles.
|
||
* La saisie de ce champ par les bénévoles est optionnelle.
|
||
"""
|
||
|
||
class Meta:
|
||
verbose_name = "niveau"
|
||
verbose_name_plural = "niveaux"
|
||
|
||
|
||
@reversion.register()
|
||
class Category(HTMLDocString, AbstractAssociated, AbstractCategory):
|
||
"""
|
||
Les catégories servent à qualifier les actions, par nature.
|
||
Afin de rendre possible des statistiques inter-associatives, elles
|
||
sont chacune attachées à une « catégorie d'instance ».
|
||
Ainsi, chaque association peut personnaliser sa liste de catégories.
|
||
|
||
* La saisie de ce champ par les bénévoles est obligatoire.
|
||
"""
|
||
|
||
# FIXME: removing the last category should be prohibited
|
||
class Meta:
|
||
verbose_name = "catégorie"
|
||
|
||
verbose_name_plural = "catégories"
|
||
|
||
|
||
class Role(HTMLDocString, AbstractAssociated, AbstractRole):
|
||
"""
|
||
Les rôles définissent les permissions dont jouit chaque bénévole.
|
||
Ces permissions sont, par ordre d'importance croissante :
|
||
|
||
* Liste les utilisat⋅eurs⋅rices : donne au bénévole la possibilité de voir
|
||
qui sont les autres bénévoles engagés dans l'association.
|
||
|
||
* Délègue ses permissions : donne au bénévole la possibilité de gérer les
|
||
rôles des bénévoles et de transférer son rôle aux autres bénévoles. Ce
|
||
rôle implique de lister les autres bénévoles engagés dans l'association.
|
||
|
||
* Gère le bénévolat : donne au bénévole la possibilité de modérer, saisir,
|
||
corriger ou effacer le bénévolat des autres bénévoles. Ce rôle implique
|
||
de lister les autres bénévoles engagés dans l'association.
|
||
|
||
* Gère les niveaux, projets, catégories : donne respectivement au bénévole
|
||
la capacité de gérer respectivement les niveaux, les projets et les
|
||
catégories.
|
||
|
||
* Gère les rôles : donne au bénévole la capacité de gérer les rôles. Ce
|
||
rôle permet un accès total à tous les rôles de l'association.
|
||
|
||
* Gère l'association : donne au bénévole la capacité de gérer la totalité
|
||
des attributs de l'association.
|
||
|
||
Le « rôle des nouveaux arrivants » est le rôle associé aux
|
||
bénévole lorsque ceux-ci rejoignent l'association.
|
||
"""
|
||
|
||
class Meta:
|
||
verbose_name = "rôle"
|
||
verbose_name_plural = "rôles"
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
def clean(self):
|
||
if self.association_id:
|
||
if (
|
||
self.new_comers
|
||
and Role.objects.filter(
|
||
~models.Q(id=self.id),
|
||
new_comers=True,
|
||
association_id=self.association_id,
|
||
).exists()
|
||
):
|
||
raise ValidationError(
|
||
"Un autre rôle possède déjà l'attribut « nouvel arrivant "
|
||
"». Il ne peut n'y en avoir qu'un."
|
||
)
|
||
elif (
|
||
not self.new_comers
|
||
and not Role.objects.filter(
|
||
~models.Q(id=self.id),
|
||
new_comers=True,
|
||
association_id=self.association_id,
|
||
).exists()
|
||
):
|
||
raise ValidationError(
|
||
"Aucun autre rôle ne possède l'attribut « nouvel arrivant "
|
||
"». Vous devez en disposer d'un pour permettre "
|
||
"l'inscription d'un⋅e bénévole."
|
||
)
|
||
|
||
|
||
class EngagementQuerySet(models.QuerySet):
|
||
def active(self):
|
||
return self.filter(is_active=True)
|
||
|
||
def inactive(self):
|
||
return self.filter(is_active=False)
|
||
|
||
def for_user(self, user):
|
||
if user.is_anonymous:
|
||
return self.none()
|
||
return self.filter(user=user)
|
||
|
||
def viewable_by(self, user):
|
||
if user.is_anonymous:
|
||
return self.none()
|
||
elif user.is_superuser:
|
||
return self.all()
|
||
else:
|
||
own = self.for_user(user)
|
||
# Tous les engagements visibles d'un⋅e utilisat⋅eur⋅rice, c'est
|
||
# tous les engagements actifs liés à une asso dont il⋅elle a la
|
||
# capacité can_list_users, ou ceux dont il⋅elle a la capacité
|
||
# can_manage_engagements
|
||
permitted_associations = Role.objects.filter(
|
||
Q(engagement__user=user)
|
||
& Q(engagement__is_active=True)
|
||
& (
|
||
Q(list_users=True)
|
||
| Q(manage_benevalos=True)
|
||
| Q(manage_association=True)
|
||
)
|
||
).values('association')
|
||
permitted = self.filter(association__in=permitted_associations)
|
||
return own | permitted | self.manageable_by(user)
|
||
|
||
def manageable_by(self, user):
|
||
if user.is_anonymous:
|
||
return self.none()
|
||
elif user.is_superuser:
|
||
return self.all()
|
||
else:
|
||
# Tous les engagements gérables d'un⋅e utilisat⋅eur⋅rice, c'est
|
||
# tous les engagements liés à une asso pour lesquelles il.elle a un
|
||
# rôle avec la capacité can_manage_engagements
|
||
permitted_associations = Role.objects.filter(
|
||
Q(engagement__user=user)
|
||
& (Q(delegates=True) | Q(manage_association=True))
|
||
).values('association')
|
||
permitted = self.filter(association__in=permitted_associations)
|
||
return permitted
|
||
|
||
|
||
EngagementManager = models.Manager.from_queryset(EngagementQuerySet)
|
||
|
||
|
||
class Engagement(HTMLDocString, AbstractAssociated):
|
||
"""
|
||
Les bénévoles sont « engagé⋅e⋅s » dans l'association par le
|
||
biais d'un rôle. Par conséquent l'ajout ou le retrait d'un⋅e bénévole
|
||
à une association n'affecte pas directement le compte de la personne ;
|
||
cela n'affecte que l'engagement qui lie l'association au compte.
|
||
"""
|
||
|
||
user = models.ForeignKey(
|
||
User, verbose_name="bénévole", on_delete=models.CASCADE
|
||
)
|
||
role = models.ForeignKey(
|
||
Role, verbose_name="rôle", on_delete=models.PROTECT
|
||
)
|
||
|
||
is_active = models.BooleanField(default=True, editable=False)
|
||
|
||
objects = EngagementManager()
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
def clean(self):
|
||
errors = []
|
||
|
||
if self.association_id:
|
||
if (
|
||
self.role_id
|
||
and self.association_id != self.role.association_id
|
||
):
|
||
errors.append(
|
||
ValidationError(
|
||
"Le rôle sélectionné n'est pas un rôle de "
|
||
"l'association. Veuillez choisir un rôle valide."
|
||
)
|
||
)
|
||
if (
|
||
self.user_id
|
||
and Engagement.objects.filter(
|
||
~models.Q(id=self.id),
|
||
association_id=self.association_id,
|
||
user_id=self.user_id,
|
||
).exists()
|
||
):
|
||
errors.append(
|
||
ValidationError(
|
||
"Ce bénévole est déjà engagé dans cette association. "
|
||
"Modifiez le rôle existant plutôt que d'en ajouter un."
|
||
)
|
||
)
|
||
if (
|
||
self.id
|
||
and self.role
|
||
and not self.role.manage_association
|
||
and not Engagement.objects.filter(
|
||
~models.Q(id=self.id),
|
||
association_id=self.association_id,
|
||
role__manage_association=True,
|
||
).exists()
|
||
):
|
||
errors.append(
|
||
ValidationError(
|
||
"Modifier cet engagement rendrait impossible la "
|
||
"gestion de l'association. Avant ça, vous devez "
|
||
"donner le rôle de dirigeant à une autre personne.",
|
||
)
|
||
)
|
||
if errors:
|
||
raise ValidationError(errors)
|
||
|
||
def delete(self):
|
||
if not Engagement.objects.filter(
|
||
~models.Q(id=self.id),
|
||
association_id=self.association_id,
|
||
role__manage_association=True,
|
||
).exists():
|
||
raise ValidationError(
|
||
"Supprimer cet engagement rendrait impossible la "
|
||
"gestion de l'association. Avant ça, vous devez "
|
||
"donner le rôle de dirigeant à une autre personne.",
|
||
)
|
||
|
||
return super().delete()
|
||
|
||
def __str__(self):
|
||
return "{} est {}".format(self.user, self.role)
|