Files
2026-05-28 23:43:20 +02:00

294 lines
11 KiB
Python

"""
Configuration de l'interface d'administration Django pour les biens immobiliers.
"""
from django.contrib import admin
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import path, reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from adminsortable2.admin import SortableAdminMixin, SortableAdminBase, SortableInlineAdminMixin
from .models import (
Property, PropertyEnvironment, PropertyFeature,
Room, Photo, ContactRequest,
)
# ─── Couleurs des classes DPE / GES ──────────────────────────────────────────
DPE_COLORS = {
'A': '#00B050', 'B': '#67B346', 'C': '#ACCC54',
'D': '#F5D33B', 'E': '#F6A600', 'F': '#E95A0C', 'G': '#CE1622',
}
def _class_badge(letter, label=''):
"""Génère un badge coloré HTML pour une classe DPE/GES."""
if not letter:
return format_html('<span style="color:#666">—</span>')
color = DPE_COLORS.get(letter, '#888')
text = f'{letter} {label}'.strip()
return format_html(
'<span style="background:{};color:#fff;padding:2px 10px;border-radius:4px;'
'font-weight:bold;font-size:13px">{}</span>',
color, text,
)
# ─── Inlines ──────────────────────────────────────────────────────────────────
class RoomInline(SortableInlineAdminMixin, admin.TabularInline):
"""Inline d'édition des pièces avec tri drag-and-drop."""
model = Room
extra = 1
fields = ('name_fr', 'name_en', 'order')
class PhotoInline(SortableInlineAdminMixin, admin.TabularInline):
"""Inline d'édition des photos avec tri drag-and-drop."""
model = Photo
extra = 1
fields = ('image', 'room', 'caption_fr', 'caption_en', 'is_cover', 'order')
readonly_fields = ('preview',)
def preview(self, obj):
"""Aperçu miniature de la photo."""
if obj.image:
return format_html(
'<img src="{}" style="max-height:60px;border-radius:4px">',
obj.image.url,
)
return '—'
preview.short_description = _('Aperçu')
class PropertyEnvironmentInline(admin.StackedInline):
"""Inline d'édition de l'environnement du bien."""
model = PropertyEnvironment
can_delete = False
extra = 0
fieldsets = [
(_('Localisation'), {
'fields': ['location_type', 'description_fr', 'description_en'],
}),
(_('Commerces de proximité'), {
'fields': [
('has_supermarket', 'has_food_shop', 'has_bakery'),
('has_newspaper_shop', 'has_bar', 'has_tobacco_shop'),
('has_garage', 'has_gas_station'),
],
'classes': ['collapse'],
}),
(_('Services mairie / éducation'), {
'fields': [
('has_city_hall_services', 'has_nursery', 'has_public_school'),
],
'classes': ['collapse'],
}),
(_('Services de santé'), {
'fields': [
('has_doctor', 'has_pharmacy', 'has_physiotherapist', 'has_nurse'),
],
'classes': ['collapse'],
}),
]
# ─── Admin principal ──────────────────────────────────────────────────────────
@admin.register(Property)
class PropertyAdmin(SortableAdminBase, admin.ModelAdmin):
"""Administration des biens immobiliers."""
change_form_template = 'admin/properties/property/change_form.html'
list_display = [
'name_fr', 'city', 'type_badge', 'status_badge',
'price_display', 'living_area_display',
'energy_badge', 'ges_badge', 'is_active',
]
list_filter = ['property_type', 'status', 'is_active', 'energy_class', 'condition']
search_fields = ['name_fr', 'name_en', 'address', 'city', 'postal_code']
prepopulated_fields = {'slug': ('name_fr',)}
filter_horizontal = ('features',)
inlines = [PropertyEnvironmentInline, RoomInline, PhotoInline]
fieldsets = [
(_('Identification'), {
'fields': ['name_fr', 'name_en', 'slug', 'is_active'],
}),
(_('Classification'), {
'fields': [('property_type', 'status'), ('price', 'price_on_request')],
}),
(_('Surfaces'), {
'fields': [('living_area', 'total_area', 'total_land_area')],
}),
(_('Composition'), {
'fields': [('rooms_count', 'bedrooms_count', 'bathrooms_count')],
}),
(_('Équipements'), {
'fields': [
('has_elevator', 'exposure'),
('has_garden', 'has_pool', 'has_dependencies'),
'features',
],
}),
(_('Construction'), {
'fields': [('construction_year', 'condition')],
}),
(_('Diagnostics énergétiques'), {
'fields': [
('energy_class', 'energy_value'),
('ges_class', 'ges_value'),
],
}),
(_('Description'), {
'fields': ['description_fr', 'description_en'],
}),
(_('Localisation'), {
'fields': ['address', ('city', 'postal_code'), ('latitude', 'longitude')],
}),
(_('Image de couverture'), {
'fields': ['thumbnail'],
}),
(_('SEO'), {
'fields': [
('meta_title_fr', 'meta_title_en'),
('meta_description_fr', 'meta_description_en'),
('meta_keywords_fr', 'meta_keywords_en'),
],
'classes': ['collapse'],
'description': 'Laisser vide pour une génération automatique.',
}),
]
def type_badge(self, obj):
return obj.get_property_type_display()
type_badge.short_description = _('Type')
def status_badge(self, obj):
color = '#2196F3' if obj.status == 'sale' else '#4CAF50'
return format_html(
'<span style="background:{};color:#fff;padding:2px 8px;border-radius:3px;font-size:12px">{}</span>',
color, obj.get_status_display(),
)
status_badge.short_description = _('Statut')
def price_display(self, obj):
return obj.price_display
price_display.short_description = _('Prix')
def living_area_display(self, obj):
return f'{int(obj.living_area)} m²'
living_area_display.short_description = _('Surface')
def energy_badge(self, obj):
return _class_badge(obj.energy_class, f'{obj.energy_value} kWh' if obj.energy_value else '')
energy_badge.short_description = _('DPE')
def ges_badge(self, obj):
return _class_badge(obj.ges_class, f'{obj.ges_value} kg' if obj.ges_value else '')
ges_badge.short_description = _('GES')
# ── URL et vue d'upload groupé ────────────────────────────────────────────
def get_urls(self):
"""Ajoute l'URL de l'upload groupé de photos."""
urls = super().get_urls()
custom = [
path(
'<int:property_pk>/photos/upload/',
self.admin_site.admin_view(self.upload_photos_view),
name='properties_property_upload_photos',
),
]
return custom + urls
def upload_photos_view(self, request, property_pk):
"""Vue d'upload groupé : une ou plusieurs photos vers une pièce en un seul envoi."""
prop = get_object_or_404(Property, pk=property_pk)
if request.method == 'POST':
files = request.FILES.getlist('images')
room = None
# Création d'une nouvelle pièce si demandée
new_room_fr = request.POST.get('new_room_fr', '').strip()
new_room_en = request.POST.get('new_room_en', '').strip()
if new_room_fr:
room = Room.objects.create(
property=prop,
name_fr=new_room_fr,
name_en=new_room_en or new_room_fr,
order=prop.rooms.count(),
)
else:
room_pk = request.POST.get('room') or None
if room_pk:
room = get_object_or_404(Room, pk=room_pk, property=prop)
# Ordre de départ : après les photos existantes de cette pièce
base_order = (
Photo.objects.filter(property=prop, room=room).count()
)
created = 0
for i, f in enumerate(files):
Photo.objects.create(
property=prop,
room=room,
image=f,
order=base_order + i,
)
created += 1
room_label = room.name_fr if room else '(général)'
from django.contrib import messages as dj_messages
dj_messages.success(
request,
f'{created} photo(s) ajoutée(s) → pièce : {room_label}',
)
# Retour vers la page suivante ou la fiche du bien
next_url = request.POST.get('next', '')
if next_url == 'again':
return redirect('.')
return redirect(
reverse('admin:properties_property_change', args=[property_pk])
)
context = {
**self.admin_site.each_context(request),
'property': prop,
'rooms': prop.rooms.order_by('order'),
'title': f'📷 Ajout de photos — {prop.name_fr}',
'opts': self.model._meta,
}
return render(
request,
'admin/properties/property/upload_photos.html',
context,
)
@admin.register(PropertyFeature)
class PropertyFeatureAdmin(admin.ModelAdmin):
"""Administration des caractéristiques de biens."""
list_display = ['name_fr', 'name_en', 'icon']
search_fields = ['name_fr', 'name_en']
@admin.register(ContactRequest)
class ContactRequestAdmin(admin.ModelAdmin):
"""Administration des demandes de contact (lecture seule)."""
list_display = ['created_at', 'name', 'email', 'phone', 'property', 'ip_address']
list_filter = ['created_at', 'property']
search_fields = ['name', 'email', 'message']
readonly_fields = [
'property', 'name', 'email', 'phone',
'message', 'ip_address', 'created_at',
]
date_hierarchy = 'created_at'
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False