| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- 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"),
- )
|