294 lines
11 KiB
Python
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
|