40204-vm/core/views.py
Flatlogic Bot cb35610046 1.0.3
2026-06-04 18:40:22 +00:00

210 lines
8.1 KiB
Python

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())