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"]