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:
0
diario/__init__.py
Normal file
0
diario/__init__.py
Normal file
70
diario/admin.py
Normal file
70
diario/admin.py
Normal 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
6
diario/apps.py
Normal 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
96
diario/forms.py
Normal 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)',
|
||||
}
|
||||
67
diario/migrations/0001_initial.py
Normal file
67
diario/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
18
diario/migrations/0002_obiettivo_avanzamento.py
Normal file
18
diario/migrations/0002_obiettivo_avanzamento.py
Normal 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),
|
||||
),
|
||||
]
|
||||
29
diario/migrations/0003_alter_conversazione_data_and_more.py
Normal file
29
diario/migrations/0003_alter_conversazione_data_and_more.py
Normal 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),
|
||||
),
|
||||
]
|
||||
36
diario/migrations/0004_documento.py
Normal file
36
diario/migrations/0004_documento.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
31
diario/migrations/0005_commento_conversazione.py
Normal file
31
diario/migrations/0005_commento_conversazione.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
diario/migrations/__init__.py
Normal file
0
diario/migrations/__init__.py
Normal file
139
diario/models.py
Normal file
139
diario/models.py
Normal 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)
|
||||
|
||||
0
diario/templatetags/__init__.py
Normal file
0
diario/templatetags/__init__.py
Normal file
12
diario/templatetags/custom_filters.py
Normal file
12
diario/templatetags/custom_filters.py
Normal 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
3
diario/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
43
diario/urls.py
Normal file
43
diario/urls.py
Normal 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
517
diario/views.py
Normal 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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user