Tag progetto, @menzioni, appuntamenti da conversazioni

- Modello Tag con nome e colore, M2M su Conversazione
- Modello Appuntamento con luogo, note, partecipanti, link a Conversazione
- @menzioni nei commenti e aggiornamenti: @username → link al profilo
- Autocomplete JS per @menzioni nelle textarea
- Auto-data conversazioni (default=now)
- CRUD completo appuntamenti con permessi autore
- Appuntamenti in agenda, dashboard, dettaglio conversazione
- Crea riunione direttamente da una conversazione (pre-compila titolo e partecipanti)
- Admin: Tag, Appuntamento registrati
This commit is contained in:
automationkriz
2026-04-07 14:28:47 +00:00
parent 006bb24215
commit 09f51b1227
19 changed files with 828 additions and 12 deletions

View File

@@ -9,8 +9,8 @@ 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
from .models import Conversazione, Obiettivo, AggiornamentoObiettivo, CommentoConversazione, Documento, Tag, Appuntamento
from .forms import ConversazioneForm, ObiettivoForm, AggiornamentoObiettivoForm, CommentoConversazioneForm, DocumentoForm, AppuntamentoForm
def _can_edit(user, obj):
@@ -51,11 +51,17 @@ def dashboard(request):
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,
})
@@ -66,7 +72,7 @@ def dashboard(request):
def agenda(request):
oggi = timezone.now().date()
# Prossimi eventi: scadenze obiettivi, conversazioni programmate nel futuro
# 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')
@@ -75,11 +81,16 @@ def agenda(request):
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'],
)
@@ -96,10 +107,16 @@ def agenda(request):
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,
@@ -141,6 +158,7 @@ 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)
@@ -159,6 +177,7 @@ def conversazione_dettaglio(request, pk):
'commenti': commenti,
'comment_form': comment_form,
'documenti': documenti,
'appuntamenti': appuntamenti,
'can_edit': _can_edit(request.user, conv),
})
@@ -515,3 +534,114 @@ def persona_dettaglio(request, pk):
'documenti': documenti,
})
# ── 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)