Merge branch 'feature/mobile' into develop

This commit is contained in:
Nathan Chapman 2022-04-06 19:13:55 -06:00
commit 933585a194
38 changed files with 1346 additions and 468 deletions

View File

@ -5,7 +5,15 @@ from .models import Address, User
class AddressForm(forms.ModelForm):
class Meta:
model = Address
fields = '__all__'
fields = (
'first_name',
'last_name',
'street_address_1',
'street_address_2',
'city',
'state',
'postal_code',
)
class AccountCreateForm(UserCreationForm):
@ -16,8 +24,24 @@ class AccountCreateForm(UserCreationForm):
class AccountUpdateForm(UserChangeForm):
class Meta:
model = User
fields = [
fields = (
'first_name',
'last_name',
'email',
]
'default_shipping_address',
'addresses',
)
class CustomerUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['default_shipping_address'].queryset = kwargs['instance'].addresses
class Meta:
model = User
fields = (
'first_name',
'last_name',
'email',
'default_shipping_address',
)

View File

@ -73,6 +73,12 @@ class ProductPhoto(models.Model):
def __str__(self):
return self.product.name
def delete(self, *args, **kwargs):
storage, path = self.image.storage, self.image.path
super(ProductPhoto, self).delete(*args, **kwargs)
storage.delete(path)
# def save(self, *args, **kwargs):
# super().save(*args, **kwargs)
@ -109,6 +115,9 @@ class Coupon(models.Model):
class Meta:
ordering = ("code",)
def __str__(self):
return self.name
@property
def is_valid(self):
today = timezone.localtime(timezone.now())
@ -224,6 +233,9 @@ class Order(models.Model):
def get_absolute_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
class Meta:
ordering = ('-created_at',)
class Transaction(models.Model):

View File

@ -7,7 +7,7 @@
<h1><img src="{% static 'images/coupon.png' %}" alt=""> Coupon</h1>
</header>
<section class="coupon__detail object__panel">
<form method="post">{% csrf_token %}
<form method="post" class="panel__item">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>

View File

@ -6,8 +6,8 @@
<header class="object__header">
<h1><img src="{% static 'images/coupon.png' %}" alt=""> {{ coupon.name }}</h1>
<div class="object__menu">
<a href="" class="action-button action-button--warning">Delete</a>
<a href="" class="action-button">Edit</a>
<a href="{% url 'dashboard:coupon-delete' coupon.pk %}" class="action-button action-button--warning">Delete</a>
<a href="{% url 'dashboard:coupon-update' coupon.pk %}" class="action-button">Edit</a>
</div>
</header>
<section class="product__detail object__panel">

View File

@ -2,6 +2,7 @@
{% block content %}
<article class="product">
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">&larr; Back</a></p>
<header class="object__header">
<h1>Update Customer</h1>
</header>

View File

@ -29,7 +29,7 @@
{% with product=item.product %}
<figure class="item__figure">
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
<figcaption><strong>{{product.name}}</strong></figcaption>
<figcaption><strong>{{product.name}}</strong><br>Grind: {{item.customer_note}}</figcaption>
</figure>
<span>{{product.sku}}</span>
<span>{{item.quantity}}</span>

View File

@ -2,9 +2,11 @@
{% block content %}
<article class="product">
<header class="object__header">
<h1>Create product</h1>
<section>
<form method="POST" action="{% url 'dashboard:product-create' %}">
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:product-create' %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">

View File

@ -116,9 +116,10 @@ class OrderListView(LoginRequiredMixin, ListView):
def get_queryset(self):
query = self.request.GET.get('status')
if query:
if query == 'unfulfilled':
object_list = Order.objects.filter(
status=query
Q(status=OrderStatus.UNFULFILLED) |
Q(status=OrderStatus.PARTIALLY_FULFILLED)
).order_by(
'-created_at'
).select_related(
@ -279,7 +280,7 @@ class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = User
template_name = 'dashboard/customer_form.html'
context_object_name = 'customer'
success_message = '%(name)s saved.'
success_message = 'Customer saved.'
fields = (
'first_name',
'last_name',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -9,6 +9,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Add Your Required Allow Host
if DEBUG == False:
ALLOWED_HOSTS = ['ptcoffee-dev.windmillapps.org']
else:
ALLOWED_HOSTS = ['192.168.68.106', '127.0.0.1', 'localhost']
INTERNAL_IPS = [
'127.0.0.1',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -16,9 +16,9 @@ export function getCookie(name) {
const twentyYears = 20 * 365 * 24 * 60 * 60 * 1000
export function setCookie(name, value) {
export function setCookie(name, value, expiration=twentyYears) {
const body = [ name, value ].map(encodeURIComponent).join("=")
const expires = new Date(Date.now() + twentyYears).toUTCString()
const expires = new Date(Date.now() + expiration).toUTCString()
const cookie = `${body}; domain=; path=/; SameSite=Lax; expires=${expires}`
document.cookie = cookie
}

View File

@ -0,0 +1,37 @@
import { getCookie, setCookie } from "./cookie.js"
// Get the modal
const modal = document.querySelector(".modal-menu");
// Get the <span> element that closes the modal
const closeBtn = document.querySelector(".close-modal");
const oneDay = 1 * 24 * 60 * 60 * 1000
// When the user clicks on <span> (x), close the modal
closeBtn.addEventListener("click", event => {
modal.style.display = "none";
setCookie('newsletter-modal', 'true', oneDay)
})
const scrollFunction = () => {
let modalDismissed = getCookie('newsletter-modal')
console.log(modalDismissed)
if (modalDismissed != 'true') {
if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) {
modal.style.display = "block";
}
}
}
window.onscroll = () => {
scrollFunction();
};
// When the user clicks anywhere outside of the modal, close it
window.addEventListener("click", event => {
if (event.target == modal) {
modal.style.display = "none";
}
setCookie('newsletter-modal', 'true', oneDay)
});

View File

@ -1,6 +1,6 @@
import { getCookie } from "./cookie.js"
let form = document.querySelector('form.order__form')
let form = document.querySelector('form')
// Render the PayPal button into #paypal-button-container
paypal.Buttons({

View File

@ -0,0 +1,12 @@
const gallery = document.querySelectorAll('.gallery__thumbnail')
const productImage = document.querySelector('.product__image')
let currentImg = document.querySelector('.gallery__thumbnail--focus')
gallery.forEach(image => {
image.addEventListener('mouseover', event => {
currentImg.classList.remove('gallery__thumbnail--focus')
event.target.classList.add('gallery__thumbnail--focus')
currentImg = event.target
productImage.src = currentImg.src
})
})

View File

@ -1,9 +1,11 @@
:root {
--fg-color: #333;
--fg-color: #34201A;
--bg-color: #f5f5f5;
--bg-alt-color: #eee5d3;
--gray-color: #9d9d9d;
--yellow-color: #f8a911;
--yellow-alt-color: #ffce6f;
--yellow-dark-color: #b27606;
--default-border: 2px solid var(--gray-color);
}
@ -13,17 +15,22 @@ html {
}
body {
background: var(--bg-color);
background-color: var(--bg-color);
font-family: 'Inter', sans-serif;
font-weight: 400;
max-width: 1024px;
padding: 1rem;
padding: 0;
margin: 0 auto;
line-height: 1.75;
color: var(--fg-color);
}
/* ==========================================================================
Typography
========================================================================== */
p {
margin-top: 0;
margin-bottom: 1rem;
}
@ -33,9 +40,9 @@ a {
h1, h2, h3, h4, h5 {
margin: 0;
font-family: 'Eczar', serif;
font-weight: 700;
line-height: 1.3;
margin-bottom: 1rem;
}
h1 {
@ -59,61 +66,61 @@ h5 {
font-size: 1.2rem;
}
small, .text_small {
small, .text-small {
font-size: 0.833rem;
}
.text-center {
text-align: center;
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
/* ==========================================================================
Tables
========================================================================== */
table {
border-collapse: collapse;
border-spacing: 0;
font: inherit;
margin: 0;
max-width: 100%;
box-sizing: border-box;
width: 100%;
margin-bottom: 1rem;
border: 1px solid var(--gray-color);
}
input {
outline: 0;
}
label {
display: block;
td,
th {
font: inherit;
text-align: left;
font-weight: 700;
font-size: 0.85em;
padding: 2px 10px;
border-bottom: 1px solid;
border-color: var(--gray-color);
}
select {
text-align: left;
border: var(--default-border);
padding: 0.5em;
outline: 0;
th {
font-weight: bold;
}
table a {
white-space: normal;
}
/* ==========================================================================
Forms
========================================================================== */
input[type=text],
input[type=email],
input[type=number],
input[type=date],
input[type=password],
select[multiple=multiple],
select,
textarea {
display: block;
text-align: left;
color: var(--fg-color);
border: var(--default-border);
padding: 0.5rem;
outline: 0;
box-sizing: border-box;
}
input:focus,
@ -122,7 +129,7 @@ textarea:focus {
}
select[multiple=multiple] {
height: 125px;
height: 8rem;
}
@ -142,7 +149,6 @@ input[type=checkbox] + label {
textarea {
width: 100%;
height: 6.25rem;
line-height: 1.45;
}
::-webkit-input-placeholder {
@ -156,36 +162,36 @@ textarea {
}
.action-button,
button,
input[type=submit],
button {
.action-button {
font-family: inherit;
font-weight: bold;
font-size: 1rem;
color: var(--fg-color);
font-size: 1.5rem;
text-decoration: none;
text-transform: lowercase;
font-variant: small-caps;
white-space: nowrap;
color: var(--fg-color);
background-color: var(--yellow-color);
padding: 0.25rem 1rem;
border-radius: 0.2rem;
border: none;
cursor: pointer;
}
button,
input[type=submit]:hover,
.action-button:hover {
background-color: var(--yellow-alt-color);
}
.action-button--large {
font-size: 2rem;
}
form input,
form select {
width: 100%;
box-sizing: border-box;
}
@ -196,249 +202,481 @@ figure {
}
img {
box-sizing: border-box;
}
.product__item {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 0 2rem;
}
.product__list-item button {
grid-column: 1/3;
align-self: end;
}
.product__image {
/*object-fit: cover;*/
max-width: 100%;
}
.product__form {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 1rem;
/* ==========================================================================
Base Layout
========================================================================== */
.site__header > nav,
main > article,
footer > section {
max-width: 1024px;
padding: 1rem;
margin: 0 auto;
}
.product__form input[type=submit] {
grid-column: 2;
justify-self: end;
/* ==========================================================================
Modal
========================================================================== */
.modal-menu {
display: none;
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
.site__logo {
text-decoration: none;
/* Modal Content/Box */
.modal-menu__content {
background-color: var(--bg-color);
margin: 25vh auto;
padding: 20px;
border: var(--default-border);
max-width: 40rem;
}
.site__header div,
.site__header nav {
.modal-menu__header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
nav a {
text-transform: lowercase;
font-variant: small-caps;
/* The Close Button */
.close-modal {
color: #aaa;
font-size: 2rem;
line-height: 0;
font-weight: bold;
text-decoration: none;
}
nav {
margin-bottom: 4rem;
}
.site__copyright {
text-align: center;
}
.keep_calm {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
.keep_calm__img {
max-width: 250px;
}
.site__cart {
display: flex;
align-items: center;
background-color: var(--yellow-color);
padding: 0.25rem 1rem;
text-decoration: none;
border-radius: 0.2rem;
.close-modal:hover,
.close-modal:focus {
color: var(--fg-color);
text-decoration: none;
cursor: pointer;
}
/* ==========================================================================
Site
========================================================================== */
.site__ft-stamp {
max-width: 6rem;
}
/* Site Header
========================================================================== */
.site__header {
background-color: var(--bg-alt-color);
}
.site__logo > img {
height: 4rem;
}
/* Site Nav
========================================================================== */
.site__nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.site__logo,
.nav__main,
.nav__account {
margin-right: 1rem;
}
@media screen and (max-width: 900px) {
.site__logo,
.nav__main,
.nav__account {
margin-right: 0;
}
.site__nav {
display: grid;
grid-template-columns: 2fr 0.5fr 0.5fr;
gap: 1rem;
}
.site__logo {
grid-column: 1;
}
.nav__main {
grid-column: 1/4;
grid-row: 2;
justify-content: space-between;
}
.nav__account {
grid-column: 2;
grid-row: 1;
}
.site__cart {
grid-column: 3;
grid-row: 1;
}
}
@media screen and (max-width: 400px) {
.site__nav {
grid-template-columns: repeat(2, 1fr);
}
.site__logo {
grid-column: span 2;
justify-self: center;
}
.nav__main {
grid-column: 1/3;
grid-row: 3;
}
.nav__account {
grid-column: 1;
grid-row: 2;
}
.site__cart {
grid-column: 2;
grid-row: 2;
}
}
.nav__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
.nav__menu {
position: relative;
}
.nav__link {
text-transform: lowercase;
font-variant: small-caps;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
}
.nav__list li:not(:last-child) {
margin-right: 1rem;
}
.nav__list.menu {
flex-direction: column;
}
.nav__dropdown {
list-style: none;
margin: 0;
padding: 0;
background-color: var(--bg-color);
border: var(--default-border);
padding: 0.5rem;
position: absolute;
top: 100%;
right: 0;
z-index: 1000;
display: none;
}
@media screen and (max-width: 400px) {
.nav__dropdown {
left: 0;
right: unset;
}
}
.nav__menu:hover .nav__dropdown {
display: flex;
flex-direction: column;
}
/* Site Cart
========================================================================== */
.site__cart {
display: flex;
justify-content: center;
align-items: center;
justify-self: end;
font-family: inherit;
font-weight: bold;
text-decoration: none;
text-transform: lowercase;
font-variant: small-caps;
color: var(--fg-color);
background-color: var(--yellow-color);
padding: 0.25rem 1rem;
border-radius: 0.2rem;
cursor: pointer;
}
.cart__count {
font-size: 1rem;
}
.cart__icon {
width: 2rem;
height: 2rem;
}
.cart__length {
font-size: 1.5rem;
font-weight: bold;
font-family: 'Inter', sans-serif;
/* ==========================================================================
Articles
========================================================================== */
article > header {
margin-bottom: 1rem;
}
.cart__item {
/* Products
========================================================================== */
.product__list {
display: grid;
grid-template-columns: 1fr 5fr;
gap: 1rem;
padding: 2rem 0;
border-bottom: 0.05rem solid var(--gray-color);
grid-template-columns: repeat(2, 1fr);
gap: 4rem;
}
.cart__total_price {
.product__item {
text-decoration: none;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.product__item h3 {
text-decoration: underline;
}
@media screen and (max-width: 900px) {
.product__list {
grid-template-columns: 1fr;
gap: 6rem;
}
.product__figure {
grid-row: span 2;
}
}
@media screen and (max-width: 600px) {
.product__item {
grid-template-columns: 1fr;
}
.product__figure {
grid-row: 1;
justify-self: center;
}
}
/* Product Detail
========================================================================== */
.product__detail {
display: grid;
grid-template-columns: 0.25fr 2fr 1fr;
gap: 1rem;
}
.gallery__thumbnail {
border: var(--default-border);
border-color: var(--bg-color);
cursor: pointer;
object-fit: cover;
aspect-ratio: 1/1;
}
.gallery__image {
text-align: center;
}
.gallery__thumbnail--focus {
border-color: var(--yellow-color);
}
.product__form input,
.product__form select {
width: 100%;
}
@media screen and (max-width: 700px) {
.product__detail {
grid-template-columns: 0.25fr 2fr;
}
.product__info {
grid-column: span 2;
}
}
@media screen and (max-width: 700px) {
.product__detail {
grid-template-columns: 1fr;
grid-template-rows: 4rem 1fr auto;
}
.product__gallery {
display: flex;
}
.gallery__thumbnail {
max-height: 4rem;
}
.gallery__thumbnail:not(:last-child) {
margin-right: 0.5rem;
}
.product__image {
max-height: 16rem;
}
.product__info {
grid-column: 1;
}
}
/* Shopping Cart
========================================================================== */
.cart__list {
margin-bottom: 2rem;
}
.cart__item {
padding: 1rem 0;
border-bottom: var(--default-border);
display: grid;
grid-template-columns: 1fr 3fr 1fr;
gap: 1rem;
}
.cart__table-wrapper {
display: flex;
justify-content: flex-end;
}
.cart__summary {
text-align: right;
}
.cart__totals {
width: unset;
font-size: 1.5rem;
}
.cart__total {
.cart__totals th,
.cart__totals td {
border: none;
}
.cart__totals th:first-child,
.cart__totals td:first-child {
text-align: right;
}
.cart__proceed {
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
}
.coupon__form {
max-width: 16rem;
align-self: flex-end;
.cart__proceed > .action-button {
font-size: 1.75rem;
}
.item__figure img {
vertical-align: middle;
.item__image {
max-height: 12rem;
}
.item__price {
justify-self: end;
}
.item__form p,
.coupon__form p {
display: flex;
align-items: center;
}
.coupon__form p {
justify-content: flex-end;
}
.item__form input[type=number] {
max-width: 6rem;
}
.coupon__form input[type=text] {
max-width: 8rem;
}
:is(.item__form, .coupon__form) label,
:is(.item__form, .coupon__form) input:not(:last-child) {
margin-right: 0.5rem;
}
@media screen and (max-width: 500px) {
.cart__item {
grid-template-columns: 2fr 1fr;
}
.cart__proceed {
flex-direction: column;
}
.item__info {
grid-column: span 2;
grid-row: 2;
}
.item__price {
grid-column: 2;
grid-row: 1;
}
}
.product__list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8rem;
/* Checkout / Shipping Address
========================================================================== */
.checkout__address-form input,
.checkout__address-form select {
display: block;
width: 100%;
max-width: 24rem;
}
.product__list-item {
text-decoration: none;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
.checkout__address {
margin-bottom: 2rem;
}
.product__list-item form {
grid-column: 1/3;
}
.product__list-item figure {
margin: 0;
padding: 0;
#paypal-button-container {
max-width: 24rem;
margin-left: auto;
}
.order__shipping {
}
.shipping__details {
margin-bottom: 3rem;
max-width: 32rem;
}
.shipping__details input[type=submit] {
font-size: 1.25rem;
}
.order__total {
margin: 3rem 0;
text-align: right;
}
/* ==========================================================================
Footer
========================================================================== */
footer {
margin: 4rem 0 0;
box-sizing: border-box;
border-top: var(--default-border);
padding: 2rem 0;
}
.object__header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 1rem;
}
.object__list,
.object__panel {
background-color: white;
border-radius: 0.5rem;
margin-bottom: 2rem;
}
.object__item {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1rem;
padding: 1rem;
border-bottom: 0.05rem solid var(--gray-color);
text-decoration: none;
align-items: center;
}
.panel__item:last-child,
.object__item:last-child {
border-bottom: unset;
border-radius: 0 0 0.5rem 0.5rem;
}
.object__item:hover {
padding: 1rem 0;
background-color: var(--bg-alt-color);
}
.panel__header {
font-weight: bold;
background-color: var(--bg-alt-color);
border-radius: 0.5rem 0.5rem 0 0;
}
.panel__item {
padding: 1rem;
border-bottom: 0.05rem solid var(--gray-color);
text-decoration: none;
}
.product__detail {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
.user__emails {
margin-bottom: 4rem;
}
._form_1 {
margin: 0;
}
._form_1 div form {
margin: 1rem 0 !important;
padding: 0 !important;
footer > section {
text-align: center;
}

349
src/static/styles/normalize.css vendored Normal file
View File

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -23,18 +23,15 @@ class Cart:
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
def add(self, product, quantity=1, roast='', update_quantity=False):
def add(self, product, quantity=1, grind='', update_quantity=False):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'quantity': 0,
'roast': roast,
'grind': grind,
'price': str(product.price)
}
elif product_id in self.cart:
self.cart[product_id].update({
'roast': roast,
})
if update_quantity:
self.cart[product_id]['quantity'] = quantity
else:
@ -71,7 +68,7 @@ class Cart:
def clear(self):
del self.session[settings.CART_SESSION_ID]
del self.coupon_code
del self.session['coupon_code']
self.session.modified = True
def build_order_params(self):
@ -97,7 +94,7 @@ class Cart:
bulk_list = [OrderLine(
order=order,
product=item['product'],
customer_note=item['roast'],
customer_note=item['grind'],
unit_price=item['price'],
quantity=item['quantity'],
tax_rate=2,

View File

@ -19,7 +19,7 @@ class AddToCartForm(forms.Form):
AEROPRESS = 'AeroPress'
PERCOLATOR = 'Percolator'
OTHER = 'Other'
ROAST_CHOICES = [
GRIND_CHOICES = [
(WHOLE, 'Whole Beans'),
(ESPRESSO, 'Espresso'),
(CONE_DRIP, 'Cone Drip'),
@ -31,11 +31,34 @@ class AddToCartForm(forms.Form):
(OTHER, 'Other (enter below)')
]
ONE_TIME = 'One-time purchase'
SUBSCRIBE = 'Subscribe'
PURCHASE_TYPE_CHOICES = [
(ONE_TIME, 'One-time purchase'),
(SUBSCRIBE, 'Subscribe and save 10%'),
grind = forms.ChoiceField(choices=GRIND_CHOICES)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
class UpdateCartItemForm(forms.Form):
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
update = forms.BooleanField(required=False, initial=True, widget=forms.HiddenInput)
class AddToSubscriptionForm(forms.Form):
WHOLE = 'Whole Beans'
ESPRESSO = 'Espresso'
CONE_DRIP = 'Cone Drip'
BASKET_DRIP = 'Basket Drip'
FRENCH_PRESS = 'French Press'
STOVETOP_ESPRESSO = 'Stovetop Espresso (Moka Pot)'
AEROPRESS = 'AeroPress'
PERCOLATOR = 'Percolator'
OTHER = 'Other'
GRIND_CHOICES = [
(WHOLE, 'Whole Beans'),
(ESPRESSO, 'Espresso'),
(CONE_DRIP, 'Cone Drip'),
(BASKET_DRIP, 'Basket Drip'),
(FRENCH_PRESS, 'French Press'),
(STOVETOP_ESPRESSO, 'Stovetop Espresso (Moka Pot)'),
(AEROPRESS, 'AeroPress'),
(PERCOLATOR, 'Percolator'),
(OTHER, 'Other (enter below)')
]
SEVEN_DAYS = 7
@ -48,10 +71,8 @@ class AddToCartForm(forms.Form):
]
quantity = forms.IntegerField(min_value=1, initial=1)
roast = forms.ChoiceField(choices=ROAST_CHOICES)
purchase_type = forms.ChoiceField(choices=PURCHASE_TYPE_CHOICES, initial=ONE_TIME)
grind = forms.ChoiceField(choices=GRIND_CHOICES)
schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
class AddressForm(forms.Form):
@ -83,7 +104,7 @@ class OrderCreateForm(forms.ModelForm):
}
class CouponApplyForm(forms.Form):
code = forms.CharField(label='Enter coupon')
code = forms.CharField(label='Coupon code')
class ContactForm(forms.Form):
GOOGLE = 'Google Search'

View File

@ -3,10 +3,14 @@
{% block content %}
<article>
<header>
<h1>About PT Coffee</h1>
</header>
<section>
<figure>
<img src="{% static 'images/coffee_banner.jpeg' %}" alt="">
</figure>
</section>
<section>
<h2>We love coffee!</h2>
<p><strong>If youve found Port Townsend Coffee Roasting Co., you probably love coffee so much that you seek out the best tasting, Certified Fair Trade Organic coffees available.</strong></p>
@ -27,7 +31,7 @@
<p>We roast our coffee much more slowly than most roasters, especially coffee roasters in the Pacific Northwest. The slower roasting process allows for greater bean development. The bean is evenly roasted right to the center and our process mutes some of the acidic compounds, which smooths out the flavor.</p>
<p>Port Townsend Coffee is known for roasts that are darker than others available in the Northwest. Due to our roasting process, which emphasizes patience with the beans as well as air flow adjustments, the darker roasts are smooth and syrupy. Dark roasts from other companies can taste bitter, or slightly burnt due to the size and speed of the roast.</p>
<p>We have been buying beans from the same brokers for many years. They understand the flavor profiles we prefer and seek to accommodate our needs.</p>
<div class="column__layout">
<div>
<h5>Fair Trade and Organic</h5>
<p>We pay a steep premium for these beans, which are typically from smaller farms that are organized into co-ops. These farms take pride in their coffees, as the farmers make a living wage and their families are able to live in a healthier, more secure environment than farmers who grow a conventional coffee crop. The quality of our coffee is consistent, in part due to the quality of organic and fair trade beans.</p>
<h5>Freshness and Storage</h5>

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block content %}
<article>
<header>
<p><a href="{% url 'storefront:customer-detail' user.pk %}">&larr; Back</a></p>
<h1>Update Address</h1>
</header>
<section>
<form method="post" action="{% url 'storefront:address-update' customer.pk address.pk %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Save changes">
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -2,51 +2,73 @@
{% block content %}
<article>
<h2>Shopping Cart</h2>
<section>
<header>
<h1>Shopping Cart</h1>
</header>
<section class="cart__list">
{% for item in cart %}
<div class="cart__item">
{% with product=item.product %}
<figure class="item__figure">
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
<img class="item__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
</figure>
<div class="item__details">
<p>{{product.name}}<br> ${{item.price}}</p>
<div class="item__info">
<h4>{{product.name}}</h4>
<p><strong>Grind</strong>: {{item.grind}}</p>
<form class="item__form" action="{% url 'storefront:cart-update' product.pk %}" method="POST">
{% csrf_token %}
<p>
{{ item.update_quantity_form }}
<input type="submit" value="Update">
</p>
</form>
<p>
<a href="{% url 'storefront:cart-remove' product.pk %}">Remove from cart</a>
</p>
<form class="product__form" action="{% url 'storefront:cart-add' product.pk %}" method="POST">
{% csrf_token %}
{{ item.update_quantity_form }}
<input type="submit" value="Update">
</form>
</div>
<div class="item__price">
<p><strong>${{item.price}}</strong></p>
</div>
{% endwith %}
</div>
{% empty %}
<div>
<p>No items in cart yet.</p>
</div>
{% endfor %}
</section>
<section>
<div class="cart__total">
<form action="{% url 'storefront:coupon-apply' %}" method="post" class="coupon__form">
<section class="cart__summary">
<h4>Cart Totals</h4>
<div>
<form class="coupon__form" action="{% url 'storefront:coupon-apply' %}" method="post">
{% csrf_token %}
{{ coupon_apply_form.as_p }}
<p>
{{ coupon_apply_form }}
<input type="submit" value="Apply" class="action-button">
</p>
</form>
</div>
<p class="cart__total_price">
<span class="">Subtotal: ${{cart.get_total_price|floatformat:"2"}}</span><br>
<div class="cart__table-wrapper">
<table class="cart__totals">
<tr>
<td>Subtotal</td>
<td>${{cart.get_total_price|floatformat:"2"}}</td>
</tr>
{% if cart.coupon %}
<span class="">Coupon: {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</span><br>
<tr>
<td>Coupon</td>
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
</tr>
{% endif %}
<span class="cart__total_price">Cart total: <strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></span>
</p>
<p class="cart__total">
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a>&emsp;or&emsp;<a class="action-button action-button--large" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>
<tr>
<th>Total</th>
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
</tr>
</table>
</div>
<p class="cart__proceed">
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a>{% if cart|length > 0 %}&emsp;or&emsp;<a class="action-button" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>{% endif %}
</p>
</div>
</section>
</article>
{% endblock %}

View File

@ -3,16 +3,18 @@
{% block content %}
<article>
<h2>Checkout</h2>
<section class="order__shipping">
<div class="shipping__details">
<header>
<h1>Checkout</h1>
</header>
<section>
<h3>Shipping Address</h3>
<form action="" method="POST" class="address__form">
<form class="checkout__address-form" action="" method="POST">
{% csrf_token %}
{{form.as_p}}
<p>
<input type="submit" value="Continue to Payment">
</p>
</form>
</div>
</section>
</article>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<article>
<h2>Checkout</h2>
<section class="order__details">
<div class="order__shipping">
<h3>Shipping Method</h3>
<form action="post" class="address__form">
{{form.as_p}}
<input type="submit" value="Review order">
</form>
</div>
</section>
</article>
{% endblock %}

View File

@ -2,12 +2,16 @@
{% block content %}
<article>
<h1>Contact PT Coffee</h1>
<header>
<h1>Contact us</h1>
</header>
<section>
<form action="{% url 'storefront:contact' %}" method="post">
{% csrf_token %}
{{form.as_p}}
<p>
<input type="submit" value="Send message">
</p>
</form>
</section>
</article>

View File

@ -3,20 +3,18 @@
{% block content %}
<article>
<header class="object__header">
<header>
<h1>{{customer.get_full_name}}</h1>
<a href="{% url 'storefront:customer-update' customer.pk %}" class="action-button">Edit profile</a>
</header>
<section class="object__panel">
<div class="object__item panel__header">
<section>
<h4>Info</h4>
</div>
<div class="panel__item">
<div>
<strong>Email address</strong><br>
{{customer.email}}<br>
<a href="{% url 'account_email' %}">Manage</a>
</div>
<div class="panel__item">
<div>
<strong>Default shipping address</strong><br>
{% with shipping_address=customer.default_shipping_address %}
<address>
@ -28,11 +26,10 @@
{% endif %}
{{shipping_address.city}}, {{shipping_address.state}}, {{shipping_address.postal_code}}
</address>
<a href="address-update">Edit</a>
{% endwith %}
</div>
<div class="panel__item">
<strong>Other addresses</strong><br>
<div>
<p><strong>All addresses</strong></p>
{% for address in customer.addresses.all %}
<p>
<address>
@ -44,7 +41,7 @@
{% endif %}
{{address.city}}, {{address.state}}, {{address.postal_code}}
</address>
<a href="address-update">Edit</a>
<a href="{% url 'storefront:address-update' customer.pk address.pk %}">Edit</a>
</p>
{% empty %}
<p>No other addresses.</p>
@ -52,21 +49,29 @@
</div>
</section>
{% with order_list=customer.orders.all %}
<section class="object__list">
<div class="object__item panel__header" href="order-detail">
<span>Order #</span>
<span>Date</span>
<span>Total</span>
</div>
<h3>Your orders</h3>
<section>
<table>
<thead>
<tr>
<th>Order #</th>
<th>Date</th>
<th colspan="2">Total</th>
</tr>
</thead>
<tbody>
{% for order in order_list %}
<a class="object__item" href="">
<span>#{{order.pk}}</span>
<span>{{order.created_at|date:"D, M j Y"}}</span>
<span>${{order.total_net_amount}}</span>
</a>
<tr>
<td>#{{order.pk}}</td>
<td>{{order.created_at|date:"M j, Y"}}</td>
<td>${{order.total_net_amount}}</td>
<td><a href="{% url 'storefront:order-detail' customer.pk order.pk %}">See details &rarr;</a></td>
</tr>
{% empty %}
<span class="object__item">No orders</span>
{% endfor %}
</tbody>
</table>
</section>
{% endwith %}
</article>

View File

@ -1,14 +1,17 @@
{% extends "base.html" %}
{% block content %}
<article class="product">
<article>
<header>
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">&larr; Back</a></p>
<h1>Update your profile</h1>
</header>
<section>
<form method="POST" action="{% url 'storefront:customer-update' customer.pk %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'storefront:customer-detail' customer.pk %}">cancel</a>
<p>
<input type="submit" value="Save changes">
</p>
</form>
</section>

View File

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<article>
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">&larr; Back</a></p>
<header>
<h1>Order #{{order.pk}}</h1>
<h3>Placed on {{order.created_at|date:"M j, Y"}}</h3>
</header>
<section>
<table>
<thead>
<tr>
<th colspan="2">Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in order.lines.all %}
<tr>
{% with product=item.product %}
<td>
<img src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
</td>
<td><strong>{{product.name}}</strong></td>
<td>{{item.quantity}}</td>
<td>${{product.price}}</td>
<td>${{item.get_total}}</td>
{% endwith %}
</tr>
{% empty %}
<tr>
<td colspan="5">No items in order</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section>
<h4>Payment</h4>
<table>
<tr>
<td>Subtotal</td>
<td>${{order.total_net_amount}}</td>
</tr>
{% if order.coupon %}
<tr>
<td>Discount</td>
<td>{{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</td>
</tr>
{% endif %}
<tr>
<th>Total</th>
<td><strong>${{order.get_total_price_after_discount}}</strong></td>
</tr>
</table>
</section>
</article>
{% endblock content %}

View File

@ -2,16 +2,17 @@
{% load static %}
{% block head %}
<script defer src="https://www.paypal.com/sdk/js?client-id=AVELl5svEDTPo3FRNViyjWnqODfFsAHbKKdm5BlwpKCwZCVxTraFF9Ax3N1xlkoEWSpOj7AI_T9xYMRu&currency=USD"></script>
<script defer src="https://www.paypal.com/sdk/js?client-id={{PAYPAL_CLIENT_ID}}&currency=USD"></script>
<script type="module" defer src="{% static 'scripts/payment.js' %}"></script>
{% endblock %}
{% block content %}
<article>
<h2>Checkout</h2>
<section class="order__details">
<div class="order__shipping">
<h3>Shipping Address</h3>
<header>
<h1>Checkout</h1>
</header>
<section class="checkout__address">
<h3>Shipping address</h3>
<address>
{{shipping_address.first_name}}
{{shipping_address.last_name}}<br>
@ -21,40 +22,53 @@
{% endif %}
{{shipping_address.city}}, {{shipping_address.state}}, {{shipping_address.postal_code}}
</address>
<a class="action-button" href="{% url 'storefront:checkout-address' %}">edit</a>
</div>
<div class="order__list">
<h3>Cart Summary</h3>
<a class="action-button" href="{% url 'storefront:checkout-address' %}">Change</a>
</section>
<section class="cart__list">
<h3>Review items</h3>
{% for item in cart %}
<div class="cart__item">
{% with product=item.product %}
<figure class="item__figure">
{{item.quantity}} x
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
<figure>
<img src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
</figure>
<div class="item__details">
<p>{{product.name}}<br> ${{item.price}}</p>
<p><strong>Grind options</strong>: {{item.customer_note}}</p>
<div>
<h4>{{product.name}}</h4>
<p><strong>Grind options</strong>: {{item.grind}}</p>
<p><strong>Quantity</strong>: {{item.quantity}}</p>
</div>
<div class="item__price">
<p><strong>${{item.price}}</strong></p>
</div>
{% endwith %}
</div>
{% endfor %}
</div>
<div class="order__total">
<form action="" method="POST" class="order__form">
</section>
<section class="cart__summary">
<h3>Order summary</h3>
<form action="" method="POST">
{% csrf_token %}
{{form.as_p}}
{# <input type="submit" value="Place order"> #}
</form>
<p class="cart__total_price">
<span class="">Subtotal: ${{cart.get_total_price|floatformat:"2"}}</span><br>
<div class="cart__table-wrapper">
<table class="cart__totals">
<tr>
<td>Subtotal</td>
<td>${{cart.get_total_price|floatformat:"2"}}</td>
</tr>
{% if cart.coupon %}
<span class="">Coupon: {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</span><br>
<tr>
<td>Coupon</td>
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
</tr>
{% endif %}
<span class="cart__total_price">Cart total: <strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></span>
</p>
<div id="paypal-button-container"></div>
<tr>
<th>Total</th>
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
</tr>
</table>
</div>
<div id="paypal-button-container"></div>
</section>
</article>
{% endblock %}

View File

@ -2,24 +2,30 @@
{% load static %}
{% block head %}
<script defer src="{% static 'scripts/product_form.js' %}"></script>
<script defer src="{% static 'scripts/product_gallery.js' %}"></script>
{% endblock %}
{% block content %}
<article class="product__item">
<figure class="product__figure">
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
<article class="product__detail">
<section class="product__gallery">
{% for photo in product.productphoto_set.all %}
<img class="gallery__thumbnail {% if forloop.first %}gallery__thumbnail--focus{% endif %}" src="{{ photo.image.url }}" alt="{{ photo }}">
{% endfor %}
</section>
<figure class="gallery__image">
<img class="product__image" src="{{ product.productphoto_set.first.image.url }}" alt="{{ product.name }}">
</figure>
<section>
<section class="product__info">
<h1>{{product.name}}</h1>
<p>{{product.description}}</p>
<p class="site__ft-stamp"><img class="fair_trade--small" src="{% static 'images/fair_trade_stamp.png' %}" alt=""></p>
<p>$<strong>{{product.price}}</strong></p>
<p>{{product.weight.oz}} oz</p>
<form method="post" action="{% url 'storefront:cart-add' product.pk %}">
<form class="product__form" method="post" action="{% url 'storefront:cart-add' product.pk %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Add to cart" class="action-button">
<input type="submit" value="Add to cart">
</p>
</form>
</section>

View File

@ -2,18 +2,20 @@
{% block content %}
<article>
<header>
<h1>Coffee</h1>
</header>
<section class="product__list">
{% for product in product_list %}
<a href="{% url 'storefront:product-detail' product.pk %}" class="product__list-item">
<a class="product__item" href="{% url 'storefront:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
</figure>
<div>
<h3>{{ product.name }}</h3><br>
<h3>$<strong>{{product.price}}</strong></h3>
<p>{{product.description}}</p>
<h3>{{ product.name }}</h3>
<p>{{product.description|truncatewords:20}}</p>
<p>$<strong>{{product.price}}</strong> | {{product.weight.oz}} oz</p>
</div>
<button class="action-button">View product</button>
</a>
{% endfor %}
</section>

View File

@ -12,6 +12,7 @@ urlpatterns = [
path('cart/', views.CartView.as_view(), name='cart-detail'),
path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'),
path('cart/<int:pk>/update/', views.CartUpdateProductView.as_view(), name='cart-update'),
path('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'),
path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'),
@ -28,5 +29,9 @@ urlpatterns = [
path('', views.CustomerDetailView.as_view(), name='customer-detail'),
path('update/', views.CustomerUpdateView.as_view(), name='customer-update'),
# path('delete/', views.CustomerDeleteView.as_view(), name='customer-delete'),
path('orders/<int:order_pk>/', views.OrderDetailView.as_view(), name='order-detail'),
path('addresses/<int:address_pk>/update/', views.AddressUpdateView.as_view(), name='address-update'),
])),
]

View File

@ -23,10 +23,11 @@ from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from accounts.models import User, Address
from accounts.utils import get_or_create_customer
from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm
from .forms import AddToCartForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
from .cart import Cart
from .payments import CaptureOrder
@ -39,11 +40,9 @@ class CartView(TemplateView):
context = super().get_context_data(**kwargs)
cart = Cart(self.request)
for item in cart:
item['update_quantity_form'] = AddToCartForm(
item['update_quantity_form'] = UpdateCartItemForm(
initial={
'quantity': item['quantity'],
'roast': item['roast'],
'update': True
}
)
context['cart'] = cart
@ -63,7 +62,30 @@ class CartAddProductView(SingleObjectMixin, FormView):
if form.is_valid():
cart.add(
product=self.get_object(),
roast=form.cleaned_data['roast'],
grind=form.cleaned_data['grind'],
quantity=form.cleaned_data['quantity']
)
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
return super().form_valid(form)
class CartUpdateProductView(SingleObjectMixin, FormView):
model = Product
form_class = UpdateCartItemForm
def get_success_url(self):
return reverse('storefront:cart-detail')
def post(self, request, *args, **kwargs):
cart = Cart(request)
form = self.get_form()
if form.is_valid():
cart.add(
product=self.get_object(),
quantity=form.cleaned_data['quantity'],
update_quantity=form.cleaned_data['update']
)
@ -181,6 +203,7 @@ class OrderCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['shipping_address'] = self.request.session.get('shipping_address')
context['PAYPAL_CLIENT_ID'] = settings.PAYPAL_CLIENT_ID
return context
def form_valid(self, form):
@ -227,25 +250,46 @@ class PaymentCanceledView(TemplateView):
template_name = 'storefront/payment_canceled.html'
class CustomerDetailView(DetailView):
class CustomerDetailView(LoginRequiredMixin, DetailView):
model = User
template_name = 'storefront/customer_detail.html'
context_object_name = 'customer'
class CustomerUpdateView(UpdateView):
class CustomerUpdateView(LoginRequiredMixin, UpdateView):
model = User
template_name = 'storefront/customer_form.html'
context_object_name = 'customer'
fields = (
'first_name',
'last_name',
'email',
'default_shipping_address'
)
form_class = CustomerUpdateForm
def get_success_url(self):
return reverse('storefront:customer-detail', kwargs={'pk': self.object.pk})
class OrderDetailView(LoginRequiredMixin, DetailView):
model = Order
template_name = 'storefront/order_detail.html'
pk_url_kwarg = 'order_pk'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context
class AddressUpdateView(LoginRequiredMixin, UpdateView):
model = Address
pk_url_kwarg = 'address_pk'
template_name = 'storefront/address_form.html'
form_class = AccountAddressForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context
def get_success_url(self):
return reverse('storefront:customer-detail',kwargs={'pk': self.kwargs['pk']})
class AboutView(TemplateView):
template_name = 'storefront/about.html'

View File

@ -6,6 +6,7 @@
{% block content %}
<article>
<p><a href="{% url 'storefront:customer-detail' user.pk %}">&larr; Back</a></p>
<section>
<h1>{% trans "E-mail Addresses" %}</h1>
</section>

View File

@ -14,73 +14,77 @@
<link href="https://fonts.googleapis.com/css2?family=Eczar:wght@400;700&family=Inter:wght@100;400;700&display=swap" rel="stylesheet">
{% compress css %}
<link rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
{% endcompress %}
<script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script>
<script type="module" defer src="{% static 'scripts/index.js' %}"></script>
{% block head %}
{% endblock %}
</head>
<body>
<header class="site__header">
<div>
<h1><a class="site__logo" href="{% url 'storefront:product-list' %}">Port Townsend Coffee</a></h1>
{% if user.is_authenticated %}
<p>
Hi <a href="{% url 'storefront:customer-detail' user.pk %}">{{user.first_name}} {{user.last_name}}</a>!
</p>
{% if user.is_staff %}
<p><a href="{% url 'dashboard:home' %}">Dashboard</a></p>
{% endif %}
<p><a href="{% url 'account_logout' %}">Log Out</a></p>
{% else %}
<p>
<a href="{% url 'account_login' %}">Log In</a>&emsp;|&emsp;<a href="{% url 'account_signup' %}">Sign Up</a>
</p>
{% endif %}
<div class="modal-menu">
<div class="modal-menu__content">
<div class="modal-menu__header">
<h4>Sign up for our newsletter</h4>
<span class="close-modal">&times;</span>
</div>
<nav>
<a href="{% url 'storefront:product-list' %}">Shop</a>
{# <a href="">Wholesale</a> #}
<a href="">Subscribe</a>
<a href="">Cafe</a>
<a href="">Fair Trade</a>
<a href="{% url 'storefront:about' %}">About</a>
<a href="{% url 'storefront:contact' %}">Contact</a>
<div class="modal-menu__form">
<div class="_form_1"></div>
</div>
</div>
</div>
<header class="site__header">
<nav class="site__nav">
<a class="site__logo" href="{% url 'storefront:product-list' %}"><img src="{% static 'images/site_logo.svg' %}" alt="Port Townsend Roasting Co."></a>
<ul class="nav__list nav__main">
<li><a class="nav__link" href="{% url 'storefront:product-list' %}">Shop</a></li>
<li><a class="nav__link" href="">Cafe</a></li>
<li><a class="nav__link" href="">Fair trade</a></li>
<li><a class="nav__link" href="{% url 'storefront:about' %}">About</a></li>
<li><a class="nav__link" href="{% url 'storefront:contact' %}">Contact</a></li>
</ul>
{% if user.is_authenticated %}
<div class="nav__menu nav__account">
<a class="nav__link" href="{% url 'storefront:customer-detail' user.pk %}">Account ▼</a>
<ul class="nav__dropdown">
{% if user.is_staff %}
<li><a class="nav__link" href="{% url 'dashboard:home' %}">Dashboard</a></li>
{% endif %}
<li><a class="nav__link" href="{% url 'storefront:customer-detail' user.pk %}">Orders</a></li>
<li><a class="nav__link" href="{% url 'account_logout' %}">Logout</a></li>
</ul>
</div>
{% else %}
<ul class="nav__list nav__account">
<li><a class="nav__link" href="{% url 'account_login' %}">Login</a></li>
<li>/</li>
<li><a class="nav__link" href="{% url 'account_signup' %}">Signup</a></li>
</ul>
{% endif %}
<a class="site__cart" href="{% url 'storefront:cart-detail' %}">
<span class="cart__length">{{cart|length}}</span>
<span class="cart__count">{{cart|length}}</span>
<img class="cart__icon" src="{% static 'images/shopping_cart.svg' %}" alt="Shopping cart">
</a>
</nav>
</header>
<aside>
{% block aside %}
{% endblock %}
</aside>
<main>
{% block content %}
{% endblock content %}
</main>
<footer>
<div class="keep_calm">
<figure>
<img class="keep_calm__img" src="{% static 'images/keep_calm.jpg' %}" alt="Keep calm and drink coffee">
</figure>
<div>
<h4>Problem with your order?<br>Have a question?</h4>
<p>Please contact us, were happy to help you over the phone at <a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.</p>
<div class="_form_1"></div><script src="https://bltc999.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
</div>
</div>
<div class="site__copyright">
<section>
<p>
<small>Copyright © 2016-{% now "Y" %} Better Living Food Company Inc.</small><br>
<small>Fair Trade&nbsp;|&nbsp;Organic&nbsp;|&nbsp;Port Townsend, WA 98368</small><br><br>
<img src="{% static 'images/fair_trade_stamp.png' %}" alt="">
<img class="site__ft-stamp" src="{% static 'images/fair_trade_stamp.png' %}" alt="">
</p>
</div>
</section>
</footer>
<script src="https://ptcoffee.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
</body>
</html>