loko/streetup/observations/admin.py
2026-07-22 14:48:40 +02:00

242 lines
8.9 KiB
Python

from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django import forms
from .models import (
Observation, ObservationPhoto,
ChecklistTemplate, ChecklistItem, ObservationChecklistResponse,
ObservationAttributeChoiceSet, ObservationAttributeChoice
)
from common.models import Thematic
from common.private_files.helpers import build_private_url
from assets.models import AssetCategory
class ObservationPhotoInline(admin.TabularInline):
model = ObservationPhoto
extra = 0
readonly_fields = ['photo_preview']
def photo_preview(self, obj):
if obj.photo and obj.pk:
url = build_private_url("observations", "ObservationPhoto", obj.pk, "photo")
return format_html('<img src="{}" style="max-height: 150px;" />', url)
return "-"
photo_preview.short_description = "Aperçu de la photo"
class ObservationChecklistResponseInline(admin.TabularInline):
model = ObservationChecklistResponse
extra = 0
readonly_fields = ['checklist_item', 'get_value_display']
fields = ['checklist_item', 'get_value_display', 'comment']
def get_value_display(self, obj):
return obj.get_value()
get_value_display.short_description = _("Valeur")
@admin.register(Observation)
class ObservationAdmin(admin.ModelAdmin):
list_display = ('id', 'thematic', 'category', 'address', 'created_at', 'created_by', 'latitude', 'longitude')
list_filter = ('thematic', 'category', 'created_at')
search_fields = ('address', 'description', 'created_by__username')
inlines = [ObservationPhotoInline, ObservationChecklistResponseInline]
readonly_fields = ('geom', 'created_at')
fieldsets = (
(None, {
'fields': ('status', 'observation_type', 'description', 'address', 'latitude', 'longitude', 'geom')
}),
("Métadonnées", {
'fields': ('created_by', 'thematic', 'category'),
}),
)
@admin.register(ObservationPhoto)
class ObservationPhotoAdmin(admin.ModelAdmin):
list_display = ('id', 'observation', 'photo_thumbnail')
readonly_fields = ('photo_thumbnail',)
def photo_thumbnail(self, obj):
if obj.photo and obj.pk:
url = build_private_url("observations", "ObservationPhoto", obj.pk, "photo")
return format_html('<img src="{}" style="max-height: 100px;" />', url)
return "-"
photo_thumbnail.short_description = "Miniature"
# ============================================================================
# Attribute Choice Sets Admin
# ============================================================================
class ObservationAttributeChoiceInline(admin.TabularInline):
model = ObservationAttributeChoice
extra = 1
fields = ['order', 'value', 'label_fr', 'label_nl', 'bootstrap_icon', 'bootstrap_color', 'image', 'is_active']
ordering = ['order']
def get_readonly_fields(self, request, obj=None):
# Une fois créé, la valeur (clé) ne doit pas être modifiée
if obj:
return ['value']
return []
@admin.register(ObservationAttributeChoiceSet)
class ObservationAttributeChoiceSetAdmin(admin.ModelAdmin):
list_display = ('name', 'description_short', 'choices_count', 'is_active', 'updated_at')
list_filter = ('is_active',)
search_fields = ('name', 'description')
inlines = [ObservationAttributeChoiceInline]
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {
'fields': ('name', 'description', 'is_active')
}),
(_("Métadonnées"), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def description_short(self, obj):
if obj.description:
return obj.description[:50] + '...' if len(obj.description) > 50 else obj.description
return "-"
description_short.short_description = _("Description")
def choices_count(self, obj):
return obj.choices.filter(is_active=True).count()
choices_count.short_description = _("Nb. de choix")
@admin.register(ObservationAttributeChoice)
class ObservationAttributeChoiceAdmin(admin.ModelAdmin):
list_display = ('value', 'label_fr', 'choice_set', 'bootstrap_icon', 'color_preview', 'order', 'is_active')
list_filter = ('choice_set', 'bootstrap_color', 'is_active')
search_fields = ('value', 'label_fr', 'label_nl')
ordering = ['choice_set', 'order']
def color_preview(self, obj):
color_map = {
'primary': '#0d6efd',
'secondary': '#6c757d',
'success': '#198754',
'danger': '#dc3545',
'warning': '#ffc107',
'info': '#0dcaf0',
'light': '#f8f9fa',
'dark': '#212529',
}
bg_color = color_map.get(obj.bootstrap_color, '#0d6efd')
return format_html(
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 3px;">{}</span>',
bg_color,
obj.bootstrap_color
)
color_preview.short_description = _("Couleur")
# ============================================================================
# Checklist Admin
# ============================================================================
class ChecklistItemInline(admin.TabularInline):
model = ChecklistItem
extra = 1
fields = ['order', 'name_fr', 'name_nl', 'item_type', 'choice_set', 'is_required']
ordering = ['order']
autocomplete_fields = ['choice_set']
class ChecklistTemplateAdminForm(forms.ModelForm):
"""Formulaire admin avec filtre thématique -> catégorie."""
thematic = forms.ModelChoiceField(
queryset=Thematic.objects.all().order_by('name_fr'),
required=False,
label=_("Thématique"),
help_text=_("Sélectionnez d'abord une thématique pour filtrer les catégories")
)
class Meta:
model = ChecklistTemplate
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Ordonner les catégories par nom
self.fields['category'].queryset = AssetCategory.objects.all().order_by('name_fr')
# Si on édite une instance existante, pré-sélectionner la thématique
if self.instance and self.instance.pk and self.instance.category:
self.fields['thematic'].initial = self.instance.category.thematic
# Filtrer les catégories par cette thématique
self.fields['category'].queryset = AssetCategory.objects.filter(
thematic=self.instance.category.thematic
).order_by('name_fr')
@admin.register(ChecklistTemplate)
class ChecklistTemplateAdmin(admin.ModelAdmin):
form = ChecklistTemplateAdminForm
list_display = ('name_fr', 'category', 'observation_type', 'is_active', 'items_count', 'created_at')
list_filter = ('category__thematic', 'category', 'observation_type', 'is_active')
search_fields = ('name_fr', 'name_nl', 'description')
inlines = [ChecklistItemInline]
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {
'fields': ('name_fr', 'name_nl', 'description')
}),
(_("Configuration"), {
'fields': ('thematic', 'category', 'observation_type', 'is_active')
}),
(_("Métadonnées"), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def items_count(self, obj):
return obj.items.count()
items_count.short_description = _("Nombre d'éléments")
class Media:
js = ('observations/admin/checklist_template.js',)
@admin.register(ChecklistItem)
class ChecklistItemAdmin(admin.ModelAdmin):
list_display = ('name_fr', 'template', 'item_type', 'choice_set', 'is_required', 'order')
list_filter = ('template', 'item_type', 'is_required', 'choice_set')
search_fields = ('name_fr', 'name_nl', 'description_fr')
ordering = ['template', 'order']
autocomplete_fields = ['choice_set']
fieldsets = (
(None, {
'fields': ('template', 'name_fr', 'name_nl', 'description_fr', 'description_nl')
}),
(_("Type de réponse"), {
'fields': ('item_type', 'choice_set', 'is_required', 'order')
}),
(_("Options textuelles (ancien système)"), {
'fields': ('choices_fr', 'choices_nl', 'choices_icons', 'choices_colors'),
'classes': ('collapse',),
'description': _("Utilisez ces champs uniquement si vous ne voulez pas utiliser un ensemble de choix réutilisable.")
}),
)
@admin.register(ObservationChecklistResponse)
class ObservationChecklistResponseAdmin(admin.ModelAdmin):
list_display = ('observation', 'checklist_item', 'get_value', 'created_at')
list_filter = ('checklist_item__template', 'created_at')
search_fields = ('observation__description', 'checklist_item__name_fr')
readonly_fields = ('created_at', 'updated_at')