165 lines
6.3 KiB
Python
165 lines
6.3 KiB
Python
from urllib.parse import urlparse
|
||
|
||
from django import forms
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.validators import URLValidator
|
||
|
||
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:
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
for field in self.fields.values():
|
||
widget = field.widget
|
||
if widget.__class__.__name__ == "HiddenInput":
|
||
continue
|
||
current = widget.attrs.get("class", "")
|
||
if widget.__class__.__name__ == "Select":
|
||
widget.attrs["class"] = f"form-select {current}".strip()
|
||
elif widget.__class__.__name__ == "Textarea":
|
||
widget.attrs["class"] = f"form-control {current}".strip()
|
||
else:
|
||
widget.attrs["class"] = f"form-control {current}".strip()
|
||
|
||
|
||
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())
|
||
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
|
||
|
||
class Meta:
|
||
model = PropertyEntry
|
||
fields = ["address", "latitude", "longitude", "phone", "email", "listing_type", "idealista_url"]
|
||
widgets = {
|
||
"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"}),
|
||
"email": forms.EmailInput(attrs={"placeholder": "owner@example.com"}),
|
||
"listing_type": forms.Select(),
|
||
}
|
||
|
||
def clean(self):
|
||
cleaned = super().clean()
|
||
address = cleaned.get("address")
|
||
latitude = cleaned.get("latitude")
|
||
longitude = cleaned.get("longitude")
|
||
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.")
|
||
return cleaned
|
||
|
||
|
||
class PropertyPhotoForm(IdealistaUrlCleanMixin, BootstrapFormMixin, forms.ModelForm):
|
||
idealista_url = idealista_url_field()
|
||
|
||
class Meta:
|
||
model = PropertyEntry
|
||
fields = ["photo", "address", "phone", "email", "listing_type", "idealista_url"]
|
||
widgets = {
|
||
"address": forms.TextInput(attrs={"placeholder": "Optional if the photo contains GPS or visible text"}),
|
||
"phone": forms.TextInput(attrs={"placeholder": "Optional contact phone"}),
|
||
"email": forms.EmailInput(attrs={"placeholder": "Optional contact email"}),
|
||
"listing_type": forms.Select(),
|
||
}
|
||
|
||
def clean_photo(self):
|
||
photo = self.cleaned_data.get("photo")
|
||
if not photo:
|
||
raise forms.ValidationError("Choose a property photo to upload.")
|
||
if photo.size > 12 * 1024 * 1024:
|
||
raise forms.ValidationError("Please upload an image under 12MB for this MVP.")
|
||
return photo
|
||
|
||
|
||
class PropertySuggestionForm(BootstrapFormMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = PropertySuggestion
|
||
fields = ["address", "phone", "email", "listing_type", "note"]
|
||
widgets = {
|
||
"address": forms.TextInput(attrs={"placeholder": "Correct or missing address"}),
|
||
"phone": forms.TextInput(attrs={"placeholder": "Correct or missing phone"}),
|
||
"email": forms.EmailInput(attrs={"placeholder": "Correct or missing email"}),
|
||
"listing_type": forms.Select(),
|
||
"note": forms.Textarea(attrs={"rows": 3, "placeholder": "What should be updated?"}),
|
||
}
|
||
|
||
def clean(self):
|
||
cleaned = super().clean()
|
||
if not any(cleaned.get(field) for field in self.fields):
|
||
raise forms.ValidationError("Add at least one suggested detail.")
|
||
return cleaned
|
||
|
||
|
||
class PropertyFlagForm(BootstrapFormMixin, forms.ModelForm):
|
||
class Meta:
|
||
model = PropertyFlag
|
||
fields = ["reason"]
|
||
widgets = {
|
||
"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"]
|
||
|