1.0.3
This commit is contained in:
parent
a0758d4015
commit
cb35610046
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/tests.cpython-311.pyc
Normal file
BIN
core/__pycache__/tests.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -19,7 +19,7 @@ class PropertyFlagInline(admin.TabularInline):
|
|||||||
class PropertyEntryAdmin(admin.ModelAdmin):
|
class PropertyEntryAdmin(admin.ModelAdmin):
|
||||||
list_display = ("id", "address", "listing_type", "source", "has_gps_data", "flag_count", "created_at")
|
list_display = ("id", "address", "listing_type", "source", "has_gps_data", "flag_count", "created_at")
|
||||||
list_filter = ("listing_type", "source", "has_gps_data", "is_flagged")
|
list_filter = ("listing_type", "source", "has_gps_data", "is_flagged")
|
||||||
search_fields = ("address", "phone", "email", "extracted_text")
|
search_fields = ("address", "phone", "email", "idealista_url", "extracted_text")
|
||||||
readonly_fields = ("created_at", "updated_at")
|
readonly_fields = ("created_at", "updated_at")
|
||||||
inlines = [PropertySuggestionInline, PropertyFlagInline]
|
inlines = [PropertySuggestionInline, PropertyFlagInline]
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,65 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import URLValidator
|
||||||
|
|
||||||
from .models import PropertyEntry, PropertyFlag, PropertySuggestion
|
from .models import PropertyEntry, PropertyFlag, PropertySuggestion
|
||||||
|
|
||||||
|
|
||||||
|
IDEALISTA_DOMAINS = ("idealista.com", "idealista.pt", "idealista.it")
|
||||||
|
|
||||||
|
|
||||||
|
def idealista_url_field(*, required=False, label="Idealista link"):
|
||||||
|
return forms.CharField(
|
||||||
|
required=required,
|
||||||
|
label=label,
|
||||||
|
help_text=(
|
||||||
|
"Optional. Paste the exact Idealista listing URL if you already found it. "
|
||||||
|
"If left empty, we’ll create a best-effort Idealista search link."
|
||||||
|
),
|
||||||
|
widget=forms.URLInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "https://www.idealista.com/inmueble/123456/",
|
||||||
|
"autocomplete": "url",
|
||||||
|
"inputmode": "url",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_idealista_url_value(value, *, required=False):
|
||||||
|
value = (value or "").strip()
|
||||||
|
if not value:
|
||||||
|
if required:
|
||||||
|
raise ValidationError("Paste the exact Idealista listing URL.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if "://" not in value:
|
||||||
|
value = f"https://{value}"
|
||||||
|
|
||||||
|
validator = URLValidator(schemes=["http", "https"])
|
||||||
|
try:
|
||||||
|
validator(value)
|
||||||
|
except ValidationError as exc:
|
||||||
|
raise ValidationError(
|
||||||
|
"Paste a valid Idealista URL, for example https://www.idealista.com/inmueble/123456/."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
host = (urlparse(value).hostname or "").lower()
|
||||||
|
is_idealista = any(host == domain or host.endswith(f".{domain}") for domain in IDEALISTA_DOMAINS)
|
||||||
|
if not is_idealista:
|
||||||
|
raise ValidationError("Use a link from idealista.com, idealista.pt, or idealista.it.")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class IdealistaUrlCleanMixin:
|
||||||
|
def clean_idealista_url(self):
|
||||||
|
field = self.fields["idealista_url"]
|
||||||
|
return clean_idealista_url_value(self.cleaned_data.get("idealista_url"), required=field.required)
|
||||||
|
|
||||||
|
|
||||||
class BootstrapFormMixin:
|
class BootstrapFormMixin:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -19,13 +76,14 @@ class BootstrapFormMixin:
|
|||||||
widget.attrs["class"] = f"form-control {current}".strip()
|
widget.attrs["class"] = f"form-control {current}".strip()
|
||||||
|
|
||||||
|
|
||||||
class PropertyLocationForm(BootstrapFormMixin, forms.ModelForm):
|
class PropertyLocationForm(IdealistaUrlCleanMixin, BootstrapFormMixin, forms.ModelForm):
|
||||||
|
idealista_url = idealista_url_field()
|
||||||
latitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
|
latitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
|
||||||
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
|
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PropertyEntry
|
model = PropertyEntry
|
||||||
fields = ["address", "latitude", "longitude", "phone", "email", "listing_type"]
|
fields = ["address", "latitude", "longitude", "phone", "email", "listing_type", "idealista_url"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"address": forms.TextInput(attrs={"placeholder": "Type or paste the full address", "autocomplete": "street-address", "data-manual-address": "true"}),
|
"address": forms.TextInput(attrs={"placeholder": "Type or paste the full address", "autocomplete": "street-address", "data-manual-address": "true"}),
|
||||||
"phone": forms.TextInput(attrs={"placeholder": "+34 600 000 000"}),
|
"phone": forms.TextInput(attrs={"placeholder": "+34 600 000 000"}),
|
||||||
@ -38,15 +96,21 @@ class PropertyLocationForm(BootstrapFormMixin, forms.ModelForm):
|
|||||||
address = cleaned.get("address")
|
address = cleaned.get("address")
|
||||||
latitude = cleaned.get("latitude")
|
latitude = cleaned.get("latitude")
|
||||||
longitude = cleaned.get("longitude")
|
longitude = cleaned.get("longitude")
|
||||||
if not address and (latitude is None or longitude is None):
|
if address:
|
||||||
|
cleaned["latitude"] = None
|
||||||
|
cleaned["longitude"] = None
|
||||||
|
return cleaned
|
||||||
|
if latitude is None or longitude is None:
|
||||||
raise forms.ValidationError("Type or paste an address, or allow location, so this property can be placed on the pinboard.")
|
raise forms.ValidationError("Type or paste an address, or allow location, so this property can be placed on the pinboard.")
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
class PropertyPhotoForm(BootstrapFormMixin, forms.ModelForm):
|
class PropertyPhotoForm(IdealistaUrlCleanMixin, BootstrapFormMixin, forms.ModelForm):
|
||||||
|
idealista_url = idealista_url_field()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PropertyEntry
|
model = PropertyEntry
|
||||||
fields = ["photo", "address", "phone", "email", "listing_type"]
|
fields = ["photo", "address", "phone", "email", "listing_type", "idealista_url"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"address": forms.TextInput(attrs={"placeholder": "Optional if the photo contains GPS or visible text"}),
|
"address": forms.TextInput(attrs={"placeholder": "Optional if the photo contains GPS or visible text"}),
|
||||||
"phone": forms.TextInput(attrs={"placeholder": "Optional contact phone"}),
|
"phone": forms.TextInput(attrs={"placeholder": "Optional contact phone"}),
|
||||||
@ -89,3 +153,12 @@ class PropertyFlagForm(BootstrapFormMixin, forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
"reason": forms.TextInput(attrs={"placeholder": "Duplicate, spam, private info, already removed..."}),
|
"reason": forms.TextInput(attrs={"placeholder": "Duplicate, spam, private info, already removed..."}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyIdealistaLinkForm(IdealistaUrlCleanMixin, BootstrapFormMixin, forms.ModelForm):
|
||||||
|
idealista_url = idealista_url_field(required=True, label="Exact Idealista listing URL")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PropertyEntry
|
||||||
|
fields = ["idealista_url"]
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,14 @@ class PropertyEntry(models.Model):
|
|||||||
def has_location(self):
|
def has_location(self):
|
||||||
return self.latitude is not None and self.longitude is not None
|
return self.latitude is not None and self.longitude is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def idealista_is_search(self):
|
||||||
|
return "/search/" in (self.idealista_url or "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def idealista_link_label(self):
|
||||||
|
return "Open Idealista search" if self.idealista_is_search else "Open Idealista listing"
|
||||||
|
|
||||||
|
|
||||||
class PropertySuggestion(models.Model):
|
class PropertySuggestion(models.Model):
|
||||||
property_entry = models.ForeignKey(PropertyEntry, on_delete=models.CASCADE, related_name="suggestions")
|
property_entry = models.ForeignKey(PropertyEntry, on_delete=models.CASCADE, related_name="suggestions")
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="mini-tags">
|
<div class="mini-tags">
|
||||||
{% if entry.phone %}<span>Phone</span>{% endif %}
|
{% if entry.phone %}<span>Phone</span>{% endif %}
|
||||||
{% if entry.email %}<span>Email</span>{% endif %}
|
{% if entry.email %}<span>Email</span>{% endif %}
|
||||||
{% if entry.idealista_url %}<span>Idealista link</span>{% endif %}
|
{% if entry.idealista_url %}<span>{% if entry.idealista_is_search %}Idealista search{% else %}Idealista listing{% endif %}</span>{% endif %}
|
||||||
{% if not entry.phone and not entry.email %}<span>Needs details</span>{% endif %}
|
{% if not entry.phone and not entry.email %}<span>Needs details</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<a class="stretched-link" href="{{ entry.get_absolute_url }}" aria-label="Open property {{ entry.pk }}"></a>
|
<a class="stretched-link" href="{{ entry.get_absolute_url }}" aria-label="Open property {{ entry.pk }}"></a>
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
<div class="analysis-box"><h2>Text spotted in photo</h2><p>{{ entry.extracted_text }}</p></div>
|
<div class="analysis-box"><h2>Text spotted in photo</h2><p>{{ entry.extracted_text }}</p></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if entry.idealista_url %}
|
{% if entry.idealista_url %}
|
||||||
<a class="btn btn-ghost" href="{{ entry.idealista_url }}" target="_blank" rel="noopener">Open best-effort Idealista search</a>
|
<a class="btn btn-ghost" href="{{ entry.idealista_url }}" target="_blank" rel="noopener">{{ entry.idealista_link_label }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -40,6 +40,19 @@
|
|||||||
<button class="btn btn-nest w-100" type="submit">Save suggestion</button>
|
<button class="btn btn-nest w-100" type="submit">Save suggestion</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="form-card compact-card">
|
||||||
|
<h2>{% if entry.idealista_url and not entry.idealista_is_search %}Update Idealista link{% else %}Add exact Idealista link{% endif %}</h2>
|
||||||
|
<p class="text-muted">{% if entry.idealista_url and entry.idealista_is_search %}We only have an automatic search link right now. Paste the exact listing URL if you find it.{% elif entry.idealista_url %}This listing already has an Idealista URL. You can replace it if needed.{% else %}No Idealista link was found automatically. Paste the exact listing URL if you know it.{% endif %}</p>
|
||||||
|
<form method="post" action="{% url 'update_idealista_link' entry.pk %}" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="{{ idealista_form.idealista_url.id_for_label }}">{{ idealista_form.idealista_url.label }}</label>
|
||||||
|
{{ idealista_form.idealista_url }}
|
||||||
|
<div class="form-text">Only Idealista links are accepted. You can paste without https://.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nest w-100" type="submit">Save Idealista link</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<section class="form-card compact-card danger-soft">
|
<section class="form-card compact-card danger-soft">
|
||||||
<h2>Flag for removal</h2>
|
<h2>Flag for removal</h2>
|
||||||
<p class="text-muted">Repeated flags hide an entry until a future admin review flow is added.</p>
|
<p class="text-muted">Repeated flags hide an entry until a future admin review flow is added.</p>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<p class="eyebrow">Create from location or address</p>
|
<p class="eyebrow">Create from location or address</p>
|
||||||
<h1>Add a property pin</h1>
|
<h1>Add a property pin</h1>
|
||||||
<p class="text-muted">Use your current GPS position when available, or type/paste the address manually if browser location is blocked, denied, or unsupported.</p>
|
<p class="text-muted">Use your current GPS position when available, or type/paste the address manually if browser location is blocked, denied, or unsupported. If you paste an address, NearbyNest uses that address for the listing instead of the browser’s current location. If you already have the exact Idealista listing, paste it before publishing.</p>
|
||||||
<div class="location-choice-panel mb-3" aria-label="Choose how to add location information">
|
<div class="location-choice-panel mb-3" aria-label="Choose how to add location information">
|
||||||
<button class="btn btn-ghost" type="button" data-action="fill-current-location">Use my current location</button>
|
<button class="btn btn-ghost" type="button" data-action="fill-current-location">Use my current location</button>
|
||||||
<button class="btn btn-outline-dark rounded-pill" type="button" data-action="manual-address">Type or paste address</button>
|
<button class="btn btn-outline-dark rounded-pill" type="button" data-action="manual-address">Type or paste address</button>
|
||||||
@ -26,7 +26,8 @@
|
|||||||
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% if field.name == "address" %}<div class="form-text">Required if you do not grant current-location access.</div>{% endif %}
|
{% if field.name == "address" %}<div class="form-text">Required if you do not grant current-location access.</div>{% endif %}
|
||||||
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
|
{% if field.name == "idealista_url" %}<div class="form-text">Optional: manual links are saved first. If empty, NearbyNest creates a best-effort Idealista search link.</div>{% endif %}
|
||||||
|
{% if field.help_text and field.name != "idealista_url" %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
|
||||||
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
|
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<p class="eyebrow">Create from photo</p>
|
<p class="eyebrow">Create from photo</p>
|
||||||
<h1>Upload a property photo</h1>
|
<h1>Upload a property photo</h1>
|
||||||
<p class="text-muted">The MVP stores only a resized JPEG under 256KB, checks EXIF GPS, and attempts local OCR if a system OCR binary is installed.</p>
|
<p class="text-muted">The MVP stores only a resized JPEG under 256KB, checks EXIF GPS, and attempts local OCR. You can also paste the exact Idealista listing link before we create any search fallback.</p>
|
||||||
<form method="post" enctype="multipart/form-data" novalidate>
|
<form method="post" enctype="multipart/form-data" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
|
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
|
||||||
@ -15,6 +15,7 @@
|
|||||||
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% if field.name == 'photo' %}<div class="form-text">Originals are discarded after compression; max upload 12MB.</div>{% endif %}
|
{% if field.name == 'photo' %}<div class="form-text">Originals are discarded after compression; max upload 12MB.</div>{% endif %}
|
||||||
|
{% if field.name == 'idealista_url' %}<div class="form-text">Optional: paste the exact Idealista listing URL. If empty, NearbyNest creates a best-effort search link.</div>{% endif %}
|
||||||
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
|
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -1,3 +1,56 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
from .forms import PropertyLocationForm
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyLocationFormTests(TestCase):
|
||||||
|
def test_address_overrides_browser_location(self):
|
||||||
|
form = PropertyLocationForm(
|
||||||
|
data={
|
||||||
|
"address": "123 Main St, Madrid",
|
||||||
|
"latitude": "40.416800",
|
||||||
|
"longitude": "-3.703800",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"listing_type": "unknown",
|
||||||
|
"idealista_url": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(form.is_valid(), form.errors)
|
||||||
|
self.assertIsNone(form.cleaned_data["latitude"])
|
||||||
|
self.assertIsNone(form.cleaned_data["longitude"])
|
||||||
|
self.assertEqual(form.cleaned_data["address"], "123 Main St, Madrid")
|
||||||
|
|
||||||
|
def test_browser_location_is_kept_when_no_address_is_entered(self):
|
||||||
|
form = PropertyLocationForm(
|
||||||
|
data={
|
||||||
|
"address": "",
|
||||||
|
"latitude": "40.416800",
|
||||||
|
"longitude": "-3.703800",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"listing_type": "unknown",
|
||||||
|
"idealista_url": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(form.is_valid(), form.errors)
|
||||||
|
self.assertIsNotNone(form.cleaned_data["latitude"])
|
||||||
|
self.assertIsNotNone(form.cleaned_data["longitude"])
|
||||||
|
|
||||||
|
def test_address_or_location_is_required(self):
|
||||||
|
form = PropertyLocationForm(
|
||||||
|
data={
|
||||||
|
"address": "",
|
||||||
|
"latitude": "",
|
||||||
|
"longitude": "",
|
||||||
|
"phone": "",
|
||||||
|
"email": "",
|
||||||
|
"listing_type": "unknown",
|
||||||
|
"idealista_url": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from .views import (
|
|||||||
property_detail,
|
property_detail,
|
||||||
property_list,
|
property_list,
|
||||||
suggest_property_update,
|
suggest_property_update,
|
||||||
|
update_idealista_link,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -19,5 +20,6 @@ urlpatterns = [
|
|||||||
path("properties/add/photo/", add_photo_property, name="add_photo_property"),
|
path("properties/add/photo/", add_photo_property, name="add_photo_property"),
|
||||||
path("properties/<int:pk>/", property_detail, name="property_detail"),
|
path("properties/<int:pk>/", property_detail, name="property_detail"),
|
||||||
path("properties/<int:pk>/suggest/", suggest_property_update, name="suggest_property_update"),
|
path("properties/<int:pk>/suggest/", suggest_property_update, name="suggest_property_update"),
|
||||||
|
path("properties/<int:pk>/idealista/", update_idealista_link, name="update_idealista_link"),
|
||||||
path("properties/<int:pk>/flag/", flag_property, name="flag_property"),
|
path("properties/<int:pk>/flag/", flag_property, name="flag_property"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -6,7 +6,13 @@ from django.db import transaction
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .forms import PropertyFlagForm, PropertyLocationForm, PropertyPhotoForm, PropertySuggestionForm
|
from .forms import (
|
||||||
|
PropertyFlagForm,
|
||||||
|
PropertyIdealistaLinkForm,
|
||||||
|
PropertyLocationForm,
|
||||||
|
PropertyPhotoForm,
|
||||||
|
PropertySuggestionForm,
|
||||||
|
)
|
||||||
from .image_tools import compress_image, ocr_text_best_effort
|
from .image_tools import compress_image, ocr_text_best_effort
|
||||||
from .models import PropertyEntry
|
from .models import PropertyEntry
|
||||||
|
|
||||||
@ -18,6 +24,12 @@ def build_idealista_url(entry):
|
|||||||
return f"https://www.idealista.com/en/search/{quote_plus(query)}/"
|
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):
|
def _distance_km(lat1, lng1, lat2, lng2):
|
||||||
if None in (lat1, lng1, lat2, lng2):
|
if None in (lat1, lng1, lat2, lng2):
|
||||||
return None
|
return None
|
||||||
@ -85,6 +97,9 @@ def property_detail(request, pk):
|
|||||||
"entry": entry,
|
"entry": entry,
|
||||||
"suggestion_form": PropertySuggestionForm(),
|
"suggestion_form": PropertySuggestionForm(),
|
||||||
"flag_form": PropertyFlagForm(),
|
"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)
|
return render(request, "core/property_detail.html", context)
|
||||||
|
|
||||||
@ -96,7 +111,7 @@ def add_location_property(request):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
entry = form.save(commit=False)
|
entry = form.save(commit=False)
|
||||||
entry.source = PropertyEntry.Source.CURRENT_LOCATION
|
entry.source = PropertyEntry.Source.CURRENT_LOCATION
|
||||||
entry.idealista_url = build_idealista_url(entry)
|
apply_idealista_fallback(entry)
|
||||||
entry.save()
|
entry.save()
|
||||||
messages.success(request, "Property pinned to the public list.")
|
messages.success(request, "Property pinned to the public list.")
|
||||||
return redirect(entry.get_absolute_url())
|
return redirect(entry.get_absolute_url())
|
||||||
@ -129,7 +144,7 @@ def add_photo_property(request):
|
|||||||
entry.longitude = processed["longitude"]
|
entry.longitude = processed["longitude"]
|
||||||
entry.has_gps_data = True
|
entry.has_gps_data = True
|
||||||
entry.extracted_text = ocr_text_best_effort(uploaded)
|
entry.extracted_text = ocr_text_best_effort(uploaded)
|
||||||
entry.idealista_url = build_idealista_url(entry)
|
apply_idealista_fallback(entry)
|
||||||
entry.save()
|
entry.save()
|
||||||
messages.success(request, "Photo compressed under 256KB and added to the pinboard.")
|
messages.success(request, "Photo compressed under 256KB and added to the pinboard.")
|
||||||
return redirect(entry.get_absolute_url())
|
return redirect(entry.get_absolute_url())
|
||||||
@ -143,6 +158,22 @@ def add_photo_property(request):
|
|||||||
return render(request, "core/property_form_photo.html", context)
|
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
|
@transaction.atomic
|
||||||
def suggest_property_update(request, pk):
|
def suggest_property_update(request, pk):
|
||||||
entry = get_object_or_404(PropertyEntry, pk=pk)
|
entry = get_object_or_404(PropertyEntry, pk=pk)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user