Files
diario_coversazioni/diario/views.py
2026-04-08 12:07:43 +00:00

661 lines
26 KiB
Python

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, Tag, Appuntamento
from .forms import ConversazioneForm, ObiettivoForm, AggiornamentoObiettivoForm, CommentoConversazioneForm, DocumentoForm, AppuntamentoForm
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]
# Prossimi appuntamenti
prossimi_appuntamenti = Appuntamento.objects.filter(
data_ora__gte=timezone.now(),
).select_related('creato_da', 'conversazione').order_by('data_ora')[:5]
return render(request, 'diario/dashboard.html', {
'eventi': eventi,
'obiettivi_aperti': obiettivi_aperti,
'scadenze_prossime': scadenze_prossime,
'scaduti': scaduti,
'prossimi_appuntamenti': prossimi_appuntamenti,
'oggi': oggi,
})
# ── Agenda ─────────────────────────────────────────────────────────────────────
@login_required
def agenda(request):
oggi = timezone.now().date()
# Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro, appuntamenti
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')
appuntamenti_futuri = Appuntamento.objects.filter(
data_ora__date__gte=oggi,
).select_related('creato_da', 'conversazione').order_by('data_ora')
# 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],
[{'tipo': 'appuntamento', 'data': a.data_ora.date(), 'obj': a} for a in appuntamenti_futuri],
),
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')
appuntamenti_passati = Appuntamento.objects.filter(
data_ora__date__gte=data_inizio,
data_ora__date__lt=oggi,
).select_related('creato_da', 'conversazione').order_by('-data_ora')
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],
[{'tipo': 'appuntamento', 'data': a.data_ora.date(), 'obj': a, 'scaduto': False} for a in appuntamenti_passati],
),
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()
appuntamenti = conv.appuntamenti.select_related('creato_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,
'appuntamenti': appuntamenti,
'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]
# Menzioni: commenti e aggiornamenti dove @username appare nel testo
mention_pattern = f'@{persona.username}'
menzioni_commenti = CommentoConversazione.objects.filter(
testo__icontains=mention_pattern
).select_related('autore', 'conversazione').order_by('-data')[:20]
menzioni_aggiornamenti = AggiornamentoObiettivo.objects.filter(
testo__icontains=mention_pattern
).select_related('autore', 'obiettivo').order_by('-data')[:20]
return render(request, 'diario/persone/dettaglio.html', {
'persona': persona,
'conversazioni': conversazioni,
'obiettivi': obiettivi,
'documenti': documenti,
'menzioni_commenti': menzioni_commenti,
'menzioni_aggiornamenti': menzioni_aggiornamenti,
'totale_menzioni': len(menzioni_commenti) + len(menzioni_aggiornamenti),
})
# ── Appuntamenti ───────────────────────────────────────────────────────────────
@login_required
def appuntamenti_lista(request):
oggi = timezone.now()
futuri = Appuntamento.objects.filter(data_ora__gte=oggi).select_related('creato_da', 'conversazione').order_by('data_ora')
passati = Appuntamento.objects.filter(data_ora__lt=oggi).select_related('creato_da', 'conversazione').order_by('-data_ora')[:30]
return render(request, 'diario/appuntamenti/lista.html', {
'futuri': futuri,
'passati': passati,
})
@login_required
def appuntamento_nuovo(request):
conversazione_pk = request.GET.get('conversazione')
initial = {}
if conversazione_pk:
conv = get_object_or_404(Conversazione, pk=conversazione_pk)
initial['titolo'] = f"Riunione: {conv.titolo}"
initial['partecipanti'] = conv.partecipanti.all()
if request.method == 'POST':
form = AppuntamentoForm(request.POST)
if form.is_valid():
app = form.save(commit=False)
app.creato_da = request.user
if conversazione_pk:
app.conversazione_id = conversazione_pk
app.save()
form.save_m2m()
messages.success(request, 'Appuntamento creato.')
if conversazione_pk:
return redirect('conversazione_dettaglio', pk=conversazione_pk)
return redirect('appuntamento_dettaglio', pk=app.pk)
else:
form = AppuntamentoForm(initial=initial)
return render(request, 'diario/appuntamenti/form.html', {
'form': form,
'titolo_pagina': 'Nuovo appuntamento',
'conversazione_pk': conversazione_pk,
})
@login_required
def appuntamento_dettaglio(request, pk):
app = get_object_or_404(Appuntamento.objects.select_related('creato_da', 'conversazione'), pk=pk)
return render(request, 'diario/appuntamenti/dettaglio.html', {
'app': app,
'can_edit': _can_edit(request.user, app),
})
@login_required
def appuntamento_modifica(request, pk):
app = get_object_or_404(Appuntamento, pk=pk)
if not _can_edit(request.user, app):
return HttpResponseForbidden('Non hai i permessi per modificare questo appuntamento.')
if request.method == 'POST':
form = AppuntamentoForm(request.POST, instance=app)
if form.is_valid():
form.save()
messages.success(request, 'Appuntamento aggiornato.')
return redirect('appuntamento_dettaglio', pk=app.pk)
else:
form = AppuntamentoForm(instance=app)
return render(request, 'diario/appuntamenti/form.html', {
'form': form,
'titolo_pagina': 'Modifica appuntamento',
'app': app,
})
@login_required
def appuntamento_elimina(request, pk):
app = get_object_or_404(Appuntamento, pk=pk)
if not _can_edit(request.user, app):
return HttpResponseForbidden('Non hai i permessi per eliminare questo appuntamento.')
if request.method == 'POST':
app.delete()
messages.success(request, 'Appuntamento eliminato.')
return redirect('appuntamenti_lista')
return render(request, 'diario/conferma_elimina.html', {
'oggetto': app,
'tipo': 'appuntamento',
'cancel_url': 'appuntamento_dettaglio',
})
# ── API: utenti per @menzioni ──────────────────────────────────────────────────
@login_required
def api_utenti(request):
"""Restituisce lista utenti per autocomplete @menzioni."""
q = request.GET.get('q', '').strip()
utenti = User.objects.filter(is_active=True)
if q:
utenti = utenti.filter(
Q(username__icontains=q) | Q(first_name__icontains=q) | Q(last_name__icontains=q)
)
data = [
{
'username': u.username,
'nome': u.get_full_name() or u.username,
'pk': u.pk,
}
for u in utenti.order_by('first_name', 'username')[:15]
]
return JsonResponse(data, safe=False)