Initial commit: Diario Conversazioni Olimpic Nastri

- Django 5.2 + PostgreSQL + Gunicorn
- Conversazioni, Obiettivi, Documenti PDF, Persone
- Commenti e aggiornamenti con modifica/eliminazione
- Agenda, ricerca live, giorni rimanenti scadenze
- Bootstrap 5 + HTMX + toast notifications
- Deploy: Nginx + Gunicorn + SSL
This commit is contained in:
automationkriz
2026-04-05 14:48:22 +00:00
commit d296353dcb
48 changed files with 3538 additions and 0 deletions

0
diario/__init__.py Normal file
View File

70
diario/admin.py Normal file
View File

@@ -0,0 +1,70 @@
from django.contrib import admin
from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento
class AggiornamentoInline(admin.TabularInline):
model = AggiornamentoObiettivo
extra = 0
readonly_fields = ('data',)
class CommentoConversazioneInline(admin.TabularInline):
model = CommentoConversazione
extra = 0
readonly_fields = ('data',)
class DocumentoInline(admin.TabularInline):
model = Documento
extra = 0
readonly_fields = ('data_caricamento',)
fk_name = 'conversazione'
class DocumentoObiettivoInline(admin.TabularInline):
model = Documento
extra = 0
readonly_fields = ('data_caricamento',)
fk_name = 'obiettivo'
@admin.register(Conversazione)
class ConversazioneAdmin(admin.ModelAdmin):
list_display = ('titolo', 'data', 'registrato_da')
list_filter = ('data', 'registrato_da')
search_fields = ('titolo', 'contenuto')
filter_horizontal = ('partecipanti',)
readonly_fields = ('data',)
inlines = [CommentoConversazioneInline, DocumentoInline]
@admin.register(Obiettivo)
class ObiettivoAdmin(admin.ModelAdmin):
list_display = ('titolo', 'tipo', 'stato', 'data_scadenza', 'creato_da')
list_filter = ('tipo', 'stato', 'assegnato_a')
search_fields = ('titolo', 'descrizione')
readonly_fields = ('data_creazione',)
inlines = [AggiornamentoInline, DocumentoObiettivoInline]
@admin.register(AggiornamentoObiettivo)
class AggiornamentoObiettivoAdmin(admin.ModelAdmin):
list_display = ('obiettivo', 'autore', 'data')
list_filter = ('autore', 'data')
readonly_fields = ('data',)
@admin.register(CommentoConversazione)
class CommentoConversazioneAdmin(admin.ModelAdmin):
list_display = ('conversazione', 'autore', 'data')
list_filter = ('autore', 'data')
readonly_fields = ('data',)
@admin.register(Documento)
class DocumentoAdmin(admin.ModelAdmin):
list_display = ('titolo', 'caricato_da', 'data_caricamento', 'conversazione', 'obiettivo')
list_filter = ('caricato_da', 'data_caricamento')
search_fields = ('titolo', 'descrizione')
readonly_fields = ('data_caricamento',)

6
diario/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class DiarioConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'diario'

96
diario/forms.py Normal file
View File

@@ -0,0 +1,96 @@
from django import forms
from django.contrib.auth.models import User
from django.utils import timezone
from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento
class ConversazioneForm(forms.ModelForm):
data = forms.DateTimeField(
label='Data e ora',
initial=timezone.now,
widget=forms.DateTimeInput(
attrs={'class': 'form-control', 'type': 'datetime-local'},
format='%Y-%m-%dT%H:%M',
),
input_formats=['%Y-%m-%dT%H:%M'],
)
partecipanti = forms.ModelMultipleChoiceField(
queryset=User.objects.filter(is_active=True).order_by('first_name', 'username'),
widget=forms.CheckboxSelectMultiple,
required=False,
label='Partecipanti',
)
class Meta:
model = Conversazione
fields = ['titolo', 'data', 'contenuto', 'partecipanti']
widgets = {
'titolo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Titolo della conversazione'}),
'contenuto': forms.Textarea(attrs={'class': 'form-control', 'rows': 6, 'placeholder': 'Descrivi cosa è stato discusso...'}),
}
labels = {
'titolo': 'Titolo',
'contenuto': 'Contenuto',
}
class ObiettivoForm(forms.ModelForm):
assegnato_a = forms.ModelMultipleChoiceField(
queryset=User.objects.filter(is_active=True).order_by('first_name', 'username'),
widget=forms.CheckboxSelectMultiple,
required=False,
label='Assegnato a',
)
class Meta:
model = Obiettivo
fields = ['titolo', 'descrizione', 'tipo', 'assegnato_a', 'stato', 'data_scadenza']
widgets = {
'titolo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Titolo obiettivo'}),
'descrizione': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'Descrizione (opzionale)'}),
'tipo': forms.Select(attrs={'class': 'form-select'}),
'stato': forms.Select(attrs={'class': 'form-select'}),
'data_scadenza': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
}
class AggiornamentoObiettivoForm(forms.ModelForm):
class Meta:
model = AggiornamentoObiettivo
fields = ['testo']
widgets = {
'testo': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Scrivi un aggiornamento...'}),
}
labels = {
'testo': 'Aggiornamento',
}
class CommentoConversazioneForm(forms.ModelForm):
class Meta:
model = CommentoConversazione
fields = ['testo']
widgets = {
'testo': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Scrivi un commento...'}),
}
labels = {
'testo': 'Commento',
}
class DocumentoForm(forms.ModelForm):
class Meta:
model = Documento
fields = ['titolo', 'descrizione', 'file', 'conversazione', 'obiettivo']
widgets = {
'titolo': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Titolo del documento'}),
'descrizione': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Descrizione (opzionale)'}),
'file': forms.ClearableFileInput(attrs={'class': 'form-control', 'accept': '.pdf'}),
'conversazione': forms.Select(attrs={'class': 'form-select'}),
'obiettivo': forms.Select(attrs={'class': 'form-select'}),
}
labels = {
'file': 'File PDF',
'conversazione': 'Collegato a conversazione (opzionale)',
'obiettivo': 'Collegato a obiettivo (opzionale)',
}

View File

@@ -0,0 +1,67 @@
# Generated by Django 5.2.12 on 2026-04-01 12:43
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Conversazione',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titolo', models.CharField(max_length=200)),
('data', models.DateTimeField(auto_now_add=True)),
('contenuto', models.TextField()),
('partecipanti', models.ManyToManyField(blank=True, related_name='conversazioni_partecipate', to=settings.AUTH_USER_MODEL)),
('registrato_da', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversazioni_registrate', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Conversazione',
'verbose_name_plural': 'Conversazioni',
'ordering': ['-data'],
},
),
migrations.CreateModel(
name='Obiettivo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titolo', models.CharField(max_length=200)),
('descrizione', models.TextField(blank=True)),
('tipo', models.CharField(choices=[('collettivo', 'Collettivo'), ('individuale', 'Individuale')], default='collettivo', max_length=20)),
('stato', models.CharField(choices=[('aperto', 'Aperto'), ('in_corso', 'In corso'), ('completato', 'Completato'), ('sospeso', 'Sospeso')], default='aperto', max_length=20)),
('data_scadenza', models.DateField(blank=True, null=True)),
('data_creazione', models.DateTimeField(auto_now_add=True)),
('assegnato_a', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='obiettivi_assegnati', to=settings.AUTH_USER_MODEL)),
('creato_da', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='obiettivi_creati', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Obiettivo',
'verbose_name_plural': 'Obiettivi',
'ordering': ['-data_creazione'],
},
),
migrations.CreateModel(
name='AggiornamentoObiettivo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('testo', models.TextField()),
('data', models.DateTimeField(auto_now_add=True)),
('autore', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aggiornamenti', to=settings.AUTH_USER_MODEL)),
('obiettivo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aggiornamenti', to='diario.obiettivo')),
],
options={
'verbose_name': 'Aggiornamento',
'verbose_name_plural': 'Aggiornamenti',
'ordering': ['-data'],
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.12 on 2026-04-01 13:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diario', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='obiettivo',
name='avanzamento',
field=models.PositiveSmallIntegerField(default=0),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.12 on 2026-04-01 13:59
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diario', '0002_obiettivo_avanzamento'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='conversazione',
name='data',
field=models.DateTimeField(),
),
migrations.RemoveField(
model_name='obiettivo',
name='assegnato_a',
),
migrations.AddField(
model_name='obiettivo',
name='assegnato_a',
field=models.ManyToManyField(blank=True, related_name='obiettivi_assegnati', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.12 on 2026-04-04 16:45
import diario.models
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diario', '0003_alter_conversazione_data_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Documento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='documenti/%Y/%m/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf']), diario.models.validate_file_size])),
('titolo', models.CharField(max_length=200)),
('descrizione', models.TextField(blank=True)),
('data_caricamento', models.DateTimeField(auto_now_add=True)),
('caricato_da', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documenti_caricati', to=settings.AUTH_USER_MODEL)),
('conversazione', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documenti', to='diario.conversazione')),
('obiettivo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documenti', to='diario.obiettivo')),
],
options={
'verbose_name': 'Documento',
'verbose_name_plural': 'Documenti',
'ordering': ['-data_caricamento'],
},
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.12 on 2026-04-05 14:30
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diario', '0004_documento'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CommentoConversazione',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('testo', models.TextField()),
('data', models.DateTimeField(auto_now_add=True)),
('autore', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commenti_conversazione', to=settings.AUTH_USER_MODEL)),
('conversazione', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commenti', to='diario.conversazione')),
],
options={
'verbose_name': 'Commento',
'verbose_name_plural': 'Commenti',
'ordering': ['-data'],
},
),
]

View File

139
diario/models.py Normal file
View File

@@ -0,0 +1,139 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import FileExtensionValidator
from django.utils import timezone
def validate_file_size(value):
limit = 10 * 1024 * 1024 # 10 MB
if value.size > limit:
from django.core.exceptions import ValidationError
raise ValidationError('Il file non può superare i 10 MB.')
class Conversazione(models.Model):
titolo = models.CharField(max_length=200)
data = models.DateTimeField()
partecipanti = models.ManyToManyField(User, related_name='conversazioni_partecipate', blank=True)
contenuto = models.TextField()
registrato_da = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name='conversazioni_registrate'
)
class Meta:
ordering = ['-data']
verbose_name = 'Conversazione'
verbose_name_plural = 'Conversazioni'
def __str__(self):
return f"{self.titolo} ({self.data.strftime('%d/%m/%Y')})"
class Obiettivo(models.Model):
TIPO_CHOICES = [
('collettivo', 'Collettivo'),
('individuale', 'Individuale'),
]
STATO_CHOICES = [
('aperto', 'Aperto'),
('in_corso', 'In corso'),
('completato', 'Completato'),
('sospeso', 'Sospeso'),
]
titolo = models.CharField(max_length=200)
descrizione = models.TextField(blank=True)
avanzamento = models.PositiveSmallIntegerField(default=0) # 0-100
tipo = models.CharField(max_length=20, choices=TIPO_CHOICES, default='collettivo')
assegnato_a = models.ManyToManyField(
User, blank=True,
related_name='obiettivi_assegnati'
)
stato = models.CharField(max_length=20, choices=STATO_CHOICES, default='aperto')
data_scadenza = models.DateField(null=True, blank=True)
creato_da = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name='obiettivi_creati'
)
data_creazione = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-data_creazione']
verbose_name = 'Obiettivo'
verbose_name_plural = 'Obiettivi'
def __str__(self):
return self.titolo
@property
def giorni_rimanenti(self):
"""Restituisce i giorni rimanenti alla scadenza (None se non impostata)."""
if not self.data_scadenza:
return None
delta = self.data_scadenza - timezone.now().date()
return delta.days
class AggiornamentoObiettivo(models.Model):
obiettivo = models.ForeignKey(Obiettivo, on_delete=models.CASCADE, related_name='aggiornamenti')
testo = models.TextField()
autore = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='aggiornamenti')
data = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-data']
verbose_name = 'Aggiornamento'
verbose_name_plural = 'Aggiornamenti'
def __str__(self):
return f"Aggiornamento su '{self.obiettivo}' del {self.data.strftime('%d/%m/%Y')}"
class CommentoConversazione(models.Model):
conversazione = models.ForeignKey(Conversazione, on_delete=models.CASCADE, related_name='commenti')
testo = models.TextField()
autore = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='commenti_conversazione')
data = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-data']
verbose_name = 'Commento'
verbose_name_plural = 'Commenti'
def __str__(self):
return f"Commento su '{self.conversazione}' del {self.data.strftime('%d/%m/%Y')}"
class Documento(models.Model):
file = models.FileField(
upload_to='documenti/%Y/%m/',
validators=[
FileExtensionValidator(allowed_extensions=['pdf']),
validate_file_size,
],
)
titolo = models.CharField(max_length=200)
descrizione = models.TextField(blank=True)
caricato_da = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name='documenti_caricati'
)
data_caricamento = models.DateTimeField(auto_now_add=True)
conversazione = models.ForeignKey(
'Conversazione', on_delete=models.SET_NULL, null=True, blank=True, related_name='documenti'
)
obiettivo = models.ForeignKey(
'Obiettivo', on_delete=models.SET_NULL, null=True, blank=True, related_name='documenti'
)
class Meta:
ordering = ['-data_caricamento']
verbose_name = 'Documento'
verbose_name_plural = 'Documenti'
def __str__(self):
return self.titolo
@property
def filename(self):
import os
return os.path.basename(self.file.name)

View File

View File

@@ -0,0 +1,12 @@
from django import template
register = template.Library()
@register.filter
def abs_val(value):
"""Restituisce il valore assoluto."""
try:
return abs(int(value))
except (ValueError, TypeError):
return value

3
diario/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

43
diario/urls.py Normal file
View File

@@ -0,0 +1,43 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('agenda/', views.agenda, name='agenda'),
# Conversazioni
path('conversazioni/', views.conversazioni_lista, name='conversazioni_lista'),
path('conversazioni/nuova/', views.conversazione_nuova, name='conversazione_nuova'),
path('conversazioni/<int:pk>/', views.conversazione_dettaglio, name='conversazione_dettaglio'),
path('conversazioni/<int:pk>/modifica/', views.conversazione_modifica, name='conversazione_modifica'),
path('conversazioni/<int:pk>/elimina/', views.conversazione_elimina, name='conversazione_elimina'),
# Commenti conversazioni
path('commenti/<int:pk>/modifica/', views.commento_modifica, name='commento_modifica'),
path('commenti/<int:pk>/elimina/', views.commento_elimina, name='commento_elimina'),
# Aggiornamenti obiettivi
path('aggiornamenti/<int:pk>/modifica/', views.aggiornamento_modifica, name='aggiornamento_modifica'),
path('aggiornamenti/<int:pk>/elimina/', views.aggiornamento_elimina, name='aggiornamento_elimina'),
# Obiettivi
path('obiettivi/', views.obiettivi_lista, name='obiettivi_lista'),
path('obiettivi/nuovo/', views.obiettivo_nuovo, name='obiettivo_nuovo'),
path('obiettivi/<int:pk>/', views.obiettivo_dettaglio, name='obiettivo_dettaglio'),
path('obiettivi/<int:pk>/modifica/', views.obiettivo_modifica, name='obiettivo_modifica'),
path('obiettivi/<int:pk>/elimina/', views.obiettivo_elimina, name='obiettivo_elimina'),
path('obiettivi/<int:pk>/avanzamento/', views.obiettivo_avanzamento_ajax, name='obiettivo_avanzamento_ajax'),
# Documenti
path('documenti/', views.documenti_lista, name='documenti_lista'),
path('documenti/nuovo/', views.documento_nuovo, name='documento_nuovo'),
path('documenti/<int:pk>/', views.documento_dettaglio, name='documento_dettaglio'),
path('documenti/<int:pk>/elimina/', views.documento_elimina, name='documento_elimina'),
# Ricerca
path('ricerca/', views.ricerca, name='ricerca'),
# Persone
path('persone/', views.persone_lista, name='persone_lista'),
path('persone/<int:pk>/', views.persona_dettaglio, name='persona_dettaglio'),
]

517
diario/views.py Normal file
View File

@@ -0,0 +1,517 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib import messages
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.http import require_POST
from django.db.models import Q, Count
from django.utils import timezone
from itertools import chain
from datetime import timedelta
from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento
from .forms import ConversazioneForm, ObiettivoForm, AggiornamentoObiettivoForm, CommentoConversazioneForm, DocumentoForm
def _can_edit(user, obj):
"""Autore o superuser possono modificare/eliminare."""
owner = getattr(obj, 'registrato_da', None) or getattr(obj, 'creato_da', None) or getattr(obj, 'caricato_da', None) or getattr(obj, 'autore', None)
return user == owner or user.is_superuser
# ── Dashboard ──────────────────────────────────────────────────────────────────
@login_required
def dashboard(request):
conversazioni = Conversazione.objects.select_related('registrato_da').order_by('-data')[:20]
aggiornamenti = AggiornamentoObiettivo.objects.select_related('autore', 'obiettivo').order_by('-data')[:20]
documenti_recenti = Documento.objects.select_related('caricato_da').order_by('-data_caricamento')[:10]
eventi = sorted(
chain(
[{'tipo': 'conversazione', 'data': c.data, 'obj': c} for c in conversazioni],
[{'tipo': 'aggiornamento', 'data': a.data, 'obj': a} for a in aggiornamenti],
[{'tipo': 'documento', 'data': d.data_caricamento, 'obj': d} for d in documenti_recenti],
),
key=lambda x: x['data'],
reverse=True,
)[:30]
obiettivi_aperti = Obiettivo.objects.exclude(stato='completato').order_by('-data_creazione')[:5]
# Agenda: prossime scadenze obiettivi (prossimi 30 giorni)
oggi = timezone.now().date()
scadenze_prossime = Obiettivo.objects.filter(
data_scadenza__gte=oggi,
data_scadenza__lte=oggi + timedelta(days=30),
).exclude(stato='completato').order_by('data_scadenza')[:8]
# Scadenze passate non completate
scaduti = Obiettivo.objects.filter(
data_scadenza__lt=oggi,
).exclude(stato='completato').order_by('-data_scadenza')[:5]
return render(request, 'diario/dashboard.html', {
'eventi': eventi,
'obiettivi_aperti': obiettivi_aperti,
'scadenze_prossime': scadenze_prossime,
'scaduti': scaduti,
'oggi': oggi,
})
# ── Agenda ─────────────────────────────────────────────────────────────────────
@login_required
def agenda(request):
oggi = timezone.now().date()
# Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro
scadenze_future = Obiettivo.objects.filter(
data_scadenza__gte=oggi,
).exclude(stato='completato').order_by('data_scadenza')
conversazioni_future = Conversazione.objects.filter(
data__date__gte=oggi,
).select_related('registrato_da').order_by('data')
# Eventi futuri unificati
eventi_futuri = sorted(
chain(
[{'tipo': 'scadenza', 'data': o.data_scadenza, 'obj': o} for o in scadenze_future],
[{'tipo': 'conversazione', 'data': c.data.date(), 'obj': c} for c in conversazioni_future],
),
key=lambda x: x['data'],
)
# Eventi passati (ultimi 30 giorni)
data_inizio = oggi - timedelta(days=30)
conversazioni_passate = Conversazione.objects.filter(
data__date__gte=data_inizio,
data__date__lt=oggi,
).select_related('registrato_da').order_by('-data')
scadenze_passate = Obiettivo.objects.filter(
data_scadenza__gte=data_inizio,
data_scadenza__lt=oggi,
).order_by('-data_scadenza')
eventi_passati = sorted(
chain(
[{'tipo': 'scadenza', 'data': o.data_scadenza, 'obj': o, 'scaduto': o.stato != 'completato'} for o in scadenze_passate],
[{'tipo': 'conversazione', 'data': c.data.date(), 'obj': c, 'scaduto': False} for c in conversazioni_passate],
),
key=lambda x: x['data'],
reverse=True,
)
return render(request, 'diario/agenda.html', {
'eventi_futuri': eventi_futuri,
'eventi_passati': eventi_passati,
'oggi': oggi,
})
# ── Conversazioni ──────────────────────────────────────────────────────────────
@login_required
def conversazioni_lista(request):
qs = Conversazione.objects.select_related('registrato_da').order_by('-data')
return render(request, 'diario/conversazioni/lista.html', {'conversazioni': qs})
@login_required
def conversazione_nuova(request):
if request.method == 'POST':
form = ConversazioneForm(request.POST)
if form.is_valid():
conv = form.save(commit=False)
conv.registrato_da = request.user
conv.save()
form.save_m2m()
messages.success(request, 'Conversazione registrata.')
return redirect('conversazione_dettaglio', pk=conv.pk)
else:
form = ConversazioneForm()
return render(request, 'diario/conversazioni/form.html', {'form': form, 'titolo_pagina': 'Nuova conversazione'})
@login_required
def conversazione_dettaglio(request, pk):
conv = get_object_or_404(Conversazione, pk=pk)
commenti = conv.commenti.select_related('autore').order_by('-data')
documenti = conv.documenti.select_related('caricato_da').all()
if request.method == 'POST':
comment_form = CommentoConversazioneForm(request.POST)
if comment_form.is_valid():
c = comment_form.save(commit=False)
c.conversazione = conv
c.autore = request.user
c.save()
messages.success(request, 'Commento aggiunto.')
return redirect('conversazione_dettaglio', pk=conv.pk)
else:
comment_form = CommentoConversazioneForm()
return render(request, 'diario/conversazioni/dettaglio.html', {
'conv': conv,
'commenti': commenti,
'comment_form': comment_form,
'documenti': documenti,
'can_edit': _can_edit(request.user, conv),
})
@login_required
def conversazione_modifica(request, pk):
conv = get_object_or_404(Conversazione, pk=pk)
if not _can_edit(request.user, conv):
return HttpResponseForbidden('Non hai i permessi per modificare questa conversazione.')
if request.method == 'POST':
form = ConversazioneForm(request.POST, instance=conv)
if form.is_valid():
form.save()
messages.success(request, 'Conversazione aggiornata.')
return redirect('conversazione_dettaglio', pk=conv.pk)
else:
form = ConversazioneForm(instance=conv)
return render(request, 'diario/conversazioni/form.html', {'form': form, 'titolo_pagina': 'Modifica conversazione', 'conv': conv})
@login_required
def conversazione_elimina(request, pk):
conv = get_object_or_404(Conversazione, pk=pk)
if not _can_edit(request.user, conv):
return HttpResponseForbidden('Non hai i permessi per eliminare questa conversazione.')
if request.method == 'POST':
conv.delete()
messages.success(request, 'Conversazione eliminata.')
return redirect('conversazioni_lista')
return render(request, 'diario/conferma_elimina.html', {
'oggetto': conv,
'tipo': 'conversazione',
'cancel_url': 'conversazione_dettaglio',
})
# ── Commenti conversazione — modifica/elimina ─────────────────────────────────
@login_required
def commento_modifica(request, pk):
commento = get_object_or_404(CommentoConversazione, pk=pk)
if not _can_edit(request.user, commento):
return HttpResponseForbidden('Non puoi modificare questo commento.')
if request.method == 'POST':
form = CommentoConversazioneForm(request.POST, instance=commento)
if form.is_valid():
form.save()
messages.success(request, 'Commento aggiornato.')
return redirect('conversazione_dettaglio', pk=commento.conversazione.pk)
else:
form = CommentoConversazioneForm(instance=commento)
return render(request, 'diario/commento_modifica.html', {
'form': form,
'commento': commento,
'tipo': 'commento',
'back_url': redirect('conversazione_dettaglio', pk=commento.conversazione.pk).url,
})
@login_required
def commento_elimina(request, pk):
commento = get_object_or_404(CommentoConversazione, pk=pk)
if not _can_edit(request.user, commento):
return HttpResponseForbidden('Non puoi eliminare questo commento.')
conv_pk = commento.conversazione.pk
if request.method == 'POST':
commento.delete()
messages.success(request, 'Commento eliminato.')
return redirect('conversazione_dettaglio', pk=conv_pk)
return render(request, 'diario/conferma_elimina.html', {
'oggetto': commento,
'tipo': 'commento',
'cancel_url': 'conversazione_dettaglio',
'cancel_pk': conv_pk,
})
# ── Aggiornamenti obiettivo — modifica/elimina ────────────────────────────────
@login_required
def aggiornamento_modifica(request, pk):
agg = get_object_or_404(AggiornamentoObiettivo, pk=pk)
if not _can_edit(request.user, agg):
return HttpResponseForbidden('Non puoi modificare questo aggiornamento.')
if request.method == 'POST':
form = AggiornamentoObiettivoForm(request.POST, instance=agg)
if form.is_valid():
form.save()
messages.success(request, 'Aggiornamento modificato.')
return redirect('obiettivo_dettaglio', pk=agg.obiettivo.pk)
else:
form = AggiornamentoObiettivoForm(instance=agg)
return render(request, 'diario/commento_modifica.html', {
'form': form,
'commento': agg,
'tipo': 'aggiornamento',
'back_url': redirect('obiettivo_dettaglio', pk=agg.obiettivo.pk).url,
})
@login_required
def aggiornamento_elimina(request, pk):
agg = get_object_or_404(AggiornamentoObiettivo, pk=pk)
if not _can_edit(request.user, agg):
return HttpResponseForbidden('Non puoi eliminare questo aggiornamento.')
obj_pk = agg.obiettivo.pk
if request.method == 'POST':
agg.delete()
messages.success(request, 'Aggiornamento eliminato.')
return redirect('obiettivo_dettaglio', pk=obj_pk)
return render(request, 'diario/conferma_elimina.html', {
'oggetto': agg,
'tipo': 'aggiornamento',
'cancel_url': 'obiettivo_dettaglio',
'cancel_pk': obj_pk,
})
# ── Obiettivi ──────────────────────────────────────────────────────────────────
@login_required
def obiettivi_lista(request):
filtro = request.GET.get('filtro', 'tutti')
qs = Obiettivo.objects.prefetch_related('assegnato_a').select_related('creato_da')
if filtro == 'collettivi':
qs = qs.filter(tipo='collettivo')
elif filtro == 'individuali':
qs = qs.filter(tipo='individuale')
elif filtro == 'miei':
qs = qs.filter(assegnato_a=request.user)
return render(request, 'diario/obiettivi/lista.html', {'obiettivi': qs, 'filtro': filtro})
@login_required
def obiettivo_nuovo(request):
if request.method == 'POST':
form = ObiettivoForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.creato_da = request.user
obj.save()
form.save_m2m()
messages.success(request, 'Obiettivo creato.')
return redirect('obiettivo_dettaglio', pk=obj.pk)
else:
form = ObiettivoForm()
return render(request, 'diario/obiettivi/form.html', {'form': form, 'titolo_pagina': 'Nuovo obiettivo'})
@login_required
def obiettivo_dettaglio(request, pk):
obj = get_object_or_404(Obiettivo, pk=pk)
aggiornamenti = obj.aggiornamenti.select_related('autore').order_by('-data')
documenti = obj.documenti.select_related('caricato_da').all()
if request.method == 'POST':
agg_form = AggiornamentoObiettivoForm(request.POST)
if agg_form.is_valid():
agg = agg_form.save(commit=False)
agg.obiettivo = obj
agg.autore = request.user
agg.save()
messages.success(request, 'Aggiornamento aggiunto.')
return redirect('obiettivo_dettaglio', pk=obj.pk)
else:
agg_form = AggiornamentoObiettivoForm()
return render(request, 'diario/obiettivi/dettaglio.html', {
'obj': obj,
'aggiornamenti': aggiornamenti,
'documenti': documenti,
'agg_form': agg_form,
'can_edit': _can_edit(request.user, obj),
})
@login_required
def obiettivo_modifica(request, pk):
obj = get_object_or_404(Obiettivo, pk=pk)
if not _can_edit(request.user, obj):
return HttpResponseForbidden('Non hai i permessi per modificare questo obiettivo.')
if request.method == 'POST':
form = ObiettivoForm(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.success(request, 'Obiettivo aggiornato.')
return redirect('obiettivo_dettaglio', pk=obj.pk)
else:
form = ObiettivoForm(instance=obj)
return render(request, 'diario/obiettivi/form.html', {'form': form, 'titolo_pagina': 'Modifica obiettivo', 'obj': obj})
@login_required
def obiettivo_elimina(request, pk):
obj = get_object_or_404(Obiettivo, pk=pk)
if not _can_edit(request.user, obj):
return HttpResponseForbidden('Non hai i permessi per eliminare questo obiettivo.')
if request.method == 'POST':
obj.delete()
messages.success(request, 'Obiettivo eliminato.')
return redirect('obiettivi_lista')
return render(request, 'diario/conferma_elimina.html', {
'oggetto': obj,
'tipo': 'obiettivo',
'cancel_url': 'obiettivo_dettaglio',
})
@login_required
@require_POST
def obiettivo_avanzamento_ajax(request, pk):
obj = get_object_or_404(Obiettivo, pk=pk)
try:
valore = int(request.POST.get('avanzamento', 0))
if not 0 <= valore <= 100:
return JsonResponse({'ok': False, 'error': 'Valore fuori range'}, status=400)
obj.avanzamento = valore
obj.save(update_fields=['avanzamento'])
return JsonResponse({'ok': True, 'avanzamento': obj.avanzamento})
except (ValueError, TypeError):
return JsonResponse({'ok': False, 'error': 'Valore non valido'}, status=400)
# ── Documenti ──────────────────────────────────────────────────────────────────
@login_required
def documenti_lista(request):
qs = Documento.objects.select_related('caricato_da', 'conversazione', 'obiettivo').order_by('-data_caricamento')
return render(request, 'diario/documenti/lista.html', {'documenti': qs})
@login_required
def documento_nuovo(request):
conversazione_pk = request.GET.get('conversazione')
obiettivo_pk = request.GET.get('obiettivo')
initial = {}
if conversazione_pk:
initial['conversazione'] = conversazione_pk
if obiettivo_pk:
initial['obiettivo'] = obiettivo_pk
if request.method == 'POST':
form = DocumentoForm(request.POST, request.FILES)
if form.is_valid():
doc = form.save(commit=False)
doc.caricato_da = request.user
doc.save()
messages.success(request, 'Documento caricato.')
if doc.conversazione:
return redirect('conversazione_dettaglio', pk=doc.conversazione.pk)
if doc.obiettivo:
return redirect('obiettivo_dettaglio', pk=doc.obiettivo.pk)
return redirect('documento_dettaglio', pk=doc.pk)
else:
form = DocumentoForm(initial=initial)
return render(request, 'diario/documenti/form.html', {'form': form, 'titolo_pagina': 'Carica documento'})
@login_required
def documento_dettaglio(request, pk):
doc = get_object_or_404(Documento.objects.select_related('caricato_da', 'conversazione', 'obiettivo'), pk=pk)
return render(request, 'diario/documenti/dettaglio.html', {
'doc': doc,
'can_edit': _can_edit(request.user, doc),
})
@login_required
def documento_elimina(request, pk):
doc = get_object_or_404(Documento, pk=pk)
if not _can_edit(request.user, doc):
return HttpResponseForbidden('Non hai i permessi per eliminare questo documento.')
if request.method == 'POST':
doc.file.delete(save=False)
doc.delete()
messages.success(request, 'Documento eliminato.')
return redirect('documenti_lista')
return render(request, 'diario/conferma_elimina.html', {
'oggetto': doc,
'tipo': 'documento',
'cancel_url': 'documento_dettaglio',
})
# ── Ricerca ────────────────────────────────────────────────────────────────────
@login_required
def ricerca(request):
q = request.GET.get('q', '').strip()
risultati = {'conversazioni': [], 'obiettivi': [], 'documenti': [], 'persone': []}
total = 0
if len(q) >= 2:
risultati['conversazioni'] = Conversazione.objects.filter(
Q(titolo__icontains=q) | Q(contenuto__icontains=q)
).select_related('registrato_da')[:20]
risultati['obiettivi'] = Obiettivo.objects.filter(
Q(titolo__icontains=q) | Q(descrizione__icontains=q)
).select_related('creato_da')[:20]
risultati['documenti'] = Documento.objects.filter(
Q(titolo__icontains=q) | Q(descrizione__icontains=q)
).select_related('caricato_da')[:20]
risultati['persone'] = User.objects.filter(
Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(username__icontains=q),
is_active=True,
)[:20]
total = sum(len(v) if hasattr(v, '__len__') else v.count() for v in risultati.values())
ctx = {'q': q, 'risultati': risultati, 'total': total}
if getattr(request, 'htmx', False):
return render(request, 'diario/ricerca_risultati.html', ctx)
return render(request, 'diario/ricerca.html', ctx)
# ── Persone ────────────────────────────────────────────────────────────────────
@login_required
def persone_lista(request):
persone = User.objects.filter(is_active=True).annotate(
num_conversazioni=Count('conversazioni_registrate', distinct=True),
num_obiettivi=Count('obiettivi_creati', distinct=True),
num_documenti=Count('documenti_caricati', distinct=True),
).order_by('first_name', 'username')
return render(request, 'diario/persone/lista.html', {'persone': persone})
@login_required
def persona_dettaglio(request, pk):
persona = get_object_or_404(User, pk=pk, is_active=True)
conversazioni = Conversazione.objects.filter(
Q(registrato_da=persona) | Q(partecipanti=persona)
).distinct().select_related('registrato_da').order_by('-data')[:20]
obiettivi = Obiettivo.objects.filter(
Q(creato_da=persona) | Q(assegnato_a=persona)
).distinct().select_related('creato_da').prefetch_related('assegnato_a').order_by('-data_creazione')[:20]
documenti = Documento.objects.filter(
caricato_da=persona
).select_related('conversazione', 'obiettivo').order_by('-data_caricamento')[:20]
return render(request, 'diario/persone/dettaglio.html', {
'persona': persona,
'conversazioni': conversazioni,
'obiettivi': obiettivi,
'documenti': documenti,
})