import math from urllib.parse import quote_plus from django.contrib import messages from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from .forms import ( PropertyFlagForm, PropertyIdealistaLinkForm, PropertyLocationForm, PropertyPhotoForm, PropertySuggestionForm, ) from .image_tools import compress_image, ocr_text_best_effort from .models import PropertyEntry def build_idealista_url(entry): query = entry.address or " ".join(filter(None, [entry.phone, entry.email])) if not query: return "" return f"https://www.idealista.com/en/search/{quote_plus(query)}/" def apply_idealista_fallback(entry): if not entry.idealista_url: entry.idealista_url = build_idealista_url(entry) return entry def _distance_km(lat1, lng1, lat2, lng2): if None in (lat1, lng1, lat2, lng2): return None radius = 6371 phi1, phi2 = math.radians(float(lat1)), math.radians(float(lat2)) d_phi = math.radians(float(lat2) - float(lat1)) d_lambda = math.radians(float(lng2) - float(lng1)) a = math.sin(d_phi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2 return radius * (2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))) def _decorate_distances(entries, user_lat, user_lng): decorated = [] for entry in entries: distance = _distance_km(user_lat, user_lng, entry.latitude, entry.longitude) if user_lat and user_lng else None entry.distance_km = distance decorated.append(entry) return decorated def home(request): entries = list(PropertyEntry.objects.filter(is_flagged=False).order_by("-created_at")[:6]) context = { "page_title": "NearbyNest — mobile property pinboard", "meta_description": "A mobile-first PWA for posting nearby property sightings by current location or photo.", "recent_entries": entries, "total_entries": PropertyEntry.objects.count(), "photo_entries": PropertyEntry.objects.exclude(photo="").count(), } return render(request, "core/index.html", context) def onboarding(request): context = { "page_title": "Onboarding permissions — NearbyNest", "meta_description": "Grant location, notification, and photo permissions for the mobile property pinboard.", } return render(request, "core/onboarding.html", context) def property_list(request): sort = request.GET.get("sort", "recent") user_lat = request.GET.get("lat") user_lng = request.GET.get("lng") entries = list(PropertyEntry.objects.filter(is_flagged=False).order_by("-created_at")) entries = _decorate_distances(entries, user_lat, user_lng) if sort == "distance" and user_lat and user_lng: entries.sort(key=lambda entry: entry.distance_km if entry.distance_km is not None else float("inf")) context = { "page_title": "Public property list — NearbyNest", "meta_description": "Browse public property posts by recency or distance from your current location.", "entries": entries, "sort": sort, "user_lat": user_lat or "", "user_lng": user_lng or "", } return render(request, "core/property_list.html", context) def property_detail(request, pk): entry = get_object_or_404(PropertyEntry.objects.prefetch_related("suggestions", "flags"), pk=pk) context = { "page_title": f"Property #{entry.pk} — NearbyNest", "meta_description": "Review a public property pin, extracted details, community suggestions, and flagging options.", "entry": entry, "suggestion_form": PropertySuggestionForm(), "flag_form": PropertyFlagForm(), "idealista_form": PropertyIdealistaLinkForm( initial={"idealista_url": entry.idealista_url} if entry.idealista_url and not entry.idealista_is_search else None ), } return render(request, "core/property_detail.html", context) @transaction.atomic def add_location_property(request): if request.method == "POST": form = PropertyLocationForm(request.POST) if form.is_valid(): entry = form.save(commit=False) entry.source = PropertyEntry.Source.CURRENT_LOCATION apply_idealista_fallback(entry) entry.save() messages.success(request, "Property pinned to the public list.") return redirect(entry.get_absolute_url()) else: form = PropertyLocationForm() context = { "page_title": "Add property by location or address — NearbyNest", "meta_description": "Add a property sighting from current GPS location or a typed/pasted address, with optional contact details.", "form": form, } return render(request, "core/property_form_location.html", context) @transaction.atomic def add_photo_property(request): if request.method == "POST": form = PropertyPhotoForm(request.POST, request.FILES) if form.is_valid(): uploaded = form.cleaned_data["photo"] processed, error = compress_image(uploaded, uploaded.name) if error: form.add_error("photo", error) else: uploaded.seek(0) entry = form.save(commit=False) entry.source = PropertyEntry.Source.PHOTO entry.photo = processed["file"] if processed["latitude"] is not None and not entry.latitude: entry.latitude = processed["latitude"] entry.longitude = processed["longitude"] entry.has_gps_data = True entry.extracted_text = ocr_text_best_effort(uploaded) apply_idealista_fallback(entry) entry.save() messages.success(request, "Photo compressed under 256KB and added to the pinboard.") return redirect(entry.get_absolute_url()) else: form = PropertyPhotoForm() context = { "page_title": "Add property photo — NearbyNest", "meta_description": "Upload a property photo; the MVP compresses it, checks EXIF GPS, and attempts local OCR.", "form": form, } return render(request, "core/property_form_photo.html", context) @transaction.atomic def update_idealista_link(request, pk): entry = get_object_or_404(PropertyEntry, pk=pk) if request.method != "POST": return redirect(entry.get_absolute_url()) form = PropertyIdealistaLinkForm(request.POST, instance=entry) if form.is_valid(): form.save() messages.success(request, "Idealista listing link saved.") else: message = form.errors.get("idealista_url", ["Paste a valid Idealista listing link before saving."])[0] messages.error(request, message) return redirect(entry.get_absolute_url()) @transaction.atomic def suggest_property_update(request, pk): entry = get_object_or_404(PropertyEntry, pk=pk) if request.method != "POST": return redirect(entry.get_absolute_url()) form = PropertySuggestionForm(request.POST) if form.is_valid(): suggestion = form.save(commit=False) suggestion.property_entry = entry suggestion.save() messages.success(request, "Thanks — your suggested details were saved for review.") else: messages.error(request, "Please add at least one valid detail before submitting a suggestion.") return redirect(entry.get_absolute_url()) @transaction.atomic def flag_property(request, pk): entry = get_object_or_404(PropertyEntry, pk=pk) if request.method != "POST": return redirect(entry.get_absolute_url()) form = PropertyFlagForm(request.POST) if form.is_valid(): flag = form.save(commit=False) flag.property_entry = entry flag.save() entry.flag_count = entry.flags.count() entry.is_flagged = entry.flag_count >= 3 entry.save(update_fields=["flag_count", "is_flagged", "updated_at"]) messages.success(request, "Flag saved. Entries with repeated flags are hidden from the public list.") else: messages.error(request, "Add a short reason so reviewers know what to check.") return redirect(entry.get_absolute_url())