Brak opisu

forms.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. from __future__ import annotations
  2. from django import forms
  3. from django.contrib.auth import get_user_model
  4. from django.conf import settings
  5. from django.core.exceptions import ValidationError
  6. from crispy_forms.helper import FormHelper
  7. from crispy_forms.layout import Layout, Field
  8. from dal import autocomplete, forward
  9. from django.urls import reverse_lazy
  10. from django.contrib.auth import get_user_model
  11. from orgs.models import UserProfile
  12. class MultiFileInput(forms.ClearableFileInput):
  13. allow_multiple_selected = True
  14. class MultiFileField(forms.Field):
  15. widget = MultiFileInput
  16. def __init__(self, *args, **kwargs):
  17. kwargs.setdefault("required", False)
  18. super().__init__(*args, **kwargs)
  19. def to_python(self, data):
  20. # Accept a list of UploadedFile or an empty list/None
  21. return data
  22. def validate(self, value):
  23. # Only enforce presence if required=True
  24. if self.required and not value:
  25. raise forms.ValidationError("This field is required.")
  26. class PickupRequestForm(forms.Form):
  27. name = forms.CharField(max_length=255)
  28. email = forms.EmailField(required=False)
  29. phone = forms.CharField(max_length=64, required=False)
  30. address = forms.CharField(widget=forms.Textarea)
  31. preferred_at = forms.DateTimeField(required=False, widget=forms.DateTimeInput(attrs={"type": "datetime-local"}))
  32. materials = forms.CharField(label="Materials/Notes", widget=forms.Textarea, required=False)
  33. photos = MultiFileField(widget=MultiFileInput, help_text="Optional: upload photos of scrap")
  34. class ContactForm(forms.Form):
  35. name = forms.CharField(max_length=255)
  36. email = forms.EmailField()
  37. phone = forms.CharField(max_length=64, required=False)
  38. subject = forms.CharField(max_length=255, required=False)
  39. message = forms.CharField(widget=forms.Textarea)
  40. class RegistrationForm(forms.Form):
  41. username = forms.CharField(max_length=150)
  42. email = forms.EmailField()
  43. password1 = forms.CharField(widget=forms.PasswordInput)
  44. password2 = forms.CharField(widget=forms.PasswordInput)
  45. role = forms.ChoiceField(choices=[(k, v) for k, v in UserProfile.ROLE_CHOICES if k != UserProfile.ROLE_OWNER])
  46. def clean_username(self):
  47. username = self.cleaned_data["username"].strip()
  48. User = get_user_model()
  49. if User.objects.filter(username__iexact=username).exists():
  50. raise forms.ValidationError("Username already taken.")
  51. return username
  52. def clean_email(self):
  53. email = self.cleaned_data["email"].strip()
  54. User = get_user_model()
  55. if User.objects.filter(email__iexact=email).exists():
  56. raise forms.ValidationError("Email already registered.")
  57. return email
  58. def clean(self):
  59. data = super().clean()
  60. p1 = data.get("password1")
  61. p2 = data.get("password2")
  62. if p1 and p2 and p1 != p2:
  63. self.add_error("password2", "Passwords do not match.")
  64. return data
  65. class PublicUserForm(forms.ModelForm):
  66. class Meta:
  67. model = get_user_model()
  68. fields = ["first_name", "last_name", "email"]
  69. def __init__(self, *args, **kwargs):
  70. super().__init__(*args, **kwargs)
  71. self.helper = FormHelper()
  72. self.helper.form_tag = False
  73. self.helper.layout = Layout(
  74. Field("first_name"),
  75. Field("last_name"),
  76. Field("email"),
  77. )
  78. from api.models import Profile as ApiProfile
  79. from orgs.models import UserProfile
  80. class PublicProfileForm(forms.ModelForm):
  81. tags = forms.CharField(label="Tags", required=False, help_text="Comma-separated")
  82. class Meta:
  83. model = ApiProfile
  84. fields = ["bio", "interests", "industry"]
  85. def __init__(self, *args, **kwargs):
  86. super().__init__(*args, **kwargs)
  87. self.helper = FormHelper()
  88. self.helper.form_tag = False
  89. self.helper.layout = Layout(
  90. Field("bio"),
  91. Field("interests"),
  92. Field("industry"),
  93. Field("tags"),
  94. )
  95. if self.instance and getattr(self.instance, "pk", None):
  96. try:
  97. names = list(self.instance.tags.names())
  98. self.fields["tags"].initial = ", ".join(names)
  99. except Exception:
  100. self.fields["tags"].initial = ""
  101. def save(self, commit: bool = True):
  102. profile = super().save(commit)
  103. tags = [t.strip() for t in self.cleaned_data.get("tags", "").split(",") if t.strip()]
  104. try:
  105. if commit and hasattr(profile, "tags"):
  106. profile.tags.set(tags)
  107. except Exception:
  108. pass
  109. return profile
  110. class PublicUserPhotoForm(forms.ModelForm):
  111. remove_photo = forms.BooleanField(label="Remove current photo", required=False)
  112. class Meta:
  113. model = UserProfile
  114. fields = ["my_photo", "remove_photo"]
  115. def __init__(self, *args, **kwargs):
  116. super().__init__(*args, **kwargs)
  117. self.fields["my_photo"].widget = forms.ClearableFileInput(attrs={
  118. "accept": "image/*",
  119. "class": "hidden",
  120. "id": "id_my_photo",
  121. })
  122. self.helper = FormHelper()
  123. self.helper.form_tag = False
  124. self.helper.layout = Layout(
  125. Field("my_photo"),
  126. Field("remove_photo"),
  127. )
  128. def clean_my_photo(self):
  129. f = self.cleaned_data.get("my_photo")
  130. if not f:
  131. return f
  132. try:
  133. content_type = getattr(f, "content_type", "")
  134. if content_type and not content_type.startswith("image/"):
  135. raise ValidationError("Please upload an image file.")
  136. except Exception:
  137. pass
  138. max_bytes = 5 * 1024 * 1024
  139. if getattr(f, "size", 0) and f.size > max_bytes:
  140. raise ValidationError("Image too large (max 5MB).")
  141. return f
  142. def save(self, commit: bool = True):
  143. instance: UserProfile = super().save(commit=False)
  144. remove = self.cleaned_data.get("remove_photo", False)
  145. new_file = self.cleaned_data.get("my_photo")
  146. if remove and not new_file:
  147. try:
  148. if instance.my_photo:
  149. instance.my_photo.delete(save=False)
  150. except Exception:
  151. pass
  152. instance.my_photo = None
  153. if commit:
  154. instance.save()
  155. return instance
  156. class PublicUserProfileExtraForm(forms.ModelForm):
  157. """Profile extras with dependent Country → State → City using DAL.
  158. Stores country as ISO2 code (e.g., TH), state as state_code/iso2, and city as name.
  159. Data sources are loaded from static JSON under `static/json/`.
  160. """
  161. class Meta:
  162. model = UserProfile
  163. fields = [
  164. "phone",
  165. "job_title",
  166. "department",
  167. "preferred_language",
  168. "address_line1",
  169. "address_line2",
  170. "country",
  171. "state",
  172. "city",
  173. "postal_code",
  174. ]
  175. def __init__(self, *args, **kwargs):
  176. super().__init__(*args, **kwargs)
  177. # Render preferred_language as a dropdown of available LANGUAGES
  178. try:
  179. lang_choices = list(getattr(settings, "LANGUAGES", []))
  180. except Exception:
  181. lang_choices = []
  182. if lang_choices:
  183. self.fields["preferred_language"] = forms.ChoiceField(
  184. choices=[("", "—")] + lang_choices,
  185. required=False,
  186. label=self.fields.get("preferred_language").label if self.fields.get("preferred_language") else "Preferred language",
  187. )
  188. # Replace country/state/city with Select2 autocompletes backed by JSON endpoints
  189. # Country uses ISO2 code values; state forwards selected country; city forwards both.
  190. self.fields["country"] = forms.CharField(
  191. required=False,
  192. widget=autocomplete.Select2(url=reverse_lazy("public_frontend:ac_countries"), attrs={"data-minimum-input-length": 0}),
  193. label=self.fields.get("country").label if self.fields.get("country") else "Country",
  194. )
  195. self.fields["state"] = forms.CharField(
  196. required=False,
  197. widget=autocomplete.Select2(
  198. url=reverse_lazy("public_frontend:ac_states"),
  199. forward=(forward.Field("country"),),
  200. attrs={"data-minimum-input-length": 0},
  201. ),
  202. label=self.fields.get("state").label if self.fields.get("state") else "State/Province",
  203. )
  204. self.fields["city"] = forms.CharField(
  205. required=False,
  206. widget=autocomplete.Select2(
  207. url=reverse_lazy("public_frontend:ac_cities"),
  208. forward=(forward.Field("country"), forward.Field("state")),
  209. attrs={"data-minimum-input-length": 1},
  210. ),
  211. label=self.fields.get("city").label if self.fields.get("city") else "City",
  212. )
  213. # Prepopulate initial choices so Select2 shows current values
  214. try:
  215. from .utils.geo import country_label, state_label
  216. if self.instance and getattr(self.instance, "pk", None):
  217. cval = getattr(self.instance, "country", "") or ""
  218. sval = getattr(self.instance, "state", "") or ""
  219. cityval = getattr(self.instance, "city", "") or ""
  220. if cval:
  221. clabel = country_label(cval) or cval
  222. self.fields["country"].widget.choices = [(cval, clabel)]
  223. self.initial["country"] = cval
  224. if cval and sval:
  225. slabel = state_label(cval, sval) or sval
  226. self.fields["state"].widget.choices = [(sval, slabel)]
  227. self.initial["state"] = sval
  228. if cityval:
  229. self.fields["city"].widget.choices = [(cityval, cityval)]
  230. self.initial["city"] = cityval
  231. except Exception:
  232. pass
  233. self.helper = FormHelper()
  234. self.helper.form_tag = False
  235. self.helper.layout = Layout(
  236. Field("phone"),
  237. Field("job_title"),
  238. Field("department"),
  239. Field("preferred_language"),
  240. Field("address_line1"),
  241. Field("address_line2"),
  242. Field("country"),
  243. Field("state"),
  244. Field("city"),
  245. Field("postal_code"),
  246. )