Initial commit
							
								
								
									
										0
									
								
								aemo/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										197
									
								
								aemo/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,197 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
 | 
			
		||||
from django.contrib.auth.forms import UserChangeForm
 | 
			
		||||
from django.contrib.auth.models import Group, Permission
 | 
			
		||||
 | 
			
		||||
from .models import (
 | 
			
		||||
    Bilan, CercleScolaire, Contact, Document, Famille, Formation, GroupInfo,
 | 
			
		||||
    Intervenant, LibellePrestation, Personne, Prestation, Rapport, Region, Role,
 | 
			
		||||
    Service, Suivi, Utilisateur
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TypePrestationFilter(admin.SimpleListFilter):
 | 
			
		||||
    title = 'Prest. famil./générales'
 | 
			
		||||
    parameter_name = 'prest'
 | 
			
		||||
    default_value = None
 | 
			
		||||
 | 
			
		||||
    def lookups(self, request, model_admin):
 | 
			
		||||
        return (
 | 
			
		||||
            ('fam', 'Familiales'),
 | 
			
		||||
            ('gen', 'Générales'),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def queryset(self, request, queryset):
 | 
			
		||||
        value = self.value()
 | 
			
		||||
        if value == 'fam':
 | 
			
		||||
            return queryset.filter(famille__isnull=False).order_by('famille__nom')
 | 
			
		||||
        elif value == 'gen':
 | 
			
		||||
            return queryset.filter(famille__isnull=True).order_by('-date_prestation')
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RegionFilter(admin.SimpleListFilter):
 | 
			
		||||
    title = 'Région'
 | 
			
		||||
    parameter_name = 'region'
 | 
			
		||||
 | 
			
		||||
    def lookups(self, request, model_admin):
 | 
			
		||||
        typ = model_admin.model._meta.app_label
 | 
			
		||||
        kwargs = {f"{typ}": True}
 | 
			
		||||
        return [(reg.pk, reg.nom) for reg in Region.objects.filter(**kwargs)]
 | 
			
		||||
 | 
			
		||||
    def queryset(self, request, queryset):
 | 
			
		||||
        if self.value():
 | 
			
		||||
            return queryset.filter(region_id=self.value())
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DocumentInline(admin.TabularInline):
 | 
			
		||||
    model = Document
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PersonneInLine(admin.StackedInline):
 | 
			
		||||
    model = Personne
 | 
			
		||||
    exclude = ('reseaux', 'remarque')
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Personne)
 | 
			
		||||
class PersonneAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('nom_prenom', 'adresse')
 | 
			
		||||
    search_fields = ('nom', 'prenom')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Contact)
 | 
			
		||||
class ContactAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('nom', 'prenom', 'service', 'roles_display', 'est_actif')
 | 
			
		||||
    list_filter = ('service', 'est_actif')
 | 
			
		||||
    search_fields = ('nom', 'prenom', 'service__sigle')
 | 
			
		||||
 | 
			
		||||
    def roles_display(self, contact):
 | 
			
		||||
        return contact.roles_str(sep=' / ')
 | 
			
		||||
    roles_display.short_description = 'Rôles'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Famille)
 | 
			
		||||
class FamilleAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('nom', 'npa', 'localite', 'get_region')
 | 
			
		||||
    list_filter = (RegionFilter,)
 | 
			
		||||
    inlines = [PersonneInLine, DocumentInline]
 | 
			
		||||
    ordering = ('nom',)
 | 
			
		||||
    search_fields = ('nom', 'localite')
 | 
			
		||||
 | 
			
		||||
    def get_region(self, obj):
 | 
			
		||||
        return obj.region.nom if obj.region else None
 | 
			
		||||
    get_region.short_description = 'Région'
 | 
			
		||||
 | 
			
		||||
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
 | 
			
		||||
        if db_field.name == 'region':
 | 
			
		||||
            app_label = self.model._meta.app_label
 | 
			
		||||
            param = {f"{app_label}": True}
 | 
			
		||||
            kwargs['queryset'] = Region.objects.filter(**param)
 | 
			
		||||
            kwargs['label'] = 'Région'
 | 
			
		||||
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Formation)
 | 
			
		||||
class FormationAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('personne', 'statut')
 | 
			
		||||
    search_fields = ('personne__nom',)
 | 
			
		||||
    ordering = ('personne__nom',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UtilisateurChangeForm(UserChangeForm):
 | 
			
		||||
    class Meta(UserChangeForm.Meta):
 | 
			
		||||
        model = Utilisateur
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Utilisateur)
 | 
			
		||||
class UtilisateurAdmin(UserAdmin):
 | 
			
		||||
    form = UtilisateurChangeForm
 | 
			
		||||
    list_display = [
 | 
			
		||||
        'nom', 'prenom', 'sigle', 'tel_prof', 'tel_prive', 'email',
 | 
			
		||||
        'taux_activite', 'is_active', 'last_login'
 | 
			
		||||
    ]
 | 
			
		||||
    fieldsets = UserAdmin.fieldsets + (
 | 
			
		||||
        (None, {'fields': (
 | 
			
		||||
            'sigle', 'prenom', 'nom', 'rue', 'npa', 'localite',
 | 
			
		||||
            'tel_prof', 'tel_prive', 'service', 'taux_activite'
 | 
			
		||||
        )}),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
admin.site.unregister(Group)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GroupInfoInline(admin.StackedInline):
 | 
			
		||||
    model = GroupInfo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Group)
 | 
			
		||||
class GroupAdmin(GroupAdmin):
 | 
			
		||||
    inlines = [GroupInfoInline]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Region)
 | 
			
		||||
class RegionAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['nom']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Role)
 | 
			
		||||
class RoleAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['nom', 'est_famille', 'est_intervenant', 'est_editeur']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Service)
 | 
			
		||||
class ServiceAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['sigle', 'nom_complet']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(LibellePrestation)
 | 
			
		||||
class LibellePrestationAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['code', 'nom']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Permission)
 | 
			
		||||
class PermissionAdmin(admin.ModelAdmin):
 | 
			
		||||
    search_fields = ['name', 'codename']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Document)
 | 
			
		||||
class DocumentAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('famille', 'titre')
 | 
			
		||||
    search_fields = ('famille__nom',)
 | 
			
		||||
    ordering = ('famille__nom',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Suivi)
 | 
			
		||||
class SuiviAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('famille', 'equipe', 'etape')
 | 
			
		||||
    list_filter = ('equipe',)
 | 
			
		||||
    ordering = ('famille__nom',)
 | 
			
		||||
    search_fields = ('famille__nom',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Intervenant)
 | 
			
		||||
class IntervenantAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('intervenant', 'famille', 'role', 'date_debut', 'date_fin', 'fin_suivi')
 | 
			
		||||
    list_filter = ('role',)
 | 
			
		||||
 | 
			
		||||
    def famille(self, obj):
 | 
			
		||||
        return obj.suivi.famille
 | 
			
		||||
 | 
			
		||||
    def fin_suivi(self, obj):
 | 
			
		||||
        return obj.suivi.date_fin_suivi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Prestation)
 | 
			
		||||
class PrestationAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('lib_prestation', 'date_prestation', 'duree', 'auteur')
 | 
			
		||||
    list_filter = (TypePrestationFilter,)
 | 
			
		||||
    search_fields = ('famille__nom', 'texte')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
admin.site.register(Bilan)
 | 
			
		||||
admin.site.register(Rapport)
 | 
			
		||||
admin.site.register(CercleScolaire)
 | 
			
		||||
							
								
								
									
										212
									
								
								aemo/export.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,212 @@
 | 
			
		|||
from datetime import timedelta
 | 
			
		||||
from tempfile import NamedTemporaryFile
 | 
			
		||||
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
 | 
			
		||||
from openpyxl import Workbook
 | 
			
		||||
from openpyxl.styles import Font
 | 
			
		||||
from openpyxl.utils import get_column_letter
 | 
			
		||||
 | 
			
		||||
from .utils import format_d_m_Y
 | 
			
		||||
 | 
			
		||||
openxml_contenttype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OpenXMLExport:
 | 
			
		||||
    def __init__(self, sheet_title=None):
 | 
			
		||||
        self.wb = Workbook()
 | 
			
		||||
        self.ws = self.wb.active
 | 
			
		||||
        if sheet_title:
 | 
			
		||||
            self.ws.title = sheet_title
 | 
			
		||||
        self.bold = Font(name='Calibri', bold=True)
 | 
			
		||||
        self.row_idx = 1
 | 
			
		||||
 | 
			
		||||
    def write_line(self, values, bold=False, col_widths=()):
 | 
			
		||||
        # A values item can be an object with a `value` attribute
 | 
			
		||||
        for col_idx, value in enumerate(values, start=1):
 | 
			
		||||
            cell = self.ws.cell(row=self.row_idx, column=col_idx)
 | 
			
		||||
            if isinstance(value, timedelta):
 | 
			
		||||
                cell.number_format = '[h]:mm;@'
 | 
			
		||||
            elif hasattr(value, 'number_format'):
 | 
			
		||||
                cell.number_format = value.number_format
 | 
			
		||||
            cell.value = getattr(value, 'value', value)
 | 
			
		||||
            if bold:
 | 
			
		||||
                cell.font = self.bold
 | 
			
		||||
            if col_widths and len(col_widths) >= col_idx:
 | 
			
		||||
                self.ws.column_dimensions[get_column_letter(col_idx)].width = col_widths[col_idx - 1]
 | 
			
		||||
        self.row_idx += 1
 | 
			
		||||
 | 
			
		||||
    def add_sheet(self, title):
 | 
			
		||||
        self.wb.create_sheet(title)
 | 
			
		||||
        self.ws = self.wb[title]
 | 
			
		||||
        self.row_idx = 1
 | 
			
		||||
 | 
			
		||||
    def get_http_response(self, filename):
 | 
			
		||||
        with NamedTemporaryFile() as tmp:
 | 
			
		||||
            self.wb.save(tmp.name)
 | 
			
		||||
            tmp.seek(0)
 | 
			
		||||
            response = HttpResponse(tmp, content_type=openxml_contenttype)
 | 
			
		||||
            response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExportReporting(OpenXMLExport):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__('temp')
 | 
			
		||||
        self.first_sheet = True
 | 
			
		||||
        # Totaux pour vérification interne dans les tests.
 | 
			
		||||
        self._total_spe = {'eval': timedelta(0), 'suivi': timedelta(0), 'gen': timedelta(0)}
 | 
			
		||||
 | 
			
		||||
    def setup_sheet(self, title):
 | 
			
		||||
        if not self.first_sheet:
 | 
			
		||||
            self.add_sheet(title)
 | 
			
		||||
        else:
 | 
			
		||||
            self.ws.title = title
 | 
			
		||||
        self.first_sheet = False
 | 
			
		||||
 | 
			
		||||
    def produce_suivis(self, sheet_title, query, mois):
 | 
			
		||||
        self.setup_sheet(sheet_title)
 | 
			
		||||
        SuiviSheet(self, mois).produce(query)
 | 
			
		||||
 | 
			
		||||
    def produce_nouveaux(self, sheet_title, query):
 | 
			
		||||
        self.setup_sheet(sheet_title)
 | 
			
		||||
        NouveauxSheet(self).produce(query)
 | 
			
		||||
 | 
			
		||||
    def produce_termines(self, sheet_title, query):
 | 
			
		||||
        self.setup_sheet(sheet_title)
 | 
			
		||||
        TerminesSheet(self).produce(query)
 | 
			
		||||
 | 
			
		||||
    def produce_totaux(self, sheet_title):
 | 
			
		||||
        self.setup_sheet(sheet_title)
 | 
			
		||||
        self.write_line(['Évaluation', self._total_spe['eval']], col_widths=[25, 10])
 | 
			
		||||
        self.write_line(['Accompagnement', self._total_spe['suivi']])
 | 
			
		||||
        self.write_line(['Intervention générale', self._total_spe['gen']])
 | 
			
		||||
        self.write_line(['Total', self._total_spe['eval'] + self._total_spe['suivi'] + self._total_spe['gen']])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseFamilleSheet:
 | 
			
		||||
    en_tetes = [
 | 
			
		||||
        ('Institution', 12), ('Prestation', 10), ('Nom', 25), ('Prenom', 15), ('Genre', 8),
 | 
			
		||||
        ('Date de naissance', 17), ('Adresse', 20), ('NPA', 8), ('Localité', 20), ('Canton', 8),
 | 
			
		||||
        ('OPE', 25), ('Nom mère', 20), ('Prénom mère', 20), ('Nom père', 20), ('Prénom père', 20),
 | 
			
		||||
        ('Autorité parentale', 18), ('Statut marital', 15), ('Statut financier', 15),
 | 
			
		||||
        ('Fam. monopar.', 15), ('Nbre enfants', 12),
 | 
			
		||||
        ('Date demande', 15), ('Provenance', 15), ('Motif demande', 40), ('Début suivi', 15),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, exp):
 | 
			
		||||
        self.exp = exp
 | 
			
		||||
 | 
			
		||||
    def _set_col_dimensions(self):
 | 
			
		||||
        for col_idx, (_, size) in enumerate(self.en_tetes, start=1):
 | 
			
		||||
            self.exp.ws.column_dimensions[get_column_letter(col_idx)].width = size
 | 
			
		||||
 | 
			
		||||
    def produce(self, query):
 | 
			
		||||
        self._set_col_dimensions()
 | 
			
		||||
        self.exp.write_line([et[0] for et in self.en_tetes], bold=True)
 | 
			
		||||
        for famille in query:
 | 
			
		||||
            self.produce_famille(famille)
 | 
			
		||||
        self.exp.ws.freeze_panes = self.exp.ws['A2']
 | 
			
		||||
 | 
			
		||||
    def produce_famille(self, famille):
 | 
			
		||||
        membres_suivis = famille.membres_suivis()
 | 
			
		||||
        if membres_suivis:
 | 
			
		||||
            for pers in famille.membres_suivis():
 | 
			
		||||
                data = self.collect_pers_data(famille, pers)
 | 
			
		||||
                self.exp.write_line(data)
 | 
			
		||||
        else:
 | 
			
		||||
            data = self.collect_pers_data(famille, None)
 | 
			
		||||
            self.exp.write_line(data)
 | 
			
		||||
 | 
			
		||||
    def collect_pers_data(self, famille, pers):
 | 
			
		||||
        parents = famille.parents()
 | 
			
		||||
        mere = next((par for par in parents if par.role.nom == 'Mère'), None)
 | 
			
		||||
        pere = next((par for par in parents if par.role.nom == 'Père'), None)
 | 
			
		||||
        return [
 | 
			
		||||
            'Fondation Transit',
 | 
			
		||||
            'AEMO',
 | 
			
		||||
            pers.nom if pers else famille.nom,
 | 
			
		||||
            pers.prenom if pers else "-",
 | 
			
		||||
            pers.genre if pers else "-",
 | 
			
		||||
            format_d_m_Y(pers.date_naissance) if pers else "-",
 | 
			
		||||
            pers.rue if pers else famille.rue,
 | 
			
		||||
            pers.npa if pers else famille.npa,
 | 
			
		||||
            pers.localite if pers else famille.localite,
 | 
			
		||||
            'NE',
 | 
			
		||||
            famille.suivi.ope_referent.nom_prenom if famille.suivi.ope_referent else '',
 | 
			
		||||
            mere.nom if mere else '',
 | 
			
		||||
            mere.prenom if mere else '',
 | 
			
		||||
            pere.nom if pere else '',
 | 
			
		||||
            pere.prenom if pere else '',
 | 
			
		||||
            famille.get_autorite_parentale_display(),
 | 
			
		||||
            famille.get_statut_marital_display(),
 | 
			
		||||
            famille.get_statut_financier_display(),
 | 
			
		||||
            {True: 'OUI', False: 'NON', None: ''}[famille.monoparentale],
 | 
			
		||||
            len(famille.membres_suivis()) + len(famille.enfants_non_suivis()),
 | 
			
		||||
 | 
			
		||||
            format_d_m_Y(famille.suivi.date_demande),
 | 
			
		||||
            famille.get_provenance_display(),
 | 
			
		||||
            famille.suivi.get_motif_demande_display(),
 | 
			
		||||
            format_d_m_Y(famille.suivi.date_debut_suivi),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SuiviSheet(BaseFamilleSheet):
 | 
			
		||||
    en_tetes = BaseFamilleSheet.en_tetes + [('H. Évaluation', 12), ('H. Suivi', 12), ('H. Prest. gén.', 12)]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, exp, date_debut_mois):
 | 
			
		||||
        self.date_debut_mois = date_debut_mois
 | 
			
		||||
        super().__init__(exp)
 | 
			
		||||
 | 
			
		||||
    def produce_famille(self, famille):
 | 
			
		||||
        # Prepare some data common to famille
 | 
			
		||||
        nb_membres_suivis = len(famille.membres_suivis()) or 1
 | 
			
		||||
        famille._h_evaluation_par_pers = famille.total_mensuel_evaluation(self.date_debut_mois) // nb_membres_suivis
 | 
			
		||||
        famille._h_suivi_par_pers = famille.total_mensuel_suivi(self.date_debut_mois) // nb_membres_suivis
 | 
			
		||||
        super().produce_famille(famille)
 | 
			
		||||
 | 
			
		||||
    def collect_pers_data(self, famille, pers):
 | 
			
		||||
        data = super().collect_pers_data(famille, pers)
 | 
			
		||||
        h_evaluation = famille._h_evaluation_par_pers
 | 
			
		||||
        h_suivi = famille._h_suivi_par_pers
 | 
			
		||||
        h_prest_gen = famille.prest_gen  # Annotation from the view.
 | 
			
		||||
        data.extend([h_evaluation, h_suivi, h_prest_gen])
 | 
			
		||||
        # Variables des totaux
 | 
			
		||||
        self.exp._total_spe['eval'] += h_evaluation
 | 
			
		||||
        self.exp._total_spe['suivi'] += h_suivi
 | 
			
		||||
        self.exp._total_spe['gen'] += h_prest_gen
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NouveauxSheet(BaseFamilleSheet):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TerminesSheet(BaseFamilleSheet):
 | 
			
		||||
    en_tetes = BaseFamilleSheet.en_tetes + [
 | 
			
		||||
        ('Date fin suivi', 15), ('Motif fin suivi', 15), ('Destination', 15), ('Total heures', 12),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def collect_pers_data(self, famille, pers):
 | 
			
		||||
        data = super().collect_pers_data(famille, pers)
 | 
			
		||||
        suivi = famille.suivi
 | 
			
		||||
        data.extend([
 | 
			
		||||
            format_d_m_Y(suivi.date_fin_suivi),
 | 
			
		||||
            famille.suivi.get_motif_fin_suivi_display(),
 | 
			
		||||
            famille.get_destination_display(),
 | 
			
		||||
            famille.temps_total_prestations_reparti(),
 | 
			
		||||
        ])
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExportStatistique(OpenXMLExport):
 | 
			
		||||
    def __init__(self, *args, col_widths=None, **kwargs):
 | 
			
		||||
        self.col_widths = col_widths
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def fill_data(self, generator):
 | 
			
		||||
        for row in generator:
 | 
			
		||||
            if row and row[0] == 'BOLD':
 | 
			
		||||
                self.write_line(row[1:], bold=True, col_widths=self.col_widths)
 | 
			
		||||
            else:
 | 
			
		||||
                self.write_line(row, col_widths=self.col_widths)
 | 
			
		||||
							
								
								
									
										131
									
								
								aemo/file_array_field.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,131 @@
 | 
			
		|||
"""Uploaded on https://code.djangoproject.com/ticket/25756 by Riccardo Di Virgilio"""
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db.models.fields.files import FieldFile, File
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MultiFileInput(forms.FileInput):
 | 
			
		||||
 | 
			
		||||
    def render(self, name, value, attrs={}, renderer=None):
 | 
			
		||||
        attrs['multiple'] = 'multiple'
 | 
			
		||||
        return super().render(name, None, attrs=attrs)
 | 
			
		||||
 | 
			
		||||
    def value_from_datadict(self, data, files, name):
 | 
			
		||||
        if hasattr(files, 'getlist'):
 | 
			
		||||
            return files.getlist(name)
 | 
			
		||||
        else:
 | 
			
		||||
            return [files.get(name)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MultiFileField(forms.FileField):
 | 
			
		||||
    widget = MultiFileInput
 | 
			
		||||
    default_error_messages = {
 | 
			
		||||
        'min_num': "Ensure at least %(min_num)s files are uploaded (received %(num_files)s).",
 | 
			
		||||
        'max_num': "Ensure at most %(max_num)s files are uploaded (received %(num_files)s).",
 | 
			
		||||
        'file_size': "File: %(uploaded_file_name)s, exceeded maximum upload size."
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        self.min_num = kwargs.pop('min_num', 0)
 | 
			
		||||
        self.max_num = kwargs.pop('max_num', None)
 | 
			
		||||
        self.maximum_file_size = kwargs.pop('maximum_file_size', None)
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def to_python(self, data):
 | 
			
		||||
        ret = []
 | 
			
		||||
        for item in data:
 | 
			
		||||
            ret.append(super().to_python(item))
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
        super().validate(data)
 | 
			
		||||
        num_files = len(data)
 | 
			
		||||
        if len(data) and not data[0]:
 | 
			
		||||
            num_files = 0
 | 
			
		||||
        if num_files < self.min_num:
 | 
			
		||||
            raise ValidationError(self.error_messages['min_num'] % {'min_num': self.min_num, 'num_files': num_files})
 | 
			
		||||
        elif self.max_num and num_files > self.max_num:
 | 
			
		||||
            raise ValidationError(self.error_messages['max_num'] % {'max_num': self.max_num, 'num_files': num_files})
 | 
			
		||||
        for uploaded_file in data:
 | 
			
		||||
            if self.maximum_file_size and uploaded_file.size > self.maximum_file_size:
 | 
			
		||||
                raise ValidationError(self.error_messages['file_size'] % {'uploaded_file_name': uploaded_file.name})
 | 
			
		||||
 | 
			
		||||
    def clean(self, data, initial=None):
 | 
			
		||||
        value = super().clean(data, initial=initial)
 | 
			
		||||
        # Do not overwrite, but append to initial
 | 
			
		||||
        if data and initial:
 | 
			
		||||
            value = initial + value
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_file_object(field, instance, file):
 | 
			
		||||
    if isinstance(file, str) or file is None:
 | 
			
		||||
        return field.attr_class(instance, field, file)
 | 
			
		||||
    elif isinstance(file, File) and not isinstance(file, FieldFile):
 | 
			
		||||
        file_copy = field.attr_class(instance, field, file.name)
 | 
			
		||||
        file_copy.file = file
 | 
			
		||||
        file_copy._committed = False
 | 
			
		||||
        return file_copy
 | 
			
		||||
    elif isinstance(file, FieldFile) and not hasattr(file, 'field'):
 | 
			
		||||
        file.instance = instance
 | 
			
		||||
        file.field = field
 | 
			
		||||
        file.storage = field.storage
 | 
			
		||||
        return file
 | 
			
		||||
    else:
 | 
			
		||||
        return file
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArrayFileDescriptor:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, field):
 | 
			
		||||
        self.field = field
 | 
			
		||||
 | 
			
		||||
    def __get__(self, instance=None, owner=None):
 | 
			
		||||
        if instance is None:
 | 
			
		||||
            raise AttributeError(
 | 
			
		||||
                "The '%s' attribute can only be accessed from %s instances."
 | 
			
		||||
                % (self.field.name, owner.__name__))
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            to_file_object(self.field.base_field, instance, file)
 | 
			
		||||
            for file in (instance.__dict__[self.field.name] or [])
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def __set__(self, instance, value):
 | 
			
		||||
        instance.__dict__[self.field.name] = value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArrayFileField(ArrayField):
 | 
			
		||||
 | 
			
		||||
    descriptor_class = ArrayFileDescriptor
 | 
			
		||||
 | 
			
		||||
    def set_attributes_from_name(self, name):
 | 
			
		||||
        super(ArrayField, self).set_attributes_from_name(name)
 | 
			
		||||
        self.base_field.set_attributes_from_name("%s_array" % name)
 | 
			
		||||
 | 
			
		||||
    def contribute_to_class(self, cls, name, **kwargs):
 | 
			
		||||
        super().contribute_to_class(cls, name, **kwargs)
 | 
			
		||||
        setattr(cls, self.name, self.descriptor_class(self))
 | 
			
		||||
 | 
			
		||||
    def pre_save(self, instance, add):
 | 
			
		||||
        "Returns field's value just before saving."
 | 
			
		||||
        files = [
 | 
			
		||||
            to_file_object(self.base_field, instance, file)
 | 
			
		||||
            for file in super(ArrayField, self).pre_save(instance, add)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for file_copy in files:
 | 
			
		||||
            if file_copy and not file_copy._committed:
 | 
			
		||||
                file_copy.save(file_copy.name, file_copy, save=False)
 | 
			
		||||
 | 
			
		||||
        return files
 | 
			
		||||
 | 
			
		||||
    def formfield(self, **kwargs):
 | 
			
		||||
        defaults = {
 | 
			
		||||
            'form_class': MultiFileField,
 | 
			
		||||
            'max_num': self.size
 | 
			
		||||
        }
 | 
			
		||||
        defaults.update(kwargs)
 | 
			
		||||
        return super(ArrayField, self).formfield(**defaults)
 | 
			
		||||
							
								
								
									
										969
									
								
								aemo/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,969 @@
 | 
			
		|||
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
 | 
			
		||||
							
								
								
									
										126
									
								
								aemo/management/commands/anonymize.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,126 @@
 | 
			
		|||
"""
 | 
			
		||||
Anonymiser les données de l'application AEMO
 | 
			
		||||
"""
 | 
			
		||||
import sys
 | 
			
		||||
from datetime import date, timedelta
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from django_otp.plugins.otp_static.models import StaticDevice
 | 
			
		||||
from django_otp.plugins.otp_totp.models import TOTPDevice
 | 
			
		||||
 | 
			
		||||
from aemo.models import Bilan, Contact, Famille, Personne, Prestation, Rapport, Suivi, Utilisateur
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from faker import Faker
 | 
			
		||||
except ImportError:
 | 
			
		||||
    print("La commande anonymize exige la présence du paquet Python Faker (pip install faker)")
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        fake = Faker('fr_CH')
 | 
			
		||||
 | 
			
		||||
        StaticDevice.objects.all().delete()
 | 
			
		||||
        TOTPDevice.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        # Contacts et Utilisateurs
 | 
			
		||||
        Contact.objects.filter(utilisateur__isnull=True, est_actif=False).delete()
 | 
			
		||||
        for contact in Contact.objects.all().select_related('utilisateur'):
 | 
			
		||||
            nom, prenom, service = fake.last_name(), fake.first_name(), contact.service
 | 
			
		||||
            while Contact.objects.filter(nom=nom, prenom=prenom, service=service).exists():
 | 
			
		||||
                nom, prenom, service = fake.last_name(), fake.first_name(), contact.service
 | 
			
		||||
            contact.prenom = prenom
 | 
			
		||||
            contact.nom = nom
 | 
			
		||||
            contact.rue = fake.street_address()[:30]
 | 
			
		||||
            contact.npa = fake.postcode()
 | 
			
		||||
            contact.localite = fake.city()
 | 
			
		||||
            if contact.tel_prive:
 | 
			
		||||
                contact.tel_prive = fake.phone_number()
 | 
			
		||||
            if contact.tel_prof:
 | 
			
		||||
                contact.tel_prof = fake.phone_number()
 | 
			
		||||
            if contact.email:
 | 
			
		||||
                contact.email = fake.email()
 | 
			
		||||
            try:
 | 
			
		||||
                util = contact.utilisateur
 | 
			
		||||
                util.first_name = contact.prenom
 | 
			
		||||
                util.last_name = contact.nom
 | 
			
		||||
                username = f'{util.last_name}{util.first_name[0]}'
 | 
			
		||||
                while Utilisateur.objects.filter(username=username).exists():
 | 
			
		||||
                    username += '0'
 | 
			
		||||
                util.username = username
 | 
			
		||||
                util.set_password(fake.password())
 | 
			
		||||
                util.save()
 | 
			
		||||
            except Utilisateur.DoesNotExist:
 | 
			
		||||
                pass
 | 
			
		||||
            contact.save()
 | 
			
		||||
 | 
			
		||||
        # Familles et Personnes
 | 
			
		||||
        Famille.objects.filter(archived_at__date__gt=date.today() - timedelta(days=360)).delete()
 | 
			
		||||
        for famille in Famille.objects.all():
 | 
			
		||||
            famille.nom = fake.last_name()
 | 
			
		||||
            famille.rue = fake.street_address()
 | 
			
		||||
            famille.npa = fake.postcode()
 | 
			
		||||
            famille.localite = fake.city()
 | 
			
		||||
            famille.telephone = fake.phone_number()
 | 
			
		||||
            if famille.remarques:
 | 
			
		||||
                famille.remarques = fake.text(max_nb_chars=100)
 | 
			
		||||
            famille.save()
 | 
			
		||||
 | 
			
		||||
        for personne in Personne.objects.all().select_related('famille'):
 | 
			
		||||
            personne.nom = personne.famille.nom
 | 
			
		||||
            personne.prenom = fake.first_name()
 | 
			
		||||
            if personne.date_naissance:
 | 
			
		||||
                personne.date_naissance = fake.date_between(
 | 
			
		||||
                    personne.date_naissance - timedelta(days=100),
 | 
			
		||||
                    personne.date_naissance + timedelta(days=100),
 | 
			
		||||
                )
 | 
			
		||||
            personne.rue = personne.famille.rue
 | 
			
		||||
            personne.npa = personne.famille.npa
 | 
			
		||||
            personne.localite = personne.famille.localite
 | 
			
		||||
            if personne.telephone:
 | 
			
		||||
                personne.telephone = fake.phone_number()
 | 
			
		||||
            if personne.email:
 | 
			
		||||
                personne.email = fake.email()
 | 
			
		||||
            if personne.remarque:
 | 
			
		||||
                personne.remarque = fake.text(max_nb_chars=100)
 | 
			
		||||
            if personne.remarque_privee:
 | 
			
		||||
                personne.remarque_privee = fake.text(max_nb_chars=100)
 | 
			
		||||
            personne.save()
 | 
			
		||||
 | 
			
		||||
        # Suivi
 | 
			
		||||
        for suivi in Suivi.objects.all():
 | 
			
		||||
            for text_field in [
 | 
			
		||||
                'difficultes', 'aides', 'competences', 'disponibilites', 'collaboration',
 | 
			
		||||
                'ressource', 'crise', 'remarque', 'remarque_privee'
 | 
			
		||||
            ]:
 | 
			
		||||
                if getattr(suivi, text_field):
 | 
			
		||||
                    setattr(suivi, text_field, fake.text(max_nb_chars=200))
 | 
			
		||||
            for pres_field in ['pers_famille_presentes', 'ref_presents', 'autres_pers_presentes']:
 | 
			
		||||
                if getattr(suivi, pres_field):
 | 
			
		||||
                    mots = getattr(suivi, pres_field).split(" ")
 | 
			
		||||
                    mots_rempl = " ".join([
 | 
			
		||||
                        fake.first_name() if (len(mot) and mot[0].isupper()) else mot for mot in mots
 | 
			
		||||
                    ])
 | 
			
		||||
                    setattr(suivi, pres_field, mots_rempl[:100])
 | 
			
		||||
            suivi.save()
 | 
			
		||||
 | 
			
		||||
        # Rapports/Bilans
 | 
			
		||||
        for rapport in Rapport.objects.all():
 | 
			
		||||
            for text_field in [
 | 
			
		||||
                'situation', 'evolutions', 'evaluation', 'projet',
 | 
			
		||||
            ]:
 | 
			
		||||
                if getattr(rapport, text_field):
 | 
			
		||||
                    setattr(rapport, text_field, fake.text(max_nb_chars=200))
 | 
			
		||||
            rapport.save()
 | 
			
		||||
        for bilan in Bilan.objects.all():
 | 
			
		||||
            for text_field in ['objectifs', 'rythme']:
 | 
			
		||||
                if getattr(bilan, text_field):
 | 
			
		||||
                    setattr(bilan, text_field, fake.text(max_nb_chars=200))
 | 
			
		||||
            bilan.save()
 | 
			
		||||
 | 
			
		||||
        # Prestations
 | 
			
		||||
        for prest in Prestation.objects.all():
 | 
			
		||||
            prest.texte = fake.text(max_nb_chars=200)
 | 
			
		||||
            prest.save(update_fields=['texte'])
 | 
			
		||||
							
								
								
									
										10
									
								
								aemo/management/commands/test.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
from django.core.management.commands.test import Command as TestCommand
 | 
			
		||||
from django.test.utils import override_settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(TestCommand):
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        with override_settings(
 | 
			
		||||
            PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher'],
 | 
			
		||||
        ):
 | 
			
		||||
            return super().handle(*args, **options)
 | 
			
		||||
							
								
								
									
										382
									
								
								aemo/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,382 @@
 | 
			
		|||
# Generated by Django 5.0.4 on 2024-06-03 09:59
 | 
			
		||||
 | 
			
		||||
import common.fields
 | 
			
		||||
import django.contrib.auth.models
 | 
			
		||||
import django.contrib.auth.validators
 | 
			
		||||
import django.core.validators
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
import django_countries.fields
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('auth', '0012_alter_user_first_name_max_length'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Contact',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('prenom', models.CharField(max_length=30, verbose_name='Prénom')),
 | 
			
		||||
                ('nom', models.CharField(max_length=30, verbose_name='Nom')),
 | 
			
		||||
                ('rue', models.CharField(blank=True, max_length=30, verbose_name='Rue')),
 | 
			
		||||
                ('npa', models.CharField(blank=True, max_length=4, verbose_name='NPA')),
 | 
			
		||||
                ('localite', models.CharField(blank=True, max_length=30, verbose_name='Localité')),
 | 
			
		||||
                ('tel_prive', models.CharField(blank=True, max_length=30, verbose_name='Tél. privé')),
 | 
			
		||||
                ('tel_prof', models.CharField(blank=True, max_length=30, verbose_name='Tél. prof.')),
 | 
			
		||||
                ('email', models.EmailField(blank=True, max_length=100)),
 | 
			
		||||
                ('profession', models.CharField(blank=True, max_length=100, verbose_name='Activité/prof.')),
 | 
			
		||||
                ('remarque', models.TextField(blank=True, verbose_name='Remarque')),
 | 
			
		||||
                ('est_actif', models.BooleanField(default=True, verbose_name='actif')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Contact',
 | 
			
		||||
                'ordering': ('nom', 'prenom'),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CercleScolaire',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('nom', models.CharField(max_length=50, unique=True)),
 | 
			
		||||
                ('telephone', models.CharField(blank=True, max_length=35, verbose_name='tél.')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Cercle scolaire',
 | 
			
		||||
                'verbose_name_plural': 'Cercles scolaires',
 | 
			
		||||
                'ordering': ('nom',),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Famille',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created_at', models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Archivée le')),
 | 
			
		||||
                ('nom', models.CharField(max_length=40, verbose_name='Nom de famille')),
 | 
			
		||||
                ('rue', models.CharField(blank=True, max_length=60, verbose_name='Rue')),
 | 
			
		||||
                ('npa', models.CharField(blank=True, max_length=4, verbose_name='NPA')),
 | 
			
		||||
                ('localite', models.CharField(blank=True, max_length=30, verbose_name='Localité')),
 | 
			
		||||
                ('telephone', models.CharField(blank=True, max_length=60, verbose_name='Tél.')),
 | 
			
		||||
                ('autorite_parentale', models.CharField(blank=True, choices=[('conjointe', 'Conjointe'), ('pere', 'Père'), ('mere', 'Mère'), ('tutelle', 'Tutelle')], max_length=20, verbose_name='Autorité parentale')),
 | 
			
		||||
                ('monoparentale', models.BooleanField(blank=True, default=None, null=True, verbose_name='Famille monoparent.')),
 | 
			
		||||
                ('statut_marital', models.CharField(blank=True, choices=[('celibat', 'Célibataire'), ('mariage', 'Marié'), ('pacs', 'PACS'), ('concubin', 'Concubin'), ('veuf', 'Veuf'), ('separe', 'Séparé'), ('divorce', 'Divorcé')], max_length=20, verbose_name='Statut marital')),
 | 
			
		||||
                ('connue', models.BooleanField(default=False, verbose_name='famille déjà suivie')),
 | 
			
		||||
                ('accueil', models.BooleanField(default=False, verbose_name="famille d'accueil")),
 | 
			
		||||
                ('besoins_part', models.BooleanField(default=False, verbose_name='famille à besoins particuliers')),
 | 
			
		||||
                ('sap', models.BooleanField(default=False, verbose_name='famille s@p')),
 | 
			
		||||
                ('garde', models.CharField(blank=True, choices=[('partage', 'garde partagée'), ('droit', 'droit de garde'), ('visite', 'droit de visite')], max_length=20, verbose_name='Type de garde')),
 | 
			
		||||
                ('provenance', models.CharField(blank=True, choices=[('famille', 'Famille'), ('ies-ne', 'IES-NE'), ('ies-hc', 'IES-HC'), ('aemo', 'SAEMO'), ('fah', "Famille d'accueil"), ('refug', 'Centre d’accueil réfugiés'), ('hopital', 'Hôpital'), ('autre', 'Autre')], max_length=30, verbose_name='Provenance')),
 | 
			
		||||
                ('destination', models.CharField(blank=True, choices=[('famille', 'Famille'), ('ies-ne', 'IES-NE'), ('ies-hc', 'IES-HC'), ('aemo', 'SAEMO'), ('fah', "Famille d'accueil"), ('refug', 'Centre d’accueil réfugiés'), ('hopital', 'Hôpital'), ('autre', 'Autre')], max_length=30, verbose_name='Destination')),
 | 
			
		||||
                ('statut_financier', models.CharField(blank=True, choices=[('ai', 'AI PC'), ('gsr', 'GSR'), ('osas', 'OSAS'), ('revenu', 'Revenu')], max_length=30, verbose_name='Statut financier')),
 | 
			
		||||
                ('remarques', models.TextField(blank=True)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('nom', 'npa'),
 | 
			
		||||
                'permissions': (
 | 
			
		||||
                    ('can_manage_waiting_list', "Gérer la liste d'attente"),
 | 
			
		||||
                    ('can_archive', 'Archiver les dossiers AEMO'),
 | 
			
		||||
                    ('export_stats', 'Exporter les statistiques')
 | 
			
		||||
                ),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='LibellePrestation',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('code', models.CharField(max_length=6, unique=True, verbose_name='Code')),
 | 
			
		||||
                ('nom', models.CharField(max_length=30, verbose_name='Nom')),
 | 
			
		||||
                ('actes', models.TextField(blank=True, verbose_name='Actes à prester')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('code',),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Region',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('nom', models.CharField(max_length=30, unique=True)),
 | 
			
		||||
                ('rue', models.CharField(blank=True, max_length=100, verbose_name='Rue')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Role',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('nom', models.CharField(max_length=50, unique=True, verbose_name='Nom')),
 | 
			
		||||
                ('est_famille', models.BooleanField(default=False, verbose_name='Famille')),
 | 
			
		||||
                ('est_intervenant', models.BooleanField(default=False, verbose_name='Intervenant')),
 | 
			
		||||
                ('est_editeur', models.BooleanField(default=False, help_text='Un rôle éditeur donne le droit de modification des dossiers familles si la personne est intervenante.', verbose_name='Éditeur')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('nom',),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Service',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('sigle', models.CharField(max_length=20, unique=True, verbose_name='Sigle')),
 | 
			
		||||
                ('nom_complet', models.CharField(blank=True, max_length=80, verbose_name='Nom complet')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('sigle',),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Utilisateur',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('contact_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='aemo.contact')),
 | 
			
		||||
                ('password', models.CharField(max_length=128, verbose_name='password')),
 | 
			
		||||
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
 | 
			
		||||
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
 | 
			
		||||
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
 | 
			
		||||
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
 | 
			
		||||
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
 | 
			
		||||
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
 | 
			
		||||
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
 | 
			
		||||
                ('sigle', models.CharField(blank=True, max_length=5)),
 | 
			
		||||
                ('equipe', models.CharField(blank=True, choices=[('montagnes', 'Montagnes et V-d-T'), ('littoral', 'Littoral et V-d-R')], max_length=10, verbose_name='Équipe')),
 | 
			
		||||
                ('date_desactivation', models.DateField(blank=True, null=True, verbose_name='Date désactivation')),
 | 
			
		||||
                ('taux_activite', models.PositiveSmallIntegerField(blank=True, default=0, validators=[django.core.validators.MaxValueValidator(100)], verbose_name='Taux d’activité (en %)')),
 | 
			
		||||
                ('decharge', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Heures de décharge')),
 | 
			
		||||
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
 | 
			
		||||
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'user',
 | 
			
		||||
                'verbose_name_plural': 'users',
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
            bases=('aemo.contact', models.Model),
 | 
			
		||||
            managers=[
 | 
			
		||||
                ('objects', django.contrib.auth.models.UserManager()),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='GroupInfo',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('description', models.TextField(blank=True)),
 | 
			
		||||
                ('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Niveau',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('niveau_interv', models.PositiveSmallIntegerField(choices=[(0, '0'), (1, '1'), (2, '2'), (3, '3')], verbose_name='Niveau d’intervention')),
 | 
			
		||||
                ('date_debut', models.DateField(blank=True, null=True, verbose_name='Date début')),
 | 
			
		||||
                ('date_fin', models.DateField(blank=True, null=True, verbose_name='Date fin')),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='niveaux', to='aemo.famille', verbose_name='Famille')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Personne',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created_at', models.DateTimeField(auto_now_add=True, null=True)),
 | 
			
		||||
                ('nom', models.CharField(max_length=30, verbose_name='Nom')),
 | 
			
		||||
                ('prenom', models.CharField(blank=True, max_length=30, verbose_name='Prénom')),
 | 
			
		||||
                ('date_naissance', models.DateField(blank=True, null=True, verbose_name='Date de naissance')),
 | 
			
		||||
                ('genre', models.CharField(choices=[('M', 'M'), ('F', 'F')], default='M', max_length=1, verbose_name='Genre')),
 | 
			
		||||
                ('rue', models.CharField(blank=True, max_length=60, verbose_name='Rue')),
 | 
			
		||||
                ('npa', models.CharField(blank=True, max_length=4, verbose_name='NPA')),
 | 
			
		||||
                ('localite', models.CharField(blank=True, max_length=30, verbose_name='Localité')),
 | 
			
		||||
                ('telephone', models.CharField(blank=True, max_length=60, verbose_name='Tél.')),
 | 
			
		||||
                ('email', models.EmailField(blank=True, max_length=254, verbose_name='Courriel')),
 | 
			
		||||
                ('pays_origine', django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Nationalité')),
 | 
			
		||||
                ('remarque', models.TextField(blank=True)),
 | 
			
		||||
                ('remarque_privee', models.TextField(blank=True, verbose_name='Remarque privée')),
 | 
			
		||||
                ('profession', models.CharField(blank=True, max_length=50, verbose_name='Profession')),
 | 
			
		||||
                ('filiation', models.CharField(blank=True, max_length=80, verbose_name='Filiation')),
 | 
			
		||||
                ('decedee', models.BooleanField(default=False, verbose_name='Cette personne est décédée')),
 | 
			
		||||
                ('allergies', models.TextField(blank=True, verbose_name='Allergies')),
 | 
			
		||||
                ('employeur', models.CharField(blank=True, max_length=50, verbose_name='Adresse empl.')),
 | 
			
		||||
                ('permis', models.CharField(blank=True, max_length=30, verbose_name='Permis/séjour')),
 | 
			
		||||
                ('validite', models.DateField(blank=True, null=True, verbose_name='Date validité')),
 | 
			
		||||
                ('animaux', models.BooleanField(default=None, null=True, verbose_name='Animaux')),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='membres', to='aemo.famille')),
 | 
			
		||||
                ('reseaux', models.ManyToManyField(blank=True, to='aemo.contact')),
 | 
			
		||||
                ('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='aemo.role')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Personne',
 | 
			
		||||
                'ordering': ('nom', 'prenom'),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Formation',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('statut', models.CharField(blank=True, choices=[('pre_scol', 'Pré-scolaire'), ('cycle1', 'Cycle 1'), ('cycle2', 'Cycle 2'), ('cycle3', 'Cycle 3'), ('apprenti', 'Apprentissage'), ('etudiant', 'Etudiant'), ('en_emploi', 'En emploi'), ('sans_emploi', 'Sans emploi'), ('sans_occupation', 'Sans occupation')], max_length=20, verbose_name='Scolarité')),
 | 
			
		||||
                ('college', models.CharField(blank=True, max_length=50, verbose_name='Collège')),
 | 
			
		||||
                ('classe', models.CharField(blank=True, max_length=50, verbose_name='Classe')),
 | 
			
		||||
                ('enseignant', models.CharField(blank=True, max_length=50, verbose_name='Enseignant')),
 | 
			
		||||
                ('creche', models.CharField(blank=True, max_length=50, verbose_name='Crèche')),
 | 
			
		||||
                ('creche_resp', models.CharField(blank=True, max_length=50, verbose_name='Resp.crèche')),
 | 
			
		||||
                ('entreprise', models.CharField(blank=True, max_length=50, verbose_name='Entreprise')),
 | 
			
		||||
                ('maitre_apprentissage', models.CharField(blank=True, max_length=50, verbose_name="Maître d'appr.")),
 | 
			
		||||
                ('remarque', models.TextField(blank=True)),
 | 
			
		||||
                ('cercle_scolaire', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='aemo.cerclescolaire', verbose_name='Cercle scolaire')),
 | 
			
		||||
                ('personne', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='aemo.personne')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Scolarité',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='famille',
 | 
			
		||||
            name='region',
 | 
			
		||||
            field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='aemo.region'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='contact',
 | 
			
		||||
            name='roles',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='contacts', to='aemo.role', verbose_name='Rôles'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='contact',
 | 
			
		||||
            name='service',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='aemo.service'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Suivi',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('equipe', models.CharField(choices=[('montagnes', 'Montagnes et V-d-T'), ('littoral', 'Littoral et V-d-R'), ('neuch_ville', 'Neuchâtel-ville (archives)'), ('litt_est', 'Littoral Est (archives)'), ('litt_ouest', 'Littoral Ouest (archives)')], max_length=15, verbose_name='Équipe')),
 | 
			
		||||
                ('heure_coord', models.BooleanField(default=False, verbose_name='Heure de coordination')),
 | 
			
		||||
                ('difficultes', models.TextField(blank=True, verbose_name='Difficultés')),
 | 
			
		||||
                ('aides', models.TextField(blank=True, verbose_name='Aides souhaitées')),
 | 
			
		||||
                ('competences', models.TextField(blank=True, verbose_name='Ressources/Compétences')),
 | 
			
		||||
                ('dates_demande', models.CharField(blank=True, max_length=128, verbose_name='Dates')),
 | 
			
		||||
                ('autres_contacts', models.TextField(blank=True, verbose_name='Autres services contactés')),
 | 
			
		||||
                ('disponibilites', models.TextField(blank=True, verbose_name='Disponibilités')),
 | 
			
		||||
                ('remarque', models.TextField(blank=True)),
 | 
			
		||||
                ('remarque_privee', models.TextField(blank=True, verbose_name='Remarque privée')),
 | 
			
		||||
                ('service_orienteur', models.CharField(blank=True, choices=[('famille', 'Famille'), ('ope', 'OPE'), ('aemo', 'AEMO'), ('cnpea', 'CNPea'), ('ecole', 'École'), ('res_prim', 'Réseau primaire'), ('res_sec', 'Réseau secondaire'), ('pediatre', 'Pédiatre'), ('autre', 'Autre')], max_length=15, verbose_name='Orienté vers l’AEMO par')),
 | 
			
		||||
                ('service_annonceur', models.CharField(blank=True, max_length=60, verbose_name='Service annonceur')),
 | 
			
		||||
                ('motif_demande', common.fields.ChoiceArrayField(base_field=models.CharField(choices=[('accompagnement', 'Accompagnement psycho-éducatif'), ('integration', 'Aide à l’intégration'), ('demande', 'Elaboration d’une demande (contrainte)'), ('crise', 'Travail sur la crise'), ('post-placement', 'Post-placement'), ('pre-placement', 'Pré-placement'), ('violence', 'Violence / maltraitances')], max_length=60), blank=True, null=True, size=None, verbose_name='Motif de la demande')),
 | 
			
		||||
                ('motif_detail', models.TextField(blank=True, verbose_name='Motif')),
 | 
			
		||||
                ('mandat_ope', common.fields.ChoiceArrayField(base_field=models.CharField(blank=True, choices=[('volontaire', 'Mandat volontaire'), ('curatelle', 'Curatelle 308'), ('referent', 'Référent'), ('enquete', 'Enquête'), ('tutelle', 'Curatelle de portée générale')], max_length=65), blank=True, null=True, size=None, verbose_name='Mandat OPE')),
 | 
			
		||||
                ('referent_note', models.TextField(blank=True, verbose_name='Autres contacts')),
 | 
			
		||||
                ('collaboration', models.TextField(blank=True, verbose_name='Collaboration')),
 | 
			
		||||
                ('ressource', models.TextField(blank=True, verbose_name='Ressource')),
 | 
			
		||||
                ('crise', models.TextField(blank=True, verbose_name='Gestion de crise')),
 | 
			
		||||
                ('date_demande', models.DateField(blank=True, default=None, null=True, verbose_name='Demande déposée le')),
 | 
			
		||||
                ('date_debut_evaluation', models.DateField(blank=True, default=None, null=True, verbose_name='Début de l’évaluation le')),
 | 
			
		||||
                ('date_fin_evaluation', models.DateField(blank=True, default=None, null=True, verbose_name='Fin de l’évaluation le')),
 | 
			
		||||
                ('date_debut_suivi', models.DateField(blank=True, default=None, null=True, verbose_name='Début du suivi le')),
 | 
			
		||||
                ('date_fin_suivi', models.DateField(blank=True, default=None, null=True, verbose_name='Fin du suivi le')),
 | 
			
		||||
                ('demande_prioritaire', models.BooleanField(default=False, verbose_name='Demande prioritaire')),
 | 
			
		||||
                ('demarche', common.fields.ChoiceArrayField(base_field=models.CharField(blank=True, choices=[('volontaire', 'Volontaire'), ('contrainte', 'Contrainte'), ('post_placement', 'Post placement'), ('non_placement', 'Eviter placement')], max_length=60), blank=True, null=True, size=None, verbose_name='Démarche')),
 | 
			
		||||
                ('pers_famille_presentes', models.CharField(blank=True, max_length=200, verbose_name='Membres famille présents')),
 | 
			
		||||
                ('ref_presents', models.CharField(blank=True, max_length=250, verbose_name='Intervenants présents')),
 | 
			
		||||
                ('autres_pers_presentes', models.CharField(blank=True, max_length=100, verbose_name='Autres pers. présentes')),
 | 
			
		||||
                ('motif_fin_suivi', models.CharField(blank=True, choices=[('desengagement', 'Désengagement'), ('evol_positive', 'Autonomie familiale'), ('relai_amb', 'Relai vers ambulatoire'), ('relai', 'Relai vers autre service'), ('placement', 'Placement'), ('non_aboutie', 'Demande non aboutie'), ('non_dispo', 'Pas de disponibilités/place'), ('erreur', 'Erreur de saisie'), ('autres', 'Autres')], max_length=20, verbose_name='Motif de fin de suivi')),
 | 
			
		||||
                ('famille', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='aemo.famille')),
 | 
			
		||||
                ('ope_referent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='aemo.contact', verbose_name='as. OPE')),
 | 
			
		||||
                ('ope_referent_2', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='aemo.contact', verbose_name='as. OPE 2')),
 | 
			
		||||
                ('sse_referent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='aemo.contact', verbose_name='SSE')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Intervenant',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('date_debut', models.DateField(default=django.utils.timezone.now, verbose_name='Date début')),
 | 
			
		||||
                ('date_fin', models.DateField(blank=True, null=True, verbose_name='Date fin')),
 | 
			
		||||
                ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='aemo.role')),
 | 
			
		||||
                ('suivi', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='aemo.suivi')),
 | 
			
		||||
                ('intervenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='suivi',
 | 
			
		||||
            name='intervenants',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='interventions', through='aemo.Intervenant', to=settings.AUTH_USER_MODEL),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Rapport',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('date', models.DateField(verbose_name='Date du résumé')),
 | 
			
		||||
                ('situation', models.TextField(blank=True, verbose_name='Situation / contexte familial')),
 | 
			
		||||
                ('observations', models.TextField(blank=True, verbose_name='Observations, évolution et hypothèses')),
 | 
			
		||||
                ('projet', models.TextField(blank=True, verbose_name='Perspectives d’avenir')),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rapports', to='aemo.famille')),
 | 
			
		||||
                ('auteur', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('pres_interv', models.ManyToManyField(blank=True, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Intervenants cités dans le résumé')),
 | 
			
		||||
                ('sig_interv', models.ManyToManyField(blank=True, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Signature des intervenants')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Prestation',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('date_prestation', models.DateField(verbose_name='date de l’intervention')),
 | 
			
		||||
                ('duree', models.DurationField(verbose_name='durée')),
 | 
			
		||||
                ('familles_actives', models.PositiveSmallIntegerField(blank=True, default=0)),
 | 
			
		||||
                ('texte', models.TextField(blank=True, verbose_name='Contenu')),
 | 
			
		||||
                ('manque', models.BooleanField(default=False, verbose_name='Rendez-vous manqué')),
 | 
			
		||||
                ('fichier', models.FileField(blank=True, upload_to='prestations', verbose_name='Fichier/image')),
 | 
			
		||||
                ('famille', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prestations', to='aemo.famille', verbose_name='Famille')),
 | 
			
		||||
                ('lib_prestation', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prestations_%(app_label)s', to='aemo.libelleprestation')),
 | 
			
		||||
                ('auteur', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='auteur')),
 | 
			
		||||
                ('intervenants', models.ManyToManyField(related_name='prestations', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('-date_prestation',),
 | 
			
		||||
                'permissions': (('edit_prest_prev_month', 'Modifier prestations du mois précédent'),),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='JournalAcces',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('ordinaire', models.BooleanField(default=True)),
 | 
			
		||||
                ('quand', models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='aemo.famille')),
 | 
			
		||||
                ('utilisateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Bilan',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('date', models.DateField(verbose_name='Date du bilan')),
 | 
			
		||||
                ('objectifs', models.TextField(verbose_name='Objectifs')),
 | 
			
		||||
                ('rythme', models.TextField(verbose_name='Rythme et fréquence')),
 | 
			
		||||
                ('sig_famille', models.BooleanField(default=True, verbose_name='Apposer signature de la famille')),
 | 
			
		||||
                ('fichier', models.FileField(blank=True, upload_to='bilans', verbose_name='Fichier/image')),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bilans', to='aemo.famille')),
 | 
			
		||||
                ('auteur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='bilans', to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
                ('sig_interv', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Signature des intervenants')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Document',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('fichier', models.FileField(upload_to='doc', verbose_name='Nouveau fichier')),
 | 
			
		||||
                ('titre', models.CharField(max_length=100)),
 | 
			
		||||
                ('famille', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='aemo.famille')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'unique_together': {('famille', 'titre')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='contact',
 | 
			
		||||
            unique_together={('nom', 'prenom', 'service')},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										21
									
								
								aemo/migrations/0002_unaccent_extension.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
from django.db import migrations
 | 
			
		||||
from django.contrib.postgres.operations import UnaccentExtension
 | 
			
		||||
 | 
			
		||||
# ref for this migration: https://stackoverflow.com/questions/47230566
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('aemo', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        UnaccentExtension(),
 | 
			
		||||
        migrations.RunSQL("CREATE TEXT SEARCH CONFIGURATION french_unaccent(COPY = french);"),
 | 
			
		||||
        migrations.RunSQL(
 | 
			
		||||
            "ALTER TEXT SEARCH CONFIGURATION french_unaccent "
 | 
			
		||||
            "ALTER MAPPING FOR hword, hword_part, word "
 | 
			
		||||
            "WITH unaccent, french_stem;"
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								aemo/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1446
									
								
								aemo/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										636
									
								
								aemo/pdf.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,636 @@
 | 
			
		|||
from io import BytesIO
 | 
			
		||||
 | 
			
		||||
import extract_msg
 | 
			
		||||
import nh3
 | 
			
		||||
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
from pypdf import PdfWriter
 | 
			
		||||
 | 
			
		||||
from functools import partial
 | 
			
		||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
 | 
			
		||||
from reportlab.lib.pagesizes import A4
 | 
			
		||||
from reportlab.lib import colors
 | 
			
		||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
 | 
			
		||||
from reportlab.lib.units import cm
 | 
			
		||||
from reportlab.pdfgen import canvas
 | 
			
		||||
from reportlab.platypus import (
 | 
			
		||||
    PageBreak, Paragraph, Preformatted, SimpleDocTemplate, Spacer, Table, TableStyle
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from django.contrib.staticfiles.finders import find
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
 | 
			
		||||
from .utils import format_d_m_Y, format_duree, format_Ymd
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_booleen(val):
 | 
			
		||||
    return '?' if val is None else ('oui' if val else 'non')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageNumCanvas(canvas.Canvas):
 | 
			
		||||
    """A special canvas to be able to draw the total page number in the footer."""
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self._pages = []
 | 
			
		||||
 | 
			
		||||
    def showPage(self):
 | 
			
		||||
        self._pages.append(dict(self.__dict__))
 | 
			
		||||
        self._startPage()
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        page_count = len(self._pages)
 | 
			
		||||
        for page in self._pages:
 | 
			
		||||
            self.__dict__.update(page)
 | 
			
		||||
            self.draw_page_number(page_count)
 | 
			
		||||
            canvas.Canvas.showPage(self)
 | 
			
		||||
        super().save()
 | 
			
		||||
 | 
			
		||||
    def draw_page_number(self, page_count):
 | 
			
		||||
        self.setFont("Helvetica", 9)
 | 
			
		||||
        self.drawRightString(self._pagesize[0] - 1.6*cm, 2.3*cm, "p. %s/%s" % (self._pageNumber, page_count))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CleanParagraph(Paragraph):
 | 
			
		||||
    """If the (HTML) text cannot be parsed, try to clean it."""
 | 
			
		||||
    def __init__(self, text, *args, **kwargs):
 | 
			
		||||
        if text:
 | 
			
		||||
            text = text.replace('</p>', '</p><br/>')
 | 
			
		||||
            if '<ul>' in text:
 | 
			
		||||
                text = text.replace('<li>', '  • ').replace('</ul>','<br>').replace('</li>', '<br>')
 | 
			
		||||
        try:
 | 
			
		||||
            super().__init__(text, *args, **kwargs)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            text = nh3.clean(
 | 
			
		||||
                text, tags={'p', 'br', 'b', 'strong', 'u', 'i', 'em', 'ul', 'li'}
 | 
			
		||||
            ).replace('<br>', '<br/>')
 | 
			
		||||
        super().__init__(text, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RawParagraph(Paragraph):
 | 
			
		||||
    """Raw text, replace new lines by <br/>."""
 | 
			
		||||
    def __init__(self, text='', *args, **kwargs):
 | 
			
		||||
        if text:
 | 
			
		||||
            text = text.replace('\r\n', '\n').replace('\n', '\n<br/>')
 | 
			
		||||
        super().__init__(text, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StyleMixin:
 | 
			
		||||
    FONTSIZE = 9
 | 
			
		||||
    MAXLINELENGTH = 120
 | 
			
		||||
 | 
			
		||||
    def __init__(self, base_font='Helvetica', **kwargs):
 | 
			
		||||
        self.styles = getSampleStyleSheet()
 | 
			
		||||
        self.style_title = ParagraphStyle(
 | 
			
		||||
            name='title', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 4,
 | 
			
		||||
            leading=self.FONTSIZE + 5, alignment=TA_CENTER
 | 
			
		||||
        )
 | 
			
		||||
        self.style_normal = ParagraphStyle(
 | 
			
		||||
            name='normal', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT,
 | 
			
		||||
            leading=self.FONTSIZE + 1, spaceAfter=0
 | 
			
		||||
        )
 | 
			
		||||
        self.style_justifie = ParagraphStyle(
 | 
			
		||||
            name='justifie', parent=self.style_normal, alignment=TA_JUSTIFY, spaceAfter=0.2 * cm
 | 
			
		||||
        )
 | 
			
		||||
        self.style_sub_title = ParagraphStyle(
 | 
			
		||||
            name='sous_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 2,
 | 
			
		||||
            alignment=TA_LEFT, spaceBefore=0.5 * cm, spaceAfter=0.1 * cm
 | 
			
		||||
        )
 | 
			
		||||
        self.style_inter_title = ParagraphStyle(
 | 
			
		||||
            name='inter_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 1,
 | 
			
		||||
            alignment=TA_LEFT, spaceBefore=0.3 * cm, spaceAfter=0
 | 
			
		||||
        )
 | 
			
		||||
        self.style_bold = ParagraphStyle(
 | 
			
		||||
            name='bold', fontName='Helvetica-Bold', fontSize=self.FONTSIZE, leading=self.FONTSIZE + 1
 | 
			
		||||
        )
 | 
			
		||||
        self.style_italic = ParagraphStyle(
 | 
			
		||||
            name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE
 | 
			
		||||
        )
 | 
			
		||||
        self.style_indent = ParagraphStyle(
 | 
			
		||||
            name='indent', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT,
 | 
			
		||||
            leftIndent=1 * cm
 | 
			
		||||
        )
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HeaderFooterMixin:
 | 
			
		||||
    LOGO = find('img/logo-cr.png')
 | 
			
		||||
    EDUQUA = find('img/eduqua.png')
 | 
			
		||||
    DON = find('img/logo-zewo.png')
 | 
			
		||||
 | 
			
		||||
    def draw_header(self, canvas, doc):
 | 
			
		||||
        canvas.saveState()
 | 
			
		||||
        canvas.drawImage(
 | 
			
		||||
            self.LOGO, doc.leftMargin + 316, doc.height+60, 7 * cm, 1.6 * cm, preserveAspectRatio=True, mask='auto'
 | 
			
		||||
        )
 | 
			
		||||
        canvas.restoreState()
 | 
			
		||||
 | 
			
		||||
    def draw_footer(self, canvas, doc):
 | 
			
		||||
        canvas.saveState()
 | 
			
		||||
        canvas.drawImage(
 | 
			
		||||
            self.EDUQUA, doc.leftMargin, doc.height - 670, 1.8 * cm, 0.8 * cm, preserveAspectRatio=True
 | 
			
		||||
        )
 | 
			
		||||
        canvas.drawImage(
 | 
			
		||||
            self.DON, doc.leftMargin + 60, doc.height - 670, 2.5 * cm, 0.8 * cm, preserveAspectRatio=True, mask='auto'
 | 
			
		||||
        )
 | 
			
		||||
        tab = [220, 365]
 | 
			
		||||
        line = [658, 667, 676, 685]
 | 
			
		||||
 | 
			
		||||
        canvas.setFont("Helvetica", 8)
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[2], "CCP ?")
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[3], "IBAN ?")
 | 
			
		||||
        canvas.setLineWidth(0.5)
 | 
			
		||||
        canvas.line(doc.leftMargin + 230, 2.2 * cm, doc.leftMargin + 230, 1.0 * cm)
 | 
			
		||||
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[0], "Rte d’Englisberg 3")
 | 
			
		||||
        canvas.setFont("Helvetica-Bold", 8)
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[1], "1763 Granges-Paccot")
 | 
			
		||||
        canvas.setFont("Helvetica", 8)
 | 
			
		||||
        canvas.line(doc.leftMargin + 375, 2.2 * cm, doc.leftMargin + 375, 1.0 * cm)
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[0], "+41 26 407 70 44")
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[1], "secretariat@fondation-transit.ch")
 | 
			
		||||
        canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[2], "fondation-transit.ch/aemo")
 | 
			
		||||
        canvas.restoreState()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePDF(HeaderFooterMixin, StyleMixin):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, tampon, instance, **kwargs):
 | 
			
		||||
        self.instance = instance
 | 
			
		||||
        self.kwargs = kwargs
 | 
			
		||||
        self.doc = SimpleDocTemplate(
 | 
			
		||||
            tampon, title=self.title, pagesize=A4,
 | 
			
		||||
            leftMargin=1.5 * cm, rightMargin=1.5 * cm, topMargin=2 * cm, bottomMargin=2.5 * cm
 | 
			
		||||
        )
 | 
			
		||||
        self.story = []
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def draw_header_footer(self, canvas, doc):
 | 
			
		||||
        self.draw_header(canvas, doc)
 | 
			
		||||
        self.draw_footer(canvas, doc)
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        # subclass should call self.doc.build(self.story, onFirstPage=self.draw_header_footer)
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def format_note(note, length=StyleMixin.MAXLINELENGTH):
 | 
			
		||||
        return Preformatted(note.replace('\r\n', '\n'), maxLineLength=length)
 | 
			
		||||
 | 
			
		||||
    def set_title(self, title=''):
 | 
			
		||||
        self.story.append(Spacer(0, 1 * cm))
 | 
			
		||||
        self.story.append(Paragraph(title, self.style_title))
 | 
			
		||||
        self.story.append(Spacer(0, 1.2 * cm))
 | 
			
		||||
 | 
			
		||||
    def parent_data(self, person):
 | 
			
		||||
        """Return parent data ready to be used in a 2-column table."""
 | 
			
		||||
        parents = person.parents()
 | 
			
		||||
        par1 = parents[0] if len(parents) > 0 else None
 | 
			
		||||
        par2 = parents[1] if len(parents) > 1 else None
 | 
			
		||||
        data = [
 | 
			
		||||
            [('Parent 1 (%s)' % par1.role.nom) if par1 else '-',
 | 
			
		||||
                ('Parent 2 (%s)' % par2.role.nom) if par2 else '-'],
 | 
			
		||||
            [par1.contact.nom_prenom if par1 else '', par2.contact.nom_prenom if par2 else ''],
 | 
			
		||||
            [par1.contact.adresse if par1 else '', par2.contact.adresse if par2 else ''],
 | 
			
		||||
            [par1.contact.contact if par1 else '', par2.contact.contact if par2 else ''],
 | 
			
		||||
 | 
			
		||||
            ['Autorité parentale: {}'.format(format_booleen(par1.contact.autorite_parentale)) if par1 else '',
 | 
			
		||||
                'Autorité parentale: {}'.format(format_booleen(par2.contact.autorite_parentale)) if par2 else ''],
 | 
			
		||||
        ]
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def formate_persons(self, parents_list):
 | 
			
		||||
        labels = (
 | 
			
		||||
            ("Nom", "nom"), ("Prénom", "prenom"), ("Adresse", "rue"),
 | 
			
		||||
            ("Localité", 'localite_display'), ("Profession", 'profession'), ("Tél.", 'telephone'),
 | 
			
		||||
            ("Courriel", 'email'), ('Remarque', 'remarque'),
 | 
			
		||||
        )
 | 
			
		||||
        P = partial(Paragraph, style=self.style_normal)
 | 
			
		||||
        Pbold = partial(Paragraph, style=self.style_bold)
 | 
			
		||||
        data = []
 | 
			
		||||
        parents = [parent for parent in parents_list if parent is not None]
 | 
			
		||||
        if len(parents) == 0:
 | 
			
		||||
            pass
 | 
			
		||||
        elif len(parents) == 1:
 | 
			
		||||
            data.append([Pbold('Rôle:'), Pbold(parents[0].role.nom), '', ''])
 | 
			
		||||
            for label in labels:
 | 
			
		||||
                data.append([
 | 
			
		||||
                    label[0], P(getattr(parents[0], label[1])), '', ''
 | 
			
		||||
                ])
 | 
			
		||||
        elif len(parents) == 2:
 | 
			
		||||
            data.append([
 | 
			
		||||
                Pbold('Rôle:'), Pbold(parents[0].role.nom),
 | 
			
		||||
                Pbold('Rôle:'), Pbold(parents[1].role.nom)
 | 
			
		||||
            ])
 | 
			
		||||
            for label in labels:
 | 
			
		||||
                data.append([
 | 
			
		||||
                    label[0], P(getattr(parents[0], label[1]) if parents[0] else ''),
 | 
			
		||||
                    label[0], P(getattr(parents[1], label[1]) if parents[1] else '')
 | 
			
		||||
                ])
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def get_table(self, data, columns, before=0.0, after=0.0, inter=None):
 | 
			
		||||
        """Prepare a Table instance with data and columns, with a common style."""
 | 
			
		||||
        if inter:
 | 
			
		||||
            inter = inter * cm
 | 
			
		||||
        cols = [c * cm for c in columns]
 | 
			
		||||
 | 
			
		||||
        t = Table(
 | 
			
		||||
            data=data, colWidths=cols, hAlign=TA_LEFT,
 | 
			
		||||
            spaceBefore=before * cm, spaceAfter=after * cm
 | 
			
		||||
        )
 | 
			
		||||
        t.hAlign = 0
 | 
			
		||||
        t.setStyle(tblstyle=TableStyle([
 | 
			
		||||
            ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
 | 
			
		||||
            ('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE),
 | 
			
		||||
            ('VALIGN', (0, 0), (-1, -1), "TOP"),
 | 
			
		||||
            ('LEFTPADDING', (0, 0), (0, -1), 1),
 | 
			
		||||
            ('LEADING', (0, 0), (-1, -1), 7),
 | 
			
		||||
        ]))
 | 
			
		||||
        return t
 | 
			
		||||
 | 
			
		||||
    def _add_subtitle(self, data):
 | 
			
		||||
        t = Table(
 | 
			
		||||
            data=[data], colWidths=[18 * cm / len(data)] * len(data),
 | 
			
		||||
            hAlign=TA_LEFT,
 | 
			
		||||
            spaceBefore=0.2 * cm, spaceAfter=0.5 * cm
 | 
			
		||||
        )
 | 
			
		||||
        t.hAlign = 0
 | 
			
		||||
        t.setStyle(tblstyle=TableStyle([
 | 
			
		||||
            ('FONT', (0, 0), (-1, -1), "Helvetica-Bold"),
 | 
			
		||||
            ('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE + 2),
 | 
			
		||||
            ('LINEBELOW', (0, -1), (-1, -1), 0.25, colors.black),
 | 
			
		||||
            ('ALIGN', (-1, -1), (-1, -1), 'RIGHT'),
 | 
			
		||||
        ]))
 | 
			
		||||
        self.story.append(t)
 | 
			
		||||
 | 
			
		||||
    def write_paragraph(self, title, text, html=False, justifie=False):
 | 
			
		||||
        if title:
 | 
			
		||||
            self.story.append(Paragraph(title, self.style_sub_title))
 | 
			
		||||
        style = self.style_justifie if justifie else self.style_normal
 | 
			
		||||
        if html:
 | 
			
		||||
            if text.startswith('<p>') and text.endswith('</p>'):
 | 
			
		||||
                soup = BeautifulSoup(text, features="html5lib")
 | 
			
		||||
                for tag in soup.find_all(['p', 'ul']):
 | 
			
		||||
                    self.story.append(CleanParagraph(str(tag), style))
 | 
			
		||||
            else:
 | 
			
		||||
                self.story.append(CleanParagraph(text, style))
 | 
			
		||||
        else:
 | 
			
		||||
            self.story.append(RawParagraph(text, style))
 | 
			
		||||
 | 
			
		||||
    def enfant_data(self, enfant):
 | 
			
		||||
        labels = [('Tél.', enfant.telephone)]
 | 
			
		||||
        if hasattr(enfant, 'formation'):
 | 
			
		||||
            labels.extend([
 | 
			
		||||
                ('Statut scol', enfant.formation.get_statut_display()),
 | 
			
		||||
                ('Centre', enfant.formation.cercle_scolaire),
 | 
			
		||||
                ('Collège', enfant.formation.college),
 | 
			
		||||
                ('Classe', enfant.formation.classe),
 | 
			
		||||
                ('Struct. extra-fam.', enfant.formation.creche),
 | 
			
		||||
                ('Ens.', enfant.formation.enseignant),
 | 
			
		||||
            ])
 | 
			
		||||
        labels.extend([
 | 
			
		||||
            ('Permis de séjour', enfant.permis),
 | 
			
		||||
            ('Validité', enfant.validite),
 | 
			
		||||
        ])
 | 
			
		||||
        row = [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"]
 | 
			
		||||
        for label in labels:
 | 
			
		||||
            if label[1]:
 | 
			
		||||
                row.append(f"{label[0]}: {label[1]}")
 | 
			
		||||
        return '; '.join(row)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DemandeAccompagnement(BasePDF):
 | 
			
		||||
    title = None
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        famille = self.instance
 | 
			
		||||
        suivi = famille.suivi
 | 
			
		||||
        self.set_title("Famille {} - {}".format(famille.nom, self.title))
 | 
			
		||||
 | 
			
		||||
        self.story.append(Paragraph("<strong>Motif(s) de la demande:</strong> {}".format(
 | 
			
		||||
            suivi.get_motif_demande_display()), self.style_normal
 | 
			
		||||
        ))
 | 
			
		||||
        self.story.append(Paragraph('_' * 90, self.style_normal))
 | 
			
		||||
 | 
			
		||||
        self.write_paragraph("Dates", suivi.dates_demande)
 | 
			
		||||
        self.write_paragraph(
 | 
			
		||||
            "Difficultés",
 | 
			
		||||
            "{}<br/>{}<br/><br/>{}".format(
 | 
			
		||||
                "<em>Quelles sont les difficultés éducatives que vous rencontrez et depuis combien de "
 | 
			
		||||
                "temps ?",
 | 
			
		||||
                "Fonctionnement familial: règles, coucher, lever, repas, jeux, relations "
 | 
			
		||||
                "parent-enfants, rapport au sein de la fratrie, … (exemple)</em>",
 | 
			
		||||
                suivi.difficultes
 | 
			
		||||
            ),
 | 
			
		||||
            html=True
 | 
			
		||||
        )
 | 
			
		||||
        self.write_paragraph(
 | 
			
		||||
            "Autres services",
 | 
			
		||||
            "<em>{}</em><br/>{}".format(
 | 
			
		||||
                "Avez-vous fait appel à d'autres services ? Si oui, avec quels vécus ?",
 | 
			
		||||
                suivi.autres_contacts
 | 
			
		||||
            ),
 | 
			
		||||
            html=True
 | 
			
		||||
        )
 | 
			
		||||
        self.write_paragraph("Aides souhaitées", suivi.aides, html=True)
 | 
			
		||||
        self.write_paragraph("Ressources/Compétences", suivi.competences, html=True)
 | 
			
		||||
        self.write_paragraph("Disponibilités", suivi.disponibilites, html=True)
 | 
			
		||||
        self.write_paragraph("Remarques", suivi.remarque)
 | 
			
		||||
 | 
			
		||||
        self.doc.build(self.story, onFirstPage=self.draw_header_footer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JournalPdf(BasePDF):
 | 
			
		||||
    title = "Journal de bord"
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return '{}_journal.pdf'.format(slugify(self.instance.nom))
 | 
			
		||||
 | 
			
		||||
    def get_title(self):
 | 
			
		||||
        return 'Famille {} - {}'.format(self.instance.nom, self.title)
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        famille = self.instance
 | 
			
		||||
        self.set_title(self.get_title())
 | 
			
		||||
 | 
			
		||||
        self.style_bold.spaceAfter = 0.2*cm
 | 
			
		||||
        self.style_italic.spaceBefore = 0.2 * cm
 | 
			
		||||
        self.style_italic.spaceAfter = 0.7 * cm
 | 
			
		||||
        for prest in famille.prestations.all().prefetch_related('intervenants'):
 | 
			
		||||
            self.story.append(CleanParagraph(prest.texte, self.style_normal))
 | 
			
		||||
            self.story.append(
 | 
			
		||||
                Paragraph('{} - {} ({})'.format(
 | 
			
		||||
                    '/'.join(interv.sigle for interv in prest.intervenants.all()),
 | 
			
		||||
                    format_d_m_Y(prest.date_prestation),
 | 
			
		||||
                    format_duree(prest.duree)
 | 
			
		||||
                ), self.style_italic)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.doc.build(
 | 
			
		||||
            self.story,
 | 
			
		||||
            onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer,
 | 
			
		||||
            canvasmaker=PageNumCanvas
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RapportPdf(BasePDF):
 | 
			
		||||
    title = None
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return "{}_resume_{}.pdf".format(
 | 
			
		||||
            slugify(self.instance.famille.nom),
 | 
			
		||||
            format_Ymd(self.instance.date)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        rapport = self.instance
 | 
			
		||||
        self.style_normal.fontSize += 2
 | 
			
		||||
        self.style_normal.leading += 2
 | 
			
		||||
        self.style_justifie.fontSize += 2
 | 
			
		||||
        self.style_justifie.leading += 2
 | 
			
		||||
        self.doc.title = 'Résumé "AEMO"'
 | 
			
		||||
        self.set_title('Famille {} - {}'.format(rapport.famille.nom, self.doc.title))
 | 
			
		||||
        self._add_subtitle([f"Date: {format_d_m_Y(rapport.date)}"])
 | 
			
		||||
 | 
			
		||||
        data = [
 | 
			
		||||
            ("Enfant(s)", '<br/>'.join(
 | 
			
		||||
                [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"
 | 
			
		||||
                 for enfant in rapport.famille.membres_suivis()]), False),
 | 
			
		||||
            ("Intervenant-e-s", ', '.join([i.nom_prenom for i in rapport.intervenants()]), False),
 | 
			
		||||
            ("Début du suivi", format_d_m_Y(rapport.famille.suivi.date_debut_suivi), False),
 | 
			
		||||
            ("Situation / contexte familial", rapport.situation, True),
 | 
			
		||||
        ]
 | 
			
		||||
        data.append(("Observations", rapport.observations, True))
 | 
			
		||||
        data.append(("Perspectives d'avenir", rapport.projet, True))
 | 
			
		||||
 | 
			
		||||
        for title, text, html in data:
 | 
			
		||||
            self.write_paragraph(title, text, html=html, justifie=True)
 | 
			
		||||
 | 
			
		||||
        if hasattr(rapport, 'sig_interv'):
 | 
			
		||||
            self.story.append(Spacer(0, 0.5 * cm))
 | 
			
		||||
 | 
			
		||||
            for idx, interv in enumerate(rapport.sig_interv.all()):
 | 
			
		||||
                if idx == 0:
 | 
			
		||||
                    self.write_paragraph("Signature des intervenant-e-s :", '')
 | 
			
		||||
                    self.story.append(Spacer(0, 0.2 * cm))
 | 
			
		||||
                self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else ''))
 | 
			
		||||
                self.story.append(Spacer(0, 1 * cm))
 | 
			
		||||
 | 
			
		||||
        secret_style = ParagraphStyle(
 | 
			
		||||
            name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE + 1,
 | 
			
		||||
            backColor=colors.Color(0.96, 0.96, 0.96, 1), borderRadius=12,
 | 
			
		||||
        )
 | 
			
		||||
        self.story.append(Paragraph(
 | 
			
		||||
            "Le présent résumé comporte des éléments <b>couverts par le secret professionnel au sens "
 | 
			
		||||
            "de la LPSy et du Code pénal</b>. Seuls les propriétaires des données, à savoir les membres "
 | 
			
		||||
            "de la famille faisant l’objet du résumé, peuvent <b>ensemble</b> lever ce secret ou "
 | 
			
		||||
            "accepter la divulgation des données. Si cette autorisation n’est pas donnée, l’autorité "
 | 
			
		||||
            "compétente en matière de levée du secret professionnel doit impérativement être saisie.",
 | 
			
		||||
            secret_style
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
        self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MessagePdf(BasePDF):
 | 
			
		||||
    title = 'Message'
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return '{}_message.pdf'.format(slugify(self.instance.subject))
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        doc = self.instance
 | 
			
		||||
        self.set_title('{} - Famille {}'.format(self.title, doc.famille.nom))
 | 
			
		||||
        with extract_msg.Message(doc.fichier.path) as msg:
 | 
			
		||||
            P = partial(Paragraph, style=self.style_normal)
 | 
			
		||||
            Pbold = partial(Paragraph, style=self.style_bold)
 | 
			
		||||
 | 
			
		||||
            msg_headers = [
 | 
			
		||||
                [Pbold('De:'), P(msg.sender)],
 | 
			
		||||
                [Pbold('À:'), P(msg.to)],
 | 
			
		||||
            ]
 | 
			
		||||
            if msg.cc:
 | 
			
		||||
                msg_headers.append([Pbold('CC:'), P(msg.cc)])
 | 
			
		||||
            if msg.date:
 | 
			
		||||
                msg_headers.append([Pbold('Date:'), P(msg.date)])
 | 
			
		||||
            msg_headers.append([Pbold('Sujet:'), P(msg.subject)])
 | 
			
		||||
            self.story.append(self.get_table(msg_headers, [3, 15]))
 | 
			
		||||
            self.story.append(Pbold('Message:'))
 | 
			
		||||
            self.story.append(RawParagraph(msg.body, style=self.style_normal))
 | 
			
		||||
        return self.doc.build(
 | 
			
		||||
            self.story,
 | 
			
		||||
            onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer,
 | 
			
		||||
            canvasmaker=PageNumCanvas
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EvaluationPdf:
 | 
			
		||||
    def __init__(self, tampon, famille):
 | 
			
		||||
        self.tampon = tampon
 | 
			
		||||
        self.famille = famille
 | 
			
		||||
        self.merger = PdfWriter()
 | 
			
		||||
 | 
			
		||||
    def append_pdf(self, PDFClass):
 | 
			
		||||
        tampon = BytesIO()
 | 
			
		||||
        pdf = PDFClass(tampon, self.famille)
 | 
			
		||||
        pdf.produce()
 | 
			
		||||
        self.merger.append(tampon)
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
 | 
			
		||||
        self.append_pdf(CoordonneesPagePdf)
 | 
			
		||||
        self.append_pdf(DemandeAccompagnementPagePdf)
 | 
			
		||||
        self.merger.write(self.tampon)
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return '{}_aemo_evaluation.pdf'.format(slugify(self.famille.nom))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoordonneesFamillePdf(BasePDF):
 | 
			
		||||
    title = "Informations"
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return '{}_coordonnees.pdf'.format(slugify(self.instance.nom))
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        famille = self.instance
 | 
			
		||||
        suivi = famille.suivi
 | 
			
		||||
        self.set_title('Famille {} - {}'.format(famille.nom, self.title))
 | 
			
		||||
 | 
			
		||||
        # Parents
 | 
			
		||||
        self.story.append(Paragraph('Parents', self.style_sub_title))
 | 
			
		||||
        data = self.formate_persons(famille.parents())
 | 
			
		||||
        if data:
 | 
			
		||||
            self.story.append(self.get_table(data, columns=[2, 7, 2, 7], before=0, after=0))
 | 
			
		||||
        else:
 | 
			
		||||
            self.story.append(Paragraph('-', self.style_normal))
 | 
			
		||||
 | 
			
		||||
        # Situation matrimoniale
 | 
			
		||||
        data = [
 | 
			
		||||
            ['Situation matrimoniale: {}'.format(famille.get_statut_marital_display()),
 | 
			
		||||
             'Autorité parentale: {}'.format(famille.get_autorite_parentale_display())],
 | 
			
		||||
        ]
 | 
			
		||||
        self.story.append(self.get_table(data, columns=[9, 9], before=0, after=0))
 | 
			
		||||
 | 
			
		||||
        # Personnes significatives
 | 
			
		||||
        autres_parents = list(famille.autres_parents())
 | 
			
		||||
        if autres_parents:
 | 
			
		||||
            self.story.append(Paragraph('Personne-s significative-s', self.style_sub_title))
 | 
			
		||||
            data = self.formate_persons(autres_parents)
 | 
			
		||||
            self.story.append(self.get_table(data, columns=[2, 7, 3, 6], before=0, after=0))
 | 
			
		||||
            if len(autres_parents) > 2:
 | 
			
		||||
                self.story.append(PageBreak())
 | 
			
		||||
 | 
			
		||||
        # Enfants suivis
 | 
			
		||||
        self.write_paragraph(
 | 
			
		||||
            "Enfant(s)",
 | 
			
		||||
            '<br/>'.join(self.enfant_data(enfant) for enfant in famille.membres_suivis())
 | 
			
		||||
        )
 | 
			
		||||
        # Réseau
 | 
			
		||||
        self.story.append(Paragraph("Réseau", self.style_sub_title))
 | 
			
		||||
        data = [
 | 
			
		||||
            ['AS OPE', '{} - (Mandat: {})'.format(
 | 
			
		||||
                ', '.join(ope.nom_prenom for ope in suivi.ope_referents),
 | 
			
		||||
                suivi.get_mandat_ope_display()
 | 
			
		||||
            )],
 | 
			
		||||
            ['Interv. CRNE', '{}'.format(
 | 
			
		||||
                ', '.join('{}'.format(i.nom_prenom) for i in suivi.intervenants.all().distinct())
 | 
			
		||||
            )]
 | 
			
		||||
        ]
 | 
			
		||||
        for enfant in famille.membres_suivis():
 | 
			
		||||
            for contact in enfant.reseaux.all():
 | 
			
		||||
                data.append([
 | 
			
		||||
                    enfant.prenom,
 | 
			
		||||
                    '{} ({})'.format(contact, contact.contact)
 | 
			
		||||
                ])
 | 
			
		||||
        self.story.append(self.get_table(data, columns=[2, 16], before=0, after=0))
 | 
			
		||||
 | 
			
		||||
        self.write_paragraph("Motif de la demande", famille.suivi.motif_detail)
 | 
			
		||||
        self.write_paragraph("Collaborations", famille.suivi.collaboration)
 | 
			
		||||
 | 
			
		||||
        # Historique
 | 
			
		||||
        self.story.append(Paragraph('Historique', self.style_sub_title))
 | 
			
		||||
        P = partial(Paragraph, style=self.style_normal)
 | 
			
		||||
        Pbold = partial(Paragraph, style=self.style_bold)
 | 
			
		||||
        fields = ['date_demande', 'date_debut_evaluation', 'date_fin_evaluation',
 | 
			
		||||
                  'date_debut_suivi', 'date_fin_suivi']
 | 
			
		||||
        data = []
 | 
			
		||||
        for field_name in fields:
 | 
			
		||||
            field = famille.suivi._meta.get_field(field_name)
 | 
			
		||||
            if getattr(famille.suivi, field_name):
 | 
			
		||||
                data.append(
 | 
			
		||||
                    [Pbold(f"{field.verbose_name} :"), P(format_d_m_Y(getattr(famille.suivi, field_name)))]
 | 
			
		||||
                )
 | 
			
		||||
        if famille.suivi.motif_fin_suivi:
 | 
			
		||||
            data.append([Pbold("Motif de fin de suivi :"), famille.suivi.get_motif_fin_suivi_display()])
 | 
			
		||||
        if famille.destination:
 | 
			
		||||
            data.append([Pbold("Destination :"), famille.get_destination_display()])
 | 
			
		||||
        if famille.archived_at:
 | 
			
		||||
            data.append([Pbold("Date d'archivage :"), format_d_m_Y(famille.archived_at)])
 | 
			
		||||
 | 
			
		||||
        self.story.append(self.get_table(data, [4, 5]))
 | 
			
		||||
        self.doc.build(self.story, onFirstPage=self.draw_header_footer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DemandeAccompagnementPagePdf(DemandeAccompagnement):
 | 
			
		||||
    title = "Évaluation AEMO"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BilanPdf(BasePDF):
 | 
			
		||||
    title = "Bilan AEMO"
 | 
			
		||||
 | 
			
		||||
    def get_filename(self):
 | 
			
		||||
        return "{}_bilan_{}.pdf".format(
 | 
			
		||||
            slugify(self.instance.famille.nom),
 | 
			
		||||
            format_Ymd(self.instance.date)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def produce(self):
 | 
			
		||||
        bilan = self.instance
 | 
			
		||||
        self.style_normal.fontSize += 2
 | 
			
		||||
        self.style_normal.leading += 2
 | 
			
		||||
        self.style_justifie.fontSize += 2
 | 
			
		||||
        self.style_justifie.leading += 2
 | 
			
		||||
        self.set_title('Famille {} - {}'.format(bilan.famille.nom, self.title))
 | 
			
		||||
        self._add_subtitle([f"Date: {format_d_m_Y(bilan.date)}"])
 | 
			
		||||
 | 
			
		||||
        for title, text, html in (
 | 
			
		||||
            ("Enfant(s)", '<br/>'.join(
 | 
			
		||||
                [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"
 | 
			
		||||
                 for enfant in bilan.famille.membres_suivis()]), False),
 | 
			
		||||
            ("Intervenant-e-s", ', '.join(
 | 
			
		||||
                [i.intervenant.nom_prenom for i in bilan.famille.interventions_actives(bilan.date)]
 | 
			
		||||
            ), False),
 | 
			
		||||
            ("Début du suivi", format_d_m_Y(bilan.famille.suivi.date_debut_suivi), False),
 | 
			
		||||
            ("Besoins et objectifs", bilan.objectifs, True),
 | 
			
		||||
            ("Rythme et fréquence", bilan.rythme, True),
 | 
			
		||||
        ):
 | 
			
		||||
            self.write_paragraph(title, text, html=html, justifie=True)
 | 
			
		||||
 | 
			
		||||
        self.story.append(Spacer(0, 0.5 * cm))
 | 
			
		||||
 | 
			
		||||
        for idx, interv in enumerate(bilan.sig_interv.all()):
 | 
			
		||||
            if idx == 0:
 | 
			
		||||
                self.write_paragraph("Signature des intervenant-e-s AEMO :", '')
 | 
			
		||||
                self.story.append(Spacer(0, 0.2 * cm))
 | 
			
		||||
            self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else ''))
 | 
			
		||||
            self.story.append(Spacer(0, 1 * cm))
 | 
			
		||||
 | 
			
		||||
        if bilan.sig_famille:
 | 
			
		||||
            self.write_paragraph("Signature de la famille :", '')
 | 
			
		||||
 | 
			
		||||
        self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoordonneesPagePdf(CoordonneesFamillePdf):
 | 
			
		||||
    title = "Informations"
 | 
			
		||||
 | 
			
		||||
    def get_filename(self, famille):
 | 
			
		||||
        return '{}_aemo_evaluation.pdf'.format(slugify(famille.nom))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def str_or_empty(value):
 | 
			
		||||
    return '' if not value else str(value)
 | 
			
		||||
							
								
								
									
										1
									
								
								aemo/static/css/autocomplete.min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
.autocomplete{background:#fff;z-index:1000;font:14px/22px "-apple-system",BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;overflow:auto;box-sizing:border-box;border:1px solid rgba(50,50,50,.6)}.autocomplete *{font:inherit}.autocomplete>div{padding:0 4px}.autocomplete .group{background:#eee}.autocomplete>div.selected,.autocomplete>div:hover:not(.group){background:#FFF3F3;cursor:pointer}
 | 
			
		||||
							
								
								
									
										359
									
								
								aemo/static/css/main.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,359 @@
 | 
			
		|||
header { background-color: #fff3f3; }
 | 
			
		||||
 | 
			
		||||
a { text-decoration: none; }
 | 
			
		||||
a:hover:not(.btn) {text-decoration: underline; }
 | 
			
		||||
 | 
			
		||||
#logo-cr { display: block; height: 70px; margin: 4px; }
 | 
			
		||||
#bandeau-cr { display: block; width: 100%; height: 15px; }
 | 
			
		||||
#menu_crne { font-size: 80%; }
 | 
			
		||||
 | 
			
		||||
/* This CSS allows the footer to be always at bottom */
 | 
			
		||||
div.top-container { display: flex; min-height: 100vh; flex-direction: column; }
 | 
			
		||||
.main-content { flex: 1; }
 | 
			
		||||
 | 
			
		||||
.hidden { display: none; }
 | 
			
		||||
.red { color: red; }
 | 
			
		||||
.green { color: green; }
 | 
			
		||||
.orange { color: orange; }
 | 
			
		||||
.footer {
 | 
			
		||||
    font-size: 80%;
 | 
			
		||||
    background-color: #f8f9fa;
 | 
			
		||||
    color: #9a1629;
 | 
			
		||||
    margin-top: 2rem;
 | 
			
		||||
}
 | 
			
		||||
.spaceabovetr > td { padding-top: 0.7em; }
 | 
			
		||||
 | 
			
		||||
#bg_home {
 | 
			
		||||
    background: url(../img/SRK_DL_Portal.jpg) no-repeat center fixed;
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
}
 | 
			
		||||
#home-app-line { position:absolute; bottom:112px; left:5%; }
 | 
			
		||||
 | 
			
		||||
.small { font-size: 0.9rem; }
 | 
			
		||||
.nowrap { white-space: nowrap; }
 | 
			
		||||
.icon-ui { display: inline-block; width: 1rem; height: 1rem; }
 | 
			
		||||
img.ficon { height: 1.5em; width: 1.5em; vertical-align: bottom; }
 | 
			
		||||
input[readonly] { background-color: #eee; }
 | 
			
		||||
#search-button, #reset-button { height: 2rem; min-width: 2.7rem; }
 | 
			
		||||
#reset-button img { height: 18px; }
 | 
			
		||||
 | 
			
		||||
td.zero { color: #ccc; }
 | 
			
		||||
#user-bar { position:absolute; top:40px; right:5vw; font-size:80%; }
 | 
			
		||||
#user-bar a, #user-bar button { color: #9c1717; font-weight: bold; }
 | 
			
		||||
#user-bar button { font-size: inherit; text-decoration: none; }
 | 
			
		||||
#user-bar button:hover { text-decoration: underline; }
 | 
			
		||||
#user-tools * { vertical-align: middle; }
 | 
			
		||||
 | 
			
		||||
/* For usage inside bs modal */
 | 
			
		||||
.select2-dropdown { z-index: 1100 !important; }
 | 
			
		||||
 | 
			
		||||
ul.errorlist, ul.nobullets { list-style-type: none; padding-left: 0; }
 | 
			
		||||
ul.errorlist li {
 | 
			
		||||
    background-color: #f8d7da;
 | 
			
		||||
    color: #721c24;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    padding: .75rem 1.25rem;
 | 
			
		||||
    margin-bottom: 1rem;
 | 
			
		||||
    border: 1px solid #ebcccc;
 | 
			
		||||
    border-radius: .25rem;
 | 
			
		||||
}
 | 
			
		||||
form.inline { display: inline-block; }
 | 
			
		||||
form label { font-weight: bold; }
 | 
			
		||||
.custom-file-label::after { content: 'Choisir'; }
 | 
			
		||||
form[name=LoginForm] input[type=text], form[name=LoginForm] input[type=password] { width: 20em; }
 | 
			
		||||
input[type=text], input[type=email] { width: 100%; }
 | 
			
		||||
#id_auth-username { width: auto; }
 | 
			
		||||
#id_mois, #id_annee { width: auto; display: inline-block; }
 | 
			
		||||
input:disabled + span.datetimeshortcuts { display: none; }
 | 
			
		||||
input:disabled + label { color: #999; font-style: italic }
 | 
			
		||||
input.card-text { width: 4em; }
 | 
			
		||||
 | 
			
		||||
textarea { min-height: 3em; }
 | 
			
		||||
textarea#id_note { width: 35em; height: 2em; }
 | 
			
		||||
textarea#id_scolarite, textarea#id_loisirs, textarea#id_activite{ width: 100%; height: 4em; }
 | 
			
		||||
textarea#id_referent_note, textarea#id_allergies, textarea#id_remarque, textarea#id_remarque_privee,
 | 
			
		||||
textarea#id_remarques {
 | 
			
		||||
  width: 100%; height: 2em;
 | 
			
		||||
}
 | 
			
		||||
textarea#id_projet, textarea#id_texte { width: 100%; height: 15em; }
 | 
			
		||||
select#id_motif_fin_suivi { width: 100%; }
 | 
			
		||||
#id_niveau_interv { width: 5em; }
 | 
			
		||||
 | 
			
		||||
#id_equipe.immediate-submit { width: auto; display: inline-block; }
 | 
			
		||||
 | 
			
		||||
form[name=DemandeForm] th { width: 25%; }
 | 
			
		||||
form[name=DemandeForm] input[type=text] { width: 100% }
 | 
			
		||||
form[name=DemandeForm] textarea#id_remarque { width: 100%; height: 2em; }
 | 
			
		||||
form[name=DemandeForm] textarea#id_autres_contacts { width: 100%; height:2em; }
 | 
			
		||||
 | 
			
		||||
form[name=PrestationForm] textarea#id_texte { width: 100%; }
 | 
			
		||||
form[name=PrestationForm] th { width: 12em; }
 | 
			
		||||
 | 
			
		||||
form[name=Suivi] input#id_service_annonceur { width: 80%; }
 | 
			
		||||
form[name=Suivi] textarea { width: 100%; height: 2em;}
 | 
			
		||||
 | 
			
		||||
form[name=JournalForm] textarea#id_texte { width: 100%; }
 | 
			
		||||
form[name=FamilleForm] textarea#id_motif_detail { width: 100%; height: 5em; }
 | 
			
		||||
input.vDateField { width: 7em; display: inline-block; }
 | 
			
		||||
input.TimeField { width: 4em; margin-left: 0.5em; }
 | 
			
		||||
input#id_duree { width: 6em; }
 | 
			
		||||
input#id_username { width: none; }
 | 
			
		||||
input#id_npa, input#id_npa_actuelle { width: 4em; }
 | 
			
		||||
input#id_sigle { width: 8em; }
 | 
			
		||||
#id_groups label, .choicearray label, .filter-form label, #id_lib_prestation label,
 | 
			
		||||
#id_membres label, #id_sig_interv label, #id_pres_interv label, #id_roles label { font-weight: normal; }
 | 
			
		||||
#id_roles { column-count: 2; }
 | 
			
		||||
 | 
			
		||||
tr.decedee td:first-child:before {
 | 
			
		||||
  content: '† ';
 | 
			
		||||
}
 | 
			
		||||
tr.decedee td {
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
  opacity: 0.8;
 | 
			
		||||
}
 | 
			
		||||
span.date_theorique { color:#FF8000; font-style: italic; }
 | 
			
		||||
span.hoverimage { visibility: hidden; }
 | 
			
		||||
span.hoverimage img { cursor: pointer; }
 | 
			
		||||
div:hover > span.hoverimage, summary:hover > span.hoverimage { visibility: visible; }
 | 
			
		||||
 | 
			
		||||
#benef_table > tbody > tr > td { padding: 2px 0.3em; line-height: 1.2em; }
 | 
			
		||||
 | 
			
		||||
table.statut_suivi td {
 | 
			
		||||
    min-width: 35px;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    font-size: 10px;
 | 
			
		||||
    background-color: #eee;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
table.statut_suivi { margin-top: 4px;}
 | 
			
		||||
table.statut_suivi td.filled { background-color: #AACDAA; }
 | 
			
		||||
table.statut_suivi td.current { background-color: #FFDB78; }
 | 
			
		||||
 | 
			
		||||
table.statut_suivi td div.filled { background-color: #AACDAA; }
 | 
			
		||||
table.statut_suivi td div.next { background-color: #F2F5A9; }
 | 
			
		||||
table.statut_suivi td div.urgent { background-color: #FF8000; }
 | 
			
		||||
table.statut_suivi td div.depasse { background-color: #FF0000; }
 | 
			
		||||
 | 
			
		||||
.stat_table th { padding-top: 1.5em; text-align: right; }
 | 
			
		||||
.stat_table th.month { text-align: right; width: 90px; }
 | 
			
		||||
.stat_table th.total { text-align: right; width: 90px; }
 | 
			
		||||
.stat_table th.left { text-align: left; }
 | 
			
		||||
.stat_table .app_line { background-color: #eee; }
 | 
			
		||||
.stat_table td.num { text-align: right; }
 | 
			
		||||
.stat_table tr.first td { border-top: 1px solid #999; }
 | 
			
		||||
.stat_table .subdiv2 td { background-color: beige; font-style: italic; }
 | 
			
		||||
 | 
			
		||||
p.app_line { background-color: #eee; padding: 0.2em 0 0.6em 0.2em; }
 | 
			
		||||
 | 
			
		||||
#id_mandat_ope, #id_motif_demande, #id_demarche {
 | 
			
		||||
  display: flex; flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
#id_mandat_ope > div, #id_motif_demande > div, #id_demarche > div {
 | 
			
		||||
  padding-right: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
#id_mandat_ope label, #id_motif_demande label, #id_demarche label {
 | 
			
		||||
  font-weight: normal; padding-right: 2em;
 | 
			
		||||
}
 | 
			
		||||
/* padding needed for bootstrap style */
 | 
			
		||||
#id_motif_demande { padding-left: 2rem; }
 | 
			
		||||
 | 
			
		||||
#id_demande_prioritaire label { padding-left: 2rem; }
 | 
			
		||||
 | 
			
		||||
table-condensed td{ padding:1px; }
 | 
			
		||||
 | 
			
		||||
.btn-mini {
 | 
			
		||||
    line-height:14px;
 | 
			
		||||
    font-weight:800;
 | 
			
		||||
}
 | 
			
		||||
.btn-xs {
 | 
			
		||||
  padding: .2rem .2rem;
 | 
			
		||||
  font-size: .750rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
  border-radius: .2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-lg input[type=text] { margin-bottom: 1em; }
 | 
			
		||||
.modal-lg input[name=theme] { width: 160%; }
 | 
			
		||||
.modal-lg textarea { width: 160%; }
 | 
			
		||||
th {
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a[role=button] { margin-left: 0.4em; }
 | 
			
		||||
 | 
			
		||||
.topnav-right {font-size: 95%;}
 | 
			
		||||
.selection_form { display: flex; justify-content: flex-end; flex-wrap: wrap; }
 | 
			
		||||
.select-container { margin-left: 0.5em; }
 | 
			
		||||
select#id_interv { max-width: 12em; }
 | 
			
		||||
input#id_letter { width:10em; }
 | 
			
		||||
 | 
			
		||||
ul.nav-prestations .nav-link.active {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  font-size: 110%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.left-label { width: 15em; }
 | 
			
		||||
 | 
			
		||||
.icon { width:20px; height:20px; }
 | 
			
		||||
.icon-xs {width:15px; height:15px; }
 | 
			
		||||
 | 
			
		||||
.table-absence {
 | 
			
		||||
    display: block;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    max-height:300px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-fixed tbody {
 | 
			
		||||
    height: 300px;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.table-fixed thead,
 | 
			
		||||
.table-fixed tbody,
 | 
			
		||||
.table-fixed tr,
 | 
			
		||||
.table-fixed td,
 | 
			
		||||
.table-fixed th {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-fixed tbody td,
 | 
			
		||||
.table-fixed tbody th,
 | 
			
		||||
.table-fixed thead > tr > th {
 | 
			
		||||
    float: left;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
        content: '';
 | 
			
		||||
        clear: both;
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-success-2 { background-color: #d3ffd0;}
 | 
			
		||||
.bg-warning-2 { background-color: #FAF1B8; }
 | 
			
		||||
.bg-primary-2 { background-color: #B8C3FA; }
 | 
			
		||||
.bg-danger-2 { background-color: #F7E4E3 }
 | 
			
		||||
 | 
			
		||||
.bg-danger-3 { background-color: #fff5f5; }
 | 
			
		||||
.bg-success-3 { background-color: #edfced; }
 | 
			
		||||
 | 
			
		||||
.calendarbox { z-index:1100; background: white; }
 | 
			
		||||
/* Needed because of bootstrap reboot */
 | 
			
		||||
.calendar caption { caption-side: top; background: #fff3f3; }
 | 
			
		||||
 | 
			
		||||
.popup textarea#id_texte { width:100%; height:2em; }
 | 
			
		||||
 | 
			
		||||
.table-ext-bordered {
 | 
			
		||||
  border-width:1px;
 | 
			
		||||
  border-style:solid;
 | 
			
		||||
  border-color:lightgray;
 | 
			
		||||
  padding:2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-wrapper { display: block; position: relative; overflow: auto; }
 | 
			
		||||
 | 
			
		||||
.prestation_titre { background-color: #fcf0c6; font-weight: bold; }
 | 
			
		||||
table.prestations th { position: sticky; top: 0; }
 | 
			
		||||
table.prestations td.total { font-weight: 600; background-color: #eee; text-align: center; }
 | 
			
		||||
table.prestations th.mesprest, table.prestations td.mesprest { text-align: right; }
 | 
			
		||||
table.prestations th.action, table.prestations td.action { text-align: right; }
 | 
			
		||||
 | 
			
		||||
p.secret {
 | 
			
		||||
  background-color: #eee;
 | 
			
		||||
  border-radius: 0.5em;
 | 
			
		||||
  padding: 0.5rem;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Image modal styles */
 | 
			
		||||
div.modal-image {
 | 
			
		||||
  display: none; position: fixed; z-index: 100; padding-top: 100px;
 | 
			
		||||
  left: 0; top: 0; width: 100%; height: 100%; overflow: auto;
 | 
			
		||||
  background-color: rgba(0,0,0,0.6);
 | 
			
		||||
}
 | 
			
		||||
img.modal-content {
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: 80%;
 | 
			
		||||
  max-width: 700px;
 | 
			
		||||
}
 | 
			
		||||
#modalClose {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 15px;
 | 
			
		||||
  right: 35px;
 | 
			
		||||
  color: #f1f1f1;
 | 
			
		||||
  font-size: 40px;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  transition: 0.3s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#modalClose:hover, #modalClose:focus {
 | 
			
		||||
  color: #bbb;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
/* End of Image modal styles */
 | 
			
		||||
 | 
			
		||||
#id_rythme { width: 100%; height:4em; }
 | 
			
		||||
 | 
			
		||||
.red {color: red;}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th[data-col] {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th[data-col]:after,
 | 
			
		||||
.table-sortable > thead > tr > th[data-col]:after,
 | 
			
		||||
.table-sortable > thead > tr > th[data-col]:after {
 | 
			
		||||
    content: ' ';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    right: 10px;
 | 
			
		||||
    top: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th[data-col]:after {
 | 
			
		||||
    border-left: 5px solid transparent;
 | 
			
		||||
    border-right: 5px solid transparent;
 | 
			
		||||
    border-top: 5px solid #ccc;
 | 
			
		||||
    border-bottom: 0px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th[data-col]:hover:after {
 | 
			
		||||
    border-top: 5px solid #888;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th.desc:after {
 | 
			
		||||
    border-left: 5px solid transparent;
 | 
			
		||||
    border-right: 5px solid transparent;
 | 
			
		||||
    border-top: 0px solid transparent;
 | 
			
		||||
    border-bottom: 5px solid #333;
 | 
			
		||||
}
 | 
			
		||||
.table-sortable > thead > tr > th.desc:hover:after {
 | 
			
		||||
    border-bottom: 5px solid #888;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sortable > thead > tr > th.asc:after {
 | 
			
		||||
    border-left: 5px solid transparent;
 | 
			
		||||
    border-right: 5px solid transparent;
 | 
			
		||||
    border-top: 5px solid #333;
 | 
			
		||||
    border-bottom: 5px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vDateField-rounded {
 | 
			
		||||
    padding: 0.375rem 0.75rem;
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
    border-radius: 0.375rem;
 | 
			
		||||
    border: 1px solid #ced4da;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.search-form-fields {
 | 
			
		||||
    width: 200px !important;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								aemo/static/css/tablesort.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
th[role=columnheader]:not(.no-sort) {
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th[role=columnheader]:not(.no-sort):after {
 | 
			
		||||
	content: '';
 | 
			
		||||
	float: right;
 | 
			
		||||
	margin-top: 7px;
 | 
			
		||||
	border-width: 0 4px 4px;
 | 
			
		||||
	border-style: solid;
 | 
			
		||||
	border-color: #404040 transparent;
 | 
			
		||||
	visibility: hidden;
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	-ms-user-select: none;
 | 
			
		||||
	-webkit-user-select: none;
 | 
			
		||||
	-moz-user-select: none;
 | 
			
		||||
	user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th[aria-sort=ascending]:not(.no-sort):after {
 | 
			
		||||
	border-bottom: none;
 | 
			
		||||
	border-width: 4px 4px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th[aria-sort]:not(.no-sort):after {
 | 
			
		||||
	visibility: visible;
 | 
			
		||||
	opacity: 0.4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th[role=columnheader]:not(.no-sort):hover:after {
 | 
			
		||||
	visibility: visible;
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								aemo/static/docs/sifp_agenda_hebdo.doc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								aemo/static/favicon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 592 B  | 
							
								
								
									
										2
									
								
								aemo/static/ficons/README
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
Many thanks to Daniel M. Hendricks, http://daniel.hn
 | 
			
		||||
https://github.com/dmhendricks/file-icon-vectors/
 | 
			
		||||
							
								
								
									
										1
									
								
								aemo/static/ficons/docx.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#2372ba}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><path class="st1" d="M27.6 27l7.9 29.7L45.2 27h9.5l9.8 29.7L72.4 27H85L71.2 73H59l-9-26.7L41 73H28.8L15 27h12.6z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 319 B  | 
							
								
								
									
										1
									
								
								aemo/static/ficons/image.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#5b2d8d}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><circle class="st1" cx="32.4" cy="35" r="8"/><path class="st1" d="M78.9 47.3l-9.7-9.6L50 57l-9.6-9.7-19.3 19.3V73h57.8z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 326 B  | 
							
								
								
									
										1
									
								
								aemo/static/ficons/master.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="#fff" d="M0 0h100v100H0z"/><path d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7" fill="#bababa"/><path d="M71 36.3L57.8 23.1c-.4-.4-.9-.6-1.4-.6h-26c-1.1 0-2 .9-2 2v51.1c0 1.1.9 2 2 2h39.3c1.1 0 2-.9 2-2V37.7c-.1-.5-.3-1-.7-1.4zm-3.9 2.3H55.5V27l11.6 11.6zm.1 34.5H32.8V26.9h18.5v13.3c0 1.4 1.2 2.6 2.6 2.6h13.3v30.3z" fill="#bababa" stroke="#bababa" stroke-miterlimit="10"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 452 B  | 
							
								
								
									
										1
									
								
								aemo/static/ficons/pdf.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="#fff" d="M0 0h100v100H0z"/><path d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7" fill="#c11e07"/><path d="M46.2 21.8c-3.5 0-6.3 2.9-6.3 6.3 0 4.3 2.4 9.6 4.9 14.7-2 6.1-4.1 12.7-7 18.2-5.8 2.3-11 4-14 6.6l-.2.2c-1.1 1.2-1.8 2.7-1.8 4.4 0 3.5 2.9 6.3 6.3 6.3 1.7 0 3.4-.6 4.4-1.8 0 0 .2 0 .2-.2 2.3-2.7 5-7.8 7.5-12.2 5.5-2.1 11.5-4.4 16.9-5.8 4.1 3.4 10.1 5.5 15 5.5 3.5 0 6.3-2.9 6.3-6.3 0-3.5-2.9-6.3-6.3-6.3-4 0-9.6 1.4-13.9 2.9-3.5-3.4-6.7-7.5-9.2-11.9C50.6 37 52.6 32 52.6 28c-.2-3.5-2.9-6.2-6.4-6.2zm0 3.6c1.4 0 2.4 1.1 2.4 2.4 0 1.8-1.1 5.3-2.1 9-1.5-3.7-2.9-7.2-2.9-9 .1-1.2 1.2-2.4 2.6-2.4zm1.1 21.5c1.8 3.1 4.1 5.8 6.6 8.2-3.7 1.1-7.3 2.3-11 3.7 1.8-3.8 3.1-7.9 4.4-11.9zM72 55c1.4 0 2.4 1.1 2.4 2.4 0 1.4-1.1 2.4-2.4 2.4-2.9 0-6.9-1.2-10.1-3.1C65.6 56 69.7 55 72 55zM34.6 66.2c-1.8 3.2-3.5 6.1-4.7 7.6-.5.5-.9.6-1.7.6-1.4 0-2.4-1.1-2.4-2.4 0-.6.3-1.4.6-1.7 1.3-1.2 4.5-2.6 8.2-4.1z" fill="#c11e07" stroke="#c11e07" stroke-width="1.25" stroke-miterlimit="10"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1 KiB  | 
							
								
								
									
										1
									
								
								aemo/static/ficons/xlsx.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#30723f}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><path class="st1" d="M41.8 48.5L23.9 27h18.5l7.8 11.9L59.8 27h18.5l-19 21.5L80.7 73H62.4L50.9 58 37.4 73H19.3l22.5-24.5z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 327 B  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/SRK_DL_Portal.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 135 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/bandeau_rouge.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 466 B  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/bandeau_vert.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 255 B  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/coordonnees.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.5 KiB  | 
							
								
								
									
										8
									
								
								aemo/static/img/edit.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
			
		||||
<svg version="1.1" id="Layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
 | 
			
		||||
<polygon points="128,448 32,448 32,352 "/>
 | 
			
		||||
<rect x="45.7" y="140.1" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -67.4113 253.2548)" width="452.5" height="135.8"/>
 | 
			
		||||
<path d="M32,512v-32h352v32H32z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 555 B  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/eduqua.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/family.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 23 KiB  | 
							
								
								
									
										76
									
								
								aemo/static/img/filter_off.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   enable-background="new 0 0 24 24"
 | 
			
		||||
   height="24px"
 | 
			
		||||
   viewBox="0 0 24 24"
 | 
			
		||||
   width="24px"
 | 
			
		||||
   fill="#000000"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg14"
 | 
			
		||||
   sodipodi:docname="filter_off.svg"
 | 
			
		||||
   inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
 | 
			
		||||
  <metadata
 | 
			
		||||
     id="metadata20">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work
 | 
			
		||||
         rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs18" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1"
 | 
			
		||||
     objecttolerance="10"
 | 
			
		||||
     gridtolerance="10"
 | 
			
		||||
     guidetolerance="10"
 | 
			
		||||
     inkscape:pageopacity="0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:window-width="1296"
 | 
			
		||||
     inkscape:window-height="894"
 | 
			
		||||
     id="namedview16"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     inkscape:zoom="30.625"
 | 
			
		||||
     inkscape:cx="12"
 | 
			
		||||
     inkscape:cy="12"
 | 
			
		||||
     inkscape:window-x="26"
 | 
			
		||||
     inkscape:window-y="23"
 | 
			
		||||
     inkscape:window-maximized="0"
 | 
			
		||||
     inkscape:current-layer="svg14" />
 | 
			
		||||
  <g
 | 
			
		||||
     id="g4">
 | 
			
		||||
    <rect
 | 
			
		||||
       fill="none"
 | 
			
		||||
       height="24"
 | 
			
		||||
       width="24"
 | 
			
		||||
       id="rect2" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <g
 | 
			
		||||
     id="g12"
 | 
			
		||||
     style="fill:#666666">
 | 
			
		||||
    <g
 | 
			
		||||
       id="g10"
 | 
			
		||||
       style="fill:#666666">
 | 
			
		||||
      <path
 | 
			
		||||
         d="M19.79,5.61C20.3,4.95,19.83,4,19,4H6.83l7.97,7.97L19.79,5.61z"
 | 
			
		||||
         id="path6"
 | 
			
		||||
         style="fill:#666666" />
 | 
			
		||||
      <path
 | 
			
		||||
         d="M2.81,2.81L1.39,4.22L10,13v6c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1v-2.17l5.78,5.78l1.41-1.41L2.81,2.81z"
 | 
			
		||||
         id="path8"
 | 
			
		||||
         style="fill:#666666" />
 | 
			
		||||
    </g>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/formation.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/help.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 543 B  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/icon_add.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/journal.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 85 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/logo-cr.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 41 KiB  | 
							
								
								
									
										219
									
								
								aemo/static/img/logo-cr.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,219 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   viewBox="0 0 1238.1333 262.01334"
 | 
			
		||||
   height="262.01334"
 | 
			
		||||
   width="1238.1333"
 | 
			
		||||
   xml:space="preserve"
 | 
			
		||||
   id="svg2"
 | 
			
		||||
   version="1.1"><metadata
 | 
			
		||||
     id="metadata8"><rdf:RDF><cc:Work
 | 
			
		||||
         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
 | 
			
		||||
     id="defs6" /><g
 | 
			
		||||
     transform="matrix(1.3333333,0,0,-1.3333333,0,262.01333)"
 | 
			
		||||
     id="g10"><g
 | 
			
		||||
       transform="scale(0.1)"
 | 
			
		||||
       id="g12"><path
 | 
			
		||||
         id="path14"
 | 
			
		||||
         style="fill:#d92838;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 9286.04,1310.05 h -655.02 v 655.02 H 7976 V 1310.05 H 7320.97 V 655.031 H 7976 V 0 h 655.02 v 655.031 h 655.02 v 655.019" /><path
 | 
			
		||||
         id="path16"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 282.129,1211.91 c -13.301,3.5 -25.902,5.6 -39.203,5.6 -91.707,0 -131.617,-83.3 -131.617,-164.51 0,-78.41 39.91,-175.02 130.914,-175.02 13.304,0 26.605,3.489 39.906,7.7 v -98.711 c -15.399,-6.309 -31.5,-9.098 -48.305,-9.098 C 83.3086,777.871 0,913.68 0,1052.29 c 0,130.92 86.8086,265.34 228.922,265.34 18.203,0 35.703,-3.51 53.207,-9.81 v -95.91" /><path
 | 
			
		||||
         id="path18"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 424.207,1137 v -42.7 h 1.398 c 18.2,30.81 42.004,52.5 80.508,56 v -95.91 c -4.902,0.71 -9.797,1.41 -15.398,1.41 -65.813,0 -66.508,-43.41 -66.508,-95.921 V 783.469 H 325.496 V 1137 h 98.711" /><path
 | 
			
		||||
         id="path20"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 627.203,959.879 c 0,-20.289 1.399,-95.199 35.703,-95.199 34.305,0 35.703,74.91 35.703,95.199 0,20.305 -1.398,95.221 -35.703,95.221 -34.304,0 -35.703,-74.916 -35.703,-95.221 z m 170.117,0 c 0,-90.297 -28.707,-183.418 -134.414,-183.418 -105.711,0 -134.414,93.121 -134.414,183.418 0,90.321 28.703,184.121 134.414,184.121 105.707,0 134.414,-93.8 134.414,-184.121" /><path
 | 
			
		||||
         id="path22"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 940.793,1137 V 783.469 H 842.082 V 1137 Z m -112.012,105.71 c 0,35.71 28.699,62.31 64.403,62.31 34.304,0 60.91,-30.1 60.91,-63.7 0,-33.61 -28.707,-62.31 -63.008,-62.31 -34.301,0 -62.305,28.7 -62.305,63.7" /><path
 | 
			
		||||
         id="path24"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1098.99,1137 28.01,-53.91 10.5,-21.7 39.9,75.61 h 102.21 L 1189.3,973.887 1293.61,783.469 H 1189.3 l -51.8,97.312 -52.51,-97.312 H 983.484 L 1084.99,973.887 996.785,1137 h 102.205" /><path
 | 
			
		||||
         id="path26"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1511.3,1041.39 v -93.101 h -175.02 v 93.101 h 175.02" /><path
 | 
			
		||||
         id="path28"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1693.29,1084.5 18.2,-0.7 c 47.61,0 66.51,32.2 66.51,75.6 0,37.81 -18.2,68.62 -70.71,65.81 h -14 z M 1588.98,783.469 v 527.861 h 105.01 c 66.51,0 185.52,-7.01 185.52,-151.22 0,-58.11 -23.1,-107.82 -74.91,-136.52 l 91.72,-240.121 H 1786.4 l -91.71,243.621 h -1.4 V 783.469 h -104.31" /><path
 | 
			
		||||
         id="path30"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2014.59,959.879 c 0,-20.289 1.4,-95.199 35.7,-95.199 34.31,0 35.71,74.91 35.71,95.199 0,20.305 -1.4,95.221 -35.71,95.221 -34.3,0 -35.7,-74.916 -35.7,-95.221 z m 170.12,0 c 0,-90.297 -28.71,-183.418 -134.42,-183.418 -105.71,0 -134.41,93.121 -134.41,183.418 0,90.321 28.7,184.121 134.41,184.121 105.71,0 134.42,-93.8 134.42,-184.121" /><path
 | 
			
		||||
         id="path32"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 2342.88,1137 V 943.082 c 0,-19.602 -7.7,-74.203 25.2,-74.203 26.61,0 24.51,35 24.51,52.512 l 0.7,215.609 h 98.01 V 911.582 c 0,-81.902 -38.51,-135.121 -126.02,-135.121 -112.01,0 -121.11,84.019 -121.11,148.418 V 1137 h 98.71" /><path
 | 
			
		||||
         id="path34"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2706.18,961.988 c 0,19.602 -1.4,94.502 -35,94.502 -33.61,0 -35.7,-74.205 -35.7,-94.502 0,-22.398 1.39,-95.207 37.09,-95.207 31.51,0 33.61,76.301 33.61,95.207 z m -78.41,-225.429 c 1.4,-22.399 7.71,-50.399 35.71,-50.399 44.1,0 37.8,66.5 37.8,96.61 v 44.812 h -1.4 c -6.3,-12.613 -14,-25.91 -23.8,-35.012 -9.8,-9.8 -22.41,-16.109 -39.91,-16.109 -36.4,0 -60.9,28.711 -76.31,58.809 -17.5,35 -23.1,80.511 -23.1,119.718 0,61.602 11.21,189.012 95.21,189.012 32.9,0 56.71,-21.7 67.91,-49.7 h 1.4 v 42.7 h 98.71 V 752.66 c 0,-103.609 -46.2,-144.91 -132.31,-144.91 -84.01,0 -122.52,48.301 -127.41,128.809 h 87.5" /><path
 | 
			
		||||
         id="path36"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3027.48,1003.29 c 0,23.1 -4.9,65.11 -35.7,65.11 -30.1,0 -36.41,-40.61 -36.41,-63.01 v -10.499 h 72.11 z m 83.31,-69.31 h -156.12 c 0.7,-25.898 -0.7,-81.898 37.11,-81.898 25.2,0 31.5,19.598 31.5,40.59 h 84.01 c -4.2,-32.192 -14,-61.602 -32.21,-82.602 -17.5,-21 -43.4,-33.609 -79.8,-33.609 -100.11,0 -135.82,88.918 -135.82,176.43 0,84.709 30.81,191.109 133.72,191.109 65.1,0 125.31,-58.1 119.71,-169.41 l -2.1,-40.61" /><path
 | 
			
		||||
         id="path38"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3431.35,1082.4 h 1.41 c 13.3,33.6 43.4,61.6 81.9,61.6 72.82,0 66.52,-81.2 66.52,-132.31 V 783.469 h -98.71 v 193.922 c 0,18.203 7,67.209 -24.5,67.209 -22.41,0 -26.62,-23.11 -26.62,-40.61 V 783.469 h -98.7 V 1137 h 98.7 v -54.6" /><path
 | 
			
		||||
         id="path40"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3793.96,1003.29 c 0,23.1 -4.9,65.11 -35.7,65.11 -30.11,0 -36.41,-40.61 -36.41,-63.01 v -10.499 h 72.11 z m 83.31,-69.31 h -156.12 c 0.7,-25.898 -0.69,-81.898 37.11,-81.898 25.2,0 31.5,19.598 31.5,40.59 h 84.01 c -4.2,-32.192 -14,-61.602 -32.2,-82.602 -17.51,-21 -43.41,-33.609 -79.82,-33.609 -100.1,0 -135.81,88.918 -135.81,176.43 0,84.709 30.8,191.109 133.71,191.109 65.11,0 125.32,-58.1 119.72,-169.41 l -2.1,-40.61" /><path
 | 
			
		||||
         id="path42"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 4023.55,1137 V 943.082 c 0,-19.602 -7.7,-74.203 25.21,-74.203 26.6,0 24.5,35 24.5,52.512 l 0.69,215.609 h 98.02 V 911.582 c 0,-81.902 -38.51,-135.121 -126.02,-135.121 -112.01,0 -121.11,84.019 -121.11,148.418 V 1137 h 98.71" /><path
 | 
			
		||||
         id="path44"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4403.65,789.07 c -18.2,-8.41 -36.41,-12.609 -56.71,-12.609 -95.91,0 -129.51,100.809 -129.51,181.32 0,88.919 35,186.219 137.22,186.219 17.5,0 32.9,-3.49 49,-10.5 v -86.8 c -9.1,8.4 -16.81,11.2 -28.7,11.2 -48.31,0 -58.81,-58.107 -58.81,-94.509 0,-37.809 7.69,-94.512 57.4,-94.512 11.2,0 21.01,4.191 30.11,10.492 V 789.07" /><path
 | 
			
		||||
         id="path46"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 4545.73,1348.43 V 1082.4 h 1.4 c 13.3,33.6 43.4,61.6 81.91,61.6 72.8,0 66.5,-81.2 66.5,-132.31 V 783.469 h -98.71 v 193.922 c 0,18.203 7,67.209 -24.5,67.209 -22.4,0 -26.6,-23.11 -26.6,-40.61 V 783.469 h -98.71 v 564.961 h 98.71" /><path
 | 
			
		||||
         id="path48"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4909.73,961.988 c 0,19.602 -2.1,94.502 -35.7,94.502 -32.9,0 -35,-74.205 -35,-94.502 0,-22.398 1.4,-95.207 36.41,-95.207 31.5,0 34.29,76.301 34.29,95.207 z m 72.81,260.422 -42.7,-43.4 -59.51,49.01 -61.6,-49.01 -38.51,49.01 100.11,79.1 z m -79.81,-399.039 h -1.4 c -13.3,-26.601 -33.6,-46.91 -65.1,-46.91 -79.81,0 -95.91,127.418 -95.91,184.121 0,60.908 11.9,183.418 94.51,183.418 32.9,0 55.3,-21.7 66.5,-50.4 h 1.4 v 43.4 h 98.71 V 783.469 h -98.71 v 39.902" /><path
 | 
			
		||||
         id="path50"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 5182.02,1235.02 V 1137 h 40.61 v -81.2 h -40.61 V 783.469 h -98.71 V 1055.8 h -34.29 v 81.2 h 34.29 v 98.02 h 98.71" /><path
 | 
			
		||||
         id="path52"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5415.57,1003.29 c 0,23.1 -4.9,65.11 -35.7,65.11 -30.11,0 -36.41,-40.61 -36.41,-63.01 v -10.499 h 72.11 z m 83.31,-69.31 h -156.12 c 0.7,-25.898 -0.69,-81.898 37.11,-81.898 25.2,0 31.5,19.598 31.5,40.59 h 84.01 c -4.2,-32.192 -14.01,-61.602 -32.2,-82.602 -17.51,-21 -43.41,-33.609 -79.81,-33.609 -100.11,0 -135.81,88.918 -135.81,176.43 0,84.709 30.8,191.109 133.71,191.109 65.1,0 125.31,-58.1 119.71,-169.41 l -2.1,-40.61" /><path
 | 
			
		||||
         id="path54"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 5645.16,1348.43 V 783.469 h -98.71 v 564.961 h 98.71" /><path
 | 
			
		||||
         id="path56"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5788.66,959.879 c 0,-20.289 1.39,-95.199 35.7,-95.199 34.31,0 35.7,74.91 35.7,95.199 0,20.305 -1.39,95.221 -35.7,95.221 -34.31,0 -35.7,-74.916 -35.7,-95.221 z m 170.12,0 c 0,-90.297 -28.71,-183.418 -134.42,-183.418 -105.71,0 -134.41,93.121 -134.41,183.418 0,90.321 28.7,184.121 134.41,184.121 105.71,0 134.42,-93.8 134.42,-184.121" /><path
 | 
			
		||||
         id="path58"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 6102.25,1137 V 783.469 h -98.71 V 1137 Z m -112.01,105.71 c 0,35.71 28.71,62.31 64.41,62.31 34.3,0 60.9,-30.1 60.9,-63.7 0,-33.61 -28.7,-62.31 -63,-62.31 -34.3,0 -62.31,28.7 -62.31,63.7" /><path
 | 
			
		||||
         id="path60"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6331.16,1041.8 c -14.01,12.59 -27.31,23.1 -47.61,23.1 -17.5,0 -26.6,-10.51 -26.6,-25.91 1.4,-41.295 112.7,-32.9 112.7,-138.611 0,-70.008 -40.59,-123.918 -113.4,-123.918 -37.81,0 -77.01,16.109 -106.41,40.609 l 43.4,70.012 c 14.71,-12.602 29.4,-23.113 49,-23.113 15.4,0 28.71,11.203 28.71,29.41 1.4,46.902 -112.71,35.012 -112.71,147.021 0,67.21 56,103.6 118.31,103.6 35.7,0 65.81,-9.1 94.51,-30.09 l -39.9,-72.11" /><path
 | 
			
		||||
         id="path62"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6581.05,1003.29 c 0,23.1 -4.9,65.11 -35.7,65.11 -30.11,0 -36.41,-40.61 -36.41,-63.01 v -10.499 h 72.11 z m 83.31,-69.31 h -156.12 c 0.7,-25.898 -0.69,-81.898 37.11,-81.898 25.2,0 31.5,19.598 31.5,40.59 h 84.01 c -4.2,-32.192 -14,-61.602 -32.2,-82.602 -17.51,-21 -43.41,-33.609 -79.82,-33.609 -100.1,0 -135.81,88.918 -135.81,176.43 0,84.709 30.8,191.109 133.71,191.109 65.11,0 125.32,-58.1 119.72,-169.41 l -2.1,-40.61" /><path
 | 
			
		||||
         id="path64"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1120.11,225.961 -21.5,124.258 c -3.87,21.929 -5.59,44.293 -8.17,66.222 h -1.72 c -3.01,-21.929 -5.16,-44.293 -8.6,-66.222 L 1061.2,225.961 Z m 49.02,-92.891 h -32.25 l -12.04,66.66 h -68.8 l -11.6,-66.66 h -32.26 l 63.64,324.219 h 28.38 l 64.93,-324.219" /><path
 | 
			
		||||
         id="path66"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1272.76,308.512 h -0.86 c -8.17,11.617 -21.07,22.359 -36.55,22.359 -16.77,0 -27.52,-12.902 -27.52,-29.242 0,-21.07 18.06,-33.969 35.69,-48.578 18.06,-15.051 35.69,-31.403 35.69,-60.211 0,-35.25 -23.65,-63.199 -60.2,-63.199 -13.76,0 -29.24,6.441 -39.99,15.05 v 32.668 c 10.75,-11.179 22.36,-21.488 39.13,-21.488 19.35,0 32.25,15.051 32.25,33.527 0,21.942 -17.63,36.563 -35.69,52.461 -17.63,15.481 -35.69,32.68 -35.69,58.911 0,33.121 21.93,56.332 55.04,56.332 14.19,0 27.52,-5.59 38.7,-14.192 v -34.398" /><path
 | 
			
		||||
         id="path68"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1392.31,308.512 h -0.87 c -8.17,11.617 -21.07,22.359 -36.55,22.359 -16.77,0 -27.52,-12.902 -27.52,-29.242 0,-21.07 18.06,-33.969 35.69,-48.578 18.06,-15.051 35.69,-31.403 35.69,-60.211 0,-35.25 -23.65,-63.199 -60.2,-63.199 -13.76,0 -29.24,6.441 -39.99,15.05 v 32.668 c 10.75,-11.179 22.36,-21.488 39.13,-21.488 19.35,0 32.25,15.051 32.25,33.527 0,21.942 -17.63,36.563 -35.69,52.461 -17.63,15.481 -35.69,32.68 -35.69,58.911 0,33.121 21.93,56.332 55.04,56.332 14.19,0 27.52,-5.59 38.71,-14.192 v -34.398" /><path
 | 
			
		||||
         id="path70"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1449.92,243.582 c 0,-21.5 0.86,-87.711 34.41,-87.711 33.53,0 34.39,66.211 34.39,87.711 0,21.078 -0.86,87.289 -34.39,87.289 -33.55,0 -34.41,-66.211 -34.41,-87.289 z m 97.61,0 c 0,-41.711 -5.16,-113.941 -63.2,-113.941 -58.06,0 -63.22,72.23 -63.22,113.941 0,41.277 5.16,113.52 63.22,113.52 58.04,0 63.2,-72.243 63.2,-113.52" /><path
 | 
			
		||||
         id="path72"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1668.79,316.25 c -6.88,7.309 -15.91,14.621 -26.66,14.621 -36.12,0 -39.56,-62.781 -39.56,-88.152 0,-21.93 4.73,-86.848 37.84,-86.848 12.9,0 21.07,9.879 27.52,19.77 h 0.86 V 140.82 c -8.17,-6.89 -21.07,-11.179 -31.82,-11.179 -50.74,0 -63.21,71.371 -63.21,109.218 0,41.7 8.6,118.243 64.5,118.243 10.32,0 21.5,-3.012 30.53,-8.602 v -32.25" /><path
 | 
			
		||||
         id="path74"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1727.7,133.07 h -27.95 v 220.59 h 27.95 z m -13.76,264.879 c -11.6,0 -21.07,9.461 -21.07,21.071 0,11.179 9.47,20.64 21.07,20.64 11.18,0 20.64,-9.461 20.64,-20.64 0,-11.61 -9.46,-21.071 -20.64,-21.071" /><path
 | 
			
		||||
         id="path76"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1821.44,334.309 c -30.96,0 -32.68,-49.02 -32.68,-86.43 0,-91.149 15.05,-95.457 31.39,-95.457 27.09,0 32.25,25.367 32.25,94.598 0,47.73 -3.44,87.289 -30.96,87.289 z m 56.33,-201.239 h -27.95 v 17.2 h -0.86 c -8.17,-12.469 -20.64,-20.629 -36.55,-20.629 -35.26,0 -52.46,19.769 -52.46,116.089 0,49.45 1.72,111.372 55.04,111.372 15.48,0 25.37,-6.454 33.97,-18.493 h 0.86 v 15.051 h 27.95 V 133.07" /><path
 | 
			
		||||
         id="path78"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 1992.58,327.859 h -36.55 V 133.07 h -27.95 v 194.789 h -23.64 v 25.801 h 23.64 v 60.199 h 27.95 V 353.66 h 36.55 v -25.801" /><path
 | 
			
		||||
         id="path80"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2034.71,133.07 h -27.95 v 220.59 h 27.95 z m -13.76,264.879 c -11.6,0 -21.07,9.461 -21.07,21.071 0,11.179 9.47,20.64 21.07,20.64 11.18,0 20.64,-9.461 20.64,-20.64 0,-11.61 -9.46,-21.071 -20.64,-21.071" /><path
 | 
			
		||||
         id="path82"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2095.77,243.582 c 0,-21.5 0.86,-87.711 34.41,-87.711 33.53,0 34.39,66.211 34.39,87.711 0,21.078 -0.86,87.289 -34.39,87.289 -33.55,0 -34.41,-66.211 -34.41,-87.289 z m 97.61,0 c 0,-41.711 -5.16,-113.941 -63.2,-113.941 -58.06,0 -63.22,72.23 -63.22,113.941 0,41.277 5.16,113.52 63.22,113.52 58.04,0 63.2,-72.243 63.2,-113.52" /><path
 | 
			
		||||
         id="path84"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2254.01,336.031 c 11.18,12.899 26.23,21.071 43.43,21.071 39.12,0 42.57,-36.122 42.57,-66.223 V 133.07 h -27.95 v 154.801 c 0,26.231 -1.72,46.438 -24.94,46.438 -31.82,0 -33.11,-34.829 -33.11,-58.047 V 133.07 h -27.95 v 220.59 h 27.95 v -17.629" /><path
 | 
			
		||||
         id="path86"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2556.72,316.25 c -6.88,7.309 -15.91,14.621 -26.66,14.621 -36.12,0 -39.56,-62.781 -39.56,-88.152 0,-21.93 4.73,-86.848 37.84,-86.848 12.9,0 21.07,9.879 27.52,19.77 h 0.86 V 140.82 c -8.17,-6.89 -21.07,-11.179 -31.82,-11.179 -50.74,0 -63.21,71.371 -63.21,109.218 0,41.7 8.6,118.243 64.5,118.243 10.32,0 21.5,-3.012 30.53,-8.602 v -32.25" /><path
 | 
			
		||||
         id="path88"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2642.72,334.309 c -30.96,0 -32.68,-49.02 -32.68,-86.43 0,-91.149 15.05,-95.457 31.39,-95.457 27.09,0 32.25,25.367 32.25,94.598 0,47.73 -3.44,87.289 -30.96,87.289 z m 56.32,-201.239 h -27.94 v 17.2 h -0.86 c -8.17,-12.469 -20.64,-20.629 -36.55,-20.629 -35.26,0 -52.46,19.769 -52.46,116.089 0,49.45 1.72,111.372 55.04,111.372 15.48,0 25.37,-6.454 33.97,-18.493 h 0.86 v 15.051 h 27.94 V 133.07" /><path
 | 
			
		||||
         id="path90"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2765.69,336.031 c 11.19,12.899 26.24,21.071 43.44,21.071 39.12,0 42.57,-36.122 42.57,-66.223 V 133.07 h -27.95 v 154.801 c 0,26.231 -1.72,46.438 -24.94,46.438 -31.82,0 -33.12,-34.829 -33.12,-58.047 V 133.07 h -27.94 v 220.59 h 27.94 v -17.629" /><path
 | 
			
		||||
         id="path92"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 2966.51,327.859 h -36.56 V 133.07 H 2902 v 194.789 h -23.64 V 353.66 H 2902 v 60.199 h 27.95 V 353.66 h 36.56 v -25.801" /><path
 | 
			
		||||
         id="path94"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3003.05,243.582 c 0,-21.5 0.86,-87.711 34.4,-87.711 33.54,0 34.4,66.211 34.4,87.711 0,21.078 -0.86,87.289 -34.4,87.289 -33.54,0 -34.4,-66.211 -34.4,-87.289 z m 97.61,0 c 0,-41.711 -5.16,-113.941 -63.21,-113.941 -58.05,0 -63.21,72.23 -63.21,113.941 0,41.277 5.16,113.52 63.21,113.52 58.05,0 63.21,-72.243 63.21,-113.52" /><path
 | 
			
		||||
         id="path96"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3161.28,336.031 c 11.18,12.899 26.23,21.071 43.44,21.071 39.12,0 42.57,-36.122 42.57,-66.223 V 133.07 h -27.95 v 154.801 c 0,26.231 -1.72,46.438 -24.94,46.438 -31.82,0 -33.12,-34.829 -33.12,-58.047 V 133.07 h -27.95 v 220.59 h 27.95 v -17.629" /><path
 | 
			
		||||
         id="path98"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3341.02,334.309 c -30.96,0 -32.68,-49.02 -32.68,-86.43 0,-91.149 15.05,-95.457 31.39,-95.457 27.09,0 32.25,25.367 32.25,94.598 0,47.73 -3.44,87.289 -30.96,87.289 z m 56.33,-201.239 h -27.95 v 17.2 h -0.86 c -8.17,-12.469 -20.64,-20.629 -36.55,-20.629 -35.26,0 -52.46,19.769 -52.46,116.089 0,49.45 1.72,111.372 55.04,111.372 15.48,0 25.37,-6.454 33.97,-18.493 h 0.86 v 15.051 h 27.95 V 133.07" /><path
 | 
			
		||||
         id="path100"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3464,133.07 h -27.95 v 344 H 3464 v -344" /><path
 | 
			
		||||
         id="path102"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3589.56,261.211 -0.43,3.879 c -0.86,21.058 -1.72,65.781 -30.96,65.781 -26.66,0 -32.68,-49.883 -32.68,-69.66 z m -64.5,-23.641 v -6.461 c 0,-21.918 3.87,-75.238 34.4,-75.238 24.51,0 27.95,34.391 27.95,52.02 h 28.81 c -0.43,-33.539 -13.33,-78.25 -54.18,-78.25 -58.05,0 -65.79,68.8 -65.79,113.078 0,39.562 8.61,114.383 61.92,114.383 51.6,0 59.77,-67.512 59.77,-106.204 V 237.57 h -92.88" /><path
 | 
			
		||||
         id="path104"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3794.65,334.309 c -31.39,0 -32.68,-55.028 -32.68,-86.43 0,-20.641 -4.29,-95.457 31.39,-95.457 29.67,0 32.26,36.547 32.26,94.598 0,50.312 -5.17,87.289 -30.97,87.289 z m 28.38,-184.039 h -0.86 c -8.16,-12.469 -20.64,-20.629 -36.54,-20.629 -46.02,0 -52.46,55.898 -52.46,116.089 0,60.211 6.01,111.372 55.03,111.372 15.48,0 25.38,-6.454 33.97,-18.493 h 0.86 V 477.07 h 27.95 v -344 h -27.95 v 17.2" /><path
 | 
			
		||||
         id="path106"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 3976.54,261.211 -0.43,3.879 c -0.85,21.058 -1.71,65.781 -30.96,65.781 -26.65,0 -32.68,-49.883 -32.68,-69.66 z m -64.5,-23.641 v -6.461 c 0,-21.918 3.87,-75.238 34.41,-75.238 24.5,0 27.95,34.391 27.95,52.02 h 28.8 c -0.43,-33.539 -13.33,-78.25 -54.18,-78.25 -58.04,0 -65.79,68.8 -65.79,113.078 0,39.562 8.61,114.383 61.92,114.383 51.6,0 59.77,-67.512 59.77,-106.204 V 237.57 h -92.88" /><path
 | 
			
		||||
         id="path108"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4154.54,133.07 h -27.95 v 344 h 27.95 v -344" /><path
 | 
			
		||||
         id="path110"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4248.28,334.309 c -30.95,0 -32.67,-49.02 -32.67,-86.43 0,-91.149 15.05,-95.457 31.38,-95.457 27.09,0 32.26,25.367 32.26,94.598 0,47.73 -3.45,87.289 -30.97,87.289 z m 56.33,-201.239 h -27.95 v 17.2 h -0.86 c -8.16,-12.469 -20.63,-20.629 -36.54,-20.629 -35.26,0 -52.46,19.769 -52.46,116.089 0,49.45 1.72,111.372 55.04,111.372 15.48,0 25.37,-6.454 33.96,-18.493 h 0.86 v 15.051 h 27.95 V 133.07" /><path
 | 
			
		||||
         id="path112"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4558.31,142.109 c -11.18,-7.75 -27.09,-13.328 -41.28,-13.328 -76.11,0 -87.29,99.321 -87.29,154.789 0,52.461 1.72,178.02 90.3,178.02 12.04,0 28.38,-3.008 38.27,-10.32 v -33.11 c -12.04,8.172 -22.79,14.192 -37.84,14.192 -52.46,0 -59.78,-76.114 -59.78,-136.313 0,-37.84 0,-138.019 57.63,-138.019 14.62,0 28.38,6.441 39.99,14.621 v -30.532" /><path
 | 
			
		||||
         id="path114"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4618.5,324.422 h 0.86 c 6.02,15.476 18.06,33.109 36.55,32.68 v -32.68 l -5.59,0.43 c -28.8,0 -31.82,-27.954 -31.82,-63.211 V 133.07 h -27.95 v 220.59 h 27.95 v -29.238" /><path
 | 
			
		||||
         id="path116"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4701.06,243.582 c 0,-21.5 0.86,-87.711 34.4,-87.711 33.54,0 34.4,66.211 34.4,87.711 0,21.078 -0.86,87.289 -34.4,87.289 -33.54,0 -34.4,-66.211 -34.4,-87.289 z m 97.6,0 c 0,-41.711 -5.15,-113.941 -63.2,-113.941 -58.06,0 -63.21,72.23 -63.21,113.941 0,41.277 5.15,113.52 63.21,113.52 58.05,0 63.2,-72.243 63.2,-113.52" /><path
 | 
			
		||||
         id="path118"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4859.29,133.07 h -27.95 v 220.59 h 27.95 z m -13.76,264.879 c -11.6,0 -21.07,9.461 -21.07,21.071 0,11.179 9.47,20.64 21.07,20.64 11.18,0 20.64,-9.461 20.64,-20.64 0,-11.61 -9.46,-21.071 -20.64,-21.071" /><path
 | 
			
		||||
         id="path120"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 4966.79,245.73 49.45,-112.66 h -30.96 l -22.79,58.051 c -4.72,12.039 -9.46,24.078 -11.61,36.981 h -1.72 c -1.72,-12.903 -6.01,-25.372 -10.75,-37.411 l -21.92,-57.621 h -31.4 l 50.74,112.66 -45.57,107.93 h 30.95 l 18.06,-49.019 c 5.17,-14.621 9.89,-29.243 12.9,-44.719 h 1.72 c 3.88,15.476 6.88,30.957 12.9,46.019 l 18.49,47.719 h 30.96 l -49.45,-107.93" /><path
 | 
			
		||||
         id="path122"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5022.69,287.441 h 67.08 V 253.48 h -67.08 v 33.961" /><path
 | 
			
		||||
         id="path124"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5145.67,316.25 h 10.75 c 42.14,0 49.01,14.621 49.01,57.191 0,51.168 -15.48,56.329 -53.75,56.329 h -6.01 z m -30.1,-183.18 v 324.219 h 32.24 c 45.16,0 88.59,-3.437 88.59,-82.559 0,-41.269 -9.04,-81.691 -61.07,-83.421 l 71.81,-158.239 h -31.82 l -68.8,158.68 h -0.85 V 133.07 h -30.1" /><path
 | 
			
		||||
         id="path126"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5295.3,243.582 c 0,-21.5 0.86,-87.711 34.41,-87.711 33.54,0 34.4,66.211 34.4,87.711 0,21.078 -0.86,87.289 -34.4,87.289 -33.55,0 -34.41,-66.211 -34.41,-87.289 z m 97.62,0 c 0,-41.711 -5.16,-113.941 -63.21,-113.941 -58.06,0 -63.22,72.23 -63.22,113.941 0,41.277 5.16,113.52 63.22,113.52 58.05,0 63.21,-72.243 63.21,-113.52" /><path
 | 
			
		||||
         id="path128"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 5451.82,353.66 V 215.199 c 0,-30.09 1.72,-59.328 29.24,-59.328 28.38,0 29.67,29.238 30.1,59.328 V 353.66 h 27.95 V 191.551 c 0,-36.969 -18.07,-61.91 -57.62,-61.91 -39.12,0 -57.62,24.507 -57.62,61.91 V 353.66 h 27.95" /><path
 | 
			
		||||
         id="path130"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5633.29,155.871 c 29.67,0 30.95,43.43 30.95,91.578 0,40.422 -2.57,86.86 -33.12,86.86 -20.2,0 -32.68,-13.75 -32.68,-94.59 0,-24.09 -0.43,-83.848 34.85,-83.848 z m 56.33,-30.973 c 0,-41.2769 -1.72,-79.5386 -57.64,-79.5386 -38.69,0 -56.76,23.2109 -56.76,61.0506 v 8.18 h 27.52 v -3.879 c 0,-20.629 5.61,-39.1212 29.69,-39.1212 31.38,0 28.81,27.5196 28.81,51.1602 v 25.801 c -9.48,-10.75 -21.08,-15.481 -35.71,-15.481 -55.04,0 -55.89,76.121 -55.89,115.239 0,36.98 4.29,108.793 55.46,108.793 15.06,0 27.52,-6.883 35.71,-18.493 h 0.86 v 15.051 h 27.95 V 124.898" /><path
 | 
			
		||||
         id="path132"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 5815.16,261.211 -0.43,3.879 c -0.86,21.058 -1.72,65.781 -30.95,65.781 -26.66,0 -32.68,-49.883 -32.68,-69.66 z m -64.49,-23.641 v -6.461 c 0,-21.918 3.87,-75.238 34.39,-75.238 24.52,0 27.95,34.391 27.95,52.02 h 28.81 c -0.43,-33.539 -13.32,-78.25 -54.18,-78.25 -58.04,0 -65.78,68.8 -65.78,113.078 0,39.562 8.6,114.383 61.92,114.383 51.6,0 59.76,-67.512 59.76,-106.204 V 237.57 h -92.87" /><path
 | 
			
		||||
         id="path134"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6049.5,308.512 h -0.86 c -8.17,11.617 -21.08,22.359 -36.54,22.359 -16.78,0 -27.52,-12.902 -27.52,-29.242 0,-21.07 18.04,-33.969 35.68,-48.578 18.07,-15.051 35.68,-31.403 35.68,-60.211 0,-35.25 -23.65,-63.199 -60.19,-63.199 -13.75,0 -29.24,6.441 -39.98,15.05 v 32.668 c 10.74,-11.179 22.34,-21.488 39.12,-21.488 19.35,0 32.25,15.051 32.25,33.527 0,21.942 -17.62,36.563 -35.69,52.461 -17.64,15.481 -35.68,32.68 -35.68,58.911 0,33.121 21.91,56.332 55.04,56.332 14.18,0 27.52,-5.59 38.69,-14.192 v -34.398" /><path
 | 
			
		||||
         id="path136"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="M 6110.98,353.66 V 215.199 c 0,-30.09 1.72,-59.328 29.24,-59.328 28.4,0 29.69,29.238 30.12,59.328 V 353.66 h 27.95 V 191.551 c 0,-36.969 -18.07,-61.91 -57.64,-61.91 -39.12,0 -57.62,24.507 -57.62,61.91 V 353.66 h 27.95" /><path
 | 
			
		||||
         id="path138"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6263.21,133.07 h -27.95 v 220.59 h 27.95 z m -13.77,264.879 c -11.6,0 -21.05,9.461 -21.05,21.071 0,11.179 9.45,20.64 21.05,20.64 11.19,0 20.64,-9.461 20.64,-20.64 0,-11.61 -9.45,-21.071 -20.64,-21.071" /><path
 | 
			
		||||
         id="path140"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6386.2,308.512 h -0.88 c -8.17,11.617 -21.06,22.359 -36.54,22.359 -16.76,0 -27.52,-12.902 -27.52,-29.242 0,-21.07 18.06,-33.969 35.68,-48.578 18.07,-15.051 35.7,-31.403 35.7,-60.211 0,-35.25 -23.65,-63.199 -60.19,-63.199 -13.77,0 -29.24,6.441 -40,15.05 v 32.668 c 10.76,-11.179 22.36,-21.488 39.14,-21.488 19.33,0 32.24,15.051 32.24,33.527 0,21.942 -17.63,36.563 -35.7,52.461 -17.62,15.481 -35.68,32.68 -35.68,58.911 0,33.121 21.93,56.332 55.04,56.332 14.2,0 27.52,-5.59 38.71,-14.192 v -34.398" /><path
 | 
			
		||||
         id="path142"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6505.73,308.512 h -0.86 c -8.16,11.617 -21.07,22.359 -36.54,22.359 -16.78,0 -27.52,-12.902 -27.52,-29.242 0,-21.07 18.04,-33.969 35.68,-48.578 18.07,-15.051 35.68,-31.403 35.68,-60.211 0,-35.25 -23.65,-63.199 -60.19,-63.199 -13.75,0 -29.24,6.441 -39.98,15.05 v 32.668 c 10.74,-11.179 22.34,-21.488 39.12,-21.488 19.35,0 32.25,15.051 32.25,33.527 0,21.942 -17.62,36.563 -35.69,52.461 -17.63,15.481 -35.68,32.68 -35.68,58.911 0,33.121 21.91,56.332 55.04,56.332 14.18,0 27.52,-5.59 38.69,-14.192 v -34.398" /><path
 | 
			
		||||
         id="path144"
 | 
			
		||||
         style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
 | 
			
		||||
         d="m 6627.86,261.211 -0.43,3.879 c -0.86,21.058 -1.72,65.781 -30.98,65.781 -26.64,0 -32.67,-49.883 -32.67,-69.66 z m -64.51,-23.641 v -6.461 c 0,-21.918 3.86,-75.238 34.41,-75.238 24.49,0 27.95,34.391 27.95,52.02 h 28.81 c -0.43,-33.539 -13.34,-78.25 -54.18,-78.25 -58.05,0 -65.8,68.8 -65.8,113.078 0,39.562 8.61,114.383 61.91,114.383 51.6,0 59.79,-67.512 59.79,-106.204 V 237.57 h -92.89" /></g></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/logo-zewo.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										1
									
								
								aemo/static/img/logo-zewo.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/printer.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 10 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/reseau.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/stat.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								aemo/static/img/telephone.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 117 KiB  | 
							
								
								
									
										3
									
								
								aemo/static/img/warning.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
 | 
			
		||||
    <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 466 B  | 
							
								
								
									
										424
									
								
								aemo/static/js/DateTimeShortcuts.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,424 @@
 | 
			
		|||
/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/
 | 
			
		||||
// Inserts shortcut buttons after all of the following:
 | 
			
		||||
//     <input type="text" class="vDateField">
 | 
			
		||||
//     <input type="text" class="vTimeField">
 | 
			
		||||
'use strict';
 | 
			
		||||
{
 | 
			
		||||
    const DateTimeShortcuts = {
 | 
			
		||||
        calendars: [],
 | 
			
		||||
        calendarInputs: [],
 | 
			
		||||
        clockInputs: [],
 | 
			
		||||
        clockHours: {
 | 
			
		||||
            default_: [
 | 
			
		||||
                [gettext_noop('Now'), -1],
 | 
			
		||||
                [gettext_noop('Midnight'), 0],
 | 
			
		||||
                [gettext_noop('6 a.m.'), 6],
 | 
			
		||||
                [gettext_noop('Noon'), 12],
 | 
			
		||||
                [gettext_noop('6 p.m.'), 18]
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        dismissClockFunc: [],
 | 
			
		||||
        dismissCalendarFunc: [],
 | 
			
		||||
        calendarDivName1: 'calendarbox', // name of calendar <div> that gets toggled
 | 
			
		||||
        calendarDivName2: 'calendarin', // name of <div> that contains calendar
 | 
			
		||||
        calendarLinkName: 'calendarlink', // name of the link that is used to toggle
 | 
			
		||||
        clockDivName: 'clockbox', // name of clock <div> that gets toggled
 | 
			
		||||
        clockLinkName: 'clocklink', // name of the link that is used to toggle
 | 
			
		||||
        shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
 | 
			
		||||
        timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
 | 
			
		||||
        timezoneOffset: 0,
 | 
			
		||||
        init: function(selector) {
 | 
			
		||||
            selector = selector || document;
 | 
			
		||||
            const serverOffset = document.body.dataset.adminUtcOffset;
 | 
			
		||||
            if (serverOffset) {
 | 
			
		||||
                const localOffset = new Date().getTimezoneOffset() * -60;
 | 
			
		||||
                DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const inp of selector.getElementsByTagName('input')) {
 | 
			
		||||
                if (inp.type === 'text' && inp.classList.contains('vTimeField')) {
 | 
			
		||||
                    DateTimeShortcuts.addClock(inp);
 | 
			
		||||
                    DateTimeShortcuts.addTimezoneWarning(inp);
 | 
			
		||||
                }
 | 
			
		||||
                else if (inp.type === 'text' && inp.classList.contains('vDateField')) {
 | 
			
		||||
                    DateTimeShortcuts.addCalendar(inp);
 | 
			
		||||
                    DateTimeShortcuts.addTimezoneWarning(inp);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        // Return the current time while accounting for the server timezone.
 | 
			
		||||
        now: function() {
 | 
			
		||||
            const serverOffset = document.body.dataset.adminUtcOffset;
 | 
			
		||||
            if (serverOffset) {
 | 
			
		||||
                const localNow = new Date();
 | 
			
		||||
                const localOffset = localNow.getTimezoneOffset() * -60;
 | 
			
		||||
                localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
 | 
			
		||||
                return localNow;
 | 
			
		||||
            } else {
 | 
			
		||||
                return new Date();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        // Add a warning when the time zone in the browser and backend do not match.
 | 
			
		||||
        addTimezoneWarning: function(inp) {
 | 
			
		||||
            const warningClass = DateTimeShortcuts.timezoneWarningClass;
 | 
			
		||||
            let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;
 | 
			
		||||
 | 
			
		||||
            // Only warn if there is a time zone mismatch.
 | 
			
		||||
            if (!timezoneOffset) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if warning is already there.
 | 
			
		||||
            if (inp.parentNode.querySelectorAll('.' + warningClass).length) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let message;
 | 
			
		||||
            if (timezoneOffset > 0) {
 | 
			
		||||
                message = ngettext(
 | 
			
		||||
                    'Note: You are %s hour ahead of server time.',
 | 
			
		||||
                    'Note: You are %s hours ahead of server time.',
 | 
			
		||||
                    timezoneOffset
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                timezoneOffset *= -1;
 | 
			
		||||
                message = ngettext(
 | 
			
		||||
                    'Note: You are %s hour behind server time.',
 | 
			
		||||
                    'Note: You are %s hours behind server time.',
 | 
			
		||||
                    timezoneOffset
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            message = interpolate(message, [timezoneOffset]);
 | 
			
		||||
 | 
			
		||||
            const warning = document.createElement('span');
 | 
			
		||||
            warning.className = warningClass;
 | 
			
		||||
            warning.textContent = message;
 | 
			
		||||
            inp.parentNode.appendChild(document.createElement('br'));
 | 
			
		||||
            inp.parentNode.appendChild(warning);
 | 
			
		||||
        },
 | 
			
		||||
        // Add clock widget to a given field
 | 
			
		||||
        addClock: function(inp) {
 | 
			
		||||
            const num = DateTimeShortcuts.clockInputs.length;
 | 
			
		||||
            DateTimeShortcuts.clockInputs[num] = inp;
 | 
			
		||||
            DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; };
 | 
			
		||||
 | 
			
		||||
            // Shortcut links (clock icon and "Now" link)
 | 
			
		||||
            const shortcuts_span = document.createElement('span');
 | 
			
		||||
            shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
 | 
			
		||||
            inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
 | 
			
		||||
            const now_link = document.createElement('a');
 | 
			
		||||
            now_link.href = "#";
 | 
			
		||||
            now_link.textContent = gettext('Now');
 | 
			
		||||
            now_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.handleClockQuicklink(num, -1);
 | 
			
		||||
            });
 | 
			
		||||
            const clock_link = document.createElement('a');
 | 
			
		||||
            clock_link.href = '#';
 | 
			
		||||
            clock_link.id = DateTimeShortcuts.clockLinkName + num;
 | 
			
		||||
            clock_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                // avoid triggering the document click handler to dismiss the clock
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                DateTimeShortcuts.openClock(num);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            quickElement(
 | 
			
		||||
                'span', clock_link, '',
 | 
			
		||||
                'class', 'clock-icon',
 | 
			
		||||
                'title', gettext('Choose a Time')
 | 
			
		||||
            );
 | 
			
		||||
            shortcuts_span.appendChild(document.createTextNode('\u00A0'));
 | 
			
		||||
            shortcuts_span.appendChild(now_link);
 | 
			
		||||
            shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
 | 
			
		||||
            shortcuts_span.appendChild(clock_link);
 | 
			
		||||
 | 
			
		||||
            // Create clock link div
 | 
			
		||||
            //
 | 
			
		||||
            // Markup looks like:
 | 
			
		||||
            // <div id="clockbox1" class="clockbox module">
 | 
			
		||||
            //     <h2>Choose a time</h2>
 | 
			
		||||
            //     <ul class="timelist">
 | 
			
		||||
            //         <li><a href="#">Now</a></li>
 | 
			
		||||
            //         <li><a href="#">Midnight</a></li>
 | 
			
		||||
            //         <li><a href="#">6 a.m.</a></li>
 | 
			
		||||
            //         <li><a href="#">Noon</a></li>
 | 
			
		||||
            //         <li><a href="#">6 p.m.</a></li>
 | 
			
		||||
            //     </ul>
 | 
			
		||||
            //     <p class="calendar-cancel"><a href="#">Cancel</a></p>
 | 
			
		||||
            // </div>
 | 
			
		||||
 | 
			
		||||
            const clock_box = document.createElement('div');
 | 
			
		||||
            clock_box.style.display = 'none';
 | 
			
		||||
            clock_box.className = 'clockbox module';
 | 
			
		||||
            clock_box.id = DateTimeShortcuts.clockDivName + num;
 | 
			
		||||
            document.body.appendChild(clock_box);
 | 
			
		||||
            // cpa: Make it work with bootstrap
 | 
			
		||||
            clock_box.style.position = clock_box.closest('body').classList.contains('modal-open') ? 'fixed' : 'absolute';
 | 
			
		||||
            clock_box.addEventListener('click', function(e) { e.stopPropagation(); });
 | 
			
		||||
 | 
			
		||||
            quickElement('h2', clock_box, gettext('Choose a time'));
 | 
			
		||||
            const time_list = quickElement('ul', clock_box);
 | 
			
		||||
            time_list.className = 'timelist';
 | 
			
		||||
            // The list of choices can be overridden in JavaScript like this:
 | 
			
		||||
            // DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]];
 | 
			
		||||
            // where name is the name attribute of the <input>.
 | 
			
		||||
            const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name;
 | 
			
		||||
            DateTimeShortcuts.clockHours[name].forEach(function(element) {
 | 
			
		||||
                const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'href', '#');
 | 
			
		||||
                time_link.addEventListener('click', function(e) {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    DateTimeShortcuts.handleClockQuicklink(num, element[1]);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const cancel_p = quickElement('p', clock_box);
 | 
			
		||||
            cancel_p.className = 'calendar-cancel';
 | 
			
		||||
            const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
 | 
			
		||||
            cancel_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.dismissClock(num);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            document.addEventListener('keyup', function(event) {
 | 
			
		||||
                if (event.which === 27) {
 | 
			
		||||
                    // ESC key closes popup
 | 
			
		||||
                    DateTimeShortcuts.dismissClock(num);
 | 
			
		||||
                    event.preventDefault();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        openClock: function(num) {
 | 
			
		||||
            const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num);
 | 
			
		||||
            const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num);
 | 
			
		||||
 | 
			
		||||
            // Recalculate the clockbox position
 | 
			
		||||
            // is it left-to-right or right-to-left layout ?
 | 
			
		||||
            if (window.getComputedStyle(document.body).direction !== 'rtl') {
 | 
			
		||||
                clock_box.style.left = findPosX(clock_link) + 17 + 'px';
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                // since style's width is in em, it'd be tough to calculate
 | 
			
		||||
                // px value of it. let's use an estimated px for now
 | 
			
		||||
                clock_box.style.left = findPosX(clock_link) - 110 + 'px';
 | 
			
		||||
            }
 | 
			
		||||
            clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px';
 | 
			
		||||
 | 
			
		||||
            // Show the clock box
 | 
			
		||||
            clock_box.style.display = 'block';
 | 
			
		||||
            document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
 | 
			
		||||
        },
 | 
			
		||||
        dismissClock: function(num) {
 | 
			
		||||
            document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
 | 
			
		||||
            document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
 | 
			
		||||
        },
 | 
			
		||||
        handleClockQuicklink: function(num, val) {
 | 
			
		||||
            let d;
 | 
			
		||||
            if (val === -1) {
 | 
			
		||||
                d = DateTimeShortcuts.now();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                d = new Date(1970, 1, 1, val, 0, 0, 0);
 | 
			
		||||
            }
 | 
			
		||||
            DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
 | 
			
		||||
            DateTimeShortcuts.clockInputs[num].focus();
 | 
			
		||||
            DateTimeShortcuts.dismissClock(num);
 | 
			
		||||
            // Added by cpa:
 | 
			
		||||
            DateTimeShortcuts.clockInputs[num].dispatchEvent(new Event('change'));
 | 
			
		||||
        },
 | 
			
		||||
        // Add calendar widget to a given field.
 | 
			
		||||
        addCalendar: function(inp) {
 | 
			
		||||
            const num = DateTimeShortcuts.calendars.length;
 | 
			
		||||
 | 
			
		||||
            DateTimeShortcuts.calendarInputs[num] = inp;
 | 
			
		||||
            DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; };
 | 
			
		||||
 | 
			
		||||
            // Shortcut links (calendar icon and "Today" link)
 | 
			
		||||
            const shortcuts_span = document.createElement('span');
 | 
			
		||||
            shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
 | 
			
		||||
            inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
 | 
			
		||||
            const today_link = document.createElement('a');
 | 
			
		||||
            today_link.href = '#';
 | 
			
		||||
            today_link.appendChild(document.createTextNode(gettext('Today')));
 | 
			
		||||
            today_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.handleCalendarQuickLink(num, 0);
 | 
			
		||||
            });
 | 
			
		||||
            const cal_link = document.createElement('a');
 | 
			
		||||
            cal_link.href = '#';
 | 
			
		||||
            cal_link.id = DateTimeShortcuts.calendarLinkName + num;
 | 
			
		||||
            cal_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                // avoid triggering the document click handler to dismiss the calendar
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                DateTimeShortcuts.openCalendar(num);
 | 
			
		||||
            });
 | 
			
		||||
            quickElement(
 | 
			
		||||
                'span', cal_link, '',
 | 
			
		||||
                'class', 'date-icon',
 | 
			
		||||
                'title', gettext('Choose a Date')
 | 
			
		||||
            );
 | 
			
		||||
            shortcuts_span.appendChild(document.createTextNode('\u00A0'));
 | 
			
		||||
            // CUSTOMIZED: Today link removed.
 | 
			
		||||
            //shortcuts_span.appendChild(today_link);
 | 
			
		||||
            //shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
 | 
			
		||||
            shortcuts_span.appendChild(cal_link);
 | 
			
		||||
 | 
			
		||||
            // Create calendarbox div.
 | 
			
		||||
            //
 | 
			
		||||
            // Markup looks like:
 | 
			
		||||
            //
 | 
			
		||||
            // <div id="calendarbox3" class="calendarbox module">
 | 
			
		||||
            //     <h2>
 | 
			
		||||
            //           <a href="#" class="link-previous">‹</a>
 | 
			
		||||
            //           <a href="#" class="link-next">›</a> February 2003
 | 
			
		||||
            //     </h2>
 | 
			
		||||
            //     <div class="calendar" id="calendarin3">
 | 
			
		||||
            //         <!-- (cal) -->
 | 
			
		||||
            //     </div>
 | 
			
		||||
            //     <div class="calendar-shortcuts">
 | 
			
		||||
            //          <a href="#">Yesterday</a> | <a href="#">Today</a> | <a href="#">Tomorrow</a>
 | 
			
		||||
            //     </div>
 | 
			
		||||
            //     <p class="calendar-cancel"><a href="#">Cancel</a></p>
 | 
			
		||||
            // </div>
 | 
			
		||||
            const cal_box = document.createElement('div');
 | 
			
		||||
            cal_box.style.display = 'none';
 | 
			
		||||
            cal_box.className = 'calendarbox module';
 | 
			
		||||
            cal_box.id = DateTimeShortcuts.calendarDivName1 + num;
 | 
			
		||||
            document.body.appendChild(cal_box);
 | 
			
		||||
            // cpa: Make it work with bootstrap
 | 
			
		||||
            cal_box.style.position = cal_box.closest('body').classList.contains('modal-open') ? 'fixed' : 'absolute';
 | 
			
		||||
            cal_box.addEventListener('click', function(e) { e.stopPropagation(); });
 | 
			
		||||
 | 
			
		||||
            // next-prev links
 | 
			
		||||
            const cal_nav = quickElement('div', cal_box);
 | 
			
		||||
            const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#');
 | 
			
		||||
            cal_nav_prev.className = 'calendarnav-previous';
 | 
			
		||||
            cal_nav_prev.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.drawPrev(num);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#');
 | 
			
		||||
            cal_nav_next.className = 'calendarnav-next';
 | 
			
		||||
            cal_nav_next.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.drawNext(num);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // main box
 | 
			
		||||
            const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
 | 
			
		||||
            cal_main.className = 'calendar';
 | 
			
		||||
            DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
 | 
			
		||||
            DateTimeShortcuts.calendars[num].drawCurrent();
 | 
			
		||||
 | 
			
		||||
            // calendar shortcuts
 | 
			
		||||
            const shortcuts = quickElement('div', cal_box);
 | 
			
		||||
            shortcuts.className = 'calendar-shortcuts';
 | 
			
		||||
            let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#');
 | 
			
		||||
            day_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.handleCalendarQuickLink(num, -1);
 | 
			
		||||
            });
 | 
			
		||||
            shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
 | 
			
		||||
            day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#');
 | 
			
		||||
            day_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.handleCalendarQuickLink(num, 0);
 | 
			
		||||
            });
 | 
			
		||||
            shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
 | 
			
		||||
            day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#');
 | 
			
		||||
            day_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.handleCalendarQuickLink(num, +1);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // cancel bar
 | 
			
		||||
            const cancel_p = quickElement('p', cal_box);
 | 
			
		||||
            cancel_p.className = 'calendar-cancel';
 | 
			
		||||
            const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
 | 
			
		||||
            cancel_link.addEventListener('click', function(e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                DateTimeShortcuts.dismissCalendar(num);
 | 
			
		||||
            });
 | 
			
		||||
            document.addEventListener('keyup', function(event) {
 | 
			
		||||
                if (event.which === 27) {
 | 
			
		||||
                    // ESC key closes popup
 | 
			
		||||
                    DateTimeShortcuts.dismissCalendar(num);
 | 
			
		||||
                    event.preventDefault();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        openCalendar: function(num) {
 | 
			
		||||
            const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num);
 | 
			
		||||
            const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num);
 | 
			
		||||
            const inp = DateTimeShortcuts.calendarInputs[num];
 | 
			
		||||
 | 
			
		||||
            // Determine if the current value in the input has a valid date.
 | 
			
		||||
            // If so, draw the calendar with that date's year and month.
 | 
			
		||||
            if (inp.value) {
 | 
			
		||||
                const format = get_format('DATE_INPUT_FORMATS')[0];
 | 
			
		||||
                const selected = inp.value.strptime(format);
 | 
			
		||||
                const year = selected.getUTCFullYear();
 | 
			
		||||
                const month = selected.getUTCMonth() + 1;
 | 
			
		||||
                const re = /\d{4}/;
 | 
			
		||||
                if (re.test(year.toString()) && month >= 1 && month <= 12) {
 | 
			
		||||
                    DateTimeShortcuts.calendars[num].drawDate(month, year, selected);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Recalculate the clockbox position
 | 
			
		||||
            // is it left-to-right or right-to-left layout ?
 | 
			
		||||
            if (window.getComputedStyle(document.body).direction !== 'rtl') {
 | 
			
		||||
                cal_box.style.left = findPosX(cal_link) + 17 + 'px';
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                // since style's width is in em, it'd be tough to calculate
 | 
			
		||||
                // px value of it. let's use an estimated px for now
 | 
			
		||||
                cal_box.style.left = findPosX(cal_link) - 180 + 'px';
 | 
			
		||||
            }
 | 
			
		||||
            cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px';
 | 
			
		||||
 | 
			
		||||
            cal_box.style.display = 'block';
 | 
			
		||||
            document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
 | 
			
		||||
        },
 | 
			
		||||
        dismissCalendar: function(num) {
 | 
			
		||||
            document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
 | 
			
		||||
            document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
 | 
			
		||||
        },
 | 
			
		||||
        drawPrev: function(num) {
 | 
			
		||||
            DateTimeShortcuts.calendars[num].drawPreviousMonth();
 | 
			
		||||
        },
 | 
			
		||||
        drawNext: function(num) {
 | 
			
		||||
            DateTimeShortcuts.calendars[num].drawNextMonth();
 | 
			
		||||
        },
 | 
			
		||||
        handleCalendarCallback: function(num) {
 | 
			
		||||
            let format = get_format('DATE_INPUT_FORMATS')[0];
 | 
			
		||||
            // the format needs to be escaped a little
 | 
			
		||||
            format = format.replace('\\', '\\\\')
 | 
			
		||||
                .replace('\r', '\\r')
 | 
			
		||||
                .replace('\n', '\\n')
 | 
			
		||||
                .replace('\t', '\\t')
 | 
			
		||||
                .replace("'", "\\'");
 | 
			
		||||
            return function(y, m, d) {
 | 
			
		||||
                DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
 | 
			
		||||
                DateTimeShortcuts.calendarInputs[num].focus();
 | 
			
		||||
                document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
 | 
			
		||||
                // Added by cpa:
 | 
			
		||||
                DateTimeShortcuts.calendarInputs[num].dispatchEvent(new Event('change'));
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        handleCalendarQuickLink: function(num, offset) {
 | 
			
		||||
            const d = DateTimeShortcuts.now();
 | 
			
		||||
            d.setDate(d.getDate() + offset);
 | 
			
		||||
            DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
 | 
			
		||||
            DateTimeShortcuts.calendarInputs[num].focus();
 | 
			
		||||
            DateTimeShortcuts.dismissCalendar(num);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // CUSTOMIZED: We call it ourselves.
 | 
			
		||||
    //window.addEventListener('load', DateTimeShortcuts.init);
 | 
			
		||||
    window.DateTimeShortcuts = DateTimeShortcuts;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								aemo/static/js/autocomplete.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).autocomplete=t()}(this,(function(){"use strict";return function(e){var t,n,o=document,i=o.createElement("div"),r=i.style,f=navigator.userAgent,l=-1!==f.indexOf("Firefox")&&-1!==f.indexOf("Mobile"),u=e.debounceWaitMs||0,a=e.preventSubmit||!1,s=e.disableAutoSelect||!1,d=l?"input":"keyup",c=[],p="",v=2,m=e.showOnFocus,g=0;if(void 0!==e.minLength&&(v=e.minLength),!e.input)throw new Error("input undefined");var h=e.input;function E(){n&&window.clearTimeout(n)}function w(){return!!i.parentNode}function L(){var e;g++,c=[],p="",t=void 0,(e=i.parentNode)&&e.removeChild(i)}function b(){for(;i.firstChild;)i.removeChild(i.firstChild);var n=function(e,t){var n=o.createElement("div");return n.textContent=e.label||"",n};e.render&&(n=e.render);var f=function(e,t){var n=o.createElement("div");return n.textContent=e,n};e.renderGroup&&(f=e.renderGroup);var l=o.createDocumentFragment(),u="#9?$";if(c.forEach((function(o){if(o.group&&o.group!==u){u=o.group;var i=f(o.group,p);i&&(i.className+=" group",l.appendChild(i))}var r=n(o,p);r&&(r.addEventListener("click",(function(t){e.onSelect(o,h),L(),t.preventDefault(),t.stopPropagation()})),o===t&&(r.className+=" selected"),l.appendChild(r))})),i.appendChild(l),c.length<1){if(!e.emptyMsg)return void L();var a=o.createElement("div");a.className="empty",a.textContent=e.emptyMsg,i.appendChild(a)}i.parentNode||o.body.appendChild(i),function(){if(w()){r.height="auto",r.width=h.offsetWidth+"px";var t,n=0;f(),f(),e.customize&&t&&e.customize(h,t,i,n)}function f(){var e=o.documentElement,i=e.clientTop||o.body.clientTop||0,f=e.clientLeft||o.body.clientLeft||0,l=window.pageYOffset||e.scrollTop,u=window.pageXOffset||e.scrollLeft,a=(t=h.getBoundingClientRect()).top+h.offsetHeight+l-i,s=t.left+u-f;r.top=a+"px",r.left=s+"px",(n=window.innerHeight-(t.top+h.offsetHeight))<0&&(n=0),r.top=a+"px",r.bottom="",r.left=s+"px",r.maxHeight=n+"px"}}(),function(){var e=i.getElementsByClassName("selected");if(e.length>0){var t=e[0],n=t.previousElementSibling;if(n&&-1!==n.className.indexOf("group")&&!n.previousElementSibling&&(t=n),t.offsetTop<i.scrollTop)i.scrollTop=t.offsetTop;else{var o=t.offsetTop+t.offsetHeight,r=i.scrollTop+i.offsetHeight;o>r&&(i.scrollTop+=o-r)}}}()}function y(){w()&&b()}function x(){y()}function C(e){e.target!==i?y():e.preventDefault()}function T(e){for(var t=e.which||e.keyCode||0,n=0,o=[38,13,27,39,37,16,17,18,20,91,9];n<o.length;n++){if(t===o[n])return}t>=112&&t<=123||40===t&&w()||D(0)}function N(n){var o=n.which||n.keyCode||0;if(38===o||40===o||27===o){var i=w();if(27===o)L();else{if(!i||c.length<1)return;38===o?function(){if(c.length<1)t=void 0;else if(t===c[0])t=c[c.length-1];else for(var e=c.length-1;e>0;e--)if(t===c[e]||1===e){t=c[e-1];break}}():function(){if(c.length<1&&(t=void 0),t&&t!==c[c.length-1]){for(var e=0;e<c.length-1;e++)if(t===c[e]){t=c[e+1];break}}else t=c[0]}(),b()}return n.preventDefault(),void(i&&n.stopPropagation())}13===o&&(t&&(e.onSelect(t,h),L()),a&&n.preventDefault())}function k(){m&&D(1)}function D(o){var i=++g,r=h.value;r.length>=v||1===o?(E(),n=window.setTimeout((function(){e.fetch(r,(function(e){g===i&&e&&(p=r,t=(c=e).length<1||s?void 0:c[0],b())}),o)}),0===o?u:0)):L()}function H(){setTimeout((function(){o.activeElement!==h&&L()}),200)}return i.className="autocomplete "+(e.className||""),r.position="absolute",i.addEventListener("mousedown",(function(e){e.stopPropagation(),e.preventDefault()})),i.addEventListener("focus",(function(){return h.focus()})),h.addEventListener("keydown",N),h.addEventListener(d,T),h.addEventListener("blur",H),h.addEventListener("focus",k),window.addEventListener("resize",x),o.addEventListener("scroll",C,!0),{destroy:function(){h.removeEventListener("focus",k),h.removeEventListener("keydown",N),h.removeEventListener(d,T),h.removeEventListener("blur",H),window.removeEventListener("resize",x),o.removeEventListener("scroll",C,!0),E(),L()}}}}));
 | 
			
		||||
							
								
								
									
										6
									
								
								aemo/static/js/autosize.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/*!
 | 
			
		||||
	autosize 4.0.2
 | 
			
		||||
	license: MIT
 | 
			
		||||
	http://www.jacklmoore.com/autosize
 | 
			
		||||
*/
 | 
			
		||||
!function(e,t){if("function"==typeof define&&define.amd)define(["module","exports"],t);else if("undefined"!=typeof exports)t(module,exports);else{var n={exports:{}};t(n,n.exports),e.autosize=n.exports}}(this,function(e,t){"use strict";var n,o,p="function"==typeof Map?new Map:(n=[],o=[],{has:function(e){return-1<n.indexOf(e)},get:function(e){return o[n.indexOf(e)]},set:function(e,t){-1===n.indexOf(e)&&(n.push(e),o.push(t))},delete:function(e){var t=n.indexOf(e);-1<t&&(n.splice(t,1),o.splice(t,1))}}),c=function(e){return new Event(e,{bubbles:!0})};try{new Event("test")}catch(e){c=function(e){var t=document.createEvent("Event");return t.initEvent(e,!0,!1),t}}function r(r){if(r&&r.nodeName&&"TEXTAREA"===r.nodeName&&!p.has(r)){var e,n=null,o=null,i=null,d=function(){r.clientWidth!==o&&a()},l=function(t){window.removeEventListener("resize",d,!1),r.removeEventListener("input",a,!1),r.removeEventListener("keyup",a,!1),r.removeEventListener("autosize:destroy",l,!1),r.removeEventListener("autosize:update",a,!1),Object.keys(t).forEach(function(e){r.style[e]=t[e]}),p.delete(r)}.bind(r,{height:r.style.height,resize:r.style.resize,overflowY:r.style.overflowY,overflowX:r.style.overflowX,wordWrap:r.style.wordWrap});r.addEventListener("autosize:destroy",l,!1),"onpropertychange"in r&&"oninput"in r&&r.addEventListener("keyup",a,!1),window.addEventListener("resize",d,!1),r.addEventListener("input",a,!1),r.addEventListener("autosize:update",a,!1),r.style.overflowX="hidden",r.style.wordWrap="break-word",p.set(r,{destroy:l,update:a}),"vertical"===(e=window.getComputedStyle(r,null)).resize?r.style.resize="none":"both"===e.resize&&(r.style.resize="horizontal"),n="content-box"===e.boxSizing?-(parseFloat(e.paddingTop)+parseFloat(e.paddingBottom)):parseFloat(e.borderTopWidth)+parseFloat(e.borderBottomWidth),isNaN(n)&&(n=0),a()}function s(e){var t=r.style.width;r.style.width="0px",r.offsetWidth,r.style.width=t,r.style.overflowY=e}function u(){if(0!==r.scrollHeight){var e=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push({node:e.parentNode,scrollTop:e.parentNode.scrollTop}),e=e.parentNode;return t}(r),t=document.documentElement&&document.documentElement.scrollTop;r.style.height="",r.style.height=r.scrollHeight+n+"px",o=r.clientWidth,e.forEach(function(e){e.node.scrollTop=e.scrollTop}),t&&(document.documentElement.scrollTop=t)}}function a(){u();var e=Math.round(parseFloat(r.style.height)),t=window.getComputedStyle(r,null),n="content-box"===t.boxSizing?Math.round(parseFloat(t.height)):r.offsetHeight;if(n<e?"hidden"===t.overflowY&&(s("scroll"),u(),n="content-box"===t.boxSizing?Math.round(parseFloat(window.getComputedStyle(r,null).height)):r.offsetHeight):"hidden"!==t.overflowY&&(s("hidden"),u(),n="content-box"===t.boxSizing?Math.round(parseFloat(window.getComputedStyle(r,null).height)):r.offsetHeight),i!==n){i=n;var o=c("autosize:resized");try{r.dispatchEvent(o)}catch(e){}}}}function i(e){var t=p.get(e);t&&t.destroy()}function d(e){var t=p.get(e);t&&t.update()}var l=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?((l=function(e){return e}).destroy=function(e){return e},l.update=function(e){return e}):((l=function(e,t){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return r(e)}),e}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],i),e},l.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],d),e}),t.default=l,e.exports=t.default});
 | 
			
		||||
							
								
								
									
										241
									
								
								aemo/static/js/main.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,241 @@
 | 
			
		|||
function htmlToElem(html) {
 | 
			
		||||
    let temp = document.createElement('template');
 | 
			
		||||
    html = html.trim(); // Never return a space text node as a result
 | 
			
		||||
    temp.innerHTML = html;
 | 
			
		||||
    return temp.content.firstChild;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dateFormat(input) {
 | 
			
		||||
    if (input) {
 | 
			
		||||
        var dt = new Date(input);
 | 
			
		||||
        return dt.toLocaleDateString("fr-CH");
 | 
			
		||||
    }
 | 
			
		||||
    return '-';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var changed = false;
 | 
			
		||||
 | 
			
		||||
function check_changed(ev) {
 | 
			
		||||
    if (changed) {
 | 
			
		||||
        alert("Vos données n'ont pas été sauvegardées !");
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggle_read_more(ev) {
 | 
			
		||||
    ev.preventDefault();
 | 
			
		||||
    const link = ev.target;
 | 
			
		||||
    link.innerHTML = (link.innerHTML == 'Afficher la suite') ? 'Réduire' : 'Afficher la suite';
 | 
			
		||||
    link.parentNode.querySelector('.long').classList.toggle('hidden');
 | 
			
		||||
    link.parentNode.querySelector('.short').classList.toggle('hidden');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showImage(ev) {
 | 
			
		||||
    var modal = document.getElementById('imgModal'); /* Present in base.html */
 | 
			
		||||
    var imgTag = document.getElementById("img01");
 | 
			
		||||
    var captionText = document.getElementById("caption");
 | 
			
		||||
    ev.preventDefault();
 | 
			
		||||
    modal.style.display = "block";
 | 
			
		||||
    imgTag.src = this.href;
 | 
			
		||||
    captionText.innerHTML = this.textContent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setConfirmHandlers(section) {
 | 
			
		||||
    if (typeof section === 'undefined') section = document;
 | 
			
		||||
    const selector = section.querySelectorAll(".btn-danger, .confirm");
 | 
			
		||||
    selector.forEach(button => {
 | 
			
		||||
        button.addEventListener('click', ev => {
 | 
			
		||||
            if (button.dataset.confirm) {
 | 
			
		||||
                ev.preventDefault();
 | 
			
		||||
                if (!confirm(button.dataset.confirm)) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (button.getAttribute('formaction')) button.form.action = button.formAction;
 | 
			
		||||
                    button.form.submit();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openFormInModal(url) {
 | 
			
		||||
    const popup = document.querySelector('#popup0');
 | 
			
		||||
 | 
			
		||||
    function setupForm() {
 | 
			
		||||
        DateTimeShortcuts.init(popup);
 | 
			
		||||
        setConfirmHandlers(popup);
 | 
			
		||||
        document.querySelectorAll("#popup0 form").forEach((form) => {
 | 
			
		||||
            form.addEventListener('submit', (ev) => {
 | 
			
		||||
                ev.preventDefault();
 | 
			
		||||
                const form = ev.target;
 | 
			
		||||
                const formData = new FormData(form);
 | 
			
		||||
                // GET/POST with fetch
 | 
			
		||||
                let url = form.action;
 | 
			
		||||
                let params = {method: form.method};
 | 
			
		||||
                if (form.method == 'post') {
 | 
			
		||||
                    params['body'] = formData;
 | 
			
		||||
                }
 | 
			
		||||
                fetch(url, params).then(res => {
 | 
			
		||||
                    if (res.redirected) {
 | 
			
		||||
                        window.location.reload(true);
 | 
			
		||||
                        return '';
 | 
			
		||||
                    }
 | 
			
		||||
                    return res.text();
 | 
			
		||||
                }).then(html => {
 | 
			
		||||
                    if (html) {
 | 
			
		||||
                        // Redisplay form with errors or display confirm page
 | 
			
		||||
                        popup.querySelector('.modal-body').innerHTML = html;
 | 
			
		||||
                        setupForm();
 | 
			
		||||
                    }
 | 
			
		||||
                }).catch((err) => {
 | 
			
		||||
                    console.log(err);
 | 
			
		||||
                    alert("Désolé, une erreur s'est produite");
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return fetch(url).then(res => res.text()).then(html => {
 | 
			
		||||
        const modal = new bootstrap.Modal(popup);
 | 
			
		||||
        popup.querySelector('.modal-body').innerHTML = html;
 | 
			
		||||
        modal.show();
 | 
			
		||||
        setupForm();
 | 
			
		||||
        return popup;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetForm(ev) {
 | 
			
		||||
    const form = ev.target.closest('form');
 | 
			
		||||
    Array.from(form.elements).forEach(el => { el.value = ''; });
 | 
			
		||||
    form.submit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function submitFilterForm(form) {
 | 
			
		||||
    let action = form.action || '.';
 | 
			
		||||
    const formData = new FormData(form);
 | 
			
		||||
    action += '?' + new URLSearchParams(formData).toString();
 | 
			
		||||
    fetch(action, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        headers: {'X-Requested-With': 'Fetch'}
 | 
			
		||||
    }).then(response => { return response.text(); }).then(output => {
 | 
			
		||||
        const parser = new DOMParser();
 | 
			
		||||
        const doc = parser.parseFromString(output, "text/html");
 | 
			
		||||
        const tableBody = doc.querySelector('.table-sortable tbody');
 | 
			
		||||
        document.querySelector('.table-sortable tbody').replaceWith(tableBody);
 | 
			
		||||
        const pagination = doc.querySelector('#pagination');
 | 
			
		||||
        document.querySelector('#pagination').replaceWith(pagination);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortColumn(ev) {
 | 
			
		||||
    const header = ev.target;
 | 
			
		||||
    const form = document.querySelector('.selection_form');
 | 
			
		||||
    const desc = header.classList.contains('asc');
 | 
			
		||||
    form.elements['sort_by'].value = (desc ? '-' : '') + header.dataset.col;
 | 
			
		||||
    submitFilterForm(form);
 | 
			
		||||
    // Reset colums classes
 | 
			
		||||
    Array.from(header.parentNode.children).forEach(head => {
 | 
			
		||||
        head.classList.remove('desc');
 | 
			
		||||
        head.classList.remove('asc');
 | 
			
		||||
    });
 | 
			
		||||
    header.classList.add(desc ? 'desc': 'asc');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
    if (typeof DateTimeShortcuts !== 'undefined') {
 | 
			
		||||
        DateTimeShortcuts.init();
 | 
			
		||||
    }
 | 
			
		||||
    autosize(document.querySelectorAll('textarea'));
 | 
			
		||||
    $("form").not(".selection_form").not("[method='get']").change(function() {
 | 
			
		||||
        changed = true;
 | 
			
		||||
    });
 | 
			
		||||
    $("#menu_crne, #aemo_buttons, #aemo_print_buttons").click
 | 
			
		||||
    (check_changed);
 | 
			
		||||
    setConfirmHandlers();
 | 
			
		||||
    $("table.sortable").each(function(idx) {
 | 
			
		||||
        new Tablesort(this);
 | 
			
		||||
    });
 | 
			
		||||
    $(".table-sortable th").click(sortColumn);
 | 
			
		||||
    $('a.read_more').click(toggle_read_more);
 | 
			
		||||
 | 
			
		||||
    // Attachment images
 | 
			
		||||
    $('a.image').click(showImage);
 | 
			
		||||
    $('#modalClose').click(function(ev) {$(this).closest('div').hide(); });
 | 
			
		||||
 | 
			
		||||
    $('input[name=dh_debut_1]').change(function(){
 | 
			
		||||
        var dateFin = $('input[name=dh_fin_0]');
 | 
			
		||||
        if (dateFin.val() == '') {
 | 
			
		||||
            // Copier date de début vers date de fin
 | 
			
		||||
            dateFin.val($('input[name=dh_debut_0]').val());
 | 
			
		||||
        }
 | 
			
		||||
        var heureFin = $('input[name=dh_fin_1]');
 | 
			
		||||
        if (heureFin.val() == '') {
 | 
			
		||||
            // Mettre heure de fin 1 heure après heure de début
 | 
			
		||||
            var dh = $('input[name=dh_debut_1]').val().split(":");
 | 
			
		||||
            h = parseInt(dh[0]) + 1;
 | 
			
		||||
            heureFin.val(h.toString() + ":" + dh[1]);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    document.querySelectorAll('.immediate-submit').forEach(immediate => {
 | 
			
		||||
        //immediate.addEventListener('click', immediateSubmit);
 | 
			
		||||
        // With screen readers, users don't click but change the radio value
 | 
			
		||||
        immediate.addEventListener('change', (ev) => {
 | 
			
		||||
            ev.target.form.submit()
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(".js-add, .js-edit").click(function(e) {
 | 
			
		||||
        const url = this.dataset.url || this.href;
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        openFormInModal(url);
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Activation des tooltips
 | 
			
		||||
    const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
 | 
			
		||||
    tooltipTriggerList.map((el) => new bootstrap.Tooltip(el));
 | 
			
		||||
 | 
			
		||||
    $('#reset-button').click(resetForm);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function debounce(func, timeout=300) {
 | 
			
		||||
  let timer;
 | 
			
		||||
  return (...args) => {
 | 
			
		||||
    if (timeout <= 0) func.apply(this, args);
 | 
			
		||||
    else {
 | 
			
		||||
      clearTimeout(timer);
 | 
			
		||||
      timer = setTimeout(() => { func.apply(this, args); }, timeout);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function archiveFamilies(ev) {
 | 
			
		||||
    const btn = ev.target;
 | 
			
		||||
    const archiveUrl = btn.dataset.archiveurl;
 | 
			
		||||
    const counterSpan = document.querySelector('#archive-counter');
 | 
			
		||||
    const totalSpan = document.querySelector('#archive-total');
 | 
			
		||||
 | 
			
		||||
    bootstrap.Modal.getInstance(document.getElementById('archiveModal')).hide();
 | 
			
		||||
    document.getElementById('archive-message').removeAttribute('hidden');
 | 
			
		||||
    const resp = await fetch(btn.dataset.getarchivableurl);
 | 
			
		||||
    const data = await resp.json();
 | 
			
		||||
    let compteur = 0;
 | 
			
		||||
    totalSpan.textContent = data.length;
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
 | 
			
		||||
    for (let i = 0; i < data.length; i++) {
 | 
			
		||||
        const archResp = await fetch(
 | 
			
		||||
            archiveUrl.replace('999', data[i]),
 | 
			
		||||
            {method: 'POST', headers: {'X-Requested-With': 'Fetch'}, body: formData}
 | 
			
		||||
        );
 | 
			
		||||
        const jsonResp = await archResp.json();
 | 
			
		||||
        compteur += 1;
 | 
			
		||||
        counterSpan.textContent = compteur;
 | 
			
		||||
    }
 | 
			
		||||
    const messageP = document.querySelector("#archive-message p");
 | 
			
		||||
    messageP.textContent = `${compteur} dossiers ont été archivés avec succès.`;
 | 
			
		||||
    messageP.classList.remove('alert-danger');
 | 
			
		||||
    messageP.classList.add('alert-success');
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								aemo/static/js/sorts/tablesort.date.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/*!
 | 
			
		||||
 * tablesort v5.1.0 (2020-01-22)
 | 
			
		||||
 * http://tristen.ca/tablesort/demo/
 | 
			
		||||
 * Copyright (c) 2020 ; Licensed MIT
 | 
			
		||||
*/
 | 
			
		||||
!function(){var a=function(a){return a=a.replace(/\-/g,"/"),a=a.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/,"$3-$2-$1"),new Date(a).getTime()||-1};Tablesort.extend("date",function(b){return(-1!==b.search(/(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\.?\,?\s*/i)||-1!==b.search(/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/)||-1!==b.search(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i))&&!isNaN(a(b))},function(b,c){return b=b.toLowerCase(),c=c.toLowerCase(),a(c)-a(b)})}();
 | 
			
		||||
							
								
								
									
										6
									
								
								aemo/static/js/sorts/tablesort.number.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/*!
 | 
			
		||||
 * tablesort v5.1.0 (2020-01-22)
 | 
			
		||||
 * http://tristen.ca/tablesort/demo/
 | 
			
		||||
 * Copyright (c) 2020 ; Licensed MIT
 | 
			
		||||
*/
 | 
			
		||||
!function(){var a=function(a){return a.replace(/[^\-?0-9.]/g,"")},b=function(a,b){return a=parseFloat(a),b=parseFloat(b),a=isNaN(a)?0:a,b=isNaN(b)?0:b,a-b};Tablesort.extend("number",function(a){return a.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||a.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||a.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(c,d){return c=a(c),d=a(d),b(d,c)})}();
 | 
			
		||||
							
								
								
									
										6
									
								
								aemo/static/js/tablesort.min.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/*!
 | 
			
		||||
 * tablesort v5.1.0 (2020-01-22)
 | 
			
		||||
 * http://tristen.ca/tablesort/demo/
 | 
			
		||||
 * Copyright (c) 2020 ; Licensed MIT
 | 
			
		||||
*/
 | 
			
		||||
!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a<b?1:-1},f=function(a,b){return[].slice.call(a).find(function(a){return a.getAttribute("data-sort-column-key")===b})},g=function(a,b){return function(c,d){var e=a(c.td,d.td);return 0===e?b?d.index-c.index:c.index-d.index:e}};a.extend=function(a,c,d){if("function"!=typeof c||"function"!=typeof d)throw new Error("Pattern and sort must be a function");b.push({name:a,pattern:c,sort:d})},a.prototype={init:function(a,b){var c,d,e,f,g=this;if(g.table=a,g.thead=!1,g.options=b,a.rows&&a.rows.length>0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e<a.tHead.rows.length;e++)if("thead"===a.tHead.rows[e].getAttribute("data-sort-method")){c=a.tHead.rows[e];break}c||(c=a.tHead.rows[a.tHead.rows.length-1]),g.thead=!0}else c=a.rows[0];if(c){var h=function(){g.current&&g.current!==this&&g.current.removeAttribute("aria-sort"),g.current=this,g.sortTable(this)};for(e=0;e<c.cells.length;e++)f=c.cells[e],f.setAttribute("role","columnheader"),"none"!==f.getAttribute("data-sort-method")&&(f.tabindex=0,f.addEventListener("click",h,!1),null!==f.getAttribute("data-sort-default")&&(d=f));d&&(g.current=d,g.sortTable(d))}},sortTable:function(a,h){var i=this,j=a.getAttribute("data-sort-column-key"),k=a.cellIndex,l=e,m="",n=[],o=i.thead?0:1,p=a.getAttribute("data-sort-method"),q=a.getAttribute("aria-sort");if(i.table.dispatchEvent(c("beforeSort")),h||(q="ascending"===q?"descending":"descending"===q?"ascending":i.options.descending?"descending":"ascending",a.setAttribute("aria-sort",q)),!(i.table.rows.length<2)){if(!p){for(var r;n.length<3&&o<i.table.tBodies[0].rows.length;)r=j?f(i.table.tBodies[0].rows[o].cells,j):i.table.tBodies[0].rows[o].cells[k],m=r?d(r):"",m=m.trim(),m.length>0&&n.push(m),o++;if(!n)return}for(o=0;o<b.length;o++)if(m=b[o],p){if(m.name===p){l=m.sort;break}}else if(n.every(m.pattern)){l=m.sort;break}for(i.col=k,o=0;o<i.table.tBodies.length;o++){var s,t=[],u={},v=0,w=0;if(!(i.table.tBodies[o].rows.length<2)){for(s=0;s<i.table.tBodies[o].rows.length;s++){var r;m=i.table.tBodies[o].rows[s],"none"===m.getAttribute("data-sort-method")?u[v]=m:(r=j?f(m.cells,j):m.cells[i.col],t.push({tr:m,td:r?d(r):"",index:v})),v++}for("descending"===q?t.sort(g(l,!0)):(t.sort(g(l,!1)),t.reverse()),s=0;s<v;s++)u[s]?(m=u[s],w++):m=t[s-w].tr,i.table.tBodies[o].appendChild(m)}}i.table.dispatchEvent(c("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=a:window.Tablesort=a}();
 | 
			
		||||
							
								
								
									
										1
									
								
								aemo/templates/widgets/group_checkbox_option.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{% load my_tags %}{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label }} {% if widget.help %}{% help_tooltip widget.help %}{% endif %}</label>{% endif %}
 | 
			
		||||
							
								
								
									
										2
									
								
								aemo/templates/widgets/input_option.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{# input before label (CSSable) #}
 | 
			
		||||
{% include "django/forms/widgets/input.html" %} {% if widget.wrap_label %}<label {% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{{ widget.label }}</label>{% endif %}
 | 
			
		||||
							
								
								
									
										2
									
								
								aemo/templates/widgets/prestation_radio.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{% load my_tags static %}
 | 
			
		||||
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label }} {% help_tooltip widget.value.instance.actes %}</label>{% endif %}
 | 
			
		||||
							
								
								
									
										0
									
								
								aemo/templatetags/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										351
									
								
								aemo/templatetags/my_tags.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,351 @@
 | 
			
		|||
import os
 | 
			
		||||
from datetime import date, timedelta
 | 
			
		||||
from operator import itemgetter
 | 
			
		||||
 | 
			
		||||
from django import template
 | 
			
		||||
from django.contrib.admin.templatetags.admin_list import _boolean_icon
 | 
			
		||||
from django.template.defaultfilters import linebreaksbr
 | 
			
		||||
from django.templatetags.static import static
 | 
			
		||||
from django.utils.dates import MONTHS
 | 
			
		||||
from django.utils.html import escape, format_html_join, format_html
 | 
			
		||||
from django.utils.safestring import SafeString, mark_safe
 | 
			
		||||
from django.utils.text import Truncator
 | 
			
		||||
 | 
			
		||||
from aemo.utils import format_d_m_Y, format_duree as _format_duree, format_adresse
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.tif', '.tiff', '.gif']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def relative_url(value, field_name, urlencode=None):
 | 
			
		||||
    url = '?{}={}'.format(field_name, value)
 | 
			
		||||
    if urlencode:
 | 
			
		||||
        querystring = urlencode.split('&')
 | 
			
		||||
        filtered_querystring = filter(lambda p: p.split('=')[0] != field_name, querystring)
 | 
			
		||||
        encoded_querystring = '&'.join(filtered_querystring)
 | 
			
		||||
        url = '{}&{}'.format(url, encoded_querystring)
 | 
			
		||||
    return url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def get_verbose_field_name(instance, field_name):
 | 
			
		||||
    """
 | 
			
		||||
    Returns verbose_name for a field.
 | 
			
		||||
    """
 | 
			
		||||
    return instance._meta.get_field(field_name).verbose_name.capitalize()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def get_field_value(instance, field_name):
 | 
			
		||||
    value = getattr(instance, field_name)
 | 
			
		||||
    if isinstance(value, str):
 | 
			
		||||
        return mark_safe(value)
 | 
			
		||||
    elif hasattr(value, 'all'):
 | 
			
		||||
        return mark_safe("<br>".join([str(v) for v in value.all()]))
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def help_tooltip(text):
 | 
			
		||||
    template = (
 | 
			
		||||
        '<span class="help" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" '
 | 
			
		||||
        'title="{text}"><img src="{icon}"></span>'
 | 
			
		||||
    )
 | 
			
		||||
    return format_html(template, text=linebreaksbr(escape(text)), icon=static("img/help.png"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def boolean_icon(val):
 | 
			
		||||
    return _boolean_icon(val)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def get_item(obj, key):
 | 
			
		||||
    try:
 | 
			
		||||
        return obj.get(key) if obj is not None else obj
 | 
			
		||||
    except Exception:
 | 
			
		||||
        raise TypeError(f"Unable to get key '{key}' from obj '{obj}'")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def get_field(form, field_name):
 | 
			
		||||
    return form[field_name]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def as_field_group(ffield):
 | 
			
		||||
    # Waiting for Django 5.0
 | 
			
		||||
    if (ffield):
 | 
			
		||||
        return SafeString(" ".join([ffield.label_tag(), ffield.errors.as_ul(), str(ffield)]))
 | 
			
		||||
    return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def can_edit(obj, user):
 | 
			
		||||
    return obj.can_edit(user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def can_delete(obj, user):
 | 
			
		||||
    return obj.can_delete(user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def sigles_referents(suivi):
 | 
			
		||||
    """
 | 
			
		||||
    Affichage d'abord duo psy/educ, puis autres ressources. par ex: "Psy/Educ - Coach - ASE"
 | 
			
		||||
    """
 | 
			
		||||
    interventions = [{
 | 
			
		||||
        'nom': interv.intervenant.nom_prenom,
 | 
			
		||||
        'role': interv.role.nom,
 | 
			
		||||
        'sigle': interv.intervenant.sigle or interv.intervenant.nom
 | 
			
		||||
    } for interv in suivi.intervenant_set.all()
 | 
			
		||||
    ]
 | 
			
		||||
    template = '<span title="{nom}, {role}">{sigle}</span>'
 | 
			
		||||
    psyeduc = sorted([i for i in interventions if i['role'] in ['Psy', 'Educ']], key=itemgetter('sigle'))
 | 
			
		||||
    autres = sorted([i for i in interventions if i['role'] not in ['Psy', 'Educ']], key=itemgetter('sigle'))
 | 
			
		||||
    return SafeString(" - ".join([res for res in [
 | 
			
		||||
        "/".join([format_html(template, **i) for i in psyeduc]),
 | 
			
		||||
        " - ".join([format_html(template, **i)  for i in autres]),
 | 
			
		||||
    ] if res]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def noms_referents_ope(suivi):
 | 
			
		||||
    return ' / '.join(ope.nom for ope in suivi.ope_referents)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def referents_pk_data(suivi):
 | 
			
		||||
    return ':'.join([str(ref.pk) for ref in suivi.intervenants.all()])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def join_qs(queryset, sep='/'):
 | 
			
		||||
    return sep.join([str(q) for q in queryset])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def in_parens(value):
 | 
			
		||||
    """Enclose value in parentheses only if it's not empty."""
 | 
			
		||||
    return '' if value in (None, '', []) else '({})'.format(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def etape_cellule(suivi, code_etape):
 | 
			
		||||
    """Produit le contenu d'une cellule du tableau de suivi des étapes."""
 | 
			
		||||
    def _css_class(date_suiv, default='next'):
 | 
			
		||||
        if date_suiv is None:
 | 
			
		||||
            # Ne devrait pas se produire, mais le cas échéant, éviter un crash.
 | 
			
		||||
            return default
 | 
			
		||||
        delta = date_suiv - date.today()
 | 
			
		||||
        if 19 > delta.days > 0:
 | 
			
		||||
            return 'urgent'
 | 
			
		||||
        if delta.days < 0:
 | 
			
		||||
            return 'depasse'
 | 
			
		||||
        return default
 | 
			
		||||
 | 
			
		||||
    etape = suivi.WORKFLOW[code_etape]
 | 
			
		||||
    etape_suiv = suivi.etape_suivante
 | 
			
		||||
    date_etape = etape.date(suivi)
 | 
			
		||||
    date_formatted = ''
 | 
			
		||||
    css_class = ''
 | 
			
		||||
    if date_etape:
 | 
			
		||||
        date_formatted = format_d_m_Y(date_etape)
 | 
			
		||||
        css_class = 'filled'
 | 
			
		||||
    elif etape_suiv and code_etape == etape_suiv.code:
 | 
			
		||||
        date_suiv = suivi.date_suivante()
 | 
			
		||||
        date_formatted = format_d_m_Y(date_suiv)
 | 
			
		||||
        css_class = _css_class(date_suiv)
 | 
			
		||||
        code_etape = etape_suiv.abrev
 | 
			
		||||
    else:
 | 
			
		||||
        # Certaines dates sont strictement liées au suivi
 | 
			
		||||
        date_etape = etape.delai_depuis(suivi, None)
 | 
			
		||||
        if date_etape:
 | 
			
		||||
            date_formatted = format_d_m_Y(date_etape)
 | 
			
		||||
            css_class = _css_class(date_etape, default='')
 | 
			
		||||
    return format_html(
 | 
			
		||||
        '<div title="{}:{}" class="{}" data-bs-toggle="tooltip">{}</div>',
 | 
			
		||||
        code_etape, date_formatted, css_class, etape.abrev
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def default_if_zero(duree):
 | 
			
		||||
    if isinstance(duree, timedelta):
 | 
			
		||||
        duree = _format_duree(duree)
 | 
			
		||||
    return '' if duree == '00:00' else duree
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def strip_seconds(duree):
 | 
			
		||||
    if duree is None:
 | 
			
		||||
        return ''
 | 
			
		||||
    if str(duree).count(':') > 1:
 | 
			
		||||
        return ':'.join(format_duree(duree).split(':')[:2])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def format_duree(duree):
 | 
			
		||||
    return _format_duree(duree)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def strip_zeros(decimal):
 | 
			
		||||
    return str(decimal).rstrip('0').rstrip('.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(is_safe=True)
 | 
			
		||||
def strip_colon(txt):
 | 
			
		||||
    return txt.replace(' :', '')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def month_name(month_number):
 | 
			
		||||
    return MONTHS[month_number]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(name='has_group')
 | 
			
		||||
def has_group(user, group_name):
 | 
			
		||||
    return user.groups.filter(name=group_name).exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def info_ope(ope):
 | 
			
		||||
    if ope:
 | 
			
		||||
        return format_html(
 | 
			
		||||
            '<span title="{}">{}</span>', ope.tel_prof, ope.nom_prenom,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def age_a(personne, date_):
 | 
			
		||||
    return personne.age_str(date_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def is_passed(date_):
 | 
			
		||||
    return date_ < date.today()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def nom_prenom_abreg(person):
 | 
			
		||||
    return '{} {}.'.format(person.nom, person.prenom[0].upper() if person.prenom else '')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def mes_totaux_mensuels(user, src, annee):
 | 
			
		||||
    return user.totaux_mensuels(src, annee)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def mon_total_annuel(user, src, annee):
 | 
			
		||||
    return format_duree(user.total_annuel(src, annee))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def sigles_intervenants(prestation):
 | 
			
		||||
    return format_html_join(
 | 
			
		||||
        '/', '{}', ((i.sigle or i.nom,) for i in prestation.intervenants.all())
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def sigle_personne(pers):
 | 
			
		||||
    if pers is None:
 | 
			
		||||
        return '-'
 | 
			
		||||
    return format_html(
 | 
			
		||||
        '<span title="{}">{}</span>', pers.nom_prenom, pers.sigle or pers.nom
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def as_icon(fichier):
 | 
			
		||||
    if not fichier:
 | 
			
		||||
        return ''
 | 
			
		||||
    ext = os.path.splitext(fichier.name)[1].lower()
 | 
			
		||||
    if ext in IMAGE_EXTS:
 | 
			
		||||
        icon = 'image'
 | 
			
		||||
    elif ext in ('.xls', '.xlsx'):
 | 
			
		||||
        icon = 'xlsx'
 | 
			
		||||
    elif ext in ('.doc', '.docx'):
 | 
			
		||||
        icon = 'docx'
 | 
			
		||||
    elif ext == '.pdf':
 | 
			
		||||
        icon = 'pdf'
 | 
			
		||||
    else:
 | 
			
		||||
        icon = 'master'
 | 
			
		||||
    return format_html(
 | 
			
		||||
        '<a class="{klass}" href="{url}"><img class="ficon" src="{ficon}" alt="Télécharger"></a>',
 | 
			
		||||
        klass=icon, url=fichier.url, ficon=static(f"ficons/{icon}.svg")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(takes_context=True)
 | 
			
		||||
def param_replace(context, **kwargs):
 | 
			
		||||
    d = context['request'].GET.copy()
 | 
			
		||||
    for k, v in kwargs.items():
 | 
			
		||||
        d[k] = v
 | 
			
		||||
    for k in [k for k, v in d.items() if not v]:
 | 
			
		||||
        del d[k]
 | 
			
		||||
    return d.urlencode()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def colorier_delai(value):
 | 
			
		||||
    if value <= 0:
 | 
			
		||||
        return "bg-danger-3"
 | 
			
		||||
    return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def benef_nom_adresse(obj):
 | 
			
		||||
    if obj:
 | 
			
		||||
        return '{} - {}'.format(obj.nom_prenom(),
 | 
			
		||||
                                format_adresse(obj.rue_actuelle, obj.npa_actuelle, obj.localite_actuelle))
 | 
			
		||||
    return 'Nouveau bénéficiaire'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def role_profession(contact):
 | 
			
		||||
    if contact.profession:
 | 
			
		||||
        return f"{contact.roles_str()} / {contact.profession}"
 | 
			
		||||
    return contact.roles_str()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def truncate_html_with_more(text, length):
 | 
			
		||||
    template = '''
 | 
			
		||||
        <div class="long hidden">{txt_long}</div>
 | 
			
		||||
        <div class="short">{txt_short}</div>
 | 
			
		||||
        {read_more}
 | 
			
		||||
    '''
 | 
			
		||||
    read_more = '<a class="read_more" href=".">Afficher la suite</a>'
 | 
			
		||||
    text_trunc = Truncator(text).words(int(length), html=True)
 | 
			
		||||
    return format_html(
 | 
			
		||||
        template,
 | 
			
		||||
        txt_long=mark_safe(text),
 | 
			
		||||
        txt_short=mark_safe(text_trunc),
 | 
			
		||||
        read_more=mark_safe(read_more) if text != text_trunc else '',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def raw_or_html(value):
 | 
			
		||||
    if value and '</' in value:  # Considered as HTML
 | 
			
		||||
        return mark_safe(value)
 | 
			
		||||
    else:
 | 
			
		||||
        return linebreaksbr(escape(value))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def can_be_reactivated(obj, user):
 | 
			
		||||
    return obj.can_be_reactivated(user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def archivable(famille, user):
 | 
			
		||||
    return famille.can_be_archived(user)
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								aemo/test.pdf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										2910
									
								
								aemo/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										165
									
								
								aemo/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,165 @@
 | 
			
		|||
from django.apps import apps
 | 
			
		||||
from django.urls import include, path
 | 
			
		||||
from aemo import views, views_stats
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('contact/add/', views.ContactCreateView.as_view(), name='contact-add'),
 | 
			
		||||
    path('contact/list/', views.ContactListView.as_view(), name='contact-list'),
 | 
			
		||||
    path('contact/<int:pk>/edit/', views.ContactUpdateView.as_view(), name='contact-edit'),
 | 
			
		||||
    path('contact/<int:pk>/delete/', views.ContactDeleteView.as_view(), name='contact-delete'),
 | 
			
		||||
    path('contact/dal/', views.ContactAutocompleteView.as_view(), name='contact-autocomplete'),
 | 
			
		||||
    path('contact/dal-ope/', views.ContactAutocompleteView.as_view(ope=True),
 | 
			
		||||
         name='contact-ope-autocomplete'),
 | 
			
		||||
    path('contact/dal-externe/', views.ContactExterneAutocompleteView.as_view(),
 | 
			
		||||
         name='contact-externe-autocomplete'),
 | 
			
		||||
    path('contact/test/doublon/', views.ContactTestDoublon.as_view(), name='contact-doublon'),
 | 
			
		||||
 | 
			
		||||
    path('service/add/', views.ServiceCreateView.as_view(), name='service-add'),
 | 
			
		||||
    path('service/list/', views.ServiceListView.as_view(), name='service-list'),
 | 
			
		||||
    path('service/<int:pk>/edit/', views.ServiceUpdateView.as_view(), name='service-edit'),
 | 
			
		||||
    path('service/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service-delete'),
 | 
			
		||||
 | 
			
		||||
    path('famille/dal/', views.FamilleAutoCompleteView.as_view(), name="famille-autocomplete"),
 | 
			
		||||
    # Famille
 | 
			
		||||
    path('famille/list/', views.FamilleListView.as_view(), name='famille-list'),
 | 
			
		||||
    path('famille/attente/', views.FamilleListView.as_view(mode='attente'), name='famille-attente'),
 | 
			
		||||
    path('famille/add/', views.FamilleCreateView.as_view(), name='famille-add'),
 | 
			
		||||
    path('famille/<int:pk>/edit/', views.FamilleUpdateView.as_view(), name='famille-edit'),
 | 
			
		||||
    path('famille/<int:obj_pk>/niveau/add/', views.NiveauCreateUpdateView.as_view(), name='niveau-add'),
 | 
			
		||||
    path('famille/<int:obj_pk>/niveau/<int:pk>/edit/', views.NiveauCreateUpdateView.as_view(),
 | 
			
		||||
         name='niveau-edit'
 | 
			
		||||
         ),
 | 
			
		||||
    path('famille/<int:obj_pk>/niveau/<int:pk>/delete/', views.NiveauDeleteView.as_view(),
 | 
			
		||||
         name='niveau-delete'),
 | 
			
		||||
 | 
			
		||||
    # Personne
 | 
			
		||||
    path('famille/<int:pk>/personne/add/', views.PersonneCreateView.as_view(),
 | 
			
		||||
         name="personne-add"),
 | 
			
		||||
    path('famille/<int:pk>/personne/<int:obj_pk>/edit/',
 | 
			
		||||
         views.PersonneUpdateView.as_view(), name="personne-edit"),
 | 
			
		||||
    path('famille/<int:pk>/personne/<int:obj_pk>/delete/',
 | 
			
		||||
         views.PersonneDeleteView.as_view(), name='personne-delete'),
 | 
			
		||||
 | 
			
		||||
    path('personne/<int:pk>/formation/', views.FormationView.as_view(), name='formation'),
 | 
			
		||||
    path('personne/<int:pk>/contacts/', views.PersonneReseauView.as_view(),
 | 
			
		||||
         name='personne-reseau-list'),
 | 
			
		||||
    path('personne/<int:pk>/contact/add/', views.PersonneReseauAdd.as_view(),
 | 
			
		||||
         name='personne-reseau-add'),
 | 
			
		||||
    path('personne/<int:pk>/contact/<int:obj_pk>/remove/', views.PersonneReseauRemove.as_view(),
 | 
			
		||||
         name='personne-reseau-remove'),
 | 
			
		||||
 | 
			
		||||
    # Prestations
 | 
			
		||||
    path('prestation/menu/', views.PrestationMenu.as_view(), name='prestation-menu'),
 | 
			
		||||
    path('famille/<int:pk>/prestation/list/', views.PrestationListView.as_view(), name='journal-list'),
 | 
			
		||||
    path('famille/<int:pk>/prestation/add/', views.PrestationCreateView.as_view(), name='prestation-famille-add'),
 | 
			
		||||
    path('famille/<int:pk>/prestation/<int:obj_pk>/edit/', views.PrestationUpdateView.as_view(),
 | 
			
		||||
         name='prestation-edit'),
 | 
			
		||||
    path('famille/<int:pk>/prestation/<int:obj_pk>/delete/', views.PrestationDeleteView.as_view(),
 | 
			
		||||
         name='prestation-delete'),
 | 
			
		||||
    path('prestation_gen/list/', views.PrestationListView.as_view(), name='prestation-gen-list'),
 | 
			
		||||
    path('prestation_gen/add/', views.PrestationCreateView.as_view(), name='prestation-gen-add'),
 | 
			
		||||
 | 
			
		||||
    path('famille/<int:pk>/upload/', views.DocumentUploadView.as_view(),
 | 
			
		||||
         name='famille-doc-upload'),
 | 
			
		||||
    path('famille/<int:pk>/doc/<int:doc_pk>/delete/', views.DocumentDeleteView.as_view(),
 | 
			
		||||
         name='famille-doc-delete'),
 | 
			
		||||
    path('famille/<int:pk>/reactivation/', views.FamilleReactivationView.as_view(),
 | 
			
		||||
         name='famille-reactivation'),
 | 
			
		||||
 | 
			
		||||
    # Doc. à imprimer
 | 
			
		||||
    path('famille/<int:pk>/print-evaluation/', views.EvaluationPDFView.as_view(), name='print-evaluation'),
 | 
			
		||||
    path('famille/<int:pk>/print-info/', views.CoordonneesPDFView.as_view(), name='print-coord-famille'),
 | 
			
		||||
    path('famille/<int:pk>/print-journal', views.JournalPDFView.as_view(), name='print-journal'),
 | 
			
		||||
    path('bilan/<int:pk>/print/', views.BilanPDFView.as_view(), name='print-bilan'),
 | 
			
		||||
 | 
			
		||||
    # Rapport
 | 
			
		||||
    path('famille/<int:pk>/rapport/add/', views.RapportCreateView.as_view(),
 | 
			
		||||
         name='famille-rapport-add'),
 | 
			
		||||
    path('famille/<int:pk>/rapport/<int:obj_pk>/', views.RapportDisplayView.as_view(),
 | 
			
		||||
         name='famille-rapport-view'),
 | 
			
		||||
    path('famille/<int:pk>/rapport/<int:obj_pk>/edit/', views.RapportUpdateView.as_view(),
 | 
			
		||||
         name='famille-rapport-edit'),
 | 
			
		||||
    path('famille/<int:pk>/rapport/<int:obj_pk>/delete/', views.RapportDeleteView.as_view(),
 | 
			
		||||
         name='famille-rapport-delete'),
 | 
			
		||||
    path('famille/<int:pk>/rapport/<int:obj_pk>/print/', views.RapportPDFView.as_view(),
 | 
			
		||||
         name='famille-rapport-print'),
 | 
			
		||||
    path('famille/<int:pk>/adresse/change/', views.FamilleAdresseChangeView.as_view(),
 | 
			
		||||
         name='famille-adresse-change'),
 | 
			
		||||
 | 
			
		||||
    # Demande, suivi, agenda, suivis terminés
 | 
			
		||||
    path('famille/<int:pk>/demande/', views.DemandeView.as_view(), name='demande'),
 | 
			
		||||
    path('famille/<int:pk>/suivi/', views.SuiviView.as_view(), name='famille-suivi'),
 | 
			
		||||
    path('famille/<int:pk>/intervenant/add/', views.SuiviIntervenantCreate.as_view(), name='intervenant-add'),
 | 
			
		||||
    path('famille/<int:pk>/intervenant/<int:obj_pk>/edit/', views.SuiviIntervenantUpdateView.as_view(),
 | 
			
		||||
         name='intervenant-edit'),
 | 
			
		||||
    path('famille/<int:pk>/agenda/', views.AgendaSuiviView.as_view(), name='famille-agenda'),
 | 
			
		||||
    path('famille/<int:pk>/bilan/add/', views.BilanEditView.as_view(is_create=True),
 | 
			
		||||
         name='famille-bilan-add'),
 | 
			
		||||
    path('famille/<int:pk>/bilan/<int:obj_pk>/', views.BilanDetailView.as_view(),
 | 
			
		||||
         name='famille-bilan-view'),
 | 
			
		||||
    path('famille/<int:pk>/bilan/<int:obj_pk>/edit/', views.BilanEditView.as_view(),
 | 
			
		||||
         name='famille-bilan-edit'),
 | 
			
		||||
    path('famille/<int:pk>/bilan/<int:obj_pk>/delete/', views.BilanDeleteView.as_view(),
 | 
			
		||||
         name='famille-bilan-delete'),
 | 
			
		||||
    path('famille/archivable/list/', views.FamilleArchivableListe.as_view(), name='famille-archivable'),
 | 
			
		||||
 | 
			
		||||
    path('suivis_termines/', views.SuivisTerminesListView.as_view(), name='suivis-termines'),
 | 
			
		||||
 | 
			
		||||
    path('charge_utilisateurs/', views.UtilisateurChargeDossierView.as_view(), name='charge-utilisateurs'),
 | 
			
		||||
 | 
			
		||||
    path('utilisateur/', views.UtilisateurListView.as_view(),
 | 
			
		||||
         name='utilisateur-list'),
 | 
			
		||||
    path('utilisateur/<int:pk>/edit/', views.UtilisateurUpdateView.as_view(),
 | 
			
		||||
         name='utilisateur-edit'),
 | 
			
		||||
    path('utilisateur/add/', views.UtilisateurCreateView.as_view(),
 | 
			
		||||
         name='utilisateur-add'),
 | 
			
		||||
    path('utilisateur/<int:pk>/delete/', views.UtilisateurDeleteView.as_view(),
 | 
			
		||||
         name='utilisateur-delete'),
 | 
			
		||||
    path('utilisateur/<int:pk>/password_reinit/', views.UtilisateurPasswordReinitView.as_view(),
 | 
			
		||||
         name='utilisateur-password-reinit'),
 | 
			
		||||
    path('utilisateur/<int:pk>/otp_device/reinit/', views.UtilisateurOtpDeviceReinitView.as_view(),
 | 
			
		||||
         name='utilisateur-otp-device-reinit'),
 | 
			
		||||
    path('utilisateur/<int:pk>/journalacces/', views.UtilisateurJournalAccesView.as_view(),
 | 
			
		||||
         name='utilisateur-journalacces'),
 | 
			
		||||
    path('utilisateur/dal/', views.UtilisateurAutocompleteView.as_view(),
 | 
			
		||||
         name='utilisateur-autocomplete'),
 | 
			
		||||
    path('utilisateur/desactive/list/', views.UtilisateurListView.as_view(is_active=False),
 | 
			
		||||
         name='utilisateur-desactive-list'),
 | 
			
		||||
    path('utilisateur/<int:pk>/reactiver/', views.UtilisateurReactivateView.as_view(),
 | 
			
		||||
         name='utilisateur-reactiver'),
 | 
			
		||||
 | 
			
		||||
    path('utilisateur/prestation/', views.PrestationPersonnelleListView.as_view(),
 | 
			
		||||
         name='prestation-personnelle'),
 | 
			
		||||
    path('prestation/generale/', views.PrestationGeneraleListView.as_view(),
 | 
			
		||||
         name='prestation-generale'),
 | 
			
		||||
 | 
			
		||||
    path('cerclescolaire/', views.CercleScolaireListView.as_view(),
 | 
			
		||||
         name='cercle-list'),
 | 
			
		||||
    path('cerclescolaire/<int:pk>/edit/', views.CercleScolaireUpdateView.as_view(),
 | 
			
		||||
         name='cercle-edit'),
 | 
			
		||||
    path('cerclescolaire/add/', views.CercleScolaireCreateView.as_view(),
 | 
			
		||||
         name='cercle-add'),
 | 
			
		||||
    path('cerclescolaire/<int:pk>/delete/', views.CercleScolaireDeleteView.as_view(),
 | 
			
		||||
         name='cercle-delete'),
 | 
			
		||||
 | 
			
		||||
    path('role/', views.RoleListView.as_view(), name='role-list'),
 | 
			
		||||
    path('role/<int:pk>/edit/', views.RoleUpdateView.as_view(), name='role-edit'),
 | 
			
		||||
    path('role/add/', views.RoleCreateView.as_view(), name='role-add'),
 | 
			
		||||
    path('role/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role-delete'),
 | 
			
		||||
 | 
			
		||||
    path('permissions/', views.PermissionOverview.as_view(), name='permissions'),
 | 
			
		||||
 | 
			
		||||
    path('export/prestation/', views.ExportPrestationView.as_view(), name='export-prestation'),
 | 
			
		||||
 | 
			
		||||
    path('statistiques/', views_stats.StatistiquesView.as_view(), name='stats'),
 | 
			
		||||
    path('statistiques/localite/', views_stats.StatistiquesParLocaliteView.as_view(), name='stats-localite'),
 | 
			
		||||
    path('statistiques/region/', views_stats.StatistiquesParRegionView.as_view(), name='stats-region'),
 | 
			
		||||
    path('statistiques/duree/', views_stats.StatistiquesParDureeView.as_view(), name='stats-duree'),
 | 
			
		||||
    path('statistiques/age/', views_stats.StatistiquesParAgeView.as_view(), name='stats-age'),
 | 
			
		||||
    path('statistiques/motifs/', views_stats.StatistiquesMotifsView.as_view(), name='stats-motifs'),
 | 
			
		||||
    path('statistiques/prestations/', views_stats.StatistiquesPrestationView.as_view(), name='stats-prestations'),
 | 
			
		||||
    path('statistiques/niveaux/', views_stats.StatistiquesNiveauxView.as_view(), name='stats-niveaux'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if apps.is_installed('debug_toolbar'):
 | 
			
		||||
    urlpatterns.append(path('__debug__/', include('debug_toolbar.urls')))
 | 
			
		||||
							
								
								
									
										207
									
								
								aemo/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,207 @@
 | 
			
		|||
import random
 | 
			
		||||
import string
 | 
			
		||||
import unicodedata
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.mail import EmailMessage, mail_admins
 | 
			
		||||
from django.utils.dateformat import format as django_format
 | 
			
		||||
from django.utils.html import Urlizer
 | 
			
		||||
 | 
			
		||||
ANTICIPATION_POUR_DEBUT_SUIVI = 90
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_d_m_Y(date):
 | 
			
		||||
    return django_format(date, 'd.m.Y') if date else ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_d_m_Y_HM(date):
 | 
			
		||||
    return django_format(date, 'l d.m.Y - G:i') if date else ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_duree(duree, centiemes=False):
 | 
			
		||||
    if duree is None:
 | 
			
		||||
        return '00:00'
 | 
			
		||||
    elif isinstance(duree, str):
 | 
			
		||||
        return duree
 | 
			
		||||
    secondes = duree.total_seconds()
 | 
			
		||||
    heures = secondes // 3600
 | 
			
		||||
    if centiemes:
 | 
			
		||||
        # Arrondi, cf. #455
 | 
			
		||||
        minutes = round((secondes % 3600) / 36)
 | 
			
		||||
        return '{:02}.{:02}'.format(int(heures), minutes)
 | 
			
		||||
    else:
 | 
			
		||||
        minutes = (secondes % 3600) // 60
 | 
			
		||||
        return '{:02}:{:02}'.format(int(heures), int(minutes))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_Ymd(date):
 | 
			
		||||
    return django_format(date, 'Ymd') if date else ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_adresse(rue, npa, localite):
 | 
			
		||||
    """ Formate the complete adress """
 | 
			
		||||
 | 
			
		||||
    if rue and npa and localite:
 | 
			
		||||
        return f"{rue}, {npa} {localite}"
 | 
			
		||||
    if npa and localite:
 | 
			
		||||
        return f"{npa} {localite}"
 | 
			
		||||
    return f"{localite}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_contact(telephone, email):
 | 
			
		||||
    """ Formate the contact data (phone and email) """
 | 
			
		||||
    return '{} {} {}'.format(telephone, '-' if telephone != '' and email != '' else '', email)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unaccent(text):
 | 
			
		||||
    if isinstance(text, list):
 | 
			
		||||
        text = ' '.join(text)
 | 
			
		||||
    text = text.replace('-', ' ')
 | 
			
		||||
    text = text.lower()
 | 
			
		||||
    t = unicodedata.normalize('NFD', text).encode('ascii', 'ignore')
 | 
			
		||||
    return t.decode('utf-8')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_ajax(request):
 | 
			
		||||
    return request.META.get('HTTP_X_REQUESTED_WITH') in ['XMLHttpRequest', 'Fetch']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_nom_prenom(nom):
 | 
			
		||||
    if nom is None or nom == '':
 | 
			
		||||
        return ''
 | 
			
		||||
    if nom.startswith('de ') and len(nom) > 3:
 | 
			
		||||
        return f"de {nom[3].upper()}{nom[4:]}"
 | 
			
		||||
    return f"{nom[0].upper()}{nom[1:]}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_for_sms(phone):
 | 
			
		||||
    return phone and phone[:2] == '07'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SMSUrlizer(Urlizer):
 | 
			
		||||
    """
 | 
			
		||||
    Replace links with short links: "<-short->" -> take 19 chars
 | 
			
		||||
    See https://doc.smsup.ch/en/api/sms/send/short-url
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    url_template = '<-short->'
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.links = []
 | 
			
		||||
 | 
			
		||||
    def trim_url(self, url, **kwargs):
 | 
			
		||||
        self.links.append(url)
 | 
			
		||||
        return super().trim_url(url, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_sms(text, to, err):
 | 
			
		||||
    # Try to remain below 160 chars
 | 
			
		||||
    # https://pypi.org/project/smsutil/ if we want to check against GSM-7
 | 
			
		||||
    url = settings.SMSUP_SEND_URL
 | 
			
		||||
    urlizer = SMSUrlizer()
 | 
			
		||||
    if 'https://' in text:
 | 
			
		||||
        text = urlizer(text)
 | 
			
		||||
 | 
			
		||||
    headers = {
 | 
			
		||||
        "Authorization": f"Bearer {settings.SMSUP_API_TOKEN}",
 | 
			
		||||
        "Accept": "application/json",
 | 
			
		||||
    }
 | 
			
		||||
    if urlizer.links:
 | 
			
		||||
        # Presence of links requires usage of the POST API.
 | 
			
		||||
        params = {
 | 
			
		||||
            'sms': {
 | 
			
		||||
                'message': {
 | 
			
		||||
                    'text': text,
 | 
			
		||||
                    'pushtype': 'alert',
 | 
			
		||||
                    'sender': 'F.Transit',  # Max 11 chars
 | 
			
		||||
                    'links': urlizer.links,
 | 
			
		||||
                },
 | 
			
		||||
                'recipients': {
 | 
			
		||||
                    'gsm': [{'gsmsmsid': '', 'value': to.replace(' ', '')}],
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        response = requests.post(url, json=params, headers=headers)
 | 
			
		||||
    else:
 | 
			
		||||
        params = {
 | 
			
		||||
            'text': text,
 | 
			
		||||
            'to': to.replace(' ', ''),
 | 
			
		||||
            'sender': 'F.Transit',  # Max 11 chars
 | 
			
		||||
        }
 | 
			
		||||
        response = requests.get(url, params=params, headers=headers)
 | 
			
		||||
 | 
			
		||||
    result = response.json()
 | 
			
		||||
    if not result or result.get('message') != 'OK':
 | 
			
		||||
        mail_admins(
 | 
			
		||||
            "[AEMO-FR] Erreur SMS",
 | 
			
		||||
            f"{err}\n\nParams: {params}\n\n{result}",
 | 
			
		||||
        )
 | 
			
		||||
        return False
 | 
			
		||||
    return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_email(subject, message, to):
 | 
			
		||||
    email = EmailMessage(
 | 
			
		||||
        subject,
 | 
			
		||||
        message,
 | 
			
		||||
        to=[to],
 | 
			
		||||
        reply_to=['secretariat@fondation-transit.ch'],
 | 
			
		||||
    )
 | 
			
		||||
    try:
 | 
			
		||||
        email.send()
 | 
			
		||||
        return True
 | 
			
		||||
    except OSError:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONTINENTS = {
 | 
			
		||||
    'NA': [
 | 
			
		||||
        'AI', 'AG', 'AW', 'BS', 'BB', 'BQ', 'BZ', 'BM', 'CA', 'KY', 'CR', 'CU',
 | 
			
		||||
        'CW', 'DM', 'DO', 'SV', 'GL', 'GD', 'GP', 'GT', 'HT', 'HN', 'JM', 'MQ',
 | 
			
		||||
        'MX', 'PM', 'MS', 'CW', 'KN', 'NI', 'PA', 'PR', 'KN', 'LC', 'PM',
 | 
			
		||||
        'SX', 'TT', 'TC', 'VI', 'US', 'VC', 'VG',
 | 
			
		||||
    ],
 | 
			
		||||
    'SA': [
 | 
			
		||||
        'AR', 'BO', 'BR', 'CL', 'CO', 'EC', 'FK', 'GF', 'GY', 'PY', 'PE', 'SR',
 | 
			
		||||
        'UY', 'VE',
 | 
			
		||||
    ],
 | 
			
		||||
    'EU': [
 | 
			
		||||
        'AL', 'AD', 'AT', 'BY', 'BE', 'BA', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE',
 | 
			
		||||
        'FO', 'FI', 'FR', 'DE', 'GI', 'GR', 'HU', 'IS', 'IE', 'IT', 'LV', 'LI',
 | 
			
		||||
        'LT', 'LU', 'MK', 'MT', 'MD', 'MC', 'NL', 'NO', 'PL', 'PT', 'RO', 'RU',
 | 
			
		||||
        'SM', 'RS', 'SK', 'SI', 'ES', 'SE', 'CH', 'UA', 'GB', 'VA', 'RS', 'IM',
 | 
			
		||||
        'RS', 'ME',
 | 
			
		||||
    ],
 | 
			
		||||
    'AF': [
 | 
			
		||||
        'AO', 'DZ', 'BJ', 'BW', 'BF', 'BI', 'CD', 'CI', 'CM', 'CV', 'CF', 'KM',
 | 
			
		||||
        'CG', 'DJ', 'EG', 'GQ', 'ER', 'ET', 'GA', 'GH', 'GM', 'GW', 'GN', 'KE',
 | 
			
		||||
        'LS', 'LR', 'LY', 'MG', 'MW', 'ML', 'MR', 'MU', 'YT', 'MA', 'MZ', 'NA',
 | 
			
		||||
        'NE', 'NG', 'ST', 'RE', 'RW', 'SH', 'ST', 'SN', 'SC', 'SL', 'SO', 'SH',
 | 
			
		||||
        'SD', 'SZ', 'TD', 'TG', 'TN', 'TZ', 'UG', 'ZM', 'TZ', 'ZW', 'SS', 'ZA',
 | 
			
		||||
    ],
 | 
			
		||||
    'AS': [
 | 
			
		||||
        'AF', 'AM', 'AZ', 'BH', 'BD', 'BT', 'BN', 'KH', 'CN', 'CX', 'CC', 'IO',
 | 
			
		||||
        'GE', 'HK', 'IN', 'ID', 'IR', 'IQ', 'IL', 'JP', 'JO', 'KZ', 'KP', 'KR',
 | 
			
		||||
        'KW', 'KG', 'LA', 'LB', 'MO', 'MY', 'MV', 'MN', 'MM', 'NP', 'OM', 'PK',
 | 
			
		||||
        'PH', 'QA', 'SA', 'SG', 'LK', 'SY', 'TW', 'TJ', 'TH', 'TR', 'TM', 'AE',
 | 
			
		||||
        'UZ', 'VN', 'YE', 'PS',
 | 
			
		||||
    ],
 | 
			
		||||
    'OC': [
 | 
			
		||||
        'AS', 'AU', 'NZ', 'CK', 'FJ', 'PF', 'GU', 'KI', 'MP', 'MH', 'FM', 'UM',
 | 
			
		||||
        'NR', 'NC', 'NZ', 'NU', 'NF', 'PW', 'PG', 'MP', 'SB', 'TK', 'TO', 'TV',
 | 
			
		||||
        'VU', 'UM', 'WF', 'WS', 'TL',
 | 
			
		||||
    ],
 | 
			
		||||
    'AN': ['AQ'],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def continent_from_country_code(code):
 | 
			
		||||
    for cont_code, country_codes in CONTINENTS.items():
 | 
			
		||||
        if code in country_codes:
 | 
			
		||||
            return cont_code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
 | 
			
		||||
    return ''.join(random.choice(chars) for _ in range(size))
 | 
			
		||||
							
								
								
									
										1673
									
								
								aemo/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										909
									
								
								aemo/views_stats.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,909 @@
 | 
			
		|||
import calendar
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from datetime import date, timedelta
 | 
			
		||||
from operator import attrgetter
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.auth.mixins import PermissionRequiredMixin
 | 
			
		||||
from django.db.models import (
 | 
			
		||||
    Case, Count, DurationField, ExpressionWrapper, F, IntegerField, OuterRef, Q,
 | 
			
		||||
    Subquery, Sum, When
 | 
			
		||||
)
 | 
			
		||||
from django.db.models.functions import Coalesce, TruncMonth
 | 
			
		||||
from django.utils.dates import MONTHS
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
 | 
			
		||||
from .forms import DateYearForm
 | 
			
		||||
from .models import Intervenant, LibellePrestation, Niveau, Personne, Prestation, Suivi, Utilisateur
 | 
			
		||||
from .utils import format_d_m_Y, format_duree
 | 
			
		||||
from common.choices import (
 | 
			
		||||
    MOTIF_DEMANDE_CHOICES, MOTIFS_FIN_SUIVI_CHOICES, PROVENANCE_DESTINATION_CHOICES,
 | 
			
		||||
    SERVICE_ORIENTEUR_CHOICES,
 | 
			
		||||
)
 | 
			
		||||
from .export import ExportStatistique
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DateLimitForm(forms.Form):
 | 
			
		||||
    YEAR_CHOICES = tuple(
 | 
			
		||||
        (str(y), str(y))
 | 
			
		||||
        for y in range(2020, date.today().year + (1 if date.today().month < 12 else 2))
 | 
			
		||||
    )
 | 
			
		||||
    start_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
 | 
			
		||||
    start_year = forms.ChoiceField(choices=YEAR_CHOICES)
 | 
			
		||||
    end_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
 | 
			
		||||
    end_year = forms.ChoiceField(choices=YEAR_CHOICES)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, data, **kwargs):
 | 
			
		||||
        if not data:
 | 
			
		||||
            today = date.today()
 | 
			
		||||
            data = {
 | 
			
		||||
                'start_year': today.year, 'start_month': today.month,
 | 
			
		||||
                'end_year': today.year if today.month < 12 else today.year + 1,
 | 
			
		||||
                'end_month': (today.month + 1) if today.month < 12 else 1,
 | 
			
		||||
            }
 | 
			
		||||
        super().__init__(data, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        cleaned_data = super().clean()
 | 
			
		||||
        if not self.errors and self.start > self.end:
 | 
			
		||||
            raise forms.ValidationError("Les dates ne sont pas dans l’ordre.")
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def start(self):
 | 
			
		||||
        return date(int(self.cleaned_data['start_year']), int(self.cleaned_data['start_month']), 1)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def end(self):
 | 
			
		||||
        return date(
 | 
			
		||||
            int(self.cleaned_data['end_year']),
 | 
			
		||||
            int(self.cleaned_data['end_month']),
 | 
			
		||||
            calendar.monthrange(int(self.cleaned_data['end_year']), int(self.cleaned_data['end_month']))[1]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class Month:
 | 
			
		||||
    year: int
 | 
			
		||||
    month: int
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f'{self.month:0>2}.{self.year}'
 | 
			
		||||
 | 
			
		||||
    def __lt__(self, other):
 | 
			
		||||
        return (self.year, self.month) < (other.year, other.month)
 | 
			
		||||
 | 
			
		||||
    def __hash__(self):
 | 
			
		||||
        return hash((self.year, self.month))
 | 
			
		||||
 | 
			
		||||
    def next(self):
 | 
			
		||||
        if self.month == 12:
 | 
			
		||||
            return Month(self.year + 1, 1)
 | 
			
		||||
        else:
 | 
			
		||||
            return Month(self.year, self.month + 1)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_date(cls, dt):
 | 
			
		||||
        return Month(dt.year, dt.month)
 | 
			
		||||
 | 
			
		||||
    def is_future(self):
 | 
			
		||||
        return date(self.year, self.month, 1) > date.today()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatsMixin:
 | 
			
		||||
    permission_required = 'aemo.export_stats'
 | 
			
		||||
 | 
			
		||||
    def get_months(self):
 | 
			
		||||
        """Return a list of tuples [(year, month), ...] from date_start to date_end."""
 | 
			
		||||
        months = [Month(self.date_start.year, self.date_start.month)]
 | 
			
		||||
        while True:
 | 
			
		||||
            next_m = months[-1].next()
 | 
			
		||||
            if next_m > Month(self.date_end.year, self.date_end.month):
 | 
			
		||||
                break
 | 
			
		||||
            months.append(next_m)
 | 
			
		||||
        return months
 | 
			
		||||
 | 
			
		||||
    def month_limits(self, month):
 | 
			
		||||
        """From (2020, 4), return (date(2020, 4, 1), date(2020, 5, 1))."""
 | 
			
		||||
        next_m = month.next()
 | 
			
		||||
        return (
 | 
			
		||||
            date(month.year, month.month, 1),
 | 
			
		||||
            date(next_m.year, next_m.month, 1)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def init_counters(names, months, default=0, total=0):
 | 
			
		||||
        """
 | 
			
		||||
        Create stat counters:
 | 
			
		||||
            {<counter_name>: {(2020, 3): 0, (2020, 4): 0, …, 'total': 0}, …}
 | 
			
		||||
        """
 | 
			
		||||
        counters = {}
 | 
			
		||||
        for count_name in names:
 | 
			
		||||
            counters[count_name] = {}
 | 
			
		||||
            for month in months:
 | 
			
		||||
                counters[count_name][month] = '-' if month.is_future() else default
 | 
			
		||||
            counters[count_name]['total'] = total
 | 
			
		||||
        return counters
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        # Here subclasses produce stats to be merged in view context
 | 
			
		||||
        return {}
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        get_params = self.request.GET.copy()
 | 
			
		||||
        self.export_flag = get_params.pop('export', None)
 | 
			
		||||
        date_form = DateLimitForm(get_params)
 | 
			
		||||
        context['date_form'] = date_form
 | 
			
		||||
        if not date_form.is_valid():
 | 
			
		||||
            return context
 | 
			
		||||
        self.date_start = date_form.start
 | 
			
		||||
        self.date_end = date_form.end
 | 
			
		||||
        months = self.get_months()
 | 
			
		||||
        context.update({
 | 
			
		||||
            'date_form': date_form,
 | 
			
		||||
            'months': months,
 | 
			
		||||
        })
 | 
			
		||||
        context.update(self.get_stats(months))
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def export_as_openxml(self, context):
 | 
			
		||||
        export = ExportStatistique(col_widths=[50])
 | 
			
		||||
        export.fill_data(self.export_lines(context))
 | 
			
		||||
        return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')
 | 
			
		||||
 | 
			
		||||
    def render_to_response(self, context, **response_kwargs):
 | 
			
		||||
        if self.export_flag:
 | 
			
		||||
            return self.export_as_openxml(context)
 | 
			
		||||
        else:
 | 
			
		||||
            return super().render_to_response(context, **response_kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/statistiques.html'
 | 
			
		||||
    labels = {
 | 
			
		||||
        'familles_total': 'Total familles (évaluation et/ou accomp.)',
 | 
			
		||||
        'enfants_total': 'Total enfants suivis (évaluation et/ou accomp.)',
 | 
			
		||||
        'familles_evaluees': 'Familles évaluées',
 | 
			
		||||
        'enfants_evalues': 'Enfants évalués',
 | 
			
		||||
        'enfants_evalues_non_suivis': 'Enfants non suivis de familles évaluées',
 | 
			
		||||
        'familles_eval_sans_suivi': 'Familles évaluées sans aboutir à un suivi',
 | 
			
		||||
        'familles_suivies': 'Familles suivies',
 | 
			
		||||
        'enfants_suivis': 'Enfants suivis',
 | 
			
		||||
        'enfants_suivis_non_suivis': 'Enfants non suivis de familles suivies',
 | 
			
		||||
        'familles_accueil': 'dont Familles d’accueil',
 | 
			
		||||
        'familles_connues': 'dont Familles déjà suivies',
 | 
			
		||||
        'prioritaires': 'Demandes prioritaires',
 | 
			
		||||
        'rdv_manques': 'Rendez-vous manqués',
 | 
			
		||||
        'duree_attente': 'Durée moyenne entre demande et début de suivi',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def suivi_stats(self, model, months):
 | 
			
		||||
        suivis_base = model.objects.annotate(
 | 
			
		||||
            date_fin=Coalesce('date_fin_suivi', date.today()),
 | 
			
		||||
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
 | 
			
		||||
        ).filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        ).exclude(motif_fin_suivi='erreur')
 | 
			
		||||
 | 
			
		||||
        # Annotations pour Count("Enfant suivi"), Count("Enfant non-suivi"), duree_attente
 | 
			
		||||
        suivis = suivis_base.annotate(enf_suivis=Count(Case(
 | 
			
		||||
            When(famille__membres__role__nom="Enfant suivi", then=1),
 | 
			
		||||
            output_field=IntegerField(),
 | 
			
		||||
        )), enf_nonsuivis=Count(Case(
 | 
			
		||||
            When(famille__membres__role__nom="Enfant non-suivi", then=1),
 | 
			
		||||
            output_field=IntegerField(),
 | 
			
		||||
        )), duree_attente=F('date_debut_suivi') - F('date_demande'),
 | 
			
		||||
        ).select_related('famille')
 | 
			
		||||
 | 
			
		||||
        count_keys = [
 | 
			
		||||
            'familles_total', 'enfants_total',
 | 
			
		||||
            'familles_evaluees', 'enfants_evalues', 'enfants_evalues_non_suivis',
 | 
			
		||||
            'familles_eval_sans_suivi', 'familles_suivies', 'enfants_suivis',
 | 
			
		||||
            'enfants_suivis_non_suivis', 'familles_accueil', 'familles_connues',
 | 
			
		||||
            'prioritaires', 'rdv_manques',
 | 
			
		||||
        ]
 | 
			
		||||
        if not getattr(model, 'demande_prioritaire', False):
 | 
			
		||||
            count_keys.remove('prioritaires')
 | 
			
		||||
        self.counters = self.init_counters(count_keys, months)
 | 
			
		||||
        self.counters['duree_attente'] = {'familles': 0, 'total': timedelta(), 'moyenne': timedelta()}
 | 
			
		||||
        count_keys.append('duree_attente')
 | 
			
		||||
        for suivi in suivis:
 | 
			
		||||
            self.update_counters(suivi, months)
 | 
			
		||||
        if self.counters['duree_attente']['familles'] > 0:
 | 
			
		||||
            self.counters['duree_attente']['moyenne'] = (
 | 
			
		||||
                self.counters['duree_attente']['total'] / self.counters['duree_attente']['familles']
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Rendez-vous manqués
 | 
			
		||||
        rdv_manques = suivis_base.annotate(
 | 
			
		||||
            month=TruncMonth('famille__prestations__date_prestation'),
 | 
			
		||||
        ).values('month').annotate(
 | 
			
		||||
            rdv_manques=Count(
 | 
			
		||||
                'famille__prestations',
 | 
			
		||||
                filter=Q(**{'famille__prestations__manque': True})
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        for line in rdv_manques:
 | 
			
		||||
            if not line['month']:
 | 
			
		||||
                continue
 | 
			
		||||
            month = Month.from_date(line['month'])
 | 
			
		||||
            if month not in self.counters['rdv_manques']:
 | 
			
		||||
                continue
 | 
			
		||||
            self.counters['rdv_manques'][month] = line['rdv_manques']
 | 
			
		||||
            self.counters['rdv_manques']['total'] += line['rdv_manques']
 | 
			
		||||
        data = {key: self.counters[key] for key in count_keys}
 | 
			
		||||
        data['total_familles'] = len(suivis)
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def update_counters(self, suivi, months):
 | 
			
		||||
        def suivi_entre(date_deb, date_fin):
 | 
			
		||||
            return (
 | 
			
		||||
                suivi.date_debut_suivi and suivi.date_debut_suivi < date_fin and
 | 
			
		||||
                (not suivi.date_fin_suivi or suivi.date_fin_suivi > date_deb)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        def evaluation_entre(date_deb, date_fin):
 | 
			
		||||
            # Dès la demande éventuelle, on considère la famille en cours d'évaluation
 | 
			
		||||
            # pour les stats, même sans date de debut d'évaluation
 | 
			
		||||
            debut_eval = suivi.date_demande or suivi.date_debut_evaluation
 | 
			
		||||
            fin_eval = suivi.date_fin_evaluation or suivi.date_fin_suivi
 | 
			
		||||
            return (
 | 
			
		||||
                debut_eval and debut_eval < date_fin
 | 
			
		||||
                and (not fin_eval or fin_eval > date_deb)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Pour chaque mois:
 | 
			
		||||
        for month in months:
 | 
			
		||||
            if month.is_future():
 | 
			
		||||
                continue
 | 
			
		||||
            month_start, month_end = self.month_limits(month)
 | 
			
		||||
            month_evalue = evaluation_entre(month_start, month_end)
 | 
			
		||||
            month_suivi = suivi_entre(month_start, month_end)
 | 
			
		||||
            if month_evalue:
 | 
			
		||||
                self.counters['familles_evaluees'][month] += 1
 | 
			
		||||
                self.counters['enfants_evalues'][month] += suivi.enf_suivis
 | 
			
		||||
                self.counters['enfants_evalues_non_suivis'][month] += suivi.enf_nonsuivis
 | 
			
		||||
                if suivi.famille.accueil:
 | 
			
		||||
                    self.counters['familles_accueil'][month] += 1
 | 
			
		||||
                if suivi.famille.connue:
 | 
			
		||||
                    self.counters['familles_connues'][month] += 1
 | 
			
		||||
            if month_suivi:
 | 
			
		||||
                self.counters['familles_suivies'][month] += 1
 | 
			
		||||
                self.counters['enfants_suivis'][month] += suivi.enf_suivis
 | 
			
		||||
                self.counters['enfants_suivis_non_suivis'][month] += suivi.enf_nonsuivis
 | 
			
		||||
                if suivi.famille.accueil:
 | 
			
		||||
                    self.counters['familles_accueil'][month] += 1
 | 
			
		||||
                if suivi.famille.connue:
 | 
			
		||||
                    self.counters['familles_connues'][month] += 1
 | 
			
		||||
            if month_evalue or month_suivi:
 | 
			
		||||
                if getattr(suivi, 'demande_prioritaire', False):
 | 
			
		||||
                    self.counters['prioritaires'][month] += 1
 | 
			
		||||
                    self.counters['prioritaires']['total'] += 1
 | 
			
		||||
                self.counters['familles_total'][month] += 1
 | 
			
		||||
                self.counters['enfants_total'][month] += suivi.enf_suivis
 | 
			
		||||
 | 
			
		||||
            if not suivi.date_debut_suivi and suivi.motif_fin_suivi and (
 | 
			
		||||
                suivi.date_fin_suivi >= month_start and suivi.date_fin_suivi < month_end
 | 
			
		||||
            ):
 | 
			
		||||
                self.counters['familles_eval_sans_suivi'][month] += 1
 | 
			
		||||
                self.counters['familles_eval_sans_suivi']['total'] += 1
 | 
			
		||||
 | 
			
		||||
        # Au total:
 | 
			
		||||
        suivi_evalue = evaluation_entre(self.date_start, self.date_end)
 | 
			
		||||
        suivi_acc = suivi_entre(self.date_start, self.date_end)
 | 
			
		||||
        if suivi_evalue:
 | 
			
		||||
            self.counters['familles_evaluees']['total'] += 1
 | 
			
		||||
            self.counters['enfants_evalues']['total'] += suivi.enf_suivis
 | 
			
		||||
            self.counters['enfants_evalues_non_suivis']['total'] += suivi.enf_nonsuivis
 | 
			
		||||
            if suivi.famille.accueil:
 | 
			
		||||
                self.counters['familles_accueil']['total'] += 1
 | 
			
		||||
            if suivi.famille.connue:
 | 
			
		||||
                self.counters['familles_connues']['total'] += 1
 | 
			
		||||
        if suivi_acc:
 | 
			
		||||
            self.counters['familles_suivies']['total'] += 1
 | 
			
		||||
            self.counters['enfants_suivis']['total'] += suivi.enf_suivis
 | 
			
		||||
            self.counters['enfants_suivis_non_suivis']['total'] += suivi.enf_nonsuivis
 | 
			
		||||
            if suivi.famille.accueil:
 | 
			
		||||
                self.counters['familles_accueil']['total'] += 1
 | 
			
		||||
            if suivi.famille.connue:
 | 
			
		||||
                self.counters['familles_connues']['total'] += 1
 | 
			
		||||
        if suivi_evalue or suivi_acc:
 | 
			
		||||
            self.counters['familles_total']['total'] += 1
 | 
			
		||||
            self.counters['enfants_total']['total'] += suivi.enf_suivis
 | 
			
		||||
        if suivi.duree_attente is not None and (
 | 
			
		||||
            suivi.date_debut_suivi >= self.date_start and suivi.date_debut_suivi <= self.date_end
 | 
			
		||||
        ):
 | 
			
		||||
            self.counters['duree_attente']['familles'] += 1
 | 
			
		||||
            self.counters['duree_attente']['total'] += suivi.duree_attente
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        return {
 | 
			
		||||
            'familles': self.suivi_stats(Suivi, months),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key, vals in values.items():
 | 
			
		||||
                if key in ['duree_attente', 'total_familles']:
 | 
			
		||||
                    continue
 | 
			
		||||
                yield [self.labels[key]] + [vals[month] for month in months] + [vals['total']]
 | 
			
		||||
            yield (
 | 
			
		||||
                [self.labels['duree_attente'] + ' (jours)'] +
 | 
			
		||||
                (len(months)) * [''] +
 | 
			
		||||
                [values['duree_attente']['moyenne'].days]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'Familles AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        yield from stats_for(context['familles'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesParIntervView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    """This view is currently unused (#344). It might be deleted in the future."""
 | 
			
		||||
    template_name = 'statistiques/stats-interv.html'
 | 
			
		||||
 | 
			
		||||
    def interv_stats(self, model, months):
 | 
			
		||||
        intervs = model.objects.annotate(
 | 
			
		||||
            date_fin=Coalesce('suivi__date_fin_suivi', date.today()),
 | 
			
		||||
            date_debut=Coalesce('suivi__date_debut_evaluation', 'suivi__date_debut_suivi')
 | 
			
		||||
        ).filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            suivi__motif_fin_suivi='erreur'
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            enf_suivis=Count(Case(
 | 
			
		||||
                When(suivi__famille__membres__role__nom="Enfant suivi", then=1),
 | 
			
		||||
                output_field=IntegerField(),
 | 
			
		||||
            ))
 | 
			
		||||
        ).select_related('suivi', 'intervenant')
 | 
			
		||||
 | 
			
		||||
        counters = {}
 | 
			
		||||
        for interv in intervs:
 | 
			
		||||
            if interv.intervenant not in counters:
 | 
			
		||||
                counters[interv.intervenant] = self.init_counters(['num_familles', 'num_enfants'], months)
 | 
			
		||||
            counters[interv.intervenant]['num_familles']['total'] += 1
 | 
			
		||||
            counters[interv.intervenant]['num_enfants']['total'] += interv.enf_suivis
 | 
			
		||||
            for month in months:
 | 
			
		||||
                month_start, month_end = self.month_limits(month)
 | 
			
		||||
                if interv.date_debut <= month_end and interv.date_fin >= month_start:
 | 
			
		||||
                    counters[interv.intervenant]['num_familles'][month] += 1
 | 
			
		||||
                    counters[interv.intervenant]['num_enfants'][month] += interv.enf_suivis
 | 
			
		||||
        return {key: counters[key] for key in sorted(counters.keys(), key=attrgetter('nom'))}
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        return {
 | 
			
		||||
            'intervs': self.interv_stats(Intervenant, months),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key, vals in values.items():
 | 
			
		||||
                yield [key.nom_prenom, 'Familles'] + [vals['num_familles'][month] for month in months]
 | 
			
		||||
                yield [key.nom_prenom, 'Enfants'] + [vals['num_enfants'][month] for month in months]
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'AEMO', ''] + [str(month) for month in months]
 | 
			
		||||
        yield from stats_for(context['intervs'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesParLocaliteView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/stats-localite.html'
 | 
			
		||||
 | 
			
		||||
    def localite_query(self, model):
 | 
			
		||||
        return model.objects.annotate(
 | 
			
		||||
            date_fin=Coalesce('date_fin_suivi', date.today()),
 | 
			
		||||
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
 | 
			
		||||
        ).filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            motif_fin_suivi='erreur'
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            enf_suivis=Count(Case(
 | 
			
		||||
                When(famille__membres__role__nom="Enfant suivi", then=1),
 | 
			
		||||
                output_field=IntegerField(),
 | 
			
		||||
            ))
 | 
			
		||||
        ).values('date_debut', 'date_fin', 'famille__npa', 'famille__localite', 'enf_suivis')
 | 
			
		||||
 | 
			
		||||
    def localite_stats(self, model, months):
 | 
			
		||||
        suivis = self.localite_query(model)
 | 
			
		||||
        counters = self.init_counters(['totals'], months)
 | 
			
		||||
        for suivi in suivis:
 | 
			
		||||
            loc_key = f"{suivi['famille__npa']} {suivi['famille__localite']}"
 | 
			
		||||
            if loc_key not in counters:
 | 
			
		||||
                counters.update(self.init_counters([loc_key], months))
 | 
			
		||||
            counters[loc_key]['total'] += suivi['enf_suivis']
 | 
			
		||||
            counters['totals']['total'] += suivi['enf_suivis']
 | 
			
		||||
            for month in months:
 | 
			
		||||
                month_start, month_end = self.month_limits(month)
 | 
			
		||||
                if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
 | 
			
		||||
                    counters[loc_key][month] += suivi['enf_suivis']
 | 
			
		||||
                    counters['totals'][month] += suivi['enf_suivis']
 | 
			
		||||
        return {localite: counters[localite] for localite in sorted(counters.keys())}
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        return {
 | 
			
		||||
            'localites': self.localite_stats(Suivi, months),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key, vals in values.items():
 | 
			
		||||
                yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        yield from stats_for(context['localites'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesParRegionView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/stats-region.html'
 | 
			
		||||
 | 
			
		||||
    def region_query(self, model, region_key):
 | 
			
		||||
        return model.objects.annotate(
 | 
			
		||||
            date_fin=Coalesce('date_fin_suivi', date.today()),
 | 
			
		||||
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
 | 
			
		||||
        ).filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            motif_fin_suivi='erreur'
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            enf_suivis=Count(Case(
 | 
			
		||||
                When(famille__membres__role__nom="Enfant suivi", then=1),
 | 
			
		||||
                output_field=IntegerField(),
 | 
			
		||||
            ))
 | 
			
		||||
        ).values('date_debut', 'date_fin', region_key, 'enf_suivis')
 | 
			
		||||
 | 
			
		||||
    def region_stats(self, model, months, region_key):
 | 
			
		||||
        suivis = self.region_query(model, region_key)
 | 
			
		||||
        counters = self.init_counters(['totals'], months)
 | 
			
		||||
        for suivi in suivis:
 | 
			
		||||
            loc_key = suivi[region_key] or '?'
 | 
			
		||||
            if loc_key not in counters:
 | 
			
		||||
                counters.update(self.init_counters([loc_key], months))
 | 
			
		||||
            counters[loc_key]['total'] += suivi['enf_suivis']
 | 
			
		||||
            counters['totals']['total'] += suivi['enf_suivis']
 | 
			
		||||
            for month in months:
 | 
			
		||||
                month_start, month_end = self.month_limits(month)
 | 
			
		||||
                if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
 | 
			
		||||
                    counters[loc_key][month] += suivi['enf_suivis']
 | 
			
		||||
                    counters['totals'][month] += suivi['enf_suivis']
 | 
			
		||||
        return {region: counters[region] for region in sorted(counters.keys())}
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        return {
 | 
			
		||||
            'regions': self.region_stats(Suivi, months, 'famille__region__nom'),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key, vals in values.items():
 | 
			
		||||
                yield (
 | 
			
		||||
                    ['Totaux' if key == 'totals' else key] +
 | 
			
		||||
                    [vals[month] for month in months] +
 | 
			
		||||
                    [vals['total']]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        yield from stats_for(context['regions'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesParAgeView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/stats-age.html'
 | 
			
		||||
 | 
			
		||||
    def age_stats(self, model, months):
 | 
			
		||||
        enfants_suivis = Personne.objects.filter(role__nom='Enfant suivi')
 | 
			
		||||
        enfants_suivis = enfants_suivis.exclude(
 | 
			
		||||
            famille__suivi__motif_fin_suivi='erreur'
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            date_fin=Coalesce(
 | 
			
		||||
                'famille__suivi__date_fin_suivi', date.today()
 | 
			
		||||
            ),
 | 
			
		||||
            date_debut=Coalesce(
 | 
			
		||||
                'famille__suivi__date_demande',
 | 
			
		||||
                'famille__suivi__date_debut_evaluation', 'famille__suivi__date_debut_suivi'
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        enfants_suivis = enfants_suivis.filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        counters = {}
 | 
			
		||||
        means = {'total': [], **{m: [] for m in months}}
 | 
			
		||||
        for enfant in enfants_suivis:
 | 
			
		||||
            age = enfant.age_a(enfant.date_debut + ((enfant.date_fin - enfant.date_debut) / 2))
 | 
			
		||||
            if age is None:
 | 
			
		||||
                continue
 | 
			
		||||
            age = age_real = int(age)
 | 
			
		||||
            if age > 18:
 | 
			
		||||
                age = 18
 | 
			
		||||
            if age not in counters:
 | 
			
		||||
                counters.update(self.init_counters([age], months))
 | 
			
		||||
            counters[age]['total'] += 1
 | 
			
		||||
            means['total'].append(age_real)
 | 
			
		||||
            for month in months:
 | 
			
		||||
                month_start, month_end = self.month_limits(month)
 | 
			
		||||
                if enfant.date_debut <= month_end and enfant.date_fin >= month_start:
 | 
			
		||||
                    counters[age][month] += 1
 | 
			
		||||
                    means[month].append(age_real)
 | 
			
		||||
        stats = {str(age): counters[age] for age in sorted(counters.keys())}
 | 
			
		||||
        if '18' in stats:
 | 
			
		||||
            stats['18 et plus'] = stats['18']
 | 
			
		||||
            del stats['18']
 | 
			
		||||
        # Calcul des moyennes à 1 décimale
 | 
			
		||||
        means['total'] = int(sum(means['total']) / max(len(means['total']), 1) * 10) / 10
 | 
			
		||||
        for month in months:
 | 
			
		||||
            if month.is_future():
 | 
			
		||||
                means[month] = '-'
 | 
			
		||||
            else:
 | 
			
		||||
                means[month] = int(sum(means[month]) / max(len(means[month]), 1) * 10) / 10
 | 
			
		||||
        return stats, means
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        stats, means = self.age_stats(Suivi, months)
 | 
			
		||||
        return {
 | 
			
		||||
            'ages': stats,
 | 
			
		||||
            'means': means,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key, vals in values.items():
 | 
			
		||||
                yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        yield from stats_for(context['ages'])
 | 
			
		||||
        yield ['Âge moyen'] + [context['means'][month] for month in months] + [context['means']['total']]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesParDureeView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/stats-duree.html'
 | 
			
		||||
 | 
			
		||||
    slices = {
 | 
			
		||||
        Suivi: [
 | 
			
		||||
            {'label': '0 à 3 mois', 'start': 0, 'stop': 120},
 | 
			
		||||
            {'label': '4 à 6 mois', 'start': 121, 'stop': 210},
 | 
			
		||||
            {'label': '7 à 9 mois', 'start': 211, 'stop': 300},
 | 
			
		||||
            {'label': '10 à 12 mois', 'start': 301, 'stop': 394},
 | 
			
		||||
            {'label': '13 à 15 mois', 'start': 395, 'stop': 484},
 | 
			
		||||
            {'label': '16 à 18 mois', 'start': 485, 'stop': 574},
 | 
			
		||||
            {'label': '19 à 24 mois', 'start': 575, 'stop': 760},
 | 
			
		||||
            {'label': '25 à 36 mois', 'start': 761, 'stop': 1125},
 | 
			
		||||
            {'label': '37 à 48 mois', 'start': 1126, 'stop': 1490},
 | 
			
		||||
            {'label': '49 mois et plus', 'start': 1491, 'stop': 100000},
 | 
			
		||||
        ],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def duree_stats(self, model, months):
 | 
			
		||||
        suivis = model.objects.filter(
 | 
			
		||||
            date_debut_suivi__isnull=False,
 | 
			
		||||
            date_fin_suivi__lte=self.date_end,
 | 
			
		||||
            date_fin_suivi__gte=self.date_start
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            motif_fin_suivi='erreur'
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            duree_suivi=F('date_fin_suivi') - F('date_debut_suivi')
 | 
			
		||||
        )
 | 
			
		||||
        counters = {sl['label']: 0 for sl in self.slices[model]}
 | 
			
		||||
        for suivi in suivis:
 | 
			
		||||
            duree_days = suivi.duree_suivi.days
 | 
			
		||||
            for sl in self.slices[model]:
 | 
			
		||||
                if sl['stop'] > duree_days > sl['start']:
 | 
			
		||||
                    counters[sl['label']] += 1
 | 
			
		||||
                    break
 | 
			
		||||
        return counters
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        return {
 | 
			
		||||
            'slices': self.slices[Suivi],
 | 
			
		||||
            'durees': self.duree_stats(Suivi, months),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        def stats_for(values, slices):
 | 
			
		||||
            for _slice in slices:
 | 
			
		||||
                yield [_slice['label'], values[_slice['label']]]
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', f'AEMO, du {format_d_m_Y(self.date_start)} au {format_d_m_Y(self.date_end)}']
 | 
			
		||||
        yield ['BOLD', 'Durée', 'Nombre de suivis']
 | 
			
		||||
        yield from stats_for(context['durees'], self.slices[Suivi])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesMotifsView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    template_name = 'statistiques/stats-motifs.html'
 | 
			
		||||
    labels = {
 | 
			
		||||
        'ann': 'Motif d’annonce',
 | 
			
		||||
        'orient': 'Service annonceur',
 | 
			
		||||
        'fin_preeval': 'Abandon avant évaluation',
 | 
			
		||||
        'fin_posteval': 'Abandon après évaluation',
 | 
			
		||||
        'fin_postacc': 'Fin de l’accompagnement',
 | 
			
		||||
        'fin_total': 'Total',
 | 
			
		||||
        'prov': 'Provenance',
 | 
			
		||||
        'dest': 'Destination',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def motifs_stats(self, model, months):
 | 
			
		||||
        suivis = model.objects.annotate(
 | 
			
		||||
            date_fin=Coalesce('date_fin_suivi', date.today()),
 | 
			
		||||
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
 | 
			
		||||
        ).filter(
 | 
			
		||||
            date_debut__lte=self.date_end,
 | 
			
		||||
            date_fin__gte=self.date_start
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            motif_fin_suivi='erreur'
 | 
			
		||||
        ).values(
 | 
			
		||||
            'famille_id', 'date_debut', 'date_fin', 'date_debut_evaluation', 'date_debut_suivi',
 | 
			
		||||
            'motif_demande', 'motif_fin_suivi',
 | 
			
		||||
            'service_orienteur', 'famille__provenance', 'famille__destination',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Initiate all counters to 0 for each month.
 | 
			
		||||
        counters = {
 | 
			
		||||
            'dem': {}, 'orient': {}, 'prov': {}, 'dest': {},
 | 
			
		||||
            'fin': {'preeval': {}, 'posteval': {}, 'postacc': {}, 'total': {}}
 | 
			
		||||
        }
 | 
			
		||||
        for key, choices in (
 | 
			
		||||
                ('dem', MOTIF_DEMANDE_CHOICES), ('fin', MOTIFS_FIN_SUIVI_CHOICES),
 | 
			
		||||
                ('orient', SERVICE_ORIENTEUR_CHOICES), ('prov', PROVENANCE_DESTINATION_CHOICES),
 | 
			
		||||
                ('dest', PROVENANCE_DESTINATION_CHOICES)):
 | 
			
		||||
            for ch in choices:
 | 
			
		||||
                if key == 'fin':
 | 
			
		||||
                    if ch[0] == 'erreur':
 | 
			
		||||
                        continue
 | 
			
		||||
                    for subkey in counters['fin'].keys():
 | 
			
		||||
                        counters[key][subkey].update(self.init_counters([ch[0]], months))
 | 
			
		||||
                else:
 | 
			
		||||
                    counters[key].update(self.init_counters([ch[0]], months))
 | 
			
		||||
        counters['orient'].update(self.init_counters(['undefined'], months))
 | 
			
		||||
 | 
			
		||||
        def suivi_in_month(suivi, month):
 | 
			
		||||
            month_start, month_end = self.month_limits(month)
 | 
			
		||||
            return suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start
 | 
			
		||||
 | 
			
		||||
        for suivi in suivis:
 | 
			
		||||
            # Stats motif demande
 | 
			
		||||
            for motif in (suivi['motif_demande'] or []):
 | 
			
		||||
                counters['dem'][motif]['total'] += 1
 | 
			
		||||
            for month in months:
 | 
			
		||||
                if suivi_in_month(suivi, month):
 | 
			
		||||
                    for motif in (suivi['motif_demande'] or []):
 | 
			
		||||
                        counters['dem'][motif][month] += 1
 | 
			
		||||
            # Stats service annonceur
 | 
			
		||||
            counters['orient'][suivi['service_orienteur'] or 'undefined']['total'] += 1
 | 
			
		||||
            for month in months:
 | 
			
		||||
                if suivi_in_month(suivi, month):
 | 
			
		||||
                    counters['orient'][suivi['service_orienteur'] or 'undefined'][month] += 1
 | 
			
		||||
            # Stats motif fin de suivi
 | 
			
		||||
            if suivi['motif_fin_suivi'] and suivi['motif_fin_suivi'] != 'erreur':
 | 
			
		||||
                counters['fin']['total'][suivi['motif_fin_suivi']]['total'] += 1
 | 
			
		||||
                if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
 | 
			
		||||
                    counters['fin']['preeval'][suivi['motif_fin_suivi']]['total'] += 1
 | 
			
		||||
                elif not suivi['date_debut_suivi']:
 | 
			
		||||
                    counters['fin']['posteval'][suivi['motif_fin_suivi']]['total'] += 1
 | 
			
		||||
                else:
 | 
			
		||||
                    counters['fin']['postacc'][suivi['motif_fin_suivi']]['total'] += 1
 | 
			
		||||
                for month in months:
 | 
			
		||||
                    if suivi_in_month(suivi, month):
 | 
			
		||||
                        counters['fin']['total'][suivi['motif_fin_suivi']][month] += 1
 | 
			
		||||
                        if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
 | 
			
		||||
                            counters['fin']['preeval'][suivi['motif_fin_suivi']][month] += 1
 | 
			
		||||
                        elif not suivi['date_debut_suivi']:
 | 
			
		||||
                            counters['fin']['posteval'][suivi['motif_fin_suivi']][month] += 1
 | 
			
		||||
                        else:
 | 
			
		||||
                            counters['fin']['postacc'][suivi['motif_fin_suivi']][month] += 1
 | 
			
		||||
            # Stats provenance
 | 
			
		||||
            if suivi['famille__provenance']:
 | 
			
		||||
                counters['prov'][suivi['famille__provenance']]['total'] += 1
 | 
			
		||||
                for month in months:
 | 
			
		||||
                    if suivi_in_month(suivi, month):
 | 
			
		||||
                        counters['prov'][suivi['famille__provenance']][month] += 1
 | 
			
		||||
            # Stats destination
 | 
			
		||||
            if suivi['famille__destination']:
 | 
			
		||||
                counters['dest'][suivi['famille__destination']]['total'] += 1
 | 
			
		||||
                for month in months:
 | 
			
		||||
                    if suivi_in_month(suivi, month):
 | 
			
		||||
                        counters['dest'][suivi['famille__destination']][month] += 1
 | 
			
		||||
        return counters
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        motif_ann_dict = dict(MOTIF_DEMANDE_CHOICES)
 | 
			
		||||
        annonceur_dict = dict(SERVICE_ORIENTEUR_CHOICES)
 | 
			
		||||
        motif_fin_dict = dict(MOTIFS_FIN_SUIVI_CHOICES)
 | 
			
		||||
        provdest_dict = dict(PROVENANCE_DESTINATION_CHOICES)
 | 
			
		||||
        stats = self.motifs_stats(Suivi, months)
 | 
			
		||||
        return {
 | 
			
		||||
            'data': {
 | 
			
		||||
                'ann': {motif_ann_dict[key]: data for key, data in stats['dem'].items()},
 | 
			
		||||
                'orient': {annonceur_dict.get(key, 'Non défini'): data for key, data in stats['orient'].items()},
 | 
			
		||||
                'fin_preeval': {motif_fin_dict[key]: data for key, data in stats['fin']['preeval'].items()},
 | 
			
		||||
                'fin_posteval': {motif_fin_dict[key]: data for key, data in stats['fin']['posteval'].items()},
 | 
			
		||||
                'fin_postacc': {motif_fin_dict[key]: data for key, data in stats['fin']['postacc'].items()},
 | 
			
		||||
                'fin_total': {motif_fin_dict[key]: data for key, data in stats['fin']['total'].items()},
 | 
			
		||||
                'prov': {provdest_dict[key]: data for key, data in stats['prov'].items()},
 | 
			
		||||
                'dest': {provdest_dict[key]: data for key, data in stats['dest'].items()},
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
 | 
			
		||||
        def stats_for(values):
 | 
			
		||||
            for key1, vals in values.items():
 | 
			
		||||
                yield ['BOLD', self.labels[key1]]
 | 
			
		||||
                for key2, subvals in vals.items():
 | 
			
		||||
                    yield [key2] + [subvals[month] for month in months] + [subvals['total']]
 | 
			
		||||
 | 
			
		||||
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        yield from stats_for(context['data'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesPrestationView(PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    permission_required = 'aemo.export_stats'
 | 
			
		||||
    template_name = 'statistiques/stats-prestations.html'
 | 
			
		||||
 | 
			
		||||
    def _sum_list(self, liste):
 | 
			
		||||
        tot = timedelta()
 | 
			
		||||
        for data in liste:
 | 
			
		||||
            if data != '-':
 | 
			
		||||
                tot += data
 | 
			
		||||
        return tot
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def temps_totaux_mensuels_fam_gen(prest_model, annee):
 | 
			
		||||
        """
 | 
			
		||||
        Renvoie un dictionnaire avec les totaux mensuels de toutes les prestations familiales
 | 
			
		||||
        et générales pour l'année en cours (de janv. à déc.).
 | 
			
		||||
        """
 | 
			
		||||
        query = prest_model.objects.filter(
 | 
			
		||||
            date_prestation__year=annee
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            month=TruncMonth('date_prestation'),
 | 
			
		||||
            duree_tot=ExpressionWrapper(Count('intervenants') * F('duree'), output_field=DurationField())
 | 
			
		||||
        ).values('month', 'lib_prestation__nom', 'duree_tot')
 | 
			
		||||
        # La somme est calculée en Python, car Django ne sait pas faire la somme de duree_tot.
 | 
			
		||||
        months = [Month(annee, num_month) for num_month in range(1, 13)]
 | 
			
		||||
        data = {month: {'total': '-' if month.is_future() else timedelta(0)} for month in months}
 | 
			
		||||
        for result in query:
 | 
			
		||||
            month = Month.from_date(result['month'])
 | 
			
		||||
            if result['lib_prestation__nom'] not in data[month]:
 | 
			
		||||
                data[month][result['lib_prestation__nom']] = '-' if month.is_future() else timedelta(0)
 | 
			
		||||
            data[month][result['lib_prestation__nom']] += result['duree_tot']
 | 
			
		||||
            data[month]['total'] += result['duree_tot']
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        get_params = self.request.GET.copy()
 | 
			
		||||
        self.export_flag = get_params.pop('export', None)
 | 
			
		||||
        date_form = DateYearForm(get_params)
 | 
			
		||||
        context['date_form'] = date_form
 | 
			
		||||
        if not date_form.is_valid():
 | 
			
		||||
            return context
 | 
			
		||||
        if date_form.is_valid():
 | 
			
		||||
            annee = int(date_form.cleaned_data['year'])
 | 
			
		||||
        counters = {}
 | 
			
		||||
        tot_dus_mensuels = [timedelta()] * 12
 | 
			
		||||
        tot_ecarts_mensuels = [timedelta()] * 12
 | 
			
		||||
 | 
			
		||||
        intervenants = Utilisateur.objects.annotate(
 | 
			
		||||
            num_prest=Count('prestations', filter=Q(prestations__date_prestation__year=annee))
 | 
			
		||||
        ).filter(num_prest__gt=0).order_by('nom', 'prenom')
 | 
			
		||||
        for interv in intervenants:
 | 
			
		||||
            h_prestees = interv.totaux_mensuels('aemo', annee)
 | 
			
		||||
            h_prestees = [('-' if Month(annee, idx).is_future() else h) for idx, h in enumerate(h_prestees, start=1)]
 | 
			
		||||
 | 
			
		||||
            tot_prestees = self._sum_list(h_prestees)
 | 
			
		||||
            counters[interv] = {
 | 
			
		||||
                'heures_prestees': h_prestees,
 | 
			
		||||
                'tot_prestees': tot_prestees,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        tot_prest_mensuels = self.temps_totaux_mensuels_fam_gen(Prestation, annee)
 | 
			
		||||
 | 
			
		||||
        tot_dus_mensuels.append(self._sum_list(tot_dus_mensuels))
 | 
			
		||||
        tot_ecarts_mensuels.append(self._sum_list(tot_ecarts_mensuels))
 | 
			
		||||
 | 
			
		||||
        totaux_par_prest = {}
 | 
			
		||||
        for month, data in tot_prest_mensuels.items():
 | 
			
		||||
            for label, duration in data.items():
 | 
			
		||||
                if label not in totaux_par_prest:
 | 
			
		||||
                    totaux_par_prest[label] = timedelta(0)
 | 
			
		||||
                if duration != '-':
 | 
			
		||||
                    totaux_par_prest[label] += duration
 | 
			
		||||
 | 
			
		||||
        context.update({
 | 
			
		||||
            'annee': annee,
 | 
			
		||||
            'titre': 'Prestations AEMO',
 | 
			
		||||
            'intervenants': counters,
 | 
			
		||||
            'months': [date(annee, m, 1) for m in range(1, 13)],
 | 
			
		||||
            'libelles_prest': LibellePrestation.objects.all().order_by('code'),
 | 
			
		||||
            'totaux_prest_mensuels': tot_prest_mensuels,
 | 
			
		||||
            'totaux_par_prest': totaux_par_prest,
 | 
			
		||||
            'total_gen': totaux_par_prest['total'],
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def render_to_response(self, context, **response_kwargs):
 | 
			
		||||
        if self.export_flag:
 | 
			
		||||
            export = ExportStatistique(col_widths=[30])
 | 
			
		||||
            export.fill_data(self.export_lines(context))
 | 
			
		||||
            return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')
 | 
			
		||||
        else:
 | 
			
		||||
            return super().render_to_response(context, **response_kwargs)
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        for user, vals in context['intervenants'].items():
 | 
			
		||||
            yield ([user.nom_prenom] + [format_duree(val) for val in vals['heures_prestees']]
 | 
			
		||||
                   + [format_duree(vals['tot_prestees'])])
 | 
			
		||||
        yield ['BOLD', 'Par type d’intervention']
 | 
			
		||||
        for prest in context['libelles_prest']:
 | 
			
		||||
            yield ([prest.nom] + [
 | 
			
		||||
                format_duree(context['totaux_prest_mensuels'][month].get(prest.nom,
 | 
			
		||||
                                                                         '-' if month.is_future() else timedelta(0)))
 | 
			
		||||
                for month in [Month.from_date(m) for m in months]
 | 
			
		||||
            ] + [format_duree(context['totaux_par_prest'].get(prest.nom, timedelta(0)))])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatistiquesNiveauxView(StatsMixin, PermissionRequiredMixin, TemplateView):
 | 
			
		||||
    permission_required = 'aemo.export_stats'
 | 
			
		||||
    template_name = 'statistiques/stats-niveaux.html'
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, months):
 | 
			
		||||
        prest_list = ['aemo04', 'aemo05', 'aemo06', 'aemo07']
 | 
			
		||||
        prest_query = Prestation.objects.filter(
 | 
			
		||||
            famille__isnull=False,
 | 
			
		||||
            date_prestation__range=(self.date_start, self.date_end),
 | 
			
		||||
            lib_prestation__code__in=prest_list,
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            month=TruncMonth('date_prestation'),
 | 
			
		||||
            niveau_interv=Subquery(
 | 
			
		||||
                Niveau.objects.filter(
 | 
			
		||||
                    Q(famille=OuterRef('famille_id')) &
 | 
			
		||||
                    Q(date_debut__lt=OuterRef('date_prestation')) & (
 | 
			
		||||
                        Q(date_fin__isnull=True) | Q(date_fin__gt=OuterRef('date_prestation'))
 | 
			
		||||
                    )
 | 
			
		||||
                ).order_by('-date_debut').values('niveau_interv')[:1]
 | 
			
		||||
            ),
 | 
			
		||||
        ).values(
 | 
			
		||||
            'month', 'niveau_interv', 'lib_prestation__code',
 | 
			
		||||
        ).annotate(
 | 
			
		||||
            sum_hours=Sum('duree'),
 | 
			
		||||
        )
 | 
			
		||||
        niveaux = {
 | 
			
		||||
            niv: {
 | 
			
		||||
                p: {
 | 
			
		||||
                    **{m: timedelta(0) for m in months}, 'total': timedelta(0)
 | 
			
		||||
                } for p in prest_list
 | 
			
		||||
            } for niv in [0, 1, 2, 3]
 | 
			
		||||
        }
 | 
			
		||||
        for line in prest_query:
 | 
			
		||||
            if line['niveau_interv'] is None:
 | 
			
		||||
                continue
 | 
			
		||||
            niveaux[line['niveau_interv']][line['lib_prestation__code']][Month.from_date(line['month'])] = line['sum_hours']
 | 
			
		||||
            niveaux[line['niveau_interv']][line['lib_prestation__code']]['total'] += line['sum_hours']
 | 
			
		||||
        return {
 | 
			
		||||
            'stats': niveaux,
 | 
			
		||||
            'prest_map': LibellePrestation.objects.in_bulk(prest_list, field_name='code'),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def export_lines(self, context):
 | 
			
		||||
        months = context['months']
 | 
			
		||||
        yield ['BOLD', 'Ressources par niveau'] + [str(month) for month in months] + ['Total']
 | 
			
		||||
        for niv, prests in context['stats'].items():
 | 
			
		||||
            yield ['BOLD', niv]
 | 
			
		||||
            for prest, numbers in prests.items():
 | 
			
		||||
                yield [context['prest_map'][prest].nom] + [numbers[m] for m in months] + [numbers['total']]
 | 
			
		||||