First commit
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user