970 lines
36 KiB
Python
970 lines
36 KiB
Python
from datetime import date, timedelta
|
||
|
||
import nh3
|
||
from dal import autocomplete
|
||
from dal.widgets import WidgetMixin
|
||
from tinymce.widgets import TinyMCE
|
||
|
||
from django import forms
|
||
from django.contrib import messages
|
||
from django.contrib.postgres.search import SearchQuery, SearchVector
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.db.models import Count, Q
|
||
from django.utils.dates import MONTHS
|
||
|
||
from city_ch_autocomplete.forms import CityChField, CityChMixin
|
||
|
||
from common.choices import PROVENANCE_DESTINATION_CHOICES
|
||
from .models import (
|
||
Bilan, Contact, Document, EquipeChoices, Famille, Formation, Intervenant,
|
||
Niveau, Personne, Prestation, Region, Rapport, Role, Service, Suivi, Utilisateur,
|
||
)
|
||
from .utils import format_nom_prenom, ANTICIPATION_POUR_DEBUT_SUIVI
|
||
|
||
|
||
class BootstrapMixin:
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
for ffield in self.fields.values():
|
||
if (isinstance(ffield.widget, (PickSplitDateTimeWidget, PickDateWidget, WidgetMixin)) or
|
||
getattr(ffield.widget, '_bs_enabled', False)):
|
||
continue
|
||
elif isinstance(ffield.widget, (forms.Select, forms.NullBooleanSelect)):
|
||
self.add_attr(ffield.widget, 'form-select')
|
||
elif isinstance(ffield.widget, (forms.CheckboxInput, forms.RadioSelect)):
|
||
self.add_attr(ffield.widget, 'form-check-input')
|
||
else:
|
||
self.add_attr(ffield.widget, 'form-control')
|
||
|
||
@staticmethod
|
||
def add_attr(widget, class_name):
|
||
if 'class' in widget.attrs:
|
||
widget.attrs['class'] += f' {class_name}'
|
||
else:
|
||
widget.attrs.update({'class': class_name})
|
||
|
||
|
||
class BootstrapChoiceMixin:
|
||
"""
|
||
Mixin to customize choice widgets to set 'form-check' on container and
|
||
'form-check-input' on sub-options.
|
||
"""
|
||
_bs_enabled = True
|
||
|
||
def get_context(self, *args, **kwargs):
|
||
context = super().get_context(*args, **kwargs)
|
||
if 'class' in context['widget']['attrs']:
|
||
context['widget']['attrs']['class'] += ' form-check'
|
||
else:
|
||
context['widget']['attrs']['class'] = 'form-check'
|
||
return context
|
||
|
||
def create_option(self, *args, attrs=None, **kwargs):
|
||
attrs = attrs or {}
|
||
if 'class' in attrs:
|
||
attrs['class'] += ' form-check-input'
|
||
else:
|
||
attrs.update({'class': 'form-check-input'})
|
||
return super().create_option(*args, attrs=attrs, **kwargs)
|
||
|
||
|
||
class BSRadioSelect(BootstrapChoiceMixin, forms.RadioSelect):
|
||
pass
|
||
|
||
|
||
class ReadOnlyableMixin:
|
||
def __init__(self, *args, readonly=False, **kwargs):
|
||
self.readonly = readonly
|
||
super().__init__(*args, **kwargs)
|
||
if self.readonly:
|
||
for field in self.fields.values():
|
||
field.disabled = True
|
||
|
||
|
||
class BSCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||
"""
|
||
Custom widget to set 'form-check' on container and 'form-check-input' on sub-options.
|
||
"""
|
||
_bs_enabled = True
|
||
|
||
def get_context(self, *args, **kwargs):
|
||
context = super().get_context(*args, **kwargs)
|
||
context['widget']['attrs']['class'] = 'form-check'
|
||
return context
|
||
|
||
def create_option(self, *args, attrs=None, **kwargs):
|
||
attrs = attrs.copy() if attrs else {}
|
||
if 'class' in attrs:
|
||
attrs['class'] += ' form-check-input'
|
||
else:
|
||
attrs.update({'class': 'form-check-input'})
|
||
return super().create_option(*args, attrs=attrs, **kwargs)
|
||
|
||
|
||
class RichTextField(forms.CharField):
|
||
widget = TinyMCE
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
kwargs['widget'] = self.widget
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def clean(self, value):
|
||
value = super().clean(value)
|
||
return nh3.clean(
|
||
value, tags={'p', 'br', 'b', 'strong', 'u', 'i', 'em', 'ul', 'li'}
|
||
)
|
||
|
||
|
||
class HMDurationField(forms.DurationField):
|
||
"""A duration field taking HH:MM as input."""
|
||
widget = forms.TextInput(attrs={'placeholder': 'hh:mm'})
|
||
|
||
def to_python(self, value):
|
||
if value in self.empty_values or isinstance(value, timedelta):
|
||
return super().to_python(value)
|
||
value += ':00' # Simulate seconds
|
||
return super().to_python(value)
|
||
|
||
def prepare_value(self, value):
|
||
if isinstance(value, timedelta):
|
||
seconds = value.days * 24 * 3600 + value.seconds
|
||
hours = seconds // 3600
|
||
minutes = seconds % 3600 // 60
|
||
value = '{:02d}:{:02d}'.format(hours, minutes)
|
||
return value
|
||
|
||
|
||
class PickDateWidget(forms.DateInput):
|
||
class Media:
|
||
js = [
|
||
'admin/js/core.js',
|
||
'admin/js/calendar.js',
|
||
# Include the Django 3.2 version without today link.
|
||
'js/DateTimeShortcuts.js',
|
||
]
|
||
|
||
def __init__(self, attrs=None, **kwargs):
|
||
attrs = {'class': 'vDateField vDateField-rounded', 'size': '10', **(attrs or {})}
|
||
super().__init__(attrs=attrs, **kwargs)
|
||
|
||
|
||
class PickSplitDateTimeWidget(forms.SplitDateTimeWidget):
|
||
def __init__(self, attrs=None):
|
||
widgets = [PickDateWidget, forms.TimeInput(attrs={'class': 'TimeField'}, format='%H:%M')]
|
||
forms.MultiWidget.__init__(self, widgets, attrs)
|
||
|
||
|
||
class ContactForm(CityChMixin, BootstrapMixin, forms.ModelForm):
|
||
service = forms.ModelChoiceField(queryset=Service.objects.exclude(sigle='CRNE'), required=False)
|
||
roles = forms.ModelMultipleChoiceField(
|
||
label='Rôles',
|
||
queryset=Role.objects.exclude(est_famille=True).order_by('nom'),
|
||
widget=BSCheckboxSelectMultiple,
|
||
required=False
|
||
)
|
||
city_auto = CityChField(required=False)
|
||
postal_code_model_field = 'npa'
|
||
city_model_field = 'localite'
|
||
|
||
class Meta:
|
||
model = Contact
|
||
fields = [
|
||
'nom', 'prenom', 'profession', 'service', 'roles', 'rue', 'city_auto', 'npa', 'localite',
|
||
'tel_prof', 'tel_prive', 'email', 'remarque'
|
||
]
|
||
|
||
|
||
class RoleForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Role
|
||
fields = '__all__'
|
||
|
||
|
||
class ServiceForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Service
|
||
fields = ['sigle', 'nom_complet']
|
||
|
||
def clean_sigle(self):
|
||
return self.cleaned_data['sigle'].upper()
|
||
|
||
|
||
class FormationForm(BootstrapMixin, ReadOnlyableMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Formation
|
||
exclude = ('personne',)
|
||
|
||
|
||
class GroupSelectMultiple(BSCheckboxSelectMultiple):
|
||
option_template_name = "widgets/group_checkbox_option.html"
|
||
|
||
def create_option(self, name, value, *args, **kwargs):
|
||
try:
|
||
help_ = value.instance.groupinfo.description
|
||
except ObjectDoesNotExist:
|
||
help_= ''
|
||
return {
|
||
**super().create_option(name, value, *args, **kwargs),
|
||
'help': help_,
|
||
}
|
||
|
||
|
||
class UtilisateurForm(BootstrapMixin, forms.ModelForm):
|
||
roles = forms.ModelMultipleChoiceField(
|
||
label="Rôles",
|
||
queryset=Role.objects.exclude(
|
||
Q(est_famille=True) | Q(nom__in=['Personne significative', 'Référent'])
|
||
).order_by('nom'),
|
||
widget=BSCheckboxSelectMultiple,
|
||
required=False
|
||
)
|
||
|
||
class Meta:
|
||
model = Utilisateur
|
||
fields = [
|
||
'nom', 'prenom', 'sigle', 'profession', 'tel_prof', 'tel_prive', 'username',
|
||
'taux_activite', 'decharge', 'email', 'equipe', 'roles', 'groups'
|
||
]
|
||
widgets = {'groups': GroupSelectMultiple}
|
||
labels = {'profession': 'Titre'}
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.fields['groups'].queryset = self.fields['groups'].queryset.order_by('name')
|
||
|
||
|
||
class PersonneForm(CityChMixin, BootstrapMixin, ReadOnlyableMixin, forms.ModelForm):
|
||
role = forms.ModelChoiceField(label="Rôle", queryset=Role.objects.filter(est_famille=True), required=True)
|
||
|
||
city_auto = CityChField(required=False)
|
||
postal_code_model_field = 'npa'
|
||
city_model_field = 'localite'
|
||
|
||
class Meta:
|
||
model = Personne
|
||
fields = (
|
||
'role', 'nom', 'prenom', 'date_naissance', 'genre', 'filiation',
|
||
'rue', 'npa', 'localite', 'telephone',
|
||
'email', 'profession', 'pays_origine', 'decedee',
|
||
'allergies', 'remarque', 'remarque_privee',
|
||
)
|
||
widgets = {
|
||
'date_naissance': PickDateWidget,
|
||
}
|
||
labels = {
|
||
'remarque_privee': 'Remarque privée (pas imprimée)',
|
||
}
|
||
|
||
def __init__(self, **kwargs):
|
||
self.famille = kwargs.pop('famille', None)
|
||
role_id = kwargs.pop('role', None)
|
||
|
||
super().__init__(**kwargs)
|
||
if self.famille:
|
||
self.initial['nom'] = self.famille.nom
|
||
self.initial['rue'] = self.famille.rue
|
||
self.initial['npa'] = self.famille.npa
|
||
self.initial['localite'] = self.famille.localite
|
||
self.initial['telephone'] = self.famille.telephone
|
||
|
||
if role_id:
|
||
if role_id == 'ps': # personne significative
|
||
excl = Role.ROLES_PARENTS + ['Enfant suivi', 'Enfant non-suivi']
|
||
self.fields['role'].queryset = Role.objects.filter(
|
||
famille=True
|
||
).exclude(nom__in=excl)
|
||
elif role_id == 'parent':
|
||
self.fields['role'].queryset = Role.objects.filter(nom__in=Role.ROLES_PARENTS)
|
||
else:
|
||
self.role = Role.objects.get(pk=role_id)
|
||
|
||
if self.role.nom in Role.ROLES_PARENTS:
|
||
self.fields['role'].queryset = Role.objects.filter(nom=self.role.nom)
|
||
self.initial['genre'] = 'M' if self.role.nom == 'Père' else 'F'
|
||
elif self.role.nom in ['Enfant suivi', 'Enfant non-suivi']:
|
||
self.fields['role'].queryset = Role.objects.filter(nom__in=['Enfant suivi', 'Enfant non-suivi'])
|
||
self.fields['profession'].label = 'Profession/École'
|
||
self.initial['role'] = self.role.pk
|
||
else:
|
||
if self.instance.pk:
|
||
famille = self.instance.famille
|
||
# Cloisonnement des choix pour les rôles en fonction du rôle existant
|
||
if self.instance.role.nom in Role.ROLES_PARENTS:
|
||
self.fields['role'].queryset = Role.objects.filter(nom__in=Role.ROLES_PARENTS)
|
||
elif self.instance.role.nom in ['Enfant suivi', 'Enfant non-suivi']:
|
||
self.fields['role'].queryset = Role.objects.filter(nom__in=['Enfant suivi', 'Enfant non-suivi'])
|
||
else:
|
||
excl = ['Enfant suivi', 'Enfant non-suivi']
|
||
if len(famille.parents()) == 2:
|
||
excl.extend(Role.ROLES_PARENTS)
|
||
self.fields['role'].queryset = Role.objects.filter(
|
||
famille=True
|
||
).exclude(nom__in=excl)
|
||
|
||
def clean_nom(self):
|
||
return format_nom_prenom(self.cleaned_data['nom'])
|
||
|
||
def clean_prenom(self):
|
||
return format_nom_prenom(self.cleaned_data['prenom'])
|
||
|
||
def clean_localite(self):
|
||
localite = self.cleaned_data.get('localite')
|
||
if localite and localite[0].islower():
|
||
localite = localite[0].upper() + localite[1:]
|
||
return localite
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
if cleaned_data['decedee'] and cleaned_data['role'] == 'Enfant suivi':
|
||
raise forms.ValidationError('Un enfant décédé ne peut pas être «Enfant suivi»')
|
||
return cleaned_data
|
||
|
||
def save(self, **kwargs):
|
||
if self.instance.pk is None:
|
||
self.instance.famille = self.famille
|
||
pers = Personne.objects.create_personne(**self.instance.__dict__)
|
||
else:
|
||
pers = super().save(**kwargs)
|
||
pers = Personne.objects.add_formation(pers)
|
||
return pers
|
||
|
||
|
||
class AgendaFormBase(BootstrapMixin, forms.ModelForm):
|
||
destination = forms.ChoiceField(choices=PROVENANCE_DESTINATION_CHOICES, required=False)
|
||
|
||
class Meta:
|
||
fields = (
|
||
'date_demande', 'date_debut_evaluation', 'date_fin_evaluation',
|
||
'date_debut_suivi', 'date_fin_suivi', 'motif_fin_suivi', 'destination'
|
||
)
|
||
widgets = {field: PickDateWidget for field in fields[:-2]}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
destination = kwargs.pop('destination', '')
|
||
self.request = kwargs.pop('request', None)
|
||
super().__init__(*args, **kwargs)
|
||
if self.instance.date_debut_suivi is not None or self.instance.motif_fin_suivi:
|
||
for fname in ('date_demande', 'date_debut_evaluation', 'date_fin_evaluation'):
|
||
self.fields[fname].disabled = True
|
||
if self.instance.motif_fin_suivi:
|
||
self.fields['date_debut_suivi'].disabled = True
|
||
else:
|
||
# Choix 'Autres' obsolète (#435), pourrait être supprimé quand plus référencé
|
||
self.fields['motif_fin_suivi'].choices = [
|
||
ch for ch in self.fields['motif_fin_suivi'].choices if ch[0] != 'autres'
|
||
]
|
||
|
||
self.fields['destination'].choices = [('', '------')] + [
|
||
ch for ch in PROVENANCE_DESTINATION_CHOICES if (ch[0] != 'autre' or destination == 'autre')
|
||
]
|
||
self.initial['destination'] = destination
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
|
||
# Check date chronology
|
||
date_preced = None
|
||
for field_name in self._meta.fields[:-2]:
|
||
dt = cleaned_data.get(field_name)
|
||
if not dt:
|
||
continue
|
||
if date_preced and dt < date_preced:
|
||
raise forms.ValidationError(
|
||
"La date «{}» ne respecte pas l’ordre chronologique!".format(self.fields[field_name].label)
|
||
)
|
||
date_preced = dt
|
||
|
||
# Check mandatory dates
|
||
workflow = self._meta.model.WORKFLOW
|
||
for field, etape in reversed((workflow.items())):
|
||
if field == 'archivage':
|
||
continue
|
||
date_etape = cleaned_data.get(etape.date_nom())
|
||
etape_courante = etape
|
||
etape_preced_oblig = workflow[etape.preced_oblig]
|
||
date_preced_oblig = cleaned_data.get(etape_preced_oblig.date_nom())
|
||
while True:
|
||
etape_preced = workflow[etape_courante.precedente]
|
||
date_preced = cleaned_data.get(etape_preced.date_nom())
|
||
if date_preced is None and etape_courante.num > 1:
|
||
etape_courante = etape_preced
|
||
else:
|
||
break
|
||
if date_etape and date_preced_oblig is None:
|
||
raise forms.ValidationError("La date «{}» est obligatoire".format(etape_preced_oblig.nom))
|
||
|
||
# Check dates out of range
|
||
for field_name in self.changed_data:
|
||
value = self.cleaned_data.get(field_name)
|
||
if isinstance(value, date):
|
||
if value > date.today():
|
||
if field_name in ['date_debut_suivi', 'date_debut_evaluation', 'date_fin_evaluation']:
|
||
if value > date.today() + timedelta(days=ANTICIPATION_POUR_DEBUT_SUIVI):
|
||
self.add_error(
|
||
field_name,
|
||
forms.ValidationError(
|
||
"La saisie de cette date ne peut être "
|
||
f"anticipée de plus de {ANTICIPATION_POUR_DEBUT_SUIVI} jours !")
|
||
)
|
||
else:
|
||
self.add_error(field_name, forms.ValidationError("La saisie anticipée est impossible !"))
|
||
elif not Prestation.check_date_allowed(self.request.user, value):
|
||
if self.request and self.request.user.has_perm('aemo.change_famille'):
|
||
messages.warning(
|
||
self.request,
|
||
'Les dates saisies peuvent affecter les statistiques déjà communiquées !'
|
||
)
|
||
else:
|
||
self.add_error(
|
||
field_name,
|
||
forms.ValidationError("La saisie de dates pour le mois précédent n’est pas permise !")
|
||
)
|
||
|
||
ddebut = cleaned_data.get('date_debut_suivi')
|
||
dfin = cleaned_data.get('date_fin_suivi')
|
||
motif = cleaned_data.get('motif_fin_suivi')
|
||
dest = cleaned_data.get('destination')
|
||
|
||
if ddebut and dfin and motif and dest: # dossier terminé
|
||
return cleaned_data
|
||
elif ddebut and (dfin is None or motif == '' or dest == ''): # suivi en cours
|
||
if any([dfin, motif, dest]):
|
||
raise forms.ValidationError(
|
||
"Les champs «Fin de l'accompagnement», «Motif de fin» et «Destination» "
|
||
"sont obligatoires pour fermer le dossier."
|
||
)
|
||
elif ddebut is None and dfin is None: # evaluation
|
||
if motif != '': # abandon
|
||
cleaned_data['date_fin_suivi'] = date.today()
|
||
return cleaned_data
|
||
|
||
def save(self):
|
||
instance = super().save()
|
||
if instance.date_fin_suivi:
|
||
instance.famille.destination = self.cleaned_data['destination']
|
||
instance.famille.save()
|
||
return instance
|
||
|
||
|
||
class PrestationRadioSelect(BSRadioSelect):
|
||
option_template_name = 'widgets/prestation_radio.html'
|
||
|
||
|
||
class PrestationForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Prestation
|
||
fields = ['date_prestation', 'duree', 'texte', 'manque', 'fichier', 'intervenants']
|
||
widgets = {
|
||
'date_prestation': PickDateWidget,
|
||
'intervenants': BSCheckboxSelectMultiple,
|
||
'lib_prestation': PrestationRadioSelect,
|
||
}
|
||
labels = {
|
||
'lib_prestation': 'Prestation',
|
||
}
|
||
field_classes = {
|
||
'duree': HMDurationField,
|
||
'texte': RichTextField,
|
||
}
|
||
|
||
def __init__(self, *args, famille=None, user, **kwargs):
|
||
self.user = user
|
||
self.famille = famille
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['intervenants'].queryset = self.defaults_intervenants()
|
||
if famille:
|
||
intervenants = list(famille.suivi.intervenants.all())
|
||
if len(intervenants):
|
||
if self.user not in intervenants:
|
||
intervenants.insert(0, self.user)
|
||
self.fields['intervenants'].choices = [(i.pk, i.nom_prenom) for i in intervenants]
|
||
|
||
def defaults_intervenants(self):
|
||
return Utilisateur.intervenants()
|
||
|
||
def clean_date_prestation(self):
|
||
date_prestation = self.cleaned_data['date_prestation']
|
||
today = date.today()
|
||
if date_prestation > today:
|
||
raise forms.ValidationError("La saisie anticipée est impossible !")
|
||
|
||
if not Prestation.check_date_allowed(self.user, date_prestation):
|
||
raise forms.ValidationError(
|
||
"La saisie des prestations des mois précédents est close !"
|
||
)
|
||
return date_prestation
|
||
|
||
def clean_texte(self):
|
||
texte = self.cleaned_data['texte']
|
||
for snip in ('<p></p>', '<p><br></p>', '<p> </p>'):
|
||
while texte.startswith(snip):
|
||
texte = texte[len(snip):].strip()
|
||
for snip in ('<p></p>', '<p><br></p>', '<p> </p>'):
|
||
while texte.endswith(snip):
|
||
texte = texte[:-len(snip)].strip()
|
||
return texte
|
||
|
||
|
||
class DocumentUploadForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Document
|
||
fields = '__all__'
|
||
widgets = {'famille': forms.HiddenInput}
|
||
labels = {'fichier': ''}
|
||
|
||
|
||
class RapportEditForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Rapport
|
||
exclude = ['famille', 'auteur']
|
||
widgets = {
|
||
'famille': forms.HiddenInput,
|
||
'date': PickDateWidget,
|
||
'pres_interv': BSCheckboxSelectMultiple,
|
||
'sig_interv': BSCheckboxSelectMultiple,
|
||
}
|
||
field_classes = {
|
||
'situation': RichTextField,
|
||
'projet': RichTextField,
|
||
'observations': RichTextField,
|
||
}
|
||
field_order = [
|
||
'date', 'pres_interv', 'situation', 'observations', 'projet', 'sig_interv',
|
||
]
|
||
|
||
def __init__(self, user=None, **kwargs):
|
||
super().__init__(**kwargs)
|
||
if 'sig_interv' in self.fields:
|
||
interv_qs = Utilisateur.objects.filter(
|
||
pk__in=kwargs['initial']['famille'].suivi.intervenant_set.actifs(
|
||
self.instance.date or date.today()
|
||
).values_list('intervenant', flat=True)
|
||
)
|
||
self.fields['pres_interv'].queryset = interv_qs
|
||
self.fields['sig_interv'].queryset = interv_qs
|
||
|
||
|
||
class MonthSelectionForm(BootstrapMixin, forms.Form):
|
||
mois = forms.ChoiceField(choices=((mois_idx, MONTHS[mois_idx]) for mois_idx in range(1, 13)))
|
||
annee = forms.ChoiceField(
|
||
label='Année',
|
||
choices=((2019, 2019),
|
||
(2020, 2020),
|
||
(2021, 2021),
|
||
(2022, 2022),
|
||
(2023, 2023),
|
||
(2024, 2024))
|
||
)
|
||
|
||
|
||
class DateYearForm(forms.Form):
|
||
year = forms.ChoiceField(choices=[(str(y), str(y)) for y in range(2020, date.today().year + 1)])
|
||
|
||
def __init__(self, data=None, **kwargs):
|
||
if not data:
|
||
data = {'year': date.today().year}
|
||
super().__init__(data, **kwargs)
|
||
|
||
|
||
class ContactExterneAutocompleteForm(forms.ModelForm):
|
||
|
||
class Meta:
|
||
model = Personne
|
||
fields = ('reseaux',)
|
||
widgets = {'reseaux': autocomplete.ModelSelect2Multiple(url='contact-externe-autocomplete')}
|
||
labels = {'reseaux': 'Contacts'}
|
||
|
||
|
||
class ContactFilterForm(forms.Form):
|
||
service = forms.ModelChoiceField(label="Service", queryset=Service.objects.all(), required=False)
|
||
role = forms.ModelChoiceField(label="Rôle", queryset=Role.objects.exclude(est_famille=True), required=False)
|
||
texte = forms.CharField(
|
||
widget=forms.TextInput(attrs={'placeholder': 'Recherche…', 'autocomplete': 'off'}),
|
||
required=False
|
||
)
|
||
sort_by = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||
|
||
sort_by_mapping = {
|
||
'nom': ['nom', 'prenom'],
|
||
'service': ['service'],
|
||
'role': ['roles__nom'],
|
||
'activite': ['profession'],
|
||
}
|
||
|
||
def filter(self, contacts):
|
||
if self.cleaned_data['service']:
|
||
contacts = contacts.filter(service=self.cleaned_data['service'])
|
||
if self.cleaned_data['role']:
|
||
contacts = contacts.filter(roles=self.cleaned_data['role'])
|
||
if self.cleaned_data['texte']:
|
||
contacts = contacts.filter(nom__icontains=self.cleaned_data['texte'])
|
||
if self.cleaned_data['sort_by']:
|
||
order_desc = self.cleaned_data['sort_by'].startswith('-')
|
||
contacts = contacts.order_by(*([
|
||
('-' if order_desc else '') + key
|
||
for key in self.sort_by_mapping.get(self.cleaned_data['sort_by'].strip('-'), [])
|
||
]))
|
||
return contacts
|
||
|
||
|
||
class FamilleAdresseForm(CityChMixin, BootstrapMixin, forms.ModelForm):
|
||
city_auto = CityChField(required=False)
|
||
postal_code_model_field = 'npa'
|
||
city_model_field = 'localite'
|
||
|
||
class Meta:
|
||
model = Famille
|
||
fields = ('rue', 'npa', 'localite')
|
||
|
||
def __init__(self, **kwargs):
|
||
self.famille = kwargs.pop('famille', None)
|
||
super().__init__(**kwargs)
|
||
|
||
membres = [
|
||
(m.pk, f"{m.nom_prenom} ({m.role}), {m.adresse}")
|
||
for m in self.famille.membres.all()
|
||
]
|
||
self.fields['membres'] = forms.MultipleChoiceField(
|
||
widget=BSCheckboxSelectMultiple,
|
||
choices=membres,
|
||
required=False
|
||
)
|
||
|
||
|
||
class FamilleForm(BootstrapMixin, ReadOnlyableMixin, forms.ModelForm):
|
||
equipe = forms.ChoiceField(label="Équipe", choices=[('', '------')] + Suivi.EQUIPES_CHOICES[:2])
|
||
|
||
# Concernant 'npa' & 'location', ils sont en lecture seule ici => pas besoin de faire de l'autocomplete
|
||
class Meta:
|
||
model = Famille
|
||
fields = ['nom', 'rue', 'npa', 'localite', 'telephone', 'region', 'autorite_parentale',
|
||
'monoparentale', 'statut_marital', 'connue', 'accueil', 'garde',
|
||
'provenance', 'statut_financier']
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
if self.instance and self.instance.pk:
|
||
self.fields['rue'].widget.attrs['readonly'] = True
|
||
self.fields['npa'].widget.attrs['readonly'] = True
|
||
self.fields['localite'].widget.attrs['readonly'] = True
|
||
self.initial['equipe'] = kwargs['instance'].suivi.equipe
|
||
self.fields['region'] = forms.ModelChoiceField(
|
||
label='Région',
|
||
queryset=Region.objects.order_by('nom'),
|
||
required=False,
|
||
disabled=self.fields['region'].disabled,
|
||
widget=forms.Select(attrs={'class': 'form-select'})
|
||
)
|
||
self.fields['provenance'].choices = [
|
||
ch for ch in self.fields['provenance'].choices
|
||
if (ch[0] != 'autre' or self.instance.provenance == 'autre')
|
||
]
|
||
|
||
def save(self, **kwargs):
|
||
famille = super().save(**kwargs)
|
||
if famille.suivi.equipe != self.cleaned_data['equipe']:
|
||
famille.suivi.equipe = self.cleaned_data['equipe']
|
||
famille.suivi.save()
|
||
return famille
|
||
|
||
def clean_nom(self):
|
||
return format_nom_prenom(self.cleaned_data['nom'])
|
||
|
||
def clean_localite(self):
|
||
localite = self.cleaned_data.get('localite')
|
||
if localite and localite[0].islower():
|
||
localite = localite[0].upper() + localite[1:]
|
||
return localite
|
||
|
||
|
||
class FamilleCreateForm(CityChMixin, FamilleForm):
|
||
motif_detail = forms.CharField(
|
||
label="Motif de la demande", widget=forms.Textarea(), required=False
|
||
)
|
||
city_auto = CityChField(required=False)
|
||
postal_code_model_field = 'npa'
|
||
city_model_field = 'localite'
|
||
field_order = ['nom', 'rue', 'city_auto']
|
||
|
||
class Meta(FamilleForm.Meta):
|
||
exclude = ['typ', 'destination']
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['provenance'].choices = [
|
||
ch for ch in self.fields['provenance'].choices if ch[0] != 'autre'
|
||
]
|
||
|
||
def save(self, **kwargs):
|
||
famille = self._meta.model.objects.create_famille(
|
||
equipe=self.cleaned_data['equipe'], **self.instance.__dict__
|
||
)
|
||
famille.suivi.motif_detail = self.cleaned_data['motif_detail']
|
||
famille.suivi.save()
|
||
return famille
|
||
|
||
|
||
class SuiviForm(ReadOnlyableMixin, forms.ModelForm):
|
||
equipe = forms.TypedChoiceField(
|
||
choices=[('', '-------')] + Suivi.EQUIPES_CHOICES[:2], required=False
|
||
)
|
||
|
||
class Meta:
|
||
model = Suivi
|
||
fields = [
|
||
'equipe', 'heure_coord', 'ope_referent', 'ope_referent_2',
|
||
'mandat_ope', 'service_orienteur', 'service_annonceur', 'motif_demande',
|
||
'motif_detail', 'demande_prioritaire', 'demarche', 'collaboration',
|
||
'ressource', 'crise', 'remarque', 'remarque_privee',
|
||
]
|
||
widgets = {
|
||
'mandat_ope': BSCheckboxSelectMultiple,
|
||
'motif_demande': BSCheckboxSelectMultiple,
|
||
}
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.fields['ope_referent'].queryset = Contact.membres_ope()
|
||
self.fields['ope_referent_2'].queryset = Contact.membres_ope()
|
||
|
||
|
||
class IntervenantForm(BootstrapMixin, forms.ModelForm):
|
||
intervenant = forms.ModelChoiceField(
|
||
queryset=Utilisateur.objects.filter(groups__name__startswith='aemo').distinct().order_by('nom')
|
||
)
|
||
date_debut = forms.DateField(
|
||
label='Date de début', initial=date.today(), widget=PickDateWidget()
|
||
)
|
||
|
||
class Meta:
|
||
model = Intervenant
|
||
fields = ['intervenant', 'role', 'date_debut']
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['role'].queryset = Role.objects.filter(est_intervenant=True)
|
||
|
||
|
||
class IntervenantEditForm(BootstrapMixin, forms.ModelForm):
|
||
|
||
class Meta:
|
||
model = Intervenant
|
||
fields = ['intervenant', 'role', 'date_debut', 'date_fin']
|
||
widgets = {
|
||
'date_debut': PickDateWidget(),
|
||
'date_fin': PickDateWidget(),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['intervenant'].disabled = True
|
||
self.fields['role'].disabled = True
|
||
|
||
|
||
class DemandeForm(BootstrapMixin, ReadOnlyableMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Suivi
|
||
fields = (
|
||
'dates_demande', 'ref_presents', 'pers_famille_presentes', 'autres_pers_presentes',
|
||
'difficultes', 'aides', 'competences', 'disponibilites', 'autres_contacts', 'remarque'
|
||
)
|
||
field_classes = {
|
||
'difficultes': RichTextField,
|
||
'aides': RichTextField,
|
||
'disponibilites': RichTextField,
|
||
'competences': RichTextField,
|
||
}
|
||
widgets = {
|
||
'autres_contacts': forms.Textarea(attrs={'cols': 120, 'rows': 4}),
|
||
'remarque': forms.Textarea(attrs={'cols': 120, 'rows': 4}),
|
||
}
|
||
|
||
|
||
class AgendaForm(ReadOnlyableMixin, AgendaFormBase):
|
||
ope_referent = forms.ModelChoiceField(
|
||
queryset=Contact.membres_ope(), required=False
|
||
)
|
||
|
||
class Meta(AgendaFormBase.Meta):
|
||
model = Suivi
|
||
|
||
|
||
class BilanForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Bilan
|
||
exclude = ['famille', 'auteur']
|
||
field_classes = {
|
||
'objectifs': RichTextField,
|
||
'rythme': RichTextField,
|
||
}
|
||
widgets = {
|
||
'famille': forms.HiddenInput,
|
||
'date': PickDateWidget,
|
||
'sig_interv': BSCheckboxSelectMultiple,
|
||
}
|
||
labels = {'fichier': ''}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['sig_interv'].queryset = Utilisateur.objects.filter(
|
||
pk__in=kwargs['initial']['famille'].suivi.intervenant_set.actifs(
|
||
self.instance.date or date.today()
|
||
).values_list('intervenant', flat=True)
|
||
)
|
||
|
||
|
||
class NiveauForm(BootstrapMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = Niveau
|
||
fields = ['niveau_interv', 'date_debut', 'date_fin']
|
||
widgets = {
|
||
'date_debut': PickDateWidget(),
|
||
'date_fin': PickDateWidget(),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.famille = kwargs.pop('famille')
|
||
super().__init__(*args, **kwargs)
|
||
if self.instance.pk is None:
|
||
self.fields.pop('date_fin')
|
||
if self.famille.niveaux.count() > 0:
|
||
self.fields['date_debut'].required = True
|
||
|
||
def clean_niveau_interv(self):
|
||
niv = self.cleaned_data['niveau_interv']
|
||
if self.instance.pk is None:
|
||
der_niv = self.famille.niveaux.last() if self.famille.niveaux.count() > 0 else None
|
||
if der_niv and der_niv.niveau_interv == niv:
|
||
raise forms.ValidationError(f"Le niveau {niv} est déjà actif.")
|
||
return niv
|
||
|
||
|
||
class EquipeFilterForm(forms.Form):
|
||
equipe = forms.ChoiceField(
|
||
label="Équipe", choices=[('', 'Toutes')] + EquipeChoices.choices, required=False,
|
||
widget=forms.Select(attrs={'class': 'form-select form-select-sm immediate-submit'}),
|
||
)
|
||
|
||
def filter(self, familles):
|
||
if self.cleaned_data['equipe']:
|
||
familles = familles.filter(suivi__equipe=self.cleaned_data['equipe'])
|
||
return familles
|
||
|
||
|
||
class FamilleFilterForm(EquipeFilterForm):
|
||
ressource = forms.ChoiceField(
|
||
label='Ressources',
|
||
required=False,
|
||
widget=forms.Select(attrs={'class': 'form-select form-select-sm immediate-submit'})
|
||
)
|
||
niveau = forms.ChoiceField(
|
||
label="Niv. d'interv.",
|
||
required=False,
|
||
widget=forms.Select(attrs={'class': 'form-select form-select-sm immediate-submit'})
|
||
)
|
||
interv = forms.ChoiceField(
|
||
label="Intervenant-e",
|
||
required=False,
|
||
widget=forms.Select(attrs={'class': 'form-select form-select-sm immediate-submit'})
|
||
)
|
||
nom = forms.CharField(
|
||
widget=forms.TextInput(
|
||
attrs={'placeholder': 'Nom de famille…', 'autocomplete': 'off',
|
||
'class': 'form-control form-control-sm inline'}),
|
||
required=False
|
||
)
|
||
duos = forms.ChoiceField(
|
||
label="Duos Educ/Psy",
|
||
required=False,
|
||
widget=forms.Select(attrs={'class': 'form-select form-select-sm immediate-submit'})
|
||
)
|
||
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
intervenants = Utilisateur.objects.filter(is_active=True, groups__name='aemo')
|
||
self.fields['interv'].choices = [
|
||
('', 'Tout-e-s'), ('0', 'Aucun')] + [
|
||
(user.id, user.nom_prenom) for user in intervenants
|
||
]
|
||
self.fields['niveau'].choices = [
|
||
('', 'Tous')
|
||
] + [(key, val) for key, val in Niveau.INTERV_CHOICES]
|
||
|
||
self.fields['ressource'].choices = [
|
||
('', 'Toutes')
|
||
] + [(el.pk, el.nom) for el in Role.objects.filter(
|
||
nom__in=['ASE', 'IPE', 'Coach APA', 'Assistant-e social-e']
|
||
)]
|
||
self.fields['duos'].choices = [
|
||
('', 'Tous')
|
||
] + [
|
||
(duo, duo) for duo in Famille.objects.exclude(
|
||
suivi__date_fin_suivi__isnull=False
|
||
).with_duos().exclude(duo='').values_list('duo', flat=True).distinct().order_by('duo')
|
||
]
|
||
|
||
def filter(self, familles):
|
||
if self.cleaned_data['interv']:
|
||
if self.cleaned_data['interv'] == '0':
|
||
familles = familles.annotate(
|
||
num_interv=Count('suivi__intervenants', filter=(
|
||
Q(suivi__intervenant__date_fin__isnull=True) |
|
||
Q(suivi__intervenant__date_fin__gt=date.today())
|
||
))
|
||
).filter(num_interv=0)
|
||
else:
|
||
familles = familles.filter(
|
||
Q(suivi__intervenant__intervenant=self.cleaned_data['interv']) & (
|
||
Q(suivi__intervenant__date_fin__isnull=True) |
|
||
Q(suivi__intervenant__date_fin__gt=date.today())
|
||
)
|
||
)
|
||
|
||
if self.cleaned_data['nom']:
|
||
familles = familles.filter(nom__istartswith=self.cleaned_data['nom'])
|
||
|
||
familles = super().filter(familles)
|
||
|
||
if self.cleaned_data['niveau']:
|
||
familles = familles.with_niveau_interv().filter(niveau_interv=self.cleaned_data['niveau'])
|
||
|
||
if self.cleaned_data['ressource']:
|
||
ress = Intervenant.objects.actifs().filter(
|
||
role=self.cleaned_data['ressource'],
|
||
).values_list('intervenant', flat=True)
|
||
familles = familles.filter(suivi__intervenants__in=ress).distinct()
|
||
|
||
if self.cleaned_data['duos']:
|
||
familles = familles.with_duos().filter(duo=self.cleaned_data['duos'])
|
||
return familles
|
||
|
||
|
||
class JournalAuteurFilterForm(forms.Form):
|
||
recherche = forms.CharField(
|
||
widget=forms.TextInput(attrs={
|
||
'class': 'search-form-fields form-control d-inline-block',
|
||
'placeholder': 'recherche',
|
||
}),
|
||
required=False,
|
||
)
|
||
auteur = forms.ModelChoiceField(
|
||
queryset=Utilisateur.objects.all(),
|
||
widget=forms.Select(attrs={'class': 'search-form-fields form-select d-inline-block immediate-submit'}),
|
||
required=False,
|
||
)
|
||
|
||
def __init__(self, famille=None, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.fields['auteur'].queryset = Utilisateur.objects.filter(prestations__famille=famille).distinct()
|
||
|
||
def filter(self, prestations):
|
||
if self.cleaned_data['auteur']:
|
||
prestations = prestations.filter(auteur=self.cleaned_data['auteur'])
|
||
if self.cleaned_data['recherche']:
|
||
prestations = prestations.annotate(
|
||
search=SearchVector("texte", config="french_unaccent")
|
||
).filter(
|
||
search=SearchQuery(self.cleaned_data['recherche'], config="french_unaccent")
|
||
)
|
||
return prestations
|