diff --git a/anagrafica/admin.py b/anagrafica/admin.py index bbfc8b1a1..ed384bbb4 100755 --- a/anagrafica/admin.py +++ b/anagrafica/admin.py @@ -15,9 +15,10 @@ from autenticazione.models import Utenza from base.admin import InlineAutorizzazione from gruppi.readonly_admin import ReadonlyAdminMixin -from .models import Persona, Sede, Appartenenza, Delega, Documento,\ - Fototessera, Estensione, Trasferimento, Riserva, Dimissione, Telefono, \ - ProvvedimentoDisciplinare +from formazione.models import PartecipazioneCorsoBase +from .models import (Persona, Sede, Appartenenza, Delega, Documento, + Fototessera, Estensione, Trasferimento, Riserva, Dimissione, Telefono, + ProvvedimentoDisciplinare) @@ -72,6 +73,14 @@ class InlineTelefonoPersona(ReadonlyAdminMixin, admin.StackedInline): extra = 0 +class InlinePartecipazioneCorsoBase(ReadonlyAdminMixin, admin.TabularInline): + from formazione.admin import RAW_ID_FIELDS_PARTECIPAZIONECORSOBASE + + model = PartecipazioneCorsoBase + raw_id_fields = RAW_ID_FIELDS_PARTECIPAZIONECORSOBASE + extra = 0 + + @admin.register(Persona) class AdminPersona(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ['nome', 'cognome', 'codice_fiscale', 'utenza__email', 'email_contatto', '=id',] @@ -79,7 +88,9 @@ class AdminPersona(ReadonlyAdminMixin, admin.ModelAdmin): 'ultima_modifica', ) list_filter = ('stato', ) list_display_links = ('nome', 'cognome', 'codice_fiscale',) - inlines = [InlineUtenzaPersona, InlineAppartenenzaPersona, InlineDelegaPersona, InlineDocumentoPersona, InlineTelefonoPersona] + inlines = [InlineUtenzaPersona, InlineAppartenenzaPersona, + InlineDelegaPersona, InlineDocumentoPersona, + InlinePartecipazioneCorsoBase, InlineTelefonoPersona,] actions = ['sposta_persone',] messaggio_spostamento = ungettext_lazy( @@ -271,7 +282,7 @@ class AdminAppartenenza(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ["membro", "persona__nome", "persona__cognome", "persona__codice_fiscale", "persona__utenza__email", "sede__nome"] list_display = ("persona", "sede", "attuale", "inizio", "fine", "creazione") - list_filter = ("membro", "inizio", "fine") + list_filter = ('confermata', "membro", "inizio", "fine") raw_id_fields = RAW_ID_FIELDS_APPARTENENZA inlines = [InlineAutorizzazione] diff --git a/anagrafica/autocomplete_light_registry.py b/anagrafica/autocomplete_light_registry.py index 16990461a..b102741c3 100644 --- a/anagrafica/autocomplete_light_registry.py +++ b/anagrafica/autocomplete_light_registry.py @@ -1,11 +1,11 @@ -from autocomplete_light import shortcuts as autocomplete_light -from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from anagrafica.costanti import NAZIONALE, REGIONALE, PROVINCIALE, LOCALE, TERRITORIALE -from anagrafica.models import Persona, Sede, Appartenenza -from anagrafica.permessi.applicazioni import UFFICIO_SOCI_UNITA, UFFICIO_SOCI -from anagrafica.permessi.costanti import GESTIONE_CORSI_SEDE +from autocomplete_light import shortcuts as autocomplete_light + +from .costanti import NAZIONALE, REGIONALE, PROVINCIALE, LOCALE, TERRITORIALE +from .permessi.applicazioni import UFFICIO_SOCI_UNITA, UFFICIO_SOCI +from .permessi.costanti import GESTIONE_CORSI_SEDE +from .models import Persona, Sede, Appartenenza from formazione.models import PartecipazioneCorsoBase diff --git a/anagrafica/costanti.py b/anagrafica/costanti.py index 329d8b4da..84af24980 100755 --- a/anagrafica/costanti.py +++ b/anagrafica/costanti.py @@ -23,4 +23,6 @@ } LIMITE_ETA = 35 -LIMITE_ANNI_ATTIVITA = 1 \ No newline at end of file +LIMITE_ANNI_ATTIVITA = 1 + +ESTENDIONI_DICT = dict(ESTENSIONE) diff --git a/anagrafica/forms.py b/anagrafica/forms.py index 4bf57a3e8..f1623ed96 100755 --- a/anagrafica/forms.py +++ b/anagrafica/forms.py @@ -3,27 +3,28 @@ import stdnum from dateutil.parser import parse -from django import forms from django.conf import settings +from django import forms +from django.forms import ModelForm, ChoiceField from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.widgets import AdminDateWidget from django.contrib.auth.forms import PasswordChangeForm from django.core.exceptions import ValidationError -from django.db.models import QuerySet -from django.forms import ModelForm, ChoiceField from django.utils.safestring import mark_safe from django.utils.timezone import now +# from django.db.models import QuerySet -from anagrafica.models import Sede, Persona, Appartenenza, Documento, Estensione, ProvvedimentoDisciplinare, Delega, \ - Fototessera, Trasferimento, Riserva -from anagrafica.validators import valida_almeno_14_anni, valida_data_nel_passato -from autenticazione.models import Utenza from autocomplete_light import shortcuts as autocomplete_light - +from autenticazione.models import Utenza from base.forms import ModuloMotivoNegazione from curriculum.models import TitoloPersonale from sangue.models import Donatore, Donazione -from anagrafica.permessi.applicazioni import PRESIDENTE, COMMISSARIO, CONSIGLIERE, CONSIGLIERE_GIOVANE, VICE_PRESIDENTE +from formazione.models import Corso +from .models import (Sede, Persona, Appartenenza, Documento, Estensione, + ProvvedimentoDisciplinare, Delega, Fototessera, Trasferimento, Riserva) +from .validators import valida_almeno_14_anni, valida_data_nel_passato +from anagrafica.permessi.applicazioni import (PRESIDENTE, COMMISSARIO, + CONSIGLIERE, CONSIGLIERE_GIOVANE, VICE_PRESIDENTE) class ModuloSpostaPersone(object): @@ -318,9 +319,32 @@ class Meta: class ModuloCreazioneDocumento(ModelForm): + expires = forms.DateField(required=False, label='Data di scadenza') + class Meta: model = Documento - fields = ['tipo', 'file'] + fields = ['tipo', 'file', 'expires'] + + def clean(self): + cd = self.cleaned_data + type = cd['tipo'] + + if type in [Documento.CARTA_IDENTITA, Documento.PATENTE_CIVILE]: + expires_error = None + expires = cd['expires'] + + if not expires: + expires_error = 'Indicare la data di scadenza del documento' + elif now().date() > expires: + expires_error = 'Non si può caricare documento scaduto.' + + if expires_error: + raise ValidationError({'expires': ValidationError(expires_error)}) + + return cd + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class ModuloModificaPassword(PasswordChangeForm): @@ -428,20 +452,24 @@ class Meta: fields = ['persona',] def __init__(self, *args, **kwargs): - self.me = kwargs.pop('me') + # These attrs are passed in anagrafica.viste.strumenti_delegati() + for attr in ['me', 'course']: + if attr in kwargs: + setattr(self, attr, kwargs.pop(attr)) super().__init__(*args, **kwargs) - def clean_persona(self): - me_sede = self.me.sede_riferimento() # Authorized user's (myself) - persona = self.cleaned_data['persona'] # Selected user whom to be given in + def _validate_delega_per_corso(self, persona): + """ + Validate only Volontario membership on url: + /formazione/corsi-base//direttori/ - # Queries for possible cases - persona_appartenenze = persona.appartenenze_attuali( - membro__in=Appartenenza.MEMBRO_DIRETTO) - persona_estesa = persona_appartenenze.filter(sede=me_sede).count() - persona_volontario = persona_appartenenze.filter(membro=Appartenenza.VOLONTARIO) - stesse_sedi = me_sede == persona.sede_riferimento() + Task: https://jira.gaia.cri.it/browse/JO-754 + """ + if not self.persona_volontario: + raise forms.ValidationError("Questa persona non è Volontario.") + return persona + def _validate_delega(self, me_sede, persona): """ Possible cases: 1) [OK] Persona è estesa (ES) nel mio comitato. @@ -452,20 +480,39 @@ def clean_persona(self): 5) [OK] Persona come Volontario (VO), è Presidente nella mia sede. """ CASES = ( - persona_estesa, - stesse_sedi, - stesse_sedi and persona_estesa, - any([a.appartiene_a(me_sede) for a in persona_appartenenze]), - any([a for a in persona_appartenenze if a.sede.presidente() == self.me]) + self.persona_estesa, + self.stesse_sedi, + self.stesse_sedi and self.persona_estesa, + any([a.appartiene_a(me_sede) for a in self.persona_volontario]), + any([a for a in self.persona_volontario if + a.sede.presidente() == self.me]) ) - if not any(CASES): # All CASES return False, so the form returns validation error. raise forms.ValidationError( "Il volontario non è appartenente alla tua sede." ) + # Some case returned True, so can proceed with creating new delega. return persona + + def clean_persona(self): + me_sede = self.me.sede_riferimento() # Authorized user's (myself) + persona = self.cleaned_data['persona'] # Selected user whom to be given in + + # Queries for possible cases + persona_appartenenze = persona.appartenenze_attuali( + membro__in=Appartenenza.MEMBRO_ATTIVITA) + self.persona_estesa = persona_appartenenze.filter( + sede=me_sede).count() + self.persona_volontario = persona_appartenenze.filter( + membro=Appartenenza.VOLONTARIO) + self.stesse_sedi = me_sede == persona.sede_riferimento() + + if self.course.tipo == Corso.CORSO_NUOVO: + return self._validate_delega_per_corso(persona) + else: + return self._validate_delega(me_sede, persona) def clean_inizio(self): """ Impedisce inizio passato """ diff --git a/anagrafica/migrations/0051_auto_20190130_1442.py b/anagrafica/migrations/0051_auto_20190130_1442.py new file mode 100644 index 000000000..6d47bb2a4 --- /dev/null +++ b/anagrafica/migrations/0051_auto_20190130_1442.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-01-30 14:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anagrafica', '0050_auto_20190115_0957'), + ] + + operations = [ + migrations.AddField( + model_name='documento', + name='expires', + field=models.DateField(null=True), + ) + ] diff --git a/anagrafica/models.py b/anagrafica/models.py index dd4d27560..0fb5c2ea0 100755 --- a/anagrafica/models.py +++ b/anagrafica/models.py @@ -1,77 +1,43 @@ -# coding=utf-8 - -""" -Questo modulo definisce i modelli del modulo anagrafico di Gaia. - -- Persona -- Telefono -- Documento -- Utente -- Appartenenza -- Comitato -- Delega -""" from datetime import date, timedelta, datetime -import stdnum -from django.conf import settings -from django.db.transaction import atomic -from django.utils import timezone - import codicefiscale +import phonenumbers import mptt -from django.contrib.auth.models import PermissionsMixin +from mptt.querysets import TreeQuerySet +from autoslug import AutoSlugField + +from django.apps import apps +from django.db import models +from django.db.models import Q, QuerySet, Avg +from django.db.transaction import atomic from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db import models -from django.db import models -from django.db.models import Q, QuerySet, Avg +from django.utils import timezone from django.utils.functional import cached_property from django_countries.fields import CountryField -import phonenumbers -from mptt.querysets import TreeQuerySet -from anagrafica.costanti import ESTENSIONE, TERRITORIALE, LOCALE, PROVINCIALE, REGIONALE, NAZIONALE - -from anagrafica.permessi.applicazioni import PRESIDENTE, PERMESSI_NOMI, PERMESSI_NOMI_DICT, UFFICIO_SOCI_UNITA, \ - DELEGHE_RUBRICA, DELEGATO_OBIETTIVO_2, DELEGATO_OBIETTIVO_3, DELEGATO_OBIETTIVO_1, DELEGATO_OBIETTIVO_4, \ - RESPONSABILE_FORMAZIONE, DELEGATO_OBIETTIVO_6, DELEGATO_OBIETTIVO_5, RESPONSABILE_AUTOPARCO, DELEGATO_CO, \ - DIRETTORE_CORSO, RESPONSABILE_AREA, REFERENTE, OBIETTIVI, COMMISSARIO, CONSIGLIERE, CONSIGLIERE_GIOVANE, VICE_PRESIDENTE -from anagrafica.permessi.applicazioni import UFFICIO_SOCI, PERMESSI_NOMI_DICT -from anagrafica.permessi.costanti import GESTIONE_ATTIVITA, PERMESSI_OGGETTI_DICT, GESTIONE_SOCI, GESTIONE_CORSI_SEDE, GESTIONE_CORSO, \ - GESTIONE_SEDE, GESTIONE_AUTOPARCHI_SEDE, GESTIONE_CENTRALE_OPERATIVA_SEDE -from anagrafica.permessi.delega import delega_permessi, delega_incarichi -from anagrafica.permessi.incarichi import INCARICO_GESTIONE_APPARTENENZE, INCARICO_GESTIONE_TRASFERIMENTI, \ - INCARICO_GESTIONE_ESTENSIONI, INCARICO_GESTIONE_RISERVE, INCARICO_ASPIRANTE -from anagrafica.permessi.persona import persona_ha_permesso, persona_oggetti_permesso, persona_permessi, \ - persona_permessi_almeno, persona_ha_permessi -from anagrafica.validators import valida_codice_fiscale, ottieni_genere_da_codice_fiscale, \ - crea_validatore_dimensione_file, valida_dimensione_file_8mb, valida_dimensione_file_5mb, valida_almeno_14_anni, \ - valida_partita_iva, valida_iban, valida_email_personale + +from .costanti import (ESTENSIONE, TERRITORIALE, LOCALE, PROVINCIALE, REGIONALE, NAZIONALE) +from .validators import (valida_codice_fiscale, ottieni_genere_da_codice_fiscale, + valida_dimensione_file_8mb, valida_partita_iva, valida_dimensione_file_5mb, + valida_iban, valida_email_personale) # valida_almeno_14_anni, crea_validatore_dimensione_file) +from .permessi.shortcuts import * +from .permessi.costanti import RUBRICA_DELEGATI_OBIETTIVO_ALL from attivita.models import Turno, Partecipazione from base.files import PDF, Excel, FoglioExcel -from base.geo import ConGeolocalizzazioneRaggio, ConGeolocalizzazione -from base.models import ModelloSemplice, ModelloAlbero, ConAutorizzazioni, ConAllegati, \ - Autorizzazione, ConVecchioID +from base.geo import ConGeolocalizzazione from base.stringhe import normalizza_nome, GeneratoreNomeFile -from base.tratti import ConMarcaTemporale, ConStorico, ConProtocollo, ConDelegati, ConPDF -from base.utils import is_list, sede_slugify, UpperCaseCharField, TitleCharField, poco_fa, mezzanotte_24_ieri, \ - mezzanotte_00, mezzanotte_24, concept -from autoslug import AutoSlugField - +from base.models import (ModelloSemplice, ModelloAlbero, ConAutorizzazioni, + ConAllegati, Autorizzazione, ConVecchioID) +from base.tratti import (ConMarcaTemporale, ConStorico, ConProtocollo, ConDelegati, ConPDF) +from base.utils import (is_list, sede_slugify, UpperCaseCharField, concept, oggi, + TitleCharField, poco_fa, mezzanotte_24_ieri, mezzanotte_00, mezzanotte_24) from curriculum.models import Titolo, TitoloPersonale from posta.models import Messaggio -from django.apps import apps - -from base.notifiche import NOTIFICA_INVIA, NOTIFICA_NON_INVIARE class Persona(ModelloSemplice, ConMarcaTemporale, ConAllegati, ConVecchioID): - """ - Rappresenta un record anagrafico in Gaia. - """ - # Genere MASCHIO = 'M' FEMMINA = 'F' @@ -197,7 +163,6 @@ def cognome_nome_completo(self): """ return normalizza_nome(self.cognome + " " + self.nome) - # Q: Qual e' l'email di questa persona? # A: Una persona puo' avere da zero a due indirizzi email. # - Persona.email_contatto e' quella scelta dalla persona per il contatto. @@ -259,28 +224,13 @@ def in_riserva(self): def ultimo_tesserino(self): return self.tesserini.all().order_by("creazione").last() - def __str__(self): - return self.nome_completo - - class Meta: - verbose_name_plural = "Persone" - app_label = 'anagrafica' - index_together = [ - ['nome', 'cognome'], - ['nome', 'cognome', 'codice_fiscale'], - ['id', 'nome', 'cognome', 'codice_fiscale'], - ] - permissions = ( - ('view_persona', "Can view persona"), - ('transfer_persona', "Can transfer persona"), - ) - # Q: Qual e' il numero di telefono di questa persona? # A: Una persona puo' avere da zero ad illimitati numeri di telefono. # - Persona.numeri_telefono ottiene l'elenco di oggetti Telefono. # - Per avere un elenco di numeri di telefono formattati, usare ad esempio # numeri = [str(x) for x in Persona.numeri_telefono] # - Usare Persona.aggiungi_numero_telefono per aggiungere un numero di telefono. + def numeri_pubblici(self): numeri_servizio = self.numeri_telefono.filter(servizio=True) if numeri_servizio.exists(): @@ -410,9 +360,7 @@ def sedi_attuali(self, **kwargs): return Sede.objects.filter(pk__in=[x.sede.pk for x in self.appartenenze_attuali(**kwargs)]) def titoli_personali_confermati(self): - """ - Ottiene queryset per TitoloPersonale con conferma approvata. - """ + """ Ottiene queryset per TitoloPersonale con conferma approvata. """ return self.titoli_personali.filter(confermata=True).select_related('titolo') def titoli_confermati(self): @@ -576,7 +524,9 @@ def applicazioni_disponibili(self): if self.ha_permesso(GESTIONE_CENTRALE_OPERATIVA_SEDE): lista += [('/centrale-operativa/', "CO", "fa-compass")] - if self.ha_permesso(GESTIONE_CORSO) or self.ha_permesso(GESTIONE_CORSI_SEDE): + if self.ha_permesso(GESTIONE_CORSO) or \ + self.ha_permesso(GESTIONE_CORSI_SEDE) or \ + True in [self.ha_permesso(i) for i in RUBRICA_DELEGATI_OBIETTIVO_ALL]: lista += [('/formazione/', 'Formazione', 'fa-graduation-cap')] tipi = [] @@ -585,6 +535,7 @@ def applicazioni_disponibili(self): continue tipi.append(d.tipo) #lista += [(APPLICAZIONI_SLUG_DICT[d.tipo], PERMESSI_NOMI_DICT[d.tipo])] + lista += [('/articoli/', 'Articoli', 'fa-newspaper-o')] lista += [('/documenti/', 'Documenti', 'fa-folder')] return lista @@ -734,6 +685,16 @@ def partecipazione_corso_base(self): from formazione.models import PartecipazioneCorsoBase, CorsoBase return PartecipazioneCorsoBase.con_esito_ok().filter(persona=self, corso__stato=CorsoBase.ATTIVO).first() + def richieste_di_partecipazione(self): + """ Restituisce richieste di partecipazione confermate e in attesa """ + from formazione.models import PartecipazioneCorsoBase, CorsoBase + + confirmed = PartecipazioneCorsoBase.con_esito_ok() + pending = PartecipazioneCorsoBase.con_esito_pending() + requests_to_courses = confirmed | pending + + return requests_to_courses.filter(persona=self, corso__stato=CorsoBase.ATTIVO) + @property def volontario_da_meno_di_un_anno(self): """ @@ -744,6 +705,10 @@ def volontario_da_meno_di_un_anno(self): r = self.appartenenze_attuali().filter(membro=Appartenenza.VOLONTARIO, inizio__gte=inizio_anno) return r.exists() + def personal_identity_documents(self): + return self.documenti.filter(tipo__in=[Documento.CARTA_IDENTITA, + Documento.PATENTE_CIVILE]) + @property def url(self): return "/profilo/%d/" % (self.pk,) @@ -1305,6 +1270,45 @@ def segmenti_collegati(self): attivi.append({'sedi_sottostanti': True, 'sede': sede.genitore}) return attivi + @classmethod + @concept + def to_contact_for_courses(cls, corso, membro='VO', *args, **kwargs): + from formazione.models import Corso, PartecipazioneCorsoBase + + if membro == Appartenenza.VOLONTARIO: + # Exclude persons that have already made participation request + # to as and the request has been + + to_exclude = cls.objects.filter( + PartecipazioneCorsoBase.con_esito( + PartecipazioneCorsoBase.ESITO_OK, + corso__tipo=Corso.CORSO_NUOVO, + corso=corso + ).via("partecipazioni_corsi") + ) + + without_to_exclude = ~Q(id__in=to_exclude.values_list('id', flat=True)) + return Q(without_to_exclude, *args, **kwargs) + + elif membro == Appartenenza.ESTESO: + # TODO: place for another Appartenenza membro + pass + else: + # TODO: what to return otherwise? + pass + + def has_required_titles_for_course(self, course): + volunteer_titles = self.titoli_personali_confermati().filter( + titolo__tipo=Titolo.TITOLO_CRI).values_list('titolo', flat=True) + + corso_titles = course.get_extensions_titles().values_list('id',flat=True) + intersection = set(volunteer_titles) & set(corso_titles) + + return len(intersection) == len(corso_titles) + + def __str__(self): + return self.nome_completo + def save(self, *args, **kwargs): self.nome = normalizza_nome(self.nome) self.cognome = normalizza_nome(self.cognome) @@ -1315,6 +1319,19 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) + class Meta: + verbose_name_plural = "Persone" + app_label = 'anagrafica' + index_together = [ + ['nome', 'cognome'], + ['nome', 'cognome', 'codice_fiscale'], + ['id', 'nome', 'cognome', 'codice_fiscale'], + ] + permissions = ( + ('view_persona', "Can view persona"), + ('transfer_persona', "Can transfer persona"), + ) + class Telefono(ConMarcaTemporale, ModelloSemplice): """ @@ -1374,9 +1391,7 @@ def e164(self): class Documento(ModelloSemplice, ConMarcaTemporale): - """ - Rappresenta un documento caricato da un utente. - """ + """ Rappresenta un documento caricato da un utente. """ # Tipologie di documento caricabili CARTA_IDENTITA = 'I' @@ -1396,6 +1411,25 @@ class Documento(ModelloSemplice, ConMarcaTemporale): persona = models.ForeignKey(Persona, related_name="documenti", db_index=True, on_delete=models.CASCADE) file = models.FileField("File", upload_to=GeneratoreNomeFile('documenti/'), validators=[valida_dimensione_file_8mb]) + expires = models.DateField(null=True) + + @property + def is_requested_for_course(self): + """ Restituisce True se il proprietario del documento ha richieste di + partecipazione ai corsi confermate o in attesa. """ + if self.tipo in [self.CARTA_IDENTITA, self.PATENTE_CIVILE]: + if self.persona.richieste_di_partecipazione().count(): + return True + return False + + @property + def can_be_deleted(self): + if self.is_requested_for_course: + return False + return True + + def __str__(self): + return str(self.file) class Meta: verbose_name_plural = "Documenti" @@ -1406,9 +1440,7 @@ class Meta: class Appartenenza(ModelloSemplice, ConStorico, ConMarcaTemporale, ConAutorizzazioni): - """ - Rappresenta un'appartenenza di una Persona ad un Sede. - """ + """ Rappresenta un'appartenenza di una Persona ad un Sede. """ class Meta: verbose_name_plural = "Appartenenze" @@ -1467,6 +1499,9 @@ class Meta: # Membri sotto il diretto controllo di una altra Sede MEMBRO_ESTESO = (ESTESO,) + # Utilizzati in Corso Nuovo (formazione) + MEMBRO_CORSO = (VOLONTARIO, ESTESO, DIPENDENTE, INFERMIERA, MILITARE) + MEMBRO = ( (VOLONTARIO, 'Volontario'), (ESTESO, 'Volontario in Estensione'), @@ -1858,8 +1893,8 @@ def membri_attuali(self, figli=False, al_giorno=None, **kwargs): else: kwargs.update({'sede': self.pk}) - return Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, **kwargs).via("appartenenze"))\ - .distinct('nome', 'cognome', 'codice_fiscale') + return Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, **kwargs).via("appartenenze")) + #.distinct('nome', 'cognome', 'codice_fiscale') def appartenenze_persona(self, persona, membro=None, figli=False, **kwargs): """ @@ -2094,9 +2129,16 @@ def autorizzazioni(self): # TODO Rimuovere destinatario_oggetto_id=self.oggetto.pk) def invia_notifica_creazione(self): + delega_tipo = self.get_tipo_display() + obj = self.oggetto + + msg_subject = "%s per %s" % (delega_tipo, obj) + if hasattr(obj, 'is_nuovo_corso') and obj.is_nuovo_corso: + msg_subject = '%s per %s (%s)' % (delega_tipo, obj, obj.titolo_cri) + messaggi = [] messaggi += [Messaggio.costruisci_e_invia( - oggetto="%s per %s" % (self.get_tipo_display(), self.oggetto,), + oggetto=msg_subject, modello="email_delega_notifica_creazione.html", corpo={ "delega": self, diff --git a/anagrafica/permessi/applicazioni.py b/anagrafica/permessi/applicazioni.py index 5783bed20..ae4a03066 100755 --- a/anagrafica/permessi/applicazioni.py +++ b/anagrafica/permessi/applicazioni.py @@ -1,10 +1,7 @@ -# coding=utf-8 from collections import OrderedDict -__author__ = 'alfioemanuele' # Tipologie di applicativi esistenti - PRESIDENTE = 'PR' VICE_PRESIDENTE = 'VP' COMMISSARIO = 'CM' @@ -96,4 +93,3 @@ ('direttori_corsi', (DIRETTORE_CORSO, 'Direttori Corsi', True)), ('responsabili_autoparco', (RESPONSABILE_AUTOPARCO, 'Responsabili Autoparco', True)), )) - diff --git a/anagrafica/permessi/costanti.py b/anagrafica/permessi/costanti.py index 6ce86401f..d5863add1 100755 --- a/anagrafica/permessi/costanti.py +++ b/anagrafica/permessi/costanti.py @@ -1,18 +1,17 @@ -# coding=utf-8 - +from ..permessi.applicazioni import (PRESIDENTE, DELEGATO_AREA, RESPONSABILE_AREA, + REFERENTE, DIRETTORE_CORSO, RESPONSABILE_AUTOPARCO, REFERENTE_GRUPPO, COMMISSARIO, + CONSIGLIERE, UFFICIO_SOCI) """ -Questo file gestisce i permessi in Gaia. - ============================================================================================ - | ! HEEEEY, TU ! | - ============================================================================================ - Prima di avventurarti da queste parti, assicurati di leggere la documentazione a: - https://github.com/CroceRossaItaliana/jorvik/wiki/Deleghe,-Permessi-e-Livelli-di-Accesso - ============================================================================================ + Questo file gestisce i permessi in Gaia. + =============================================================================== + | ! HEEEEY, TU ! | + =============================================================================== + Prima di avventurarti da queste parti, assicurati di leggere la documentazione: + https://github.com/CroceRossaItaliana/jorvik/wiki/Deleghe,-Permessi-e-Livelli-di-Accesso + =============================================================================== """ -from anagrafica.permessi.applicazioni import PRESIDENTE, DELEGATO_AREA, RESPONSABILE_AREA, REFERENTE, DIRETTORE_CORSO, \ - RESPONSABILE_AUTOPARCO, REFERENTE_GRUPPO, COMMISSARIO, CONSIGLIERE -from anagrafica.permessi.applicazioni import UFFICIO_SOCI + GESTIONE_SEDE = "GESTIONE_SEDE" GESTIONE_SOCI = "GESTIONE_SOCI" @@ -106,6 +105,12 @@ (REFERENTE_GRUPPO, ('anagrafica', 'Gruppo', 'sede__in')), ) +RUBRICA_DELEGATI_OBIETTIVO_ALL = [RUBRICA_DELEGATI_OBIETTIVO_1, + RUBRICA_DELEGATI_OBIETTIVO_2, + RUBRICA_DELEGATI_OBIETTIVO_3, + RUBRICA_DELEGATI_OBIETTIVO_4, + RUBRICA_DELEGATI_OBIETTIVO_6,] + # Livelli di permesso # IMPORTANTE: Tenere in ordine, sia numerico che di linea. @@ -171,4 +176,3 @@ def permesso_minimo(tipo): # Costanti URL ERRORE_PERMESSI = '/errore/permessi/' ERRORE_ORFANO = '/errore/orfano/' - diff --git a/anagrafica/permessi/delega.py b/anagrafica/permessi/delega.py index 230e53211..06652f353 100755 --- a/anagrafica/permessi/delega.py +++ b/anagrafica/permessi/delega.py @@ -1,7 +1,5 @@ -from anagrafica.permessi.funzioni import PERMESSI_FUNZIONI_DICT -from anagrafica.permessi.incarichi import ESPANSIONE_DELEGHE - -__author__ = 'alfioemanuele' +from ..permessi.funzioni import PERMESSI_FUNZIONI_DICT +from ..permessi.incarichi import ESPANSIONE_DELEGHE def delega_permessi(delega, solo_deleghe_attive=True): diff --git a/anagrafica/permessi/espansioni.py b/anagrafica/permessi/espansioni.py index c4294d5d7..bdcf19133 100755 --- a/anagrafica/permessi/espansioni.py +++ b/anagrafica/permessi/espansioni.py @@ -96,29 +96,16 @@ def espandi_elenchi_soci(qs_sedi, al_giorno=None): from ufficio_soci.models import Quota, Tesserino try: return [ - (LETTURA, Persona.objects.filter( - Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi).via("appartenenze"))), - (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, - membro__in=Appartenenza.MEMBRO_DIRETTO).via( - "appartenenze"))), - (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, - membro__in=Appartenenza.MEMBRO_ESTESO).via( - "appartenenze"))), - (LETTURA, Quota.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, - membro__in=Appartenenza.MEMBRO_ESTESO).via( - "persona__appartenenze"))), + (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi).via("appartenenze"))), + (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, membro__in=Appartenenza.MEMBRO_DIRETTO).via("appartenenze"))), + (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, membro__in=Appartenenza.MEMBRO_ESTESO).via("appartenenze"))), + (LETTURA, Quota.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, membro__in=Appartenenza.MEMBRO_ESTESO).via("persona__appartenenze"))), (LETTURA, Quota.objects.filter(Q(Q(sede__in=qs_sedi) | Q(appartenenza__sede__in=qs_sedi)))), - (LETTURA, Persona.objects.filter( - Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via( - "appartenenze"))), + (LETTURA, Persona.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via("appartenenze"))), (LETTURA, Persona.objects.filter(Appartenenza.con_esito_pending(sede__in=qs_sedi).via("appartenenze"))), (LETTURA, Persona.objects.filter(Appartenenza.con_esito_no(sede__in=qs_sedi).via("appartenenze"))), - (LETTURA, Riserva.objects.filter( - Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via( - "persona__appartenenze"))), - (LETTURA, Tesserino.objects.filter( - Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via( - "persona__appartenenze"))), + (LETTURA, Riserva.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via("persona__appartenenze"))), + (LETTURA, Tesserino.objects.filter(Appartenenza.query_attuale(al_giorno=al_giorno, sede__in=qs_sedi, confermata=True, ritirata=False).via("persona__appartenenze"))), ] except (AttributeError, ValueError, KeyError): return [] diff --git a/anagrafica/permessi/funzioni.py b/anagrafica/permessi/funzioni.py index 18cfa5a92..118805295 100755 --- a/anagrafica/permessi/funzioni.py +++ b/anagrafica/permessi/funzioni.py @@ -1,20 +1,14 @@ -""" -Questo modulo contiene tutte le funzioni per testare i permessi -a partire da un oggetto sul quale ho una delega ed un oggetto da testare. -""" from datetime import timedelta - from django.db.models import QuerySet, Q -from anagrafica.permessi.applicazioni import PRESIDENTE, DIRETTORE_CORSO, RESPONSABILE_AUTOPARCO, REFERENTE_GRUPPO, COMMISSARIO,\ - UFFICIO_SOCI_UNITA, DELEGATO_OBIETTIVO_1, DELEGATO_OBIETTIVO_2, DELEGATO_OBIETTIVO_3, DELEGATO_OBIETTIVO_4, \ - DELEGATO_OBIETTIVO_5, DELEGATO_OBIETTIVO_6, RESPONSABILE_FORMAZIONE, DELEGATO_CO, CONSIGLIERE, CONSIGLIERE_GIOVANE, VICE_PRESIDENTE -from anagrafica.permessi.applicazioni import UFFICIO_SOCI -from anagrafica.permessi.applicazioni import DELEGATO_AREA -from anagrafica.permessi.applicazioni import RESPONSABILE_AREA -from anagrafica.permessi.applicazioni import REFERENTE - -from anagrafica.permessi.costanti import GESTIONE_SOCI, ELENCHI_SOCI, GESTIONE_ATTIVITA_SEDE, GESTIONE_CORSI_SEDE, \ +from ..permessi.applicazioni import (PRESIDENTE, DIRETTORE_CORSO, RESPONSABILE_AUTOPARCO, + REFERENTE_GRUPPO, COMMISSARIO, UFFICIO_SOCI_UNITA, DELEGATO_OBIETTIVO_1, + DELEGATO_OBIETTIVO_2, DELEGATO_OBIETTIVO_3, DELEGATO_OBIETTIVO_4, DELEGATO_OBIETTIVO_5, + DELEGATO_OBIETTIVO_6, RESPONSABILE_FORMAZIONE, DELEGATO_CO, CONSIGLIERE, + CONSIGLIERE_GIOVANE, VICE_PRESIDENTE, UFFICIO_SOCI, DELEGATO_AREA, + RESPONSABILE_AREA, REFERENTE) +from ..permessi.costanti import (GESTIONE_SOCI, ELENCHI_SOCI, \ + GESTIONE_ATTIVITA_SEDE, GESTIONE_CORSI_SEDE, \ GESTIONE_SEDE, GESTIONE_ATTIVITA_AREA, GESTIONE_ATTIVITA, GESTIONE_CORSO, GESTIONE_AUTOPARCHI_SEDE, \ GESTIONE_GRUPPI_SEDE, GESTIONE_GRUPPO, GESTIONE_GRUPPI, GESTIONE_AREE_SEDE, GESTIONE_REFERENTI_ATTIVITA, \ GESTIONE_CENTRALE_OPERATIVA_SEDE, EMISSIONE_TESSERINI, GESTIONE_POTERI_CENTRALE_OPERATIVA_SEDE, \ @@ -23,7 +17,13 @@ RUBRICA_DELEGATI_OBIETTIVO_3, RUBRICA_DELEGATI_OBIETTIVO_4, RUBRICA_DELEGATI_OBIETTIVO_6, \ RUBRICA_DELEGATI_GIOVANI, RUBRICA_RESPONSABILI_AREA, RUBRICA_REFERENTI_ATTIVITA, \ RUBRICA_REFERENTI_GRUPPI, RUBRICA_CENTRALI_OPERATIVE, RUBRICA_RESPONSABILI_FORMAZIONE, \ - RUBRICA_DIRETTORI_CORSI, RUBRICA_RESPONSABILI_AUTOPARCO, RUBRICA_COMMISSARI + RUBRICA_DIRETTORI_CORSI, RUBRICA_RESPONSABILI_AUTOPARCO, RUBRICA_COMMISSARI) + + +""" +Questo modulo contiene tutte le funzioni per testare i permessi +a partire da un oggetto sul quale ho una delega ed un oggetto da testare. +""" def permessi_persona(persona): @@ -91,6 +91,7 @@ def permessi_presidente(sede): + permessi_delegato_centrale_operativa(sede) \ + _espandi(sede) + def permessi_commissario(sede): """ Permessi della delega di COMMISSARIO. @@ -124,7 +125,6 @@ def permessi_consigliere(sede): return [] - def permessi_ufficio_soci_unita(sede): """ Permessi della delega di UFFICIO SOCI. @@ -185,7 +185,7 @@ def permessi_delegato_obiettivo_1(sede): sede_espansa = sede.espandi(includi_me=True) return [ (RUBRICA_DELEGATI_OBIETTIVO_1, sede.espandi(includi_me=True, pubblici=True)), - ] + permessi_delegato_area(Area.objects.filter(sede__in=sede_espansa, obiettivo=1)) + ] + permessi_delegato_area(Area.objects.filter(sede__in=sede_espansa, obiettivo=1)) def permessi_delegato_obiettivo_2(sede): diff --git a/anagrafica/permessi/persona.py b/anagrafica/permessi/persona.py index 4841aa233..31070cbc1 100755 --- a/anagrafica/permessi/persona.py +++ b/anagrafica/permessi/persona.py @@ -1,11 +1,6 @@ -from datetime import date -from anagrafica.permessi.costanti import permesso_minimo, LETTURA -from anagrafica.permessi.espansioni import ESPANDI_PERMESSI, espandi_persona -from django.utils import timezone - -from anagrafica.permessi.funzioni import permessi_persona - -__author__ = 'alfioemanuele' +from ..permessi.costanti import permesso_minimo, LETTURA +from ..permessi.espansioni import ESPANDI_PERMESSI, espandi_persona +from ..permessi.funzioni import permessi_persona def persona_oggetti_permesso(persona, permesso, al_giorno=None, solo_deleghe_attive=True): @@ -48,8 +43,7 @@ def persona_oggetti_permesso(persona, permesso, al_giorno=None, solo_deleghe_att return qs -def persona_permessi(persona, oggetto, al_giorno=None, - solo_deleghe_attive=True): +def persona_permessi(persona, oggetto, al_giorno=None, solo_deleghe_attive=True): """ Ritorna il livello di permessi che si ha su un qualunque oggetto. @@ -140,8 +134,7 @@ def persona_permessi_almeno(persona, oggetto, minimo=LETTURA, al_giorno=None, return False -def persona_ha_permesso(persona, permesso, al_giorno=None, - solo_deleghe_attive=True): +def persona_ha_permesso(persona, permesso, al_giorno=None, solo_deleghe_attive=True): """ Dato un permesso, ritorna true se il permesso e' posseduto. :param permesso: Permesso singolo. @@ -183,4 +176,4 @@ def persona_ha_permessi(persona, *permessi, solo_deleghe_attive=True): else: # Singolo permesso if not persona.ha_permesso(p, solo_deleghe_attive=solo_deleghe_attive): return False - return True \ No newline at end of file + return True diff --git a/anagrafica/permessi/shortcuts.py b/anagrafica/permessi/shortcuts.py new file mode 100644 index 000000000..b4d6b3eda --- /dev/null +++ b/anagrafica/permessi/shortcuts.py @@ -0,0 +1,23 @@ +from ..permessi.applicazioni import (PRESIDENTE, PERMESSI_NOMI, COMMISSARIO, + PERMESSI_NOMI_DICT, UFFICIO_SOCI_UNITA, DELEGHE_RUBRICA, OBIETTIVI, + DELEGATO_OBIETTIVO_2, DELEGATO_OBIETTIVO_3, DELEGATO_OBIETTIVO_1, + REFERENTE, DELEGATO_OBIETTIVO_4, RESPONSABILE_FORMAZIONE, + DELEGATO_OBIETTIVO_6, DELEGATO_OBIETTIVO_5, RESPONSABILE_AUTOPARCO, + DELEGATO_CO, DIRETTORE_CORSO, RESPONSABILE_AREA, CONSIGLIERE, + CONSIGLIERE_GIOVANE, VICE_PRESIDENTE) + +from ..permessi.applicazioni import UFFICIO_SOCI, PERMESSI_NOMI_DICT + +from ..permessi.costanti import (GESTIONE_ATTIVITA, PERMESSI_OGGETTI_DICT, + GESTIONE_SOCI, GESTIONE_CORSI_SEDE, GESTIONE_CORSO, GESTIONE_SEDE, + GESTIONE_AUTOPARCHI_SEDE, GESTIONE_CENTRALE_OPERATIVA_SEDE) + +from ..permessi.delega import delega_permessi, delega_incarichi + +from ..permessi.incarichi import (INCARICO_GESTIONE_APPARTENENZE, + INCARICO_GESTIONE_TRASFERIMENTI, INCARICO_GESTIONE_ESTENSIONI, + INCARICO_GESTIONE_RISERVE, INCARICO_ASPIRANTE) + +from ..permessi.persona import (persona_ha_permesso, + persona_oggetti_permesso, persona_permessi, persona_permessi_almeno, + persona_ha_permessi) diff --git a/anagrafica/statistica/__init__.py b/anagrafica/statistica/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/anagrafica/statistica/applicazione.py b/anagrafica/statistica/applicazione.py new file mode 100644 index 000000000..64a99fca5 --- /dev/null +++ b/anagrafica/statistica/applicazione.py @@ -0,0 +1,49 @@ +from django import forms +from curriculum.areas import OBBIETTIVI_STRATEGICI +from formazione.models import Titolo +from collections import OrderedDict + +from .stat_costanti import ( + GENERALI, NUM_VOL_M_F, NUM_SOCI_VOL, NUM_VOL_FASCIA_ETA, NUM_NUOVI_VOL, NUM_DIMESSI, NUM_SEDI, + NUM_SEDI_NUOVE, NUMERO_CORSI, IIVV_CM, ORE_SERVIZIO, get_type, get_years, TIPO_CHOICES, FILTRO_ANNO +) + +from .qs_statistiche import ( + statistica_generale, statistica_num_vol_m_f, statistica_num_soci_vol, statistica_num_vol_fascia_eta, + statistica_num_nuovi_vol, statistica_num_dimessi, statistica_num_sedi, statistiche_num_sedi_nuove, + statistica_num_corsi, statistica_iivv_cm, statistica_ore_servizio +) + + +from .download_statistiche import ( + xlsx_generali, xlsx_num_soci_vol, xlsx_tot, xlsx_comitati_collapse +) + +''' + FUNZIONI CHE CALCOLANO LE STATISTICHE +''' + + +STATISTICHE = OrderedDict() +STATISTICHE[GENERALI] = (statistica_generale, ('statistiche_generali.html', ), xlsx_generali) +STATISTICHE[NUM_VOL_M_F] = (statistica_num_vol_m_f, ('statistiche_per_comitati_collapse.html', ), xlsx_comitati_collapse) +STATISTICHE[NUM_SOCI_VOL] = (statistica_num_soci_vol, ('statistiche_per_comitati.html', 'statistiche_totali.html', ), xlsx_num_soci_vol) +STATISTICHE[NUM_VOL_FASCIA_ETA] = (statistica_num_vol_fascia_eta, ('statistiche_per_comitati_collapse.html', ), xlsx_comitati_collapse) +STATISTICHE[NUM_NUOVI_VOL] = (statistica_num_nuovi_vol, ('statistiche_totali.html', ), xlsx_tot) +STATISTICHE[NUM_DIMESSI] = (statistica_num_dimessi, ('statistiche_totali.html', ), xlsx_tot) +STATISTICHE[NUM_SEDI] = (statistica_num_sedi, ('statistiche_totali.html', ), xlsx_tot) +STATISTICHE[NUM_SEDI_NUOVE] = (statistiche_num_sedi_nuove, ('statistiche_totali.html', ), xlsx_tot) +STATISTICHE[NUMERO_CORSI] = (statistica_num_corsi, ('statistiche_totali.html', ), xlsx_tot) +STATISTICHE[IIVV_CM] = (statistica_iivv_cm, ('statistiche_per_comitati_collapse.html', ), xlsx_comitati_collapse) +STATISTICHE[ORE_SERVIZIO] = (statistica_ore_servizio, ('statistiche_totali.html', ), xlsx_tot) + + +class ModuloStatisticheBase(forms.Form): + tipo_statistiche = forms.ChoiceField(widget=forms.Select(), choices=get_type(), required=True) + nome_corso = forms.CharField(required=False) + livello_riferimento = forms.ChoiceField(widget=forms.Select(), choices=Titolo.CDF_LIVELLI, required=False) + area_riferimento = forms.ChoiceField(widget=forms.Select(), choices=OBBIETTIVI_STRATEGICI, required=False) + tipo_filtro = forms.ChoiceField(choices=TIPO_CHOICES, initial=FILTRO_ANNO, widget=forms.RadioSelect()) + anno_di_riferimento = forms.ChoiceField(widget=forms.Select(), choices=get_years(), required=False) + dal = forms.DateField(required=False) + al = forms.DateField(required=False) diff --git a/anagrafica/statistica/download_statistiche.py b/anagrafica/statistica/download_statistiche.py new file mode 100644 index 000000000..b0fd8ceba --- /dev/null +++ b/anagrafica/statistica/download_statistiche.py @@ -0,0 +1,248 @@ +import xlsxwriter +import tempfile +from anagrafica.costanti import REGIONALE, NAZIONALE, LOCALE, TERRITORIALE +from anagrafica.statistica.stat_costanti import COLORI_COMITATI + + +def inserisci_comitati(worksheet, comitati, count, bold): + + count = count + isIntestazione = False + c = 1 + cr = 1 + + for el in comitati: + worksheet.write(count+cr, 0, str(el['comitato'].nome), bold) + cr += 2 + for k, v in el['statistiche'].items(): + if not isIntestazione: + worksheet.write(c, count, str(k), bold) + worksheet.write(c + 1, count, str(v)) + count += 1 + isIntestazione = True + c += 2 + count = 1 + + return count + + +def inserisci_tot(worksheet, statistiche, count): + + count = count + + for k, v in statistiche.items(): + worksheet.write(count, 0, str(k)) + worksheet.write(count, 1, str(v)) + count += 1 + + return count + + +def intestazione(workbook, ws): + + nome = '' + nome_nomitato = ws['comitato'].nome + + if 'Comitato Regionale ' in nome_nomitato: + nome = nome_nomitato.replace('Comitato Regionale ', '') + elif 'Comitato dell\'Area Metropolitana di ' in nome_nomitato: + nome = nome_nomitato.replace('Comitato dell\'Area Metropolitana di ', '') + elif 'Comitato della Provincia Autonoma di ' in nome_nomitato: + nome = nome_nomitato.replace('Comitato della Provincia Autonoma di ', '') + else: + nome = nome_nomitato + + worksheet = workbook.add_worksheet(nome) + bold = workbook.add_format({'bold': True}) + c = 0 + worksheet.write(0, c, str('Comitato'), bold) + c+=1 + worksheet.write(0, c, str('Genitore'), bold) + c+=1 + for k, v in ws['statistiche'].items(): + worksheet.write(0, c, str(k), bold) + c += 1 + + # imposto una leggenda + cell_reg = workbook.add_format({'bg_color': COLORI_COMITATI[REGIONALE]}) + cell_loc = workbook.add_format({'bg_color': COLORI_COMITATI[LOCALE]}) + cell_ter = workbook.add_format({'bg_color': COLORI_COMITATI[TERRITORIALE]}) + c += 1 + worksheet.write(5, c, 'Regionale', cell_reg) + worksheet.write(6, c, 'Locale', cell_loc) + worksheet.write(7, c, 'Territoriale', cell_ter) + + return worksheet + + +def xlsx_num_soci_vol(obj): + + new_file, filename = tempfile.mkstemp() + + workbook = xlsxwriter.Workbook(filename) + bold = workbook.add_format({'bold': True}) + + worksheet = workbook.add_worksheet('Regionali') + + count = 1 + + if 'regionali' in obj: + count = inserisci_comitati(worksheet, obj['regionali'], count, bold) + + count += 5 + if 'tot' in obj: + for el in obj['tot']: + worksheet.write(1, count, str(el['nome']), bold) + count += 1 + for k, v in el['statistiche'].items(): + worksheet.write(0, count, str(k), bold) + worksheet.write(1, count, str(v)) + count += 1 + + workbook.close() + + return filename + + +def xlsx_tot(obj, ws=False): + + new_file, filename = tempfile.mkstemp() + + workbook = xlsxwriter.Workbook(filename) + bold = workbook.add_format({'bold': True}) + + worksheet = None + isIntestazione = False + count = 1 + + if not ws: + worksheet = workbook.add_worksheet('Statistiche totali') + + if 'tot' in obj: + c = 0 + for el in obj['tot']: + worksheet.write(c+1, 0, str(el['nome']), bold) + for k, v in el['statistiche'].items(): + if not isIntestazione: + worksheet.write(c, count, str(k), bold) + worksheet.write(c+1, count, str(v)) + count += 1 + isIntestazione = True + c += 2 + count = 1 + + + + workbook.close() + + return filename + + +def scrivi_comitato(workbook, worksheet, count, comitato): + + cell_color = workbook.add_format({'bg_color': COLORI_COMITATI[comitato['comitato'].estensione]}) + + c = 0 + worksheet.write(count, c, str(comitato['comitato'].nome) + '(' + comitato['comitato'].estensione + ')', cell_color) + c += 1 + worksheet.write(count, c, str(comitato['comitato'].genitore.nome), cell_color) + c += 1 + for k, v in comitato['statistiche'].items(): + worksheet.write(count, c, v, cell_color) + c += 1 + count += 1 + return count + + +def inserisci_comitati_ric(worksheet=None, workbook=None, comitati=[], count=0): + + count = count + + for com in comitati: + if com['comitato'].estensione == NAZIONALE: + count = inserisci_comitati_ric( + workbook=workbook, + comitati=com['figli'], + count=1 + ) + break + + if com['comitato'].estensione == REGIONALE: + worksheet = intestazione(workbook, com) + count = scrivi_comitato(workbook, worksheet, 1, com) + else: + count = scrivi_comitato(workbook, worksheet, count, com) + + if com['figli']: + count = inserisci_comitati_ric( + worksheet=worksheet, + workbook=workbook, + comitati=com['figli'], + count=count + ) + + return count + + +def xlsx_generali(obj, ws=False): + new_file, filename = tempfile.mkstemp() + + workbook = xlsxwriter.Workbook(filename) + + bold = workbook.add_format({'bold': True}) + + worksheet = workbook.add_worksheet('Genarali') + + # HEADER + worksheet.write(0, 0, str('Nome metrica'), bold) + worksheet.write(0, 1, str('Valore metrica'), bold) + worksheet.write(0, 2, str('Rapporti'), bold) + worksheet.write(0, 3, str('Descrizione'), bold) + + # Numero di Persone + worksheet.write(1, 0, str('Numero di Persone')) + worksheet.write(1, 1, obj['persone_numero']) + worksheet.write(1, 2, str('')) + worksheet.write(1, 3, str('Include tutti i Soggetti registrati su Gaia.')) + + # Numero di Soci CRI + worksheet.write(2, 0, str('Numero di Soci CRI')) + worksheet.write(2, 1, obj['soci_numero']) + worksheet.write(2, 2, str(obj['soci_percentuale']) + '% ' + 'delle Persone') + worksheet.write(2, 3, str('Persone con appartenenza attuale come Socio CRI.')) + + # Numero di Soci CRI Giovani + worksheet.write(3, 0, str('Numero di Soci CRI Giovani')) + worksheet.write(3, 1, obj['soci_giovani_35_numero']) + worksheet.write(3, 2, str(obj['soci_giovani_35_percentuale']) + ' % ' + 'dei Soci CRI') + worksheet.write(3, 3, str('Soci CRI con età inferiore ai 36 anni')) + + # Numero di Sedi CRI + worksheet.write(4, 0, str('Numero di Sedi CRI')) + worksheet.write(4, 1, obj['sedi_numero']) + worksheet.write(4, 2, str('')) + worksheet.write(4, 3, str('Include Sede Nazionale, Regionali, Comitati e Unità Territoriali distaccate.')) + + # Numero di Comitati CRI + worksheet.write(5, 0, str('Numero di Comitati CRI')) + worksheet.write(5, 1, obj['comitati_numero']) + worksheet.write(5, 2, str('')) + worksheet.write(5, 3, str('Include Sede Nazionale, Regionali e Comitati.')) + + workbook.close() + + return filename + + +def xlsx_comitati_collapse(obj, ws=False): + new_file, filename = tempfile.mkstemp() + + workbook = xlsxwriter.Workbook(filename) + + comitati = obj['comitati'] + + inserisci_comitati_ric(workbook=workbook, comitati=comitati, count=1) + + workbook.close() + + return filename diff --git a/anagrafica/statistica/qs_statistiche.py b/anagrafica/statistica/qs_statistiche.py new file mode 100644 index 000000000..a348b9eb3 --- /dev/null +++ b/anagrafica/statistica/qs_statistiche.py @@ -0,0 +1,818 @@ +from anagrafica.models import Appartenenza, Persona, Sede, Partecipazione, Appartenenza +from formazione.models import CorsoBase +from anagrafica.costanti import TERRITORIALE, REGIONALE, NAZIONALE, LOCALE, ESTENDIONI_DICT, PROVINCIALE +import datetime +from datetime import datetime +from formazione.models import Corso +from django.db.models import Q, F, Sum +from .stat_costanti import ( + STATISTICA, COMITATI_DA_EXLUDERE, NUM_VOL_M_F, NUM_SOCI_VOL, NUM_NUOVI_VOL, FILTRO_ANNO, FILTRO_DATA, + NUM_SEDI, NUM_DIMESSI, NUM_SEDI_NUOVE, NUMERO_CORSI, IIVV_CM, ORE_SERVIZIO, QUERY_NUM_ORE +) +from collections import OrderedDict +from django.db import connection + + +def formato_data(data): + return data.strftime('%d-%m-%Y') + + +def get_statistica_collapse_ric(comitati, f, **kwargs): + estensione = {NAZIONALE: REGIONALE, REGIONALE: LOCALE, PROVINCIALE: False, LOCALE: TERRITORIALE, TERRITORIALE: None} + obj = [] + for comitato in comitati: + if comitato.estensione == REGIONALE: + # trattare i provinciali come i locali ricordarsi che al di sotto dei provinciali ci sono i locali + locali = Sede.objects.filter(estensione=LOCALE, genitore=comitato, attiva=True) + provinciali = Sede.objects.filter(estensione=PROVINCIALE, genitore=comitato, attiva=True) + + locali = list(locali) + list(provinciali) + + for provinciale in provinciali: + locali += list(Sede.objects.filter(genitore=provinciale, estensione=LOCALE, attiva=True)) + + obj.append( + { + "comitato": comitato, + "statistiche": f(comitato=comitato, **{'figli': True}) if comitato.estensione != PROVINCIALE else f(comitato=comitato, **{'figli': False}), + "figli": get_statistica_collapse_ric( + comitati=locali, f=f, **kwargs + ) + } + ) + continue + else: + obj.append( + { + "comitato": comitato, + "statistiche": f(comitato=comitato, **{'figli': True}) if comitato.estensione != PROVINCIALE else f(comitato=comitato, **{'figli': False}), + "figli": get_statistica_collapse_ric( + comitati=Sede.objects.filter( + genitore=comitato, estensione=estensione[comitato.estensione], attiva=True + ).exclude(nome__contains='Provinciale Di Roma') if estensione[comitato.estensione] else [], + f=f, **kwargs + ) + } + ) + return obj + + +''' + STATISTICHE PER FASCI DI ETA + Numero di volontari per fasce: + 14/18 - 18/25 - 25/32 - 32/45 - 45/60 - over 60 + per Ogni comitato Nazionale/Regione/locale/territoriale +''' + + +def statistica_num_vol_fascia_eta(**kwargs): + def get_num_vol_per_eta(comitato=None, **kwargs): + + def count(query, min=0, max=9999): + i = 0 + for el in query: + if min <= el.eta <= max: + i += 1 + return i + + figli = kwargs.get('figli') if kwargs.get('figli') else False + persone = comitato.membri_attuali(figli=figli, membro=Appartenenza.VOLONTARIO) + result = OrderedDict() + result["Da 14 a 18"] = count(persone, 14, 18) + result["Da 18 a 25"] = count(persone, 18, 25) + result["Da 25 a 32"] = count(persone, 25, 32) + result["Da 32 a 45"] = count(persone, 32, 45) + result["Da 45 a 60"] = count(persone, 45, 60) + result["Over 60"] = count(persone, 60) + return result + + obj = { + "nome": STATISTICA[NUM_VOL_M_F], + "comitati": get_statistica_collapse_ric( + comitati=Sede.objects.filter(estensione=NAZIONALE), f=get_num_vol_per_eta + ) + } + + return obj + + +''' + STATISTICA NUMERO M/F + Numero di volontario divesi per sesso + Per ogni comitato Nazionale/Regionale/Locale/Territoriale +''' + + +def statistica_num_vol_m_f(**kwargs): + def get_m_f_statistiche(comitato=None, **kwargs): + figli = kwargs.get('figli') if kwargs.get('figli') else False + persone = comitato.membri_attuali(figli=figli, membro=Appartenenza.VOLONTARIO) + + return { + "M": persone.filter(genere=Persona.MASCHIO).count(), + "F": persone.filter(genere=Persona.FEMMINA).count() + } + + obj = { + "nome": STATISTICA[NUM_VOL_M_F], + "comitati": get_statistica_collapse_ric( + comitati=Sede.objects.filter(estensione=NAZIONALE), f=get_m_f_statistiche + ) + } + + return obj + + +''' + STATISTICA NUMERO DI SOCI/VOLONTARI + Per Ogni comitato Regionale + valori visibili per anno corrente/precedente +''' + + +def statistica_num_soci_vol(**kwargs): + regionali = Sede.objects.filter(estensione=REGIONALE).exclude( + nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + totale_regione_soci_currente = 0 + totale_regione_volontari_current = 0 + totale_regione_soci_before = 0 + totale_regione_volontari_before = 0 + + regione_soci_volontari = [] + + finish_current = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento'))) + current_year = finish_current.year + finish_before = finish_current.replace(year=finish_current.year - 1) + before_year = finish_before.year + + for regione in regionali: + regione_soci_current = int( + regione.membri_attuali( + figli=True, membro__in=Appartenenza.MEMBRO_SOCIO, creazione__lte=finish_current + ).count() + ) + regione_soci_before = int( + regione.membri_attuali( + figli=True, membro__in=Appartenenza.MEMBRO_SOCIO, creazione__lte=finish_before + ).count() + ) + regione_volontari_current = int( + regione.membri_attuali( + figli=True, membro=Appartenenza.VOLONTARIO, creazione__lte=finish_current + ).count() + ) + regione_volontari_before = int( + regione.membri_attuali( + figli=True, membro=Appartenenza.VOLONTARIO, creazione__lte=finish_before + ).count() + ) + + regione_soci_volontari += [ + { + "comitato": regione, + "statistiche": { + "Soci al {}:".format(current_year): regione_soci_current, + "Volontari al {}:".format(current_year): regione_volontari_current, + "Soci al {}:".format(before_year): regione_soci_before, + "Volontari al {}:".format(before_year): regione_volontari_before, + } + } + ] + + totale_regione_soci_currente += regione_soci_current + totale_regione_volontari_current += regione_volontari_current + totale_regione_soci_before += regione_soci_before + totale_regione_volontari_before += regione_volontari_before + + obj = { + "nome": STATISTICA[NUM_SOCI_VOL], + "regionali": regione_soci_volontari, + "tot": [ + { + "nome": "Regionale", + "statistiche": { + "Totale Soci al {}".format(current_year): totale_regione_soci_currente, + "Totale Volontari al {}".format(current_year): totale_regione_volontari_current, + "Totale Soci al {}".format(before_year): totale_regione_soci_before, + "Totale Volontari al {}".format(before_year): totale_regione_volontari_before, + }, + } + ] + } + + return obj + + +''' + STATISTICHE GENERALI (DEFAULT) + persone_numero + soci_numero + soci_percentuale + soci_giovani_35_numero + soci_giovani_35_percentuale + sedi_numero + comitati_numero + totale_regione_soci + totale_regione_volontari +''' + + +def statistica_generale(**kwargs): + import datetime + oggi = datetime.date.today() + nascita_minima_35 = datetime.date(oggi.year - 36, oggi.month, oggi.day) + persone = Persona.objects.all() + + soci = Persona.objects.filter( + Appartenenza.query_attuale(membro__in=Appartenenza.MEMBRO_SOCIO).via("appartenenze") + ).distinct('nome', 'cognome', 'codice_fiscale') + soci_giovani_35 = soci.filter( + data_nascita__gt=nascita_minima_35, + ) + + sedi = Sede.objects.filter(attiva=True) + comitati = sedi.comitati() + + obj = { + "persone_numero": persone.count(), + "soci_numero": soci.count(), + "soci_percentuale": soci.count() / persone.count() * 100, + "soci_giovani_35_numero": soci_giovani_35.count(), + "soci_giovani_35_percentuale": soci_giovani_35.count() / soci.count() * 100, + "sedi_numero": sedi.count(), + "comitati_numero": comitati.count() + } + return obj + + +''' + STATISTICHE NUMERO NUOVI VOLONTARI + livello nazionale/regionale/locale/territoriale + valori visibili per anno corrente/precedente +''' + + +def statistica_num_nuovi_vol(**kwargs): + def get_tot_anno(start=None, finish=None, estensione=None): + + current_year = start.year + before_year = start.replace(year=current_year - 1).year + + current = Appartenenza.objects.filter( + creazione__gte=start, + creazione__lte=finish, + terminazione=None, + membro=Appartenenza.VOLONTARIO, + sede__estensione=estensione, + ) + + before = Appartenenza.objects.filter( + creazione__gte=start.replace(year=start.year - 1), + creazione__lte=finish.replace(year=finish.year - 1), + terminazione=None, + membro=Appartenenza.VOLONTARIO, + sede__estensione=estensione, + ) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale nuovi volontari al {}'.format(current_year): current.count(), + 'Totale nuovi volontari al {}'.format(before_year): before.count() + } + } + + def get_tot_date(start=None, finish=None, estensione=None): + + current = Appartenenza.objects.filter( + creazione__gte=start, + creazione__lte=finish, + terminazione=None, + membro=Appartenenza.VOLONTARIO, + sede__estensione=estensione, + ) + + start = formato_data(start) + finish = formato_data(finish) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale nuovi volontari dal {} al {}'.format(start, finish): current.count() + } + } + + obj = { + "nome": STATISTICA[NUM_NUOVI_VOL], + "tot": [] + } + + tipo = kwargs.get('tipo_filtro') + + if tipo == FILTRO_ANNO: + start = datetime.now().date().replace(month=1, day=1, year=int(kwargs.get('anno_di_riferimento'))) + finish = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento'))) + obj['tot'] = [ + get_tot_anno(start, finish, NAZIONALE), + get_tot_anno(start, finish, REGIONALE), + get_tot_anno(start, finish, LOCALE), + get_tot_anno(start, finish, TERRITORIALE), + ] + elif tipo == FILTRO_DATA: + start = kwargs.get('dal') + finish = kwargs.get('al') + obj['tot'] = [ + get_tot_date(start, finish, NAZIONALE), + get_tot_date(start, finish, REGIONALE), + get_tot_date(start, finish, LOCALE), + get_tot_date(start, finish, TERRITORIALE), + ] + + return obj + + +''' + STATISTICA NUMERO DIMESSI + livello nazionale/regionale/locale/territoriale + valori visibili per anno corrente/precedente +''' + + +def statistica_num_dimessi(**kwargs): + def get_tot_anno(start=None, finish=None, estensione=None): + + current_year = start.year + before_year = current_year - 1 + + current = Appartenenza.objects.filter( + creazione__gte=start, + creazione__lte=finish, + terminazione=Appartenenza.DIMISSIONE, + sede__estensione=estensione, + ) + + before = Appartenenza.objects.filter( + creazione__gte=start.replace(year=before_year), + creazione__lte=finish.replace(year=before_year), + terminazione=Appartenenza.DIMISSIONE, + sede__estensione=estensione, + ) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale al {}'.format(current_year): current.count(), + 'Totale al {}'.format(before_year): before.count() + } + } + + def get_tot_date(start=None, finish=None, estensione=None): + + current = Appartenenza.objects.filter( + creazione__gte=start, + creazione__lte=finish, + terminazione=Appartenenza.DIMISSIONE, + sede__estensione=estensione, + ) + start = formato_data(start) + finish = formato_data(finish) + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale Volontari dimessi dal {} al {}'.format(start, finish): current.count() + } + } + + obj = { + "nome": STATISTICA[NUM_DIMESSI], + "tot": [] + } + + tipo = kwargs.get('tipo_filtro') + + if tipo == FILTRO_ANNO: + start = datetime.now().date().replace(month=1, day=1, year=int(kwargs.get('anno_di_riferimento'))) + finish = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento'))) + obj['tot'] = [ + get_tot_anno(start, finish, NAZIONALE), + get_tot_anno(start, finish, REGIONALE), + get_tot_anno(start, finish, LOCALE), + get_tot_anno(start, finish, TERRITORIALE), + ] + elif tipo == FILTRO_DATA: + start = kwargs.get('dal') + finish = kwargs.get('al') + obj['tot'] = [ + get_tot_date(start, finish, NAZIONALE), + get_tot_date(start, finish, REGIONALE), + get_tot_date(start, finish, LOCALE), + get_tot_date(start, finish, TERRITORIALE), + ] + + return obj + + +''' + STATISTICHE NUMERO SEDI + livello nazionale/regionale/locale/territoriale + valori visibili per anno corrente/precedente +''' + + +def statistica_num_sedi(**kwargs): + def get_tot_anno(current=None, before=None, estensione=None): + current_attivo = Sede.objects.filter( + creazione__lte=current, + estensione=estensione, + attiva=True + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + current_disattivo = Sede.objects.filter( + creazione__lte=current, + estensione=estensione, + attiva=False + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + before_attivo = Sede.objects.filter( + creazione__lte=before, + estensione=estensione, + attiva=True + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + before_disattivo = Sede.objects.filter( + creazione__lte=before, + estensione=estensione, + attiva=False + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale attivo {}'.format(current.year): current_attivo.count(), + 'Totale disattivo {}'.format(current.year): current_disattivo.count(), + 'Totale attivo {}'.format(before.year): before_attivo.count(), + 'Totale disattivo {}'.format(before.year): before_disattivo.count(), + } + } + + obj = { + "nome": STATISTICA[NUM_SEDI], + "tot": [] + } + + start = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento'))) + finish = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento')) - 1) + obj['tot'] = [ + get_tot_anno(start, finish, NAZIONALE), + get_tot_anno(start, finish, REGIONALE), + get_tot_anno(start, finish, LOCALE), + get_tot_anno(start, finish, TERRITORIALE), + ] + + return obj + + +''' + STATISTICHE NUMERO NUOVE SEDI + numero di nuove sedi nell'anno corrente/precedente + + livello nazionale/regionale/locale/territoriale + valori visibili per anno corrente/precedente +''' + + +def statistiche_num_sedi_nuove(**kwargs): + def get_tot_anno(start=None, finish=None, estensione=None): + + current_year = start.year + before_year = start.replace(year=current_year - 1).year + + current = Sede.objects.filter( + creazione__gte=start, + creazione__lte=finish, + estensione=estensione, + attiva=True + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + before = Sede.objects.filter( + creazione__gte=start.replace(year=before_year), + creazione__lte=finish.replace(year=before_year), + estensione=estensione, + attiva=True + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale nuove sedi al {}'.format(current_year): current.count(), + 'Totale nuove sedi al {}'.format(before_year): before.count(), + } + } + + def get_tot_date(start=None, finish=None, estensione=None): + + current = Sede.objects.filter( + creazione__gte=start, + creazione__lte=finish, + estensione=estensione, + attiva=True + ).exclude(nome__contains=(Q(nome__contains=x) for x in COMITATI_DA_EXLUDERE)) + start = formato_data(start) + finish = formato_data(finish) + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + 'Totale nuove sedi dal {} al {}'.format(start, finish): current.count() + } + } + + obj = { + "nome": STATISTICA[NUM_SEDI_NUOVE], + "tot": [] + } + + tipo = kwargs.get('tipo_filtro') + + if tipo == FILTRO_ANNO: + start = datetime.now().date().replace(month=1, day=1, year=int(kwargs.get('anno_di_riferimento'))) + finish = datetime.now().date().replace(month=12, day=31, year=int(kwargs.get('anno_di_riferimento'))) + obj['tot'] = [ + get_tot_anno(start, finish, NAZIONALE), + get_tot_anno(start, finish, REGIONALE), + get_tot_anno(start, finish, LOCALE), + get_tot_anno(start, finish, TERRITORIALE), + ] + elif tipo == FILTRO_DATA: + start = kwargs.get('dal') + finish = kwargs.get('al') + obj['tot'] = [ + get_tot_date(start, finish, NAZIONALE), + get_tot_date(start, finish, REGIONALE), + get_tot_date(start, finish, LOCALE), + get_tot_date(start, finish, TERRITORIALE), + ] + + return obj + + +def statistica_num_corsi(**kwargs): + def get_tot_anno(estensione=None, **kwargs): + livello_riferimento = kwargs.get('livello_riferimento') + nome_corso = kwargs.get('nome_corso') + area_riferimento = kwargs.get('area_riferimento') + + print(livello_riferimento, nome_corso, area_riferimento, kwargs.get('anno')) + + filter_name = lambda x: nome_corso in x.nome + + corsi_current = CorsoBase.objects.filter( + anno=kwargs.get('anno'), + sede__estensione=estensione, + cdf_level=livello_riferimento, + cdf_area=area_riferimento + ) + + corsi_before = CorsoBase.objects.filter( + anno=kwargs.get('anno') - 1, + sede__estensione=estensione, + cdf_level=livello_riferimento, + cdf_area=area_riferimento + ) + + corsi_current_attivi = corsi_current.filter(stato=Corso.ATTIVO) + corsi_current_disattivi = corsi_current.filter(stato=Corso.TERMINATO) + corsi_before_attivi = corsi_before.filter(stato=Corso.ATTIVO) + corsi_before_disattivi = corsi_before.filter(stato=Corso.TERMINATO) + + if nome_corso: + corsi_current_attivi = filter(filter_name, corsi_current_attivi) + corsi_current_disattivi = filter(filter_name, corsi_current_disattivi) + corsi_before_attivi = filter(filter_name, corsi_before_attivi) + corsi_before_disattivi = filter(filter_name, corsi_before_disattivi) + + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + "Corsi nel {} Attivi".format(anno): len(list(corsi_current_attivi)), + "Corsi nel {} Terminati".format(anno): len(list(corsi_current_disattivi)), + "Corsi nel {} Attivi".format(anno - 1): len(list(corsi_before_attivi)), + "Corsi nel {} Terminati".format(anno - 1): len(list(corsi_before_disattivi)), + } + } + + def get_tot_date(start=True, finish=True, estensione=None, **kwargs): + livello_riferimento = kwargs.get('livello_riferimento') + nome_corso = kwargs.get('nome_corso') + area_riferimento = kwargs.get('area_riferimento') + + filter_name = lambda x: nome_corso in x.nome + + corsi = CorsoBase.objects.filter( + creazione__lte=finish, + creazione__gte=start, + sede__estensione=estensione, + cdf_level=livello_riferimento, + cdf_area=area_riferimento + ) + + corsi_attivi = corsi.filter(stato=Corso.ATTIVO) + corsi_disattivi = corsi.filter(stato=Corso.TERMINATO) + + if nome_corso: + corsi_attivi = filter(filter_name, corsi_attivi) + corsi_disattivi = filter(filter_name, corsi_disattivi) + start = formato_data(start) + finish = formato_data(finish) + return { + "nome": ESTENDIONI_DICT[estensione], + "statistiche": { + "Corsi Attivi dal {} al {}".format(start, finish): len(list(corsi_attivi)), + "Corsi Disattivi dal {} al {}".format(start, finish): len(list(corsi_disattivi)) + } + } + + obj = { + "nome": STATISTICA[NUMERO_CORSI], + "tot": [] + } + + tipo = kwargs.get('tipo_filtro') + anno = int(kwargs.get('anno_di_riferimento')) + if tipo == FILTRO_ANNO: + obj['tot'] = [ + get_tot_anno(estensione=NAZIONALE, anno=anno, **kwargs), + get_tot_anno(estensione=REGIONALE, anno=anno, **kwargs), + get_tot_anno(estensione=LOCALE, anno=anno, **kwargs), + get_tot_anno(estensione=TERRITORIALE, anno=anno, **kwargs), + ] + elif tipo == FILTRO_DATA: + start = kwargs.get('dal') + finish = kwargs.get('al') + obj['tot'] = [ + get_tot_date(start=start, finish=finish, estensione=NAZIONALE, **kwargs), + get_tot_date(start=start, finish=finish, estensione=REGIONALE, **kwargs), + get_tot_date(start=start, finish=finish, estensione=LOCALE, **kwargs), + get_tot_date(start=start, finish=finish, estensione=TERRITORIALE, **kwargs), + ] + + return obj + + +''' + STATISTICHE IIVV/CM + + livello nazionale/regionale/locale/territoriale +''' + + +def statistica_iivv_cm(**kwargs): + def get_iivv_cm(comitato, **kwargs): + figli = kwargs.get('figli') if kwargs.get('figli') else False + membri_iv_n = comitato.membri_attuali(figli=figli).filter(iv=True) + membri_cm_n = comitato.membri_attuali(figli=figli).filter(cm=True) + + return { + "IIVV": membri_iv_n.count(), + "CM": membri_cm_n.count(), + } + + obj = { + "nome": STATISTICA[IIVV_CM], + "comitati": get_statistica_collapse_ric( + comitati=Sede.objects.filter(estensione=NAZIONALE), f=get_iivv_cm + ) + } + + print(obj) + + return obj + + +''' + STATISTICHE ORE DI SERVIZIO + + livello nazionale/regionale/locale/territoriale +''' + + +def statistica_ore_servizio(**kwargs): + + def get_tot(estensioni=[], inizio=None, fine=None, row=[], no_turni=[]): + + c_0, c_0_10, c_10_20, c_20_30, c_30, count = 0, 0, 0, 0, 0, 0 + c_0_b, c_0_10_b, c_10_20_b, c_20_30_b, c_30_b, count_b = 0, 0, 0, 0, 0, 0 + + for el in row: + for est in estensioni: + if int(el['anno']) == inizio: + if el['estensione'] == est: + durata = el['durata'].seconds//3600 + count += 1 + if 0 < durata <= 10: + c_0_10 += 1 + elif 10 < durata <= 20: + c_10_20 += 1 + elif 20 < durata <= 30: + c_20_30 += 1 + elif durata > 30: + c_30 += 1 + elif int(el['anno']) == fine: + if el['estensione'] == est: + durata = el['durata'].seconds//3600 + count_b += 1 + if 0 < durata <= 10: + c_0_10_b += 1 + elif 10 < durata <= 20: + c_10_20_b += 1 + elif 20 < durata <= 30: + c_20_30_b += 1 + elif durata > 30: + c_30_b += 1 + + + statistica = OrderedDict([ + ("Uguale a 0h di servizio al {}".format(inizio), no_turni[0]-count), + ("Da 0h a 10h di servizio al {}".format(inizio), c_0_10), + ("Da 10h a 20h di servizio al {}".format(inizio), c_10_20), + ("Da 20h a 30h di servizio al {}".format(inizio), c_20_30), + ("Maggiore di 30h di servizio al {}".format(inizio), c_30), + ('', ''),# per mantenere un spazio tra gli anni di riferimento + ("Uguale a 0h di servizio al {}".format(fine), no_turni[0]-count_b), + ("Da 0h a 10h di servizio al {}".format(fine), c_0_10_b), + ("Da 10h a 20h di servizio al {}".format(fine), c_10_20_b), + ("Da 20h a 30h di servizio al {}".format(fine), c_20_30_b), + ("Maggiore di 30h di servizio al {}".format(fine), c_30_b), + ]) + + return { + "nome": ESTENDIONI_DICT[estensioni[0]], + "statistiche": statistica, + } + + inizio = int(kwargs.get('anno_di_riferimento')) + fine = int(kwargs.get('anno_di_riferimento'))-1 + + p = Partecipazione.objects.select_related('turno').filter( + Q(turno__inizio__year=fine) | Q(turno__inizio__year=inizio), + ).extra({'anno': "Extract(year from attivita_turno.inizio)"}).values( + 'persona', 'turno__attivita__sede__estensione', 'anno' + ).annotate(estensione=F('turno__attivita__sede__estensione'), durata=Sum(F('turno__fine') - F('turno__inizio'))) + + storico_all = Partecipazione.objects.filter( + turno__fine__gte=datetime.now().date().replace(month=1, day=1, year=fine), + turno__inizio__lte=datetime.now().date().replace(month=12, day=12, year=inizio), + confermata=True, + )#.values_list('persona_id', flat=True) + + persone_all = Persona.objects.filter( + Appartenenza.query_attuale( + membro__in=Appartenenza.MEMBRO_ATTIVITA, + ).via("appartenenze") + ) + + def calcola_no_turni(estensioni=[], storico_all=[], persone_all=[], inizio=None, fine=None): + persone_all = persone_all.filter( + Appartenenza.query_attuale( + sede__estensione__in=estensioni + ).via("appartenenze") + ) + inizio = persone_all.exclude(pk__in=storico_all.filter( + turno__fine__gte=datetime.now().date().replace(month=1, day=1, year=inizio), + turno__inizio__lte=datetime.now().date().replace(month=12, day=12, year=inizio), + ).values_list('persona_id', flat=True)).count() + + fine = persone_all.exclude(pk__in=storico_all.filter( + turno__fine__gte=datetime.now().date().replace(month=1, day=1, year=fine), + turno__inizio__lte=datetime.now().date().replace(month=12, day=12, year=fine), + ).values_list('persona_id', flat=True)).count() + + return [inizio, fine] + + obj = { + "nome": STATISTICA[ORE_SERVIZIO], + "tot": [ + get_tot( + [NAZIONALE], inizio, fine, p, calcola_no_turni( + [NAZIONALE], storico_all, persone_all, inizio, fine + ) + ), + get_tot( + [REGIONALE], inizio, fine, p, calcola_no_turni( + [REGIONALE], storico_all, persone_all, inizio, fine + ) + ), + get_tot( + [LOCALE, PROVINCIALE], inizio, fine, p, calcola_no_turni( + [LOCALE, PROVINCIALE], storico_all, persone_all, inizio, fine + ) + ), + get_tot( + [TERRITORIALE], inizio, fine, p, calcola_no_turni( + [TERRITORIALE], storico_all, persone_all, inizio, fine + ) + ) + ] + } + + return obj diff --git a/anagrafica/statistica/stat_costanti.py b/anagrafica/statistica/stat_costanti.py new file mode 100644 index 000000000..3ad1b9abf --- /dev/null +++ b/anagrafica/statistica/stat_costanti.py @@ -0,0 +1,93 @@ +import datetime +from datetime import datetime +from anagrafica.costanti import TERRITORIALE, REGIONALE, LOCALE, PROVINCIALE +from collections import OrderedDict + +ANNI_DI_RIFERIMENTO = 10 + + +def get_years(): + l = [] + current_year = datetime.now().year + for i in range(0, ANNI_DI_RIFERIMENTO): + year = "{}/{}".format(current_year - i, current_year - i - 1) + l.append( + (current_year - i, year) + ) + return l + + +def get_type(): + return [(k, v) for k, v in STATISTICA.items()] + + +''' + VALORI STATISTICHE +''' +GENERALI = 'generali' +NUM_SOCI_VOL = 'num_soci_vol' +NUM_VOL_M_F = 'num_vol_m_f' +NUM_VOL_FASCIA_ETA = 'num_vol_fascia_eta' +NUM_NUOVI_VOL = 'num_nuovi_vol' +NUM_DIMESSI = 'num_dimessi' +NUM_SEDI = 'num_sedi' +NUM_SEDI_NUOVE = 'num_sedi_nuove' +NUMERO_CORSI = 'num_corsi' +IIVV_CM = 'iivv_cm' +ORE_SERVIZIO = 'ore_servizio' + +''' + NOMI VISUALIZZATI STATISTICHE +''' +STATISTICA = OrderedDict() +STATISTICA[GENERALI] = "Generali" +STATISTICA[NUM_VOL_M_F] = "Volontari M/F" +STATISTICA[NUM_SOCI_VOL] = "Soci e Volontari" +STATISTICA[NUM_VOL_FASCIA_ETA] = "Volontari per fascia di età" +STATISTICA[NUM_NUOVI_VOL] = "Nuovi volontari" +STATISTICA[NUM_DIMESSI] = "Dimessi" +STATISTICA[NUM_SEDI] = "Sedi" +STATISTICA[NUM_SEDI_NUOVE] = "Sedi nuove" +STATISTICA[NUMERO_CORSI] = "Corsi" +STATISTICA[IIVV_CM] = "IIVV/CM" +STATISTICA[ORE_SERVIZIO] = "Ore di Servizio" + + +FILTRO_ANNO = 'ANNO' +FILTRO_DATA = 'DATA' + +TIPO_CHOICES = [ + (FILTRO_ANNO, 'Anno'), + (FILTRO_DATA, 'Data'), +] + +COMITATI_DA_EXLUDERE = ["Comitato dell'Area Metropolitana di Roma Capitale", ] + +COLORI_COMITATI = { + PROVINCIALE: '#C0C0C0', + REGIONALE: '#FF0000', + LOCALE: '#C0C0C0', + TERRITORIALE: '#00FFFF', +} + + +QUERY_NUM_ORE = ''' + select storico_all_persone_attivita.persona_id, storico_all_persone_attivita.anno, estensione,sum(storico_all_persone_attivita.durata_turno) + from ( + select storico_all_persone.*, attivita.sede_id + from ( + select + partecipazione.id as partecipazione_id, + turno.attivita_id as attivita_id, + partecipazione.persona_id as persona_id, + EXTRACT(YEAR FROM turno.inizio) as anno, + EXTRACT(HOURS FROM (turno.fine-turno.inizio)) as durata_turno + from attivita_partecipazione as partecipazione + join attivita_turno as turno on partecipazione.turno_id = turno.id + ) as storico_all_persone + join attivita_attivita as attivita on storico_all_persone.attivita_id = attivita.id + ) as storico_all_persone_attivita + join anagrafica_sede as sede on storico_all_persone_attivita.sede_id = sede.id + where storico_all_persone_attivita.anno = '{}' or storico_all_persone_attivita.anno = '{}' + group by storico_all_persone_attivita.persona_id, storico_all_persone_attivita.anno, estensione +''' diff --git a/anagrafica/statistica/utils.py b/anagrafica/statistica/utils.py new file mode 100644 index 000000000..ebb931749 --- /dev/null +++ b/anagrafica/statistica/utils.py @@ -0,0 +1,3 @@ +from datetime import datetime + + diff --git a/anagrafica/templates/admin_statistiche.html b/anagrafica/templates/admin_statistiche.html index 07e2eaf86..ec6fd58d6 100644 --- a/anagrafica/templates/admin_statistiche.html +++ b/anagrafica/templates/admin_statistiche.html @@ -12,6 +12,7 @@

 

+

Statistiche @@ -25,92 +26,175 @@

Tutti i dati in questa pagina sono calcolati in tempo reale.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% for regione, soci, volontari in regione_soci_volontari %} - - - - - - - {% endfor %} - - - - - - - - - - - - - - - - - - -
Nome metricaValore metricaRapportiDescrizione
Numero di Persone{{ persone_numero }} Include tutti i Soggetti registrati su Gaia.
Numero di Soci CRI{{ soci_numero }}{{ soci_percentuale }}% delle PersonePersone con appartenenza attuale come Socio CRI.
Numero di Soci CRI Giovani{{ soci_giovani_35_numero }}{{ soci_giovani_35_percentuale }}% dei Soci CRI - Soci CRI con età inferiore ai 36 anni -
Numero di Sedi CRI{{ sedi_numero }} Include Sede Nazionale, Regionali, Comitati e - Unità Territoriali distaccate.
Numero di Comitati CRI{{ comitati_numero }} Include Sede Nazionale, Regionali e Comitati.
Numero di Soci e Volontari per regione{{ regione.link|safe }} - {{ soci }} soci
- {{ volontari }} volontari -
Totale Soci{{ totale_regione_soci }} Somma dei valori per Regione.
Totale Volontari{{ totale_regione_volontari }} Somma dei valori per Regione.
+{# {% bootstrap_formset formset %}#} + +
+ +
+ +
+
+

+ + Statistiche +

+
+
+
+

+ + Selezione una delle tipologie di statistiche +

+ {% csrf_token %} +

{% bootstrap_form modulo %}

+ +
+
+
+ {% for v in views %} + {% include v %} + {% endfor %}

-{% endblock %} + +{% endblock %} diff --git a/anagrafica/templates/anagrafica_utente_curriculum.html b/anagrafica/templates/anagrafica_utente_curriculum.html index 97bee6617..f398bada7 100644 --- a/anagrafica/templates/anagrafica_utente_curriculum.html +++ b/anagrafica/templates/anagrafica_utente_curriculum.html @@ -90,21 +90,22 @@

- - {% for t in titoli %} diff --git a/anagrafica/templates/anagrafica_utente_documenti.html b/anagrafica/templates/anagrafica_utente_documenti.html index 14af41fe4..41c7750d6 100755 --- a/anagrafica/templates/anagrafica_utente_documenti.html +++ b/anagrafica/templates/anagrafica_utente_documenti.html @@ -1,53 +1,38 @@ {% extends "anagrafica_utente_vuota.html" %} {% load bootstrap3 %} - {% block pagina_titolo %}Gestione Documenti{% endblock %} {% block app_contenuto %} -
-

- Da questa pagina potrai gestire i tuoi documenti. -

- +

Da questa pagina potrai gestire i tuoi documenti.

- - + {% csrf_token %}
-
-
-

- Documenti caricati - -

- - +

Documenti caricati

- -
Nome e Stato Dettagli
-

{{ t.titolo.nome }} + + {% if t.is_course_title and not t.is_expired_course_title %} +
Scarica attestato + {% endif %}

{% if t.certificato %} @@ -113,6 +114,12 @@

Certificato + {% elif t.is_expired_course_title %} + + + Scaduto titolo del corso + + {% elif t.esito == t.ESITO_OK %} @@ -126,10 +133,7 @@

{% else %} - - {{ t.esito }} - - + {{ t.esito }} {% endif %}

- - - - - + + + + + - {% for d in documenti %} - + + - {% empty %} - + - {% endfor %} - - -
Data caricamentoTipoVisualizzaCancellaData caricamentoTipoScadenzaVisualizzaCancella
{{ d.creazione|date:"SHORT_DATETIME_FORMAT" }}{{ d.creazione|date:"SHORT_DATETIME_FORMAT" }} {{ d.get_tipo_display }}{{ d.expires|date:"d/m/Y"|default:"-" }} Scarica @@ -56,77 +41,39 @@

{% endif %}

- - Cancella - + + {% if d.can_be_deleted %} + + {% endif %}
- Nessun documento caricato. - Nessun documento caricato.
{% if documenti %} - - - Scarica tutti i documenti (ZIP) - + Scarica tutti i documenti (ZIP) {% endif %} -
- - - -
-
-
-

- Aggiungi documento -

- +

Aggiungi documento

-
{% bootstrap_form modulo_aggiunta %} - - - +
-
- - - - -
- - - - - - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/anagrafica/templates/anagrafica_utente_home.html b/anagrafica/templates/anagrafica_utente_home.html index 54a4fd300..19b10d3c4 100644 --- a/anagrafica/templates/anagrafica_utente_home.html +++ b/anagrafica/templates/anagrafica_utente_home.html @@ -5,70 +5,42 @@ {% load utils %} {% block app_contenuto %} - {% if me.aspirante %}
-

- - Vuoi trovare Corsi Base vicino a te? -

-

- Clicca su "Aspirante" in alto a questa pagina per vedere i Corsi Base - e le Sedi CRI nelle tue vicinanze. -

+

Vuoi trovare Corsi Base vicino a te?

+

Clicca su "Aspirante" in alto a questa pagina per vedere i Corsi Base e le Sedi CRI nelle tue vicinanze.

{% endif %} -
-

 

-

- Benvenut{{ me.genere_o_a }} - {{ me.nome }}
-

-

oggi è un ottimo giorno per fare volontariato

-

 

-

- Ultimo accesso {{ me.utenza.last_login }} -

-

 

-

 

+
+

Benvenut{{ me.genere_o_a }} {{ me.nome }}

+

oggi è un ottimo giorno per fare volontariato

+ {% if me.utenza.last_login %}

Ultimo accesso {{ me.utenza.last_login }}

{%endif%}
{% if me.trasferimento and not me.trasferimento.con_scadenza %}
-

- - Hai attualmente un trasferimento in attesa di approvazione -

+

Hai attualmente un trasferimento in attesa di approvazione

Per ragioni tecniche -risolte a partire dal {{ SETTINGS.DATA_AVVIO_TRASFERIMENTI_AUTO|date:"SHORT_DATE_FORMAT" }}- non è stato possibile approvare in automatico il trasferimento.
Vai alla sezione Trasferimenti per maggiori informazioni.

- {% endif %} {% if me.nuovo_presidente %} -
+ - {% endif %} {% if not me.numeri_telefono.exists %} -
+

Aggiungi il tuo numero di cellulare nella sezione Contatti @@ -86,7 +58,7 @@

{% endif %} {% if me.titoli_personali.all.count < 3 %} -
+

Completa il tuo Curriculum diff --git a/anagrafica/templates/anagrafica_utente_vuota.html b/anagrafica/templates/anagrafica_utente_vuota.html index 99eeaa33a..bbf1f455c 100755 --- a/anagrafica/templates/anagrafica_utente_vuota.html +++ b/anagrafica/templates/anagrafica_utente_vuota.html @@ -64,7 +64,13 @@ \ No newline at end of file diff --git a/anagrafica/templates/statistiche_totali.html b/anagrafica/templates/statistiche_totali.html new file mode 100644 index 000000000..3e0586ca4 --- /dev/null +++ b/anagrafica/templates/statistiche_totali.html @@ -0,0 +1,27 @@ + + + + + + + + + {% if obj.tot %} + + {% for subtot in obj.tot %} + + + + {% for k, v in subtot.statistiche.items %} + + + + + {% endfor %} + {% endfor %} + + {% endif %} + +
Valori
+ {{ subtot.nome }} +
{{ k }}{{ v }}
diff --git a/anagrafica/templatetags/utils.py b/anagrafica/templatetags/utils.py index 8ae28c27c..5a372b286 100755 --- a/anagrafica/templatetags/utils.py +++ b/anagrafica/templatetags/utils.py @@ -1,4 +1,5 @@ from django import template +from django.core.urlresolvers import reverse from django.contrib.contenttypes.models import ContentType from django.db.models import QuerySet, Max from django.template import Library @@ -19,6 +20,7 @@ from datetime import timezone from django.utils import timezone + register = Library() @@ -103,7 +105,6 @@ def checkbox(booleano, extra_classe='', con_testo=1): @register.simple_tag(takes_context=True) def localizzatore(context, oggetto_localizzatore=None, continua_url=None, solo_italia=False): - if not isinstance(oggetto_localizzatore, ConGeolocalizzazione): raise ValueError("Il tag localizzatore puo' solo essere usato con un oggetto ConGeolocalizzazione, ma e' stato usato con un oggetto %s." % (oggetto_localizzatore.__class__.__name__,)) @@ -114,9 +115,10 @@ def localizzatore(context, oggetto_localizzatore=None, continua_url=None, solo_i context.request.session['pk'] = oggetto_localizzatore.pk context.request.session['continua_url'] = continua_url - url = "/geo/localizzatore/" + url = reverse('geo_localizzatore') if solo_italia: url += '?italia=1' + context.update({ 'iframe_url': url, }) @@ -124,9 +126,11 @@ def localizzatore(context, oggetto_localizzatore=None, continua_url=None, solo_i @register.simple_tag(takes_context=True) -def delegati(context, delega=UFFICIO_SOCI, oggetto=None, continua_url=None, almeno=0): +def delegati(context, delega=UFFICIO_SOCI, oggetto=None, continua_url=None, almeno=0, *args, **kwargs): if not isinstance(oggetto, ConDelegati): - raise ValueError("Il tag delegati puo' solo essere usato con un oggetto ConDelegati, ma e' stato usato con un oggetto %s." % (oggetto_localizzatore.__class__.__name__,)) + msg = "Il tag delegati può solo essere usato con un oggetto ConDelegati, " \ + "ma è stato usato con un oggetto %s." + raise ValueError(msg % oggetto_localizzatore.__class__.__name__) oggetto_tipo = ContentType.objects.get_for_model(oggetto) context.request.session['app_label'] = oggetto_tipo.app_label @@ -135,9 +139,8 @@ def delegati(context, delega=UFFICIO_SOCI, oggetto=None, continua_url=None, alme context.request.session['continua_url'] = continua_url context.request.session['delega'] = delega context.request.session['almeno'] = almeno - url = "/strumenti/delegati/" context.update({ - 'iframe_url': url, + 'iframe_url': reverse('strumenti_delegati'), }) return render_to_string('base_iframe_4_3.html', context) @@ -231,7 +234,10 @@ def differenza(a, b, piu=0): @register.filter(name='volte') def volte(number, meno=0): - return range(number-meno) + try: + return range(number-meno) + except TypeError: + pass class NodoMappa(template.Node): diff --git a/anagrafica/urls_utente.py b/anagrafica/urls_utente.py index 3760c78e9..bb6c7d7d2 100644 --- a/anagrafica/urls_utente.py +++ b/anagrafica/urls_utente.py @@ -18,15 +18,15 @@ url(r'^fotografia/fototessera/$', views.utente_fotografia_fototessera, name='fototessera'), url(r'^documenti/$', views.utente_documenti, name='documenti'), url(r'^documenti/zip/$', views.utente_documenti_zip, name='documenti_zip'), - url(r'^documenti/cancella/(?P.*)/$', views.utente_documenti_cancella), - url(r'^storico/$', views.utente_storico), - url(r'^contatti/$', views.utente_contatti), + url(r'^documenti/cancella/(?P.*)/$', views.utente_documenti_cancella, name='remove_document'), + url(r'^storico/$', views.utente_storico, name='storico'), + url(r'^contatti/$', views.utente_contatti, name='contatti'), url(r'^rubrica/referenti/$', views.utente_rubrica_referenti), url(r'^rubrica/volontari/$', views.utente_rubrica_volontari), url(r'^rubrica/(?P.*)/$', views.rubrica_delegati), url(r'^curriculum/$', views.utente_curriculum), url(r'^curriculum/(?P.*)/cancella/$', views.utente_curriculum_cancella), - url(r'^curriculum/(?P.*)/$', views.utente_curriculum), + url(r'^curriculum/(?P.*)/$', views.utente_curriculum, name='cv_tipo'), url(r'^riserva/$', views.utente_riserva), url(r'^riserva/(?P.*)/termina/$', views.utente_riserva_termina), url(r'^riserva/(?P.*)/ritira/$', views.utente_riserva_ritira), @@ -38,12 +38,12 @@ url(r'^donazioni/profilo/$', views.utente_donazioni_profilo), url(r'^donazioni/sangue/(?P.*)/cancella/$', views.utente_donazioni_sangue_cancella), url(r'^donazioni/sangue/$', views.utente_donazioni_sangue), - url(r'^privacy/$', views.utente_privacy), + url(r'^privacy/$', views.utente_privacy, name='privacy'), url(r'^cambia-password/?$', pagina_privata_no_cambio_firma(password_change), { "template_name": "anagrafica_utente_cambia_password.html", "password_change_form": ModuloModificaPassword, "post_change_redirect": "/utente/cambia-password/fatto/" - }), + }, name='change_password'), url(r'^cambia-password/fatto/$', pagina_privata_no_cambio_firma(password_change_done), { "template_name": "anagrafica_utente_cambia_password_fatto.html", }), diff --git a/anagrafica/validators.py b/anagrafica/validators.py index 6028039a6..34c028757 100644 --- a/anagrafica/validators.py +++ b/anagrafica/validators.py @@ -1,10 +1,13 @@ -from datetime import datetime, date - import stdnum +import re + +from datetime import datetime, date from django.utils import timezone -from stdnum.it import codicefiscale + from django.core.exceptions import ValidationError -import re +from django.utils.deconstruct import deconstructible + +from stdnum.it import codicefiscale def _valida_codice_fiscale(codice_fiscale): @@ -110,3 +113,17 @@ def valida_data_nel_passato(data): raise ValidationError("La data non può essere nel futuro.") else: raise TypeError("Fornito tipo di data non valido.") + + +@deconstructible +class ValidateFileSize(object): + def __init__(self, filesize=2): + self.filesize = filesize + + def __call__(self, *args, **kwargs): + file = args[0] + filesize = file.file.size + megabyte_limit = self.filesize + if filesize > megabyte_limit * 1024 * 1024: + raise ValidationError( + "Seleziona un file più piccolo di %sMB" % str(megabyte_limit)) diff --git a/anagrafica/viste.py b/anagrafica/viste.py index e8ade3768..2cb322441 100755 --- a/anagrafica/viste.py +++ b/anagrafica/viste.py @@ -2,9 +2,10 @@ from collections import OrderedDict from importlib import import_module +from django.db import transaction +from django.db.models import Q from django.apps import apps from django.conf import settings -from django.db import transaction from django.contrib import messages from django.contrib.auth import login from django.contrib.contenttypes.models import ContentType @@ -13,6 +14,10 @@ from django.template.loader import get_template from django.utils import timezone from django.utils.crypto import get_random_string +from django.template.loader import get_template +# from django.views.generic import ListView +from django.core.urlresolvers import reverse +from django.shortcuts import (render_to_response, redirect, get_object_or_404, HttpResponse) from articoli.viste import get_articoli from attivita.forms import ModuloStatisticheAttivitaPersona @@ -28,9 +33,12 @@ from base.utils import remove_none, poco_fa, oggi from curriculum.forms import ModuloNuovoTitoloPersonale, ModuloDettagliTitoloPersonale from curriculum.models import Titolo, TitoloPersonale -from posta.models import Messaggio, Q +from posta.models import Messaggio from posta.utils import imposta_destinatari_e_scrivi_messaggio from sangue.models import Donatore, Donazione +from anagrafica.statistica.stat_costanti import GENERALI +from anagrafica.statistica.applicazione import STATISTICHE, ModuloStatisticheBase + from .costanti import TERRITORIALE, REGIONALE from .elenchi import ElencoDelegati @@ -443,37 +451,36 @@ def utente_documenti(request, me): if not me.volontario and not me.dipendente: return errore_no_volontario(request, me) - contesto = { - "documenti": me.documenti.all() + context = { + 'documenti': me.documenti.all() } - if request.method == "POST": - - nuovo_doc = Documento(persona=me) - modulo_aggiunta = ModuloCreazioneDocumento(request.POST, request.FILES, instance=nuovo_doc) - - if modulo_aggiunta.is_valid(): - modulo_aggiunta.save() - + if request.method == 'POST': + doc = Documento(persona=me) + form = ModuloCreazioneDocumento(request.POST, request.FILES, instance=doc) + if form.is_valid(): + form.save() + return redirect(reverse('utente:documenti')) else: + form = ModuloCreazioneDocumento() - modulo_aggiunta = ModuloCreazioneDocumento() - - contesto.update({"modulo_aggiunta": modulo_aggiunta}) - - return 'anagrafica_utente_documenti.html', contesto + context.update({'modulo_aggiunta': form}) + return 'anagrafica_utente_documenti.html', context @pagina_privata def utente_documenti_cancella(request, me, pk): - doc = get_object_or_404(Documento, pk=pk) if not doc.persona == me: return redirect('/errore/permessi/') - doc.delete() - return redirect('/utente/documenti/') + if doc.can_be_deleted: + doc.delete() + else: + messages.error(request, 'Questo documento non può essere cancellato') + + return redirect(reverse('utente:documenti')) @pagina_privata @@ -1051,21 +1058,34 @@ def profilo_turni_foglio(request, me, pk=None): @pagina_privata def strumenti_delegati(request, me): - app_label = request.session['app_label'] - model = request.session['model'] - pk = int(request.session['pk']) - continua_url = request.session['continua_url'] - almeno = request.session['almeno'] - delega = request.session['delega'] - oggetto = apps.get_model(app_label, model) - oggetto = oggetto.objects.get(pk=pk) + from formazione.forms import FormCreateDirettoreDelega + session = request.session + + # Get values stored in the session + app_label = session['app_label'] + model = session['model'] + pk = int(session['pk']) + continua_url = session['continua_url'] + almeno = session['almeno'] + delega = session['delega'] + + # Get object + oggetto = apps.get_model(app_label, model).objects.get(pk=pk) + + # Instantiate a new form + form_data = { + 'course': oggetto, + 'me': me, + 'initial': {'inizio': datetime.date.today()}, + } + form = ModuloCreazioneDelega(request.POST or None, **form_data) + if model == 'corsobase': + # if oggetto.is_nuovo_corso: + form = FormCreateDirettoreDelega(request.POST or None, **form_data) - modulo = ModuloCreazioneDelega(request.POST or None, initial={ - "inizio": datetime.date.today(), - }, me=me) - - if modulo.is_valid(): - d = modulo.save(commit=False) + # Check form is valid + if form.is_valid(): + d = form.save(commit=False) if oggetto.deleghe.all().filter(Delega.query_attuale().q, tipo=delega, persona=d.persona).exists(): return errore_generico( @@ -1073,7 +1093,7 @@ def strumenti_delegati(request, me): titolo="%s è già delegato" % (d.persona.nome_completo,), messaggio="%s ha già una delega attuale come %s per %s" % (d.persona.nome_completo, PERMESSI_NOMI_DICT[delega], oggetto), torna_titolo="Torna indietro", - torna_url="/strumenti/delegati/", + torna_url=reverse('strumenti_delegati'), ) d.inizio = poco_fa() @@ -1086,17 +1106,17 @@ def strumenti_delegati(request, me): deleghe = oggetto.deleghe.filter(tipo=delega) deleghe_attuali = oggetto.deleghe_attuali(tipo=delega) - contesto = { + context = { "continua_url": continua_url, "almeno": almeno, "delega": PERMESSI_NOMI_DICT[delega], - "modulo": modulo, + "modulo": form, "oggetto": oggetto, "deleghe": deleghe, "deleghe_attuali": deleghe_attuali, } - return 'anagrafica_strumenti_delegati.html', contesto + return 'anagrafica_strumenti_delegati.html', context @pagina_privata @@ -1114,9 +1134,8 @@ def strumenti_delegati_termina(request, me, delega_pk=None): if not me.permessi_almeno(delega.oggetto, GESTIONE): return redirect(ERRORE_PERMESSI) - delega.termina(mittente=me, - termina_at=poco_fa()) - return redirect("/strumenti/delegati/") + delega.termina(mittente=me, termina_at=poco_fa()) + return redirect(reverse('strumenti_delegati')) @pagina_privata @@ -1184,10 +1203,9 @@ def utente_curriculum(request, me, tipo=None): tp.autorizzazione_richiedi_sede_riferimento( me, INCARICO_GESTIONE_TITOLI ) - return redirect("/utente/curriculum/%s/?inserimento=ok" % (tipo,)) - titoli = me.titoli_personali.all().filter(titolo__tipo=tipo) + titoli = me.titoli_personali.all().filter(titolo__tipo=tipo).order_by('-data_scadenza') contesto = { "tipo": tipo, @@ -1782,7 +1800,6 @@ def unicode_csv_reader(utf8_data, dialect=csv.excel, **kwargs): if modulo.is_valid(): - nome_file = handle_uploaded_file(request.FILES['file_csv']) with codecs.open(nome_file, encoding="utf-8") as csvfile: riga = unicode_csv_reader(csvfile, delimiter=modulo.cleaned_data['delimitatore']) @@ -1819,52 +1836,76 @@ def admin_statistiche(request, me): if not me.utenza.is_staff: return redirect(ERRORE_PERMESSI) - oggi = datetime.date.today() - nascita_minima_35 = datetime.date(oggi.year - 36, oggi.month, oggi.day) - persone = Persona.objects.all() - soci = Persona.objects.filter( - Appartenenza.query_attuale(membro__in=Appartenenza.MEMBRO_SOCIO).via("appartenenze") - ).distinct('nome', 'cognome', 'codice_fiscale') - soci_giovani_35 = soci.filter( - data_nascita__gt=nascita_minima_35, - ) - sedi = Sede.objects.filter(attiva=True) - comitati = sedi.comitati() - regionali = Sede.objects.filter(estensione=REGIONALE).exclude(nome__contains='Provinciale Di Roma') - - totale_regione_soci = 0 - totale_regione_volontari = 0 - - regione_soci_volontari = [] - for regione in regionali: - regione_soci = int(regione.membri_attuali(figli=True, membro__in=Appartenenza.MEMBRO_SOCIO).count()) - regione_volontari = int(regione.membri_attuali(figli=True, membro=Appartenenza.VOLONTARIO).count()) - regione_soci_volontari += [ - ( - regione, - regione_soci, - regione_volontari, + modulo = ModuloStatisticheBase(request.POST or None) + + if request.POST and modulo.is_valid(): + tipo = modulo.cleaned_data['tipo_statistiche'] + livello_riferimento = modulo.cleaned_data['livello_riferimento'] + nome_corso = modulo.cleaned_data['nome_corso'] + area_riferimento = modulo.cleaned_data['area_riferimento'] + tipo_filtro = modulo.cleaned_data['tipo_filtro'] + anno_di_riferimento = modulo.cleaned_data['anno_di_riferimento'] + dal = modulo.cleaned_data['dal'] + al = modulo.cleaned_data['al'] + + statistica = STATISTICHE[tipo] + + contesto = { + "type": tipo, + "obj": statistica[0]( + livello_riferimento=livello_riferimento, + nome_corso=nome_corso, + area_riferimento=area_riferimento, + anno_di_riferimento=anno_di_riferimento, + tipo_filtro=tipo_filtro, + dal=dal, + al=al, ), - ] - totale_regione_soci += regione_soci - totale_regione_volontari += regione_volontari + "views": statistica[1], + "ora": timezone.now(), + "modulo": modulo + } + + request.session['ultima_statistica'] = contesto['obj'] + + return 'admin_statistiche.html', contesto + + statistica = STATISTICHE[GENERALI] contesto = { - "persone_numero": persone.count(), - "soci_numero": soci.count(), - "soci_percentuale": soci.count() / persone.count() * 100, - "soci_giovani_35_numero": soci_giovani_35.count(), - "soci_giovani_35_percentuale": soci_giovani_35.count() / soci.count() * 100, - "sedi_numero": sedi.count(), - "comitati_numero": comitati.count(), + "type": GENERALI, + "obj": statistica[0](), + "views": statistica[1], "ora": timezone.now(), - "regione_soci_volontari": regione_soci_volontari, - "totale_regione_soci": totale_regione_soci, - "totale_regione_volontari": totale_regione_volontari, + "modulo": modulo, } + + request.session['ultima_statistica'] = contesto['obj'] + return 'admin_statistiche.html', contesto +@pagina_privata +def admin_statistiche_download(request, me, statistica): + + if not me.utenza.is_superuser: + return redirect(ERRORE_PERMESSI) + + stat = STATISTICHE[statistica] + + file_name = stat[2](request.session['ultima_statistica']) + + file = open(file_name, 'rb') + + response = HttpResponse(file, content_type='application/msword') + response['Content-Disposition'] = 'attachment; filename={}_{}.xlsx'.format( + statistica, datetime.datetime.today().strftime('%d-%m-%Y') + ) + file.close() + + return response + + @pagina_privata def admin_report_federazione(request, me): diff --git a/attivita/urls.py b/attivita/urls.py new file mode 100644 index 000000000..3eac2b46d --- /dev/null +++ b/attivita/urls.py @@ -0,0 +1,59 @@ +from django.conf.urls import url + +from centrale_operativa import viste as co_views +from gruppi import viste as gruppi_views +from . import viste + + +app_label = 'attivita' +urlpatterns = [ + url(r'^$', viste.attivita), + url(r'^aree/$', viste.attivita_aree), + url(r'^aree/(?P[0-9\-]+)/$', viste.attivita_aree_sede), + url(r'^aree/(?P[0-9\-]+)/(?P[0-9\-]+)/cancella/$', viste.attivita_aree_sede_area_cancella), + url(r'^aree/(?P[0-9\-]+)/(?P[0-9\-]+)/responsabili/$', viste.attivita_aree_sede_area_responsabili), + url(r'^organizza/$', viste.attivita_organizza), + url(r'^organizza/(?P[0-9\-]+)/referenti/$', viste.attivita_referenti, {"nuova": True}), + url(r'^organizza/(?P[0-9\-]+)/fatto/$', viste.attivita_organizza_fatto), + url(r'^statistiche/$', viste.attivita_statistiche), + url(r'^gestisci/$', viste.attivita_gestisci, {"stato": "aperte"}), + url(r'^gestisci/chiuse/$', viste.attivita_gestisci, {"stato": "chiuse"}), + url(r'^calendario/$', viste.attivita_calendario), + url(r'^calendario/(?P[0-9\-]+)/(?P[0-9\-]+)/$', viste.attivita_calendario), + url(r'^storico/$', viste.attivita_storico), + url(r'^storico/excel/$', viste.attivita_storico_excel), + + url(r'^gruppo/$', gruppi_views.attivita_gruppo), + url(r'^gruppi/$', gruppi_views.attivita_gruppi), + url(r'^gruppi/(?P[0-9]+)/$', gruppi_views.attivita_gruppi_gruppo), + url(r'^gruppi/(?P[0-9]+)/iscriviti/$', gruppi_views.attivita_gruppi_gruppo_iscriviti), + url(r'^gruppi/(?P[0-9]+)/espelli/(?P[0-9]+)/$', gruppi_views.attivita_gruppi_gruppo_espelli), + url(r'^gruppi/(?P[0-9]+)/abbandona/$', gruppi_views.attivita_gruppi_gruppo_abbandona), + url(r'^gruppi/(?P[0-9]+)/elimina/$', gruppi_views.attivita_gruppi_gruppo_elimina), + url(r'^gruppi/(?P[0-9]+)/elimina_conferma/$', gruppi_views.attivita_gruppi_gruppo_elimina_conferma), + + url(r'^reperibilita/$', co_views.attivita_reperibilita), + url(r'^reperibilita/(?P[0-9]+)/cancella/$', co_views.attivita_reperibilita_cancella), + + url(r'^scheda/(?P[0-9]+)/$', viste.attivita_scheda_informazioni), + url(r'^scheda/(?P[0-9]+)/cancella-gruppo/$', viste.attivita_scheda_cancella), + url(r'^scheda/(?P[0-9]+)/cancella/$', viste.attivita_scheda_cancella), + url(r'^scheda/(?P[0-9]+)/mappa/$', viste.attivita_scheda_mappa), + url(r'^scheda/(?P[0-9]+)/partecipanti/$', viste.attivita_scheda_partecipanti), + url(r'^scheda/(?P[0-9]+)/turni/$', viste.attivita_scheda_turni), + url(r'^scheda/(?P[0-9]+)/turni/(?P[0-9]+)/$', viste.attivita_scheda_turni), + url(r'^scheda/(?P[0-9]+)/turni/(?P[0-9]+)/partecipa/$', viste.attivita_scheda_turni_partecipa), + url(r'^scheda/(?P[0-9]+)/turni/(?P[0-9]+)/ritirati/$', viste.attivita_scheda_turni_ritirati), + url(r'^scheda/(?P[0-9]+)/turni/(?P[0-9]+)/partecipanti/$', viste.attivita_scheda_turni_partecipanti), + url(r'^scheda/(?P[0-9]+)/turni/link-permanente/(?P[0-9]+)/$', viste.attivita_scheda_turni_link_permanente), + url(r'^scheda/(?P[0-9]+)/turni/cancella/(?P[0-9]+)/$', viste.attivita_scheda_turni_turno_cancella), + url(r'^scheda/(?P[0-9]+)/turni/modifica/$', viste.attivita_scheda_turni_modifica), + url(r'^scheda/(?P[0-9]+)/turni/nuovo/$', viste.attivita_scheda_turni_nuovo), + url(r'^scheda/(?P[0-9]+)/partecipazione/(?P[0-9]+)/cancella/$', viste.attivita_scheda_partecipazione_cancella), + url(r'^scheda/(?P[0-9]+)/turni/modifica/(?P[0-9]+)/$', viste.attivita_scheda_turni_modifica), + url(r'^scheda/(?P[0-9]+)/turni/modifica/link-permanente/(?P[0-9]+)/$', viste.attivita_scheda_turni_modifica_link_permanente), + url(r'^scheda/(?P[0-9]+)/modifica/$', viste.attivita_scheda_informazioni_modifica), + url(r'^scheda/(?P[0-9]+)/riapri/$', viste.attivita_riapri), + url(r'^scheda/(?P[0-9]+)/referenti/$', viste.attivita_referenti), + url(r'^scheda/(?P[0-9]+)/report/$', viste.attivita_scheda_report), +] diff --git a/base/admin.py b/base/admin.py index b1d606c4f..b3065954e 100755 --- a/base/admin.py +++ b/base/admin.py @@ -1,15 +1,16 @@ from django.contrib import admin from django.contrib.contenttypes.admin import GenericTabularInline -from base.geo import Locazione -from base.models import Autorizzazione, Token, Allegato, Menu from gruppi.readonly_admin import ReadonlyAdminMixin +from .geo import Locazione +from .models import Autorizzazione, Token, Allegato, Menu @admin.register(Token) class TokenAdmin(ReadonlyAdminMixin, admin.ModelAdmin): pass + @admin.register(Menu) class MenuAdmin(ReadonlyAdminMixin, admin.ModelAdmin): list_display = ['url', 'order', 'is_active', 'name',] diff --git a/base/classes/__init__.py b/base/classes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/base/classes/pdf.py b/base/classes/pdf.py new file mode 100644 index 000000000..52a42e9d0 --- /dev/null +++ b/base/classes/pdf.py @@ -0,0 +1,64 @@ +import os + +from django.apps import apps +from django.http import HttpResponse +from django.shortcuts import redirect + +from anagrafica.permessi.costanti import ERRORE_PERMESSI, LETTURA +from base.tratti import ConPDF +from base.errori import errore_generico + + +class BaseGeneraPDF: + def __init__(self, request, me, app_label, model, pk): + self.request = request + self.me = me + self.app_label = app_label + self.model = model + self.pk = pk + + def make(self): + oggetto = apps.get_model(self.app_label, self.model).objects.get(pk=self.pk) + + if not isinstance(oggetto, ConPDF): + return errore_generico(self.request, None, + messaggio="Impossibile generare un PDF per il tipo specificato.") + + if 'token' in self.request.GET: + if not oggetto.token_valida(self.request.GET['token']): + return errore_generico(self.request, self.me, + titolo="Token scaduta", + messaggio="Il link usato è scaduto.") + + elif not self.me.permessi_almeno(oggetto, LETTURA): + return redirect(ERRORE_PERMESSI) + + pdf = oggetto.genera_pdf() + + # Se sto scaricando un tesserino, forza lo scaricamento. + if 'tesserini' in pdf.file.path: + return self.pdf_forza_scaricamento(pdf) + + return redirect(pdf.download_url) + + def pdf_forza_scaricamento(self, pdf): + """ + Forza lo scaricamento di un file pdf. + Da usare con cautela, perche' carica il file in memoria + e blocca il thread fino al completamento della richiesta. + :param request: + :param pdf: + :return: + """ + + from mimetypes import guess_type + + percorso_completo = pdf.file.path + + with open(percorso_completo, 'rb') as f: + data = f.read() + + response = HttpResponse(data, content_type=guess_type(percorso_completo)[0]) + response['Content-Disposition'] = "attachment; filename=%s" % pdf.nome + response['Content-Length'] = os.path.getsize(percorso_completo) + return response diff --git a/base/menu.py b/base/menu.py index ee4fef59d..8a5673846 100755 --- a/base/menu.py +++ b/base/menu.py @@ -1,34 +1,29 @@ +""" +Questa pagina contiene i vari menu che vengono mostrati nella barra laterale dei template. +La costante MENU è accessibile attraverso "menu" nei template. +""" + from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse -from anagrafica.costanti import REGIONALE, TERRITORIALE, LOCALE +from anagrafica.costanti import REGIONALE #TERRITORIALE, LOCALE +from anagrafica.permessi.applicazioni import (PRESIDENTE, UFFICIO_SOCI, RUBRICHE_TITOLI, COMMISSARIO) +from anagrafica.permessi.costanti import (GESTIONE_ATTIVITA, GESTIONE_ATTIVITA_AREA, + ELENCHI_SOCI, GESTIONE_AREE_SEDE, GESTIONE_ATTIVITA_SEDE, EMISSIONE_TESSERINI, + GESTIONE_POTERI_CENTRALE_OPERATIVA_SEDE) from anagrafica.models import Sede -from anagrafica.permessi.applicazioni import DELEGATO_OBIETTIVO_1, DELEGATO_OBIETTIVO_2, DELEGATO_OBIETTIVO_3, DELEGATO_OBIETTIVO_4, \ - DELEGATO_OBIETTIVO_5, DELEGATO_OBIETTIVO_6, PRESIDENTE, \ - UFFICIO_SOCI, UFFICIO_SOCI_UNITA, DELEGATO_AREA, RESPONSABILE_AREA, \ - REFERENTE, RESPONSABILE_FORMAZIONE, DIRETTORE_CORSO, \ - RESPONSABILE_AUTOPARCO, DELEGATO_CO, REFERENTE_GRUPPO, RUBRICHE_TITOLI, COMMISSARIO -from anagrafica.permessi.costanti import GESTIONE_CORSI_SEDE, GESTIONE_ATTIVITA, GESTIONE_ATTIVITA_AREA, ELENCHI_SOCI, \ - GESTIONE_AREE_SEDE, GESTIONE_ATTIVITA_SEDE, EMISSIONE_TESSERINI, GESTIONE_POTERI_CENTRALE_OPERATIVA_SEDE + from .utils import remove_none from .models import Menu - -# __author__ = 'alfioemanuele' - -""" -Questa pagina contiene i vari menu che vengono mostrati nella barra laterale dei template. -La costante MENU e' accessibile attraverso "menu" nei template. -""" +from formazione.menus import formazione_menu def menu(request): - """ - Ottiene il menu per una data richiesta. - """ - from base.viste import ORDINE_ASCENDENTE, ORDINE_DISCENDENTE, ORDINE_DEFAULT + """ Ottiene il menu per una data richiesta. """ - me = request.me if hasattr(request, 'me') else None + # from base.viste import ORDINE_ASCENDENTE, ORDINE_DISCENDENTE, ORDINE_DEFAULT + me = request.me if hasattr(request, 'me') else None deleghe_attuali = None if me: @@ -45,8 +40,6 @@ def menu(request): oggetto_id__in=sedi ).distinct().values_list('tipo', flat=True) - gestione_corsi_sede = me.ha_permesso(GESTIONE_CORSI_SEDE) if me else False - RUBRICA_BASE = [ ("Referenti", "fa-book", "/utente/rubrica/referenti/"), ("Volontari", "fa-book", "/utente/rubrica/volontari/"), @@ -68,57 +61,58 @@ def menu(request): (titolo, "fa-book", "".join(("/utente/rubrica/", slug, '/'))) ) + ME_VOLONTARIO = me and (me.volontario or me.dipendente) + VOCE_PERSONA = ("Persona", ( + ("Benvenuto", "fa-bolt", "/utente/"), + ("Anagrafica", "fa-edit", "/utente/anagrafica/"), + ("Storico", "fa-clock-o", "/utente/storico/"), + ("Documenti", "fa-folder", "/utente/documenti/") if ME_VOLONTARIO else None, + ("Contatti", "fa-envelope", "/utente/contatti/"), + ("Fotografie", "fa-credit-card", "/utente/fotografia/"), + )) - # if UFFICIO_SOCI in deleghe_attuali: - # for rubrica in RUBRICA_BASE: - # if rubrica[0] == 'Commissari' or rubrica[0] == 'Presidenti': - # RUBRICA_BASE.remove(rubrica) + VOCE_VOLONTARIO = ("Volontario", ( + ("Corsi", "fa-list", reverse('aspirante:corsi_base')), + ("Estensione", "fa-random", "/utente/estensione/"), + ("Trasferimento", "fa-arrow-right", "/utente/trasferimento/"), + ("Riserva", "fa-pause", "/utente/riserva/"), + )) if me and me.volontario else None VOCE_RUBRICA = ("Rubrica", ( RUBRICA_BASE )) + VOCE_CV = ("Curriculum", ( + # Competenze personali commentate per non visuallizarle + #("Competenze personali", "fa-suitcase", "/utente/curriculum/CP/"), + ("Patenti Civili", "fa-car", "/utente/curriculum/PP/"), + ("Patenti CRI", "fa-ambulance", "/utente/curriculum/PC/") if ME_VOLONTARIO else None, + ("Titoli di Studio", "fa-graduation-cap", "/utente/curriculum/TS/"), + ("Titoli CRI", "fa-plus-square-o", "/utente/curriculum/TC/") if ME_VOLONTARIO else None, + )) + + VOCE_DONATORE = ("Donatore", ( + ("Profilo Donatore", "fa-user", "/utente/donazioni/profilo/"), + ("Donazioni di Sangue", "fa-flask", "/utente/donazioni/sangue/") + if hasattr(me, 'donatore') else None, + )) if me and me.volontario else None + + VOCE_SICUREZZA = ("Sicurezza", ( + ("Cambia password", "fa-key", "/utente/cambia-password/"), + ("Impostazioni Privacy", "fa-cogs", "/utente/privacy/"), + )) + VOCE_LINKS = ("Links", tuple((link.name, link.icon_class, link.url) - for link in Menu.objects.filter(is_active=True).order_by('order'))) + for link in Menu.objects.filter(is_active=True).order_by('order'))) + + VOCE_MONITORAGGIO = ("Monitoraggio", ( + ("Monitoraggio 2019 (dati 2018)", 'fa-user', reverse('pages:monitoraggio')), + )) elementi = { - "utente": ( - (("Persona", ( - ("Benvenuto", "fa-bolt", "/utente/"), - ("Anagrafica", "fa-edit", "/utente/anagrafica/"), - ("Storico", "fa-clock-o", "/utente/storico/"), - ("Documenti", "fa-folder", "/utente/documenti/") if me and (me.volontario or me.dipendente) else None, - ("Contatti", "fa-envelope", "/utente/contatti/"), - ("Fotografie", "fa-credit-card", "/utente/fotografia/"), - )), - ("Volontario", ( - ("Estensione", "fa-random", "/utente/estensione/"), - ("Trasferimento", "fa-arrow-right", "/utente/trasferimento/"), - ("Riserva", "fa-pause", "/utente/riserva/"), - )) if me and me.volontario else None, - VOCE_RUBRICA, - ("Curriculum", ( - # Competenze personali commentate per non visuallizarle - #("Competenze personali", "fa-suitcase", "/utente/curriculum/CP/"), - ("Patenti Civili", "fa-car", "/utente/curriculum/PP/"), - ("Patenti CRI", "fa-ambulance", "/utente/curriculum/PC/") if me and (me.volontario or me.dipendente) else None, - ("Titoli di Studio", "fa-graduation-cap", "/utente/curriculum/TS/"), - ("Titoli CRI", "fa-plus-square-o", "/utente/curriculum/TC/") if me and (me.volontario or me.dipendente) else None, - )), - ("Donatore", ( - ("Profilo Donatore", "fa-user", "/utente/donazioni/profilo/"), - ("Donazioni di Sangue", "fa-flask", "/utente/donazioni/sangue/") - if hasattr(me, 'donatore') else None, - )) if me and me.volontario else None, - ("Sicurezza", ( - ("Cambia password", "fa-key", "/utente/cambia-password/"), - ("Impostazioni Privacy", "fa-cogs", "/utente/privacy/"), - )), - VOCE_LINKS, - ("Monitoraggio", ( - ("Monitoraggio 2019 (dati 2018)", 'fa-user', reverse('pages:monitoraggio')), - )) if me and (me.is_presidente or me.is_comissario) else None, - )) if me and not hasattr(me, 'aspirante') else None, + "utente": (VOCE_PERSONA, VOCE_VOLONTARIO, VOCE_RUBRICA, VOCE_CV, + VOCE_DONATORE, VOCE_SICUREZZA, VOCE_LINKS, VOCE_MONITORAGGIO) \ + if me and not hasattr(me, 'aspirante') else None, "posta": ( ("Posta", ( ("Scrivi", "fa-pencil", "/posta/scrivi/"), @@ -183,8 +177,7 @@ def menu(request): ("Elettorato", "fa-list", "/us/elenchi/elettorato/"), ("Tesserini", "fa-list", "/us/tesserini/"), ("Per Titoli", "fa-search", "/us/elenchi/titoli/"), - ("Scarica elenchi richiesti", "fa-download", reverse( - 'elenchi_richiesti_download'), '', True), + ("Scarica elenchi richiesti", "fa-download", reverse('ufficio_soci:elenchi_richiesti_download'), '', True), )), ("Aggiungi", ( ("Persona", "fa-plus-square", "/us/aggiungi/"), @@ -215,45 +208,10 @@ def menu(request): if me and me.oggetti_permesso(GESTIONE_POTERI_CENTRALE_OPERATIVA_SEDE).exists() else None, )), ), - "formazione": ( - ("Corsi Base", ( - ("Elenco Corsi Base", "fa-list", "/formazione/corsi-base/elenco/"), - ("Domanda formativa", "fa-area-chart", "/formazione/corsi-base/domanda/") - if gestione_corsi_sede else None, - ("Pianifica nuovo", "fa-asterisk", "/formazione/corsi-base/nuovo/") - if gestione_corsi_sede else None, - # ("Monitoraggio 2019", 'fa-user', reverse('pages:monitoraggio')) - # if me and (me.is_presidente or me.is_comissario) else None, - )), - ("Corsi di Formazione", ( - ("Elenco Corsi di Formazione", "fa-list", "/formazione/corsi-formazione/"), - ("Pianifica nuovo", "fa-asterisk", "/formazione/corsi-formazione/nuovo/"), - )) if False else None, - ), - "aspirante": ( - ("Aspirante", ( - ("Home page", "fa-home", "/aspirante/"), - ("Anagrafica", "fa-edit", "/utente/anagrafica/"), - ("Storico", "fa-clock-o", "/utente/storico/"), - ("Contatti", "fa-envelope", "/utente/contatti/"), - ("Fotografie", "fa-credit-card", "/utente/fotografia/"), - ("Competenze personali", "fa-suitcase", "/utente/curriculum/CP/"), - ("Patenti Civili", "fa-car", "/utente/curriculum/PP/"), - ("Titoli di Studio", "fa-graduation-cap", "/utente/curriculum/TS/"), - )), - ("Nelle vicinanze", ( - ("Impostazioni", "fa-gears", "/aspirante/impostazioni/"), - ("Corsi Base", "fa-list", "/aspirante/corsi-base/"), - ("Sedi CRI", "fa-list", "/aspirante/sedi/"), - )), - ("Sicurezza", ( - ("Cambia password", "fa-key", "/utente/cambia-password/"), - ("Impostazioni Privacy", "fa-cogs", "/utente/privacy/"), - ), - ), - ) if me and hasattr(me, 'aspirante') else ( + 'formazione': formazione_menu('formazione', me), + 'aspirante': formazione_menu('aspirante') if me and hasattr(me, 'aspirante') else ( ("Gestione Corsi", ( - ("Elenco Corsi Base", "fa-list", "/formazione/corsi-base/elenco/"), + ("Elenco Corsi", "fa-list", reverse('formazione:list_courses')), )), ), } diff --git a/base/models.py b/base/models.py index 5b9108b4c..f1e8cb265 100755 --- a/base/models.py +++ b/base/models.py @@ -1,6 +1,7 @@ import os +from datetime import datetime, timedelta -from collections import defaultdict +from jorvik import settings from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core import urlresolvers @@ -9,28 +10,19 @@ from django.db.models import Q from django.utils.timezone import now from django.utils.functional import cached_property -from django.forms import forms from mptt.models import MPTTModel, TreeForeignKey -from anagrafica.permessi.applicazioni import PERMESSI_NOMI -from anagrafica.permessi.costanti import MODIFICA from anagrafica.permessi.incarichi import INCARICHI, INCARICHI_TIPO_DICT -from anagrafica.validators import crea_validatore_dimensione_file, valida_dimensione_file_10mb -from base.forms import ModuloMotivoNegazione -from base.notifiche import NOTIFICA_NON_INVIARE, NOTIFICA_INVIA -from base.stringhe import GeneratoreNomeFile, genera_uuid_casuale -from base.tratti import ConMarcaTemporale -from datetime import datetime, timezone, timedelta - -from base.utils import calcola_scadenza ,concept, iterabile +from anagrafica.validators import valida_dimensione_file_10mb -from jorvik import settings +from .utils import calcola_scadenza, concept, iterabile +from .forms import ModuloMotivoNegazione +from .stringhe import GeneratoreNomeFile, genera_uuid_casuale +from .tratti import ConMarcaTemporale class ModelloSemplice(models.Model): - """ - Questa classe astratta rappresenta un Modello generico. - """ + """ Questa classe astratta rappresenta un Modello generico. """ class Meta: abstract = True @@ -128,23 +120,6 @@ class Autorizzazione(ModelloSemplice, ConMarcaTemporale): PROTOCOLLO_AUTO = "AUTO" # Applicato a protocollo_numero se approvazione automatica - class Meta: - verbose_name_plural = "Autorizzazioni" - app_label = "base" - index_together = [ - ['necessaria', 'progressivo'], - ['necessaria', 'concessa'], - ['destinatario_ruolo', 'destinatario_oggetto_tipo',], - ['necessaria', 'destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], - ['destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], - ['destinatario_oggetto_tipo', 'destinatario_oggetto_id'], - ['necessaria', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], - ['necessaria', 'destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], - ] - permissions = ( - ("view_autorizzazione", "Can view autorizzazione"), - ) - richiedente = models.ForeignKey("anagrafica.Persona", db_index=True, related_name="autorizzazioni_richieste", on_delete=models.CASCADE) firmatario = models.ForeignKey("anagrafica.Persona", db_index=True, blank=True, null=True, default=None, related_name="autorizzazioni_firmate", on_delete=models.SET_NULL) @@ -274,9 +249,9 @@ def notifica_richiesta(self, persona): def notifica_sede_autorizzazione_concessa(self, sede, testo_extra=''): """ - Notifica presidente e ufficio soci del comitato di origine dell'avvenuta approvazione della richiesta + Notifica presidente e ufficio soci del comitato + di origine dell'avvenuta approvazione della richiesta. """ - from posta.models import Messaggio notifiche = self.espandi_notifiche(sede, [], True, True) modello = "email_autorizzazione_concessa_notifica_origine.html" @@ -418,6 +393,23 @@ def notifiche_richieste_in_attesa(cls): destinatari=[persona['persona']] ) + class Meta: + verbose_name_plural = "Autorizzazioni" + app_label = "base" + index_together = [ + ['necessaria', 'progressivo'], + ['necessaria', 'concessa'], + ['destinatario_ruolo', 'destinatario_oggetto_tipo',], + ['necessaria', 'destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], + ['destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], + ['destinatario_oggetto_tipo', 'destinatario_oggetto_id'], + ['necessaria', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], + ['necessaria', 'destinatario_ruolo', 'destinatario_oggetto_tipo', 'destinatario_oggetto_id'], + ] + permissions = ( + ("view_autorizzazione", "Can view autorizzazione"), + ) + class Log(ModelloSemplice, ConMarcaTemporale): @@ -808,6 +800,7 @@ def autorizzazione_nega_modulo(self): """ return ModuloMotivoNegazione + class ConScadenzaPulizia(models.Model): """ Aggiunge un attributo DateTimeField scadenza. @@ -901,6 +894,7 @@ class Meta: ("view_token", "Can view token"), ) + class Allegato(ConMarcaTemporale, ConScadenzaPulizia, ModelloSemplice): """ Rappresenta un allegato generico in database, con potenziale scadenza. diff --git a/base/static/css/base.css b/base/static/css/base.css index 9833980c3..435a7f923 100755 --- a/base/static/css/base.css +++ b/base/static/css/base.css @@ -206,7 +206,7 @@ body, p, span, div { } #menu-applicazione ul li a { - padding: 8px 10px; + padding: 8px 4px; } .badge-danger { @@ -245,4 +245,4 @@ body, p, span, div { .typeform_widget-tools ul {list-style:none; margin:0;} .typeform_widget-tools li {padding: 0 15px;} .typeform_widget-loading {position:absolute; z-index:100; width:100%; height:auto; min-height:100%; background-color:rgba(0,0,0,0.5);} -.typeform_widget-loading .text {text-align:center;margin: -20% 0 0 0; font-size:32px; color: #fff; font-weight: bold; padding-top: 40%;} \ No newline at end of file +.typeform_widget-loading .text {text-align:center;margin: -20% 0 0 0; font-size:32px; color: #fff; font-weight: bold; padding-top: 40%;} diff --git a/base/static/css/gaia.css b/base/static/css/gaia.css index 118fd2d7f..03b43af56 100644 --- a/base/static/css/gaia.css +++ b/base/static/css/gaia.css @@ -1,3 +1,48 @@ +.footer-container-fluid { + width: 100%; + margin-top: auto; +} +.footer-container-fluid__copyright { + text-align: center; + color: #fff; +} +.footer-main { + background-color: #cc0000; + padding-top: 25px; + padding-bottom: 25px; +} +.footer-main__nav { + margin: 0 auto; + padding: 0; + list-style: none; + text-align: center; + margin-bottom: 15px; +} +.footer-main__link { + display: inline-block; + margin-right: 10px; +} +.footer-main__link:last-child { + margin-right: 0; +} +.footer-main__link a { + color: #fff; +} +.footer-main__logo-uica { + display: block; + margin: 0 auto; + width: 150px; +} +.single-page h1 { + padding-bottom: 10px; +} +.single-page ul li { + padding-bottom: 6px; +} +.single-page p { + margin-bottom: 25px; + line-height: 22px; +} .theater-events { width: 100%; } @@ -12,4 +57,120 @@ max-width: 150px; width: 100%; display: block; -} \ No newline at end of file +} +.collapsible-menu-active { + margin-bottom: 3px; + cursor: pointer; + background-color: #c00; + color: #fff; + border-radius: 3px; +} +.dynamic-tabs { + margin: 3px 0 25px; +} +.dynamic-tabs .nav { + display: none; +} +.dynamic-tabs .displayed { + display: block; +} +.dynamic-tabs #tabAttivazione .active_link a { + border-bottom: 3px solid #52b733 !important; +} +.dynamic-tabs #tabDettagliCorso .active_link a { + border-bottom: 3px solid #337ab7 !important; +} +.dynamic-tabs #tabLezioni .active_link a { + border-bottom: 3px solid #e2c62b !important; +} +.dynamic-tabs #tabEsami .active_link a { + border-bottom: 3px solid #cc0000 !important; +} +.dynamic-tabs #tabQuestionario .active_link a { + border-bottom: 3px solid #972be2 !important; +} +.dynamic-tabs .active_link a { + background-color: #f5f5f5; + color: #333; +} +.dynamic-tabs-links li a, +.dynamic-tabs-links li .active a { + color: #fff !important; +} +.dynamic-tabs-links li .attivazione { + background-color: #52b733 !important; +} +.dynamic-tabs-links li .dettagli { + background-color: #337ab7 !important; +} +.dynamic-tabs-links li .lezioni { + background-color: #e2c62b !important; +} +.dynamic-tabs-links li .esami { + background-color: #cc0000 !important; +} +.dynamic-tabs-links li .questionario { + background-color: #972be2 !important; +} +.course-panels { + border: none; + display: flex; + justify-content: space-between; +} +.course-panels .heading { + padding: 10px 15px; + color: #fff; + margin: 0 2.5px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.course-panels .heading:first-child { + margin-left: 0; +} +.course-panels .heading:last-child { + margin-right: 0; +} +.course-panels .heading h2 { + font-size: 15px; + cursor: pointer; +} +.course-panels .heading a { + color: #fff !important; +} +.course-panels .attivazione { + background-color: #52b733 !important; +} +.course-panels .dettagli { + background-color: #337ab7 !important; +} +.course-panels .lezioni { + background-color: #e2c62b !important; +} +.course-panels .esami { + background-color: #cc0000 !important; +} +.course-panels .questionario { + background-color: #972be2 !important; +} +#titleDescription { + padding: 10px 0; + font-style: italic; + text-align: center; +} +.course__title-cri { + max-width: 650px; + margin: 0 auto; + padding-bottom: 10px; + font-size: 22px; + text-align: center; + font-weight: bold; + color: #3c763d; +} +.course__name { + margin: 0 0 20px; + padding: 0 0 10px; + border-bottom: 1px solid #e6e6e6; + text-align: center; + font-size: 16px; +} diff --git a/base/static/css/gaia.less b/base/static/css/gaia.less index 17f70b38b..d10a95f26 100644 --- a/base/static/css/gaia.less +++ b/base/static/css/gaia.less @@ -145,4 +145,110 @@ background-color: #c00; color: #fff; border-radius: 3px; +} + + +// Variables +@attivazione_hex: #52b733; +@dettagli_hex: #337ab7; +@lezioni_hex: #e2c62b; +@esami_hex: #c00; +@questionario_hex: #972be2; + + +.dynamic-tabs { + margin: 3px 0 25px; + + .nav {display:none;} + .displayed {display:block;} + + #tabAttivazione { + .active_link a {border-bottom: 3px solid @attivazione_hex !important;} + } + #tabDettagliCorso { + .active_link a {border-bottom: 3px solid @dettagli_hex !important;} + } + #tabLezioni { + .active_link a {border-bottom: 3px solid @lezioni_hex !important;} + } + #tabEsami { + .active_link a {border-bottom: 3px solid @esami_hex !important;} + } + #tabQuestionario { + .active_link a {border-bottom: 3px solid @questionario_hex !important;} + } + + .active_link { + a { + background-color: #f5f5f5; + color: #333; + } + } +} + +.dynamic-tabs-links { + li { + a, .active a { + color: #fff !important; + } + .attivazione {background-color: @attivazione_hex !important;} + .dettagli {background-color: @dettagli_hex !important;} + .lezioni {background-color: @lezioni_hex !important;} + .esami {background-color: @esami_hex !important;} + .questionario {background-color: @questionario_hex !important;} + } +} + +.course-panels { + border: none; + display: flex; + justify-content: space-between; + + .heading { + padding: 10px 15px; + color: #fff; + margin:0 2.5px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + &:first-child {margin-left:0;} + &:last-child {margin-right:0;} + h2 { + font-size:15px; + cursor:pointer; + } + a { + color: #fff !important; + } + } + .attivazione {background-color: @attivazione_hex !important;} + .dettagli {background-color: @dettagli_hex !important;} + .lezioni {background-color: @lezioni_hex !important;} + .esami {background-color: @esami_hex !important;} + .questionario {background-color: @questionario_hex !important;} +} + +#titleDescription { + padding:10px 0; + font-style: italic; + text-align:center; +} + +.course { + &__title-cri { + max-width:650px; + margin: 0 auto; + padding-bottom: 10px; + font-size: 22px; + text-align: center; + font-weight: bold; + color: #3c763d; + } + &__name { + margin: 0 0 20px; + padding:0 0 10px; + border-bottom: 1px solid #e6e6e6; + text-align:center; + font-size:16px; + } } \ No newline at end of file diff --git a/base/static/img/cri-logo-m.jpg b/base/static/img/cri-logo-m.jpg new file mode 100644 index 000000000..1e677c7d2 Binary files /dev/null and b/base/static/img/cri-logo-m.jpg differ diff --git a/base/static/img/un-italia-che-aiuta.png b/base/static/img/un-italia-che-aiuta.png new file mode 100644 index 000000000..2fc72833c Binary files /dev/null and b/base/static/img/un-italia-che-aiuta.png differ diff --git a/base/templates/base_autorizzazioni.html b/base/templates/base_autorizzazioni.html index f22aac131..a4659ff3d 100644 --- a/base/templates/base_autorizzazioni.html +++ b/base/templates/base_autorizzazioni.html @@ -1,16 +1,12 @@ {% extends 'base_autorizzazioni_vuota.html' %} -{% block pagina_titolo %} - ({{ richieste.count }}) Richieste -{% endblock %} +{% block pagina_titolo %}({{ richieste.count }}) Richieste{% endblock %} {% block app_contenuto %} -
- - Suggerimento — Cliccando su 'Conferma' o 'Nega' con il tasto centrale del mouse, - aprirà la pagina in una nuova finestra. + Suggerimento — Cliccando su 'Conferma' o 'Nega' con il tasto centrale del mouse, aprirà la pagina in una nuova finestra.
+ {% for richiesta in richieste %}
@@ -19,82 +15,44 @@ {% include richiesta.template_path %}
- {% if richiesta.pk in richieste_bloccate.corsi %} - - - Corso non ancora iniziato, impossibile processare la richiesta. - {% else %} - {% endif %}
-

- {% empty %} -
-

- - Ben fatto! -

-

- Non ci sono richieste in attesa. -

- +

Ben fatto!

+

Non ci sono richieste in attesa.

{% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/base/templates/base_autorizzazioni_concedi.html b/base/templates/base_autorizzazioni_concedi.html index 1bbc7fba4..24d67ebd7 100644 --- a/base/templates/base_autorizzazioni_concedi.html +++ b/base/templates/base_autorizzazioni_concedi.html @@ -1,44 +1,32 @@ {% extends 'base_avviso.html' %} {% load bootstrap3 %} - {% block pagina_titolo %}Consenti{% endblock %} - {% block avviso_titolo %}Consenti{% endblock %} - {% block avviso_corpo %} - {% if richiesta.necessaria %} -

Per dare consenso a questa richiesta, è necessario aggiungere le seguenti informazioni.

+ {% if not modulo.IS_CORSO_NUOVO %} +

Per dare consenso a questa richiesta, è necessario aggiungere le seguenti informazioni.

+ {% endif %}
{% csrf_token %} {% bootstrap_form modulo %}
{% else %} - -

- Richiesta autorizzata. - -

- - +

Richiesta autorizzata.

{% endif %} - - - Torna all'elenco richieste in attesa - - -

 

- -{% endblock %} \ No newline at end of file +

+ + Torna all'elenco richieste in attesa + +

+{% endblock %} diff --git a/base/templates/base_autorizzazioni_inc_formazione_partecipazionecorsobase.html b/base/templates/base_autorizzazioni_inc_formazione_partecipazionecorsobase.html index cdafcfd0a..5fca5e153 100644 --- a/base/templates/base_autorizzazioni_inc_formazione_partecipazionecorsobase.html +++ b/base/templates/base_autorizzazioni_inc_formazione_partecipazionecorsobase.html @@ -1,5 +1,3 @@ -

{{ richiesta.richiedente.link|safe }} chiede di essere - contattat{{ richiesta.richiedente.genere_o_a }} per - iscriversi al {{ richiesta.oggetto.corso.link|safe }} che inizierà il - {{ richiesta.oggetto.corso.data_inizio|date:"DATETIME_FORMAT" }}.

- +

{{ richiesta.richiedente.link|safe }} chiede di essere contattat{{ richiesta.richiedente.genere_o_a }} + per iscriversi al {{ richiesta.oggetto.corso.link|safe }} + che inizierà il {{ richiesta.oggetto.corso.data_inizio|date:"DATETIME_FORMAT" }}.

\ No newline at end of file diff --git a/base/templates/base_avviso.html b/base/templates/base_avviso.html index df095a11f..bc427f854 100755 --- a/base/templates/base_avviso.html +++ b/base/templates/base_avviso.html @@ -1,13 +1,10 @@ {% extends "base_vuota.html" %} -{% block base_body_classe %}sfondo-grigio-chiaro{% endblock %} - {% block pagina_titolo %}Avviso{% endblock %} {% block pagina_principale %}
-
diff --git a/base/templates/base_bootstrap.html b/base/templates/base_bootstrap.html index f5758b06c..364e2b01e 100755 --- a/base/templates/base_bootstrap.html +++ b/base/templates/base_bootstrap.html @@ -4,12 +4,10 @@ {% load compress %} {% block base_head %} - {% block pagina_css %} - {% compress css %} @@ -23,12 +21,9 @@ {% endcompress %} - - {% block pagina_css_extra %} - {% endblock %} - + {% block pagina_css_extra %}{% endblock %} {% endblock %} diff --git a/base/viste.py b/base/viste.py index a281b26c3..8f56db0e3 100755 --- a/base/viste.py +++ b/base/viste.py @@ -1,8 +1,7 @@ -import mimetypes +import json from datetime import date, timedelta, datetime, time -import os - +from django.apps import apps from django.conf import settings as django_settings from django.contrib.auth import get_user_model, load_backend, login from django.contrib.auth.tokens import default_token_generator @@ -13,32 +12,34 @@ from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, render_to_response, get_object_or_404, redirect from django.template.response import TemplateResponse -from django.utils import timezone from django.utils.encoding import force_bytes, force_text from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode -# Le viste base vanno qui. -from django.views.decorators.cache import cache_page -from django.apps import apps from django.views.decorators.clickjacking import xframe_options_exempt +from jorvik import settings from anagrafica.costanti import LOCALE, PROVINCIALE, REGIONALE from anagrafica.models import Sede, Persona from anagrafica.permessi.applicazioni import PRESIDENTE, UFFICIO_SOCI, UFFICIO_SOCI_TEMPORANEO, UFFICIO_SOCI_UNITA -from anagrafica.permessi.costanti import ERRORE_PERMESSI, LETTURA, GESTIONE_SEDE +from anagrafica.permessi.costanti import ERRORE_PERMESSI, GESTIONE_SEDE from autenticazione.funzioni import pagina_pubblica, pagina_anonima, pagina_privata from autenticazione.models import Utenza -from base import errori -from base.errori import errore_generico, messaggio_generico -from base.forms import ModuloRecuperaPassword, ModuloMotivoNegazione, ModuloLocalizzatore, ModuloLocalizzatoreItalia -from base.forms_extra import ModuloRichiestaSupportoPersone -from base.geo import Locazione -from base.models import Autorizzazione, Token -from base.tratti import ConPDF -from base.utils import get_drive_file, rimuovi_scelte from formazione.models import PartecipazioneCorsoBase, Aspirante -from jorvik import settings from posta.models import Messaggio -import json +from .errori import errore_generico, messaggio_generico +from .forms import ModuloRecuperaPassword, ModuloLocalizzatore, ModuloLocalizzatoreItalia +from .forms_extra import ModuloRichiestaSupportoPersone +from .geo import Locazione +from .models import Autorizzazione, Token +from .utils import get_drive_file, rimuovi_scelte + + +IGNORA_AUTORIZZAZIONI = [ + # ContentType.objects.get_for_model(PartecipazioneCorsoBase).pk +] + +ORDINE_ASCENDENTE = 'creazione' +ORDINE_DISCENDENTE = '-creazione' +ORDINE_DEFAULT = ORDINE_DISCENDENTE @pagina_pubblica @@ -211,6 +212,7 @@ def informazioni(request, me): """ return 'base_informazioni.html' + @pagina_pubblica def informazioni_aggiornamenti(request, me): """ @@ -218,6 +220,7 @@ def informazioni_aggiornamenti(request, me): """ return 'base_informazioni_aggiornamenti.html' + @pagina_pubblica def informazioni_sicurezza(request, me): """ @@ -225,6 +228,7 @@ def informazioni_sicurezza(request, me): """ return 'base_informazioni_sicurezza.html' + @pagina_pubblica def informazioni_condizioni(request, me): """ @@ -232,6 +236,7 @@ def informazioni_condizioni(request, me): """ return 'base_informazioni_condizioni.html' + @pagina_pubblica def informazioni_cookie(request, me): """ @@ -239,6 +244,7 @@ def informazioni_cookie(request, me): """ return 'base_informazioni_cookie.html' + @pagina_pubblica def imposta_cookie(request, me): """ @@ -289,11 +295,6 @@ def informazioni_sede(request, me, slug): return 'base_informazioni_sede.html', contesto -IGNORA_AUTORIZZAZIONI = [ - # ContentType.objects.get_for_model(PartecipazioneCorsoBase).pk -] - - def pulisci_autorizzazioni(richieste): pulite = False for richiesta in richieste: @@ -303,24 +304,21 @@ def pulisci_autorizzazioni(richieste): return pulite -ORDINE_ASCENDENTE = 'creazione' -ORDINE_DISCENDENTE = '-creazione' -ORDINE_DEFAULT = ORDINE_DISCENDENTE - - @pagina_privata def autorizzazioni(request, me, content_type_pk=None): - """ - Mostra elenco delle autorizzazioni in attesa. - """ + """ Mostra elenco delle autorizzazioni in attesa. """ + richieste_bloccate = dict() richieste = me._autorizzazioni_in_attesa().exclude(oggetto_tipo_id__in=IGNORA_AUTORIZZAZIONI) - richieste_bloccate['corsi'] = PartecipazioneCorsoBase.richieste_non_processabili(richieste) + + # richieste_bloccate['corsi'] = PartecipazioneCorsoBase.richieste_non_processabili(richieste) + if 'ordine' in request.GET: if request.GET['ordine'] == 'ASC': request.session['autorizzazioni_ordine'] = ORDINE_ASCENDENTE else: request.session['autorizzazioni_ordine'] = ORDINE_DISCENDENTE + ordine = request.session.get('autorizzazioni_ordine', default=ORDINE_DEFAULT) sezioni = () # Ottiene le sezioni @@ -363,11 +361,9 @@ def autorizzazioni(request, me, content_type_pk=None): @pagina_privata def autorizzazione_concedi(request, me, pk=None): - """ - Mostra il modulo da compilare per il consenso, ed eventualmente registra l'accettazione. - """ - richiesta = get_object_or_404(Autorizzazione, pk=pk) + """ Mostra il modulo da compilare per il consenso, ed eventualmente registra l'accettazione. """ + richiesta = get_object_or_404(Autorizzazione, pk=pk) torna_url = request.session.get('autorizzazioni_torna_url', default="/autorizzazioni/") # Controlla che io possa firmare questa autorizzazione @@ -378,39 +374,39 @@ def autorizzazione_concedi(request, me, pk=None): torna_titolo="Richieste in attesa", torna_url=torna_url, ) - if not PartecipazioneCorsoBase.controlla_richiesta_processabile(richiesta): - return errore_generico(request, me, - titolo="Richiesta non processabile", - messaggio="Questa richiesta non può essere processata.", - torna_titolo="Richieste in attesa", - torna_url=torna_url, - ) - modulo = None + # if not PartecipazioneCorsoBase.controlla_richiesta_processabile(richiesta): + # return errore_generico(request, me, + # titolo="Richiesta non processabile", + # messaggio="Questa richiesta non può essere processata.", + # torna_titolo="Richieste in attesa", + # torna_url=torna_url, + # ) + + form = None # Se la richiesta ha un modulo di consenso if richiesta.oggetto.autorizzazione_concedi_modulo(): if request.POST: - modulo = richiesta.oggetto.autorizzazione_concedi_modulo()(request.POST) - - if modulo.is_valid(): + form = richiesta.oggetto.autorizzazione_concedi_modulo()(request.POST) + if form.is_valid(): # Accetta la richiesta con modulo - richiesta.concedi(me, modulo=modulo) + richiesta.concedi(me, modulo=form) else: - modulo = richiesta.oggetto.autorizzazione_concedi_modulo()() + form = richiesta.oggetto.autorizzazione_concedi_modulo()() else: # Accetta la richiesta senza modulo richiesta.concedi(me) - contesto = { - "modulo": modulo, + context = { + "modulo": form, "richiesta": richiesta, "torna_url": torna_url, } - return 'base_autorizzazioni_concedi.html', contesto + return 'base_autorizzazioni_concedi.html', context @pagina_privata @@ -493,7 +489,6 @@ def geo_localizzatore(request, me): app_label = request.session['app_label'] model = request.session['model'] pk = int(request.session['pk']) - continua_url = request.session['continua_url'] oggetto = apps.get_model(app_label, model) oggetto = oggetto.objects.get(pk=pk) @@ -501,35 +496,37 @@ def geo_localizzatore(request, me): ricerca = False if request.GET.get('italia', False): - modulo = ModuloLocalizzatoreItalia(request.POST or None,) + form = ModuloLocalizzatoreItalia(request.POST or None,) else: - modulo = ModuloLocalizzatore(request.POST or None,) - if modulo.is_valid(): - comune = modulo.cleaned_data['comune'] if not modulo.cleaned_data['comune'] \ - else "%s, Province of %s" % (modulo.cleaned_data['comune'], modulo.cleaned_data['provincia']) - stringa = "%s, %s, %s" % (modulo.cleaned_data['indirizzo'], - comune, - modulo.cleaned_data['stato'],) + form = ModuloLocalizzatore(request.POST or None,) + + if form.is_valid(): + cd = form.cleaned_data + comune = cd['comune'] if not cd['comune'] else "%s, Province of %s" % (cd['comune'], cd['provincia']) + stringa = "%s, %s, %s" % (cd['indirizzo'], comune, cd['stato']) risultati = Locazione.cerca(stringa) ricerca = True - contesto = { + context = { "locazione": oggetto.locazione, - "continua_url": continua_url, - "modulo": modulo, + "continua_url": request.session['continua_url'], + "modulo": form, "ricerca": ricerca, "risultati": risultati, "oggetto": oggetto, } - return 'base_geo_localizzatore.html', contesto + if app_label == 'formazione' and model == 'corsobase': + context['is_corsobase'] = 1 # instead of True + + return 'base_geo_localizzatore.html', context @pagina_privata def geo_localizzatore_imposta(request, me): - app_label = request.session['app_label'] - model = request.session['model'] - pk = int(request.session['pk']) + session = request.session + app_label, model, pk = session['app_label'], session['model'], int(session['pk']) + oggetto = apps.get_model(app_label, model) oggetto = oggetto.objects.get(pk=pk) @@ -537,50 +534,13 @@ def geo_localizzatore_imposta(request, me): return redirect("/geo/localizzatore/") + @pagina_privata def pdf(request, me, app_label, model, pk): - oggetto = apps.get_model(app_label, model) - oggetto = oggetto.objects.get(pk=pk) - if not isinstance(oggetto, ConPDF): - return errore_generico(request, None, - messaggio="Impossibile generare un PDF per il tipo specificato.") - - if 'token' in request.GET: - if not oggetto.token_valida(request.GET['token']): - return errore_generico(request, me, titolo="Token scaduta", - messaggio="Il link usato è scaduto.") - - elif not me.permessi_almeno(oggetto, LETTURA): - return redirect(ERRORE_PERMESSI) - - pdf = oggetto.genera_pdf() - - # Se sto scaricando un tesserino, forza lo scaricamento. - if 'tesserini' in pdf.file.path: - return pdf_forza_scaricamento(request, pdf) - - return redirect(pdf.download_url) - - -def pdf_forza_scaricamento(request, pdf): - """ - Forza lo scaricamento di un file pdf. - Da usare con cautela, perche' carica il file in memoria - e blocca il thread fino al completamento della richiesta. - :param request: - :param pdf: - :return: - """ - - percorso_completo = pdf.file.path - - with open(percorso_completo, 'rb') as f: - data = f.read() + from .classes.pdf import BaseGeneraPDF - response = HttpResponse(data, content_type=mimetypes.guess_type(percorso_completo)[0]) - response['Content-Disposition'] = "attachment; filename={0}".format(pdf.nome) - response['Content-Length'] = os.path.getsize(percorso_completo) - return response + pdf = BaseGeneraPDF(request, me, app_label, model, pk) + return pdf.make() @pagina_pubblica diff --git a/centrale_operativa/urls.py b/centrale_operativa/urls.py new file mode 100644 index 000000000..e75122e35 --- /dev/null +++ b/centrale_operativa/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import url +from . import viste + + +app_label = 'centrale_operativa' +urlpatterns = [ + url(r'^$', viste.co), + url(r'^reperibilita/$', viste.co_reperibilita), + url(r'^poteri/$', viste.co_poteri), + url(r'^poteri/(?P[0-9]+)/$', viste.co_poteri_switch), + url(r'^turni/$', viste.co_turni), + url(r'^turni/(?P[0-9]+)/monta/$', viste.co_turni_monta), + url(r'^turni/(?P[0-9]+)/smonta/$', viste.co_turni_smonta), +] diff --git a/curriculum/admin.py b/curriculum/admin.py index f0d983b7d..e3c3d5ed7 100755 --- a/curriculum/admin.py +++ b/curriculum/admin.py @@ -1,22 +1,55 @@ from django.contrib import admin from base.admin import InlineAutorizzazione -from curriculum.models import TitoloPersonale -from curriculum.models import Titolo from gruppi.readonly_admin import ReadonlyAdminMixin +from .models import (Titolo, TitleGoal, TitoloPersonale) @admin.register(Titolo) class AdminTitolo(ReadonlyAdminMixin, admin.ModelAdmin): - search_fields = ["nome", ] - list_display = ("nome", "tipo", "inseribile_in_autonomia", 'area',) - list_filter = ("tipo", "richiede_conferma", "inseribile_in_autonomia",) + search_fields = ['nome', ] + list_display = ('nome', 'tipo', 'goal_obbiettivo_stragetico', 'goal_propedeuticita', + 'goal_unit_reference', 'inseribile_in_autonomia', 'expires_after', 'area',) + list_filter = ('is_active', 'cdf_livello', 'area', "tipo", "richiede_conferma", + "inseribile_in_autonomia", 'goal__unit_reference',) + + def goal_obbiettivo_stragetico(self, obj): + return obj.goal.unit_reference if hasattr(obj.goal, + 'unit_reference') else 'not set' + + def goal_propedeuticita(self, obj): + return obj.goal.propedeuticita if hasattr(obj.goal, + 'propedeuticita') else 'not set' + + def goal_unit_reference(self, obj): + return obj.goal.get_unit_reference_display() if hasattr(obj.goal, + 'unit_reference') else 'not set' + + goal_obbiettivo_stragetico.short_description = 'Obiettivo strategico di riferimento' + goal_propedeuticita.short_description = 'Propedeuticità' + goal_unit_reference.short_description = 'Unità riferimento' + + +@admin.register(TitleGoal) +class AdminTitleGoal(admin.ModelAdmin): + list_display = ['__str__', 'obbiettivo_stragetico', 'propedeuticita', + 'unit_reference'] + list_filter = ['unit_reference',] @admin.register(TitoloPersonale) class AdminTitoloPersonale(ReadonlyAdminMixin, admin.ModelAdmin): - search_fields = ["titolo__nome", "persona__nome", "persona__cognome", "persona__codice_fiscale", ] - list_display = ("titolo", "persona", "data_ottenimento", "creazione", "certificato",) - list_filter = ("titolo__tipo", "creazione", "data_ottenimento",) - raw_id_fields = ("persona", "certificato_da", "titolo",) + search_fields = ["titolo__nome", "persona__nome", "persona__cognome", + "persona__codice_fiscale",] + list_display = ("titolo", "persona", 'is_course_title', + "data_ottenimento", 'data_scadenza', "certificato", + "creazione", 'corso_partecipazione__corso') + list_filter = ("titolo__tipo", "creazione", "data_ottenimento", + 'is_course_title') + raw_id_fields = ("persona", "certificato_da", "titolo", 'corso_partecipazione',) inlines = [InlineAutorizzazione] + + def corso_partecipazione__corso(self, obj): + return obj.corso_partecipazione.corso \ + if hasattr(obj.corso_partecipazione, 'corso') else '' + corso_partecipazione__corso.short_description = 'Corso Partecipazione' diff --git a/curriculum/areas.py b/curriculum/areas.py index e548a4b82..3e443a57b 100644 --- a/curriculum/areas.py +++ b/curriculum/areas.py @@ -8,7 +8,30 @@ (6, 'Specializzazione'), ] + PATENTE_CIVILE_CHOICES = [ (0, 'Patente'), (1, 'Certificato di abilitazione professionale'), ] + + +# Area a cui appartiene titolo (dati allineati al doc. elenco_corsi.xls) +OBBIETTIVO_STRATEGICO_SALUTE = '1' +OBBIETTIVO_STRATEGICO_SOCIALE = '2' +OBBIETTIVO_STRATEGICO_EMERGENZA = '3' +OBBIETTIVO_STRATEGICO_ADVOCACY = '4' +OBBIETTIVO_STRATEGICO_GIOVANI = '5' +OBBIETTIVO_STRATEGICO_SVILUPPO = '6' +OBBIETTIVO_STRATEGICO_MIGRAZIONI = '7' +OBBIETTIVO_STRATEGICO_COOPERAZIONI_INT = '8' + +OBBIETTIVI_STRATEGICI = [ + (OBBIETTIVO_STRATEGICO_SALUTE, 'Salute'), + (OBBIETTIVO_STRATEGICO_SOCIALE, 'Inclusione Sociale'), + (OBBIETTIVO_STRATEGICO_EMERGENZA, 'Emergenza'), + (OBBIETTIVO_STRATEGICO_ADVOCACY, 'Principi e Valori'), + (OBBIETTIVO_STRATEGICO_GIOVANI, 'Giovani'), + (OBBIETTIVO_STRATEGICO_SVILUPPO, 'Sviluppo Organizzativo'), + (OBBIETTIVO_STRATEGICO_MIGRAZIONI, 'Migrazioni'), + (OBBIETTIVO_STRATEGICO_COOPERAZIONI_INT, 'Cooperazione Internazionale'), +] diff --git a/curriculum/autocomplete_light_registry.py b/curriculum/autocomplete_light_registry.py index d440c2305..4f747a33b 100644 --- a/curriculum/autocomplete_light_registry.py +++ b/curriculum/autocomplete_light_registry.py @@ -27,9 +27,10 @@ def choices_for_request(self): # Filter by area_id = r.GET.get('area', None) if area_id not in ['', None]: + # Titoli CRI (TC) if titoli_tipo == Titolo.TITOLO_CRI: - self.choices = self.choices.filter(area=area_id) + self.choices = self.choices.filter(goal__unit_reference=area_id) # Titoli di studio (TS) elif titoli_tipo == Titolo.TITOLO_STUDIO: @@ -43,4 +44,26 @@ def choices_for_request(self): return super().choices_for_request() + +class TitoloCRIAutocompletamento(autocomplete_light.AutocompleteModelBase): + model = Titolo + split_words = True + search_fields = ['nome',] + attrs = { + 'data-autocomplete-minimum-characters': 0, + } + + def choices_for_request(self): + self.choices = self.choices.filter( + is_active=True, + inseribile_in_autonomia=True, + area__isnull=False, + nome__isnull=False, + tipo=Titolo.TITOLO_CRI, + ).order_by('nome').distinct('nome') + + return super().choices_for_request() + + autocomplete_light.register(TitoloAutocompletamento) +autocomplete_light.register(TitoloCRIAutocompletamento) diff --git a/curriculum/cron.py b/curriculum/cron.py new file mode 100644 index 000000000..07c3f616b --- /dev/null +++ b/curriculum/cron.py @@ -0,0 +1,29 @@ +from django_cron import CronJobBase, Schedule +from posta.models import Messaggio +from jorvik.settings import DEFAULT_FROM_EMAIL +from .models import TitoloPersonale + + +class CronCheckExpiredCourseTitles(CronJobBase): + RUN_EVERY_HOURS = 23 + RUN_EVERY_MINS = RUN_EVERY_HOURS * 60 + + schedule = Schedule(run_every_mins=RUN_EVERY_MINS) + code = 'curriculum.check_expired_course_titles' + + def do(self): + titles = TitoloPersonale.get_expired_course_titles() + if titles: + for title in titles: + persona = title.persona + email = persona.utenza.email + Messaggio.costruisci_e_accoda( + oggetto="Il tuo titolo %s è scaduto." % title.titolo.nome, + modello="email_expired_course_titolo_personale.html", + corpo={ + 'title': title, + 'persona': persona, + }, + destinatari=[persona], + ) + print('Sono stati notificati degli utenti con dei titoli del corso scaduti.') \ No newline at end of file diff --git a/curriculum/forms.py b/curriculum/forms.py index 3bbb2a124..a57b1940e 100644 --- a/curriculum/forms.py +++ b/curriculum/forms.py @@ -1,8 +1,8 @@ from django import forms from autocomplete_light import shortcuts as autocomplete_light -from .areas import TITOLO_STUDIO_CHOICES, PATENTE_CIVILE_CHOICES -from .models import TitoloPersonale, Titolo +from .areas import TITOLO_STUDIO_CHOICES, PATENTE_CIVILE_CHOICES, OBBIETTIVI_STRATEGICI +from .models import Titolo, TitleGoal, TitoloPersonale class ModuloNuovoTitoloPersonale(autocomplete_light.ModelForm): @@ -37,7 +37,7 @@ def __init__(self, tipo, tipo_display, *args, **kwargs): """ Add field conditionally""" if tipo in [Titolo.TITOLO_CRI, Titolo.TITOLO_STUDIO, Titolo.PATENTE_CIVILE]: SELECT_AREA_CHOICES = { - Titolo.TITOLO_CRI: Titolo.AREA_CHOICES, # Titoli CRI (TC) + Titolo.TITOLO_CRI: OBBIETTIVI_STRATEGICI, # Titoli CRI (TC) Titolo.TITOLO_STUDIO: TITOLO_STUDIO_CHOICES, # Titoli di studio (TS) Titolo.PATENTE_CIVILE: PATENTE_CIVILE_CHOICES, # Patenti civili (PP) } diff --git a/curriculum/migrations/0008_auto_20181119_1429.py b/curriculum/migrations/0008_auto_20181119_1429.py new file mode 100644 index 000000000..fde0e5fb9 --- /dev/null +++ b/curriculum/migrations/0008_auto_20181119_1429.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-19 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0007_auto_20181022_1223'), + ] + + operations = [ + migrations.CreateModel( + name='TitleGoal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unit_reference', models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza)'), ('4', 'Advocacy e mediazione umanitaria'), ('5', 'Giovani'), ('6', 'Sviluppo')], max_length=3, null=True, verbose_name='Unità riferimento')), + ('propedeuticita', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propedeuticità')), + ], + options={ + 'verbose_name': 'Titolo: Propedeuticità', + 'verbose_name_plural': 'Titoli: Propedeuticità', + }, + ), + migrations.CreateModel( + name='TitleLevel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nome')), + ('goal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='curriculum.TitleGoal')), + ], + options={ + 'verbose_name': 'Titolo: Livello', + 'verbose_name_plural': 'Titoli: Livelli', + }, + ), + migrations.AlterModelOptions( + name='titolo', + options={'permissions': (('view_titolo', 'Can view titolo'),), 'verbose_name_plural': 'Titoli: Elenco'}, + ), + migrations.AlterField( + model_name='titolo', + name='nome', + field=models.CharField(db_index=True, max_length=255, verbose_name='Nome del corso'), + ), + migrations.AddField( + model_name='titolo', + name='livello', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='curriculum.TitleLevel', verbose_name='Livello'), + ), + ] diff --git a/curriculum/migrations/0009_auto_20181130_1550.py b/curriculum/migrations/0009_auto_20181130_1550.py new file mode 100644 index 000000000..27b4f701e --- /dev/null +++ b/curriculum/migrations/0009_auto_20181130_1550.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-30 15:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0008_auto_20181119_1429'), + ] + + operations = [ + migrations.RemoveField( + model_name='titlelevel', + name='goal', + ), + migrations.RemoveField( + model_name='titolo', + name='livello', + ), + migrations.AddField( + model_name='titolo', + name='goal', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='curriculum.TitleGoal', verbose_name='Obbiettivo'), + ), + migrations.AddField( + model_name='titolo', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='titolo', + name='area', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza)'), ('4', 'Advocacy e mediazione umanitaria'), ('5', 'Giovani'), ('6', 'Sviluppo')], db_index=True, max_length=5, null=True), + ), + migrations.AlterField( + model_name='titolo', + name='nome', + field=models.CharField(db_index=True, max_length=255), + ), + migrations.DeleteModel( + name='TitleLevel', + ), + ] diff --git a/curriculum/migrations/0010_titolo_expires_after.py b/curriculum/migrations/0010_titolo_expires_after.py new file mode 100644 index 000000000..0684bb4fe --- /dev/null +++ b/curriculum/migrations/0010_titolo_expires_after.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-12-05 14:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0009_auto_20181130_1550'), + ] + + operations = [ + migrations.AddField( + model_name='titolo', + name='expires_after', + field=models.IntegerField(blank=True, help_text='Indicare in giorni (es: per 1 anno indicare 365)', null=True, verbose_name='Scadenza'), + ), + ] diff --git a/curriculum/migrations/0011_titolopersonale_is_course_title.py b/curriculum/migrations/0011_titolopersonale_is_course_title.py new file mode 100644 index 000000000..b6c323466 --- /dev/null +++ b/curriculum/migrations/0011_titolopersonale_is_course_title.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-12-10 12:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0010_titolo_expires_after'), + ] + + operations = [ + migrations.AddField( + model_name='titolopersonale', + name='is_course_title', + field=models.BooleanField(default=False), + ), + ] diff --git a/curriculum/migrations/0012_auto_20190206_1427.py b/curriculum/migrations/0012_auto_20190206_1427.py new file mode 100644 index 000000000..75fc1ef2f --- /dev/null +++ b/curriculum/migrations/0012_auto_20190206_1427.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-02-06 14:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0011_titolopersonale_is_course_title'), + ] + + operations = [ + migrations.AddField( + model_name='titolo', + name='cdf_durata_corso', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='titolo', + name='cdf_livello', + field=models.CharField(blank=True, choices=[('1', 'I Livello'), ('2', 'II Livello'), ('3', 'III Livello'), ('4', 'IV Livello')], max_length=3, null=True), + ), + migrations.AlterField( + model_name='titlegoal', + name='unit_reference', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo'), ('7', 'Migrazioni'), ('8', 'Cooperazioni Internazionali')], max_length=3, null=True, verbose_name='Unità riferimento'), + ), + migrations.AlterField( + model_name='titolo', + name='area', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo'), ('7', 'Migrazioni'), ('8', 'Cooperazioni Internazionali')], db_index=True, max_length=5, null=True), + ), + ] diff --git a/curriculum/migrations/0013_titolo_description.py b/curriculum/migrations/0013_titolo_description.py new file mode 100644 index 000000000..68a1379e5 --- /dev/null +++ b/curriculum/migrations/0013_titolo_description.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-04-08 17:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0012_auto_20190206_1427'), + ] + + operations = [ + migrations.AddField( + model_name='titolo', + name='description', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/curriculum/migrations/0014_auto_20190524_1251.py b/curriculum/migrations/0014_auto_20190524_1251.py new file mode 100644 index 000000000..1b6c20bea --- /dev/null +++ b/curriculum/migrations/0014_auto_20190524_1251.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-24 12:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0013_titolo_description'), + ] + + operations = [ + migrations.AlterField( + model_name='titlegoal', + name='unit_reference', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Inclusione Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo Organizzativo'), ('7', 'Migrazioni'), ('8', 'Cooperazione Internazionale')], max_length=3, null=True, verbose_name='Unità riferimento'), + ), + migrations.AlterField( + model_name='titolo', + name='area', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Inclusione Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo Organizzativo'), ('7', 'Migrazioni'), ('8', 'Cooperazione Internazionale')], db_index=True, max_length=5, null=True), + ), + ] diff --git a/curriculum/migrations/0015_titolopersonale_corso_partecipazione.py b/curriculum/migrations/0015_titolopersonale_corso_partecipazione.py new file mode 100644 index 000000000..b77f41ab4 --- /dev/null +++ b/curriculum/migrations/0015_titolopersonale_corso_partecipazione.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-24 14:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '__first__'), + ('curriculum', '0014_auto_20190524_1251'), + ] + + operations = [ + migrations.AddField( + model_name='titolopersonale', + name='corso_partecipazione', + field=models.ForeignKey(blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, related_name='titolo_ottenuto', to='formazione.PartecipazioneCorsoBase'), + ), + ] diff --git a/curriculum/models.py b/curriculum/models.py index 88ef47197..69572e09e 100755 --- a/curriculum/models.py +++ b/curriculum/models.py @@ -1,13 +1,13 @@ +from datetime import date + from django.db import models from django.utils import timezone +from .areas import OBBIETTIVI_STRATEGICI from base.models import ModelloSemplice, ConAutorizzazioni, ConVecchioID from base.tratti import ConMarcaTemporale -__author__ = 'alfioemanuele' - - class Titolo(ModelloSemplice, ConVecchioID): # Tipo del titolo COMPETENZA_PERSONALE = "CP" @@ -23,46 +23,83 @@ class Titolo(ModelloSemplice, ConVecchioID): (TITOLO_CRI, "Titolo CRI"), ) - # Area a cui appartiene titolo (dati allineati al doc. elenco_corsi.xls) - AREA_SALUTE = 'atc1' - AREA_SOCIALE = 'atc2' - AREA_EMERGENZE = 'atc3' - AREA_PRINCIPI_VALORI = 'atc4' - AREA_GIOVANI = 'atc5' - AREA_SVILUPPO = 'atc6' - AREA_CHOICES = [ - (AREA_SALUTE, 'Salute'), - (AREA_SOCIALE, 'Sociale'), - (AREA_EMERGENZE, 'Emergenze'), - (AREA_PRINCIPI_VALORI, 'Principi e valori'), - (AREA_GIOVANI, 'Giovani'), - (AREA_SVILUPPO, 'Sviluppo'), - ] - - tipo = models.CharField(max_length=2, choices=TIPO, db_index=True) - - richiede_conferma = models.BooleanField(default=False,) - richiede_data_ottenimento = models.BooleanField(default=False,) - richiede_luogo_ottenimento = models.BooleanField(default=False,) - richiede_data_scadenza = models.BooleanField(default=False,) - richiede_codice = models.BooleanField(default=False,) - inseribile_in_autonomia = models.BooleanField(default=True,) + CDF_LIVELLO_I = '1' + CDF_LIVELLO_II = '2' + CDF_LIVELLO_III = '3' + CDF_LIVELLO_IV = '4' + CDF_LIVELLI = ( + (CDF_LIVELLO_I, 'I Livello'), + (CDF_LIVELLO_II, 'II Livello'), + (CDF_LIVELLO_III, 'III Livello'), + (CDF_LIVELLO_IV, 'IV Livello'), + ) + goal = models.ForeignKey('TitleGoal', null=True, blank=True, + verbose_name="Obbiettivo", on_delete=models.PROTECT) nome = models.CharField(max_length=255, db_index=True) - area = models.CharField(max_length=5, - choices=AREA_CHOICES, - null=True, - blank=True, - db_index=True) + area = models.CharField(max_length=5, null=True, blank=True, db_index=True, + choices=OBBIETTIVI_STRATEGICI) + tipo = models.CharField(max_length=2, choices=TIPO, db_index=True) + description = models.CharField(max_length=255, null=True, blank=True) + is_active = models.BooleanField(default=True) + expires_after = models.IntegerField(null=True, blank=True, verbose_name="Scadenza", + help_text='Indicare in giorni (es: per 1 anno indicare 365)') + cdf_livello = models.CharField(max_length=3, choices=CDF_LIVELLI, + null=True, blank=True) + cdf_durata_corso = models.CharField(max_length=255, null=True, blank=True) + richiede_conferma = models.BooleanField(default=False) + richiede_data_ottenimento = models.BooleanField(default=False) + richiede_luogo_ottenimento = models.BooleanField(default=False) + richiede_data_scadenza = models.BooleanField(default=False) + richiede_codice = models.BooleanField(default=False) + inseribile_in_autonomia = models.BooleanField(default=True) class Meta: - verbose_name_plural = "Titoli" + verbose_name_plural = "Titoli: Elenco" permissions = ( ("view_titolo", "Can view titolo"), ) + @property + def is_titolo_cri(self): + return self.tipo == self.TITOLO_CRI + + @property + def is_course_title(self): + is_titolo_cri = self.is_titolo_cri + has_goal = self.goal + return is_titolo_cri and bool(has_goal) and bool(has_goal.obbiettivo_stragetico) + + @property + def expires_after_timedelta(self): + from datetime import timedelta + days = self.expires_after if self.expires_after else 0 + return timedelta(days=days) + def __str__(self): - return self.nome + # if self.tipo == self.TITOLO_CRI and self.goal: + # return "%s - %s - %s" % (self.nome, self.goal, + # self.goal.get_unit_reference_display()) + # else: + return str(self.nome) + + +class TitleGoal(models.Model): + unit_reference = models.CharField("Unità riferimento", max_length=3, + null=True, blank=True, choices=OBBIETTIVI_STRATEGICI) + propedeuticita = models.CharField("Propedeuticità", max_length=255, + null=True, blank=True) + + @property + def obbiettivo_stragetico(self): + return self.unit_reference + + def __str__(self): + return "%s (Obbiettivo %s)" % (self.propedeuticita, self.unit_reference) + + class Meta: + verbose_name = 'Titolo: Propedeuticità' + verbose_name_plural = 'Titoli: Propedeuticità' class TitoloPersonale(ModelloSemplice, ConMarcaTemporale, ConAutorizzazioni): @@ -85,14 +122,24 @@ class TitoloPersonale(ModelloSemplice, ConMarcaTemporale, ConAutorizzazioni): codice = models.CharField(max_length=128, null=True, blank=True, db_index=True, help_text="Codice/Numero identificativo del Titolo o Patente. " "Presente sul certificato o sulla Patente.") - codice_corso = models.CharField(max_length=128, null=True, - blank=True, db_index=True) + codice_corso = models.CharField(max_length=128, null=True, blank=True, db_index=True) certificato = models.BooleanField(default=False,) certificato_da = models.ForeignKey("anagrafica.Persona", null=True, related_name="titoli_da_me_certificati", on_delete=models.SET_NULL) + is_course_title = models.BooleanField(default=False) + + + # ValueError: Related model u'app.model' cannot be resolved + # https://stackoverflow.com/questions/33496333 + # curriculum.migrations.0015 was added: " ('formazione', '__first__')," + corso_partecipazione = models.ForeignKey('formazione.PartecipazioneCorsoBase', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='titolo_ottenuto') class Meta: verbose_name = "Titolo personale" @@ -103,11 +150,28 @@ class Meta: @property def attuale(self): - return self.data_scadenza is None or timezone.now() >= self.data_scadenza + now = timezone.now() + today = date(now.year, now.month, now.day) + return self.data_scadenza is None or \ + today >= self.data_scadenza def autorizzazione_negata(self, modulo=None, notifiche_attive=True, data=None): # Alla negazione, cancella titolo personale. self.delete() + @property + def is_expired_course_title(self): + now = timezone.now() + today = date(now.year, now.month, now.day) + if self.is_course_title and today > self.data_scadenza: + return True + return False + + @classmethod + def get_expired_course_titles(cls): + now = timezone.now() + today = date(now.year, now.month, now.day) + return cls.objects.filter(is_course_title=True, data_scadenza=today) + def __str__(self): return "%s di %s" % (self.titolo, self.persona) diff --git a/curriculum/tests.py b/curriculum/tests.py index 95537c613..b133ab1c5 100644 --- a/curriculum/tests.py +++ b/curriculum/tests.py @@ -1,15 +1,17 @@ from django import test -from .models import Titolo +from .models import Titolo, TitleGoal -class TitoloModelTest(test.TestCase): - def test_model_has_area_choices(self): - self.assertTrue(hasattr(Titolo, 'AREA_CHOICES'), - msg="Some forms/queries of the app can not work without AREA_CHOICES") +# class TitoloModelTest(test.TestCase): +# def test_model_has_area_choices(self): + class AreasModuleTest(test.TestCase): def test_areas_have_cv_areas_constants(self): from . import areas self.assertTrue(hasattr(areas, 'TITOLO_STUDIO_CHOICES')) - self.assertTrue(hasattr(areas, 'PATENTE_CIVILE_CHOICES')) \ No newline at end of file + self.assertTrue(hasattr(areas, 'PATENTE_CIVILE_CHOICES')) + self.assertTrue(hasattr(areas, 'OBBIETTIVI_STRATEGICI'), + msg="Some forms/queries of (curriculum, formazione apps) " + "can not work without TitleGoal.OBBIETTIVI_STRATEGICI") \ No newline at end of file diff --git a/curriculum/urls.py b/curriculum/urls.py new file mode 100644 index 000000000..8d8756298 --- /dev/null +++ b/curriculum/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from .views import cdf_titolo_json + +app_label = 'cv' +urlpatterns = [ + url(r'^cdf_titolo_json/$', cdf_titolo_json, name='cdf_titolo_json'), +] diff --git a/curriculum/views.py b/curriculum/views.py new file mode 100644 index 000000000..549c28160 --- /dev/null +++ b/curriculum/views.py @@ -0,0 +1,22 @@ +from django.http import JsonResponse +from autenticazione.funzioni import pagina_privata +from curriculum.models import Titolo +from formazione.constants import FORMAZIONE_ROLES + + +@pagina_privata +def cdf_titolo_json(request, me): + if request.is_ajax and me.deleghe_attuali(tipo__in=FORMAZIONE_ROLES).exists(): + area_id = request.POST.get('area', None) + cdf_livello = request.POST.get('cdf_livello', None) + + if cdf_livello and area_id: + query = Titolo.objects.filter(area=area_id[0], cdf_livello=cdf_livello[0]) + options_for_select = {option['id']: { + 'nome': option['nome'], + 'description': option['description']} + for option in query.values('id', 'nome', 'description')} + + return JsonResponse(options_for_select) + + return JsonResponse({}) diff --git a/formazione/__init__.py b/formazione/__init__.py index 6f6872788..e69de29bb 100755 --- a/formazione/__init__.py +++ b/formazione/__init__.py @@ -1 +0,0 @@ -__author__ = 'alfioemanuele' diff --git a/formazione/admin.py b/formazione/admin.py index d2b0e7f37..cb347daf4 100755 --- a/formazione/admin.py +++ b/formazione/admin.py @@ -1,21 +1,22 @@ -from anagrafica.admin import RAW_ID_FIELDS_DELEGA -from anagrafica.models import Delega from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline +from anagrafica.admin import RAW_ID_FIELDS_DELEGA +from anagrafica.models import Delega from base.admin import InlineAutorizzazione -from django.contrib.contenttypes.admin import GenericTabularInline -from formazione.models import CorsoBase, PartecipazioneCorsoBase, AssenzaCorsoBase, Aspirante, LezioneCorsoBase, InvitoCorsoBase from gruppi.readonly_admin import ReadonlyAdminMixin +from .models import (CorsoBase, CorsoFile, CorsoEstensione, CorsoLink, + Aspirante, PartecipazioneCorsoBase, AssenzaCorsoBase, LezioneCorsoBase, + InvitoCorsoBase, RelazioneCorso) -__author__ = 'alfioemanuele' - RAW_ID_FIELDS_CORSOBASE = ['sede', 'locazione',] RAW_ID_FIELDS_PARTECIPAZIONECORSOBASE = ['persona', 'corso', 'destinazione',] RAW_ID_FIELDS_INVITOCORSOBASE = ['persona', 'corso', 'invitante',] -RAW_ID_FIELDS_LEZIONECORSOBASE = ['corso',] +RAW_ID_FIELDS_LEZIONECORSOBASE = ['corso', 'docente',] RAW_ID_FIELDS_ASSENZACORSOBASE = ['lezione', 'persona', 'registrata_da',] RAW_ID_FIELDS_ASPIRANTE = ['persona', 'locazione',] +RAW_ID_FIELDS_ESTENSIONE = ['sede', 'titolo',] class InlineDelegaCorsoBase(ReadonlyAdminMixin, GenericTabularInline): @@ -49,6 +50,11 @@ class InlineAssenzaCorsoBase(ReadonlyAdminMixin, admin.TabularInline): raw_id_fields = RAW_ID_FIELDS_ASSENZACORSOBASE extra = 0 +class InlineEstensioneCorso(ReadonlyAdminMixin, admin.TabularInline): + model = CorsoEstensione + raw_id_fields = RAW_ID_FIELDS_ESTENSIONE + extra = 0 + def admin_corsi_base_attivi_invia_messaggi(modeladmin, request, queryset): corsi = queryset.filter(stato=CorsoBase.ATTIVO) @@ -61,19 +67,56 @@ def admin_corsi_base_attivi_invia_messaggi(modeladmin, request, queryset): @admin.register(CorsoBase) class AdminCorsoBase(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ['sede__nome', 'sede__genitore__nome', 'progressivo', 'anno', ] - list_display = ['progressivo', 'anno', 'stato', 'sede', 'data_inizio', 'data_esame', ] - list_filter = ['anno', 'creazione', 'stato', 'data_inizio', ] + list_display = ['progressivo', 'tipo', 'stato', 'anno', 'sede', + 'data_inizio', 'data_esame', 'delibera_file_col',] + list_filter = ['tipo', 'anno', 'creazione', 'stato', 'data_inizio', ] raw_id_fields = RAW_ID_FIELDS_CORSOBASE - inlines = [InlineDelegaCorsoBase, InlinePartecipazioneCorsoBase, InlineInvitoCorsoBase, InlineLezioneCorsoBase] + inlines = [InlineDelegaCorsoBase, InlinePartecipazioneCorsoBase, + InlineInvitoCorsoBase, InlineLezioneCorsoBase, InlineEstensioneCorso] actions = [admin_corsi_base_attivi_invia_messaggi] + def delibera_file_col(self, obj): + from django.utils.html import format_html + + file = obj.delibera_file + return format_html("""Delibera""" % file) if file else '' + delibera_file_col.short_description = 'Delibera' + + +@admin.register(InvitoCorsoBase) +class AdminInvitoCorsoBase(admin.ModelAdmin): + list_display = ['persona', 'corso', 'invitante',] + list_filter = ['ritirata', 'confermata', 'automatica'] + + +@admin.register(CorsoFile) +class AdminCorsoFile(admin.ModelAdmin): + list_display = ['__str__', 'file', 'is_enabled', 'corso',] + list_filter = ['is_enabled',] + raw_id_fields = ('corso',) + + +@admin.register(CorsoLink) +class AdminCorsoLink(admin.ModelAdmin): + list_display = ['link', 'is_enabled', 'corso',] + list_filter = ['is_enabled', ] + raw_id_fields = ('corso',) + + +@admin.register(CorsoEstensione) +class AdminCorsoEstensione(admin.ModelAdmin): + list_display = ['corso', 'is_active', 'segmento', 'sedi_sottostanti'] + raw_id_fields = RAW_ID_FIELDS_ESTENSIONE + @admin.register(PartecipazioneCorsoBase) class AdminPartecipazioneCorsoBase(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ['persona__nome', 'persona__cognome', 'persona__codice_fiscale', 'corso__progressivo', ] list_display = ['persona', 'corso', 'esito', 'creazione', ] + list_filter = ['confermata',] raw_id_fields = RAW_ID_FIELDS_PARTECIPAZIONECORSOBASE inlines = [InlineAutorizzazione] + ordering = ['-creazione',] @admin.register(LezioneCorsoBase) @@ -88,7 +131,9 @@ class AdminLezioneCorsoBase(ReadonlyAdminMixin, admin.ModelAdmin): class AdminAssenzaCorsoBase(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ['persona__nome', 'persona__cognome', 'persona__codice_fiscale', 'lezione__corso__progressivo', 'lezione__corso__sede__nome'] - list_display = ['persona', 'lezione', 'creazione', ] + list_display = ['persona', 'lezione', 'creazione', 'esonero', + 'esonero_motivazione',] + list_filter = ['esonero',] raw_id_fields = RAW_ID_FIELDS_ASSENZACORSOBASE @@ -97,9 +142,16 @@ def ricalcola_raggio(modeladmin, request, queryset): a.calcola_raggio() ricalcola_raggio.short_description = "Ricalcola il raggio per gli aspiranti selezionati" + @admin.register(Aspirante) class AdminAspirante(ReadonlyAdminMixin, admin.ModelAdmin): search_fields = ['persona__nome', 'persona__cognome', 'persona__codice_fiscale'] list_display = ['persona', 'creazione', ] raw_id_fields = RAW_ID_FIELDS_ASPIRANTE actions = [ricalcola_raggio,] + + +@admin.register(RelazioneCorso) +class AdminRelazioneCorso(ReadonlyAdminMixin, admin.ModelAdmin): + list_display = ['corso', 'is_completed',] + raw_id_fields = ['corso',] diff --git a/formazione/autocomplete_light_registry.py b/formazione/autocomplete_light_registry.py new file mode 100644 index 000000000..9bb92de65 --- /dev/null +++ b/formazione/autocomplete_light_registry.py @@ -0,0 +1,92 @@ +from django.db.models import Q + +from autocomplete_light import shortcuts as autocomplete_light + +from anagrafica.autocomplete_light_registry import AutocompletamentoBase +from anagrafica.models import Persona, Appartenenza, Sede +from curriculum.models import Titolo + + +class AutocompletamentoBasePersonaModelMixin(AutocompletamentoBase): + choice_html_format = """%s %s""" + + def choice_html(self, choice): + if choice.appartenenze_attuali(membro=Appartenenza.VOLONTARIO).exists(): + app = choice.appartenenze_attuali(membro=Appartenenza.VOLONTARIO).first() + else: + app = choice.appartenenze_attuali().first() if choice else None + + # Example: Charles Campbell (Volontario del Comitato 1 sotto Metropolitano) + data_value = self.choice_value(choice) + full_name = self.choice_label(choice) + membership = ("(%s del %s)" % (app.get_membro_display(), app.sede)) if app else '' + + return self.choice_html_format % (data_value, full_name, membership) + + +class DocenteLezioniCorso(AutocompletamentoBase): + search_fields = ['nome', 'cognome', 'codice_fiscale',] + model = Persona + choice_html_format = """%s %s""" + + def choices_for_request(self): + app_attuali = Appartenenza.query_attuale(membro__in=Appartenenza.MEMBRO_CORSO) + app_attuali = app_attuali.values_list('persona__id', flat=True) + self.choices = self.choices.filter(id__in=app_attuali) + return super().choices_for_request() + + def choice_html(self, choice): + app = choice.appartenenze_attuali().first() if choice else None + return self.choice_html_format % ( + self.choice_value(choice), + self.choice_label(choice), + ("(%s del %s)" % (app.get_membro_display(), app.sede)) if app else '', + ) + + +class EstensioneLivelloRegionaleTitolo(AutocompletamentoBase): + search_fields = ['nome',] + model = Titolo + + def choices_for_request(self): + self.choices = self.choices.filter(tipo=Titolo.TITOLO_CRI) + return super().choices_for_request() + + +class EstensioneLivelloRegionaleSede(AutocompletamentoBase): + search_fields = ['nome',] + model = Sede + + +class InvitaCorsoNuovoAutocompletamento(AutocompletamentoBasePersonaModelMixin): + model = Persona + search_fields = ['codice_fiscale',] + attrs = { + 'required': False, + 'placeholder': 'Inserisci il codice fiscale', + 'data-autocomplete-minimum-characters': 16, + } + + def choices_for_request(self): + self.choices = self.choices.filter(Q(Appartenenza.query_attuale( + membro=Appartenenza.VOLONTARIO).via("appartenenze") + )) + return super().choices_for_request() + + +class CreateDirettoreDelegaAutocompletamento(AutocompletamentoBasePersonaModelMixin): + model = Persona + search_fields = ['nome', 'cognome', 'codice_fiscale',] + + def choices_for_request(self): + self.choices = self.choices.filter(Q(Appartenenza.query_attuale( + membro__in=Appartenenza.MEMBRO_CORSO).via("appartenenze") + )).distinct('nome', 'cognome', 'codice_fiscale') + return super().choices_for_request() + + +autocomplete_light.register(EstensioneLivelloRegionaleTitolo) +autocomplete_light.register(EstensioneLivelloRegionaleSede) +autocomplete_light.register(DocenteLezioniCorso) +autocomplete_light.register(InvitaCorsoNuovoAutocompletamento) +autocomplete_light.register(CreateDirettoreDelegaAutocompletamento) diff --git a/formazione/classes.py b/formazione/classes.py new file mode 100644 index 000000000..55280bdd8 --- /dev/null +++ b/formazione/classes.py @@ -0,0 +1,142 @@ +from django.shortcuts import redirect, HttpResponse +from django.core.urlresolvers import reverse +from django.contrib import messages + +from base.files import Zip +from .models import AssenzaCorsoBase + + +class GestionePresenza: + def __init__(self, request, lezione, me, partecipanti): + self.request = request + self.lezione = lezione + self.me = me + + self._verifica_presenze(partecipanti) + + @property + def presenze_lezione(self): + return self.request.POST.getlist('presenze-%s' % self.lezione.pk) + + def get_esonero(self): + key = 'esonero-%s-%s' % (self.lezione.pk, self.partecipante.pk) + return self.request.POST.get(key) + + @property + def esonero_checkbox(self): + key = 'esonero-checkbox-%s-%s' % (self.lezione.pk, self.partecipante.pk) + return self.request.POST.get(key) + + def _process_presenza(self): + if not self.esonero or self.esonero_checkbox != 'on': + # Se non è esonero - elimina oggetti (è presente) + q = AssenzaCorsoBase.objects.filter(lezione=self.lezione, + persona=self.partecipante) + q.delete() + self.esonero = None + + def _process_assenza(self): + # Assenza "ESONERO" ha un valore, ma la persona è segnata come assente + if self.esonero: + # Trova assenze che possono avere "esonero" + assenza = AssenzaCorsoBase.objects.filter(lezione=self.lezione, + persona=self.partecipante) + if assenza.exists(): + # Rimuovi "esonero" e fai oggetto come semplice assenza + assenza.update(esonero=False, esonero_motivazione=None) + # Assenza (senza esonero) + else: + assenza = AssenzaCorsoBase.create_assenza(self.lezione, + self.partecipante, + self.me) + + # Per non procedere alla lavorazione dell'esonero (il codice sotto) + self.esonero = None + + def _process_esonero(self): + assenza = AssenzaCorsoBase.create_assenza(self.lezione, + self.partecipante, self.me, esonero=self.esonero) + return assenza + + def _verifica_presenze(self, partecipanti): + for partecipante in partecipanti: + self.partecipante = partecipante + self.esonero = self.get_esonero() + + if "%s" % partecipante.pk in self.presenze_lezione: + self._process_presenza() + else: + self._process_assenza() + + if self.esonero: # Campo "Esonero" Valorizzato (impostata una motivazione) + self._process_esonero() + + +class GeneraReport: + ATTESTATO_FILENAME = "%s - Attestato.pdf" + SCHEDA_FILENAME = "%s - Scheda di Valutazione.pdf" + + def __init__(self, request, corso, single_attestato=False): + self.request = request + self.corso = corso + + def download(self): + """ Returns a HTTP response """ + + self.archive = Zip(oggetto=self.corso) + + if self.request.GET.get('download_single_attestato'): + return self._download_single_attestato() + else: + self._generate() + self.archive.comprimi_e_salva(nome="Corso %d-%d.zip" % (self.corso.progressivo, + self.corso.anno)) + return redirect(self.archive.download_url) + + def _download_single_attestato(self): + from .models import PartecipazioneCorsoBase + from curriculum.models import Titolo + + try: + partecipazione = self.corso.partecipazioni_confermate().get( + titolo_ottenuto__pk=self.request.GET.get('download_single_attestato'), + persona=self.request.user.persona) + except PartecipazioneCorsoBase.DoesNotExist: + messages.error(self.request, "Questo attestato non esiste.") + return redirect(reverse('utente:cv_tipo', args=[Titolo.TITOLO_CRI,])) + + attestato = self._attestato(partecipazione) + filename = self.ATTESTATO_FILENAME % partecipazione.titolo_ottenuto.last() + + with open(attestato.file.path, 'rb') as f: + pdf = f.read() + + response = HttpResponse(pdf, content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename=%s' % '-'.join(filename.split()) + response.write(pdf) + return response + + def _generate(self): + for partecipante in self.corso.partecipazioni_confermate(): + self._schede(partecipante) + + if partecipante.idoneo: # Se idoneo, genera l'attestato + self._attestato(partecipante) + + def _schede(self, partecipante): + """ Genera la scheda di valutazione """ + + scheda = partecipante.genera_scheda_valutazione() + self.archive.aggiungi_file( + scheda.file.path, + self.SCHEDA_FILENAME % partecipante.persona.nome_completo + ) + return scheda + + def _attestato(self, partecipante): + attestato = partecipante.genera_attestato() + self.archive.aggiungi_file( + attestato.file.path, + self.ATTESTATO_FILENAME % partecipante.persona.nome_completo + ) + return attestato diff --git a/formazione/constants.py b/formazione/constants.py new file mode 100644 index 000000000..d5f2294c8 --- /dev/null +++ b/formazione/constants.py @@ -0,0 +1,10 @@ +from anagrafica.permessi import applicazioni + + +# Queste sono le principali tipologie di deleghe di persone che hanno +# accesso/potere sui corsi della formazione +FORMAZIONE_ROLES = [applicazioni.PRESIDENTE, # Può creare + applicazioni.COMMISSARIO, # Può creare + applicazioni.DIRETTORE_CORSO, # Accesso solo al corso delegato + applicazioni.RESPONSABILE_FORMAZIONE, # Può creare +] diff --git a/formazione/decorators.py b/formazione/decorators.py new file mode 100644 index 000000000..286992266 --- /dev/null +++ b/formazione/decorators.py @@ -0,0 +1,55 @@ +from django.shortcuts import redirect +from .models import Corso +from anagrafica.permessi.costanti import ERRORE_PERMESSI + + +def can_access_to_course(function): + def wrapper(request, *args, **kwargs): + me = request.me + proceed = True + REDIRECT_ERR = redirect(ERRORE_PERMESSI) + + if me.dipendente: + pass + elif not hasattr(me, 'ha_aspirante'): + proceed = False + elif not me.ha_aspirante and not me.volontario: + proceed = False + + if not proceed: + return REDIRECT_ERR + + r = function(request, *args, **kwargs) + try: + context = r[1] # response context stored in the view + except IndexError: + return r + except AttributeError: + if r and hasattr(r, 'status_code'): + if r.status_code == 302: + return redirect(r.url) + else: + if not context: + return r + + is_aspirante = me.ha_aspirante + is_volontario = me.volontario + + # viste.aspirante_corso_base_informazioni + if 'corso' in context: + corso = context['corso'] + if corso.tipo == Corso.CORSO_NUOVO: + # Aspirante can't access to CORSO_NUOVO page. + if is_aspirante and not is_volontario: + return REDIRECT_ERR + + # viste.aspirante_corsi + if 'corsi' in context: + if is_aspirante and not is_volontario: + # Update corsi queryset + context['corsi'] = me.aspirante.corsi(tipo=Corso.BASE) + return r + + wrapper.__doc__ = function.__doc__ + wrapper.__name__ = function.__name__ + return wrapper diff --git a/formazione/forms.py b/formazione/forms.py index 2bdd73421..a0ea488c6 100644 --- a/formazione/forms.py +++ b/formazione/forms.py @@ -1,84 +1,332 @@ -from autocomplete_light import shortcuts as autocomplete_light from django import forms from django.core.exceptions import ValidationError -from django.forms import ModelForm +from django.forms import ModelForm, modelformset_factory +from autocomplete_light import shortcuts as autocomplete_light from base.wysiwyg import WYSIWYGSemplice -from formazione.models import CorsoBase, LezioneCorsoBase, PartecipazioneCorsoBase +from anagrafica.permessi import applicazioni as permessi +from anagrafica.costanti import LOCALE, REGIONALE, NAZIONALE +from anagrafica.models import Delega, Persona +from curriculum.models import Titolo +from curriculum.areas import OBBIETTIVI_STRATEGICI +from .models import (Corso, CorsoBase, CorsoLink, CorsoFile, CorsoEstensione, + LezioneCorsoBase, PartecipazioneCorsoBase, RelazioneCorso) -class ModuloCreazioneCorsoBase(ModelForm): - class Meta: - model = CorsoBase - fields = ['data_inizio', 'sede',] +class ModuloCreazioneCorsoBase(ModelForm): PRESSO_SEDE = "PS" ALTROVE = "AL" LOCAZIONE = ( - (PRESSO_SEDE, "Il corso si svolgerà presso la Sede."), - (ALTROVE, "Il corso si svolgerà altrove (specifica dopo).") + (PRESSO_SEDE, "Il corso si svolgerà presso la Sede"), + (ALTROVE, "Il corso si svolgerà altrove") ) - locazione = forms.ChoiceField(choices=LOCAZIONE, initial=PRESSO_SEDE, - help_text="La posizione del Corso è importante per " - "aiutare gli aspiranti a trovare i Corsi " - "che si svolgono vicino a loro.") + DEFAULT_BLANK_LEVEL = ('', '---------'), + LEVELS_CHOICES = () + + level = forms.ChoiceField(choices=LEVELS_CHOICES, label='Livello', required=False) + area = forms.ChoiceField(choices=OBBIETTIVI_STRATEGICI, label='Settore di riferimento', required=False) + locazione = forms.ChoiceField(choices=LOCAZIONE, initial=PRESSO_SEDE, label='Sede del Corso',) + titolo_cri = forms.ChoiceField(label='Titolo del Corso', required=False) + + def clean_tipo(self): + tipo = self.cleaned_data['tipo'] + if not tipo: + raise ValidationError('Seleziona un valore.') + return tipo def clean_sede(self): - if self.cleaned_data['sede'].locazione is None: - raise forms.ValidationError("La Sede CRI selezionata non ha alcun indirizzo impostato. " - "Il Presidente può modificare i dettagli della Sede, tra cui l'indirizzo della stessa.") - return self.cleaned_data['sede'] + sede = self.cleaned_data['sede'] + if sede.locazione is None: + raise forms.ValidationError( + "La Sede CRI selezionata non ha alcun indirizzo impostato. " + "Il Presidente può modificare i dettagli della Sede, " + "tra cui l'indirizzo della stessa.") + return sede + + def clean_delibera_file(self): + cd = self.cleaned_data['delibera_file'] + return cd + def clean(self): + cd = self.cleaned_data + tipo = cd['tipo'] + + if cd['data_esame'] < cd['data_inizio']: + self.add_error('data_esame', "La data deve essere successiva " + "alla data di inizio.") + + if tipo == Corso.BASE: + # cd non può/deve avere valori per questi campi se corso è BASE + for field in ['area', 'level', 'titolo_cri']: + if field in cd: + del cd[field] + + if tipo == Corso.CORSO_NUOVO: + cd_titolo_cri = cd.get('titolo_cri') + + if not cd_titolo_cri: + self.add_error('titolo_cri', + "Seleziona un titolo per il Corso (l'elenco dei titoli " + "si genera sulla base dell'area selezionata)") + else: + cd['titolo_cri'] = Titolo.objects.get(id=cd_titolo_cri) + + if not cd['area']: + self.add_error('area', 'Seleziona area del Corso') + if not cd['level']: + self.add_error('level', 'Seleziona livello del Corso') + + return cd -class ModuloModificaLezione(ModelForm): class Meta: - model = LezioneCorsoBase - fields = ['nome', 'inizio', 'fine'] + model = CorsoBase + fields = ['tipo', 'level', 'titolo_cri', 'data_inizio', 'data_esame', + 'delibera_file', 'sede',] + help_texts = { + 'sede': 'Inserire il Comitato CRI che organizza il Corso', + } + labels = { + 'sede': 'Comitato CRI', + } + + def __init__(self, *args, **kwargs): + from curriculum.models import Titolo + + me = kwargs.pop('me') + super().__init__(*args, **kwargs) + self.order_fields(('tipo', 'level', 'area', 'titolo_cri', 'data_inizio', + 'data_esame', 'delibera_file', 'sede', 'locazione')) + + # GAIA-16 + delega = me.deleghe_attuali().filter(tipo__in=[permessi.COMMISSARIO, + permessi.PRESIDENTE, + permessi.RESPONSABILE_FORMAZIONE]).last() + if delega: + estensione_sede = delega.sede.all().first().estensione + + levels = 2 + if estensione_sede == LOCALE: + pass + elif estensione_sede == REGIONALE: + levels = 3 + elif estensione_sede == NAZIONALE: + levels = 4 + + self.fields['level'].choices = Titolo.CDF_LIVELLI[:levels] + self.fields['area'].choices = list(self.DEFAULT_BLANK_LEVEL) + self.fields['area'].choices + self.fields['titolo_cri'].choices = list(self.DEFAULT_BLANK_LEVEL) + [ + (choice.pk, choice) for choice in Titolo.objects.filter( + area__isnull=False, + nome__isnull=False, + tipo=Titolo.TITOLO_CRI, + is_active=True, + ).order_by('nome')] + + # Sort area options 'ASC' + self.fields['area'].choices = sorted(self.fields['area'].choices, key=lambda x: x[1]) + + +class ModuloModificaLezione(ModelForm): + docente = autocomplete_light.ModelChoiceField("DocenteLezioniCorso") fine = forms.DateTimeField() + obiettivo = forms.CharField(required=False) def clean(self): + cd = self.cleaned_data try: - fine = self.cleaned_data['fine'] - inizio = self.cleaned_data['inizio'] - + inizio = cd['inizio'] + fine = cd['fine'] except KeyError: raise ValidationError("Compila correttamente tutti i campi.") if inizio >= fine: self.add_error('fine', "La fine deve essere successiva all'inizio.") + data_inizio_corso = self.corso.data_inizio + err_data_lt_inizio_corso = 'Data precedente alla data di inizo corso.' + if inizio < data_inizio_corso: + self.add_error('inizio', err_data_lt_inizio_corso) + if fine < data_inizio_corso: + self.add_error('fine', err_data_lt_inizio_corso) + + return cd + + class Meta: + model = LezioneCorsoBase + fields = ['nome', 'docente', 'inizio', 'fine', 'obiettivo', 'luogo'] + labels = { + 'nome': 'Lezione', + 'obiettivo': 'Argomento', + } + + def __init__(self, *args, **kwargs): + self.corso = kwargs.pop('corso') + super().__init__(*args, **kwargs) + class ModuloModificaCorsoBase(ModelForm): class Meta: model = CorsoBase - fields = ['data_inizio', 'data_esame', 'descrizione', - 'data_attivazione', 'data_convocazione', - 'op_attivazione', 'op_convocazione',] + fields = ['data_inizio', 'data_esame', + 'min_participants', 'max_participants', + 'descrizione',] widgets = { "descrizione": WYSIWYGSemplice(), } + def clean(self): + cd = self.cleaned_data + if 'min_participants' in cd and 'max_participants' in cd: + min, max = cd['min_participants'], cd['max_participants'] + if min > max: + self.add_error('min_participants', "Numero minimo di " + "partecipanti non può essere maggiore del numero massimo.") + if max < min: + self.add_error('max_participants', "Numero massimo di " + "partecipanti non può essere minore del numero minimo.") + return cd + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + instance = self.instance + is_nuovo = instance.tipo == Corso.CORSO_NUOVO + if not is_nuovo: + self.fields.pop('min_participants') + self.fields.pop('max_participants') + + # The fields are commented because the field's logic was moved to ModuloCreazioneCorsoBase forms step + # self.fields.pop('titolo_cri') + # if is_nuovo and instance.stato == Corso.ATTIVO: + # self.fields['titolo_cri'].widget.attrs['disabled'] = 'disabled' + + +class CorsoLinkForm(ModelForm): + class Meta: + model = CorsoFile + fields = ['file',] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + acceptable_extensions = [ + 'application/pdf', + 'application/msword', # .doc + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', # xls + 'application/vnd.ms-powerpoint', + 'application/x-rar-compressed', 'application/octet-stream', # rar + 'application/zip', 'application/octet-stream', + 'application/x-zip-compressed', 'multipart/x-zip', + 'image/jpeg', + 'image/png', + 'text/csv', + 'application/rtf', + ] + self.fields['file'].widget.attrs = {'accept': ", ".join( + acceptable_extensions)} + class ModuloIscrittiCorsoBaseAggiungi(forms.Form): - persone = autocomplete_light.ModelMultipleChoiceField("IscrivibiliCorsiAutocompletamento", - help_text="Ricerca per Codice Fiscale " - "i Sostenitori o gli Aspiranti " - "CRI da iscrivere a questo Corso Base.") + persone = autocomplete_light.ModelMultipleChoiceField( + "IscrivibiliCorsiAutocompletamento", + help_text="Ricerca per Codice Fiscale i Sostenitori o gli Aspiranti CRI " + "da iscrivere a questo Corso.") + + def __init__(self, *args, **kwargs): + self.corso = kwargs.pop('corso') + super().__init__(*args, **kwargs) + + # Change dynamically autocomplete class, depending on Corso tipo + if self.corso.is_nuovo_corso and 'persone' in self.fields: + self.fields['persone'].widget.autocomplete_arg = 'InvitaCorsoNuovoAutocompletamento' + + +class CorsoSelectExtensionTypeForm(ModelForm): + class Meta: + model = CorsoBase + fields = ['extension_type',] + labels = { + 'extension_type': "Tipo dell'estensione", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['extension_type'].choices = CorsoBase.EXTENSION_TYPE_CHOICES + + if not self.instance.get_extensions(is_active=True).count(): + # Useful to set EXT_MIA_SEDE as select's default value to avoid + # possible issues with ExtensionFormSet + self.initial['extension_type'] = CorsoBase.EXT_MIA_SEDE + + +class CorsoExtensionForm(ModelForm): + titolo = autocomplete_light.ModelMultipleChoiceField( + "EstensioneLivelloRegionaleTitolo", required=False, label='Requisiti necessari') + sede = autocomplete_light.ModelMultipleChoiceField( + "EstensioneLivelloRegionaleSede", required=False, label='Selezionare Sede/Sedi') + + class Meta: + model = CorsoEstensione + fields = ['segmento', 'titolo', 'sede', 'sedi_sottostanti',] + labels = { + 'segmento': "Destinatari del Corso", + } + + def clean(self): + cd = self.cleaned_data + + if self.corso: + if self.corso.titolo_cri in cd['titolo']: + self.add_error('titolo', "Non è possibile selezionare lo stesso titolo del Corso.") + + return cd + + def __init__(self, *args, **kwargs): + self.corso = kwargs.pop('corso') + super().__init__(*args, **kwargs) + + # if self.corso.is_nuovo_corso and self.corso.titolo_cri: + # self.fields['titolo'].initial = Titolo.objects.filter(id__in=[ + # self.corso.titolo_cri.pk]) + + +class ModuloConfermaIscrizioneCorso(forms.Form): + IS_CORSO_NUOVO = True class ModuloConfermaIscrizioneCorsoBase(forms.Form): - conferma_1 = forms.BooleanField(label="Ho incontrato questo aspirante, ad esempio alla presentazione del " - "corso, e mi ha chiesto di essere iscritto al Corso.") - conferma_2 = forms.BooleanField(label="Confermo di voler iscrivere questo aspirante al Corso e comprendo che " - "questa azione non sarà facilmente reversibile. Sarà comunque possibile " - "non ammettere l'aspirante all'esame, qualora dovesse non presentarsi " - "al resto delle lezioni (questo sarà verbalizzato).") + conferma_1 = forms.BooleanField( + label="Ho incontrato questo aspirante, ad esempio alla presentazione del corso, e mi ha chiesto di essere iscritto al Corso.") + conferma_2 = forms.BooleanField( + label="Confermo di voler iscrivere questo aspirante al Corso e comprendo che " + "questa azione non sarà facilmente reversibile. Sarà comunque possibile " + "non ammettere l'aspirante all'esame, qualora dovesse non presentarsi " + "al resto delle lezioni (questo sarà verbalizzato).") -class ModuloVerbaleAspiranteCorsoBase(ModelForm): +class FormRelazioneDelDirettoreCorso(ModelForm): + class Meta: + model = RelazioneCorso + exclude = ['corso', 'creazione', 'ultima_modifica',] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + is_stato_terminato = self.instance.corso.stato == CorsoBase.TERMINATO + + for f in self.fields: + self.fields[f].label = self.fields[f].help_text + self.fields[f].widget.attrs['placeholder'] = '' + self.fields[f].help_text = '' + + if is_stato_terminato: + self.fields[f].widget.attrs['disabled'] = 'disabled' + + +class ModuloVerbaleAspiranteCorsoBase(ModelForm): GENERA_VERBALE = 'genera_verbale' SALVA_SOLAMENTE = 'salva' @@ -94,7 +342,11 @@ class Meta: def __init__(self, *args, generazione_verbale=False, **kwargs): self.generazione_verbale = generazione_verbale - super(ModuloVerbaleAspiranteCorsoBase, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + + if self.instance.corso.is_nuovo_corso: + # This field is not required if Corso Nuovo + self.fields.pop('destinazione') def clean(self): """ @@ -102,15 +354,17 @@ def clean(self): del verbale del corso base. """ - ammissione = self.cleaned_data['ammissione'] - motivo_non_ammissione = self.cleaned_data['motivo_non_ammissione'] - esito_parte_1 = self.cleaned_data['esito_parte_1'] - argomento_parte_1 = self.cleaned_data['argomento_parte_1'] - esito_parte_2 = self.cleaned_data['esito_parte_2'] - argomento_parte_2 = self.cleaned_data['argomento_parte_2'] - extra_1 = self.cleaned_data['extra_1'] - extra_2 = self.cleaned_data['extra_2'] - destinazione = self.cleaned_data['destinazione'] + cd = super().clean() + + ammissione = cd['ammissione'] + motivo_non_ammissione = cd['motivo_non_ammissione'] + esito_parte_1 = cd['esito_parte_1'] + argomento_parte_1 = cd['argomento_parte_1'] + esito_parte_2 = cd['esito_parte_2'] + argomento_parte_2 = cd['argomento_parte_2'] + extra_1 = cd['extra_1'] + extra_2 = cd['extra_2'] + destinazione = cd.get('destinazione') # Controlla che non ci siano conflitti (incoerenze) nei dati. if ammissione != PartecipazioneCorsoBase.NON_AMMESSO: @@ -151,12 +405,51 @@ def clean(self): self.add_error('motivo_non_ammissione', "Devi specificare la motivazione di non " "ammissione all'esame.") - # Se sto generando il verbale, controlla che tutti i campi - # obbligatori siano stati riempiti. - if self.generazione_verbale: - if not destinazione: + # Se sto generando il verbale, controlla che tutti i campi obbligatori siano stati riempiti. + if self.generazione_verbale: + if not destinazione and not self.instance.corso.is_nuovo_corso: self.add_error('destinazione', - "È necessario selezionare la Sede presso la quale il Volontario " - "diventerà Volontario (nel solo caso di superamento dell'esame).") + "È necessario selezionare la Sede presso la quale il Volontario " + "diventerà Volontario (nel solo caso di superamento dell'esame)." + ) + + +class FormCreateDirettoreDelega(ModelForm): + persona = autocomplete_light.ModelChoiceField('CreateDirettoreDelegaAutocompletamento') + + class Meta: + model = Delega + fields = ['persona',] + + def __init__(self, *args, **kwargs): + # These attrs are passed in anagrafica.viste.strumenti_delegati() + for attr in ['me', 'course']: + if attr in kwargs: + setattr(self, attr, kwargs.pop(attr)) + super().__init__(*args, **kwargs) + + +class InformCourseParticipantsForm(forms.Form): + ALL = '1' + UNCONFIRMED_REQUESTS = '2' + CONFIRMED_REQUESTS = '3' + INVIA_QUESTIONARIO = '4' + + message = forms.CharField(label='Messaggio', required=True, max_length=3000) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance') + + CHOICES = [ + (self.ALL, "A tutti (Preiscritti + Partecipanti confermati)"), + (self.UNCONFIRMED_REQUESTS, "Preiscritti"), + (self.CONFIRMED_REQUESTS, "Partecipanti confermati (ok così come è ora)"), + ] + + if self.instance.concluso: + CHOICES.append((self.INVIA_QUESTIONARIO, + "Invia questionario di gradimento ai partecipanti")) + super().__init__(*args, **kwargs) + self.fields['recipient_type'] = forms.ChoiceField(choices=CHOICES, label='Destinatari') diff --git a/formazione/formsets.py b/formazione/formsets.py new file mode 100644 index 000000000..4ad4d5362 --- /dev/null +++ b/formazione/formsets.py @@ -0,0 +1,14 @@ +from django.forms import modelformset_factory + +from .forms import CorsoLinkForm, CorsoExtensionForm +from .models import CorsoFile, CorsoLink, CorsoEstensione + + +CorsoFileFormSet = modelformset_factory(CorsoFile, + form=CorsoLinkForm, extra=1, max_num=4) + +CorsoLinkFormSet = modelformset_factory(CorsoLink, + fields=('link',), extra=1, max_num=2) + +CorsoSelectExtensionFormSet = modelformset_factory(CorsoEstensione, + extra=1, max_num=3, form=CorsoExtensionForm, can_delete=True) diff --git a/formazione/menus.py b/formazione/menus.py new file mode 100644 index 000000000..8984d0a17 --- /dev/null +++ b/formazione/menus.py @@ -0,0 +1,61 @@ +from django.core.urlresolvers import reverse + +from anagrafica.permessi.costanti import GESTIONE_CORSI_SEDE, ELENCHI_SOCI, RUBRICA_DELEGATI_OBIETTIVO_ALL + + +def to_show(me, permissions): + if not me: + return False + + if isinstance(permissions, list or tuple): + for permission in permissions: + if me.ha_permesso(permission): + return True + else: + if me.ha_permesso(permissions): + return True + return False + + +def formazione_menu(menu_name, me=None): + FORMAZIONE = ( + ("Corsi", ( + ("Attiva Corso", "fa-asterisk", reverse('formazione:new_course')) if to_show(me, GESTIONE_CORSI_SEDE) else None, + # ("Elenco Corsi", "fa-list", reverse('formazione:list_courses')), + ("Domanda formativa", "fa-area-chart", reverse('formazione:domanda')) if to_show(me, GESTIONE_CORSI_SEDE) else None, + ('Catalogo Corsi', 'fa-list-alt', '/page/catalogo-corsi/'), + ('Glossario Corsi', 'fa-book', '/page/glossario-corsi/'), + ('Albo Informatizzato', 'fa-list', reverse( + 'formazione:albo_info')) if to_show(me, RUBRICA_DELEGATI_OBIETTIVO_ALL + [GESTIONE_CORSI_SEDE]) else None, + )), + ) + + ASPIRANTE = ( + ("Aspirante", ( + ("Home page", "fa-home", reverse('aspirante:home')), + ("Anagrafica", "fa-edit", reverse('utente:anagrafica')), + ("Storico", "fa-clock-o", reverse('utente:storico')), + ("Contatti", "fa-envelope", reverse('utente:contatti')), + ("Fotografie", "fa-credit-card", reverse('utente:foto')), + ("Competenze personali", "fa-suitcase", reverse('utente:cv_tipo', args=['CP'])), + ("Patenti Civili", "fa-car", reverse('utente:cv_tipo', args=['PP'])), + ("Titoli di Studio", "fa-graduation-cap", reverse('utente:cv_tipo', args=['TS'])), + )), + + ("Nelle vicinanze", ( + ("Corsi", "fa-list", reverse('aspirante:corsi_base')), + ("Sedi CRI", "fa-list", reverse('aspirante:sedi')), + ("Impostazioni", "fa-gears", reverse('aspirante:settings')), + )), + + ("Sicurezza", ( + ("Cambia password", "fa-key", reverse('utente:change_password')), + ("Impostazioni Privacy", "fa-cogs", reverse('utente:privacy')), + )), + ) + + MENUS = dict( + formazione=FORMAZIONE, + aspirante=ASPIRANTE, + ) + return MENUS[menu_name] diff --git a/formazione/migrations/0019_corsobase_tipo.py b/formazione/migrations/0019_corsobase_tipo.py new file mode 100644 index 000000000..9407369a7 --- /dev/null +++ b/formazione/migrations/0019_corsobase_tipo.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-05 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0018_auto_20180408_1952'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='tipo', + field=models.CharField(blank=True, choices=[('C1', 'Corso'), ('BA', 'Corso Base')], max_length=4, verbose_name='Tipo'), + ), + ] diff --git a/formazione/migrations/0020_auto_20181107_1204.py b/formazione/migrations/0020_auto_20181107_1204.py new file mode 100644 index 000000000..2d6a6e991 --- /dev/null +++ b/formazione/migrations/0020_auto_20181107_1204.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-07 12:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0019_corsobase_tipo'), + ] + + operations = [ + migrations.AlterField( + model_name='corsobase', + name='op_attivazione', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Ordinanza presidenziale attivazione'), + ), + migrations.AlterField( + model_name='corsobase', + name='op_convocazione', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Ordinanza presidenziale convocazione'), + ), + ] diff --git a/formazione/migrations/0021_corsofile_corsolink.py b/formazione/migrations/0021_corsofile_corsolink.py new file mode 100644 index 000000000..d297236de --- /dev/null +++ b/formazione/migrations/0021_corsofile_corsolink.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-09 11:21 +from __future__ import unicode_literals + +import anagrafica.validators +from django.db import migrations, models +import django.db.models.deletion +import formazione.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0020_auto_20181107_1204'), + ] + + operations = [ + migrations.CreateModel( + name='CorsoFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_enabled', models.BooleanField(default=True)), + ('file', models.FileField(blank=True, null=True, upload_to='corsi/', validators=[anagrafica.validators.valida_dimensione_file_8mb, formazione.validators.validate_file_extension], verbose_name='FIle')), + ('corso', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='formazione.CorsoBase')), + ], + ), + migrations.CreateModel( + name='CorsoLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_enabled', models.BooleanField(default=True)), + ('link', models.URLField(blank=True, null=True, verbose_name='Link')), + ('corso', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='formazione.CorsoBase')), + ], + ), + ] diff --git a/formazione/migrations/0022_auto_20181109_1639.py b/formazione/migrations/0022_auto_20181109_1639.py new file mode 100644 index 000000000..c8c5c8c45 --- /dev/null +++ b/formazione/migrations/0022_auto_20181109_1639.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-09 16:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0021_corsofile_corsolink'), + ] + + operations = [ + migrations.CreateModel( + name='FormazioneTitle', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creazione', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('ultima_modifica', models.DateTimeField(auto_now=True, db_index=True)), + ('name', models.CharField(max_length=255, verbose_name='Nome del corso')), + ], + options={ + 'verbose_name': 'Titolo', + 'verbose_name_plural': 'Titoli', + }, + ), + migrations.CreateModel( + name='FormazioneTitleGoal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unit_reference', models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza)'), ('4', 'Advocacy e mediazione umanitaria'), ('5', 'Giovani'), ('5', 'Sviluppo')], max_length=3, null=True, verbose_name='Unità riferimento')), + ('propedeuticita', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propedeuticità')), + ], + options={ + 'verbose_name': 'Titolo: Propedeuticità', + 'verbose_name_plural': 'Titoli: Propedeuticità', + }, + ), + migrations.CreateModel( + name='FormazioneTitleLevel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nome')), + ('goal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='formazione.FormazioneTitleGoal')), + ], + options={ + 'verbose_name': 'Titolo: Livello', + 'verbose_name_plural': 'Titoli: Livelli', + }, + ), + migrations.AddField( + model_name='formazionetitle', + name='livello', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='formazione.FormazioneTitleLevel', verbose_name='Livello'), + ), + ] diff --git a/formazione/migrations/0023_auto_20181109_1652.py b/formazione/migrations/0023_auto_20181109_1652.py new file mode 100644 index 000000000..a7f9e77ed --- /dev/null +++ b/formazione/migrations/0023_auto_20181109_1652.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-09 16:52 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0022_auto_20181109_1639'), + ] + + operations = [ + migrations.AlterField( + model_name='formazionetitle', + name='livello', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='formazione.FormazioneTitleLevel', verbose_name='Livello'), + ), + ] diff --git a/formazione/migrations/0024_auto_20181113_1644.py b/formazione/migrations/0024_auto_20181113_1644.py new file mode 100644 index 000000000..922b6b551 --- /dev/null +++ b/formazione/migrations/0024_auto_20181113_1644.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-13 16:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('anagrafica', '0049_auto_20181028_1639'), + ('formazione', '0023_auto_20181109_1652'), + ] + + operations = [ + migrations.CreateModel( + name='CorsoEstensione', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creazione', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('ultima_modifica', models.DateTimeField(auto_now=True, db_index=True)), + ('is_active', models.BooleanField(db_index=True, default=True)), + ('segmento', models.CharField(blank=True, choices=[('A', 'Tutti gli utenti di Gaia'), ('B', 'Volontari'), ('C', 'Volontari da meno di un anno'), ('D', 'Volontari da più di un anno'), ('E', 'Volontari con meno di 33 anni'), ('F', 'Volontari con 33 anni o più'), ('G', 'Sostenitori CRI'), ('H', 'Aspiranti volontari iscritti a un corso'), ('I', 'Tutti i Presidenti'), ('J', 'Presidenti di Comitati Locali'), ('K', 'Presidenti di Comitati Regionali'), ('L', 'Delegati US'), ('M', 'Delegati Obiettivo I'), ('N', 'Delegati Obiettivo II'), ('O', 'Delegati Obiettivo III'), ('P', 'Delegati Obiettivo IV'), ('Q', 'Delegati Obiettivo V'), ('R', 'Delegati Obiettivo VI'), ('S', 'Referenti di un’Attività di Area I'), ('T', 'Referenti di un’Attività di Area II'), ('U', 'Referenti di un’Attività di Area III'), ('V', 'Referenti di un’Attività di Area IV'), ('W', 'Referenti di un’Attività di Area V'), ('X', 'Referenti di un’Attività di Area VI'), ('Y', 'Delegati Autoparco'), ('Z', 'Delegati Formazione'), ('AA', 'Volontari aventi un dato titolo')], max_length=9)), + ('sedi_sottostanti', models.BooleanField(db_index=True, default=False)), + ], + options={ + 'verbose_name': 'Estensione del Corso', + 'verbose_name_plural': 'Estensioni del Corso', + }, + ), + migrations.AddField( + model_name='corsobase', + name='extension_type', + field=models.CharField(blank=True, choices=[('1', 'Solo su mia sede di appartenenza'), ('2', 'A livello regionale')], max_length=5, null=True), + ), + migrations.AddField( + model_name='corsoestensione', + name='corso', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='formazione.CorsoBase'), + ), + migrations.AddField( + model_name='corsoestensione', + name='sede', + field=models.ManyToManyField(to='anagrafica.Sede'), + ), + migrations.AddField( + model_name='corsoestensione', + name='titolo', + field=models.ManyToManyField(blank=True, to='formazione.FormazioneTitle'), + ), + ] diff --git a/formazione/migrations/0025_auto_20181114_1410.py b/formazione/migrations/0025_auto_20181114_1410.py new file mode 100644 index 000000000..3e59848de --- /dev/null +++ b/formazione/migrations/0025_auto_20181114_1410.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-14 14:10 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0024_auto_20181113_1644'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='max_participants', + field=models.SmallIntegerField(default=50, verbose_name='Massimo partecipanti'), + ), + migrations.AddField( + model_name='corsobase', + name='min_participants', + field=models.SmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(20)], verbose_name='Minimo partecipanti'), + ), + migrations.AlterField( + model_name='formazionetitlegoal', + name='unit_reference', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza)'), ('4', 'Advocacy e mediazione umanitaria'), ('5', 'Giovani'), ('6', 'Sviluppo')], max_length=3, null=True, verbose_name='Unità riferimento'), + ), + ] diff --git a/formazione/migrations/0026_auto_20181119_1427.py b/formazione/migrations/0026_auto_20181119_1427.py new file mode 100644 index 000000000..8dca506e5 --- /dev/null +++ b/formazione/migrations/0026_auto_20181119_1427.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-19 14:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0025_auto_20181114_1410'), + ] + + operations = [ + migrations.RemoveField( + model_name='formazionetitle', + name='livello', + ), + migrations.RemoveField( + model_name='formazionetitlelevel', + name='goal', + ), + migrations.AlterField( + model_name='corsoestensione', + name='titolo', + field=models.ManyToManyField(blank=True, to='curriculum.Titolo'), + ), + migrations.DeleteModel( + name='FormazioneTitle', + ), + migrations.DeleteModel( + name='FormazioneTitleGoal', + ), + migrations.DeleteModel( + name='FormazioneTitleLevel', + ), + ] diff --git a/formazione/migrations/0027_auto_20181120_1637.py b/formazione/migrations/0027_auto_20181120_1637.py new file mode 100644 index 000000000..56a2c5a94 --- /dev/null +++ b/formazione/migrations/0027_auto_20181120_1637.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-20 16:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0026_auto_20181119_1427'), + ] + + operations = [ + migrations.AlterField( + model_name='corsobase', + name='extension_type', + field=models.CharField(blank=True, choices=[('1', 'Solo su mia sede di appartenenza'), ('2', 'A livello regionale')], default='1', max_length=5, null=True), + ), + ] diff --git a/formazione/migrations/0028_auto_20181122_1700.py b/formazione/migrations/0028_auto_20181122_1700.py new file mode 100644 index 000000000..dc835c6b8 --- /dev/null +++ b/formazione/migrations/0028_auto_20181122_1700.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-22 17:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('anagrafica', '0049_auto_20181028_1639'), + ('formazione', '0027_auto_20181120_1637'), + ] + + operations = [ + migrations.AddField( + model_name='lezionecorsobase', + name='docente', + field=models.ForeignKey(default='', null=True, on_delete=django.db.models.deletion.CASCADE, to='anagrafica.Persona', verbose_name='Docente della lezione'), + ), + migrations.AddField( + model_name='lezionecorsobase', + name='obiettivo', + field=models.CharField(default='', max_length=128, null=True, verbose_name='Obiettivo formativo della lezione'), + ), + ] diff --git a/formazione/migrations/0029_auto_20181127_1547.py b/formazione/migrations/0029_auto_20181127_1547.py new file mode 100644 index 000000000..231c297c2 --- /dev/null +++ b/formazione/migrations/0029_auto_20181127_1547.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-27 15:47 +from __future__ import unicode_literals + +import anagrafica.validators +from django.db import migrations, models +import formazione.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0028_auto_20181122_1700'), + ] + + operations = [ + migrations.AlterModelOptions( + name='assenzacorsobase', + options={'permissions': (('view_assenzacorsobase', 'Can view corso Assenza a Corso Base'),), 'verbose_name': 'Assenza a Corso', 'verbose_name_plural': 'Assenze ai Corsi'}, + ), + migrations.AlterModelOptions( + name='corsobase', + options={'ordering': ['-anno', '-progressivo'], 'permissions': (('view_corsobase', 'Can view corso base'),), 'verbose_name': 'Corso', 'verbose_name_plural': 'Corsi'}, + ), + migrations.AlterModelOptions( + name='invitocorsobase', + options={'ordering': ('persona__cognome', 'persona__nome', 'persona__codice_fiscale'), 'permissions': (('view_invitocorsobase', 'Can view invito partecipazione corso base'),), 'verbose_name': 'Invito di partecipazione a corso', 'verbose_name_plural': 'Inviti di partecipazione a corso'}, + ), + migrations.AlterModelOptions( + name='lezionecorsobase', + options={'ordering': ['inizio'], 'permissions': (('view_lezionecorsobase', 'Can view corso Lezione di Corso Base'),), 'verbose_name': 'Lezione di Corso', 'verbose_name_plural': 'Lezioni di Corsi'}, + ), + migrations.AddField( + model_name='lezionecorsobase', + name='luogo', + field=models.CharField(blank=True, help_text='Compilare nel caso il luogo è diverso dal comitato che ha organizzato il corso.', max_length=255, null=True, verbose_name='il luogo di dove si svolgeranno le lezioni'), + ), + migrations.AlterField( + model_name='corsofile', + name='file', + field=models.FileField(blank=True, null=True, upload_to=formazione.validators.course_file_directory_path, validators=[anagrafica.validators.valida_dimensione_file_8mb, formazione.validators.validate_file_extension], verbose_name='FIle'), + ), + ] diff --git a/formazione/migrations/0030_corsobase_titolo_cri.py b/formazione/migrations/0030_corsobase_titolo_cri.py new file mode 100644 index 000000000..1d78ba9dc --- /dev/null +++ b/formazione/migrations/0030_corsobase_titolo_cri.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-11-30 14:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('curriculum', '0009_auto_20181130_1550'), + ('formazione', '0029_auto_20181127_1547'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='titolo_cri', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='curriculum.Titolo', verbose_name='Titolo CRI'), + ), + ] diff --git a/formazione/migrations/0031_corsobase_survey.py b/formazione/migrations/0031_corsobase_survey.py new file mode 100644 index 000000000..623e3ece9 --- /dev/null +++ b/formazione/migrations/0031_corsobase_survey.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-12-04 15:13 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('survey', '0001_initial'), + ('formazione', '0030_corsobase_titolo_cri'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='survey', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='survey.Survey', verbose_name='Questionario di gradimento'), + ), + ] diff --git a/formazione/migrations/0032_corsobase_delibera_file.py b/formazione/migrations/0032_corsobase_delibera_file.py new file mode 100644 index 000000000..4c88287d8 --- /dev/null +++ b/formazione/migrations/0032_corsobase_delibera_file.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-01-28 13:00 +from __future__ import unicode_literals + +import anagrafica.validators +from django.db import migrations, models +import formazione.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0031_corsobase_survey'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='delibera_file', + field=models.FileField(null=True, upload_to=formazione.validators.delibera_file_upload_path, validators=[anagrafica.validators.ValidateFileSize(3), formazione.validators.validate_file_extension], verbose_name='Delibera'), + ), + ] diff --git a/formazione/migrations/0033_auto_20190206_1225.py b/formazione/migrations/0033_auto_20190206_1225.py new file mode 100644 index 000000000..9d51a0968 --- /dev/null +++ b/formazione/migrations/0033_auto_20190206_1225.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-02-06 12:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0032_corsobase_delibera_file'), + ] + + operations = [ + migrations.AddField( + model_name='corsobase', + name='cdf_area', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo'), ('7', 'Migrazioni'), ('8', 'Cooperazioni Internazionali')], max_length=3, null=True), + ), + migrations.AddField( + model_name='corsobase', + name='cdf_level', + field=models.CharField(blank=True, choices=[('1', 'I Livello'), ('2', 'II Livello'), ('3', 'III Livello'), ('4', 'IV Livello')], max_length=3, null=True), + ), + ] diff --git a/formazione/migrations/0034_auto_20190408_1047.py b/formazione/migrations/0034_auto_20190408_1047.py new file mode 100644 index 000000000..efc22d5b4 --- /dev/null +++ b/formazione/migrations/0034_auto_20190408_1047.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-04-08 10:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0033_auto_20190206_1225'), + ] + + operations = [ + migrations.AlterField( + model_name='corsobase', + name='tipo', + field=models.CharField(blank=True, choices=[('BA', 'Corso di Formazione per Volontari CRI'), ('C1', 'Altri Corsi')], max_length=4, verbose_name='Tipo'), + ), + ] diff --git a/formazione/migrations/0035_auto_20190510_1149.py b/formazione/migrations/0035_auto_20190510_1149.py new file mode 100644 index 000000000..4d2b5f284 --- /dev/null +++ b/formazione/migrations/0035_auto_20190510_1149.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-10 11:49 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0034_auto_20190408_1047'), + ] + + operations = [ + migrations.CreateModel( + name='RelazioneCorso', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creazione', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('ultima_modifica', models.DateTimeField(auto_now=True, db_index=True)), + ('note_esplicative', models.TextField(help_text='note esplicative in relazione ai cambiamenti effettuati rispetto alla programmazione approvata in fase di pianificazione iniziale del corso.', verbose_name='Note esplicative')), + ('raggiungimento_obiettivi', models.TextField(help_text="Analisi sul raggiungimento degli obiettivi del corso (generali rispetto all'evento e specifici di apprendimento).", verbose_name='Raggiungimento degli obiettivi del corso')), + ('annotazioni_corsisti', models.TextField(verbose_name='Annotazioni relative alla partecipazione dei corsisti')), + ('annotazioni_risorse', models.TextField(help_text='Annotazioni relative a risorse e competenze di particolare rilevanza emerse durante il percorso formativo')), + ('annotazioni_organizzazione_struttura', models.TextField(help_text="Annotazioni e segnalazioni sull'organizzazione e la logistica e della struttura ospitante il corso")), + ('descrizione_attivita', models.TextField(help_text='Descrizione delle eventuali attività di tirocinio/affiancamento con indicazione dei Tutor')), + ('corso', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='formazione.CorsoBase')), + ], + options={ + 'verbose_name': 'Relazione del Direttore', + 'verbose_name_plural': 'Relazioni dei Direttori', + }, + ), + ] diff --git a/formazione/migrations/0036_auto_20190510_1701.py b/formazione/migrations/0036_auto_20190510_1701.py new file mode 100644 index 000000000..0178d66a3 --- /dev/null +++ b/formazione/migrations/0036_auto_20190510_1701.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-10 17:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0035_auto_20190510_1149'), + ] + + operations = [ + migrations.AlterField( + model_name='relazionecorso', + name='annotazioni_corsisti', + field=models.TextField(help_text='Annotazioni relative alla partecipazione dei corsisti ', verbose_name='Annotazioni relative alla partecipazione dei corsisti'), + ), + migrations.AlterField( + model_name='relazionecorso', + name='corso', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='relazione_corso', to='formazione.CorsoBase'), + ), + migrations.AlterField( + model_name='relazionecorso', + name='note_esplicative', + field=models.TextField(help_text='Note esplicative in relazione ai cambiamenti effettuati rispetto alla programmazione approvata in fase di pianificazione iniziale del corso.', verbose_name='Note esplicative'), + ), + ] diff --git a/formazione/migrations/0037_auto_20190510_1752.py b/formazione/migrations/0037_auto_20190510_1752.py new file mode 100644 index 000000000..c2e25e6ad --- /dev/null +++ b/formazione/migrations/0037_auto_20190510_1752.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-10 17:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0036_auto_20190510_1701'), + ] + + operations = [ + migrations.AlterField( + model_name='relazionecorso', + name='annotazioni_corsisti', + field=models.TextField(blank=True, help_text='Annotazioni relative alla partecipazione dei corsisti ', null=True, verbose_name='Annotazioni relative alla partecipazione dei corsisti'), + ), + migrations.AlterField( + model_name='relazionecorso', + name='annotazioni_organizzazione_struttura', + field=models.TextField(blank=True, help_text="Annotazioni e segnalazioni sull'organizzazione e la logistica e della struttura ospitante il corso", null=True), + ), + migrations.AlterField( + model_name='relazionecorso', + name='annotazioni_risorse', + field=models.TextField(blank=True, help_text='Annotazioni relative a risorse e competenze di particolare rilevanza emerse durante il percorso formativo', null=True), + ), + migrations.AlterField( + model_name='relazionecorso', + name='descrizione_attivita', + field=models.TextField(blank=True, help_text='Descrizione delle eventuali attività di tirocinio/affiancamento con indicazione dei Tutor', null=True), + ), + migrations.AlterField( + model_name='relazionecorso', + name='note_esplicative', + field=models.TextField(blank=True, help_text='Note esplicative in relazione ai cambiamenti effettuati rispetto alla programmazione approvata in fase di pianificazione iniziale del corso.', null=True, verbose_name='Note esplicative'), + ), + migrations.AlterField( + model_name='relazionecorso', + name='raggiungimento_obiettivi', + field=models.TextField(blank=True, help_text="Analisi sul raggiungimento degli obiettivi del corso (generali rispetto all'evento e specifici di apprendimento).", null=True, verbose_name='Raggiungimento degli obiettivi del corso'), + ), + ] diff --git a/formazione/migrations/0038_auto_20190516_1155.py b/formazione/migrations/0038_auto_20190516_1155.py new file mode 100644 index 000000000..5bebbd1a4 --- /dev/null +++ b/formazione/migrations/0038_auto_20190516_1155.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-16 11:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0037_auto_20190510_1752'), + ] + + operations = [ + migrations.AddField( + model_name='assenzacorsobase', + name='esonero', + field=models.NullBooleanField(default=False), + ), + migrations.AddField( + model_name='assenzacorsobase', + name='esonero_motivazione', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name="Motivazione dell'esonero"), + ), + ] diff --git a/formazione/migrations/0039_auto_20190521_1512.py b/formazione/migrations/0039_auto_20190521_1512.py new file mode 100644 index 000000000..985222d3c --- /dev/null +++ b/formazione/migrations/0039_auto_20190521_1512.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-21 15:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0038_auto_20190516_1155'), + ] + + operations = [ + migrations.AlterField( + model_name='corsobase', + name='cdf_area', + field=models.CharField(blank=True, choices=[('1', 'Salute'), ('2', 'Inclusione Sociale'), ('3', 'Emergenza'), ('4', 'Principi e Valori'), ('5', 'Giovani'), ('6', 'Sviluppo Organizzativo'), ('7', 'Migrazioni'), ('8', 'Cooperazione Internazionale')], max_length=3, null=True), + ), + ] diff --git a/formazione/migrations/0040_auto_20190524_1411.py b/formazione/migrations/0040_auto_20190524_1411.py new file mode 100644 index 000000000..e4e0a7837 --- /dev/null +++ b/formazione/migrations/0040_auto_20190524_1411.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-05-24 14:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0039_auto_20190521_1512'), + ] + + operations = [ + migrations.AlterField( + model_name='corsoestensione', + name='segmento', + field=models.CharField(blank=True, choices=[('A', 'Tutti gli utenti di Gaia'), ('B', 'Volontari'), ('C', 'Volontari da meno di un anno'), ('D', 'Volontari da più di un anno'), ('E', 'Volontari con meno di 33 anni'), ('F', 'Volontari con 33 anni o più'), ('G', 'Sostenitori CRI'), ('H', 'Aspiranti volontari iscritti a un corso'), ('I', 'Tutti i Presidenti'), ('J', 'Presidenti di Comitati Locali'), ('K', 'Presidenti di Comitati Regionali'), ('IC', 'Tutti i Commissari'), ('JC', 'Commissari di Comitati Locali'), ('KC', 'Commissari di Comitati Regionali'), ('L', 'Delegati US'), ('M', 'Delegati Obiettivo I'), ('N', 'Delegati Obiettivo II'), ('O', 'Delegati Obiettivo III'), ('P', 'Delegati Obiettivo IV'), ('Q', 'Delegati Obiettivo V'), ('R', 'Delegati Obiettivo VI'), ('S', 'Referenti di un’Attività di Area I'), ('T', 'Referenti di un’Attività di Area II'), ('U', 'Referenti di un’Attività di Area III'), ('V', 'Referenti di un’Attività di Area IV'), ('W', 'Referenti di un’Attività di Area V'), ('X', 'Referenti di un’Attività di Area VI'), ('Y', 'Delegati Autoparco'), ('Z', 'Delegati Formazione'), ('AA', 'Volontari aventi un dato titolo')], max_length=9), + ), + ] diff --git a/formazione/models.py b/formazione/models.py index 5ab596b4b..80fc6c7cf 100755 --- a/formazione/models.py +++ b/formazione/models.py @@ -1,39 +1,45 @@ -# coding=utf-8 - -""" -Questo modulo definisce i modelli del modulo di Formazione di Gaia. -""" import datetime from django.conf import settings -from django.contrib.contenttypes.models import ContentType +from django.core.urlresolvers import reverse +from django.db import models, transaction from django.db.models import Q from django.db.transaction import atomic +from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.timezone import now -from anagrafica.costanti import PROVINCIALE, TERRITORIALE, LOCALE -from anagrafica.models import Sede, Persona, Appartenenza -from anagrafica.permessi.incarichi import INCARICO_GESTIONE_CORSOBASE_PARTECIPANTI, INCARICO_ASPIRANTE +from anagrafica.models import Sede, Persona, Appartenenza, Delega +from anagrafica.costanti import PROVINCIALE, TERRITORIALE, LOCALE, REGIONALE, NAZIONALE +from anagrafica.validators import (valida_dimensione_file_8mb, ValidateFileSize) +from anagrafica.permessi.applicazioni import DIRETTORE_CORSO +from anagrafica.permessi.incarichi import (INCARICO_ASPIRANTE, INCARICO_GESTIONE_CORSOBASE_PARTECIPANTI) +from anagrafica.permessi.costanti import MODIFICA from base.files import PDF, Zip -from base.models import ConAutorizzazioni, ConVecchioID, Autorizzazione from base.geo import ConGeolocalizzazione, ConGeolocalizzazioneRaggio -from base.models import ModelloSemplice -from base.tratti import ConMarcaTemporale, ConDelegati, ConStorico, ConPDF from base.utils import concept, poco_fa +from base.tratti import ConMarcaTemporale, ConDelegati, ConStorico, ConPDF +from base.models import ConAutorizzazioni, ConVecchioID, Autorizzazione, ModelloSemplice +from base.errori import messaggio_generico +from curriculum.models import Titolo +from curriculum.areas import OBBIETTIVI_STRATEGICI from posta.models import Messaggio from social.models import ConCommenti, ConGiudizio -from django.db import models - - -class Corso(ModelloSemplice, ConDelegati, ConMarcaTemporale, ConGeolocalizzazione, ConCommenti, ConGiudizio): - - class Meta: - abstract = True - permissions = ( - ("view_corso", "Can view corso"), - ) +from survey.models import Survey +from .validators import (course_file_directory_path, validate_file_extension, + delibera_file_upload_path) + + +class Corso(ModelloSemplice, ConDelegati, ConMarcaTemporale, + ConGeolocalizzazione, ConCommenti, ConGiudizio): + # Tipologia di corso + CORSO_NUOVO = 'C1' + BASE = 'BA' + TIPO_CHOICES = ( + (BASE, 'Corso di Formazione per Volontari CRI'), + (CORSO_NUOVO, 'Altri Corsi'), + ) # Stato del corso PREPARAZIONE = 'P' @@ -47,30 +53,63 @@ class Meta: (ANNULLATO, 'Annullato'), ) stato = models.CharField('Stato', choices=STATO, max_length=1, default=PREPARAZIONE) - sede = models.ForeignKey(Sede, related_query_name='%(class)s_corso', help_text="La Sede organizzatrice del Corso.") + sede = models.ForeignKey(Sede, related_query_name='%(class)s_corso', + help_text="La Sede organizzatrice del Corso.") + tipo = models.CharField('Tipo', max_length=4, choices=TIPO_CHOICES, blank=True) + class Meta: + abstract = True + permissions = ( + ("view_corso", "Can view corso"), + ) -class CorsoBase(Corso, ConVecchioID, ConPDF): - ## Tipologia di corso - #BASE = 'BA' - #TIPO = ( - # (BASE, 'Corso Base'), - #) - #tipo = models.CharField('Tipo', choices=TIPO, max_length=2, default=BASE) +class CorsoFile(models.Model): + is_enabled = models.BooleanField(default=True) + corso = models.ForeignKey('CorsoBase') + file = models.FileField('FIle', null=True, blank=True, + upload_to=course_file_directory_path, + validators=[valida_dimensione_file_8mb, validate_file_extension], + # help_text="Formati dei file supportati: doc, xls, pdf, zip, " + # "jpg (max 8mb))", + ) - MAX_PARTECIPANTI = 30 + def filename(self): + import os + return os.path.basename(self.file.name) - class Meta: - verbose_name = "Corso Base" - verbose_name_plural = "Corsi Base" - ordering = ['-anno', '-progressivo'] - permissions = ( - ("view_corsobase", "Can view corso base"), - ) + def __str__(self): + file = self.file if self.file else '' + corso = self.corso if hasattr(self, 'corso') else '' + return '<%s> of %s' % (file, corso) - data_inizio = models.DateTimeField(blank=False, null=False, help_text="La data di inizio del corso. " - "Utilizzata per la gestione delle iscrizioni.") + +class CorsoLink(models.Model): + is_enabled = models.BooleanField(default=True) + corso = models.ForeignKey('CorsoBase') + link = models.URLField('Link', null=True, blank=True) + + def __str__(self): + corso = self.corso if hasattr(self, 'corso') else '' + return '<%s> of %s' % (self.link, corso) + + +class CorsoBase(Corso, ConVecchioID, ConPDF): + from django.core.validators import MinValueValidator + + MIN_PARTECIPANTI = 20 + MAX_PARTECIPANTI = 50 + + EXT_MIA_SEDE = '1' + EXT_LVL_REGIONALE = '2' + EXTENSION_TYPE_CHOICES = [ + (EXT_MIA_SEDE, "Solo su mia sede di appartenenza"), + (EXT_LVL_REGIONALE, "A livello regionale"), + ] + + data_inizio = models.DateTimeField(blank=False, null=False, + help_text="La data di inizio del corso. " + "Utilizzata per la gestione delle iscrizioni.") data_esame = models.DateTimeField(blank=False, null=False) progressivo = models.SmallIntegerField(blank=False, null=False, db_index=True) anno = models.SmallIntegerField(blank=False, null=False, db_index=True) @@ -78,41 +117,98 @@ class Meta: data_attivazione = models.DateField(blank=True, null=True) data_convocazione = models.DateField(blank=True, null=True) - op_attivazione = models.CharField(max_length=255, blank=True, null=True) - op_convocazione = models.CharField(max_length=255, blank=True, null=True) + op_attivazione = models.CharField('Ordinanza presidenziale attivazione', + max_length=255, blank=True, null=True) + op_convocazione = models.CharField('Ordinanza presidenziale convocazione', + max_length=255, blank=True, null=True) + extension_type = models.CharField(max_length=5, blank=True, null=True, + default=EXT_MIA_SEDE, + choices=EXTENSION_TYPE_CHOICES) + min_participants = models.SmallIntegerField("Minimo partecipanti", + default=MIN_PARTECIPANTI, + validators=[MinValueValidator(MIN_PARTECIPANTI)]) + max_participants = models.SmallIntegerField("Massimo partecipanti", + default=MAX_PARTECIPANTI) + delibera_file = models.FileField('Delibera', null=True, + upload_to=delibera_file_upload_path, + validators=[ValidateFileSize(3), validate_file_extension] + ) + titolo_cri = models.ForeignKey(Titolo, blank=True, null=True, + verbose_name="Titolo CRI") + cdf_level = models.CharField(max_length=3, choices=Titolo.CDF_LIVELLI, + null=True, blank=True) + cdf_area = models.CharField(max_length=3, choices=OBBIETTIVI_STRATEGICI, + null=True, blank=True) + survey = models.ForeignKey(Survey, blank=True, null=True, + verbose_name='Questionario di gradimento') PUOI_ISCRIVERTI_OK = "IS" PUOI_ISCRIVERTI = (PUOI_ISCRIVERTI_OK,) SEI_ISCRITTO_PUOI_RITIRARTI = "GIA" + SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI = "IPC" SEI_ISCRITTO_NON_PUOI_RITIRARTI = "NP" - SEI_ISCRITTO = (SEI_ISCRITTO_PUOI_RITIRARTI, SEI_ISCRITTO_NON_PUOI_RITIRARTI,) + SEI_ISCRITTO = (SEI_ISCRITTO_PUOI_RITIRARTI, + SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI,) NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO = "VOL" NON_PUOI_ISCRIVERTI_TROPPO_TARDI = "TAR" NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO = "ALT" - NON_PUOI_ISCRIVERTI = (NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO, NON_PUOI_ISCRIVERTI_TROPPO_TARDI, - NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO,) + NON_PUOI_SEI_ASPIRANTE = 'ASP' + NON_PUOI_ISCRIVERTI_NON_HAI_TITOLI = 'NHT' + NON_HAI_CARICATO_DOCUMENTI_PERSONALI = 'NHCDP' + NON_HAI_DOCUMENTO_PERSONALE_VALIDO = 'NHDPV' + NON_PUOI_ISCRIVERTI = (NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO, + NON_PUOI_ISCRIVERTI_TROPPO_TARDI, + # NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO, + NON_PUOI_SEI_ASPIRANTE, + NON_PUOI_ISCRIVERTI_NON_HAI_TITOLI, + NON_HAI_CARICATO_DOCUMENTI_PERSONALI, + NON_HAI_DOCUMENTO_PERSONALE_VALIDO) NON_PUOI_ISCRIVERTI_SOLO_SE_IN_AUTONOMIA = (NON_PUOI_ISCRIVERTI_TROPPO_TARDI,) def persona(self, persona): - if (not Aspirante.objects.filter(persona=persona).exists()) and persona.volontario: - return self.NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO + # Checks related to tipo.CORSO_NUOVO (not old CorsoBase) + if Corso.CORSO_NUOVO == self.tipo: + if persona.ha_aspirante: + return self.NON_PUOI_SEI_ASPIRANTE - if PartecipazioneCorsoBase.con_esito_ok(persona=persona, corso__stato=self.ATTIVO).exclude(corso=self).exists(): - return self.NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO + # if not persona.has_required_titles_for_course(course=self): + # return self.NON_PUOI_ISCRIVERTI_NON_HAI_TITOLI - # Controlla se gia' iscritto. - if PartecipazioneCorsoBase.con_esito_ok(persona=persona, corso=self).exists(): - return self.SEI_ISCRITTO_NON_PUOI_RITIRARTI + # if (not Aspirante.objects.filter(persona=persona).exists()) and persona.volontario: + # return self.NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO + + # UPDATE: (GAIA-93) togliere blocco che non può iscriversi a più corsi + # if PartecipazioneCorsoBase.con_esito_ok(persona=persona, + # corso__tipo=self.BASE, + # corso__stato=self.ATTIVO).exclude(corso=self).exists(): + # return self.NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO + # Controlla se già iscritto. if PartecipazioneCorsoBase.con_esito_pending(persona=persona, corso=self).exists(): return self.SEI_ISCRITTO_PUOI_RITIRARTI + if PartecipazioneCorsoBase.con_esito_ok(persona=persona, corso=self).exists(): + # UPDATE: (GAIA-93) utente può ritirarsi dal corso in qualsiasi momento. + return self.SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI + if self.troppo_tardi_per_iscriverti: return self.NON_PUOI_ISCRIVERTI_TROPPO_TARDI + if not persona.personal_identity_documents(): + return self.NON_HAI_CARICATO_DOCUMENTI_PERSONALI + + if persona.personal_identity_documents(): + today = datetime.datetime.now().date() + documents = persona.personal_identity_documents() + gt_exists = documents.filter(expires__gt=today).exists() + lt_exists = documents.filter(expires__lt=today).exists() + + if lt_exists and not gt_exists: + return self.NON_HAI_DOCUMENTO_PERSONALE_VALIDO + return self.PUOI_ISCRIVERTI_OK def possibili_destinazioni(self): @@ -133,13 +229,98 @@ def prossimo(self): @classmethod @concept def pubblici(cls): - """ - Concept per Corsi Base pubblici (attivi e non ancora iniziati...) - """ - return Q( - data_inizio__gte=timezone.now() - datetime.timedelta(days=settings.FORMAZIONE_FINESTRA_CORSI_INIZIATI), - stato=cls.ATTIVO - ) + """ Concept per Corsi pubblici (attivi e non ancora iniziati...) """ + return Q(stato=cls.ATTIVO, + data_inizio__gte=timezone.now() - datetime.timedelta( + days=settings.FORMAZIONE_FINESTRA_CORSI_INIZIATI + )) + + @classmethod + def find_courses_for_volunteer(cls, volunteer): + today = datetime.date.today() + sede = volunteer.sedi_attuali(membro=Appartenenza.VOLONTARIO) + if not sede: + return cls.objects.none() # corsi non trovati perchè utente non ha sede + + ### + # Trova corsi che hanno uguale alla + ### + qs_estensioni_1 = CorsoEstensione.objects.filter(sede__in=sede, + corso__tipo=Corso.CORSO_NUOVO, + corso__stato=Corso.ATTIVO, + corso__data_inizio__gt=today) + courses_1 = cls.objects.filter(id__in=qs_estensioni_1.values_list('corso__id')) + + ### + # Trova corsi dove la si verifica come sede sottostante + ### + four_weeks_delta = today + datetime.timedelta(weeks=4) + qs_estensioni_2 = CorsoEstensione.objects.filter( + corso__tipo=Corso.CORSO_NUOVO, + corso__stato=Corso.ATTIVO, + corso__data_inizio__gt=today, + corso__data_esame__lt=four_weeks_delta).exclude( + corso__id__in=courses_1.values_list('id', flat=True)) + + courses_2 = list() + for estensione in qs_estensioni_2: + for s in estensione.sede.all(): + sedi_sottostanti = s.esplora() + for _ in sede: + if _ in sedi_sottostanti: + _corso = estensione.corso + if _corso not in courses_2: + courses_2.append(_corso.pk) + + courses_2 = cls.objects.filter(id__in=courses_2) + + return courses_1 | courses_2 + + # @classmethod + # def find_courses_for_volunteer(cls, volunteer): + # """ + # Questo metodo è stato commentato perchè nel task GAIA-97 è stato + # chiesto di togliere i requisiti necessari per partecipare ai corsi + # (quindi anche per rendere i corsi trovabili/visualizzabili in ricerca). + # Il metodo sostituitivo è descritto sopra. + # """ + # + # sede = volunteer.sedi_attuali(membro=Appartenenza.VOLONTARIO) + # if sede: + # sede = sede.last() + # else: + # return cls.objects.none() + # + # titoli = volunteer.titoli_personali_confermati().filter( + # titolo__tipo=Titolo.TITOLO_CRI) + # courses_list = list() + # courses = cls.pubblici().filter(tipo=Corso.CORSO_NUOVO) + # for course in courses: + # # Course has extensions. + # # Filter courses by titles and sede comparsion + # if course.has_extensions(): + # volunteer_titolo = titoli.values_list('titolo', flat=True) + # t = course.get_extensions_titles() + # s = course.get_extensions_sede() + # ext_t_list = t.values_list('id', flat=True) + # + # # Course has required titles but volunteer has not at least one + # if t and not volunteer.has_required_titles_for_course(course): + # continue + # + # if s and (sede in s): + # courses_list.append(course.pk) + # else: + # # Extensions have sede but volunteer's sede is not in the list + # continue + # else: + # # Course has no extensions. + # # Filter by firmatario sede if sede of volunteer is the same + # firmatario = course.get_firmatario_sede + # if firmatario and (sede in [firmatario]): + # courses_list.append(course.pk) + # + # return CorsoBase.objects.filter(id__in=courses_list) @property def iniziato(self): @@ -147,7 +328,7 @@ def iniziato(self): @property def troppo_tardi_per_iscriverti(self): - return timezone.now() > (self.data_inizio + datetime.timedelta(days=settings.FORMAZIONE_FINESTRA_CORSI_INIZIATI)) + return timezone.now() > (self.data_inizio + datetime.timedelta(days=settings.FORMAZIONE_FINESTRA_CORSI_INIZIATI)) @property def possibile_aggiungere_iscritti(self): @@ -157,16 +338,14 @@ def possibile_aggiungere_iscritti(self): def possibile_cancellare_iscritti(self): return self.stato in [Corso.ATTIVO, Corso.PREPARAZIONE] - def __str__(self): - return self.nome - @property def url(self): return "/aspirante/corso-base/%d/" % (self.pk,) @property def nome(self): - return "Corso Base %d/%d (%s)" % (self.progressivo, self.anno, self.sede) + course_type = 'Corso Base' if self.tipo == Corso.BASE else 'Corso' + return "%s %d/%d (%s)" % (course_type, self.progressivo, self.anno, self.sede) @property def link(self): @@ -178,15 +357,15 @@ def url_direttori(self): @property def url_modifica(self): - return "%smodifica/" % (self.url,) + return reverse('aspirante:modify', args=[self.pk]) @property def url_attiva(self): - return "%sattiva/" % (self.url,) + return reverse('aspirante:activate', args=[self.pk]) @property def url_termina(self): - return "%stermina/" % (self.url,) + return reverse('aspirante:terminate', args=[self.pk]) @property def url_iscritti(self): @@ -206,23 +385,27 @@ def url_ritirati(self): @property def url_mappa(self): - return "%smappa/" % (self.url,) + return reverse('aspirante:map', args=[self.pk]) @property def url_lezioni(self): - return "%slezioni/" % (self.url,) + return reverse('aspirante:lessons', args=[self.pk]) @property def url_report(self): - return "%sreport/" % (self.url,) + return reverse('aspirante:report', args=[self.pk]) @property def url_firme(self): - return "%sfirme/" % (self.url,) + return reverse('aspirante:firme', args=[self.pk]) @property def url_report_schede(self): - return self.url + "report/schede/" + return reverse('aspirante:report_schede', args=[self.pk]) + + @property + def url_estensioni(self): + return reverse('aspirante:estensioni_modifica', args=[self.pk]) @classmethod def nuovo(cls, anno=None, **kwargs): @@ -234,26 +417,18 @@ def nuovo(cls, anno=None, **kwargs): """ anno = anno or datetime.date.today().year - try: # Per il progressivo, cerca ultimo corso - ultimo = CorsoBase.objects.filter(anno=anno).latest('progressivo') + ultimo = cls.objects.filter(anno=anno).latest('progressivo') progressivo = ultimo.progressivo + 1 + except: + progressivo = 1 # Se non esiste, inizia da 1 - except: # Se non esiste, inizia da 1 - progressivo = 1 - - c = CorsoBase( - anno=anno, - progressivo=progressivo, - **kwargs - ) + c = CorsoBase(anno=anno, progressivo=progressivo, **kwargs) c.save() return c def attivabile(self): - """ - Controlla se il corso base e' attivabile. - """ + """Controlla se il corso base e' attivabile.""" if not self.locazione: return False @@ -261,6 +436,12 @@ def attivabile(self): if not self.descrizione: return False + if self.is_nuovo_corso and self.extension_type != CorsoBase.EXT_LVL_REGIONALE: + return False + + if self.direttori_corso().count() == 0: + return False + return True def aspiranti_nelle_vicinanze(self): @@ -294,28 +475,150 @@ def partecipazioni_negate(self): def partecipazioni_ritirate(self): return PartecipazioneCorsoBase.con_esito_ritirata(corso=self) - def attiva(self, rispondi_a=None): + def attiva(self, request=None, rispondi_a=None): + from .tasks import task_invia_email_agli_aspiranti + if not self.attivabile(): raise ValueError("Questo corso non è attivabile.") - self._invia_email_agli_aspiranti(rispondi_a=rispondi_a) + if self.is_nuovo_corso: + messaggio = "A breve tutti i volontari dei segmenti selezionati "\ + "verranno informati dell'attivazione di questo corso." + else: + messaggio = "A breve tutti gli aspiranti nelle vicinanze verranno "\ + "informati dell'attivazione di questo corso base." + self.stato = self.ATTIVO self.save() + task_invia_email_agli_aspiranti.apply_async(args=(self.pk, rispondi_a.pk),) + + return messaggio_generico(request, rispondi_a, + titolo="Corso attivato con successo", + messaggio=messaggio, + torna_titolo="Torna al Corso", + torna_url=self.url) + + def _corso_activation_recipients_for_email(self): + if self.is_nuovo_corso: + recipients = self.get_volunteers_by_course_requirements() + else: + recipients = self.aspiranti_nelle_vicinanze() + return recipients + def _invia_email_agli_aspiranti(self, rispondi_a=None): - for aspirante in self.aspiranti_nelle_vicinanze(): - persona = aspirante.persona - if not aspirante.persona.volontario: - Messaggio.costruisci_e_accoda( - oggetto="Nuovo Corso per Volontari CRI", - modello="email_aspirante_corso.html", - corpo={ - "persona": persona, - "corso": self, - }, - destinatari=[persona], - rispondi_a=rispondi_a - ) + for recipient in self._corso_activation_recipients_for_email(): + if self.is_nuovo_corso: + persona = recipient + subject = "Nuovo Corso %s per Volontari CRI" % self.titolo_cri + else: + persona = recipient.persona + subject = "Nuovo Corso per Volontari CRI" + + email_data = dict( + oggetto=subject, + modello="email_aspirante_corso.html", + corpo={ + 'persona': persona, + 'corso': self, + }, + destinatari=[persona], + rispondi_a=rispondi_a + ) + + if self.is_nuovo_corso: + # If course tipo is CORSO_NUOVO to send to volunteers only + Messaggio.costruisci_e_accoda(**email_data) + elif not self.is_nuovo_corso and not recipient.persona.volontario: + # to send to only + Messaggio.costruisci_e_accoda(**email_data) + + def has_extensions(self, is_active=True, **kwargs): + """ Case: extension_type == EXT_LVL_REGIONALE """ + return self.corsoestensione_set.filter(is_active=is_active).exists() + + def get_extensions(self, **kwargs): + """ Returns CorsoEstensione objects related to the course """ + return self.corsoestensione_set.filter(**kwargs) + + def get_extensions_sede(self, with_expanded=True, **kwargs): + """ Returns SedeQuerySet """ + if with_expanded: + return self.expand_extensions_sedi_sottostanti() + else: + return CorsoEstensione.get_sede(course=self, **kwargs) + + def expand_extensions_sedi_sottostanti(self): + expanded = Sede.objects.none() + + for e in self.get_extensions(): + all_sede = e.sede.all() + expanded |= all_sede + for sede in all_sede: + if e.sedi_sottostanti: + expanded |= sede.esplora() + return expanded.distinct() + + def get_extensions_titles(self, **kwargs): + """ Returns QuerySet """ + return CorsoEstensione.get_titles(course=self, **kwargs) + + def get_volunteers_by_course_requirements(self, **kwargs): + persons = None + + if self.is_nuovo_corso: + corso_extension = self.extension_type + if CorsoBase.EXT_MIA_SEDE == corso_extension: + persons = self.get_volunteers_by_only_sede() + + if CorsoBase.EXT_LVL_REGIONALE == corso_extension: + by_ext_sede = self.get_volunteers_by_ext_sede() + by_ext_titles = self.get_volunteers_by_ext_titles() + persons = by_ext_sede | by_ext_titles + + if persons is None: + # Sede of course (was set in the first step of course creation) + persons = Persona.objects.filter(sede=self.sede) + + return persons.filter(**kwargs).distinct() + + @property + def get_firmatario(self): + last = self.deleghe.last() + return last.firmatario if hasattr(last, 'firmatario') else last + + @property + def get_firmatario_sede(self): + course_created_by = self.get_firmatario + if course_created_by is not None: + return course_created_by.sede_riferimento() + else: + return course_created_by # returns None + + def get_volunteers_by_only_sede(self): + app_attuali = Appartenenza.query_attuale(membro=Appartenenza.VOLONTARIO).q + app = Appartenenza.objects.filter(app_attuali, + sede=self.get_firmatario_sede, + confermata=True) + return self._query_get_volunteers_by_sede(app) + + def get_volunteers_by_ext_sede(self): + app_attuali = Appartenenza.query_attuale(membro=Appartenenza.VOLONTARIO).q + app = Appartenenza.objects.filter(app_attuali, + sede__in=self.get_extensions_sede(), + confermata=True) + return self._query_get_volunteers_by_sede(app) + + def _query_get_volunteers_by_sede(self, appartenenze): + return Persona.to_contact_for_courses(corso=self).filter( + id__in=appartenenze.values_list('persona__id', flat=True) + ) + + def get_volunteers_by_ext_titles(self): + sede = self.get_extensions_sede() + titles = self.get_extensions_titles().values_list('id', flat=True) + return Persona.objects.filter(sede__in=sede, + titoli_personali__in=titles) @property def concluso(self): @@ -330,38 +633,66 @@ def ha_verbale(self): return self.stato == self.TERMINATO and self.partecipazioni_confermate().exists() def termina(self, mittente=None): - """ - Termina il corso base, genera il verbale e volontarizza. - """ + """ Termina il corso base, genera il verbale e volontarizza. """ - from django.db import transaction with transaction.atomic(): - # Per maggiore sicurezza, questa cosa viene eseguita in una transazione. + # Per maggiore sicurezza, questa cosa viene eseguita in una transazione for partecipante in self.partecipazioni_confermate(): - # Calcola e salva l'esito dell'esame. - esito_esame = partecipante.IDONEO if partecipante.idoneo else partecipante.NON_IDONEO + esito_esame = partecipante.IDONEO if partecipante.idoneo \ + else partecipante.NON_IDONEO partecipante.esito_esame = esito_esame partecipante.save() - # Comunica il risultato all'aspirante/volontario. + # Comunica il risultato all'aspirante/volontario partecipante.notifica_esito_esame(mittente=mittente) - if partecipante.idoneo: # Se idoneo, volontarizza. - persona = partecipante.persona - persona.da_aspirante_a_volontario(sede=partecipante.destinazione, - mittente=mittente) - if persona.sostenitore: - persona.end_sostenitore_membership() + # Actions required only for CorsoBase (Aspirante as participant) + if not self.is_nuovo_corso: + if partecipante.idoneo: # Se idoneo, volontarizza + partecipante.persona.da_aspirante_a_volontario( + inizio=self.data_esame, + sede=partecipante.destinazione, + mittente=mittente + ) - # Cancella tutte le eventuali partecipazioni in attesa. + # Cancella tutte le eventuali partecipazioni in attesa PartecipazioneCorsoBase.con_esito_pending(corso=self).delete() - # Salva lo stato del corso come terminato. + # Salva lo stato del corso come terminato self.stato = Corso.TERMINATO self.save() + if self.is_nuovo_corso: + self.set_titolo_cri_to_participants() + + def set_titolo_cri_to_participants(self): + """ Sets in Persona's Curriculum (TitoloPersonale) """ + + from curriculum.models import TitoloPersonale + + objs = [ + TitoloPersonale( + confermata=True, + titolo=self.titolo_cri, + persona=p.persona, + certificato_da=self.get_firmatario, + data_scadenza=timezone.now() + self.titolo_cri.expires_after_timedelta, + is_course_title=True, + corso_partecipazione=p, + + # todo: attending details + # data_ottenimento='', + # luogo_ottenimento='', + # codice='', + # codice_corso='', + # certificato='', + ) + for p in self.partecipazioni_confermate() + ] + TitoloPersonale.objects.bulk_create(objs) + def non_idonei(self): return self.partecipazioni_confermate().filter(esito_esame=PartecipazioneCorsoBase.NON_IDONEO) @@ -422,6 +753,151 @@ def key_cognome(elem): ) return pdf + @property + def is_reached_max_participants_limit(self): + actual_requests = PartecipazioneCorsoBase.objects.filter(corso=self) + return self.max_participants + 10 == actual_requests + + @property + def is_nuovo_corso(self): + return self.tipo == Corso.CORSO_NUOVO + + def get_course_links(self): + return self.corsolink_set.filter(is_enabled=True) + + def get_course_files(self): + return self.corsofile_set.filter(is_enabled=True) + + def inform_presidency_with_delibera_file(self): + sede = self.sede.estensione + email_body = """

E' stato attivato un nuovo corso. La delibera si trova in allegato.

""" + + if sede == LOCALE: + # Corso in una sede Locale. - Informa presidenta della Sede + email_to = self.sede.presidente().email + elif sede in [REGIONALE, NAZIONALE]: + # Corso in una sede Regionale/Nazionale. - Informa sulla mail + email_to = 'formazione@cri.it' + + Messaggio.invia_raw( + oggetto="Delibera nuovo corso: %s" % self, + corpo_html=email_body, + email_mittente=Messaggio.NOREPLY_EMAIL, + lista_email_destinatari=[email_to,], + allegati=self.delibera_file + ) + + def direttori_corso(self): + oggetto_tipo = ContentType.objects.get_for_model(self) + deleghe = Delega.objects.filter(tipo=DIRETTORE_CORSO, + oggetto_tipo=oggetto_tipo.pk, + oggetto_id=self.pk) + deleghe_persone_id = deleghe.values_list('persona__id', flat=True) + persone_qs = Persona.objects.filter(id__in=deleghe_persone_id) + return persone_qs + + def can_modify(self, me): + if me and me.permessi_almeno(self, MODIFICA): + return True + return False + + def can_activate(self, me): + if me.is_presidente: + """ All'presidente deve sparire la sezione dell'attivazione corso se: + - ha caricato delibera + - ha impostato estensioni (/aspirante/corso-base//estensioni/) + - ha nominato almeno un direttore + """ + has_delibera = self.delibera_file is not None + has_extension = self.has_extensions() + has_directors = self.direttori_corso().count() > 0 + is_all_true = has_delibera, has_extension, has_directors + + # # Deve riapparire se: il direttore ha inserito la descrizione + # if self.descrizione: + # return True + # else: + return True if False in is_all_true else False + else: + """ Direttori del corso vedono sempre la sezione invece """ + return True + + @property + def relazione_direttore(self): + # Non creare record in db per un corso ancora in preparazione + if self.stato != CorsoBase.PREPARAZIONE: + relazione, created = RelazioneCorso.objects.get_or_create(corso=self) + return relazione + + return RelazioneCorso.objects.none() + + class Meta: + verbose_name = "Corso" + verbose_name_plural = "Corsi" + ordering = ['-anno', '-progressivo'] + permissions = ( + ("view_corsobase", "Can view corso base"), + ) + + def __str__(self): + return str(self.nome) + + +class CorsoEstensione(ConMarcaTemporale): + from segmenti.segmenti import NOMI_SEGMENTI + from curriculum.models import Titolo + + corso = models.ForeignKey(CorsoBase, db_index=True) + is_active = models.BooleanField(default=True, db_index=True) + segmento = models.CharField(max_length=9, choices=NOMI_SEGMENTI, blank=True) + titolo = models.ManyToManyField(Titolo, blank=True) + sede = models.ManyToManyField(Sede) + sedi_sottostanti = models.BooleanField(default=False, db_index=True) + + def visible_by_extension_type(self): + type = self.corso.extension_type + if type == CorsoBase.EXT_MIA_SEDE: + self.is_active = False + elif type == CorsoBase.EXT_LVL_REGIONALE: + self.is_active = True + + @classmethod + def get_sede(cls, course, **kwargs): + sede = cls._get_related_objects_to_course(course, 'sede', **kwargs) + return sede if sede else Sede.objects.none() + + @classmethod + def get_titles(cls, course, **kwargs): + titles = cls._get_related_objects_to_course(course, 'titolo', **kwargs) + return titles if titles else Titolo.objects.none() + + @classmethod + def _get_related_objects_to_course(cls, course, field, **kwargs): + course_extensions = cls.objects.filter(corso=course, **kwargs) + if not course_extensions.exists(): + return None + + objects = [] + for i in course_extensions: + elem = getattr(i, field).all() + if elem: + for e in elem: + objects.append(e) + if objects: + model = ContentType.objects.get_for_model(objects[0]).model_class() + return model.objects.filter(id__in=[obj.id for obj in objects]).distinct() + + def __str__(self): + return '%s' % self.corso if hasattr(self, 'corso') else 'No CorsoBase set.' + + def save(self): + self.visible_by_extension_type() + super().save() + + class Meta: + verbose_name = 'Estensione del Corso' + verbose_name_plural = 'Estensioni del Corso' + class InvitoCorsoBase(ModelloSemplice, ConAutorizzazioni, ConMarcaTemporale, models.Model): persona = models.ForeignKey(Persona, related_name='inviti_corsi', on_delete=models.CASCADE) @@ -434,23 +910,10 @@ class InvitoCorsoBase(ModelloSemplice, ConAutorizzazioni, ConMarcaTemporale, mod IN_ATTESA_ASPIRANTE = 2 INVITO_INVIATO = -1 - RICHIESTA_NOME = "iscrizione a Corso Base" + RICHIESTA_NOME = "iscrizione a Corso" APPROVAZIONE_AUTOMATICA = datetime.timedelta(days=settings.SCADENZA_AUTORIZZAZIONE_AUTOMATICA) - class Meta: - verbose_name = "Invito di partecipazione a corso base" - verbose_name_plural = "Inviti di partecipazione a corso base" - ordering = ('persona__cognome', 'persona__nome', 'persona__codice_fiscale',) - permissions = ( - ("view_invitocorsobase", "Can view invito partecipazione corso base"), - ) - - def __str__(self): - return "Invit di part. di %s a %s" % ( - self.persona, self.corso - ) - def autorizzazione_concessa(self, modulo=None, auto=False, notifiche_attive=True, data=None): with atomic(): corso = self.corso @@ -497,7 +960,7 @@ def disiscrivi(self, mittente=None): """ self.autorizzazioni_ritira() Messaggio.costruisci_e_invia( - oggetto="Annullamento invito al Corso Base: %s" % self.corso, + oggetto="Annullamento invito al Corso: %s" % self.corso, modello="email_aspirante_corso_deiscrizione_invito.html", corpo={ "invito": self, @@ -509,7 +972,7 @@ def disiscrivi(self, mittente=None): destinatari=[self.persona], ) Messaggio.costruisci_e_invia( - oggetto="Annullamento invito al Corso Base: %s" % self.corso, + oggetto="Annullamento invito al Corso: %s" % self.corso, modello="email_aspirante_corso_deiscrizione_invito_mittente.html", corpo={ "invito": self, @@ -521,14 +984,25 @@ def disiscrivi(self, mittente=None): destinatari=[mittente], ) + class Meta: + verbose_name = "Invito di partecipazione a corso" + verbose_name_plural = "Inviti di partecipazione a corso" + ordering = ('persona__cognome', 'persona__nome', 'persona__codice_fiscale',) + permissions = ( + ("view_invitocorsobase", "Can view invito partecipazione corso base"), + ) + + def __str__(self): + return "Invito di part. di <%s> a <%s>" % (self.persona, self.corso) + -class PartecipazioneCorsoBase(ModelloSemplice, ConMarcaTemporale, ConAutorizzazioni, ConPDF): +class PartecipazioneCorsoBase(ModelloSemplice, ConMarcaTemporale, + ConAutorizzazioni, ConPDF): persona = models.ForeignKey(Persona, related_name='partecipazioni_corsi', on_delete=models.CASCADE) corso = models.ForeignKey(CorsoBase, related_name='partecipazioni', on_delete=models.PROTECT) # Stati per l'iscrizione da parte del direttore - NON_ISCRITTO = 0 ISCRITTO = 1 IN_ATTESA_ASPIRANTE = 2 @@ -536,7 +1010,6 @@ class PartecipazioneCorsoBase(ModelloSemplice, ConMarcaTemporale, ConAutorizzazi INVITO_INVIATO = -1 # Dati per la generazione del verbale (esito) - POSITIVO = "P" NEGATIVO = "N" ESITO = ( @@ -581,15 +1054,7 @@ class PartecipazioneCorsoBase(ModelloSemplice, ConMarcaTemporale, ConAutorizzazi help_text="La Sede presso la quale verrà registrato come Volontario l'aspirante " "nel caso di superamento dell'esame.") - class Meta: - verbose_name = "Richiesta di partecipazione" - verbose_name_plural = "Richieste di partecipazione" - ordering = ('persona__cognome', 'persona__nome', 'persona__codice_fiscale',) - permissions = ( - ("view_partecipazionecorsobarse", "Can view corso Richiesta di partecipazione"), - ) - - RICHIESTA_NOME = "Iscrizione Corso Base" + RICHIESTA_NOME = "Iscrizione Corso" def autorizzazione_concessa(self, modulo=None, auto=False, notifiche_attive=True, data=None): # Quando un aspirante viene iscritto, tutte le richieste presso altri corsi devono essere cancellati. @@ -624,12 +1089,17 @@ def idoneo(self): ) def notifica_esito_esame(self, mittente=None): - """ - Invia una e-mail al partecipante con l'esito del proprio esame. - """ + """ Invia una e-mail al partecipante con l'esito del proprio esame. """ + + template = "email_%s_corso_esito.html" + if self.corso.is_nuovo_corso: + template = template % 'volontario' + else: + template = template % 'aspirante' + Messaggio.costruisci_e_accoda( - oggetto="Esito del Corso Base: %s" % self.corso, - modello="email_aspirante_corso_esito.html", + oggetto="Esito del Corso: %s" % self.corso, + modello=template, corpo={ "partecipazione": self, "corso": self.corso, @@ -641,12 +1111,10 @@ def notifica_esito_esame(self, mittente=None): ) def disiscrivi(self, mittente=None): - """ - Disiscrive partecipante dal corso base. - """ + """ Disiscrive partecipante dal corso base. """ self.autorizzazioni_ritira() Messaggio.costruisci_e_invia( - oggetto="Disiscrizione dal Corso Base: %s" % self.corso, + oggetto="Disiscrizione dal Corso: %s" % self.corso, modello="email_aspirante_corso_deiscrizione.html", corpo={ "partecipazione": self, @@ -658,7 +1126,7 @@ def disiscrivi(self, mittente=None): destinatari=[self.persona], ) Messaggio.costruisci_e_invia( - oggetto="Disiscrizione dal Corso Base: %s" % self.corso, + oggetto="Disiscrizione dal Corso: %s" % self.corso, modello="email_aspirante_corso_deiscrizione_mittente.html", corpo={ "partecipazione": self, @@ -670,14 +1138,13 @@ def disiscrivi(self, mittente=None): destinatari=[mittente], ) - def __str__(self): - return "Richiesta di part. di %s a %s" % ( - self.persona, self.corso - ) - def autorizzazione_concedi_modulo(self): - from formazione.forms import ModuloConfermaIscrizioneCorsoBase - return ModuloConfermaIscrizioneCorsoBase + from formazione.forms import (ModuloConfermaIscrizioneCorsoBase, + ModuloConfermaIscrizioneCorso) + if self.corso.is_nuovo_corso: + return ModuloConfermaIscrizioneCorso + else: + return ModuloConfermaIscrizioneCorsoBase def genera_scheda_valutazione(self): pdf = PDF(oggetto=self) @@ -735,48 +1202,105 @@ def richieste_non_processabili(cls, richieste): oggetto_tipo=tipo, oggetto_id__in=partecipazioni_da_bloccare ).values_list('pk', flat=True) + class Meta: + verbose_name = "Richiesta di partecipazione" + verbose_name_plural = "Richieste di partecipazione" + ordering = ('persona__cognome', 'persona__nome', 'persona__codice_fiscale',) + permissions = ( + ("view_partecipazionecorsobarse", "Can view corso Richiesta di partecipazione"), + ) -class LezioneCorsoBase(ModelloSemplice, ConMarcaTemporale, ConGiudizio, ConStorico): + def __str__(self): + return "Richiesta di part. di %s a %s" % (self.persona, self.corso) + +class LezioneCorsoBase(ModelloSemplice, ConMarcaTemporale, ConGiudizio, ConStorico): corso = models.ForeignKey(CorsoBase, related_name='lezioni', on_delete=models.PROTECT) nome = models.CharField(max_length=128) + docente = models.ForeignKey(Persona, null=True, default='', + verbose_name='Docente della lezione',) + obiettivo = models.CharField('Obiettivo formativo della lezione', + max_length=128, null=True, default='') + luogo = models.CharField(max_length=255, null=True, blank=True, + verbose_name="il luogo di dove si svolgeranno le lezioni", + help_text="Compilare nel caso il luogo è diverso " + "dal comitato che ha organizzato il corso.") + + @property + def url_cancella(self): + return "%s%d/cancella/" % (self.corso.url_lezioni, self.pk) + + def send_messagge_to_docente(self, me): + Messaggio.costruisci_e_invia( + oggetto='Lezione al %s' % self.corso.nome, + modello="email_docente_assegnato_a_corso.html", + corpo={ + "persona": self.docente, + "corso": self.corso, + }, + mittente=me, + destinatari=[self.docente] + ) class Meta: - verbose_name = "Lezione di Corso Base" - verbose_name_plural = "Lezioni di Corsi Base" + verbose_name = "Lezione di Corso" + verbose_name_plural = "Lezioni di Corsi" ordering = ['inizio'] permissions = ( ("view_lezionecorsobase", "Can view corso Lezione di Corso Base"), ) def __str__(self): - return "Lezione: %s" % (self.nome,) - - @property - def url_cancella(self): - return "%s%d/cancella/" % ( - self.corso.url_lezioni, self.pk - ) + return "Lezione: %s" % self.nome class AssenzaCorsoBase(ModelloSemplice, ConMarcaTemporale): + """ + NB: valorizzati i campi "is_esonero" e "esonero_motivazione" significa + "Presenza", quindi per ottenere in una queryset solo le persone assenti + bisogna escludere i risultati con questi 2 campi valorizzati. + """ lezione = models.ForeignKey(LezioneCorsoBase, related_name='assenze', on_delete=models.CASCADE) persona = models.ForeignKey(Persona, related_name='assenze_corsi_base', on_delete=models.CASCADE) registrata_da = models.ForeignKey(Persona, related_name='assenze_corsi_base_registrate', null=True, on_delete=models.SET_NULL) + # Se questi 2 campi hanno un valore, Persona sarà considerata "Presente" alla lezione (GAIA-96) + esonero = models.NullBooleanField(default=False) + esonero_motivazione = models.CharField(max_length=255, null=True, blank=True, + verbose_name="Motivazione dell'esonero") + + @classmethod + def create_assenza(cls, lezione, persona, registrata_da, esonero=None): + assenza, created = cls.objects.get_or_create(lezione=lezione, + persona=persona, + registrata_da=registrata_da) + if esonero: + # Scrivi nell'oggetto la motivazione dell'esonero + assenza.esonero = True + assenza.esonero_motivazione = esonero + assenza.save() + + return assenza + + @property + def is_esonero(self): # , lezione, persona + if self.esonero and self.esonero_motivazione: + return True + if self.esonero or self.esonero_motivazione: + return True + return False + + def __str__(self): + return 'Assenza di %s a %s' % (self.persona.codice_fiscale, self.lezione) + class Meta: - verbose_name = "Assenza a Corso Base" - verbose_name_plural = "Assenze ai Corsi Base" + verbose_name = "Assenza a Corso" + verbose_name_plural = "Assenze ai Corsi" permissions = ( ("view_assenzacorsobase", "Can view corso Assenza a Corso Base"), ) - def __str__(self): - return "Assenza di %s a %s" % ( - self.persona.codice_fiscale, self.lezione - ) - class Aspirante(ModelloSemplice, ConGeolocalizzazioneRaggio, ConMarcaTemporale): @@ -813,12 +1337,6 @@ def sedi(self, tipo=Sede.COMITATO, **kwargs): def comitati(self): return self.sedi().filter(estensione__in=[LOCALE, PROVINCIALE, TERRITORIALE]) - def corso(self): - return CorsoBase.objects.filter( - PartecipazioneCorsoBase.con_esito_ok(persona=self.persona).via("partecipazioni"), - stato=Corso.ATTIVO, - ).first() - def richiesta_corso(self): return CorsoBase.objects.filter( PartecipazioneCorsoBase.con_esito_pending(persona=self.persona).via("partecipazioni"), @@ -830,7 +1348,14 @@ def corsi(self, **kwargs): Ritorna un elenco di Corsi (Base) nelle vicinanze dell'Aspirante. :return: Un elenco di Corsi. """ - return self.nel_raggio(CorsoBase.pubblici().filter(**kwargs)) + corsi = CorsoBase.pubblici().filter(**kwargs) + return self.nel_raggio(corsi) + + def corso(self): + partecipazione = PartecipazioneCorsoBase.con_esito_ok(persona=self.persona) + partecipazione = partecipazione.via("partecipazioni") + corso = CorsoBase.objects.filter(partecipazione, stato=Corso.ATTIVO) + return corso.first() def calcola_raggio(self): """ @@ -911,4 +1436,52 @@ def _chiudi_partecipazioni(cls, qs): def pulisci_volontari(cls): volontari = cls._anche_volontari() cls._chiudi_partecipazioni(volontari) - volontari.delete() \ No newline at end of file + volontari.delete() + + +class RelazioneCorso(ModelloSemplice, ConMarcaTemporale): + SENZA_VALORE = "Non ci sono segnalazioni e/o annotazioni" + + corso = models.ForeignKey(CorsoBase, related_name='relazione_corso') + note_esplicative = models.TextField( + blank=True, null=True, + verbose_name='Note esplicative', + help_text="Note esplicative in relazione ai cambiamenti effettuati rispetto " + "alla programmazione approvata in fase di pianificazione iniziale del corso.") + raggiungimento_obiettivi = models.TextField( + blank=True, null=True, + verbose_name='Raggiungimento degli obiettivi del corso', + help_text="Analisi sul raggiungimento degli obiettivi del corso " + "(generali rispetto all'evento e specifici di apprendimento).") + annotazioni_corsisti = models.TextField( + blank=True, null=True, + verbose_name="Annotazioni relative alla partecipazione dei corsisti", + help_text="Annotazioni relative alla partecipazione dei corsisti ") + annotazioni_risorse = models.TextField( + blank=True, null=True, + help_text="Annotazioni relative a risorse e competenze di particolare " + "rilevanza emerse durante il percorso formativo") + annotazioni_organizzazione_struttura = models.TextField( + blank=True, null=True, + help_text="Annotazioni e segnalazioni sull'organizzazione e " + "la logistica e della struttura ospitante il corso") + descrizione_attivita = models.TextField( + blank=True, null=True, + help_text="Descrizione delle eventuali attività di " + "tirocinio/affiancamento con indicazione dei Tutor") + + @property + def is_completed(self): + model_fields = self._meta.get_fields() + super_class_fields_to_exclude = ['id', 'creazione', 'ultima_modifica', 'corso'] + fields = [i.name for i in model_fields if i.name not in super_class_fields_to_exclude] + if list(filter(lambda x: x in ['', None], [getattr(self, i) for i in fields])): + return False + return True + + def __str__(self): + return 'Relazione Corso <%s>' % self.corso.nome + + class Meta: + verbose_name = 'Relazione del Direttore' + verbose_name_plural = 'Relazioni dei Direttori' diff --git a/formazione/tasks.py b/formazione/tasks.py new file mode 100644 index 000000000..3a71a9b77 --- /dev/null +++ b/formazione/tasks.py @@ -0,0 +1,11 @@ +from celery import shared_task + + +@shared_task(bind=True) +def task_invia_email_agli_aspiranti(self, course_pk, rispondi_a_pk): + from anagrafica.models import Persona + from .models import CorsoBase + + me = Persona.objects.get(pk=rispondi_a_pk) + corso = CorsoBase.objects.get(pk=course_pk) + corso._invia_email_agli_aspiranti(rispondi_a=me) diff --git a/formazione/templates/aspirante_corsi_base.html b/formazione/templates/aspirante_corsi_base.html index 2def202b0..2d96961b4 100644 --- a/formazione/templates/aspirante_corsi_base.html +++ b/formazione/templates/aspirante_corsi_base.html @@ -6,15 +6,15 @@ {% load social %} {% block app_contenuto %} +

Corsi {% if me.volontario %}attivi{%else%}nelle tue vicinanze{%endif%}

-

Corsi Base nelle tue vicinanze

- + {% if me.aspirante %}
- Questo è un elenco dei Corsi Base nel raggio di {{ me.aspirante.raggio }} km - da {{ me.aspirante.locazione }}. Puoi modificare la posizione - dal menu "Aspirante" > "Impostazioni". + Questo è un elenco dei Corsi nel raggio di {{ me.aspirante.raggio|default:"0" }} km + da {{ me.aspirante.locazione|default:"n/a" }}. Puoi modificare la posizione dal menu "Aspirante" > "Impostazioni".
+ {%endif%} @@ -24,9 +24,7 @@

Corsi Base nelle tue vicinanze

{% for corso in corsi %} - - - - - - + - {% empty %} - {% endfor %} -
{{ corso.link|safe }}
@@ -35,9 +33,7 @@

Corsi Base nelle tue vicinanze

{{ corso.locazione }} {% endif %} -
{% if not corso.iniziato %} @@ -54,24 +50,15 @@

Corsi Base nelle tue vicinanze

{{ d.persona.link|safe }} {% endfor %}
- {{ corso.partecipazioni.count }} richieste - {{ corso.partecipazioni.count }} richieste

Ancora nessun corso pianificato.

-

Puoi controllare la domanda formativa della zona e valutare l'attivazione di un - nuovo corso base.

+

Puoi controllare la domanda formativa della zona e valutare l'attivazione di un nuovo corso.

- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda.html b/formazione/templates/aspirante_corso_base_scheda.html index 3b5f3759b..a9b1c9af9 100644 --- a/formazione/templates/aspirante_corso_base_scheda.html +++ b/formazione/templates/aspirante_corso_base_scheda.html @@ -4,38 +4,97 @@ {% load static %} {% load utils %} - {% block pagina_titolo %} {% block scheda_titolo %}{% endblock %} - {{ corso.nome }} ({{ corso.sede.nome_completo }}) - — Corso Base + {{ corso.nome }} ({{ corso.sede.nome_completo }}) — Corso {% endblock %} {% block app_contenuto %} + + + {% if corso.titolo_cri %}

{{ corso.titolo_cri|default:"" }}

{% endif %} + +

{{ corso.nome }}

-

- {{ corso.nome }} -

- + {% if corso.locazione %}{{ corso.locazione }}{% else %} Posizione non impostata{% endif %} — + {{ corso.get_stato_display }} +

- {% if corso.locazione %} - - {{ corso.locazione }} - +

+ {% if corso.is_nuovo_corso and corso.concluso %} +
  • Questionario
  • + {% endif %} + + +
    + {% if puo_modificare %} + + {% endif %} + + + + {% if puo_modificare %} + + + + {% endif %} + + +
    + + {% if puo_modificare and corso.stato == corso.PREPARAZIONE and corso.prossimo and not corso.is_nuovo_corso %}

    Non c'è tempo da perdere!

    Questo corso inizierà a breve, ma non hai ancora attivato il Corso su Gaia.

    @@ -48,133 +107,113 @@

    Non c'è tempo da perdere!

    {% endif %} - {% if puo_modificare and corso.stato == corso.PREPARAZIONE %} + {% if puo_modificare and corso.stato == corso.PREPARAZIONE and can_activate %}

    Il corso non è ancora attivo

    -

    Questo corso base è ancora una bozza ("In Preparazione"). È necessario - attivare il corso affinché questo possa essere trovato dagli aspiranti. +

    Questo corso è ancora una bozza ("In Preparazione"). + È necessario attivare il corso affinché questo possa essere trovato {% if corso.is_nuovo_corso %}dai volontari{%else%}dagli aspiranti{%endif%}. Completa i passi necessari e clicca sul pulsante per attivare il corso:

    - {% checkbox corso.descrizione %}: - Inserisci una descrizione del Corso per gli Aspiranti dalla scheda "Gestione corso"; -

    -

    - {% checkbox corso.locazione %}: - Inserisci l'indirizzo del Corso dalla scheda "Gestione corso"; + {% if corso.direttori_corso.count == 0 %} + DA FARE: + {% else %} + FATTO: + {% endif %} + Selezionare almeno un direttore del Corso dalla scheda "Attivazione - Direttori"

    -

    - + + {% if corso.is_nuovo_corso %} +

    + {% if corso.extension_type == corso.EXT_MIA_SEDE %} + DA FARE: + {% else %} + FATTO: + {% endif %} + Indica l'area geografica. Seleziona i Comitati ai quali è destinata la formazione +

    + {% endif %} + +

    {% checkbox corso.descrizione %}: Inserisci una descrizione del Corso per i Corsisti dalla scheda "Dettagli - Pubblicizza corso"

    + + +

    + - Attiva il corso e informa gli aspiranti in zona (più di - {{ corso.aspiranti_nelle_vicinanze.count }}) + + {% if corso.is_nuovo_corso %} + {% if corso.extension_type == corso.EXT_MIA_SEDE %} + Attiva il corso e informa i volontari della mia sede + {% elif corso.extension_type == corso.EXT_LVL_REGIONALE %} + Attiva il corso e informa i volontari dei segmenti selezionati. + {% else %} + Attiva il corso + {% endif %} + {% else %} + Attiva il corso e informa gli aspiranti in zona (più di {{ corso.aspiranti_nelle_vicinanze.count }}) + {% endif %}

    -
    {% endif %} {% if puo_modificare and corso.terminabile %}
    -

    Genera il verbale e termina il corso

    -

    - Il corso si è concluso, ma è ancora necessario generare il verbale - del corso. -

    -

    - Una volta generato il verbale, tutti i partecipanti verranno informati dell'esito - e, coloro che saranno stati promossi, verranno trasformati automaticamente in - volontari. -

    -

    - - - Completa i dati per generare il verbale e terminare il corso - -

    - +

    Terminazione corso

    +

    Il corso si è concluso, ma è ancora necessario generare il verbale del corso e compilare la relazione.

    +

    Una volta generato il verbale e compilata la relazione, tutti i partecipanti verranno informati dell'esito e, + coloro che saranno stati promossi, verranno trasformati automaticamente in volontari.

    + + + + + + {% if corso.relazione_direttore and corso.relazione_direttore.is_completed %} + + {% endif %} + +
    + Generare il verbale + Inserire la relazione Terminare il corso
    - {% endif %} - - - -

     

    - {% block scheda_contenuto %} - {% endblock %} - + {% block scheda_contenuto %}{% endblock %}

     


    -
    - - Ultimo aggiornamento {{ corso.ultima_modifica }} -
    - -{% endblock %} \ No newline at end of file +
    Ultimo aggiornamento {{ corso.ultima_modifica }}
    + + +{% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda_attiva.html b/formazione/templates/aspirante_corso_base_scheda_attiva.html index 86ad2cf32..3c26017fb 100644 --- a/formazione/templates/aspirante_corso_base_scheda_attiva.html +++ b/formazione/templates/aspirante_corso_base_scheda_attiva.html @@ -1,66 +1,44 @@ {% extends 'aspirante_corso_base_scheda.html' %} -{% block scheda_titolo %} - Attiva Corso Base -{% endblock %} +{% block scheda_titolo %}Attiva Corso{% endblock %} {% block scheda_contenuto %} -
    {% csrf_token %} -
    -

    - - Attiva il Corso Base -

    +

    Attiva il Corso {% if corso.tipo == Corso.BASE %}Base{%endif%}

    -

    Assicurati di aver inserito tutte le informazioni utili per la partecipazione - al Corso Base nella descrizione del corso.

    -

    Invieremo un messaggio a tutti i {{ corso.aspiranti_nelle_vicinanze.count }} - aspiranti nelle vicinanze avvisandoli dell'attivazione di questo corso. +

    Assicurati di aver inserito tutte le informazioni utili per la partecipazione al Corso nella descrizione del corso.

    + +

    Invieremo un messaggio a tutti i + {% if corso.is_nuovo_corso %} + volontari in base all' estensione impostata + {% else %} + {{ corso.aspiranti_nelle_vicinanze.count }} aspiranti nelle vicinanze + {% endif %} avvisandoli dell'attivazione di questo corso. + Tieni presente che puoi attivare il corso, ed inviare questa e-mail, solo una volta.

    -

    Ecco un'anteprima dell'e-mail che invieremo a tutti gli aspiranti:

    +

    Ecco un'anteprima dell'e-mail che invieremo a tutti {% if corso.is_nuovo_corso %}i volontari{%else%}gli aspiranti{%endif%}:

    - - Anteprima messaggio -
    -
    - {{ testo|safe }} + Anteprima messaggio
    +
    {{ testo|safe }}
    -

    Dopo aver verificato la correttezza e la completezza delle informazioni qui presenti, - clicca sul pulsante per attivare il corso e inviare l'e-mail.

    +

    Dopo aver verificato la correttezza e la completezza delle informazioni qui presenti, clicca sul pulsante per attivare il corso e inviare l'e-mail.

    -

    -

    - - - Torna indietro - -

    - - +

    Torna indietro

    - -
    - - -
    -
    - {% endblock %} \ No newline at end of file diff --git a/formazione/templates/aspirante_corso_base_scheda_informazioni.html b/formazione/templates/aspirante_corso_base_scheda_informazioni.html index c19e56f39..367b17147 100644 --- a/formazione/templates/aspirante_corso_base_scheda_informazioni.html +++ b/formazione/templates/aspirante_corso_base_scheda_informazioni.html @@ -3,228 +3,233 @@ {% load utils %} {% load social %} {% load humanize %} +{% load bootstrap3 %} {% block scheda_contenuto %} {% if not me or not puo_modificare %} - - {% if not me or puoi_partecipare in corso.PUOI_ISCRIVERTI %} -
    -

    Vuoi partecipare a questo corso base?

    +

    Vuoi partecipare a questo corso?

    {% if not me %} -

    Per diventare volontario di Croce Rossa Italiana, iscrivendoti a - questo o altri corsi base nelle tue vicinanze, registrati +

    Per diventare volontario di Croce Rossa Italiana, iscrivendoti a questo o altri corsi nelle tue vicinanze, registrati come aspirante su Gaia. Potrai vedere i corsi nelle tue vicinanze ed essere informato immediatamente quando un nuovo corso viene attivato vicino a te!

    - + Voglio registrarmi come Aspirante

    - {% else %}

    Se sei interessat{{ me.genere_o_a }} a iscriverti a questo corso, clicca sul pulsante seguente. - Notificheremo il tuo interesse al direttore del corso, che ti contatterà

    + Notificheremo il tuo interesse al direttore del corso, che ti contatterà + + {% if not corso.is_nuovo_corso %} + + {% endif %} +

    - + Voglio iscrivermi a questo corso -

    - {% endif %} - -
    - {% elif puoi_partecipare in corso.SEI_ISCRITTO %} + + {% elif puoi_partecipare in corso.SEI_ISCRITTO %} {% if puoi_partecipare == corso.SEI_ISCRITTO_PUOI_RITIRARTI %}

    Hai chiesto di partecipare a questo corso

    Verrai contattat{{ me.genere_o_a }} a breve dal direttore del corso.

    -

    Puoi presentarti alla prima lezione del corso base, dove la tua iscrizione +

    Puoi presentarti alla prima lezione del corso, dove la tua iscrizione verrà completata e confermata dal direttore — nota solo che il direttore ha facoltà di limitare l'accesso ai primi {{ me.aspirante.richiesta_corso.MAX_PARTECIPANTI }} iscritti.

    - - - Non posso più partecipare — voglio ritirarmi + + Non posso più partecipare — voglio ritirarmi -

    -
    - {% elif puoi_partecipare == corso.SEI_ISCRITTO_NON_PUOI_RITIRARTI %} - + {% elif puoi_partecipare == corso.SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI and corso.stato != corso.TERMINATO %}

    Sei iscritt{{ me.genere_o_a }} a questo corso!

    Meraviglioso! Presentati alle lezioni del corso secondo il programma indicato sotto.

    Per qualsiasi domanda, contatta uno dei direttori del corso, cliccando sul suo nome.

    +

    + + Non posso più partecipare — voglio ritirarmi + +

    - - - {% endif %} - {% elif puoi_partecipare in corso.NON_PUOI_ISCRIVERTI %} - +
    {% if puoi_partecipare == corso.NON_PUOI_ISCRIVERTI_GIA_VOLONTARIO %} -
    - - Sei già parte di Croce Rossa Italiana, le funzionalità di - iscrizione al corso sono disabilitate. -
    - - + Sei già parte di Croce Rossa Italiana, le funzionalità di iscrizione al corso sono disabilitate. {% elif puoi_partecipare == corso.NON_PUOI_ISCRIVERTI_TROPPO_TARDI %} - -
    - Questo corso è già iniziato da troppo tempo per iscriverti. -
    - {% elif puoi_partecipare == corso.NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO %} - -
    - - Sei già iscritt{{ me.genere_o_a }} a un altro corso, quindi - non puoi iscriverti a questo. -
    - + Sei già iscritt{{ me.genere_o_a }} a un altro corso, quindi non puoi iscriverti a questo. + {% elif puoi_partecipare == corso.NON_PUOI_SEI_ASPIRANTE %} + Non puoi iscriverti a questo corso. + {% elif puoi_partecipare == corso.NON_HAI_DOCUMENTO_PERSONALE_VALIDO %} + Per iscriverti a questo corso devi aggiornare il documento di riconoscimento in corso di validità. + {% elif puoi_partecipare == corso.NON_PUOI_ISCRIVERTI_NON_HAI_TITOLI %} + Non puoi partecipare perchè non hai titoli necessari. + + {% elif puoi_partecipare == corso.NON_HAI_CARICATO_DOCUMENTI_PERSONALI %} + Per iscriverti a questo corso inserisci la copia di un documento di riconoscimento in corso di validità (CDI o Patente Civile). + +
    +
    + {% bootstrap_form load_personal_document %} +
    + {% csrf_token %} + +
    + {% else %} + Non puoi partecipare a questo corso. {% endif %} - - - - +
    {% endif %} - - {% endif %} -
    -
    -

    - - Direttori - - {{ corso.delegati_attuali.count }} - -

    +

    Direttori {{ corso.delegati_attuali.count }}

    {% for direttore in corso.delegati_attuali %} {% card direttore extra_class='' mute_contact=True %} - {% empty %} - - Nessun direttore. - + Nessun direttore. {% endfor %}
    -
    -

    - - Organizzatore -

    +

    Organizzatore

    -
    - - - {{ corso.sede.nome }} - -
    + {{ corso.sede.nome }}
    {{ corso.sede.get_tipo_display }} — {{ corso.sede.get_estensione_display }}
    -
    -

    - - Data di inizio -

    +

    Data di inizio

    - -
    - {{ corso.data_inizio }} -
    - +
    {{ corso.data_inizio }}
    -

    - - Data di esame -

    +

    Data di esame

    - -
    - {{ corso.data_esame }} -
    - +
    {{ corso.data_esame }}
    -

    Informazioni

    - {{ corso.descrizione|safe }} + {{ corso.descrizione|default:"

    Ancora non disponibili

    "|safe }} + + {% if puoi_partecipare == corso.SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI or puo_modificare %} + {% if corso.get_course_links or corso.get_course_files %} +
    +

    Materiale didattico

    + {% if corso.get_course_links %} + Link:
    + {% for link in corso.get_course_links %} + {{link.link}}
    + {% endfor %} + {% endif %} + +
    + + {% if corso.get_course_links %} + File:
    + {% for file in corso.get_course_files %} + {{file.filename}}
    + {% endfor %} + {% endif %} +
    + {%endif%} + {% endif %} +

    + Regolamenti: +

    +

    + + {% if corso.tipo == corso.CORSO_NUOVO and corso.get_extensions_titles %} +

    + Titoli necessari: +

      + {% for e in corso.get_extensions_titles %} +
    • {{ e.nome }}
    • + {% endfor %} +
    +

    + {% endif %}

    Lezioni - - {{ corso.lezioni.count }} - - + {{ corso.lezioni.count }}

    - + - {% for lezione in corso.lezioni.all %} - + + - {% empty %} - + - - - {% endfor %} -
    Data ArgomentoDocente
    @@ -232,28 +237,20 @@

    {{ lezione.inizio.time|date:"TIME_FORMAT" }} —{{ lezione.fine.time|date:"TIME_FORMAT" }}

    - {{ lezione.nome }} - {{ lezione.nome }}{{ lezione.docente }}
    - - Lezioni non ancora inserite. - + + Lezioni non ancora inserite. +
    -
    -
    @@ -261,13 +258,9 @@

    Commenti - - {{ corso.commenti.count }} - + {{ corso.commenti.count }}

    {% commenti corso 10 %} -
    - {% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda_iscritti.html b/formazione/templates/aspirante_corso_base_scheda_iscritti.html index a022da05a..1abdea1b2 100644 --- a/formazione/templates/aspirante_corso_base_scheda_iscritti.html +++ b/formazione/templates/aspirante_corso_base_scheda_iscritti.html @@ -27,12 +27,10 @@

    {% if corso.possibile_aggiungere_iscritti %}
    - - - Iscrivi persone + + Invita Persone al corso -

    Vuoi iscrivere persone a questo corso base?

    +

    Vuoi iscrivere persone a questo corso?

    {% endif %} diff --git a/formazione/templates/aspirante_corso_base_scheda_iscritti_aggiungi.html b/formazione/templates/aspirante_corso_base_scheda_iscritti_aggiungi.html index 10ebf041f..e8ac38121 100644 --- a/formazione/templates/aspirante_corso_base_scheda_iscritti_aggiungi.html +++ b/formazione/templates/aspirante_corso_base_scheda_iscritti_aggiungi.html @@ -1,30 +1,20 @@ {% extends 'aspirante_corso_base_scheda.html' %} -{% block scheda_titolo %} - Aggiungi Iscritti -{% endblock %} +{% block scheda_titolo %}Aggiungi Iscritti{% endblock %} {% load utils %} {% load bootstrap3 %} {% block scheda_contenuto %} -
    -
    -

    - - Aggiungi iscritti -

    +

    Aggiungi Iscritti

    -

    Con questo modulo puoi selezionare uno o più - Sostenitori o Aspiranti da iscrivere a questo - corso base.

    -

    Presidente e Ufficio Soci possono inserire i sostenitori - dal pannello "Soci" > "Aggiungi Persona".

    +

    Ricerca per Codice Fiscale la persona da iscrivere a questo corso.

    +

    Presidente e Ufficio Soci possono inserire i sostenitori dal pannello "Soci" > "Aggiungi Persona".

    {% csrf_token %} @@ -43,13 +33,9 @@

    - -
    @@ -96,6 +82,12 @@

    {% elif r.esito == corso.NON_PUOI_ISCRIVERTI_GIA_ISCRITTO_ALTRO_CORSO %} Già iscritt{{ r.persona.genere_o_a }} a un altro corso + {% elif r.esito == corso.NON_HAI_DOCUMENTO_PERSONALE_VALIDO %} + Questo utente non ha un documento di riconoscimento valido/rinnovato + + {% elif r.esito == corso.NON_HAI_CARICATO_DOCUMENTI_PERSONALI %} + Questo utente non ha caricato un documento d'identità + {% endif %} {% endif %} @@ -112,29 +104,20 @@

    {% else %} No - {% endif %} - - {% empty %} - Seleziona una o più persone da iscrivere al - corso base. Qui vedrai l'esito della procedura - di iscrizione. + Seleziona una o più persone da iscrivere al corso{% if not corso.is_nuovo_corso %} base{%endif%}. Qui vedrai l'esito della procedura di iscrizione. - {% endfor %}

    -

    -
    - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda_lezioni.html b/formazione/templates/aspirante_corso_base_scheda_lezioni.html index 1f438b3ea..ab8759cc4 100644 --- a/formazione/templates/aspirante_corso_base_scheda_lezioni.html +++ b/formazione/templates/aspirante_corso_base_scheda_lezioni.html @@ -1,30 +1,27 @@ {% extends 'aspirante_corso_base_scheda.html' %} {% load bootstrap3 %} +{% load formazione_templatetags %} -{% block scheda_titolo %} - Lezioni -{% endblock %} +{% block scheda_titolo %}Lezioni{% endblock %} {% block scheda_contenuto %} - +
    - {% csrf_token %} - + {% csrf_token %}
    -
    -

    - - Lezioni -

    +

    Lezioni

    - - @@ -34,19 +31,12 @@

    {% for lezione, modulo, partecipanti_lezione in lezioni %} - {% for partecipante in partecipanti %} + {% lezione_partecipante_pk_shortcut lezione partecipante as lezione_partecipante_pk %} + {% lezione_esonero lezione partecipante as assenza_lezione_esonero %} + - {% endfor %} -
    {% bootstrap_form modulo %} - - - - Cancella lezione + + + Cancella lezione @@ -59,14 +49,32 @@

    - + + + + + {{ partecipante.link|safe }} @@ -77,72 +85,66 @@

    Nessun iscritto confermato.
    - - + - - - {% empty %} - - - Ancora nessuna lezione inserita. - Inserisci una lezione col modulo in questa pagina. + Ancora nessuna lezione inserita. Inserisci una lezione col modulo in questa pagina. - {% endfor %} - - - + -
    -
    -
    + - - -
    - {% csrf_token %} + + {% csrf_token %}
    -
    -

    - - Aggiungi lezione -

    +

    Completa Lezioni

    {% bootstrap_form modulo_nuova_lezione %} - -
    -
    -
    - - +
    - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda_modifica.html b/formazione/templates/aspirante_corso_base_scheda_modifica.html index 3de378f55..972a4b709 100644 --- a/formazione/templates/aspirante_corso_base_scheda_modifica.html +++ b/formazione/templates/aspirante_corso_base_scheda_modifica.html @@ -1,46 +1,23 @@ {% extends 'aspirante_corso_base_scheda.html' %} - -{% block scheda_titolo %} - Modifica -{% endblock %} - {% load utils %} {% load bootstrap3 %} -{% block scheda_contenuto %} +{% block scheda_titolo %}Modifica{% endblock %} +{% block scheda_contenuto %}
    -
    -

    - - Modifica corso -

    -
    +

    Modifica corso

    -
    + {% csrf_token %} {% bootstrap_form modulo %} - -
    - -
    -
    - -
    -
    -

    - - Modifica posizione -

    -
    -
    - {% localizzatore corso solo_italia=1 %} +

    Condividi il materiale didattico

    + {{ file_formset.as_p }} + {{ link_formset.as_p }} + +
    - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/aspirante_corso_base_scheda_termina.html b/formazione/templates/aspirante_corso_base_scheda_termina.html index 5319c3c7e..2d11195ce 100644 --- a/formazione/templates/aspirante_corso_base_scheda_termina.html +++ b/formazione/templates/aspirante_corso_base_scheda_termina.html @@ -1,13 +1,10 @@ {% extends 'aspirante_corso_base_scheda.html' %} -{% block scheda_titolo %} - Termina Corso Base -{% endblock %} +{% block scheda_titolo %}Termina Corso Base{% endblock %} {% load bootstrap3 %} {% block scheda_contenuto %} -
    {% csrf_token %} @@ -15,108 +12,76 @@
    -

    - - Compilazione del verbale del corso -

    +

    Compilazione del verbale del corso

    - -
    - - Per terminare il corso, completa le informazioni - necessarie per ogni partecipante, che serviranno a generare - il verbale del corso e finalizzarlo. Invieremo una e-mail - con l'esito a tutti i partecipanti. -
    - -
    - - Ricorda di salvare! - - Nuova funzionalità - - - - Puoi salvare le tue modifiche cliccando sul pulsante Salva, e continuare - in un secondo momento la compilazione del verbale. -
    - -
    - - {% for partecipante, modulo in partecipanti_moduli %} - -
    -
    -

    {{ partecipante.persona.nome_completo }}

    - -

     

    - - - - - - - - - - - - - - - - - - - - - - - - -
    Iscrizione{{ partecipante.creazione|date:"SHORT_DATETIME_FORMAT" }}
    Codice Fiscale{{ partecipante.persona.codice_fiscale }}
    Data di Nascita{{ partecipante.persona.data_nascita }}
    Luogo di Nascita - {{ partecipante.persona.comune_nascita }} - {{ partecipante.persona.provincia_nascita }} -
    Scheda - - - Apri (nuova scheda) - -
    -
    -
    - {% bootstrap_form modulo %} +
    +
    + Per terminare il corso, completa le informazioni necessarie per ogni partecipante, + che serviranno a generare il verbale del corso e finalizzarlo. Invieremo una e-mail con l'esito a tutti i partecipanti.
    -
    - - -
    - - {% endfor %} - +
    + Ricorda di salvare! + Nuova funzionalità + Puoi salvare le tue modifiche cliccando sul pulsante Salva, e continuare in un secondo momento la compilazione del verbale. +
    - +
    + + {% for partecipante, modulo in partecipanti_moduli %} +
    +
    +

    {{ partecipante.persona.nome_completo }}

    + +

     

    + + + + + + + + + + + + + + + + + + + + + + + + +
    Iscrizione{{ partecipante.creazione|date:"SHORT_DATETIME_FORMAT" }}
    Codice Fiscale{{ partecipante.persona.codice_fiscale }}
    Data di Nascita{{ partecipante.persona.data_nascita }}
    Luogo di Nascita + {{ partecipante.persona.comune_nascita }} + {{ partecipante.persona.provincia_nascita }} +
    Scheda Apri (nuova scheda)
    +
    +
    {% bootstrap_form modulo %}
    +
    +
    + {% endfor %} + + +
    + {% if corso.relazione_direttore.is_completed %} + + {% endif %}
    - -
    - - -
    -
    - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/aspirante_corso_estensioni_modifica.html b/formazione/templates/aspirante_corso_estensioni_modifica.html new file mode 100644 index 000000000..507146571 --- /dev/null +++ b/formazione/templates/aspirante_corso_estensioni_modifica.html @@ -0,0 +1,56 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} + +{% block scheda_titolo %}Modifica{% endblock %} + +{% block scheda_contenuto %} +
    +
    +

    Area geografica interessata

    +
    +
    +
    + {% csrf_token %} + + {% bootstrap_form select_extension_type_form %} +
    + {% bootstrap_formset select_extensions_formset %} +
    + + +
    +
    +
    + + + +{% endblock %} diff --git a/formazione/templates/aspirante_corso_informa_persone.html b/formazione/templates/aspirante_corso_informa_persone.html new file mode 100644 index 000000000..b81ed8888 --- /dev/null +++ b/formazione/templates/aspirante_corso_informa_persone.html @@ -0,0 +1,27 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} + +{% block scheda_titolo %}Informa persone{% endblock %} + +{% block scheda_contenuto %} +
    +
    +

    Informare persone di questo corso

    +
    +
    +
    + {% csrf_token %} + {% bootstrap_form form %} + + +
    + +
    +
    +{% endblock %} diff --git a/formazione/templates/aspirante_sedi.html b/formazione/templates/aspirante_sedi.html index c73bcff7b..2b57351fe 100644 --- a/formazione/templates/aspirante_sedi.html +++ b/formazione/templates/aspirante_sedi.html @@ -5,68 +5,39 @@ {% load utils %} {% block app_contenuto %} - - -

    Sedi CRI nelle tue vicinanze

    -
    - - Questo è un elenco delle Sedi CRI nel raggio di {{ me.aspirante.raggio }} km - da {{ me.aspirante.locazione }}. Puoi modificare la posizione - dal menu "Aspirante" > "Impostazioni". + Questo è un elenco delle Sedi CRI nel raggio di {{ me.aspirante.raggio }} km + da {{ me.aspirante.locazione }}. Puoi modificare la posizione dal menu "Aspirante" > "Impostazioni".
    -
    - {% for sede in sedi %} - - - - + + + {% endfor %}
    Sede
    - - {{ sede.link|safe }}
    - {% if sede.locazione %} - - {{ sede.locazione }}
    - {% endif %} - {% if sede.email %} - - {{ sede.email }}
    - {% endif %} - {% if sede.telefono %} - - {{ sede.telefono }}
    - {% endif %} -
    + {{ sede.link|safe }}
    + {% if sede.locazione %} {{ sede.locazione }}
    {% endif %} + {% if sede.email %} {{ sede.email }}
    {% endif %} + {% if sede.telefono %} {{ sede.telefono }}
    {% endif %} +
    -
    - {% mappa sedi %} {{ elemento.link|safe }}
    {{ elemento.locazione }} - {% icona_colore %} - red - {% endmappa %} - + {% icona_colore %} red {% endmappa %}
    - -
    - - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/course_compila_relazione_direttore.html b/formazione/templates/course_compila_relazione_direttore.html new file mode 100644 index 000000000..be7e0778a --- /dev/null +++ b/formazione/templates/course_compila_relazione_direttore.html @@ -0,0 +1,23 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} + +{% block scheda_titolo %}Inserimento relazione del Corso{% endblock %} + +{% block scheda_contenuto %} +
    +
    +

    Inserire relazione del Corso

    +
    +
    +
    + {% csrf_token %} + {% bootstrap_form form_relazione %} + + {% if corso.stato != corso.TERMINATO %} + + {% endif %} +
    +
    +
    +{% endblock %} diff --git a/formazione/templates/course_send_questionnaire_to_participants.html b/formazione/templates/course_send_questionnaire_to_participants.html new file mode 100644 index 000000000..fa98d4688 --- /dev/null +++ b/formazione/templates/course_send_questionnaire_to_participants.html @@ -0,0 +1,42 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} + +{% block scheda_titolo %}Invia il questionario di Gradimento{% endblock %} + +{% block scheda_contenuto %} +
    +
    +

    Invia il questionario di Gradimento ai partecipanti

    +
    +
    +
    + + + {% csrf_token %} + + {% for request in corso.partecipazioni_confermate %} + {% with request.persona.id as persona_id %} +
    + + +
    + {% endwith %} + {% endfor %} + + +
    + + +
    +
    +{% endblock %} diff --git a/formazione/templates/email_aspirante_corso_inc_testo.html b/formazione/templates/email_aspirante_corso_inc_testo.html index ee05cc5ff..5b4b35cf7 100644 --- a/formazione/templates/email_aspirante_corso_inc_testo.html +++ b/formazione/templates/email_aspirante_corso_inc_testo.html @@ -1,11 +1,12 @@

    Ciao {{ persona.nome }},

    -

    ti scriviamo in riferimento alla tua richiesta di diventare volontario della Croce - Rossa Italiana fatta attraverso Gaia.

    -

    Nelle tue vicinanze è stato attivato un "corso base" per Volontari CRI!

    -

    Il corso è stato organizzato dal {{ corso.sede.link|safe }} ed avrà inizio il - {{ corso.data_inizio|date:"SHORT_DATETIME_FORMAT" }}, al seguente indirizzo: - {{ corso.locazione }} (vedi in mappa).

    +{% if corso.tipo == Corso.BASE %} +

    ti scriviamo in riferimento alla tua richiesta di diventare volontario della Croce Rossa Italiana fatta attraverso Gaia.

    +{% endif %} + +

    Nelle tue vicinanze è stato attivato un corso {%if corso.tipo == Corso.BASE%}base per Aspiranti{%else%}per Volontari{%endif%} CRI!

    +

    Il corso è stato organizzato dal {{ corso.sede.link|safe }} ed avrà inizio il {{ corso.data_inizio|date:"SHORT_DATETIME_FORMAT" }}, + al seguente indirizzo: {{ corso.locazione }} (vedi in mappa).

    Alcune informazioni su questo corso:

    {{ corso.descrizione|safe }}
    diff --git a/formazione/templates/email_docente_assegnato_a_corso.html b/formazione/templates/email_docente_assegnato_a_corso.html new file mode 100644 index 000000000..2c0e8f24f --- /dev/null +++ b/formazione/templates/email_docente_assegnato_a_corso.html @@ -0,0 +1,8 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Ciao {{ persona.nome }}, sei stat{{persona.genere_o_a}} inserito come insegnate del {{ corso.nome }}.

    +

    Clicca qui per aprire la pagina del corso.

    +{% endblock %} + +{% block footer %} {% endblock %} \ No newline at end of file diff --git a/formazione/templates/formazione.html b/formazione/templates/formazione.html index f0e2a428c..7aad13592 100644 --- a/formazione/templates/formazione.html +++ b/formazione/templates/formazione.html @@ -1,30 +1,28 @@ {% extends 'formazione_vuota.html' %} {% load bootstrap3 %} +{% load formazione_templatetags %} -{% block pagina_titolo %} - Formazione -{% endblock %} +{% block pagina_titolo %}Formazione{% endblock %} {% block app_contenuto %} + -

    - Formazione -

    - -

    - Emblema CRI -

    - +

    Formazione

    +

    Emblema CRI

    Benvenuto nella sezione dedicata alla Formazione CRI.

    -

    Usa il menù sulla destra che ti permetterà di accedere alle funzioni - relative alla gestione dei Corsi Base e dei Corsi di Formazione CRI.

    +

    Usa il menù sulla destra che ti permetterà di accedere alle funzioni relative alla gestione dei Corsi Base e dei Corsi di Formazione CRI.

    -
    + {% if corsi %} +

    {{ corsi.count }}

    Corsi Gestiti

    @@ -43,9 +41,70 @@

    {{ sedi.count }}

    {% endfor %}
    -
    + + + + + + + + {% for corso in corsi %} + + + -{% endblock %} \ No newline at end of file + + + + + + {% empty %} + + + + {% endfor %} +
    Stato
    {% corsi_filter %}
    Corso e SedeLuogo e dataIscritti
    {{ corso.get_stato_display }} + {{ corso.link|safe }}
    + {{ corso.sede.link|safe }} + {% if puo_pianificare %} +
    {{ corso.deleghe.count }} direttori + {% endif %} +
    + + {% if corso.locazione %} + {{ corso.locazione }} + {% else %} + (Nessun indirizzo specificato) + {% endif %} +
    + + Inizia: {{ corso.data_inizio }} +
    + + Esami: {{ corso.data_esame }} +
    + {{ corso.partecipazioni_confermate_o_in_attesa.count }} richieste +
    + + {{ corso.partecipazioni_confermate.count }} confermate
    + {{ corso.partecipazioni_in_attesa.count }} in attesa
    + {{ corso.partecipazioni_negate.count }} neg./rit.
    +
    +
    +

    Ancora nessun corso pianificato.

    +

    Puoi controllare la domanda formativa della zona e valutare l'attivazione di un nuovo corso.

    +
    + {% endif %} + + {% if puo_pianificare %} +
    +

    Vuoi pianificare un nuovo corso?

    +

    Se vuoi pianificare un nuovo corso, clicca su Attiva un Nuovo Corso.

    +

    Potrai assegnare un Direttore del Corso che si occuperà di organizzarne i particolari.

    +
    + {% endif %} + +{% endblock %} diff --git a/formazione/templates/formazione_albo_elenco_generico.html b/formazione/templates/formazione_albo_elenco_generico.html new file mode 100644 index 000000000..5e452f150 --- /dev/null +++ b/formazione/templates/formazione_albo_elenco_generico.html @@ -0,0 +1,11 @@ +{% extends 'formazione_vuota.html' %} + +{% load bootstrap3 %} +{% load utils %} + +{% block pagina_titolo %}{{ elenco_nome }}{% endblock %} +{% block app_contenuto %} +

    {{ elenco_nome }}


    + {% elenco elenco %} + +{% endblock %} diff --git a/formazione/templates/formazione_albo_inc_elenchi_persone_titoli.html b/formazione/templates/formazione_albo_inc_elenchi_persone_titoli.html new file mode 100644 index 000000000..1ba2806ec --- /dev/null +++ b/formazione/templates/formazione_albo_inc_elenchi_persone_titoli.html @@ -0,0 +1,49 @@ +{% extends 'us_elenchi_inc_vuoto.html' %} +{% load formazione_templatetags %} + + +{% block elenco_intestazione %} + Cognome Nome + Comitato di app. + Titolo/Scadenza + {% block elenco_intestazione_extra %}{% endblock %} +{% endblock %} + + +{% block elenco_riga %} + {{ persona.cognome }} {{ persona.nome }} + {{ persona.sede_riferimento }} + + {% titoli_del_corso persona cleaned_data as titoli %} + + {% for title in titoli.lista %} + {{ title.titolo|truncatechars:80 }} - + + {{ title.data_scadenza|date:"d/m/Y" }} +
    + {% empty %} + {% if not titoli.num_of_titles %} +

    0 risultati

    + {% endif %} + {% endfor %} + + Vedi elenco completo + + + {% block elenco_riga_extra %}{% endblock %} +{% endblock %} + + +{% block elenco_riga_azioni %} + {% load utils %} + {% permessi_almeno persona "modifica" as puo_modificare %} + {% permessi_almeno persona "lettura" as puo_leggere %} + + {% if puo_leggere %} + Scheda + {% endif %} + + Msg. + + {% block elenco_riga_azioni_extra %}{% endblock %} +{% endblock %} diff --git a/formazione/templates/formazione_albo_inc_elenco_sede.html b/formazione/templates/formazione_albo_inc_elenco_sede.html new file mode 100644 index 000000000..0b4790e3e --- /dev/null +++ b/formazione/templates/formazione_albo_inc_elenco_sede.html @@ -0,0 +1,69 @@ +{% load bootstrap3 %} +{% load utils %} +{% load mptt_tags %} + +

    {{ elenco_nome }}

    +
    + +
    +

    Seleziona Sede per {{ elenco_nome }}

    +

    Ci risulta che tu abbia i permessi per richiedere l'elenco dei soci delle seguenti Sedi e unità territoriali. + Seleziona le sedi che vuoi includere nell'elenco da generare, poi clicca su 'Genera Elenco'.

    +
    + +
    + {% csrf_token %} +
    + + +
    + + + + {% livello_max sedi as max %} + {% for i in max|volte %}{% endfor %} + + + + + + + {% for sede in sedi %} + + {% for i in sede.level|volte %} + + {% endfor %} + + + + {% differenza max sede.level 1 as diff %} + + + + {% endfor %} + +
     SedeAzioni
      + + + + + Sedi sottostanti: +
    + + +
    +
    +
    + +
    + +
    +
    +
    diff --git a/formazione/templates/formazione_albo_informatizzato.html b/formazione/templates/formazione_albo_informatizzato.html new file mode 100644 index 000000000..5cc0122c2 --- /dev/null +++ b/formazione/templates/formazione_albo_informatizzato.html @@ -0,0 +1,10 @@ +{% extends 'formazione_vuota.html' %} + +{% load bootstrap3 %} +{% load utils %} + +{% block pagina_titolo %}{{ elenco_nome }}{% endblock %} + +{% block app_contenuto %} + {% include "formazione_albo_inc_elenco_sede.html" %} +{% endblock %} diff --git a/formazione/templates/formazione_albo_titoli_corso_full_list.html b/formazione/templates/formazione_albo_titoli_corso_full_list.html new file mode 100644 index 000000000..57ff788bd --- /dev/null +++ b/formazione/templates/formazione_albo_titoli_corso_full_list.html @@ -0,0 +1,29 @@ +{% extends 'formazione_vuota.html' %} +{% load bootstrap3 %} +{% load utils %} + +{% block pagina_titolo %}{{ elenco_nome }}{% endblock %} + +{% block app_contenuto %} +

    Elenco dei titoli del Corso per {{ person }}

    + + + + + + + + {% for title in titles %} + + + + + + + {% empty %} + + + + {% endfor %} +
    TitoloComitato riferimentoCreazioneData Scadenza
    {{ title }}{{ title.persona.sede_riferimento.comitato }}{{ title.creazione|date:"d/m/Y" }}{{ title.data_scadenza|date:"d/m/Y" }}
    Dati non trovati.
    +{% endblock %} diff --git a/formazione/templates/formazione_corsi_base_direttori.html b/formazione/templates/formazione_corsi_base_direttori.html index 3c756330b..0601a5d8b 100644 --- a/formazione/templates/formazione_corsi_base_direttori.html +++ b/formazione/templates/formazione_corsi_base_direttori.html @@ -1,36 +1,29 @@ -{% extends 'formazione_vuota.html' %} +{% extends 'aspirante_corso_base_scheda.html' %} {% load bootstrap3 %} {% load mptt_tags %} {% load utils %} -{% block pagina_titolo %} - Seleziona Direttore/i Corso Base -{% endblock %} - -{% block app_contenuto %} - -

    - Seleziona Direttore/i Corso Base -

    - +{% block scheda_titolo %}Seleziona Direttore/i Corso{% endblock %} +{% block scheda_contenuto %} +

    Seleziona Direttore/i Corso

    Chi è il direttore?

    • Punto di riferimento per gli aspiranti volontari che vogliono partecipare - al corso base e per i docenti;
    • + al corso e per i docenti;
    • I suoi contatti verranno divulgati agli aspiranti volontari interessati al corso;
    • Generalmente è presente durante le lezioni e conosce i docenti;
    • - Potrà accettare o negare le iscrizioni al Corso Base.
    • + Potrà accettare o negare le iscrizioni al Corso.
    - {% delegati delega corso continua_url=continua_url almeno=1 %} - - + {# template-tag "delegati" si trova nell'app "anagrafica" #} + {# la view che gestisce selezione direttore: anagrafica.strumenti_delegati #} + {% delegati delega corso continua_url=continua_url almeno=1 %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/formazione_corsi_base_domanda.html b/formazione/templates/formazione_corsi_base_domanda.html index 1ac050a21..155d55972 100644 --- a/formazione/templates/formazione_corsi_base_domanda.html +++ b/formazione/templates/formazione_corsi_base_domanda.html @@ -3,15 +3,10 @@ {% load bootstrap3 %} {% load mptt_tags %} -{% block pagina_titolo %} - Domanda Formativa per Corsi Base -{% endblock %} +{% block pagina_titolo %}Domanda Formativa per Corsi{% endblock %} {% block app_contenuto %} - -

    - Domanda Formativa per Corsi Base -

    +

    Domanda Formativa per Corsi

    Cosa è la domanda formativa?

    @@ -102,7 +97,7 @@

    Cosa è la domanda formativa?

    Vuoi pianificare un nuovo corso?

    Se vuoi pianificare un nuovo corso base, clicca su - Pianifica un Nuovo Corso.

    + Pianifica un Nuovo Corso.

    Potrai assegnare un Direttore del Corso che si occuperà di organizzarne i particolari.

    diff --git a/formazione/templates/formazione_corsi_base_elenco.html b/formazione/templates/formazione_corsi_base_elenco.html index 1b1c9328d..4b67c96f2 100644 --- a/formazione/templates/formazione_corsi_base_elenco.html +++ b/formazione/templates/formazione_corsi_base_elenco.html @@ -3,15 +3,10 @@ {% load bootstrap3 %} {% load mptt_tags %} -{% block pagina_titolo %} - Corsi Base -{% endblock %} +{% block pagina_titolo %}Corsi{% endblock %} {% block app_contenuto %} - -

    - Corsi Base -

    +

    Corsi

    @@ -22,11 +17,8 @@

    {% for corso in corsi %} - - - - {% empty %} @@ -76,24 +64,20 @@

    - {% endfor %} -
    {{ corso.get_stato_display }} {{ corso.link|safe }}
    @@ -57,18 +49,14 @@

    - {{ corso.partecipazioni_confermate_o_in_attesa.count }} richieste -
    +
    - {{ corso.partecipazioni_confermate.count }} confermate
    - {{ corso.partecipazioni_in_attesa.count }} in attesa
    - {{ corso.partecipazioni_negate.count }} neg./rit.
    - - + {{ corso.partecipazioni_confermate.count }} confermate
    + {{ corso.partecipazioni_in_attesa.count }} in attesa
    + {{ corso.partecipazioni_negate.count }} neg./rit.

    Ancora nessun corso pianificato.

    Puoi controllare la domanda formativa della zona e valutare l'attivazione di un - nuovo corso base.

    + nuovo corso.

    - {% if puo_pianificare %}

    Vuoi pianificare un nuovo corso?

    -

    Se vuoi pianificare un nuovo corso base, clicca su - Pianifica un Nuovo Corso.

    +

    Se vuoi pianificare un nuovo corso, clicca su + Pianifica un Nuovo Corso.

    Potrai assegnare un Direttore del Corso che si occuperà di organizzarne i particolari.

    {% endif %} - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/formazione_corsi_base_fine.html b/formazione/templates/formazione_corsi_base_fine.html index 313fd427a..c0dc0a514 100644 --- a/formazione/templates/formazione_corsi_base_fine.html +++ b/formazione/templates/formazione_corsi_base_fine.html @@ -4,17 +4,10 @@ {% load mptt_tags %} {% load utils %} -{% block pagina_titolo %} - Seleziona Direttore/i Corso Base -{% endblock %} +{% block pagina_titolo %}Seleziona Direttore/i Corso{% endblock %} {% block app_contenuto %} - -

    - - Corso Base pianificato! -

    - +

    Corso pianificato!

    Che succederà ora?

      @@ -29,30 +22,18 @@

      Che succederà ora?

      • Lezioni;
      • Luogo di svolgimento del corso;
      • -
      • Informazioni per gli aspiranti volontari.
      • +
      • Informazioni per {% if not corso.is_nuovo_corso %}gli aspiranti{%else%}i{%endif%} volontari.

      -
    • Se preferisci, puoi - inserire tu stesso i dettagli mancanti del corso cliccando sul link sotto.
    • +
    • Se preferisci, puoi inserire tu stesso i dettagli mancanti del corso cliccando sul link sotto.

    - - - Torna all'elenco dei Corsi Base - + Torna all'elenco dei Corsi

    -

    - - - Aggiungi dettagli al nuovo corso - + Aggiungi dettagli al nuovo corso

    - - - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/formazione_corsi_base_nuovo.html b/formazione/templates/formazione_corsi_base_nuovo.html index 24cb73d31..c6cebd7db 100644 --- a/formazione/templates/formazione_corsi_base_nuovo.html +++ b/formazione/templates/formazione_corsi_base_nuovo.html @@ -3,66 +3,192 @@ {% load bootstrap3 %} {% load mptt_tags %} -{% block pagina_titolo %} - Pianifica un Corso Base -{% endblock %} +{% block pagina_titolo %}Pianifica un corso{% endblock %} {% block app_contenuto %} -
    - -
    - -
    -
    -

    - - Pianifica un Corso Base -

    +
    +
    +
    +

    Attivazione Corso

    - +
    +

    Dettagli corso ed iscrizioni

    +
    +
    +

    Lezioni

    +
    +
    +

    Esami e verbale

    +
    +
    +

    Questionario

    +
    +
    + + + -
    - -
    -
    - Data ed ora di inizio: - Ti consigliamo di inserire in questo campo la data e l'ora nella quale sarà - effettuata la presentazione del Corso. Questa data sarà comunicata - agli aspiranti nella tua zona come data di inziio del Corso. -
    - -
    - Nota bene: - Dopo questo step potrai nominare uno o più direttori del corso. - Questi potranno inserire gli ulteriori dettagli sul corso, - come le date delle lezioni, la descrizione, nonché gestire - interamente il corso per te. -
    - - -
    - - + + + + + + + + + + + + + + + + + + + + + + +
    + - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/formazione/templates/formazione_corso_position_change.html b/formazione/templates/formazione_corso_position_change.html new file mode 100644 index 000000000..db7879485 --- /dev/null +++ b/formazione/templates/formazione_corso_position_change.html @@ -0,0 +1,11 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} + +{% block scheda_titolo %}Modifica locazione del Corso{% endblock %} +{% block scheda_contenuto %} +
    +

    Scegli l'indirizzo di svolgimento del Corso

    +
    {% localizzatore corso solo_italia=1 %}
    +
    +{% endblock %} diff --git a/formazione/templates/formazione_elenchi_inc_iscritti.html b/formazione/templates/formazione_elenchi_inc_iscritti.html index 33e07d458..19dc7e743 100644 --- a/formazione/templates/formazione_elenchi_inc_iscritti.html +++ b/formazione/templates/formazione_elenchi_inc_iscritti.html @@ -1,11 +1,18 @@ {% extends 'us_elenchi_inc_persone.html' %} {% block elenco_intestazione_extra %} - Iscrizione + + Stato {% endblock %} {% block elenco_riga_extra %} - {% if not persona.aspirante or elenco.args.0.0.pk not in persona.aspirante.inviti_attivi %}Iscritto{% else %}Invitato{% endif %} + + {% if not persona.aspirante or elenco.args.0.0.pk not in persona.aspirante.inviti_attivi %} + Iscritto + {% else %} + Invitato + {% endif %} + {% endblock %} {% block elenco_riga_azioni %} @@ -14,7 +21,7 @@ {% permessi_almeno persona "modifica" as puo_modificare %} {% permessi_almeno persona "lettura" as puo_leggere %} - + Cancella {% endblock %} diff --git a/formazione/templates/formazione_vuota.html b/formazione/templates/formazione_vuota.html index da0564a51..85816f193 100644 --- a/formazione/templates/formazione_vuota.html +++ b/formazione/templates/formazione_vuota.html @@ -2,38 +2,24 @@ {% load bootstrap3 %} -{% block pagina_titolo %} - Benvenuto in Gaia -{% endblock %} - +{% block pagina_titolo %}Benvenuto in Gaia{% endblock %} {% block menu_laterale %}
    - - -
    {% endblock %} diff --git a/formazione/templates/pdf_corso_base_attestato.html b/formazione/templates/pdf_corso_base_attestato.html index 918e7ed62..5a71db558 100644 --- a/formazione/templates/pdf_corso_base_attestato.html +++ b/formazione/templates/pdf_corso_base_attestato.html @@ -3,104 +3,47 @@ {% load static %} {% block corpo %} + +
    +
    + +
    Comitato Nazionale
    +
    +
    +

    Attestato di
    partecipazione

    +

    {{ persona.nome_completo }}

    +
    +
    + {% if corso.titolo_cri %} +

    Nome del corso {{ corso.titolo_cri }}

    + {% endif %} +

    Data inizio {{ corso.data_inizio|date:"DATE_FORMAT" }}

    +

    Data fine {{ corso.data_esame|date:"DATE_FORMAT" }}

    +

    Sede di svolgimento {{ corso.sede.nome }}

    +

    Presidente {{ corso.sede.presidente.nome_completo }}

    -
    - -

    - -
    - - {{corso.sede.comitato}} - < -

    -

    -

    -

    -
    -

    -

    - - ATTESTATO - -

    -

    -

    -

    - - Visto il verbale della Commissione d’Esame - -

    -

    - - In data - - - {{ corso.data_esame|date:"DATE_FORMAT" }} - - - si attesta che - -

    -

    -
    -

    -

    - - {{ persona.nome_completo }} - -

    -

    -
    -

    -

    - C.F. {{ persona.codice_fiscale }} -

    -

    -
    -

    -

    - - Avendo sostenuto con profitto l’esame finale del - -

    -

    - - Corso di Formazione per Volontari della Croce Rossa Italiana, - -

    -

    - - ai sensi dell’ O.C. 0592/11 del 7 dicembre 2011 della Croce Rossa Italiana è - -

    -

    -
    -

    -

    - - Volontari{{ persona.genere_o_a }} della C.R.I. - -

    -

    -

    -

    -
    -

    - - - - - -
    - Il Direttore del Corso - - Il Presidente del Comitato -
    -
    -

    - - {{ corso.locazione.comune }}, li {{ corso.data_esame }} - -

    -
    -{% endblock %} \ No newline at end of file +

    + {% if corso.direttori_corsi.length > 1 %}Direttori del corso{% else %}Direttore del corso{% endif %} + + {% for persona in corso.direttori_corso %} + {{ persona.nome_completo }}
    + {% endfor %} +
    +

    +

    Codice identificativo: {{ corso.nome }}

    + + + + + +
    +
    +{% endblock %} diff --git a/formazione/templatetags/__init__.py b/formazione/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/formazione/templatetags/formazione_templatetags.py b/formazione/templatetags/formazione_templatetags.py new file mode 100644 index 000000000..563b9ddd2 --- /dev/null +++ b/formazione/templatetags/formazione_templatetags.py @@ -0,0 +1,44 @@ +from django import template +from django.utils.safestring import mark_safe + +from base.utils import oggi + + +register = template.Library() + +@register.simple_tag +def titoli_del_corso(persona, cd): + init_query = persona.titoli_personali_confermati().filter(is_course_title=True) + lista = init_query.filter(titolo__in=cd['titoli']) + + if cd.get('show_only_active', False): + lista = lista.filter(data_scadenza__gte=oggi()) + + return { + 'lista': lista.order_by('-data_scadenza'), + 'num_of_titles': init_query.count() + } + + +@register.simple_tag +def lezione_esonero(lezione, partecipante): + from ..models import AssenzaCorsoBase + + try: + a = AssenzaCorsoBase.objects.get(lezione=lezione, persona=partecipante) + return a if a.is_esonero else None + except AssenzaCorsoBase.DoesNotExist: + return None + + +@register.simple_tag +def lezione_partecipante_pk_shortcut(lezione, partecipante): + return "%s-%s" % (lezione.pk, partecipante.pk) + + +@register.simple_tag +def corsi_filter(): + from ..models import Corso + + href = """%s""" + return mark_safe(' '.join([href % (i[0], i[1]) for i in Corso.STATO])) diff --git a/formazione/test_courses.py b/formazione/test_courses.py new file mode 100644 index 000000000..8918db0c0 --- /dev/null +++ b/formazione/test_courses.py @@ -0,0 +1,220 @@ +from datetime import datetime, timedelta +from django.test import TestCase +from django.core.urlresolvers import reverse +from base.utils_tests import (crea_persona, crea_utenza, codice_fiscale, + email_fittizzia, crea_persona_sede_appartenenza, crea_appartenenza, + crea_locazione) +from anagrafica.models import * +from formazione.models import * + + +def create_persona(with_utenza=True): + persona = crea_persona() + persona.email_contatto = email_fittizzia() + persona.codice_fiscale = codice_fiscale() + persona.save() + + if with_utenza: + utenza = crea_utenza(persona, persona.email_contatto) + return persona, utenza + else: + return persona + + +def create_aspirante(sede, persona=None): + if not persona: + persona = create_persona()[0] + + a = Aspirante(persona=persona) + a.locazione = sede.locazione + a.save() + return a + + +def create_course(data_inizio, sede, extension_type=CorsoBase.EXT_MIA_SEDE, + tipo=Corso.CORSO_NUOVO, **kwargs): + + corso = CorsoBase.nuovo(tipo=tipo, extension_type=extension_type, sede=sede, + data_inizio=data_inizio, data_esame=data_inizio + timedelta(days=14), + stato=Corso.ATTIVO, **kwargs) + corso.locazione = sede.locazione + corso.save() + return corso + +def create_delega(): + """ required for course directors """ + return + + +def create_volunteer(): + # create appartenenza + return + + +def create_extension(): + return + + +def create_extensions_for_course(course): + return + + +class TestCorsoNuovo(TestCase): + def setUp(self): + """ Create users """ + ### Presidente ### + self.presidente, self.presidente_utenza = create_persona() + self.direttore, self.sede, self.appartenenza = crea_persona_sede_appartenenza( + presidente=self.presidente) + + ### Aspirante ### + self.aspirante1 = create_aspirante(sede=self.sede) + self.aspirante2 = create_aspirante(sede=self.sede) + + ### Volontario ### + self.volontario = None + + """ Create a new courses tipo CORSO_NUOVO """ + data_inizio = datetime.datetime.now() + timedelta(days=14) + self.c1 = create_course(data_inizio, self.sede) # corso_1_ext_mia_sede + self.c2 = create_course(data_inizio, self.sede, extension_type=CorsoBase.EXT_LVL_REGIONALE) # corso_2_ext_a_livello_regionale + self.c3 = create_course(data_inizio, self.sede, tipo=Corso.BASE) + + """ Create titles """ + + """ Create extensions """ + + def _login_as(self, email, password='prova'): + self.client.login(username=email, password=password) + + def test_corso_nuovo_invisible_to_aspirante(self): + email = self.aspirante1.persona.email_contatto + login = self._login_as(self.aspirante1.persona.email_contatto) + response = self.client.get(reverse('aspirante:corsi_base')) + ctx = response.context + + ### Asserting ### + self.assertEqual(str(ctx['user']), email) # user is logged in + self.assertEqual(response.status_code, 200) + self.assertTrue('corsi' in ctx) + self.assertFalse(self.c1 in ctx['corsi']) # Nuovo excluded + self.assertFalse(self.c2 in ctx['corsi']) # Nuovo excluded + self.assertTrue(self.c3 in ctx['corsi']) # CorsoBase in list of courses + + def test_aspirante_have_access_to_corso_base(self): + c3 = self.c3 + login = self._login_as(self.aspirante1.persona.email_contatto) + response = self.client.get(reverse('aspirante:info', args=[c3.pk])) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, c3.nome, status_code=200) + + def test_aspirante_no_access_to_corso_nuovo(self): + login = self._login_as(self.aspirante1.persona.email_contatto) + + response = self.client.get(reverse('aspirante:info', args=[self.c1.pk])) + self.assertEqual(response.status_code, 302) + + response = self.client.get(reverse('aspirante:info', args=[self.c2.pk])) + self.assertEqual(response.status_code, 302) + + def test_corso_nuovo_visible_to_volunteer(self): + pass + + def test_corso_nuovo_extensions_link_visible_only_for_corso_nuovo(self): + pass + + def test_volunteer_has_required_titles(self): + pass + """ + has all titles + not all titles + """ + + def test_volunteer_available_courses_listing(self): + pass + + def test_volunteer_can_participate_at_course(self): + pass + + def test_corso_nuovo_new_fields_on_modify_page(self): + pass + + def test_corso_nuovo_fields_visible_only_for_corso_nuovo(self): + pass + + def test_corso_nuovo_extensions(self): + pass + """ + 1) CAN list course: [user titles == corso ext titles (all required)] + [ + user sede in corso ext sede] + 2) CAN list course: corso has only sede without titles, user sede in + corso sede + """ + + def test_corso_nuovo_extensions_sede(self): + pass + """ + user app. sede in course extensions sede + """ + + def test_corso_nuovo_extensions_sede_expanded(self): + pass + """ + - user appartenenza sede is in courses' extensions expanded sede + - 1st extension has sedi_sottostanti, 2nd extension not, user's sede + is in one of them. + - user app. sede is not in course extensions sede (corso non visible) + """ + + def test_volunteer_listing_course_found_by_firmatario_sede(self): + pass + """ + if CorsoBase.extension_type = MIA_SEDE + user app sede == firmatario_sede + """ + + def test_method_corsobase_has_extensions(self): + # self.assertTrue(c2.has_extensions()) + pass + + def test_method_persona_has_required_titles_for_course(self): + pass + + def test_method_corsobase_get_extensions(self): + pass + + def test_method_corsobase_get_extensions_sede(self): + pass + + def test_method_corsobase_get_extensions_titles(self): + pass + + def test_method_corsobase_get_volunteers_by__(self): + pass + """ + c.get_extensions_titles() + c.get_volunteers_by_course_requirements() + c.get_volunteers_by_only_sede() + c.get_volunteers_by_ext_sede() + c.get_volunteers_by_ext_titles() + """ + + def test_course_activated_volunteers_informed(self): + pass + + def test_property_is_corso_nuovo(self): + self.assertTrue(self.c1.is_nuovo_corso) + self.assertTrue(self.c2.is_nuovo_corso) + self.assertFalse(self.c3.is_nuovo_corso) + + def test_field_extension_type(self): + self.assertEqual(self.c1.extension_type, CorsoBase.EXT_MIA_SEDE) + self.assertEqual(self.c2.extension_type, CorsoBase.EXT_LVL_REGIONALE) + self.assertEqual(self.c3.extension_type, CorsoBase.EXT_MIA_SEDE) + + def test_termina_corso_nuovo(self): + pass + + def test_termina_corso_titolo_cri_is_set_to_volunteers(self): + pass diff --git a/formazione/tests.py b/formazione/tests.py index 684b12147..a92f36372 100644 --- a/formazione/tests.py +++ b/formazione/tests.py @@ -242,30 +242,30 @@ def test_cancellazione_partecipante(self): # Test del controllo di cancellazione nei 4 stati del corso corso.stato = CorsoBase.TERMINATO corso.save() - response = self.client.get(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.get(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "stadio della vita del corso base") corso.stato = CorsoBase.ANNULLATO corso.save() - response = self.client.get(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.get(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "stadio della vita del corso base") corso.stato = CorsoBase.PREPARAZIONE corso.save() - response = self.client.get(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.get(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "Conferma cancellazione") corso.stato = CorsoBase.ATTIVO corso.save() - response = self.client.get(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.get(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "Conferma cancellazione") # GET chiede conferma - response = self.client.get(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.get(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "Conferma cancellazione") self.assertContains(response, force_text(sostenitore)) # POST cancella - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, sostenitore.pk))) + response = self.client.post(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, sostenitore.pk))) self.assertContains(response, "Iscritto cancellato") self.assertEqual(corso.partecipazioni_confermate_o_in_attesa().count(), 2) @@ -285,12 +285,12 @@ def test_cancellazione_partecipante(self): mail.outbox = [] # Cancellare utente non esistente ritorna errore - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, altro.pk + 10000))) + response = self.client.post(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, altro.pk + 10000))) self.assertContains(response, "La persona cercata non è iscritta") # Cancellare utente non associato al corso non ritorna errore -per evitare information leak- ma non cambia # i dati - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, altro.pk))) + response = self.client.post(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, altro.pk))) self.assertContains(response, "Iscritto cancellato") self.assertEqual(corso.partecipazioni_confermate_o_in_attesa().count(), 2) @@ -299,7 +299,7 @@ def test_cancellazione_partecipante(self): self.assertEqual(len(mail.outbox), 0) # Cancellare invitato confermato - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, aspirante1.pk))) + response = self.client.post(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, aspirante1.pk))) self.assertContains(response, "Iscritto cancellato") self.assertEqual(corso.partecipazioni_confermate_o_in_attesa().count(), 1) @@ -319,7 +319,7 @@ def test_cancellazione_partecipante(self): mail.outbox = [] # Cancellare invitato in attesa - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, aspirante2.pk))) + response = self.client.post(reverse('aspirante:formazione_iscritti_cancella', args=(corso.pk, aspirante2.pk))) self.assertContains(response, "Iscritto cancellato") self.assertEqual(corso.partecipazioni_confermate_o_in_attesa().count(), 1) @@ -333,7 +333,9 @@ def test_cancellazione_partecipante(self): mail.outbox = [] # Cancellare partecipante in attesa - response = self.client.post(reverse('formazione-iscritti-cancella', args=(corso.pk, aspirante3.pk))) + response = self.client.post(reverse( + 'aspirante:formazione_iscritti_cancella', args=(corso.pk, + aspirante3.pk))) self.assertContains(response, "Iscritto cancellato") self.assertEqual(corso.partecipazioni_confermate_o_in_attesa().count(), 0) diff --git a/formazione/urls.py b/formazione/urls.py new file mode 100644 index 000000000..ff6026e57 --- /dev/null +++ b/formazione/urls.py @@ -0,0 +1,21 @@ +from django.conf.urls import url +from .viste import (formazione, formazione_corsi_base_elenco, + formazione_corsi_base_domanda, formazione_corsi_base_nuovo, + formazione_corsi_base_direttori, formazione_corsi_base_fine, + formazione_albo_informatizzato, formazione_albo_titoli_corso_full_list, +) + + +app_label = 'formazione' +urlpatterns = [ + url(r'^$', formazione, name='index'), + url(r'^corsi-base/domanda/$', formazione_corsi_base_domanda, name='domanda'), + url(r'^corsi-base/nuovo/$', formazione_corsi_base_nuovo, name='new_course'), + url(r'^corsi-base/elenco/$', formazione_corsi_base_elenco, name='list_courses'), + url(r'^corsi-base/(?P[0-9]+)/direttori/$', formazione_corsi_base_direttori, name='director'), + url(r'^corsi-base/(?P[0-9]+)/fine/$', formazione_corsi_base_fine, name='end'), + url(r'^albo-informatizzato/$', formazione_albo_informatizzato, name='albo_info'), + url(r'^albo-informatizzato/titoli-corso-di-persona/$', + formazione_albo_titoli_corso_full_list, + name='albo_titoli_corso_full_list'), +] diff --git a/formazione/urls_aspirante.py b/formazione/urls_aspirante.py new file mode 100644 index 000000000..b60e4341c --- /dev/null +++ b/formazione/urls_aspirante.py @@ -0,0 +1,53 @@ +from django.conf.urls import url +from . import viste as v + + +url_shortcut = 'corso-base/(?P[0-9]+)' +app_label = 'aspirante' +urlpatterns = [ + url(r'^$', v.aspirante_home, name='home'), + url(r'^impostazioni/$', v.aspirante_impostazioni, name='settings'), + url(r'^impostazioni/cancella/$', v.aspirante_impostazioni_cancella), + url(r'^sedi/$', v.aspirante_sedi, name='sedi'), + url(r'^corsi/$', v.aspirante_corsi, name='corsi_base'), + url(r'^%s/$' % url_shortcut, v.aspirante_corso_base_informazioni, + name='info'), + url(r'^%s/mappa/$' % url_shortcut, v.aspirante_corso_base_mappa, + name='map'), + url(r'^%s/position/change' % url_shortcut, v.formazione_corso_position_change, + name='position_change'), + url(r'^%s/firme/$' % url_shortcut, v.aspirante_corso_base_firme, + name='firme'), + url(r'^%s/ritirati/$' % url_shortcut, v.aspirante_corso_base_ritirati, + name='retired'), + url(r'^%s/report/$' % url_shortcut, v.aspirante_corso_base_report, + name='report'), + url(r'^%s/report/schede/$' % url_shortcut, + v.aspirante_corso_base_report_schede, + name='report_schede'), + url(r'^%s/modifica/$' % url_shortcut, v.aspirante_corso_base_modifica, + name='modify'), + url(r'^%s/attiva/$' % url_shortcut, v.aspirante_corso_base_attiva, + name='activate'), + url(r'^%s/termina/$' % url_shortcut, v.aspirante_corso_base_termina, + name='terminate'), + url(r'^%s/lezioni/$' % url_shortcut, v.aspirante_corso_base_lezioni, + name='lessons'), + url(r'^%s/lezioni/(?P[0-9]+)/cancella/$' % url_shortcut, + v.aspirante_corso_base_lezioni_cancella, + name='lessons_cancel'), + url(r'^%s/iscritti/$' % url_shortcut, v.aspirante_corso_base_iscritti, + name='subscribed'), + url(r'^%s/iscritti/aggiungi/$' % url_shortcut, + v.aspirante_corso_base_iscritti_aggiungi, + name='add_to_subscribed'), + url(r'^%s/iscritti/cancella/(?P[0-9]+)/$' % url_shortcut, + v.aspirante_corso_base_iscritti_cancella, + name='formazione_iscritti_cancella'), + url(r'^%s/iscriviti/$' % url_shortcut, v.aspirante_corso_base_iscriviti, + name='subscribe'), + url(r'^%s/estensioni/$' % url_shortcut, v.aspirante_corso_estensioni_modifica, + name='estensioni_modifica'), + url(r'^%s/informa/$' % url_shortcut, v.aspirante_corso_estensioni_informa, + name='informa'), +] diff --git a/formazione/urls_courses.py b/formazione/urls_courses.py new file mode 100644 index 000000000..6062531ca --- /dev/null +++ b/formazione/urls_courses.py @@ -0,0 +1,20 @@ +from django.conf.urls import url +from . import viste + +""" +La proposta è quella di spostare tutti gli url del modulo formazione +sotto unico prefisso: "courses//action?params" +""" + + +app_label = 'courses' +pk = "(?P[0-9]+)" + +urlpatterns = [ + url(r'^%s/questionnaire/send-to-participants/$' % pk, + viste.course_send_questionnaire_to_participants, + name='send_questionnaire_to_participants'), + url(r'^%s/relazione-direttore/$' % pk, + viste.corso_compila_relazione_direttore, + name='compila_relazione_direttore'), +] diff --git a/formazione/validators.py b/formazione/validators.py new file mode 100644 index 000000000..f87a56f22 --- /dev/null +++ b/formazione/validators.py @@ -0,0 +1,19 @@ +def validate_file_extension(value): + import os + from django.core.exceptions import ValidationError + + ext = os.path.splitext(value.name)[1] # [0] returns path+filename + valid_extensions = ['zip', 'rar', '.pdf', '.doc', '.docx', '.jpg', + '.png', '.xlsx', '.xls'] + if ext.lower() not in valid_extensions: + raise ValidationError("Estensione <%s> di questo file non è " + "accettabile." % ext) + + +def course_file_directory_path(instance, filename): + # file will be uploaded to MEDIA_ROOT/course/ + return 'courses/%s/%s' % (instance.id, filename) + + +def delibera_file_upload_path(instance, filename): + return 'courses/delibere/%s' % (filename,) diff --git a/formazione/viste.py b/formazione/viste.py index 3f844a0b3..07c78927e 100644 --- a/formazione/viste.py +++ b/formazione/viste.py @@ -1,84 +1,133 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.db.transaction import atomic +from django.db.models import Q +from django.utils import timezone from django.shortcuts import redirect, get_object_or_404 -from django.template import Context +from django.core.urlresolvers import reverse from django.template.loader import get_template +from django.contrib import messages -from anagrafica.models import Persona +from anagrafica.models import Persona, Documento, Sede +from anagrafica.forms import ModuloCreazioneDocumento from anagrafica.permessi.applicazioni import DIRETTORE_CORSO -from anagrafica.permessi.costanti import GESTIONE_CORSI_SEDE, GESTIONE_CORSO, ERRORE_PERMESSI, COMPLETO, MODIFICA +from anagrafica.permessi.costanti import (GESTIONE_CORSI_SEDE, + GESTIONE_CORSO, ERRORE_PERMESSI, COMPLETO, MODIFICA, RUBRICA_DELEGATI_OBIETTIVO_ALL) +from curriculum.models import TitoloPersonale +from ufficio_soci.elenchi import ElencoPerTitoliCorso from autenticazione.funzioni import pagina_privata, pagina_pubblica -from base.errori import ci_siamo_quasi, errore_generico, messaggio_generico -from base.files import Zip +from base.errori import errore_generico, messaggio_generico # ci_siamo_quasi from base.models import Log from base.utils import poco_fa -from formazione.elenchi import ElencoPartecipantiCorsiBase -from formazione.forms import ModuloCreazioneCorsoBase, ModuloModificaLezione, ModuloModificaCorsoBase, \ - ModuloIscrittiCorsoBaseAggiungi, ModuloVerbaleAspiranteCorsoBase -from formazione.models import CorsoBase, AssenzaCorsoBase, LezioneCorsoBase, PartecipazioneCorsoBase, Aspirante, \ - InvitoCorsoBase -from django.utils import timezone - from posta.models import Messaggio +from survey.models import Survey +from .elenchi import ElencoPartecipantiCorsiBase +from .decorators import can_access_to_course +from .models import (Aspirante, Corso, CorsoBase, CorsoEstensione, LezioneCorsoBase, + PartecipazioneCorsoBase, InvitoCorsoBase, RelazioneCorso) +from .forms import (ModuloCreazioneCorsoBase, ModuloModificaLezione, + ModuloModificaCorsoBase, ModuloIscrittiCorsoBaseAggiungi, + ModuloVerbaleAspiranteCorsoBase, FormRelazioneDelDirettoreCorso) +from .classes import GestionePresenza, GeneraReport @pagina_privata def formazione(request, me): - contesto = { + corsi = me.oggetti_permesso(GESTIONE_CORSO) + + # Filtra corsi by stato + if request.GET.get('stato'): + stato = request.GET.get('stato') + # Verifica che modello ha lo stato impostato in get-request + if stato in [i[0] for i in Corso.STATO]: + filtered = corsi.filter(stato=stato) + if not filtered.exists(): # queryset vuoto + # Rindirizza sulla pagina con tutti i corsi disponibili + return redirect('formazione:index') + corsi = filtered + + context = { + "corsi": corsi, "sedi": me.oggetti_permesso(GESTIONE_CORSI_SEDE), - "corsi": me.oggetti_permesso(GESTIONE_CORSO), + "puo_pianificare": me.ha_permesso(GESTIONE_CORSI_SEDE), } - return 'formazione.html', contesto + return 'formazione.html', context @pagina_privata def formazione_corsi_base_elenco(request, me): - contesto = { + context = { "corsi": me.oggetti_permesso(GESTIONE_CORSO), "puo_pianificare": me.ha_permesso(GESTIONE_CORSI_SEDE), } - return 'formazione_corsi_base_elenco.html', contesto + return 'formazione_corsi_base_elenco.html', context @pagina_privata def formazione_corsi_base_domanda(request, me): - contesto = { + context = { "sedi": me.oggetti_permesso(GESTIONE_CORSI_SEDE), "min_sedi": Aspirante.MINIMO_COMITATI, "max_km": Aspirante.MASSIMO_RAGGIO, } - return 'formazione_corsi_base_domanda.html', contesto + return 'formazione_corsi_base_domanda.html', context @pagina_privata def formazione_corsi_base_nuovo(request, me): - modulo = ModuloCreazioneCorsoBase(request.POST or None, initial={"data_inizio": - datetime.now() + timedelta(days=14)}) - modulo.fields['sede'].queryset = me.oggetti_permesso(GESTIONE_CORSI_SEDE) - - if modulo.is_valid(): - corso = CorsoBase.nuovo( - anno=modulo.cleaned_data['data_inizio'].year, - sede=modulo.cleaned_data['sede'], - data_inizio=modulo.cleaned_data['data_inizio'], - data_esame=modulo.cleaned_data['data_inizio'], + if not me.ha_permesso(GESTIONE_CORSI_SEDE): + return redirect(ERRORE_PERMESSI) + + now = datetime.now() + timedelta(days=14) + form = ModuloCreazioneCorsoBase( + request.POST or None, + request.FILES or None, + initial={'data_inizio': now, 'data_esame': now + timedelta(days=14)}, + me=me + ) + form.fields['sede'].queryset = me.oggetti_permesso(GESTIONE_CORSI_SEDE) + + if form.is_valid(): + kwargs = {} + cd = form.cleaned_data + tipo, data_inizio, data_esame = cd['tipo'], cd['data_inizio'], cd['data_esame'] + data_esame = data_esame if tipo == Corso.CORSO_NUOVO else data_inizio + + if tipo == Corso.CORSO_NUOVO: + kwargs['titolo_cri'] = cd['titolo_cri'] + kwargs['cdf_level'] = cd['level'] + kwargs['cdf_area'] = cd['area'] + kwargs['survey'] = Survey.survey_for_corso() + + course = CorsoBase.nuovo( + anno=data_inizio.year, + sede=cd['sede'], + data_inizio=data_inizio, + data_esame=data_esame, + tipo=tipo, + delibera_file=cd['delibera_file'], + **kwargs ) - if modulo.cleaned_data['locazione'] == modulo.PRESSO_SEDE: - corso.locazione = corso.sede.locazione - corso.save() + same_sede = cd['locazione'] == form.PRESSO_SEDE + if same_sede: + course.locazione = course.sede.locazione + course.save() - request.session['corso_base_creato'] = corso.pk + # Il corso è creato. Informa presidenza allegando delibera_file + course.inform_presidency_with_delibera_file() + request.session['corso_base_creato'] = course.pk - return redirect(corso.url_direttori) + if same_sede: + # Rindirizza sulla pagina: selezione direttori + return redirect(course.url_direttori) + else: + # Rindirizza sulla pagina: impostazione geolocalizzazione + return redirect(reverse('aspirante:position_change', args=[course.pk])) - contesto = { - "modulo": modulo + context = { + 'modulo': form, } - return 'formazione_corsi_base_nuovo.html', contesto + return 'formazione_corsi_base_nuovo.html', context @pagina_privata @@ -90,16 +139,16 @@ def formazione_corsi_base_direttori(request, me, pk): continua_url = corso.url if 'corso_base_creato' in request.session and int(request.session['corso_base_creato']) == int(pk): - continua_url = "/formazione/corsi-base/%d/fine/" % (int(pk),) + continua_url = "/formazione/corsi-base/%d/fine/" % int(pk) del request.session['corso_base_creato'] - contesto = { + context = { "delega": DIRETTORE_CORSO, "corso": corso, - "continua_url": continua_url + "continua_url": continua_url, + 'puo_modificare': me and me.permessi_almeno(corso, MODIFICA) } - - return 'formazione_corsi_base_direttori.html', contesto + return 'formazione_corsi_base_direttori.html', context @pagina_privata @@ -111,142 +160,216 @@ def formazione_corsi_base_fine(request, me, pk): if me in corso.delegati_attuali(): # Se sono direttore, continuo. redirect(corso.url) - contesto = { + context = { "corso": corso, } - return 'formazione_corsi_base_fine.html', contesto + return 'formazione_corsi_base_fine.html', context @pagina_pubblica +@can_access_to_course def aspirante_corso_base_informazioni(request, me=None, pk=None): - + context = dict() corso = get_object_or_404(CorsoBase, pk=pk) - puo_modificare = me and me.permessi_almeno(corso, MODIFICA) puoi_partecipare = corso.persona(me) if me else None - contesto = { - "corso": corso, - "puo_modificare": puo_modificare, - "puoi_partecipare": puoi_partecipare, - } - return 'aspirante_corso_base_scheda_informazioni.html', contesto + if corso.locazione is None: + # Il corso non ha una locazione (è stata selezionata la voce °Sede presso Altrove" + messages.error(request, "Imposta una locazione per procedere la navigazione del Corso.") + + # Rindirizzo utente sulla pagina di impostazione della locazione + return redirect(reverse('aspirante:position_change', args=[corso.pk])) + + if puoi_partecipare == CorsoBase.NON_HAI_CARICATO_DOCUMENTI_PERSONALI: + if request.method == 'POST': + doc = Documento(persona=me) + load_personal_document_form = ModuloCreazioneDocumento(request.POST, + request.FILES, instance=doc) + + if load_personal_document_form.is_valid(): + load_personal_document_form.save() + return redirect(reverse('aspirante:info', kwargs={'pk': corso.pk})) + else: + load_personal_document_form = ModuloCreazioneDocumento() + + context['load_personal_document'] = load_personal_document_form + + context['corso'] = corso + context['puo_modificare'] = corso.can_modify(me) + context['can_activate'] = corso.can_activate(me) + context['puoi_partecipare'] = puoi_partecipare + + return 'aspirante_corso_base_scheda_informazioni.html', context @pagina_privata def aspirante_corso_base_iscriviti(request, me=None, pk=None): - corso = get_object_or_404(CorsoBase, pk=pk) + puoi_partecipare = corso.persona(me) if not puoi_partecipare in corso.PUOI_ISCRIVERTI: - return errore_generico(request, me, titolo="Non puoi partecipare a questo corso", - messaggio="Siamo spiacenti, ma non sembra che tu possa partecipare " - "a questo corso per qualche motivo. ", - torna_titolo="Torna al corso", - torna_url=corso.url) + return errore_generico(request, me, + titolo="Non puoi partecipare a questo corso", + messaggio="Siamo spiacenti, ma non sembra che tu possa partecipare " + "a questo corso per qualche motivo.", + torna_titolo="Torna al corso", + torna_url=corso.url + ) + + if corso.is_reached_max_participants_limit: + # TODO: informa direttore + # send_mail() + + return errore_generico(request, me, + titolo="Non puoi partecipare a questo corso", + messaggio="È stato raggiunto il limite massimo di richieste di " + "partecipazione al corso.", + torna_titolo="Torna al corso", + torna_url=corso.url + ) p = PartecipazioneCorsoBase(persona=me, corso=corso) p.save() p.richiedi() - return messaggio_generico(request, me, titolo="Sei iscritto al corso base", - messaggio="Complimenti! Abbiamo inoltrato la tua richiesta al direttore " - "del corso, che ti contatterà appena possibile.", - torna_titolo="Torna al corso", - torna_url=corso.url) + + return messaggio_generico(request, me, + titolo="Sei iscritto al corso", + messaggio="Complimenti! Abbiamo inoltrato la tua richiesta al direttore " + "del corso, che ti contatterà appena possibile.", + torna_titolo="Torna al corso", + torna_url=corso.url + ) @pagina_privata def aspirante_corso_base_ritirati(request, me=None, pk=None): - corso = get_object_or_404(CorsoBase, pk=pk) - puoi_partecipare = corso.persona(me) - if not puoi_partecipare == corso.SEI_ISCRITTO_PUOI_RITIRARTI: - return errore_generico(request, me, titolo="Non puoi ritirarti da questo corso", - messaggio="Siamo spiacenti, ma non sembra che tu possa ritirarti " - "da questo corso per qualche motivo. ", - torna_titolo="Torna al corso", - torna_url=corso.url) + partecipazione = PartecipazioneCorsoBase.objects.none() - p = PartecipazioneCorsoBase.con_esito_pending(corso=corso, persona=me).first() - p.ritira() - - return messaggio_generico(request, me, titolo="Ti sei ritirato dal corso", - messaggio="Siamo spiacenti che hai deciso di ritirarti da questo corso. " - "La tua partecipazione è stata ritirata correttamente. " - "Non esitare a iscriverti a questo o un altro corso, nel caso cambiassi idea.", - torna_titolo="Torna alla pagina del corso", - torna_url=corso.url) + kwargs = dict(corso=corso, persona=me) + if corso.persona(me) == CorsoBase.SEI_ISCRITTO_CONFERMATO_PUOI_RITIRARTI: + partecipazione = PartecipazioneCorsoBase.con_esito_ok(**kwargs).last() + else: + partecipazione = PartecipazioneCorsoBase.con_esito_pending(**kwargs).first() + + if partecipazione: + # Caso: vuole ritirasi quando la richiesta non è stata ancora confermata + partecipazione.ritira() + + # Caso: vuole ritirasi quando la richiesta è stata confermata + if partecipazione.confermata: + partecipazione.confermata = False + partecipazione.save() # second save() call + + # Informa direttore corso + posta = Messaggio.costruisci_e_accoda( + oggetto="Ritiro richiesta di iscrizione a %s da %s" % (corso.nome, partecipazione.persona), + modello="email_corso_utente_ritirato_iscrizione.html", + corpo={ + 'corso': corso, + 'partecipante': partecipazione.persona, + }, + destinatari=corso.direttori_corso()) + + if posta: + messages.success(request, "Il direttore del corso è stato avvisato.") + + return messaggio_generico(request, me, titolo="Ti sei ritirato dal corso", + messaggio="Siamo spiacenti che hai deciso di ritirarti da questo corso. " + "La tua partecipazione è stata ritirata correttamente. " + "Non esitare a iscriverti a questo o un altro corso, nel caso cambiassi idea.", + torna_titolo="Torna alla pagina del corso", + torna_url=corso.url) + + return messaggio_generico(request, me, titolo="Non puoi ritirarti da questo corso", + messaggio="Siamo spiacenti, ma non sembra che tu possa ritirarti da questo corso per qualche motivo. ", + torna_titolo="Torna alla pagina del corso", + torna_url=corso.url) @pagina_privata +@can_access_to_course def aspirante_corso_base_mappa(request, me, pk): - corso = get_object_or_404(CorsoBase, pk=pk) puo_modificare = me.permessi_almeno(corso, MODIFICA) - contesto = { + context = { "corso": corso, "puo_modificare": puo_modificare } - return 'aspirante_corso_base_scheda_mappa.html', contesto + return 'aspirante_corso_base_scheda_mappa.html', context @pagina_privata def aspirante_corso_base_lezioni(request, me, pk): - corso = get_object_or_404(CorsoBase, pk=pk) if not me.permessi_almeno(corso, MODIFICA): return redirect(ERRORE_PERMESSI) partecipanti = Persona.objects.filter(partecipazioni_corsi__in=corso.partecipazioni_confermate()) lezioni = corso.lezioni.all() - moduli = [] - partecipanti_lezioni = [] + + moduli = list() + partecipanti_lezioni = list() + + AZIONE_SALVA = request.POST and request.POST['azione'] == 'salva' + AZIONE_NUOVA = request.POST and request.POST['azione'] == 'nuova' + + # Presenze/assenze for lezione in lezioni: - modulo = ModuloModificaLezione(request.POST if request.POST and request.POST['azione'] == 'salva' else None, - instance=lezione, - prefix="%s" % (lezione.pk,)) - if request.POST and request.POST['azione'] == 'salva' and modulo.is_valid(): - modulo.save() - - moduli += [modulo] - partecipanti_lezione = partecipanti.exclude(assenze_corsi_base__lezione=lezione).order_by('nome', 'cognome') - - if request.POST and request.POST['azione'] == 'salva': - for partecipante in partecipanti: - if ("%s" % (partecipante.pk,)) in request.POST.getlist('presenze-%s' % (lezione.pk,)): - # Se presente, rimuovi ogni assenza. - AssenzaCorsoBase.objects.filter(lezione=lezione, persona=partecipante).delete() - else: - # Assicurati che sia segnato come assente. - if not AssenzaCorsoBase.objects.filter(lezione=lezione, persona=partecipante).exists(): - a = AssenzaCorsoBase(lezione=lezione, persona=partecipante, registrata_da=me) - a.save() + form = ModuloModificaLezione(request.POST if AZIONE_SALVA else None, + instance=lezione, corso=corso, prefix="%s" % lezione.pk) + + if AZIONE_SALVA and form.is_valid(): + form.save() + + moduli += [form] + + # Excludi assenze con esonero + partecipanti_lezione = partecipanti.exclude( + Q(assenze_corsi_base__esonero=False), + assenze_corsi_base__lezione=lezione + ).order_by('nome', 'cognome') + + if AZIONE_SALVA: + gestione_presenze = GestionePresenza(request, lezione, me, partecipanti) partecipanti_lezioni += [partecipanti_lezione] - if request.POST and request.POST['azione'] == 'nuova': - modulo_nuova_lezione = ModuloModificaLezione(request.POST, prefix="nuova") - if modulo_nuova_lezione.is_valid(): - lezione = modulo_nuova_lezione.save(commit=False) + if AZIONE_NUOVA: + form_nuova_lezione = ModuloModificaLezione(request.POST, prefix="nuova", corso=corso) + if form_nuova_lezione.is_valid(): + lezione = form_nuova_lezione.save(commit=False) lezione.corso = corso lezione.save() - return redirect("%s#%d" % (corso.url_lezioni, lezione.pk,)) + + if corso.is_nuovo_corso: + # Informa docente della lezione + lezione.send_messagge_to_docente(me) + + return redirect("%s#%d" % (corso.url_lezioni, lezione.pk)) else: - modulo_nuova_lezione = ModuloModificaLezione(prefix="nuova", initial={ + form_nuova_lezione = ModuloModificaLezione(prefix="nuova", initial={ "inizio": timezone.now(), "fine": timezone.now() + timedelta(hours=2) - }) + }, corso=corso) + try: + if AZIONE_SALVA or AZIONE_NUOVA: + if not form.is_valid(): + messages.error(request, 'Verifica tutti i moduli sulla presenza degli errori.') + except: + pass lezioni = zip(lezioni, moduli, partecipanti_lezioni) - contesto = { + context = { "corso": corso, "puo_modificare": True, "lezioni": lezioni, "partecipanti": partecipanti, - "modulo_nuova_lezione": modulo_nuova_lezione, + "modulo_nuova_lezione": form_nuova_lezione, } - return 'aspirante_corso_base_scheda_lezioni.html', contesto + return 'aspirante_corso_base_scheda_lezioni.html', context @pagina_privata @@ -263,37 +386,93 @@ def aspirante_corso_base_lezioni_cancella(request, me, pk, lezione_pk): lezione.delete() return redirect(corso.url_lezioni) + @pagina_privata def aspirante_corso_base_modifica(request, me, pk): + from .models import CorsoFile, CorsoLink + from .formsets import CorsoFileFormSet, CorsoLinkFormSet - corso = get_object_or_404(CorsoBase, pk=pk) - if not me.permessi_almeno(corso, MODIFICA): - return redirect(ERRORE_PERMESSI) + course = get_object_or_404(CorsoBase, pk=pk) + course_files = CorsoFile.objects.filter(corso=course) + course_links = CorsoLink.objects.filter(corso=course) - modulo = ModuloModificaCorsoBase(request.POST or None, instance=corso) - if modulo.is_valid(): - modulo.save() + FILEFORM_PREFIX = 'files' + LINKFORM_PREFIX = 'links' - contesto = { - "corso": corso, - "puo_modificare": True, - "modulo": modulo, + if not me.permessi_almeno(course, MODIFICA): + return redirect(ERRORE_PERMESSI) + + if request.method == 'POST': + course_form = ModuloModificaCorsoBase(request.POST, instance=course) + file_formset = CorsoFileFormSet(request.POST, request.FILES, + queryset=course_files, + form_kwargs={'empty_permitted': False}, + prefix=FILEFORM_PREFIX) + link_formset = CorsoLinkFormSet(request.POST, + queryset=course_links, + prefix=LINKFORM_PREFIX) + + if course_form.is_valid(): + course_form.save() + + if file_formset.is_valid(): + file_formset.save(commit=False) + + for obj in file_formset.deleted_objects: + obj.delete() + + for form in file_formset: + if form.is_valid() and not form.empty_permitted: + instance = form.instance + instance.corso = course + file_formset.save() + + if link_formset.is_valid(): + link_formset.save(commit=False) + for form in link_formset: + instance = form.instance + instance.corso = course + link_formset.save() + + if course_form.is_valid() and file_formset.is_valid() and link_formset.is_valid(): + + if course_form.has_changed(): + messages.success(request, 'I dati della pianificazione corso sono stati salvati. ' + 'Procedi con il prossimo step') + return redirect(reverse('aspirante:lessons', args=[pk])) + + return redirect(reverse('aspirante:modify', args=[pk])) + else: + course_form = ModuloModificaCorsoBase(instance=course) + file_formset = CorsoFileFormSet(queryset=course_files, prefix=FILEFORM_PREFIX) + link_formset = CorsoLinkFormSet(queryset=course_links, prefix=LINKFORM_PREFIX) + + context = { + 'corso': course, + 'puo_modificare': True, + 'modulo': course_form, + 'file_formset': file_formset, + 'link_formset': link_formset, } - return 'aspirante_corso_base_scheda_modifica.html', contesto + return 'aspirante_corso_base_scheda_modifica.html', context @pagina_privata def aspirante_corso_base_attiva(request, me, pk): corso = get_object_or_404(CorsoBase, pk=pk) + if not me.permessi_almeno(corso, MODIFICA): return redirect(ERRORE_PERMESSI) + if corso.stato != corso.PREPARAZIONE: - return messaggio_generico(request, me, titolo="Il corso è già attivo", + return messaggio_generico(request, me, + titolo="Il corso è già attivo", messaggio="Non puoi attivare un corso già attivo", torna_titolo="Torna al Corso", torna_url=corso.url) if not corso.attivabile(): - return errore_generico(request, me, titolo="Impossibile attivare questo corso", + return errore_generico(request, me, + titolo="Impossibile attivare questo corso", messaggio="Non sono soddisfatti tutti i criteri di attivazione. " "Torna alla pagina del corso e verifica che tutti i " "criteri siano stati soddisfatti prima di attivare un " @@ -302,30 +481,28 @@ def aspirante_corso_base_attiva(request, me, pk): torna_url=corso.url) if corso.data_inizio < poco_fa(): - return errore_generico(request, me, titolo="Impossibile attivare un corso già iniziato", + return errore_generico(request, me, + titolo="Impossibile attivare un corso già iniziato", messaggio="Siamo spiacenti, ma non possiamo attivare il corso e inviare " "le e-mail a tutti gli aspiranti nella zona se il corso è " "già iniziato. Ti inviato a verificare i dati del corso.", torna_titolo="Torna al Corso", torna_url=corso.url) - corpo = {"corso": corso, "persona": me} - testo = get_template("email_aspirante_corso_inc_testo.html").render(corpo) + email_body = {"corso": corso, "persona": me} + text = get_template("email_aspirante_corso_inc_testo.html").render( + email_body) if request.POST: - corso.attiva(rispondi_a=me) - return messaggio_generico(request, me, titolo="Corso attivato con successo", - messaggio="A breve tutti gli aspiranti nelle vicinanze verranno informati " - "dell'attivazione di questo corso base.", - torna_titolo="Torna al Corso", - torna_url=corso.url) + activation = corso.attiva(request=request, rispondi_a=me) + return activation - contesto = { + context = { "corso": corso, "puo_modificare": True, - "testo": testo, + "testo": text, } - return 'aspirante_corso_base_scheda_attiva.html', contesto + return 'aspirante_corso_base_scheda_attiva.html', context @pagina_privata @@ -334,13 +511,17 @@ def aspirante_corso_base_termina(request, me, pk): if not me.permessi_almeno(corso, MODIFICA): return redirect(ERRORE_PERMESSI) + if not corso.terminabile: + messages.warning(request, "Il corso non è terminabile.") + return redirect(reverse('aspirante:info', args=(pk,))) + torna = {"torna_url": corso.url_modifica, "torna_titolo": "Modifica corso"} - if (not corso.op_attivazione) or (not corso.data_attivazione): - return errore_generico(request, me, titolo="Necessari dati attivazione", - messaggio="Per generare il verbale, sono necessari i dati (O.P. e data) " - "dell'attivazione del corso.", - **torna) + # if (not corso.op_attivazione) or (not corso.data_attivazione): + # return errore_generico(request, me, + # titolo="Necessari dati attivazione", + # messaggio="Per generare il verbale, sono necessari i dati (O.P. e data) dell'attivazione del corso.", + # **torna) if not corso.partecipazioni_confermate().exists(): return errore_generico(request, me, titolo="Impossibile terminare questo corso", @@ -357,61 +538,117 @@ def aspirante_corso_base_termina(request, me, pk): azione = request.POST.get('azione', default=ModuloVerbaleAspiranteCorsoBase.SALVA_SOLAMENTE) generazione_verbale = azione == ModuloVerbaleAspiranteCorsoBase.GENERA_VERBALE - termina_corso = generazione_verbale for partecipante in corso.partecipazioni_confermate(): - modulo = ModuloVerbaleAspiranteCorsoBase( + form = ModuloVerbaleAspiranteCorsoBase( request.POST or None, prefix="part_%d" % partecipante.pk, instance=partecipante, generazione_verbale=generazione_verbale ) - modulo.fields['destinazione'].queryset = corso.possibili_destinazioni() - modulo.fields['destinazione'].initial = corso.sede - - if modulo.is_valid(): - modulo.save() - + if corso.is_nuovo_corso: + pass + else: + form.fields['destinazione'].queryset = corso.possibili_destinazioni() + form.fields['destinazione'].initial = corso.sede + + if form.is_valid(): + form.save() elif generazione_verbale: termina_corso = False - partecipanti_moduli += [(partecipante, modulo)] + partecipanti_moduli += [(partecipante, form)] + + if termina_corso: # Se premuto pulsante "Genera verbale e termina corso" + # Verifica se la relazione è compilata + if not corso.relazione_direttore.is_completed: + messages.error(request, "Il corso non può essere terminato perchè " + "la relazione del direttore non è completata.") + return redirect(reverse('aspirante:terminate', args=(pk,))) - if termina_corso: # Se il corso può essere terminato. + # Tutto ok, posso procedere corso.termina(mittente=me) - return messaggio_generico(request, me, titolo="Corso base terminato", - messaggio="Il verbale è stato generato con successo. Tutti gli idonei " - "sono stati resi volontari delle rispettive sedi.", - torna_titolo="Vai al Report del Corso Base", - torna_url=corso.url_report) - contesto = { + if corso.is_nuovo_corso: + email_title = "Corso terminato." + return_title = "Vai al Report del Corso" + else: + email_title = "Corso base terminato" + return_title = "Vai al Report del Corso Base" + + return messaggio_generico(request, me, + titolo=email_title, + messaggio="Il verbale è stato generato con successo. Tutti gli idonei " + "sono stati resi volontari delle rispettive sedi.", + torna_titolo=return_title, + torna_url=corso.url_report + ) + + context = { "corso": corso, "puo_modificare": True, "partecipanti_moduli": partecipanti_moduli, "azione_genera_verbale": ModuloVerbaleAspiranteCorsoBase.GENERA_VERBALE, "azione_salva_solamente": ModuloVerbaleAspiranteCorsoBase.SALVA_SOLAMENTE, } - return 'aspirante_corso_base_scheda_termina.html', contesto + return 'aspirante_corso_base_scheda_termina.html', context @pagina_privata -def aspirante_corso_base_iscritti(request, me, pk): +def corso_compila_relazione_direttore(request, me, pk): + course = get_object_or_404(CorsoBase, pk=pk) + puo_modificare = course.can_modify(me) + if not puo_modificare: + return redirect(ERRORE_PERMESSI) + + if not course.terminabile: + messages.warning(request, 'Il corso non è terminabile.') + return redirect(reverse('aspirante:info', args=[pk,])) + + relazione, created = RelazioneCorso.objects.get_or_create(corso=course) + if request.method == 'POST': + form_relazione = FormRelazioneDelDirettoreCorso(request.POST, instance=relazione) + if form_relazione.is_valid(): + cd = form_relazione.cleaned_data + instance = form_relazione.save(commit=False) + + # Se nei vari il Direttore non ha nulla da inserire valorizzarlo con un valore di default + no_value_fields = [k for k,v in cd.items() if not v] + if no_value_fields: + for k in no_value_fields: + setattr(instance, k, RelazioneCorso.SENZA_VALORE) + instance.save() + + messages.success(request, 'La relazione è stata salvata.') + return redirect(reverse('courses:compila_relazione_direttore', args=(pk,))) + else: + form_relazione = FormRelazioneDelDirettoreCorso(instance=relazione) + + context = { + "corso": course, + "puo_modificare": puo_modificare, + "form_relazione": form_relazione, + } + return 'course_compila_relazione_direttore.html', context + +@pagina_privata +def aspirante_corso_base_iscritti(request, me, pk): corso = get_object_or_404(CorsoBase, pk=pk) + if not me.permessi_almeno(corso, MODIFICA): return redirect(ERRORE_PERMESSI) elenco = ElencoPartecipantiCorsiBase(corso.queryset_modello()) in_attesa = corso.partecipazioni_in_attesa() - contesto = { + context = { "corso": corso, "puo_modificare": True, "elenco": elenco, "in_attesa": in_attesa, } - return 'aspirante_corso_base_scheda_iscritti.html', contesto + return 'aspirante_corso_base_scheda_iscritti.html', context @pagina_privata @@ -451,21 +688,37 @@ def aspirante_corso_base_iscritti_cancella(request, me, pk, iscritto): @pagina_privata def aspirante_corso_base_iscritti_aggiungi(request, me, pk): corso = get_object_or_404(CorsoBase, pk=pk) + if not me.permessi_almeno(corso, MODIFICA): return redirect(ERRORE_PERMESSI) - if not corso.possibile_aggiungere_iscritti: - return errore_generico(request, me, titolo="Impossibile aggiungere iscritti", - messaggio="Non si possono aggiungere altri iscritti a questo " - "stadio della vita del corso base.", - torna_titolo="Torna al corso base", torna_url=corso.url_iscritti) + return errore_generico(request, me, + titolo="Impossibile aggiungere iscritti", + messaggio="Non si possono aggiungere altri iscritti a questo " + "stadio della vita del corso.", + torna_titolo="Torna al corso", + torna_url=corso.url_iscritti + ) - modulo = ModuloIscrittiCorsoBaseAggiungi(request.POST or None) - risultati = [] - if modulo.is_valid(): + risultati = list() + persone_con_esito_negativo = dict() - for persona in modulo.cleaned_data['persone']: + form = ModuloIscrittiCorsoBaseAggiungi(request.POST or None, corso=corso) + if form.is_valid(): + for persona in form.cleaned_data['persone']: + # esito_negative_message = None esito = corso.persona(persona) + if esito in [CorsoBase.NON_HAI_CARICATO_DOCUMENTI_PERSONALI, CorsoBase.NON_HAI_DOCUMENTO_PERSONALE_VALIDO]: + persone_con_esito_negativo[persona] = esito + + # esito_negative_message = "Questo utente non ha caricato un documento d'identità" + # elif esito == CorsoBase.NON_HAI_DOCUMENTO_PERSONALE_VALIDO: + # esito_negative_message = "Questo utente non ha un documento di riconoscimento valido/rinnovato" + + # # Persone da avvisare via posta della problematica + # if esito_negative_message: + # persone_con_esito_negativo[persona] = esito #, esito_negative_message + ok = PartecipazioneCorsoBase.NON_ISCRITTO partecipazione = None @@ -481,10 +734,19 @@ def aspirante_corso_base_iscritti_aggiungi(request, me, pk): partecipazione.richiedi() ok = PartecipazioneCorsoBase.IN_ATTESA_ASPIRANTE else: - partecipazione = PartecipazioneCorsoBase.objects.create(persona=persona, corso=corso) + partecipazione = PartecipazioneCorsoBase.objects.create( + persona=persona, + corso=corso + ) ok = PartecipazioneCorsoBase.ISCRITTO + + if corso.is_nuovo_corso: + subject = "Iscrizione a Corso %s" % corso.titolo_cri + else: + subject = "Iscrizione a Corso Base" + Messaggio.costruisci_e_invia( - oggetto="Iscrizione a Corso Base", + oggetto=subject, modello="email_corso_base_iscritto.html", corpo={ "persona": persona, @@ -503,13 +765,38 @@ def aspirante_corso_base_iscritti_aggiungi(request, me, pk): "ok": ok, }] - contesto = { + # Invia avvisi agli utenti con esito negativo + for persona, esito in persone_con_esito_negativo.items(): + already_sent_today = Messaggio.objects.filter( + mittente=me, + oggetti_destinatario__persona=persona, + oggetto__istartswith="Impossibilità di iscriversi a", + creazione__date=date.today(), + ).count() + + # Per evitare che invii lo stesso messaggio più di una volta al di + if not already_sent_today: + posta = Messaggio.costruisci_e_accoda( + oggetto="Impossibilità di iscriversi a %s" % corso.nome, + modello="email_corso_iscritti_aggiungi_esito_negativo.html", + corpo={ + 'corso': corso, + 'esito': esito, + }, + mittente=me, + destinatari=[persona]) + else: + if persone_con_esito_negativo: + utente = 'utenti' if len(persone_con_esito_negativo) > 1 else 'utente' + messages.error(request, "Abbiamo avvisato %s della problematica." % utente) + + context = { "corso": corso, "puo_modificare": True, - "modulo": modulo, + "modulo": form, "risultati": risultati, } - return 'aspirante_corso_base_scheda_iscritti_aggiungi.html', contesto + return 'aspirante_corso_base_scheda_iscritti_aggiungi.html', context @pagina_privata @@ -538,23 +825,16 @@ def aspirante_corso_base_report(request, me, pk): @pagina_privata def aspirante_corso_base_report_schede(request, me, pk): corso = get_object_or_404(CorsoBase, pk=pk) - if not me.permessi_almeno(corso, MODIFICA): - return redirect(ERRORE_PERMESSI) - archivio = Zip(oggetto=corso) - for p in corso.partecipazioni_confermate(): + can_download = False + if request.GET.get('download_single_attestato') and corso.partecipazioni_confermate().get(persona=me): + can_download = True - # Genera la scheda di valutazione. - scheda = p.genera_scheda_valutazione() - archivio.aggiungi_file(scheda.file.path, "%s - Scheda di Valutazione.pdf" % p.persona.nome_completo) + if not can_download and not me.permessi_almeno(corso, MODIFICA): + return redirect(ERRORE_PERMESSI) - # Se idoneo, genera l'attestato. - if p.idoneo: - attestato = p.genera_attestato() - archivio.aggiungi_file(attestato.file.path, "%s - Attestato.pdf" % p.persona.nome_completo) - - archivio.comprimi_e_salva(nome="Corso %d-%d.zip" % (corso.progressivo, corso.anno)) - return redirect(archivio.download_url) + report = GeneraReport(request, corso) + return report.download() @pagina_privata @@ -567,14 +847,27 @@ def aspirante_home(request, me): @pagina_privata -def aspirante_corsi_base(request, me): - if not me.ha_aspirante: - return redirect(ERRORE_PERMESSI) +@can_access_to_course +def aspirante_corsi(request, me): + """ url: /aspirante/corsi/ """ - contesto = { - "corsi": me.aspirante.corsi(), + if me.ha_aspirante: + corsi = me.aspirante.corsi(tipo=Corso.BASE) + elif me.volontario: + # Trova corsi dove l'utente ha già partecipato + partecipazione = PartecipazioneCorsoBase.objects.filter(confermata=True, persona=me) + corsi_confermati = CorsoBase.objects.filter(id__in=partecipazione.values_list('corso', flat=True)) + + # Trova corsi da partecipare + corsi_da_partecipare = CorsoBase.find_courses_for_volunteer(volunteer=me) + + # Unisci 2 categorie di corsi + corsi = corsi_confermati | corsi_da_partecipare + + context = { + 'corsi': corsi } - return 'aspirante_corsi_base.html', contesto + return 'aspirante_corsi_base.html', context @pagina_privata @@ -603,12 +896,279 @@ def aspirante_impostazioni_cancella(request, me): return redirect(ERRORE_PERMESSI) if not me.cancellabile: - return errore_generico(request, me, titolo="Impossibile cancellare automaticamente il profilo da Gaia", - messaggio="E' necessario richiedere la cancellazione manuale al personale di supporto.") + return errore_generico(request, me, + titolo="Impossibile cancellare automaticamente il profilo da Gaia", + messaggio="E' necessario richiedere la cancellazione manuale al personale di supporto." + ) # Cancella! me.delete() - return messaggio_generico(request, me, titolo="Il tuo profilo è stato cancellato da Gaia", - messaggio="Abbiamo rimosso tutti i tuoi dati dal nostro sistema. " - "Se cambierai idea, non esitare a iscriverti nuovamente! ") + return messaggio_generico(request, me, + titolo="Il tuo profilo è stato cancellato da Gaia", + messaggio="Abbiamo rimosso tutti i tuoi dati dal nostro sistema. " + "Se cambierai idea, non esitare a iscriverti nuovamente! " + ) + + +@pagina_privata +def aspirante_corso_estensioni_modifica(request, me, pk): + from .forms import CorsoSelectExtensionTypeForm + from .formsets import CorsoSelectExtensionFormSet + + SELECT_EXTENSION_TYPE_FORM_PREFIX = 'extension_type' + SELECT_EXTENSIONS_FORMSET_PREFIX = 'extensions' + + course = get_object_or_404(CorsoBase, pk=pk) + if not me.permessi_almeno(course, MODIFICA): + return redirect(ERRORE_PERMESSI) + + if not course.tipo == Corso.CORSO_NUOVO: + # The page is not accessible if the type of course is not CORSO_NUOVO + return redirect(ERRORE_PERMESSI) + + if request.method == 'POST': + select_extension_type_form = CorsoSelectExtensionTypeForm(request.POST, + instance=course, + prefix=SELECT_EXTENSION_TYPE_FORM_PREFIX) + select_extensions_formset = CorsoSelectExtensionFormSet(request.POST, + prefix=SELECT_EXTENSIONS_FORMSET_PREFIX, + form_kwargs={'corso': course}) + + if select_extension_type_form.is_valid() and select_extensions_formset.is_valid(): + select_extensions_formset.save(commit=False) + + for form in select_extensions_formset: + if form.is_valid: + cd = form.cleaned_data + instance = form.save(commit=False) + instance.corso = course + corso = instance.corso + + # Skip blank extra formset + if cd == {} and len(select_extensions_formset) > 1: + continue + + # Do validation only with specified extension type + if corso.extension_type == CorsoBase.EXT_LVL_REGIONALE: + msg = 'Questo campo è obbligatorio.' + if not cd.get('sede'): + form.add_error('sede', msg) + if not cd.get('segmento'): + form.add_error('segmento', msg) + if cd.get('sedi_sottostanti') and not cd.get('sede'): + form.add_error('sede', 'Seleziona una sede') + + # No errors nor new added error - save form instance + if not form.errors: + instance.save() + + # Return form with error without saving + if any(select_extensions_formset.errors): + pass + else: + # Save all forms and redirect to the same page. + select_extension_type_form.save() + select_extensions_formset.save() + + # Set EXT_MIA_SEDE if course has no extensions + reset_corso_ext = CorsoBase.objects.get(pk=pk) + corso_has_extensions = reset_corso_ext.has_extensions() + new_objects = select_extensions_formset.new_objects + if not corso_has_extensions and not new_objects: + reset_corso_ext.extension_type = CorsoBase.EXT_MIA_SEDE + reset_corso_ext.save() + + # Reindirizzare l'utente al prossimo step da compilare (utile + # solo in fase di primo compilamento delle form del corso). + if corso_has_extensions or new_objects: + messages.success(request, 'Le estensioni sono state salvate. Procedi con il prossimo step') + return redirect(reverse('aspirante:modify', args=[pk])) + + return redirect(reverse('aspirante:estensioni_modifica', args=[pk])) + + else: + select_extension_type_form = CorsoSelectExtensionTypeForm( + prefix=SELECT_EXTENSION_TYPE_FORM_PREFIX, + instance=course, + ) + select_extensions_formset = CorsoSelectExtensionFormSet( + prefix=SELECT_EXTENSIONS_FORMSET_PREFIX, + form_kwargs={'corso': course}, + queryset=CorsoEstensione.objects.filter(corso=course) + ) + + context = { + 'corso': course, + 'puo_modificare': True, + 'select_extension_type_form': select_extension_type_form, + 'select_extensions_formset': select_extensions_formset, + } + return 'aspirante_corso_estensioni_modifica.html', context + + +@pagina_privata +def aspirante_corso_estensioni_informa(request, me, pk): + from .forms import InformCourseParticipantsForm + + course = get_object_or_404(CorsoBase, pk=pk) + + if not me.permessi_almeno(course, MODIFICA): + return redirect(ERRORE_PERMESSI) + + qs = Persona.objects.filter() + form_data = { + 'instance': course, + } + form = InformCourseParticipantsForm(request.POST or None, **form_data) + if form.is_valid(): + cd = form.cleaned_data + recipients = PartecipazioneCorsoBase.objects.none() + sent_with_success = False + + recipient_type = cd['recipient_type'] + if recipient_type == form.ALL: + recipients = course.partecipazioni_in_attesa() | course.partecipazioni_confermate() + elif recipient_type == form.UNCONFIRMED_REQUESTS: + recipients = course.partecipazioni_in_attesa() + elif recipient_type == form.CONFIRMED_REQUESTS: + recipients = course.partecipazioni_confermate() + elif recipient_type == form.INVIA_QUESTIONARIO: + # recipients = course.partecipazioni_confermate() + return redirect(reverse('courses:send_questionnaire_to_participants', args=[course.pk])) + else: + # todo: something went wrong ... + pass + + if recipients and not recipient_type == form.INVIA_QUESTIONARIO: + sent_with_success = Messaggio.costruisci_e_invia( + oggetto="Informativa dal direttore %s (%s)" % (course.nome, course.titolo_cri), + modello="email_corso_informa_participants.html", + corpo={ + 'corso': course, + 'message': cd['message'], + }, + mittente=me, + destinatari=[r.persona for r in recipients] + ) + + if sent_with_success: + messages.success(request, "Il messaggio ai volontari è stato inviato con successo.") + return redirect(reverse('aspirante:informa', args=[pk])) + + if not recipients: + messages.success(request, "Il messaggio non è stato inviato a nessuno.") + return redirect(reverse('aspirante:informa', args=[pk])) + + context = { + 'corso': course, + 'form': form, + 'puo_modificare': True, + } + return 'aspirante_corso_informa_persone.html', context + + +@pagina_privata +def formazione_albo_informatizzato(request, me): + sedi_set = set() + + ALL_PERMESSI_TO_CHECK = RUBRICA_DELEGATI_OBIETTIVO_ALL + [GESTIONE_CORSI_SEDE] + for permesso in ALL_PERMESSI_TO_CHECK: + ids = me.oggetti_permesso(permesso).values_list('pk', flat=True) + sedi_set.update(ids) + + sedi = Sede.objects.filter(pk__in=sedi_set) + + if not sedi: + return redirect(ERRORE_PERMESSI) + + context = { + 'elenco_nome': 'Albo Informatizzato', + 'elenco_template': None, + } + + # Step 2: Elaborare elenco per le sedi selezionate + if request.method == 'POST': + elenco = ElencoPerTitoliCorso(sedi.filter(pk__in=request.POST.getlist('sedi'))) + context['elenco'] = elenco + return 'formazione_albo_elenco_generico.html', context + + # Step 1: Selezione sedi + context['sedi'] = sedi + return 'formazione_albo_informatizzato.html', context + + +@pagina_privata +def formazione_albo_titoli_corso_full_list(request, me): + context = {} + if 'persona_id' in request.GET: + persona = Persona.objects.get(id=request.GET['persona_id']) + titles = TitoloPersonale.objects.filter(persona=persona, + is_course_title=True) + context['titles'] = titles.order_by('titolo__nome', '-data_scadenza') + context['person'] = persona + + return 'formazione_albo_titoli_corso_full_list.html', context + + +@pagina_privata +def formazione_corso_position_change(request, me, pk): + course = get_object_or_404(CorsoBase, pk=pk) + + if not course.can_modify(me): + return redirect(ERRORE_PERMESSI) + + template = 'formazione_vuota.html' + puo_modificare = False # non mostrare i tab se la locazione non è impostata + + # Locazione impostata... + if course.locazione: + template = 'aspirante_corso_base_scheda.html' + puo_modificare = course.can_modify(me) + # Se il corso non ha ancora un direttore... + if not course.direttori_corso: + # Rindirizza sulla pagina selezione direttori del corso. + return redirect(course.url_direttori) + + context = {'corso': course, + 'template': template, + 'puo_modificare': puo_modificare,} + return 'formazione_corso_position_change.html', context + + +@pagina_privata +def course_send_questionnaire_to_participants(request, me, pk): + context = dict() + course = get_object_or_404(CorsoBase, pk=pk) + + if not course.can_modify(me): + return redirect(ERRORE_PERMESSI) + + if request.method == 'POST': + send_with_success = False + + recipients = request.POST.getlist('persona') + recipients = Persona.objects.filter(id__in=[int(r) for r in recipients]) + if recipients: + titolo = 'per %s' % course.titolo_cri if course.titolo_cri else '' + sent_with_success = Messaggio.costruisci_e_invia( + oggetto="Questionario di gradimento del %s %s" % (course.nome, titolo), + modello="email_corso_questionario_gradimento.html", + corpo={ + 'corso': course, + }, + mittente=me, + destinatari=recipients, + ) + + if sent_with_success: + msg = "Il questionario è stato inviato con successo a %s partecipanti selezionati" % recipients.count() + messages.success(request, msg) + return redirect(reverse('courses:send_questionnaire_to_participants', args=[course.pk])) + else: + messages.error(request, 'Non hai selezionato persone.') + + context['puo_modificare'] = course.can_modify(me) + context['corso'] = course + + return 'course_send_questionnaire_to_participants.html', context diff --git a/jorvik/celery.py b/jorvik/celery.py index bb2151447..f0d165c02 100644 --- a/jorvik/celery.py +++ b/jorvik/celery.py @@ -22,6 +22,8 @@ 'ufficio_soci.tasks.generate_elenco': {'queue': 'shared_ufficio_soci'}, 'static_page.tasks.send_mail': {'queue': 'queue_monitoraggio'}, + + 'formazione.tasks.task_invia_email_agli_aspiranti': {'queue': 'queue_formazione'}, } # Load task modules from all registered Django app configs. diff --git a/jorvik/settings.py b/jorvik/settings.py index fb8dc8f39..8c3b94759 100755 --- a/jorvik/settings.py +++ b/jorvik/settings.py @@ -19,10 +19,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -# Deployment: https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ - # Elenca le applicazioni installate da abilitare - INSTALLED_APPS = [ 'jorvik', 'django.contrib.admin', @@ -33,7 +30,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.gis', - + # Librerie terze 'nocaptcha_recaptcha', 'oauth2_provider', @@ -53,6 +50,7 @@ 'social', 'posta', 'sangue', + 'survey', 'static_page', 'formazione', 'bootstrap3', @@ -95,7 +93,8 @@ "base.cron.CronRichiesteInAttesa", "base.cron.PulisciAspirantiVolontari", "anagrafica.cron.CronReportComitati", - "centrale_operativa.cron.CronCancellaCoturniInvalidi" + "centrale_operativa.cron.CronCancellaCoturniInvalidi", + 'curriculum.cron.CronCheckExpiredCourseTitles', ] # Classi middleware (intercetta & computa) diff --git a/jorvik/urls.py b/jorvik/urls.py index af7dda098..e1bc9977d 100755 --- a/jorvik/urls.py +++ b/jorvik/urls.py @@ -1,33 +1,20 @@ -""" -Questo modulo contiene la configurazione per il routing degli URL. - -(c)2015 Croce Rossa Italiana -""" -import django, django.views, django.views.static, django.contrib.auth.views -from django.conf.urls import include, url +from .settings import MEDIA_ROOT, DEBUG from django.contrib import admin -from django.contrib.auth.views import password_change, password_change_done -from django.shortcuts import redirect +from django.conf.urls import include, url from django.views.i18n import javascript_catalog -from oauth2_provider import views as oauth2_provider_views +import django.contrib.auth.views +from oauth2_provider import views as oauth2_provider_views +from autenticazione.two_factor.urls import urlpatterns as tf_urls +from formazione import urls_aspirante as formazione_urls_aspirante import anagrafica.viste import articoli.viste -import attivita.viste import autenticazione.viste import base.viste, base.errori -import centrale_operativa.viste -import formazione.viste import gestione_file.viste -import gruppi.viste import posta.viste import social.viste import ufficio_soci.viste -import veicoli.viste - -from jorvik.settings import MEDIA_ROOT, DEBUG - -from autenticazione.two_factor.urls import urlpatterns as tf_urls handler404 = base.errori.non_trovato @@ -37,9 +24,7 @@ } urlpatterns = [ - - # Home page! - url(r'^$', base.viste.index), + url(r'^$', base.viste.index), # Home page # Moduli di registrazione url(r'^registrati/(?P\w+)/conferma/$', anagrafica.viste.registrati_conferma), @@ -68,22 +53,21 @@ url(r'^recupera_password_completo/$', base.viste.recupero_password_completo, name='recupero_password_completo'), # Informazioni - url(r'^informazioni/$', base.viste.informazioni), + url(r'^informazioni/$', base.viste.informazioni, name='informazioni'), url(r'^informazioni/statistiche/$', base.viste.informazioni_statistiche), url(r'^informazioni/aggiornamenti/$', base.viste.informazioni_aggiornamenti), url(r'^informazioni/sicurezza/$', base.viste.informazioni_sicurezza), url(r'^informazioni/condizioni/$', base.viste.informazioni_condizioni, name='informazioni_condizioni'), url(r'^informazioni/cookie/$', base.viste.informazioni_cookie, name='informazioni_cookie'), url(r'^informazioni/cookie/imposta/$', base.viste.imposta_cookie, name='imposta_cookie'), - url(r'^informazioni/verifica-tesserino/$', ufficio_soci.viste.verifica_tesserino), + url(r'^informazioni/verifica-tesserino/$', + ufficio_soci.viste.verifica_tesserino, + name='informazioni_verifica_tesserino'), url(r'^informazioni/sedi/$', base.viste.informazioni_sedi), url(r'^informazioni/sedi/(?P.*)/$', base.viste.informazioni_sede), url(r'^informazioni/formazione/$', base.viste.formazione), url(r'^informazioni/browser-supportati/$', base.viste.browser_supportati, name='browser_supportati'), - # Applicazioni - url(r'^utente/', include('anagrafica.urls_utente', namespace='utente')), - url(r'^profilo/(?P[0-9]+)/messaggio/$', anagrafica.viste.profilo_messaggio), url(r'^profilo/(?P[0-9]+)/turni/foglio/$', anagrafica.viste.profilo_turni_foglio), url(r'^profilo/(?P[0-9]+)/telefono/(?P[0-9]+)/cancella/$', anagrafica.viste.profilo_telefono_cancella), @@ -109,58 +93,13 @@ url(r'^articoli/(?P\d{4})/$', articoli.viste.ListaArticoli.as_view(), name='lista_articoli-per-anno'), url(r'^articoli/(?P\d{4})/(?P\d{1,2})/$', articoli.viste.ListaArticoli.as_view(), name='lista_articoli-per-mese'), url(r'^articoli/(?P[\w\-]+)/$', articoli.viste.DettaglioArticolo.as_view(), name='dettaglio_articolo'), + url(r'^documenti/$', gestione_file.viste.ListaDocumenti.as_view(), name='lista_documenti'), url(r'^documenti/(?P[0-9\-]+)/$', gestione_file.viste.ListaDocumenti.as_view(), name='lista_documenti'), url(r'^documenti/scarica/(?P[0-9\-]+)/$', gestione_file.viste.serve_protected_file, name='scarica_file'), url(r'documenti/immagine/(?P\d+)/$', gestione_file.viste.serve_image, name='scarica_immagine'), url(r'documenti/immagine/(?P\d+)/(?P\d+)/$', gestione_file.viste.serve_image, name='scarica_immagine'), url(r'documenti/immagine/(?P\d+)/(?P\d+)/(?P\d+)/$', gestione_file.viste.serve_image, name='scarica_immagine'), - url(r'^attivita/$', attivita.viste.attivita), - url(r'^attivita/aree/$', attivita.viste.attivita_aree), - url(r'^attivita/aree/(?P[0-9\-]+)/$', attivita.viste.attivita_aree_sede), - url(r'^attivita/aree/(?P[0-9\-]+)/(?P[0-9\-]+)/cancella/$', attivita.viste.attivita_aree_sede_area_cancella), - url(r'^attivita/aree/(?P[0-9\-]+)/(?P[0-9\-]+)/responsabili/$', attivita.viste.attivita_aree_sede_area_responsabili), - url(r'^attivita/organizza/$', attivita.viste.attivita_organizza), - url(r'^attivita/organizza/(?P[0-9\-]+)/referenti/$', attivita.viste.attivita_referenti, {"nuova": True}), - url(r'^attivita/organizza/(?P[0-9\-]+)/fatto/$', attivita.viste.attivita_organizza_fatto), - url(r'^attivita/statistiche/$', attivita.viste.attivita_statistiche), - url(r'^attivita/gestisci/$', attivita.viste.attivita_gestisci, {"stato": "aperte"}), - url(r'^attivita/gestisci/chiuse/$', attivita.viste.attivita_gestisci, {"stato": "chiuse"}), - url(r'^attivita/calendario/$', attivita.viste.attivita_calendario), - url(r'^attivita/calendario/(?P[0-9\-]+)/(?P[0-9\-]+)/$', attivita.viste.attivita_calendario), - url(r'^attivita/storico/$', attivita.viste.attivita_storico), - url(r'^attivita/storico/excel/$', attivita.viste.attivita_storico_excel), - url(r'^attivita/gruppo/$', gruppi.viste.attivita_gruppo), - url(r'^attivita/gruppi/$', gruppi.viste.attivita_gruppi), - url(r'^attivita/gruppi/(?P[0-9]+)/$', gruppi.viste.attivita_gruppi_gruppo), - url(r'^attivita/gruppi/(?P[0-9]+)/iscriviti/$', gruppi.viste.attivita_gruppi_gruppo_iscriviti), - url(r'^attivita/gruppi/(?P[0-9]+)/espelli/(?P[0-9]+)/$', gruppi.viste.attivita_gruppi_gruppo_espelli), - url(r'^attivita/gruppi/(?P[0-9]+)/abbandona/$', gruppi.viste.attivita_gruppi_gruppo_abbandona), - url(r'^attivita/gruppi/(?P[0-9]+)/elimina/$', gruppi.viste.attivita_gruppi_gruppo_elimina), - url(r'^attivita/gruppi/(?P[0-9]+)/elimina_conferma/$', gruppi.viste.attivita_gruppi_gruppo_elimina_conferma), - url(r'^attivita/reperibilita/$', centrale_operativa.viste.attivita_reperibilita), - url(r'^attivita/reperibilita/(?P[0-9]+)/cancella/$', centrale_operativa.viste.attivita_reperibilita_cancella), - url(r'^attivita/scheda/(?P[0-9]+)/$', attivita.viste.attivita_scheda_informazioni), - url(r'^attivita/scheda/(?P[0-9]+)/cancella-gruppo/$', attivita.viste.attivita_scheda_cancella), - url(r'^attivita/scheda/(?P[0-9]+)/cancella/$', attivita.viste.attivita_scheda_cancella), - url(r'^attivita/scheda/(?P[0-9]+)/mappa/$', attivita.viste.attivita_scheda_mappa), - url(r'^attivita/scheda/(?P[0-9]+)/partecipanti/$', attivita.viste.attivita_scheda_partecipanti), - url(r'^attivita/scheda/(?P[0-9]+)/turni/$', attivita.viste.attivita_scheda_turni), - url(r'^attivita/scheda/(?P[0-9]+)/turni/(?P[0-9]+)/$', attivita.viste.attivita_scheda_turni), - url(r'^attivita/scheda/(?P[0-9]+)/turni/(?P[0-9]+)/partecipa/$', attivita.viste.attivita_scheda_turni_partecipa), - url(r'^attivita/scheda/(?P[0-9]+)/turni/(?P[0-9]+)/ritirati/$', attivita.viste.attivita_scheda_turni_ritirati), - url(r'^attivita/scheda/(?P[0-9]+)/turni/(?P[0-9]+)/partecipanti/$', attivita.viste.attivita_scheda_turni_partecipanti), - url(r'^attivita/scheda/(?P[0-9]+)/turni/link-permanente/(?P[0-9]+)/$', attivita.viste.attivita_scheda_turni_link_permanente), - url(r'^attivita/scheda/(?P[0-9]+)/turni/cancella/(?P[0-9]+)/$', attivita.viste.attivita_scheda_turni_turno_cancella), - url(r'^attivita/scheda/(?P[0-9]+)/turni/modifica/$', attivita.viste.attivita_scheda_turni_modifica), - url(r'^attivita/scheda/(?P[0-9]+)/turni/nuovo/$', attivita.viste.attivita_scheda_turni_nuovo), - url(r'^attivita/scheda/(?P[0-9]+)/partecipazione/(?P[0-9]+)/cancella/$', attivita.viste.attivita_scheda_partecipazione_cancella), - url(r'^attivita/scheda/(?P[0-9]+)/turni/modifica/(?P[0-9]+)/$', attivita.viste.attivita_scheda_turni_modifica), - url(r'^attivita/scheda/(?P[0-9]+)/turni/modifica/link-permanente/(?P[0-9]+)/$', attivita.viste.attivita_scheda_turni_modifica_link_permanente), - url(r'^attivita/scheda/(?P[0-9]+)/modifica/$', attivita.viste.attivita_scheda_informazioni_modifica), - url(r'^attivita/scheda/(?P[0-9]+)/riapri/$', attivita.viste.attivita_riapri), - url(r'^attivita/scheda/(?P[0-9]+)/referenti/$', attivita.viste.attivita_referenti), - url(r'^attivita/scheda/(?P[0-9]+)/report/$', attivita.viste.attivita_scheda_report), url(r'^presidente/$', anagrafica.viste.presidente), url(r'^presidente/sedi/(?P[0-9]+)/$', anagrafica.viste.presidente_sede), @@ -169,127 +108,43 @@ url(r'^presidente/checklist/(?P[0-9]+)/(?P.*)/(?P[0-9]+)/(?P[0-9]+)/', anagrafica.viste.presidente_checklist_delegati), - url(r'^centrale-operativa/$', centrale_operativa.viste.co), - url(r'^centrale-operativa/reperibilita/$', centrale_operativa.viste.co_reperibilita), - url(r'^centrale-operativa/poteri/$', centrale_operativa.viste.co_poteri), - url(r'^centrale-operativa/poteri/(?P[0-9]+)/$', centrale_operativa.viste.co_poteri_switch), - url(r'^centrale-operativa/turni/$', centrale_operativa.viste.co_turni), - url(r'^centrale-operativa/turni/(?P[0-9]+)/monta/$', centrale_operativa.viste.co_turni_monta), - url(r'^centrale-operativa/turni/(?P[0-9]+)/smonta/$', centrale_operativa.viste.co_turni_smonta), - - url(r'^us/$', ufficio_soci.viste.us), - url(r'^us/provvedimento/$', ufficio_soci.viste.us_provvedimento), - url(r'^us/aggiungi/$', ufficio_soci.viste.us_aggiungi), - url(r'^us/reclama/$', ufficio_soci.viste.us_reclama), - url(r'^us/reclama/(?P.*)/$', ufficio_soci.viste.us_reclama_persona), - url(r'^us/estensione/$', ufficio_soci.viste.us_estensione), - url(r'^us/estensione/(?P.*)/termina/$', ufficio_soci.viste.us_estensione_termina, name='us-termina-estensione'), - url(r'^us/trasferimento/$', ufficio_soci.viste.us_trasferimento), - url(r'^us/riserva/$', ufficio_soci.viste.us_riserva), - url(r'^us/riserva/(?P.*)/termina/$', ufficio_soci.viste.us_riserva_termina), - url(r'^us/dimissioni/(?P[0-9]+)/$', ufficio_soci.viste.us_dimissioni, name='us-dimissioni'), - url(r'^us/dimissioni/sostenitore/(?P[0-9]+)/$', ufficio_soci.viste.us_chiudi_sostenitore, name='us-chiudi-sostenitore'), - - url(r'^us/elenchi/download/$', ufficio_soci.viste.us_elenchi_richiesti_download, name='elenchi_richiesti_download'), - url(r'^us/elenchi/(?P.*)/$', ufficio_soci.viste.us_elenchi), - url(r'^us/quote/$', ufficio_soci.viste.us_quote), - url(r'^us/quote/nuova/$', ufficio_soci.viste.us_quote_nuova, name='us_quote_nuova'), - url(r'^us/ricevute/$', ufficio_soci.viste.us_ricevute), - url(r'^us/ricevute/(?P[0-9]+)/annulla/$', ufficio_soci.viste.us_ricevute_annulla), - url(r'^us/ricevute/nuova/$', ufficio_soci.viste.us_ricevute_nuova, name='us_ricevute_nuova'), - - url(r'^us/tesserini/$', ufficio_soci.viste.us_tesserini), - url(r'^us/tesserini/da-richiedere/$', ufficio_soci.viste.us_tesserini_da_richiedere), - url(r'^us/tesserini/senza-fototessera/$', ufficio_soci.viste.us_tesserini_senza_fototessera), - url(r'^us/tesserini/richiesti/$', ufficio_soci.viste.us_tesserini_richiesti), - url(r'^us/tesserini/rifiutati/$', ufficio_soci.viste.us_tesserini_rifiutati), - url(r'^us/tesserini/richiedi/(?P[0-9]+)/$', ufficio_soci.viste.us_tesserini_richiedi, name='us-tesserini-richiedi'), - url(r'^us/tesserini/emissione/$', ufficio_soci.viste.us_tesserini_emissione), - url(r'^us/tesserini/emissione/processa/$', ufficio_soci.viste.us_tesserini_emissione_processa), - url(r'^us/tesserini/emissione/scarica/$', ufficio_soci.viste.us_tesserini_emissione_scarica), - - url(r'^us/elenco/(?P.*)/(?P[0-9]+)/$', ufficio_soci.viste.us_elenco), - url(r'^us/elenco/(?P.*)/download/$', ufficio_soci.viste.us_elenco_download), - url(r'^us/elenco/(?P.*)/messaggio/$', ufficio_soci.viste.us_elenco_messaggio, name='us-elenco-messaggio'), - url(r'^us/elenco/(?P.*)/modulo/$', ufficio_soci.viste.us_elenco_modulo), - url(r'^us/elenco/(?P.*)/$', ufficio_soci.viste.us_elenco), - - url(r'^veicoli/$', veicoli.viste.veicoli), - url(r'^veicoli/elenco/$', veicoli.viste.veicoli_elenco), - url(r'^veicoli/autoparchi/$', veicoli.viste.veicoli_autoparchi), - url(r'^veicoli/autoparco/elenco/(?P.*)/$', veicoli.viste.veicoli_elenco_autoparco), - url(r'^veicolo/(P.*)/$', veicoli.viste.veicoli_veicolo), - url(r'^autoparco/(P.*)/$', veicoli.viste.veicoli_autoparco), - url(r'^veicolo/nuovo/$', veicoli.viste.veicoli_veicolo_modifica_o_nuovo), - url(r'^autoparco/nuovo/$', veicoli.viste.veicoli_autoparco_modifica_o_nuovo), - url(r'^veicolo/modifica/(?P.*)/$', veicoli.viste.veicoli_veicolo_modifica_o_nuovo), - url(r'^autoparco/modifica/(?P.*)/$', veicoli.viste.veicoli_autoparco_modifica_o_nuovo), - url(r'^veicolo/manutenzioni/(?P.*)/$', veicoli.viste.veicoli_manutenzione), - url(r'^veicolo/manutenzione/(?P.*)/modifica/$', veicoli.viste.veicoli_modifica_manutenzione), - url(r'^veicolo/rifornimento/(?P.*)/modifica/$', veicoli.viste.veicoli_modifica_rifornimento), - url(r'^veicolo/rifornimenti/(?P.*)/$', veicoli.viste.veicoli_rifornimento), - url(r'^veicolo/fermi-tecnici/(?P.*)/$', veicoli.viste.veicoli_fermo_tecnico), - url(r'^veicolo/termina/fermo-tecnico/(?P.*)/$', veicoli.viste.veicoli_termina_fermo_tecnico), - url(r'^veicolo/(?P.*)/collocazioni/$', veicoli.viste.veicoli_collocazioni), - url(r'^veicolo/dettagli/(?P.*)/$', veicoli.viste.veicolo_dettagli), - - - url(r'^aspirante/$', formazione.viste.aspirante_home), - url(r'^aspirante/impostazioni/$', formazione.viste.aspirante_impostazioni), - url(r'^aspirante/impostazioni/cancella/$', formazione.viste.aspirante_impostazioni_cancella), - url(r'^aspirante/corsi-base/$', formazione.viste.aspirante_corsi_base), - url(r'^aspirante/sedi/$', formazione.viste.aspirante_sedi), - url(r'^aspirante/corso-base/(?P[0-9]+)/$', formazione.viste.aspirante_corso_base_informazioni), - url(r'^aspirante/corso-base/(?P[0-9]+)/mappa/$', formazione.viste.aspirante_corso_base_mappa), - url(r'^aspirante/corso-base/(?P[0-9]+)/iscritti/$', formazione.viste.aspirante_corso_base_iscritti), - url(r'^aspirante/corso-base/(?P[0-9]+)/iscritti/aggiungi/$', formazione.viste.aspirante_corso_base_iscritti_aggiungi), - url(r'^aspirante/corso-base/(?P[0-9]+)/iscritti/cancella/(?P[0-9]+)/$', formazione.viste.aspirante_corso_base_iscritti_cancella, name='formazione-iscritti-cancella'), - url(r'^aspirante/corso-base/(?P[0-9]+)/iscriviti/$', formazione.viste.aspirante_corso_base_iscriviti), - url(r'^aspirante/corso-base/(?P[0-9]+)/ritirati/$', formazione.viste.aspirante_corso_base_ritirati), - url(r'^aspirante/corso-base/(?P[0-9]+)/report/$', formazione.viste.aspirante_corso_base_report), - url(r'^aspirante/corso-base/(?P[0-9]+)/report/schede/$', formazione.viste.aspirante_corso_base_report_schede), - url(r'^aspirante/corso-base/(?P[0-9]+)/firme/$', formazione.viste.aspirante_corso_base_firme), - url(r'^aspirante/corso-base/(?P[0-9]+)/modifica/$', formazione.viste.aspirante_corso_base_modifica), - url(r'^aspirante/corso-base/(?P[0-9]+)/attiva/$', formazione.viste.aspirante_corso_base_attiva), - url(r'^aspirante/corso-base/(?P[0-9]+)/termina/$', formazione.viste.aspirante_corso_base_termina), - url(r'^aspirante/corso-base/(?P[0-9]+)/lezioni/$', formazione.viste.aspirante_corso_base_lezioni), - url(r'^aspirante/corso-base/(?P[0-9]+)/lezioni/(?P[0-9]+)/cancella/$', formazione.viste.aspirante_corso_base_lezioni_cancella), + # Applicazioni + url(r'^centrale-operativa/', include('centrale_operativa.urls', namespace='centrale_operativa')), + url(r'^autoparco/', include('veicoli.urls_autoparco', namespace='autoparco')), + url(r'^attivita/', include('attivita.urls', namespace='attivita')), + url(r'^veicoli/', include('veicoli.urls', namespace='veicoli')), + url(r'^utente/', include('anagrafica.urls_utente', namespace='utente')), + url(r'^us/', include('ufficio_soci.urls', namespace='ufficio_soci')), + url(r'^cv/', include('curriculum.urls', namespace='cv')), - url(r'^formazione/$', formazione.viste.formazione), - url(r'^formazione/corsi-base/elenco/$', formazione.viste.formazione_corsi_base_elenco), - url(r'^formazione/corsi-base/domanda/$', formazione.viste.formazione_corsi_base_domanda), - url(r'^formazione/corsi-base/nuovo/$', formazione.viste.formazione_corsi_base_nuovo), - url(r'^formazione/corsi-base/(?P[0-9]+)/direttori/$', formazione.viste.formazione_corsi_base_direttori), - url(r'^formazione/corsi-base/(?P[0-9]+)/fine/$', formazione.viste.formazione_corsi_base_fine), + # Formazione + url(r'^aspirante/', include(formazione_urls_aspirante, namespace='aspirante')), + url(r'^formazione/', include('formazione.urls', namespace='formazione')), + url(r'^courses/', include('formazione.urls_courses', namespace='courses')), + url(r'^survey/', include('survey.urls', namespace='survey')), # Static pages url(r'^page/', include('static_page.urls', namespace='pages')), - url(r'^supporto/$', base.viste.supporto), + url(r'^supporto/$', base.viste.supporto, name='supporto_page'), - url(r'^geo/localizzatore/imposta/$', base.viste.geo_localizzatore_imposta), - url(r'^geo/localizzatore/$', base.viste.geo_localizzatore), - url(r'^strumenti/delegati/$', anagrafica.viste.strumenti_delegati), + url(r'^strumenti/delegati/$', anagrafica.viste.strumenti_delegati, name='strumenti_delegati'), url(r'^strumenti/delegati/(?P[0-9]+)/termina/$', anagrafica.viste.strumenti_delegati_termina), - url(r'^social/commenti/nuovo/', social.viste.commenti_nuovo), url(r'^social/commenti/cancella/(?P[0-9]+)/', social.viste.commenti_cancella), - url(r'^media/(?P.*)$', django.views.static.serve, {"document_root": MEDIA_ROOT}), - + url(r'^geo/localizzatore/imposta/$', base.viste.geo_localizzatore_imposta, name='geo_localizzatore_imposta'), + url(r'^geo/localizzatore/$', base.viste.geo_localizzatore, name='geo_localizzatore'), url(r'^pdf/(?P.*)/(?P.*)/(?P[0-9]+)/$', base.viste.pdf), - url(r'^token-sicuro/(?P.*)/$', base.viste.verifica_token), - url(r'^password-dimenticata/$', base.viste.redirect_semplice, {"nuovo_url": "/recupera_password/"}), # Amministrazione - url(r'^admin/import/volontari/$', anagrafica.viste.admin_import_volontari), url(r'^admin/import/presidenti/$', anagrafica.viste.admin_import_presidenti), url(r'^admin/pulisci/email/$', anagrafica.viste.admin_pulisci_email), url(r'^admin/statistiche/$', anagrafica.viste.admin_statistiche), + url(r'^admin/statistiche/download/(?P[0-9A-Za-z_\-]+)$', anagrafica.viste.admin_statistiche_download), url(r'^admin/report_federazione/$', anagrafica.viste.admin_report_federazione), - url(r'^admin/', include(admin.site.urls)), url(r'^login/', include('loginas.urls')), # Login come utente @@ -310,9 +165,7 @@ # REST api url(r'^api/', include('api.urls', namespace='api')), - ] if DEBUG: urlpatterns += [url(r'^api-auth/', include('rest_framework.urls')),] - diff --git a/posta/models.py b/posta/models.py index 7aa70491a..99b4a4ad3 100755 --- a/posta/models.py +++ b/posta/models.py @@ -1,21 +1,24 @@ -""" -Questo modulo definisce i modelli del modulo di Posta di Gaia. -""" +import os import logging -from smtplib import SMTPException, SMTPRecipientsRefused, SMTPResponseException, SMTPAuthenticationError +from lxml import html +from celery import uuid +from smtplib import (SMTPException, SMTPRecipientsRefused, + SMTPResponseException, SMTPAuthenticationError) -from django.core.mail import EmailMultiAlternatives, get_connection +from django.core.mail import EmailMessage, EmailMultiAlternatives, get_connection from django.db import transaction, DatabaseError +from django.db import models from django.template.loader import get_template from django.utils.html import strip_tags -from celery import uuid from django.utils.text import Truncator -from lxml import html +from django.utils import timezone -from .tasks import invia_mail -from base.models import * -from base.tratti import * +from jorvik import settings +from base.models import ModelloSemplice, ConAllegati +from base.tratti import ConMarcaTemporale from social.models import ConGiudizio +from .tasks import invia_mail + logger = logging.getLogger('posta') @@ -33,24 +36,14 @@ class ErrorePostaFatale(ErrorePosta): class Messaggio(ModelloSemplice, ConMarcaTemporale, ConGiudizio, ConAllegati): - logging = getattr(settings, 'POSTA_LOG_DEBUG', False) - SUPPORTO_EMAIL = 'supporto@gaia.cri.it' SUPPORTO_NOME = 'Supporto Gaia' + SUPPORTO_EMAIL = 'supporto@gaia.cri.it' NOREPLY_EMAIL = 'noreply@gaia.cri.it' - class Meta: - verbose_name = "Messaggio di posta" - verbose_name_plural = "Messaggi di posta" - permissions = ( - ("view_messaggio", "Can view messaggio"), - ) - LUNGHEZZA_MASSIMA_OGGETTO = 256 - - # Numero massimo di tentativi di invio prima di rinunciare all'invio - TENTATIVI_MAX = 3 + TENTATIVI_MAX = 3 # Numero massimo di tentativi di invio prima di rinunciare all'invio # Limite oggetto dato da RFC 2822 e' 998 caratteri in oggetto, ma ridotto per comodita oggetto = models.CharField(max_length=LUNGHEZZA_MASSIMA_OGGETTO, db_index=True, @@ -59,28 +52,25 @@ class Meta: ultimo_tentativo = models.DateTimeField(blank=True, null=True, default=None) terminato = models.DateTimeField(blank=True, null=True, default=None, - help_text="La data di termine dell'invio. Questa data e' impostata " - "quando l'invio e' terminato con successo, oppure quando " - "sono esauriti i tentativi di invio.") + help_text="La data di termine dell'invio. Questa data e' impostata " + "quando l'invio e' terminato con successo, oppure quando " + "sono esauriti i tentativi di invio.") # Il mittente e' una persona o None (il sistema di Gaia) mittente = models.ForeignKey("anagrafica.Persona", default=None, null=True, blank=True, on_delete=models.CASCADE) rispondi_a = models.ForeignKey("anagrafica.Persona", default=None, null=True, blank=True, - related_name="messaggi_come_rispondi_a", on_delete=models.CASCADE) + related_name="messaggi_come_rispondi_a", on_delete=models.CASCADE) # Flag per i messaggi cancellati (perche' obsoleti) eliminato = models.BooleanField(default=False, null=False) - tentativi = models.IntegerField(default=0, help_text="Il numero di tentativi di invio effettuati per questo " "messaggio. Quando questo numero supera il massimo, non " "verranno effettuati nuovi tentativi.") - utenza = models.BooleanField(default=False, help_text="Se l'utente possiede un'email differente da quella " "utilizzata per il login, il messaggio viene recapito ad " "entrambe") - task_id = models.CharField(max_length=36, blank=True, null=True, default=None, - help_text="ID del task Celery per lo smistamento di questo messaggio.") + help_text="ID del task Celery per lo smistamento di questo messaggio.") @property def destinatari(self): @@ -310,31 +300,37 @@ def in_coda(cls): return cls.objects.filter(terminato__isnull=True).order_by('ultima_modifica') @classmethod - def invia_raw(cls, oggetto, corpo_html, email_mittente, - reply_to=None, lista_email_destinatari=None, - allegati=None, fallisci_silenziosamente=False, - **kwargs): - """ - Questo metodo puo' essere usato per inviare una e-mail - immediatamente. - """ + def invia_raw(cls, oggetto, corpo_html, email_mittente, reply_to=None, + lista_email_destinatari=None, allegati=None, fallisci_silenziosamente=False, **kwargs): + """ Questo metodo puo' essere usato per inviare una e-mail immediatamente. """ plain_text = strip_tags(corpo_html) lista_reply_to = [reply_to] if reply_to else [] lista_email_destinatari = lista_email_destinatari or [] - allegati = allegati or [] + attachments = allegati or [] - msg = EmailMultiAlternatives( + send_email_class = EmailMultiAlternatives if len(lista_email_destinatari) > 1 else EmailMessage + msg = send_email_class( subject=oggetto, body=plain_text, from_email=email_mittente, reply_to=lista_reply_to, to=lista_email_destinatari, - **kwargs - ) - msg.attach_alternative(corpo_html, "text/html") - for allegato in allegati: - msg.attach_file(allegato.file.path) + **kwargs) + + try: + msg.attach_alternative(corpo_html, "text/html") + except AttributeError: + msg.content_subtype = "html" + + if type(attachments) == list: + for attachment in attachments: + if hasattr(attachment, 'file'): + msg.attach_file(attachment.file.path) + else: + filename = os.path.basename(attachments.name) + msg.attach(filename, attachments.read()) + return msg.send(fail_silently=fallisci_silenziosamente) @staticmethod @@ -359,8 +355,9 @@ def _smaltisci_coda(dimensione_massima=50): messaggio.invia() @staticmethod - def costruisci_email(oggetto='Nessun oggetto', modello='email_vuoto.html', corpo=None, mittente=None, - destinatari=None, allegati=None, **kwargs): + def costruisci_email(oggetto='Nessun oggetto', modello='email_vuoto.html', + corpo=None, mittente=None, destinatari=None, + allegati=None, **kwargs): """ :param oggetto: Oggetto del messaggio. :param modello: Modello da utilizzare per l'invio. @@ -400,8 +397,8 @@ def costruisci_email(oggetto='Nessun oggetto', modello='email_vuoto.html', corpo return m @staticmethod - def costruisci_e_invia(oggetto=None, modello=None, corpo=None, mittente=None, destinatari=None, allegati=None, - **kwargs): + def costruisci_e_invia(oggetto=None, modello=None, corpo=None, mittente=None, + destinatari=None, allegati=None, **kwargs): """ Scorciatoia per costruire rapidamente un messaggio di posta e inviarlo immediatamente. IMPORTANTE. Non adatto per messaggi con molti destinatari. In caso di fallimento immediato, il messaggio @@ -415,14 +412,16 @@ def costruisci_e_invia(oggetto=None, modello=None, corpo=None, mittente=None, de :return: Un oggetto Messaggio inviato. """ - msg = Messaggio.costruisci_email(oggetto=oggetto, modello=modello, corpo=corpo, mittente=mittente, - destinatari=destinatari, allegati=allegati, **kwargs) + msg = Messaggio.costruisci_email(oggetto=oggetto, modello=modello, + corpo=corpo, mittente=mittente, + destinatari=destinatari, + allegati=allegati, **kwargs) msg.invia() return msg @staticmethod - def costruisci_e_accoda(oggetto=None, modello=None, corpo=None, mittente=None, destinatari=None, allegati=None, - **kwargs): + def costruisci_e_accoda(oggetto=None, modello=None, corpo=None, mittente=None, + destinatari=None, allegati=None, **kwargs): """ Scorciatoia per costruire rapidamente un messaggio di posta e inviarlo alla coda celery. :param oggetto: Oggetto deltilizzare per l'invio. @@ -434,9 +433,10 @@ def costruisci_e_accoda(oggetto=None, modello=None, corpo=None, mittente=None, d :return: Un oggetto Messaggio accodato. """ - - msg = Messaggio.costruisci_email(oggetto=oggetto, modello=modello, corpo=corpo, mittente=mittente, - destinatari=destinatari, allegati=allegati, **kwargs) + msg = Messaggio.costruisci_email(oggetto=oggetto, modello=modello, + corpo=corpo, mittente=mittente, + destinatari=destinatari, + allegati=allegati, **kwargs) # Crea un ID per il task Celery msg.task_id = uuid() @@ -446,6 +446,13 @@ def costruisci_e_accoda(oggetto=None, modello=None, corpo=None, mittente=None, d return msg + class Meta: + verbose_name = "Messaggio di posta" + verbose_name_plural = "Messaggi di posta" + permissions = ( + ("view_messaggio", "Can view messaggio"), + ) + def __str__(self): return self.oggetto diff --git a/posta/tasks.py b/posta/tasks.py index f2e4476ee..d3b55d7a2 100644 --- a/posta/tasks.py +++ b/posta/tasks.py @@ -6,8 +6,7 @@ @shared_task(bind=True) def invia_mail(self, pk): - - from posta.models import Messaggio + from .models import Messaggio messaggio = Messaggio.objects.get(pk=pk) logger.info("messaggio id=%d" % pk) diff --git a/posta/templates/email_autorizzazione_richiesta.html b/posta/templates/email_autorizzazione_richiesta.html index e7c092889..cb29ce29f 100644 --- a/posta/templates/email_autorizzazione_richiesta.html +++ b/posta/templates/email_autorizzazione_richiesta.html @@ -8,14 +8,9 @@

    Azioni disponibili: - CONCEDI - o - NEGA - l'autorizzazione. -

    - -

    Alla pagina "Richieste" su Gaia trovi l'elenco completo delle - richieste di autorizzazione in attesa della tua firma. + CONCEDI o + NEGA l'autorizzazione.

    +

    Alla pagina "Richieste" su Gaia trovi l'elenco completo delle richieste di autorizzazione in attesa della tua firma.

    {% endblock %} diff --git a/posta/templates/email_corso_base_invito.html b/posta/templates/email_corso_base_invito.html index 543fcb947..e0b9d6a0d 100644 --- a/posta/templates/email_corso_base_invito.html +++ b/posta/templates/email_corso_base_invito.html @@ -1,17 +1,10 @@ {% extends 'email.html' %} {% block corpo %} -

    Ciao {{ persona.nome }}!

    -

    Ricevi questo messaggio perché sei stato invitato ad un Corso Base per - diventare Volontari{{ persona.genere_o_a }}!

    -

    Il corso è organizzato dal {{ corso.sede.link|safe }} e avrà inizio - il {{ corso.data_inizio }}.

    - -

    - - Clicca qui per confermare l'invito e perfezionare l'iscrizione al corso. - -

    - +

    Ricevi questo messaggio perché sei stato invitato ad un Corso per diventare Volontari{{ persona.genere_o_a }}!

    +

    Il corso è organizzato dal {{ corso.sede.link|safe }} e avrà inizio il {{ corso.data_inizio }}.

    +

    + Clicca qui per confermare l'invito e perfezionare l'iscrizione al corso. +

    {% endblock %} diff --git a/posta/templates/email_corso_base_iscritto.html b/posta/templates/email_corso_base_iscritto.html index 727526eea..d1961de54 100644 --- a/posta/templates/email_corso_base_iscritto.html +++ b/posta/templates/email_corso_base_iscritto.html @@ -1,17 +1,10 @@ {% extends 'email.html' %} {% block corpo %} -

    Ciao {{ persona.nome }}!

    -

    Ricevi questo messaggio perché sei stato iscritto ad un Corso Base per - diventare Volontari{{ persona.genere_o_a }}!

    -

    Il corso è organizzato dal {{ corso.sede.link|safe }} e avrà inizio - il {{ corso.data_inizio }}.

    - -

    - - Clicca qui per aprire la pagina del Corso con maggiori informazioni. - -

    - +

    Ricevi questo messaggio perché ti sei iscritt{{ persona.genere_o_a }} a un Corso{% if not corso.is_nuovo_corso %} Base{%endif%} per diventare Volontari{{ persona.genere_o_a }}!

    +

    Il corso è organizzato dal {{ corso.sede.link|safe }} e avrà inizio il {{ corso.data_inizio }}.

    +

    + Clicca qui per aprire la pagina del Corso con maggiori informazioni. +

    {% endblock %} diff --git a/posta/templates/email_corso_informa_participants.html b/posta/templates/email_corso_informa_participants.html new file mode 100644 index 000000000..cbaecf4d9 --- /dev/null +++ b/posta/templates/email_corso_informa_participants.html @@ -0,0 +1,11 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Ciao, questo messaggio è stato inviato dal direttore {{ corso.nome }}.

    + +
    +

    {{ message|safe }}

    +
    + +

    Clicca qui per aprire la pagina del Corso con maggiori informazioni.

    +{% endblock %} \ No newline at end of file diff --git a/posta/templates/email_corso_iscritti_aggiungi_esito_negativo.html b/posta/templates/email_corso_iscritti_aggiungi_esito_negativo.html new file mode 100644 index 000000000..63969b51e --- /dev/null +++ b/posta/templates/email_corso_iscritti_aggiungi_esito_negativo.html @@ -0,0 +1,14 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Ciao, perché il Direttore del {{ corso.nome }} ti inviti è necessario che + {% if esito == corso.NON_HAI_CARICATO_DOCUMENTI_PERSONALI %} carichi + {% elif esito == corso.NON_HAI_DOCUMENTO_PERSONALE_VALIDO %} aggiorni il tuo profilo allegando + {% endif %} + un documento di identità in corso di validità, proseguendo su questo link. +

    + +
    + +

    Clicca qui per aprire la pagina del Corso con maggiori informazioni.

    +{% endblock %} diff --git a/posta/templates/email_corso_questionario_gradimento.html b/posta/templates/email_corso_questionario_gradimento.html new file mode 100644 index 000000000..0a3506850 --- /dev/null +++ b/posta/templates/email_corso_questionario_gradimento.html @@ -0,0 +1,9 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Ciao, questo messaggio è stato inviato dal direttore del {{ corso.nome }}.

    +

    {{ message|safe }}

    +

    Clicca qui per aprire il Questionario di gradimento.

    +{% endblock %} + +{% block footer %} {% endblock %} \ No newline at end of file diff --git a/posta/templates/email_corso_utente_ritirato_iscrizione.html b/posta/templates/email_corso_utente_ritirato_iscrizione.html new file mode 100644 index 000000000..2dc4d759e --- /dev/null +++ b/posta/templates/email_corso_utente_ritirato_iscrizione.html @@ -0,0 +1,5 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Buongiorno direttore, {{ partecipante }} ha ritirato la sua iscrizione dal Corso

    +{% endblock %} diff --git a/posta/templates/email_expired_course_titolo_personale.html b/posta/templates/email_expired_course_titolo_personale.html new file mode 100644 index 000000000..8527fd047 --- /dev/null +++ b/posta/templates/email_expired_course_titolo_personale.html @@ -0,0 +1,11 @@ + + + + + + +

    Ciao, {{ persona.nome }}.

    +

    Oggi è scaduto il tuo titolo {{ title.titolo.nome }},
    + è necessario rinnovarlo per poter partecipare in futuro ai prossimi corsi che richiedono questo titolo.

    + + diff --git a/posta/templates/email_volontario_corso_esito.html b/posta/templates/email_volontario_corso_esito.html new file mode 100644 index 000000000..9b32064eb --- /dev/null +++ b/posta/templates/email_volontario_corso_esito.html @@ -0,0 +1,34 @@ +{% extends 'email.html' %} + +{% block corpo %} +

    Ciao {{ persona.nome }}!

    +

    Il Direttore del {{ corso }} ha appena compilato il verbale di fine corso su Gaia.

    + + {% if partecipazione.esito_esame == partecipazione.IDONEO %} +

    Congratulazioni!

    +

    _______ NEW TEXT _________

    +

    Da ora in avanti accedendo a Gaia potrai vedere le attività che si svolgono + in comitato e partecipare, oltre a una serie di altre importanti funzionalità + che da oggi puoi utilizzare!

    + + {% elif partecipazione.ammissione == partecipazione.ASSENTE %} +

    Purtroppo sei risultat{{ persona.genere_o_a }} assente al corso.

    + + {% elif partecipazione.ammissione == partecipazione.NON_AMMESSO %} +

    Purtroppo non sei stat{{ persona.genere_o_a }} ammess{{ persona.genere_o_a }} al + corso per la seguente motivazione:

    +

    {{ partecipazione.motivo_non_ammissione }}

    + + {% else %} +

    Purtroppo non hai superato il corso.

    + {% endif %} + + + {% if partecipazione.esito_esame != partecipazione.IDONEO %} +

    Non ti scoraggiare, perché potrai riprovarci non appena un nuovo corso base verrà attivato!

    + {% endif %} + +

     

    +

    Il verbale è stato registrato da {{ mittente.nome_completo }} il {{ partecipazione.ultima_modifica }}.

    +

    Per avere maggiori informazioni in merito rispondi pure a questa email.

    +{% endblock %} diff --git a/posta/viste.py b/posta/viste.py index e7825680b..9b1cd7024 100755 --- a/posta/viste.py +++ b/posta/viste.py @@ -1,7 +1,8 @@ from datetime import datetime, timedelta from django.core.paginator import Paginator -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 +from django.utils.timezone import now from anagrafica.models import Persona from anagrafica.permessi.costanti import ERRORE_PERMESSI @@ -41,7 +42,7 @@ def posta(request, me, direzione="in-arrivo", pagina=1, messaggio_id=None): if messaggio_id is None: messaggio = None else: - messaggio = Messaggio.objects.get(pk=messaggio_id) + messaggio = get_object_or_404(Messaggio, pk=messaggio_id) # Controlla che io abbia i permessi per leggere il messaggio: # - Devo essere o mittente o destinatario diff --git a/requirements.txt b/requirements.txt index 43bd25cc6..0b52fce56 100755 --- a/requirements.txt +++ b/requirements.txt @@ -46,5 +46,6 @@ djangorestframework==3.6 xhtml2pdf xlwt==1.3.0 django-oidc-provider==0.6.2 +xlrd https://github.com/emfcamp/python-barcode/archive/0.7.zip # pyBarcode==0.7 https://github.com/nephila/django-filer/archive/296fa6170a3749532b85ceb033eeae0fb839e9a6.zip diff --git a/social/models.py b/social/models.py index 4ed44ca22..6e7018520 100755 --- a/social/models.py +++ b/social/models.py @@ -1,12 +1,10 @@ -from datetime import datetime from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models + from base.models import ModelloSemplice from base.tratti import ConMarcaTemporale -__author__ = 'alfioemanuele' - class Giudizio(ModelloSemplice, ConMarcaTemporale): """ diff --git a/static_page/admin.py b/static_page/admin.py index c34bb88fe..59bcd65bd 100644 --- a/static_page/admin.py +++ b/static_page/admin.py @@ -1,8 +1,88 @@ +from xlrd import open_workbook + from django.contrib import admin +from django.conf.urls import url +from django.shortcuts import HttpResponse, redirect, render +from django.core.urlresolvers import reverse +from django.contrib import messages from .models import Page +from .forms import ImportAndGenerateStaticPage @admin.register(Page) -class AdminCorsoBase(admin.ModelAdmin): +class StaticPageAdmin(admin.ModelAdmin): list_display = ['title', 'slug',] + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + url(r'^import/$', + self.admin_site.admin_view(self.import_and_generate_static_page), + name='static_page_page_import_and_generate'), + ] + + urls = custom_urls + urls + return urls + + def _process_imported_file(self, file, imported_type): + columns = { + ImportAndGenerateStaticPage.CATALOGO_CORSI: + ('Sigla Corso', 'Nome del Corso'), #'Qualifica che si ottiene', 'Spiegazione del corso'), + ImportAndGenerateStaticPage.GLOSSARIO_CORSI: + ('Acronimo/termine', 'Significato') + } + + table = ['\n', '
    '] + td = "%s\n\t" * len(columns[imported_type]) + tr = "\n\t%s\n" % td + html = table[0] + tr % (columns[imported_type]) + + xls = open_workbook(file_contents=file.read()) + sheet = xls.sheet_by_index(0) + + if imported_type == ImportAndGenerateStaticPage.GLOSSARIO_CORSI: + for i in range(0, sheet.nrows): + row = sheet.row_slice(i) + sigla = row[0].value.strip() + text = row[1].value.strip() + html += tr % (sigla, text) + + elif imported_type == ImportAndGenerateStaticPage.CATALOGO_CORSI: + for i in range(0, sheet.nrows): + row = sheet.row_slice(i) + + if i == 0 or row[0].value == 'N.': + continue + + sigla = row[1].value.strip() + text = row[2].value.strip() + html += tr % (sigla, text) + + html += table[1] + return html + + def import_and_generate_static_page(self, request): + redirect_url = redirect(reverse("admin:static_page_page_changelist")) + + if request.method == 'POST': + form = ImportAndGenerateStaticPage(request.POST, request.FILES) + if form.is_valid(): + cd = form.cleaned_data + imported_type, file = cd['type'], cd['file'] + file_processed = self._process_imported_file(file, imported_type) + title = dict(ImportAndGenerateStaticPage.TYPE_CHOICES)[imported_type] + + page, created = Page.objects.get_or_create(slug=imported_type, title=title) + if page: + page.text = file_processed + page.save() + + messages.success(request, 'I dati sono stati importati con successo!') + return redirect_url + else: + form = ImportAndGenerateStaticPage() + + return render(request, 'import_and_generate_static_page.html', { + 'form': form + }) diff --git a/static_page/forms.py b/static_page/forms.py new file mode 100644 index 000000000..9676b58a3 --- /dev/null +++ b/static_page/forms.py @@ -0,0 +1,14 @@ +from django import forms +from .models import Page + + +class ImportAndGenerateStaticPage(forms.Form): + GLOSSARIO_CORSI = "glossario-corsi" + CATALOGO_CORSI = "catalogo-corsi" + TYPE_CHOICES = [ + (GLOSSARIO_CORSI, 'Glossario Corsi'), + (CATALOGO_CORSI, 'Catalogo Corsi'), + ] + + type = forms.ChoiceField(choices=TYPE_CHOICES) + file = forms.FileField() diff --git a/static_page/models.py b/static_page/models.py index a2b735a4e..02967820b 100644 --- a/static_page/models.py +++ b/static_page/models.py @@ -12,5 +12,5 @@ def __str__(self): return str(self.title) class Meta: - verbose_name = 'Pagina static' + verbose_name = 'Pagina statica' verbose_name_plural = 'Pagine statiche' diff --git a/static_page/templates/admin/static_page/change_list.html b/static_page/templates/admin/static_page/change_list.html new file mode 100644 index 000000000..7c04a9d2d --- /dev/null +++ b/static_page/templates/admin/static_page/change_list.html @@ -0,0 +1,25 @@ +{% extends "admin/change_list.html" %} +{% load admin_urls i18n %} + + +{% block object-tools %} + +{% endblock %} + diff --git a/static_page/templates/import_and_generate_static_page.html b/static_page/templates/import_and_generate_static_page.html new file mode 100644 index 000000000..65ba9d9b9 --- /dev/null +++ b/static_page/templates/import_and_generate_static_page.html @@ -0,0 +1,10 @@ +{% extends "admin/base_custom.html" %} +{% load i18n admin_urls l10n %} + +{% block content %} +
    + {{ form.as_p }} + {% csrf_token %} +

    +
    +{% endblock %} \ No newline at end of file diff --git a/static_page/views.py b/static_page/views.py index dda61688f..853a40576 100644 --- a/static_page/views.py +++ b/static_page/views.py @@ -62,8 +62,8 @@ def monitoraggio(request, me): context['user_id'] = typeform.get_user_pk context['all_forms_are_completed'] = typeform.all_forms_are_completed - # # Get celery_task_id - # # TODO: ajax polling task is ready + # Get celery_task_id + # TODO: ajax polling task is ready # prefix = typeform.CELERY_TASK_PREFIX # message_storage = get_messages(request) # if len(message_storage) > 0: diff --git a/survey/__init__.py b/survey/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/survey/admin.py b/survey/admin.py new file mode 100644 index 000000000..003134815 --- /dev/null +++ b/survey/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from .models import * + + +class QuestionInline(admin.TabularInline): + model = Question + extra = 0 + + +@admin.register(Question) +class AdminQuestion(admin.ModelAdmin): + list_display = ['text', 'survey', 'is_active'] + list_filter = ['is_active', ] + + +@admin.register(QuestionGroup) +class AdminQuestionGroup(admin.ModelAdmin): + list_display = ['name',] + + +@admin.register(Survey) +class AdminSurvey(admin.ModelAdmin): + inlines = [QuestionInline, ] + list_display = ['text', 'is_active'] + list_filter = ['is_active',] + + +@admin.register(SurveyResult) +class AdminSurveyResult(admin.ModelAdmin): + list_display = ['course', 'user', 'question', 'response', 'created_at', 'updated_at'] + raw_id_fields = ['user', 'survey', 'question', 'course'] diff --git a/survey/apps.py b/survey/apps.py new file mode 100644 index 000000000..3d5425a3a --- /dev/null +++ b/survey/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SurveyConfig(AppConfig): + name = 'survey' diff --git a/survey/forms.py b/survey/forms.py new file mode 100644 index 000000000..7aa5f9df8 --- /dev/null +++ b/survey/forms.py @@ -0,0 +1,48 @@ +from datetime import datetime, timedelta +from django import forms +from django.forms import ModelForm + +from .models import Survey + + +class RespondToCourseSurveyForm(ModelForm): + class Meta: + model = Survey + fields = ['text'] + + def __init__(self, *args, **kwargs): + self.me = kwargs.pop('me') + self.course = kwargs.pop('course') + super().__init__(*args, **kwargs) + self.fields.pop('text') + + survey = self.instance + responses = survey.get_responses_dict(self.me) + for question in survey.get_questions(): + if not question.is_active: + # skip inactive questions + continue + + qid = question.qid + self.fields[qid] = forms.CharField(label=question.text) + field = self.fields[qid] + + if qid in responses: + # Set input values if user has already voted + response_for_question = responses[qid] + field.initial = response_for_question['response'] + + # Disable editing after N time passed since user voted + delta = datetime.now() - response_for_question['object'].created_at + if delta >= timedelta(days=2): + field.widget.attrs["disabled"] = "disabled" + + if survey.is_course_admin(self.me, self.course): + # Director of course can see questions but cannot vote + field.widget.attrs["disabled"] = "disabled" + + if question.required: + field.required = True + field.widget.attrs["class"] = "required" + else: + field.required = False diff --git a/survey/migrations/0001_initial.py b/survey/migrations/0001_initial.py new file mode 100644 index 000000000..c08c42b49 --- /dev/null +++ b/survey/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-12-04 15:13 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('anagrafica', '0049_auto_20181028_1639'), + ] + + operations = [ + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.CharField(max_length=255)), + ('is_active', models.BooleanField(default=True)), + ('required', models.BooleanField(default=True, verbose_name='Obbligatorio')), + ], + options={ + 'verbose_name': 'Domanda', + 'verbose_name_plural': 'Domande', + }, + ), + migrations.CreateModel( + name='Survey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(default=True)), + ('text', models.CharField(max_length=255)), + ], + options={ + 'verbose_name': 'Questionario di gradimento', + 'verbose_name_plural': 'Questionari di gradimento', + }, + ), + migrations.CreateModel( + name='SurveyResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('response', models.TextField(blank=True, max_length=1000, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='survey.Question')), + ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='survey.Survey')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='anagrafica.Persona')), + ], + options={ + 'verbose_name': "Risposta dell'utente", + 'verbose_name_plural': 'Risposte degli utenti', + }, + ), + migrations.AddField( + model_name='question', + name='survey', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='survey.Survey'), + ), + ] diff --git a/survey/migrations/0002_surveyresult_course.py b/survey/migrations/0002_surveyresult_course.py new file mode 100644 index 000000000..1738fa358 --- /dev/null +++ b/survey/migrations/0002_surveyresult_course.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2018-12-04 16:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('formazione', '0031_corsobase_survey'), + ('survey', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='surveyresult', + name='course', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='formazione.CorsoBase'), + ), + ] diff --git a/survey/migrations/0003_auto_20190329_1534.py b/survey/migrations/0003_auto_20190329_1534.py new file mode 100644 index 000000000..db82d9630 --- /dev/null +++ b/survey/migrations/0003_auto_20190329_1534.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-03-29 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('survey', '0002_surveyresult_course'), + ] + + operations = [ + migrations.CreateModel( + name='QuestionGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.AddField( + model_name='question', + name='question_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='survey.QuestionGroup'), + ), + ] diff --git a/survey/migrations/0004_survey_survey_type.py b/survey/migrations/0004_survey_survey_type.py new file mode 100644 index 000000000..1302ab745 --- /dev/null +++ b/survey/migrations/0004_survey_survey_type.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2019-04-02 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('survey', '0003_auto_20190329_1534'), + ] + + operations = [ + migrations.AddField( + model_name='survey', + name='survey_type', + field=models.CharField(blank=True, choices=[('f', 'Formazione')], max_length=3, null=True), + ), + ] diff --git a/survey/migrations/__init__.py b/survey/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/survey/models.py b/survey/models.py new file mode 100644 index 000000000..83309cb62 --- /dev/null +++ b/survey/models.py @@ -0,0 +1,149 @@ +from django.db import models +from anagrafica.models import Persona + + +class Survey(models.Model): + FORMAZIONE = 'f' + SURVEY_TYPE_CHOICES = ( + (FORMAZIONE, 'Formazione'), + ) + + is_active = models.BooleanField(default=True) + text = models.CharField(max_length=255) + survey_type = models.CharField(max_length=3, choices=SURVEY_TYPE_CHOICES, + null=True, blank=True) + + def get_questions(self): + return self.question_set.all() + + def get_my_responses(self, user): + return SurveyResult.objects.filter(user=user, survey=self) + + def get_responses_dict(self, me): + d = dict() + for r in self.get_my_responses(me): + qid = r.question.qid + if qid not in d: + d[qid] = dict(response=r.response, object=r) + return d + + def is_course_admin(self, me, course): + from anagrafica.permessi.costanti import MODIFICA + return me and me.permessi_almeno(course, MODIFICA) + + def can_vote(self, me, course): + """ + Cannot vote because is not participant of the course + or the course is not ended yet. + """ + if self.is_course_admin(me, course): + return True + elif me in [p.persona for p in course.partecipazioni_confermate()]: + if course.concluso: + return True + return False + + def has_user_responses(self, course): + return SurveyResult.get_responses_for_course(course).exists() + + @classmethod + def survey_for_corso(cls): + try: + return cls.objects.get(id=2) + except Survey.DoesNotExist: + s = cls.objects.filter(survey_type=cls.FORMAZIONE, is_active=True) + return s.last() if s.exists() else None + + class Meta: + verbose_name = 'Questionario di gradimento' + verbose_name_plural = 'Questionari di gradimento' + + def __str__(self): + return str(self.text) + + +class Question(models.Model): + TEXT = 'text' + RADIO = 'radio' + SELECT = 'select' + SELECT_MULTIPLE = 'select-multiple' + INTEGER = 'integer' + + QUESTION_TYPES = ( + (TEXT, 'text'), + (RADIO, 'radio'), + (SELECT, 'select'), + (SELECT_MULTIPLE, 'Select Multiple'), + (INTEGER, 'integer'), + ) + + text = models.CharField(max_length=255) + survey = models.ForeignKey(Survey) + is_active = models.BooleanField(default=True) + required = models.BooleanField(default=True, verbose_name='Obbligatorio') + question_group = models.ForeignKey('QuestionGroup', null=True, blank=True) + + # question_type = models.CharField(max_length=100, choices=QUESTION_TYPES, + # default=TEXT, null=True, blank=True) + + @property + def qid(self): + return 'qid_%s' % self.pk + + class Meta: + verbose_name = 'Domanda' + verbose_name_plural = 'Domande' + + def __str__(self): + return str(self.text) + + +class SurveyResult(models.Model): + user = models.ForeignKey(Persona) + course = models.ForeignKey('formazione.CorsoBase', blank=True, null=True) + survey = models.ForeignKey(Survey) + question = models.ForeignKey(Question) + response = models.TextField(max_length=1000, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @classmethod + def get_responses_for_course(cls, course): + return cls.objects.filter(course=course) + + @classmethod + def generate_report_for_course(cls, course): + import csv + from django.shortcuts import HttpResponse + + filename = "Questionario di gradimento [%s].csv" % course.nome + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + + writer = csv.writer(response, delimiter=';') + writer.writerow(['Corso', 'Domanda', 'Risposta', 'Creato', 'Modificato']) + + for result in cls.get_responses_for_course(course): + writer.writerow([ + course.nome, + result.question, + result.response, + result.created_at, + result.updated_at + ]) + + return response + + class Meta: + verbose_name = "Risposta dell'utente" + verbose_name_plural = "Risposte degli utenti" + + def __str__(self): + return "%s = %s" % (self.survey, self.user) + + +class QuestionGroup(models.Model): + name = models.CharField(max_length=255) + + def __str__(self): + return str(self.name) diff --git a/survey/templates/corso_questionario_di_gradimento.html b/survey/templates/corso_questionario_di_gradimento.html new file mode 100644 index 000000000..002e69495 --- /dev/null +++ b/survey/templates/corso_questionario_di_gradimento.html @@ -0,0 +1,43 @@ +{% extends 'aspirante_corso_base_scheda.html' %} +{% load utils %} +{% load bootstrap3 %} +{% load survey_templatetags %} + +{% block scheda_titolo %}Questionario di gradimento{% endblock %} +{% block scheda_contenuto %} +
    +
    +

    Questionario di gradimento

    +
    +
    +

    {{ survey.text }}

    + + {% if puo_modificare and has_responses %} +

    + Scaricare il Report con le risposte dei partecipanti +

    +
    + {% endif %} + +
    + {% csrf_token %} + + {% bootstrap_form form as bootstrap_form %} + {% add_questions_groups_to_survey_form bootstrap_form survey %} + + {% if puo_modificare %} + {# Director of course can only read form data #} + {% else %} + + {% endif %} +
    +
    +
    + + + +{% endblock %} diff --git a/survey/templatetags/__init__.py b/survey/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/survey/templatetags/survey_templatetags.py b/survey/templatetags/survey_templatetags.py new file mode 100644 index 000000000..6bb38925f --- /dev/null +++ b/survey/templatetags/survey_templatetags.py @@ -0,0 +1,29 @@ +from django import template +from django.utils.safestring import mark_safe + + +register = template.Library() + +@register.simple_tag +def add_questions_groups_to_survey_form(form, survey): + from lxml import etree + + html_output = '' + added_groups = list() + questions = survey.question_set.all() + + for f in form.split('
    '): + div_formgroup = f + "
    " + html = etree.HTML(div_formgroup) + if html is not None: + input_xpath = html.xpath('//div[@class="form-group"]//input/@name') + question_id = input_xpath[0].replace('qid_', '') + question_group = questions.get(id=question_id).question_group + + if question_group not in added_groups: + added_groups.append(question_group) + html_output += '
    %s
    ' % question_group + + html_output += div_formgroup + + return mark_safe(html_output) diff --git a/survey/tests.py b/survey/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/survey/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/survey/urls.py b/survey/urls.py new file mode 100644 index 000000000..24520a6d8 --- /dev/null +++ b/survey/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from .views import course_survey, course_survey_download_results + + +app_label = 'survey' +urlpatterns = [ + url(r'^course/(?P[0-9]+)/$', course_survey, name='course'), + url(r'^course/(?P[0-9]+)/download/$', course_survey_download_results, + name='course_download_results'), +] diff --git a/survey/views.py b/survey/views.py new file mode 100644 index 000000000..392d828eb --- /dev/null +++ b/survey/views.py @@ -0,0 +1,70 @@ +from django.shortcuts import render, redirect, get_object_or_404, HttpResponse +from django.core.urlresolvers import reverse +from django.contrib import messages + +from autenticazione.funzioni import pagina_privata + +from anagrafica.permessi.costanti import ERRORE_PERMESSI, MODIFICA +from formazione.models import CorsoBase +from .models import Question, Survey, SurveyResult +from .forms import RespondToCourseSurveyForm + + +@pagina_privata +def course_survey(request, me, pk): + course = get_object_or_404(CorsoBase, pk=pk) + course_info_page_redirect = redirect(reverse('aspirante:info', args=[pk])) + + try: + survey = Survey.objects.get(corsobase=course) + except Survey.DoesNotExist: + messages.error(request, "Il corso non ha un questionario impostato. Contattare l'amministrazione.") + return course_info_page_redirect + + if not course.concluso: + messages.error(request, "Il corso non è ancora terminato (non è superata la data di esame)") + return course_info_page_redirect + + if not survey.can_vote(me, course): + return redirect(ERRORE_PERMESSI) + + form = RespondToCourseSurveyForm(request.POST or None, instance=survey, + me=me, course=course) + if form.is_valid(): + cd = form.cleaned_data + for question in survey.get_questions(): + if question.qid in cd: + response = cd.get(question.qid) + result, created = SurveyResult.objects.get_or_create( + course=course, + user=me, + survey=survey, + question=question + ) + if result: + result.response = response + if created: + pass + result.save() + + messages.success(request, 'Grazie, abbiamo salvato le tue risposte.') + return redirect(reverse('survey:course', args=[course.pk])) + + context = { + 'corso': course, + 'survey': survey, + 'form': form, + 'puo_modificare': survey.is_course_admin(me, course), + 'has_responses': survey.has_user_responses(course), + } + return 'corso_questionario_di_gradimento.html', context + + +@pagina_privata +def course_survey_download_results(request, me, pk): + course = get_object_or_404(CorsoBase, pk=pk) + if not me.permessi_almeno(course, MODIFICA): + return redirect(ERRORE_PERMESSI) + + report = SurveyResult.generate_report_for_course(course) + return report \ No newline at end of file diff --git a/templates/two_factor/core/login.html b/templates/two_factor/core/login.html index ce0ce8fc3..35d5bb0d9 100644 --- a/templates/two_factor/core/login.html +++ b/templates/two_factor/core/login.html @@ -3,31 +3,20 @@ {% load bootstrap3 two_factor %} {% block pagina_titolo %}Accedi a Gaia{% endblock %} - -{% block avviso_titolo_classe %}text-success{% endblock %} - -{% block avviso_titolo %} - - Accedi a Gaia -{% endblock %} - +{% block avviso_titolo_classe %}{% endblock %} +{% block avviso_titolo %} Accedi a Gaia{% endblock %} {% block avviso_corpo %} -
    -
    {% csrf_token %} {{ wizard.management_form }} {% bootstrap_form wizard.form %} - +
    @@ -35,35 +24,26 @@

    O, in alternativa, usa uno dei dispositivi di backup

    {% for other in other_devices %} - + {% endfor %}

    {% endif %} + {% if backup_tokens %}

    Come ultima opzione, usa uno dei codici di backup

    - +

    {% endif %} -
    - -

     

    -
    +
    {% if wizard.steps.current == 'auth' %}

    Inserisci la tua email e la password che hai fornito alla registrazione.


    Recupera password

    Se non ricordi la tua password, puoi richiederne una nuova.

    - - - Recupera Password - + Recupera Password {% elif wizard.steps.current == 'token' %} {% if device.method == 'call' %}

    Stiamo chiamando il tuo telefono in questo momento, inserisci il codice che senti al telefono.

    @@ -76,7 +56,6 @@

    Recupera password

    Inserisci uno dei codici di backup.
    Dovresti aver generato questi codici, stampati e tenuti al sicuro.

    {% endif %} -
    diff --git a/ufficio_soci/elenchi.py b/ufficio_soci/elenchi.py index 2dba73e56..6e99d9c2f 100644 --- a/ufficio_soci/elenchi.py +++ b/ufficio_soci/elenchi.py @@ -654,6 +654,8 @@ def risultati(self): Q(Appartenenza.query_attuale(membro=Appartenenza.DIPENDENTE, sede__in=qs_sedi, al_giorno=oggi ).via("appartenenze"))) + + # print("dipendenti", dipendenti.values_list('pk', flat=True) ) r = Persona.objects.filter( Appartenenza.query_attuale( @@ -716,13 +718,31 @@ def risultati(self): base = base.filter(titoli_personali__in=TitoloPersonale.con_esito_ok()) for titolo in titoli: base = base.filter(titoli_personali__titolo=titolo) - return base.distinct('cognome', 'nome', 'codice_fiscale') + + return base.distinct('cognome', 'nome', 'codice_fiscale') def modulo(self): from .forms import ModuloElencoPerTitoli return ModuloElencoPerTitoli +class ElencoPerTitoliCorso(ElencoPerTitoli): + def risultati(self): + cd = self.modulo_riempito.cleaned_data + self.kwargs['cleaned_data'] = cd + + # Mostra persone con titoli scaduti/non scaduti + results = super().risultati() + return results.filter(titoli_personali__in=TitoloPersonale.con_esito_ok()) + + def modulo(self): + from .forms import ModuloElencoPerTitoliCorso + return ModuloElencoPerTitoliCorso + + def template(self): + return 'formazione_albo_inc_elenchi_persone_titoli.html' + + class ElencoTesseriniRichiesti(ElencoVistaSoci): REPORT_TYPE = ReportElenco.TESSERINI_RICHIESTI diff --git a/ufficio_soci/forms.py b/ufficio_soci/forms.py index c8e4cb79b..754b82864 100644 --- a/ufficio_soci/forms.py +++ b/ufficio_soci/forms.py @@ -72,6 +72,18 @@ class ModuloElencoPerTitoli(forms.Form): (METODO_AND, "Tutti i soci aventi TUTTI i titoli selezionati"), ) metodo = forms.ChoiceField(choices=METODI, initial=METODO_OR) + titoli = autocomplete_light.ModelMultipleChoiceField('TitoloAutocompletamento') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['titoli'].widget.attrs['placeholder'] = 'Seleziona uno o più titoli per la tua ricerca' + + +class ModuloElencoPerTitoliCorso(ModuloElencoPerTitoli): + titoli = autocomplete_light.ModelMultipleChoiceField( + 'TitoloCRIAutocompletamento', required=False) + show_only_active = forms.BooleanField(label='Seleziona solo i titoli attivi', + required=False) titoli = autocomplete_light.ModelMultipleChoiceField('TitoloAutocompletamento', help_text="Seleziona uno o più titoli per la tua ricerca.") diff --git a/ufficio_soci/reports.py b/ufficio_soci/reports.py index 005112404..be2d129b5 100644 --- a/ufficio_soci/reports.py +++ b/ufficio_soci/reports.py @@ -364,6 +364,6 @@ def make(self): # If the report is ready, download it without redirect user return report_db.download() else: - response = redirect(reverse('elenchi_richiesti_download')) + response = redirect(reverse('ufficio_soci:elenchi_richiesti_download')) messages.success(self.request, 'Attendi la generazione del report richiesto.') return response diff --git a/ufficio_soci/templates/us_elenchi_inc_vuoto.html b/ufficio_soci/templates/us_elenchi_inc_vuoto.html index e3a54411a..ed0908af1 100644 --- a/ufficio_soci/templates/us_elenchi_inc_vuoto.html +++ b/ufficio_soci/templates/us_elenchi_inc_vuoto.html @@ -4,7 +4,7 @@ {% block pagina_corpo %} -
    +
    {% block testo_pre_elenco %} {% endblock %} diff --git a/ufficio_soci/templates/us_elenchi_richiesti_download.html b/ufficio_soci/templates/us_elenchi_richiesti_download.html index 761e09601..2e8b051ce 100644 Binary files a/ufficio_soci/templates/us_elenchi_richiesti_download.html and b/ufficio_soci/templates/us_elenchi_richiesti_download.html differ diff --git a/ufficio_soci/urls.py b/ufficio_soci/urls.py new file mode 100644 index 000000000..522ad50e5 --- /dev/null +++ b/ufficio_soci/urls.py @@ -0,0 +1,42 @@ +from django.conf.urls import url +from . import viste + +app_label = 'ufficio_soci' +urlpatterns = [ + url(r'^$', viste.us), + url(r'^provvedimento/$', viste.us_provvedimento), + url(r'^aggiungi/$', viste.us_aggiungi), + url(r'^reclama/$', viste.us_reclama), + url(r'^reclama/(?P.*)/$', viste.us_reclama_persona), + url(r'^estensione/$', viste.us_estensione), + url(r'^estensione/(?P.*)/termina/$', viste.us_estensione_termina, name='us-termina-estensione'), + url(r'^trasferimento/$', viste.us_trasferimento), + url(r'^riserva/$', viste.us_riserva), + url(r'^riserva/(?P.*)/termina/$', viste.us_riserva_termina), + url(r'^dimissioni/(?P[0-9]+)/$', viste.us_dimissioni, name='us-dimissioni'), + url(r'^dimissioni/sostenitore/(?P[0-9]+)/$', viste.us_chiudi_sostenitore, name='us-chiudi-sostenitore'), + + url(r'^elenchi/download/$', viste.us_elenchi_richiesti_download, name='elenchi_richiesti_download'), + url(r'^elenchi/(?P.*)/$', viste.us_elenchi), + url(r'^quote/$', viste.us_quote), + url(r'^quote/nuova/$', viste.us_quote_nuova, name='us_quote_nuova'), + url(r'^ricevute/$', viste.us_ricevute), + url(r'^ricevute/(?P[0-9]+)/annulla/$', viste.us_ricevute_annulla), + url(r'^ricevute/nuova/$', viste.us_ricevute_nuova, name='us_ricevute_nuova'), + + url(r'^tesserini/$', viste.us_tesserini), + url(r'^tesserini/da-richiedere/$', viste.us_tesserini_da_richiedere), + url(r'^tesserini/senza-fototessera/$', viste.us_tesserini_senza_fototessera), + url(r'^tesserini/richiesti/$', viste.us_tesserini_richiesti), + url(r'^tesserini/rifiutati/$', viste.us_tesserini_rifiutati), + url(r'^tesserini/richiedi/(?P[0-9]+)/$', viste.us_tesserini_richiedi, name='us-tesserini-richiedi'), + url(r'^tesserini/emissione/$', viste.us_tesserini_emissione), + url(r'^tesserini/emissione/processa/$', viste.us_tesserini_emissione_processa), + url(r'^tesserini/emissione/scarica/$', viste.us_tesserini_emissione_scarica), + + url(r'^elenco/(?P.*)/(?P[0-9]+)/$', viste.us_elenco, name='elenco_page'), + url(r'^elenco/(?P.*)/download/$', viste.us_elenco_download), + url(r'^elenco/(?P.*)/messaggio/$', viste.us_elenco_messaggio, name='us-elenco-messaggio'), + url(r'^elenco/(?P.*)/modulo/$', viste.us_elenco_modulo), + url(r'^elenco/(?P.*)/$', viste.us_elenco), +] diff --git a/ufficio_soci/viste.py b/ufficio_soci/viste.py index 2fd3f261d..40130ae65 100644 --- a/ufficio_soci/viste.py +++ b/ufficio_soci/viste.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from django.core.paginator import Paginator from django.shortcuts import redirect, get_object_or_404 +from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from anagrafica.costanti import NAZIONALE, REGIONALE @@ -643,19 +644,18 @@ def us_elenco(request, me, elenco_id=None, pagina=1): def us_elenco_modulo(request, me, elenco_id): try: # Prova a ottenere l'elenco dalla sessione. - elenco = request.session["elenco_%s" % (elenco_id,)] + elenco = request.session["elenco_%s" % elenco_id] except KeyError: # Se l'elenco non e' piu' in sessione, potrebbe essere scaduto. raise ValueError("Elenco non presente in sessione.") if not elenco.modulo(): # No modulo? Vai all'elenco - return redirect("/us/elenco/%s/1/" % (elenco_id,)) + return redirect("/us/elenco/%s/1/" % elenco_id) form = elenco.modulo()(request.POST or None) if request.POST and form.is_valid(): # Modulo ok # Salva modulo in sessione request.session["elenco_modulo_%s" % elenco_id] = request.POST - # Redirigi alla prima pagina return redirect("/us/elenco/%s/1/" % elenco_id) diff --git a/veicoli/urls.py b/veicoli/urls.py new file mode 100644 index 000000000..eee14c167 --- /dev/null +++ b/veicoli/urls.py @@ -0,0 +1,22 @@ +from django.conf.urls import url +from . import viste + + +app_label = 'veicoli' +urlpatterns = [ + url(r'^$', viste.veicoli), + url(r'^elenco/$', viste.veicoli_elenco), + url(r'^autoparchi/$', viste.veicoli_autoparchi), + url(r'^autoparco/elenco/(?P.*)/$', viste.veicoli_elenco_autoparco), + url(r'^(P.*)/$', viste.veicoli_veicolo), + url(r'^nuovo/$', viste.veicoli_veicolo_modifica_o_nuovo), + url(r'^modifica/(?P.*)/$', viste.veicoli_veicolo_modifica_o_nuovo), + url(r'^manutenzioni/(?P.*)/$', viste.veicoli_manutenzione), + url(r'^manutenzione/(?P.*)/modifica/$', viste.veicoli_modifica_manutenzione), + url(r'^rifornimento/(?P.*)/modifica/$', viste.veicoli_modifica_rifornimento), + url(r'^rifornimenti/(?P.*)/$', viste.veicoli_rifornimento), + url(r'^fermi-tecnici/(?P.*)/$', viste.veicoli_fermo_tecnico), + url(r'^termina/fermo-tecnico/(?P.*)/$', viste.veicoli_termina_fermo_tecnico), + url(r'^(?P.*)/collocazioni/$', viste.veicoli_collocazioni), + url(r'^dettagli/(?P.*)/$', viste.veicolo_dettagli), +] diff --git a/veicoli/urls_autoparco.py b/veicoli/urls_autoparco.py new file mode 100644 index 000000000..005c52ca0 --- /dev/null +++ b/veicoli/urls_autoparco.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from . import viste + + +app_label = 'autoparco' +urlpatterns = [ + url(r'^(P.*)/$', viste.veicoli_autoparco), + url(r'^modifica/(?P.*)/$', viste.veicoli_autoparco_modifica_o_nuovo), + url(r'^nuovo/$', viste.veicoli_autoparco_modifica_o_nuovo), +]