Files
web_django_template/site/accounts/forms.py
T
westfarn f3e34f5099 Add shopper accounts, reviews, tracking, and seed_demo (#10)
## Summary
- Closes #9. Port shop-gated buyer accounts (register/login/profile, password reset, order history), purchase-gated reviews, and Stripe customer ids. Cards stay on Stripe.
- Slim public contact form to email + interest + message. Shipping tracking status, scan events, paste-in numbers, and EasyPost webhook when FEATURE_SHIPPING is on.
- Add template-neutral `seed_demo` (generic SKUs, demo shoppers, guest orders, tracking stubs). Prod refused. `--reset` deletes tagged demo rows after invoices.

## Test plan
- [ ] `cd site && uv run python manage.py test` (173 tests passed locally)
- [ ] Shop on: `/account/register`, login, profile, password reset, claim guest orders by email
- [ ] Shop off: `/account/` URLs absent; Sign in / Account links hidden
- [ ] Checkout as logged-in buyer stores `Order.user` and Stripe customer id; product review only after paid purchase
- [ ] Shipping on: portal paste-in tracking + EasyPost webhook updates status; shipping off: orders still work without tracking UI
- [ ] `seed_demo` refused in prod; `--reset` rebuilds tagged catalog/shoppers without deleting staff invoices

Reviewed-on: #10
2026-09-21 13:29:36 -07:00

179 lines
5.2 KiB
Python

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
AuthenticationForm,
PasswordResetForm,
SetPasswordForm,
UserCreationForm,
)
from django.core.exceptions import ValidationError
from contacts.models import Contact
User = get_user_model()
_INPUT = {"class": "form-input"}
class CustomerRegisterForm(UserCreationForm):
email = forms.EmailField(
widget=forms.EmailInput(attrs={**_INPUT, "autocomplete": "email"})
)
first_name = forms.CharField(
max_length=150,
required=False,
widget=forms.TextInput(attrs={**_INPUT, "autocomplete": "given-name"}),
)
last_name = forms.CharField(
max_length=150,
required=False,
widget=forms.TextInput(attrs={**_INPUT, "autocomplete": "family-name"}),
)
class Meta:
model = User
fields = ("email", "first_name", "last_name", "password1", "password2")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["password1"].widget.attrs.update(_INPUT)
self.fields["password2"].widget.attrs.update(_INPUT)
def clean_email(self):
email = (self.cleaned_data.get("email") or "").strip().lower()
if not email:
raise ValidationError("Enter an email address.")
taken = User.objects.filter(email__iexact=email).exists() or User.objects.filter(
username__iexact=email
).exists()
if taken:
raise ValidationError("An account with that email already exists.")
return email
def save(self, commit=True):
user = super().save(commit=False)
email = self.cleaned_data["email"]
user.username = email
user.email = email
user.first_name = (self.cleaned_data.get("first_name") or "").strip()
user.last_name = (self.cleaned_data.get("last_name") or "").strip()
if commit:
user.save()
return user
class CustomerAuthenticationForm(AuthenticationForm):
username = forms.EmailField(
label="Email",
widget=forms.EmailInput(
attrs={**_INPUT, "id": "id_username", "autocomplete": "email"}
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["password"].widget.attrs.update(
{**_INPUT, "autocomplete": "current-password"}
)
class CustomerPasswordResetForm(PasswordResetForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["email"].widget.attrs.update({**_INPUT, "autocomplete": "email"})
class CustomerSetPasswordForm(SetPasswordForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update(_INPUT)
class CustomerProfileForm(forms.Form):
first_name = forms.CharField(
max_length=150,
required=False,
widget=forms.TextInput(attrs={**_INPUT, "autocomplete": "given-name"}),
)
last_name = forms.CharField(
max_length=150,
required=False,
widget=forms.TextInput(attrs={**_INPUT, "autocomplete": "family-name"}),
)
phone = forms.CharField(
max_length=32,
required=False,
widget=forms.TextInput(attrs={**_INPUT, "autocomplete": "tel"}),
)
address_line1 = forms.CharField(
max_length=200,
required=False,
label="Street address",
widget=forms.TextInput(
attrs={
**_INPUT,
"autocomplete": "off",
"data-ac": "line1",
}
),
)
address_line2 = forms.CharField(
max_length=200,
required=False,
label="Apt / suite",
widget=forms.TextInput(
attrs={
**_INPUT,
"autocomplete": "address-line2",
"data-ac": "line2",
}
),
)
address_city = forms.CharField(
max_length=100,
required=False,
label="City",
widget=forms.TextInput(
attrs={
**_INPUT,
"autocomplete": "address-level2",
"data-ac": "city",
}
),
)
address_state = forms.CharField(
max_length=32,
required=False,
label="State",
widget=forms.TextInput(
attrs={
**_INPUT,
"autocomplete": "address-level1",
"data-ac": "state",
}
),
)
address_zip = forms.CharField(
max_length=20,
required=False,
label="ZIP",
widget=forms.TextInput(
attrs={
**_INPUT,
"autocomplete": "postal-code",
"data-ac": "zip",
}
),
)
def shipping_address(self) -> dict:
data = self.cleaned_data
return Contact.make_postal_address(
line1=data.get("address_line1") or "",
line2=data.get("address_line2") or "",
city=data.get("address_city") or "",
state=data.get("address_state") or "",
zip_code=data.get("address_zip") or "",
)