You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

forms.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import datetime
  2. import string
  3. from django import forms
  4. from django.conf import settings
  5. from django.contrib.auth import forms as auth_forms
  6. from django.contrib.auth.forms import AuthenticationForm
  7. from django.contrib.auth.password_validation import validate_password
  8. from django.contrib.sites.shortcuts import get_current_site
  9. from django.core.exceptions import ValidationError
  10. from django.utils.crypto import get_random_string
  11. from django.utils.http import url_has_allowed_host_and_scheme
  12. from django.utils.translation import gettext_lazy as _
  13. from django.utils.translation import pgettext_lazy
  14. from oscar.apps.customer.utils import get_password_reset_url, normalise_email
  15. from oscar.core.compat import existing_user_fields, get_user_model
  16. from oscar.core.loading import get_class, get_model, get_profile_class
  17. from oscar.core.utils import datetime_combine
  18. from oscar.forms import widgets
  19. CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
  20. ProductAlert = get_model('customer', 'ProductAlert')
  21. User = get_user_model()
  22. def generate_username():
  23. letters = string.ascii_letters
  24. allowed_chars = letters + string.digits + '_'
  25. uname = get_random_string(length=30, allowed_chars=allowed_chars)
  26. try:
  27. User.objects.get(username=uname)
  28. return generate_username()
  29. except User.DoesNotExist:
  30. return uname
  31. class PasswordResetForm(auth_forms.PasswordResetForm):
  32. """
  33. This form takes the same structure as its parent from :py:mod:`django.contrib.auth`
  34. """
  35. def save(self, domain_override=None, request=None, **kwargs):
  36. """
  37. Generates a one-use only link for resetting password and sends to the
  38. user.
  39. """
  40. site = get_current_site(request)
  41. if domain_override is not None:
  42. site.domain = site.name = domain_override
  43. for user in self.get_users(self.cleaned_data['email']):
  44. self.send_password_reset_email(site, user, request)
  45. def send_password_reset_email(self, site, user, request=None):
  46. extra_context = {
  47. 'user': user,
  48. 'site': site,
  49. 'reset_url': get_password_reset_url(user),
  50. 'request': request,
  51. }
  52. CustomerDispatcher().send_password_reset_email_for_user(user, extra_context)
  53. class EmailAuthenticationForm(AuthenticationForm):
  54. """
  55. Extends the standard django AuthenticationForm, to support 75 character
  56. usernames. 75 character usernames are needed to support the EmailOrUsername
  57. authentication backend.
  58. """
  59. username = forms.EmailField(label=_('Email address'))
  60. redirect_url = forms.CharField(
  61. widget=forms.HiddenInput, required=False)
  62. def __init__(self, host, *args, **kwargs):
  63. self.host = host
  64. super().__init__(*args, **kwargs)
  65. def clean_redirect_url(self):
  66. url = self.cleaned_data['redirect_url'].strip()
  67. if url and url_has_allowed_host_and_scheme(url, self.host):
  68. return url
  69. class ConfirmPasswordForm(forms.Form):
  70. """
  71. Extends the standard django AuthenticationForm, to support 75 character
  72. usernames. 75 character usernames are needed to support the EmailOrUsername
  73. authentication backend.
  74. """
  75. password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
  76. def __init__(self, user, *args, **kwargs):
  77. super().__init__(*args, **kwargs)
  78. self.user = user
  79. def clean_password(self):
  80. password = self.cleaned_data['password']
  81. if not self.user.check_password(password):
  82. raise forms.ValidationError(
  83. _("The entered password is not valid!"))
  84. return password
  85. class EmailUserCreationForm(forms.ModelForm):
  86. email = forms.EmailField(label=_('Email address'))
  87. password1 = forms.CharField(
  88. label=_('Password'), widget=forms.PasswordInput)
  89. password2 = forms.CharField(
  90. label=_('Confirm password'), widget=forms.PasswordInput)
  91. redirect_url = forms.CharField(
  92. widget=forms.HiddenInput, required=False)
  93. class Meta:
  94. model = User
  95. fields = ('email',)
  96. def __init__(self, host=None, *args, **kwargs):
  97. self.host = host
  98. super().__init__(*args, **kwargs)
  99. def _post_clean(self):
  100. super()._post_clean()
  101. password = self.cleaned_data.get('password2')
  102. # Validate after self.instance is updated with form data
  103. # otherwise validators can't access email
  104. # see django.contrib.auth.forms.UserCreationForm
  105. if password:
  106. try:
  107. validate_password(password, self.instance)
  108. except forms.ValidationError as error:
  109. self.add_error('password2', error)
  110. def clean_email(self):
  111. """
  112. Checks for existing users with the supplied email address.
  113. """
  114. email = normalise_email(self.cleaned_data['email'])
  115. if User._default_manager.filter(email__iexact=email).exists():
  116. raise forms.ValidationError(
  117. _("A user with that email address already exists"))
  118. return email
  119. def clean_password2(self):
  120. password1 = self.cleaned_data.get('password1', '')
  121. password2 = self.cleaned_data.get('password2', '')
  122. if password1 != password2:
  123. raise forms.ValidationError(
  124. _("The two password fields didn't match."))
  125. return password2
  126. def clean_redirect_url(self):
  127. url = self.cleaned_data['redirect_url'].strip()
  128. if url and url_has_allowed_host_and_scheme(url, self.host):
  129. return url
  130. return settings.LOGIN_REDIRECT_URL
  131. def save(self, commit=True):
  132. user = super().save(commit=False)
  133. user.set_password(self.cleaned_data['password1'])
  134. if 'username' in [f.name for f in User._meta.fields]:
  135. user.username = generate_username()
  136. if commit:
  137. user.save()
  138. return user
  139. class OrderSearchForm(forms.Form):
  140. date_from = forms.DateField(
  141. required=False, label=pgettext_lazy("start date", "From"),
  142. widget=widgets.DatePickerInput())
  143. date_to = forms.DateField(
  144. required=False, label=pgettext_lazy("end date", "To"),
  145. widget=widgets.DatePickerInput())
  146. order_number = forms.CharField(required=False, label=_("Order number"))
  147. def clean(self):
  148. if self.is_valid() and not any([self.cleaned_data['date_from'],
  149. self.cleaned_data['date_to'],
  150. self.cleaned_data['order_number']]):
  151. raise forms.ValidationError(_("At least one field is required."))
  152. return super().clean()
  153. def description(self):
  154. """
  155. Uses the form's data to build a useful description of what orders
  156. are listed.
  157. """
  158. if not self.is_bound or not self.is_valid():
  159. return _('All orders')
  160. else:
  161. date_from = self.cleaned_data['date_from']
  162. date_to = self.cleaned_data['date_to']
  163. order_number = self.cleaned_data['order_number']
  164. return self._orders_description(date_from, date_to, order_number)
  165. def _orders_description(self, date_from, date_to, order_number):
  166. if date_from and date_to:
  167. if order_number:
  168. desc = _('Orders placed between %(date_from)s and '
  169. '%(date_to)s and order number containing '
  170. '%(order_number)s')
  171. else:
  172. desc = _('Orders placed between %(date_from)s and '
  173. '%(date_to)s')
  174. elif date_from:
  175. if order_number:
  176. desc = _('Orders placed since %(date_from)s and '
  177. 'order number containing %(order_number)s')
  178. else:
  179. desc = _('Orders placed since %(date_from)s')
  180. elif date_to:
  181. if order_number:
  182. desc = _('Orders placed until %(date_to)s and '
  183. 'order number containing %(order_number)s')
  184. else:
  185. desc = _('Orders placed until %(date_to)s')
  186. elif order_number:
  187. desc = _('Orders with order number containing %(order_number)s')
  188. else:
  189. return None
  190. params = {
  191. 'date_from': date_from,
  192. 'date_to': date_to,
  193. 'order_number': order_number,
  194. }
  195. return desc % params
  196. def get_filters(self):
  197. date_from = self.cleaned_data['date_from']
  198. date_to = self.cleaned_data['date_to']
  199. order_number = self.cleaned_data['order_number']
  200. kwargs = {}
  201. if date_from:
  202. kwargs['date_placed__gte'] = datetime_combine(date_from, datetime.time.min)
  203. if date_to:
  204. kwargs['date_placed__lte'] = datetime_combine(date_to, datetime.time.max)
  205. if order_number:
  206. kwargs['number__contains'] = order_number
  207. return kwargs
  208. class UserForm(forms.ModelForm):
  209. def __init__(self, user, *args, **kwargs):
  210. self.user = user
  211. kwargs['instance'] = user
  212. super().__init__(*args, **kwargs)
  213. if 'email' in self.fields:
  214. self.fields['email'].required = True
  215. def clean_email(self):
  216. """
  217. Make sure that the email address is always unique as it is
  218. used instead of the username. This is necessary because the
  219. uniqueness of email addresses is *not* enforced on the model
  220. level in ``django.contrib.auth.models.User``.
  221. """
  222. email = normalise_email(self.cleaned_data['email'])
  223. if User._default_manager.filter(
  224. email__iexact=email).exclude(id=self.user.id).exists():
  225. raise ValidationError(
  226. _("A user with this email address already exists"))
  227. # Save the email unaltered
  228. return email
  229. class Meta:
  230. model = User
  231. fields = existing_user_fields(['first_name', 'last_name', 'email'])
  232. Profile = get_profile_class()
  233. if Profile: # noqa (too complex (12))
  234. class UserAndProfileForm(forms.ModelForm):
  235. def __init__(self, user, *args, **kwargs):
  236. try:
  237. instance = Profile.objects.get(user=user)
  238. except Profile.DoesNotExist:
  239. # User has no profile, try a blank one
  240. instance = Profile(user=user)
  241. kwargs['instance'] = instance
  242. super().__init__(*args, **kwargs)
  243. # Get profile field names to help with ordering later
  244. profile_field_names = list(self.fields.keys())
  245. # Get user field names (we look for core user fields first)
  246. core_field_names = set([f.name for f in User._meta.fields])
  247. user_field_names = ['email']
  248. for field_name in ('first_name', 'last_name'):
  249. if field_name in core_field_names:
  250. user_field_names.append(field_name)
  251. user_field_names.extend(User._meta.additional_fields)
  252. # Store user fields so we know what to save later
  253. self.user_field_names = user_field_names
  254. # Add additional user form fields
  255. additional_fields = forms.fields_for_model(
  256. User, fields=user_field_names)
  257. self.fields.update(additional_fields)
  258. # Ensure email is required and initialised correctly
  259. self.fields['email'].required = True
  260. # Set initial values
  261. for field_name in user_field_names:
  262. self.fields[field_name].initial = getattr(user, field_name)
  263. # Ensure order of fields is email, user fields then profile fields
  264. self.fields.keyOrder = user_field_names + profile_field_names
  265. class Meta:
  266. model = Profile
  267. exclude = ('user',)
  268. def clean_email(self):
  269. email = normalise_email(self.cleaned_data['email'])
  270. users_with_email = User._default_manager.filter(
  271. email__iexact=email).exclude(id=self.instance.user.id)
  272. if users_with_email.exists():
  273. raise ValidationError(
  274. _("A user with this email address already exists"))
  275. return email
  276. def save(self, *args, **kwargs):
  277. user = self.instance.user
  278. # Save user also
  279. for field_name in self.user_field_names:
  280. setattr(user, field_name, self.cleaned_data[field_name])
  281. user.save()
  282. return super().save(*args, **kwargs)
  283. ProfileForm = UserAndProfileForm
  284. else:
  285. ProfileForm = UserForm
  286. class ProductAlertForm(forms.ModelForm):
  287. email = forms.EmailField(required=True, label=_('Send notification to'),
  288. widget=forms.TextInput(attrs={
  289. 'placeholder': _('Enter your email')
  290. }))
  291. def __init__(self, user, product, *args, **kwargs):
  292. self.user = user
  293. self.product = product
  294. super().__init__(*args, **kwargs)
  295. # Only show email field to unauthenticated users
  296. if user and user.is_authenticated:
  297. self.fields['email'].widget = forms.HiddenInput()
  298. self.fields['email'].required = False
  299. def save(self, commit=True):
  300. alert = super().save(commit=False)
  301. if self.user.is_authenticated:
  302. alert.user = self.user
  303. alert.product = self.product
  304. if commit:
  305. alert.save()
  306. return alert
  307. def clean(self):
  308. cleaned_data = self.cleaned_data
  309. email = cleaned_data.get('email')
  310. if email:
  311. try:
  312. ProductAlert.objects.get(
  313. product=self.product, email__iexact=email,
  314. status=ProductAlert.ACTIVE)
  315. except ProductAlert.DoesNotExist:
  316. pass
  317. else:
  318. raise forms.ValidationError(_(
  319. "There is already an active stock alert for %s") % email)
  320. # Check that the email address hasn't got other unconfirmed alerts.
  321. # If they do then we don't want to spam them with more until they
  322. # have confirmed or cancelled the existing alert.
  323. if ProductAlert.objects.filter(email__iexact=email,
  324. status=ProductAlert.UNCONFIRMED).count():
  325. raise forms.ValidationError(_(
  326. "%s has been sent a confirmation email for another product "
  327. "alert on this site. Please confirm or cancel that request "
  328. "before signing up for more alerts.") % email)
  329. elif self.user.is_authenticated:
  330. try:
  331. ProductAlert.objects.get(product=self.product,
  332. user=self.user,
  333. status=ProductAlert.ACTIVE)
  334. except ProductAlert.DoesNotExist:
  335. pass
  336. else:
  337. raise forms.ValidationError(_(
  338. "You already have an active alert for this product"))
  339. return cleaned_data
  340. class Meta:
  341. model = ProductAlert
  342. fields = ['email']