from __future__ import annotations from django import forms from django.contrib.auth import get_user_model from django.conf import settings from django.core.exceptions import ValidationError from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field from dal import autocomplete, forward from django.urls import reverse_lazy from django.contrib.auth import get_user_model from orgs.models import UserProfile class MultiFileInput(forms.ClearableFileInput): allow_multiple_selected = True class MultiFileField(forms.Field): widget = MultiFileInput def __init__(self, *args, **kwargs): kwargs.setdefault("required", False) super().__init__(*args, **kwargs) def to_python(self, data): # Accept a list of UploadedFile or an empty list/None return data def validate(self, value): # Only enforce presence if required=True if self.required and not value: raise forms.ValidationError("This field is required.") class PickupRequestForm(forms.Form): name = forms.CharField(max_length=255) email = forms.EmailField(required=False) phone = forms.CharField(max_length=64, required=False) address = forms.CharField(widget=forms.Textarea) preferred_at = forms.DateTimeField(required=False, widget=forms.DateTimeInput(attrs={"type": "datetime-local"})) materials = forms.CharField(label="Materials/Notes", widget=forms.Textarea, required=False) photos = MultiFileField(widget=MultiFileInput, help_text="Optional: upload photos of scrap") class ContactForm(forms.Form): name = forms.CharField(max_length=255) email = forms.EmailField() phone = forms.CharField(max_length=64, required=False) subject = forms.CharField(max_length=255, required=False) message = forms.CharField(widget=forms.Textarea) class RegistrationForm(forms.Form): username = forms.CharField(max_length=150) email = forms.EmailField() password1 = forms.CharField(widget=forms.PasswordInput) password2 = forms.CharField(widget=forms.PasswordInput) role = forms.ChoiceField(choices=[(k, v) for k, v in UserProfile.ROLE_CHOICES if k != UserProfile.ROLE_OWNER]) def clean_username(self): username = self.cleaned_data["username"].strip() User = get_user_model() if User.objects.filter(username__iexact=username).exists(): raise forms.ValidationError("Username already taken.") return username def clean_email(self): email = self.cleaned_data["email"].strip() User = get_user_model() if User.objects.filter(email__iexact=email).exists(): raise forms.ValidationError("Email already registered.") return email def clean(self): data = super().clean() p1 = data.get("password1") p2 = data.get("password2") if p1 and p2 and p1 != p2: self.add_error("password2", "Passwords do not match.") return data class PublicUserForm(forms.ModelForm): class Meta: model = get_user_model() fields = ["first_name", "last_name", "email"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( Field("first_name"), Field("last_name"), Field("email"), ) from api.models import Profile as ApiProfile from orgs.models import UserProfile class PublicProfileForm(forms.ModelForm): tags = forms.CharField(label="Tags", required=False, help_text="Comma-separated") class Meta: model = ApiProfile fields = ["bio", "interests", "industry"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( Field("bio"), Field("interests"), Field("industry"), Field("tags"), ) if self.instance and getattr(self.instance, "pk", None): try: names = list(self.instance.tags.names()) self.fields["tags"].initial = ", ".join(names) except Exception: self.fields["tags"].initial = "" def save(self, commit: bool = True): profile = super().save(commit) tags = [t.strip() for t in self.cleaned_data.get("tags", "").split(",") if t.strip()] try: if commit and hasattr(profile, "tags"): profile.tags.set(tags) except Exception: pass return profile class PublicUserPhotoForm(forms.ModelForm): remove_photo = forms.BooleanField(label="Remove current photo", required=False) class Meta: model = UserProfile fields = ["my_photo", "remove_photo"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["my_photo"].widget = forms.ClearableFileInput(attrs={ "accept": "image/*", "class": "hidden", "id": "id_my_photo", }) self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( Field("my_photo"), Field("remove_photo"), ) def clean_my_photo(self): f = self.cleaned_data.get("my_photo") if not f: return f try: content_type = getattr(f, "content_type", "") if content_type and not content_type.startswith("image/"): raise ValidationError("Please upload an image file.") except Exception: pass max_bytes = 5 * 1024 * 1024 if getattr(f, "size", 0) and f.size > max_bytes: raise ValidationError("Image too large (max 5MB).") return f def save(self, commit: bool = True): instance: UserProfile = super().save(commit=False) remove = self.cleaned_data.get("remove_photo", False) new_file = self.cleaned_data.get("my_photo") if remove and not new_file: try: if instance.my_photo: instance.my_photo.delete(save=False) except Exception: pass instance.my_photo = None if commit: instance.save() return instance class PublicUserProfileExtraForm(forms.ModelForm): """Profile extras with dependent Country → State → City using DAL. Stores country as ISO2 code (e.g., TH), state as state_code/iso2, and city as name. Data sources are loaded from static JSON under `static/json/`. """ class Meta: model = UserProfile fields = [ "phone", "job_title", "department", "preferred_language", "address_line1", "address_line2", "country", "state", "city", "postal_code", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Render preferred_language as a dropdown of available LANGUAGES try: lang_choices = list(getattr(settings, "LANGUAGES", [])) except Exception: lang_choices = [] if lang_choices: self.fields["preferred_language"] = forms.ChoiceField( choices=[("", "—")] + lang_choices, required=False, label=self.fields.get("preferred_language").label if self.fields.get("preferred_language") else "Preferred language", ) # Replace country/state/city with Select2 autocompletes backed by JSON endpoints # Country uses ISO2 code values; state forwards selected country; city forwards both. self.fields["country"] = forms.CharField( required=False, widget=autocomplete.Select2(url=reverse_lazy("public_frontend:ac_countries"), attrs={"data-minimum-input-length": 0}), label=self.fields.get("country").label if self.fields.get("country") else "Country", ) self.fields["state"] = forms.CharField( required=False, widget=autocomplete.Select2( url=reverse_lazy("public_frontend:ac_states"), forward=(forward.Field("country"),), attrs={"data-minimum-input-length": 0}, ), label=self.fields.get("state").label if self.fields.get("state") else "State/Province", ) self.fields["city"] = forms.CharField( required=False, widget=autocomplete.Select2( url=reverse_lazy("public_frontend:ac_cities"), forward=(forward.Field("country"), forward.Field("state")), attrs={"data-minimum-input-length": 1}, ), label=self.fields.get("city").label if self.fields.get("city") else "City", ) # Prepopulate initial choices so Select2 shows current values try: from .utils.geo import country_label, state_label if self.instance and getattr(self.instance, "pk", None): cval = getattr(self.instance, "country", "") or "" sval = getattr(self.instance, "state", "") or "" cityval = getattr(self.instance, "city", "") or "" if cval: clabel = country_label(cval) or cval self.fields["country"].widget.choices = [(cval, clabel)] self.initial["country"] = cval if cval and sval: slabel = state_label(cval, sval) or sval self.fields["state"].widget.choices = [(sval, slabel)] self.initial["state"] = sval if cityval: self.fields["city"].widget.choices = [(cityval, cityval)] self.initial["city"] = cityval except Exception: pass self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( Field("phone"), Field("job_title"), Field("department"), Field("preferred_language"), Field("address_line1"), Field("address_line2"), Field("country"), Field("state"), Field("city"), Field("postal_code"), )