Template
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
This commit was merged in pull request #10.
This commit is contained in:
1 parent
9cdce7a897
commit
f3e34f5099
66 files changed
+3051
-285
No files matched your search
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Account · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row row-30">
|
||||
<div class="col-lg-3">
|
||||
<h5 class="title-6">Account</h5>
|
||||
<ul class="list-marked">
|
||||
{% if "shop" in enabled_features %}
|
||||
<li><a href="{% url 'account:orders' %}">Orders</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'account:profile' %}">Profile</a></li>
|
||||
<li><a href="{% url 'account:logout' %}">Sign out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
{% block account_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sign in · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-5">
|
||||
<h3>Sign in</h3>
|
||||
<p>View order history, shipping, and reviews.</p>
|
||||
<form class="rd-form" method="post" action="{% url 'account:login' %}">
|
||||
{% csrf_token %}
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row row-20 gutter-20">
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.username.id_for_label }}">Email</label>
|
||||
{{ form.username }}
|
||||
{{ form.username.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.password.id_for_label }}">Password</label>
|
||||
{{ form.password }}
|
||||
{{ form.password.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="button button-lg button-primary" type="submit">Sign in</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p>New here? <a href="{% url 'account:register' %}">Create an account</a>
|
||||
· <a href="{% url 'account:password_reset' %}">Forgot password?</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Password updated · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<h3>Password updated</h3>
|
||||
<p>You can sign in with your new password.</p>
|
||||
<p><a class="button button-lg button-primary" href="{% url 'account:login' %}">Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Choose a new password · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-5">
|
||||
<h3>Choose a new password</h3>
|
||||
{% if validlink %}
|
||||
<form class="rd-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row row-20 gutter-20">
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.new_password1.id_for_label }}">New password</label>
|
||||
{{ form.new_password1 }}
|
||||
{{ form.new_password1.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.new_password2.id_for_label }}">Confirm password</label>
|
||||
{{ form.new_password2 }}
|
||||
{{ form.new_password2.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="button button-lg button-primary" type="submit">Save password</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>This reset link is invalid or expired. <a href="{% url 'account:password_reset' %}">Request a new one</a>.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Check your email · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<h3>Check your email</h3>
|
||||
<p>If an account exists for that address, a reset link is on its way. Check spam if you do not see it.</p>
|
||||
<p><a href="{% url 'account:login' %}">Back to sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load i18n %}{% autoescape off %}
|
||||
Reset your {{ site_name }} password
|
||||
|
||||
Use this link to choose a new password (it expires):
|
||||
{{ protocol }}://{{ domain }}{% url 'account:password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
If you did not ask for a reset, ignore this email.
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reset password · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-5">
|
||||
<h3>Reset password</h3>
|
||||
<p>Enter the email on your account. We will send a reset link if it matches.</p>
|
||||
<form class="rd-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row row-20 gutter-20">
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.email.id_for_label }}">Email</label>
|
||||
{{ form.email }}
|
||||
{{ form.email.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="button button-lg button-primary" type="submit">Send reset link</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p><a href="{% url 'account:login' %}">Back to sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
Password reset for {{ site_name }}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends "accounts/account_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Profile · {{ SITE_NAME }}{% endblock %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{% static 'css/address-autocomplete.css' %}">
|
||||
{% endblock %}
|
||||
{% block account_content %}
|
||||
<h3>Profile & shipping</h3>
|
||||
<p>Name, phone, and a default shipping address. Payment cards stay on Stripe — we only keep a Stripe customer id, never card numbers.</p>
|
||||
{% if profile.stripe_customer_id %}
|
||||
<p class="text-gray-600">Stripe customer on file. Saved cards are offered at checkout by Stripe.</p>
|
||||
{% endif %}
|
||||
<form class="rd-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row row-20 gutter-20" data-address-autocomplete data-suggest-url="{% url 'address_suggest' %}">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.first_name.id_for_label }}">First name</label>
|
||||
{{ form.first_name }}
|
||||
{{ form.first_name.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.last_name.id_for_label }}">Last name</label>
|
||||
{{ form.last_name }}
|
||||
{{ form.last_name.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.phone.id_for_label }}">Phone</label>
|
||||
{{ form.phone }}
|
||||
{{ form.phone.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="form-label-outside">Shipping address</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.address_line1.id_for_label }}">Street address</label>
|
||||
{{ form.address_line1 }}
|
||||
{{ form.address_line1.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.address_line2.id_for_label }}">Apt / suite</label>
|
||||
{{ form.address_line2 }}
|
||||
{{ form.address_line2.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.address_city.id_for_label }}">City</label>
|
||||
{{ form.address_city }}
|
||||
{{ form.address_city.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.address_state.id_for_label }}">State</label>
|
||||
{{ form.address_state }}
|
||||
{{ form.address_state.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.address_zip.id_for_label }}">ZIP</label>
|
||||
{{ form.address_zip }}
|
||||
{{ form.address_zip.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="button button-lg button-primary" type="submit">Save profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'js/address-autocomplete.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Create account · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<h3>Create an account</h3>
|
||||
<p>Track orders, save a shipping address, and review products you bought. Card details stay with Stripe — we never store them.</p>
|
||||
<form class="rd-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row row-20 gutter-20">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.first_name.id_for_label }}">First name</label>
|
||||
{{ form.first_name }}
|
||||
{{ form.first_name.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.last_name.id_for_label }}">Last name</label>
|
||||
{{ form.last_name }}
|
||||
{{ form.last_name.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.email.id_for_label }}">Email</label>
|
||||
{{ form.email }}
|
||||
{{ form.email.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.password1.id_for_label }}">Password</label>
|
||||
{{ form.password1 }}
|
||||
{{ form.password1.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-wrap">
|
||||
<label class="form-label-outside" for="{{ form.password2.id_for_label }}">Confirm password</label>
|
||||
{{ form.password2 }}
|
||||
{{ form.password2.errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="button button-lg button-primary" type="submit">Create account</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p>Already have an account? <a href="{% url 'account:login' %}">Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user