Add customer accounts, shipment tracking, and purchase reviews.
CI / test (pull_request) Successful in 35s

Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
This commit is contained in:
2026-09-07 06:37:52 -05:00
parent 23a6035ba8
commit c9b81ceed7
59 changed files with 1905 additions and 275 deletions
+160
View File
@@ -0,0 +1,160 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm, 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 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 "",
)