""" 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('') color = DPE_COLORS.get(letter, '#888') text = f'{letter} {label}'.strip() return format_html( '{}', 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( '', 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( '{}', 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( '/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