diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..bab29bf --- /dev/null +++ b/NOTES.md @@ -0,0 +1,23 @@ +# Notes for the user flow + +Product list + +| + +Product detail (add the item to cart) + +| + +Cart view (order detail view) (all the items in our cart) + +| + +Checkout (addresses) + +| + +Payment + +| + +Order confirmation diff --git a/cart/__init__.py b/cart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/admin.py b/cart/admin.py new file mode 100644 index 0000000..3c7f8ad --- /dev/null +++ b/cart/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin +from .models import ( + Product, OrderItem, Order, ColourVariation, + SizeVariation, Address, Payment, Category, StripePayment +) + + +class AddressAdmin(admin.ModelAdmin): + list_display = [ + 'address_line_1', + 'address_line_2', + 'city', + 'zip_code', + 'address_type', + ] + + +admin.site.register(Category) +admin.site.register(Address, AddressAdmin) +admin.site.register(ColourVariation) +admin.site.register(Product) +admin.site.register(OrderItem) +admin.site.register(Order) +admin.site.register(SizeVariation) +admin.site.register(Payment) +admin.site.register(StripePayment) diff --git a/cart/apps.py b/cart/apps.py new file mode 100644 index 0000000..7cc6ec1 --- /dev/null +++ b/cart/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CartConfig(AppConfig): + name = 'cart' diff --git a/cart/forms.py b/cart/forms.py new file mode 100644 index 0000000..14a96d0 --- /dev/null +++ b/cart/forms.py @@ -0,0 +1,107 @@ +from django.contrib.auth import get_user_model +from django import forms +from .models import ( + OrderItem, ColourVariation, Product, SizeVariation, + Address +) + +User = get_user_model() + + +class AddToCartForm(forms.ModelForm): + colour = forms.ModelChoiceField(queryset=ColourVariation.objects.none()) + size = forms.ModelChoiceField(queryset=SizeVariation.objects.none()) + quantity = forms.IntegerField(min_value=1) + + class Meta: + model = OrderItem + fields = ['quantity', 'colour', 'size'] + + def __init__(self, *args, **kwargs): + self.product_id = kwargs.pop('product_id') + product = Product.objects.get(id=self.product_id) + super().__init__(*args, **kwargs) + + self.fields['colour'].queryset = product.available_colours.all() + self.fields['size'].queryset = product.available_sizes.all() + + def clean(self): + product_id = self.product_id + product = Product.objects.get(id=self.product_id) + quantity = self.cleaned_data['quantity'] + if product.stock < quantity: + raise forms.ValidationError( + f"The maximum stock available is {product.stock}") + + +class AddressForm(forms.Form): + + shipping_address_line_1 = forms.CharField(required=False) + shipping_address_line_2 = forms.CharField(required=False) + shipping_zip_code = forms.CharField(required=False) + shipping_city = forms.CharField(required=False) + + billing_address_line_1 = forms.CharField(required=False) + billing_address_line_2 = forms.CharField(required=False) + billing_zip_code = forms.CharField(required=False) + billing_city = forms.CharField(required=False) + + selected_shipping_address = forms.ModelChoiceField( + Address.objects.none(), required=False + ) + selected_billing_address = forms.ModelChoiceField( + Address.objects.none(), required=False + ) + + def __init__(self, *args, **kwargs): + user_id = kwargs.pop('user_id') + super().__init__(*args, **kwargs) + + user = User.objects.get(id=user_id) + + shipping_address_qs = Address.objects.filter( + user=user, + address_type='S' + ) + billing_address_qs = Address.objects.filter( + user=user, + address_type='B' + ) + + self.fields['selected_shipping_address'].queryset = shipping_address_qs + self.fields['selected_billing_address'].queryset = billing_address_qs + + def clean(self): + data = self.cleaned_data + + selected_shipping_address = data.get('selected_shipping_address', None) + if selected_shipping_address is None: + if not data.get('shipping_address_line_1', None): + self.add_error("shipping_address_line_1", + "Please fill in this field") + if not data.get('shipping_address_line_2', None): + self.add_error("shipping_address_line_2", + "Please fill in this field") + if not data.get('shipping_zip_code', None): + self.add_error("shipping_zip_code", + "Please fill in this field") + if not data.get('shipping_city', None): + self.add_error("shipping_city", "Please fill in this field") + + selected_billing_address = data.get('selected_billing_address', None) + if selected_billing_address is None: + if not data.get('billing_address_line_1', None): + self.add_error("billing_address_line_1", + "Please fill in this field") + if not data.get('billing_address_line_2', None): + self.add_error("billing_address_line_2", + "Please fill in this field") + if not data.get('billing_zip_code', None): + self.add_error("billing_zip_code", + "Please fill in this field") + if not data.get('billing_city', None): + self.add_error("billing_city", "Please fill in this field") + + +class StripePaymentForm(forms.Form): + selectedCard = forms.CharField() diff --git a/cart/migrations/0001_initial.py b/cart/migrations/0001_initial.py new file mode 100644 index 0000000..ad2a014 --- /dev/null +++ b/cart/migrations/0001_initial.py @@ -0,0 +1,107 @@ +# Generated by Django 3.0.6 on 2020-05-19 12:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address_line_1', models.CharField(max_length=150)), + ('address_line_2', models.CharField(max_length=150)), + ('city', models.CharField(max_length=100)), + ('zip_code', models.CharField(max_length=20)), + ('address_type', models.CharField(choices=[('B', 'Billing'), ('S', 'Shipping')], max_length=1)), + ('default', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Addresses', + }, + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='ColourVariation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateTimeField(auto_now_add=True)), + ('ordered_date', models.DateTimeField(blank=True, null=True)), + ('ordered', models.BooleanField(default=False)), + ('billing_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='billing_address', to='cart.Address')), + ('shipping_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipping_address', to='cart.Address')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SizeVariation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=150)), + ('slug', models.SlugField(unique=True)), + ('image', models.ImageField(upload_to='product_images')), + ('description', models.TextField()), + ('price', models.IntegerField(default=0)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('active', models.BooleanField(default=False)), + ('available_colours', models.ManyToManyField(to='cart.ColourVariation')), + ('available_sizes', models.ManyToManyField(to='cart.SizeVariation')), + ('primary_category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='primary_products', to='cart.Category')), + ('secondary_categories', models.ManyToManyField(blank=True, to='cart.Category')), + ], + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_method', models.CharField(choices=[('PayPal', 'PayPal')], max_length=20)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('successful', models.BooleanField(default=False)), + ('amount', models.FloatField()), + ('raw_response', models.TextField()), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='cart.Order')), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('colour', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cart.ColourVariation')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='cart.Order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cart.Product')), + ('size', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cart.SizeVariation')), + ], + ), + ] diff --git a/cart/migrations/0002_auto_20200519_1208.py b/cart/migrations/0002_auto_20200519_1208.py new file mode 100644 index 0000000..811b876 --- /dev/null +++ b/cart/migrations/0002_auto_20200519_1208.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.6 on 2020-05-19 12:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + ] diff --git a/cart/migrations/0003_product_stock.py b/cart/migrations/0003_product_stock.py new file mode 100644 index 0000000..c95f606 --- /dev/null +++ b/cart/migrations/0003_product_stock.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-05-19 12:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0002_auto_20200519_1208'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='stock', + field=models.IntegerField(default=0), + ), + ] diff --git a/cart/migrations/0004_stripepayment.py b/cart/migrations/0004_stripepayment.py new file mode 100644 index 0000000..089bc3c --- /dev/null +++ b/cart/migrations/0004_stripepayment.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.6 on 2020-05-19 16:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0003_product_stock'), + ] + + operations = [ + migrations.CreateModel( + name='StripePayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_intent_id', models.CharField(max_length=100)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('successful', models.BooleanField(default=False)), + ('amount', models.FloatField()), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stripe_payments', to='cart.Order')), + ], + ), + ] diff --git a/cart/migrations/0005_auto_20200519_1650.py b/cart/migrations/0005_auto_20200519_1650.py new file mode 100644 index 0000000..ef7eee5 --- /dev/null +++ b/cart/migrations/0005_auto_20200519_1650.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-05-19 16:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0004_stripepayment'), + ] + + operations = [ + migrations.AlterField( + model_name='stripepayment', + name='amount', + field=models.FloatField(default=0), + ), + ] diff --git a/cart/migrations/0006_auto_20200529_1313.py b/cart/migrations/0006_auto_20200529_1313.py new file mode 100644 index 0000000..60f1ed5 --- /dev/null +++ b/cart/migrations/0006_auto_20200529_1313.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.6 on 2020-05-29 13:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0005_auto_20200519_1650'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='primary_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='primary_products', to='cart.Category'), + ), + ] diff --git a/cart/migrations/__init__.py b/cart/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/models.py b/cart/models.py new file mode 100644 index 0000000..80df47e --- /dev/null +++ b/cart/models.py @@ -0,0 +1,190 @@ +from django.contrib.auth import get_user_model +from django.db import models +from django.db.models.signals import pre_save +from django.shortcuts import reverse +from django.utils.text import slugify + +User = get_user_model() + + +class Category(models.Model): + name = models.CharField(max_length=100) + + class Meta: + verbose_name_plural = "Categories" + + def __str__(self): + return self.name + + +class Address(models.Model): + ADDRESS_CHOICES = ( + ('B', 'Billing'), + ('S', 'Shipping'), + ) + + user = models.ForeignKey(User, on_delete=models.CASCADE) + address_line_1 = models.CharField(max_length=150) + address_line_2 = models.CharField(max_length=150) + city = models.CharField(max_length=100) + zip_code = models.CharField(max_length=20) + address_type = models.CharField(max_length=1, choices=ADDRESS_CHOICES) + default = models.BooleanField(default=False) + + def __str__(self): + return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.zip_code}" + + class Meta: + verbose_name_plural = 'Addresses' + + +class ColourVariation(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + +class SizeVariation(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + +class Product(models.Model): + title = models.CharField(max_length=150) + slug = models.SlugField(unique=True) + image = models.ImageField(upload_to='product_images') + description = models.TextField() + price = models.IntegerField(default=0) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + active = models.BooleanField(default=False) + available_colours = models.ManyToManyField(ColourVariation) + available_sizes = models.ManyToManyField(SizeVariation) + primary_category = models.ForeignKey( + Category, related_name='primary_products', blank=True, null=True, on_delete=models.CASCADE) + secondary_categories = models.ManyToManyField(Category, blank=True) + stock = models.IntegerField(default=0) + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("cart:product-detail", kwargs={'slug': self.slug}) + + def get_update_url(self): + return reverse("staff:product-update", kwargs={'pk': self.pk}) + + def get_delete_url(self): + return reverse("staff:product-delete", kwargs={'pk': self.pk}) + + def get_price(self): + return "{:.2f}".format(self.price / 100) + + @property + def in_stock(self): + return self.stock > 0 + + +class OrderItem(models.Model): + order = models.ForeignKey( + "Order", related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + colour = models.ForeignKey(ColourVariation, on_delete=models.CASCADE) + size = models.ForeignKey(SizeVariation, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.quantity} x {self.product.title}" + + def get_raw_total_item_price(self): + return self.quantity * self.product.price + + def get_total_item_price(self): + price = self.get_raw_total_item_price() # 1000 + return "{:.2f}".format(price / 100) + + +class Order(models.Model): + user = models.ForeignKey( + User, blank=True, null=True, on_delete=models.CASCADE) + start_date = models.DateTimeField(auto_now_add=True) + ordered_date = models.DateTimeField(blank=True, null=True) + ordered = models.BooleanField(default=False) + + billing_address = models.ForeignKey( + Address, related_name='billing_address', blank=True, null=True, on_delete=models.SET_NULL) + shipping_address = models.ForeignKey( + Address, related_name='shipping_address', blank=True, null=True, on_delete=models.SET_NULL) + + def __str__(self): + return self.reference_number + + @property + def reference_number(self): + return f"ORDER-{self.pk}" + + def get_raw_subtotal(self): + total = 0 + for order_item in self.items.all(): + total += order_item.get_raw_total_item_price() + return total + + def get_subtotal(self): + subtotal = self.get_raw_subtotal() + return "{:.2f}".format(subtotal / 100) + + def get_raw_total(self): + subtotal = self.get_raw_subtotal() + # add tax, add delivery, subtract discounts + # total = subtotal - discounts + tax + delivery + return subtotal + + def get_total(self): + total = self.get_raw_total() + return "{:.2f}".format(total / 100) + + +class Payment(models.Model): + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name='payments') + payment_method = models.CharField(max_length=20, choices=( + ('PayPal', 'PayPal'), + )) + timestamp = models.DateTimeField(auto_now_add=True) + successful = models.BooleanField(default=False) + amount = models.FloatField() + raw_response = models.TextField() + + def __str__(self): + return self.reference_number + + @property + def reference_number(self): + return f"PAYMENT-{self.order}-{self.pk}" + + +class StripePayment(models.Model): + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name='stripe_payments') + payment_intent_id = models.CharField(max_length=100) + timestamp = models.DateTimeField(auto_now_add=True) + successful = models.BooleanField(default=False) + amount = models.FloatField(default=0) + + def __str__(self): + return self.reference_number + + @property + def reference_number(self): + return f"STRIPE-PAYMENT-{self.order}-{self.pk}" + + +def pre_save_product_receiver(sender, instance, *args, **kwargs): + if not instance.slug: + instance.slug = slugify(instance.title) + + +pre_save.connect(pre_save_product_receiver, sender=Product) diff --git a/cart/templatetags/cart_template_tags.py b/cart/templatetags/cart_template_tags.py new file mode 100644 index 0000000..9ac1832 --- /dev/null +++ b/cart/templatetags/cart_template_tags.py @@ -0,0 +1,11 @@ +from django import template +from cart.utils import get_or_set_order_session + +register = template.Library() + + +@register.filter +def cart_item_count(request): + order = get_or_set_order_session(request) + count = order.items.count() + return count diff --git a/cart/tests.py b/cart/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cart/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cart/urls.py b/cart/urls.py new file mode 100644 index 0000000..9d22bef --- /dev/null +++ b/cart/urls.py @@ -0,0 +1,24 @@ +from django.urls import path +from . import views + +app_name = 'cart' + +urlpatterns = [ + path('', views.CartView.as_view(), name='summary'), + path('shop/', views.ProductListView.as_view(), name='product-list'), + path('shop//', views.ProductDetailView.as_view(), name='product-detail'), + path('increase-quantity//', + views.IncreaseQuantityView.as_view(), name='increase-quantity'), + path('decrease-quantity//', + views.DecreaseQuantityView.as_view(), name='decrease-quantity'), + path('remove-from-cart//', + views.RemoveFromCartView.as_view(), name='remove-from-cart'), + path('checkout/', views.CheckoutView.as_view(), name='checkout'), + path('payment/', views.PaymentView.as_view(), name='payment'), + path('thank-you/', views.ThankYouView.as_view(), name='thank-you'), + path('confirm-order/', views.ConfirmOrderView.as_view(), name='confirm-order'), + path('orders//', views.OrderDetailView.as_view(), name='order-detail'), + path('payment/stripe/', views.StripePaymentView.as_view(), name='payment-stripe'), + path('webhooks/stripe/', views.stripe_webhook_view, name='stripe-webhook'), + +] diff --git a/cart/utils.py b/cart/utils.py new file mode 100644 index 0000000..67051ae --- /dev/null +++ b/cart/utils.py @@ -0,0 +1,23 @@ +from .models import Order + + +def get_or_set_order_session(request): + order_id = request.session.get('order_id', None) + + if order_id is None: + order = Order() + order.save() + request.session['order_id'] = order.id + + else: + try: + order = Order.objects.get(id=order_id, ordered=False) + except Order.DoesNotExist: + order = Order() + order.save() + request.session['order_id'] = order.id + + if request.user.is_authenticated and order.user is None: + order.user = request.user + order.save() + return order diff --git a/cart/views.py b/cart/views.py new file mode 100644 index 0000000..b98c8ea --- /dev/null +++ b/cart/views.py @@ -0,0 +1,330 @@ +from django.http import HttpResponse +import datetime +import json +import stripe +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.http import JsonResponse, HttpResponse +from django.shortcuts import get_object_or_404, reverse, redirect +from django.utils import timezone +from django.views import generic +from django.views.decorators.csrf import csrf_exempt +from .forms import AddToCartForm, AddressForm, StripePaymentForm +from .models import Product, OrderItem, Address, Payment, Order, Category, StripePayment +from .utils import get_or_set_order_session + +stripe.api_key = settings.STRIPE_SECRET_KEY + + +class ProductListView(generic.ListView): + template_name = 'cart/product_list.html' + + def get_queryset(self): + qs = Product.objects.all() + category = self.request.GET.get('category', None) + if category: + qs = qs.filter(Q(primary_category__name=category) | + Q(secondary_categories__name=category)).distinct() + return qs + + def get_context_data(self, **kwargs): + context = super(ProductListView, self).get_context_data(**kwargs) + context.update({ + "categories": Category.objects.values("name") + }) + return context + + +class ProductDetailView(generic.FormView): + template_name = 'cart/product_detail.html' + form_class = AddToCartForm + + def get_object(self): + return get_object_or_404(Product, slug=self.kwargs["slug"]) + + def get_success_url(self): + return reverse("cart:summary") + + def get_form_kwargs(self): + kwargs = super(ProductDetailView, self).get_form_kwargs() + kwargs["product_id"] = self.get_object().id + return kwargs + + def form_valid(self, form): + order = get_or_set_order_session(self.request) + product = self.get_object() + + item_filter = order.items.filter( + product=product, + colour=form.cleaned_data['colour'], + size=form.cleaned_data['size'], + ) + + if item_filter.exists(): + item = item_filter.first() + item.quantity += int(form.cleaned_data['quantity']) + item.save() + + else: + new_item = form.save(commit=False) + new_item.product = product + new_item.order = order + new_item.save() + + return super(ProductDetailView, self).form_valid(form) + + def get_context_data(self, **kwargs): + context = super(ProductDetailView, self).get_context_data(**kwargs) + context['product'] = self.get_object() + return context + + +class CartView(generic.TemplateView): + template_name = "cart/cart.html" + + def get_context_data(self, **kwargs): + context = super(CartView, self).get_context_data(**kwargs) + context["order"] = get_or_set_order_session(self.request) + return context + + +class IncreaseQuantityView(generic.View): + def get(self, request, *args, **kwargs): + order_item = get_object_or_404(OrderItem, id=kwargs['pk']) + order_item.quantity += 1 + order_item.save() + return redirect("cart:summary") + + +class DecreaseQuantityView(generic.View): + def get(self, request, *args, **kwargs): + order_item = get_object_or_404(OrderItem, id=kwargs['pk']) + if order_item.quantity <= 1: + order_item.delete() + else: + order_item.quantity -= 1 + order_item.save() + return redirect("cart:summary") + + +class RemoveFromCartView(generic.View): + def get(self, request, *args, **kwargs): + order_item = get_object_or_404(OrderItem, id=kwargs['pk']) + order_item.delete() + return redirect("cart:summary") + + +class CheckoutView(LoginRequiredMixin, generic.FormView): + template_name = 'cart/checkout.html' + form_class = AddressForm + + def get_success_url(self): + return reverse("cart:payment") + + def form_valid(self, form): + order = get_or_set_order_session(self.request) + selected_shipping_address = form.cleaned_data.get( + 'selected_shipping_address') + selected_billing_address = form.cleaned_data.get( + 'selected_billing_address') + + if selected_shipping_address: + order.shipping_address = selected_shipping_address + else: + address = Address.objects.create( + address_type='S', + user=self.request.user, + address_line_1=form.cleaned_data['shipping_address_line_1'], + address_line_2=form.cleaned_data['shipping_address_line_2'], + zip_code=form.cleaned_data['shipping_zip_code'], + city=form.cleaned_data['shipping_city'], + ) + order.shipping_address = address + + if selected_billing_address: + order.billing_address = selected_billing_address + else: + address = Address.objects.create( + address_type='B', + user=self.request.user, + address_line_1=form.cleaned_data['billing_address_line_1'], + address_line_2=form.cleaned_data['billing_address_line_2'], + zip_code=form.cleaned_data['billing_zip_code'], + city=form.cleaned_data['billing_city'], + ) + order.billing_address = address + + order.save() + messages.info( + self.request, "You have successfully added your addresses") + return super(CheckoutView, self).form_valid(form) + + def get_form_kwargs(self): + kwargs = super(CheckoutView, self).get_form_kwargs() + kwargs["user_id"] = self.request.user.id + return kwargs + + def get_context_data(self, **kwargs): + context = super(CheckoutView, self).get_context_data(**kwargs) + context["order"] = get_or_set_order_session(self.request) + return context + + +class PaymentView(LoginRequiredMixin, generic.TemplateView): + template_name = 'cart/payment.html' + + def get_context_data(self, **kwargs): + context = super(PaymentView, self).get_context_data(**kwargs) + context["PAYPAL_CLIENT_ID"] = settings.PAYPAL_CLIENT_ID + context['order'] = get_or_set_order_session(self.request) + context['CALLBACK_URL'] = self.request.build_absolute_uri( + reverse("cart:thank-you")) + return context + + +class StripePaymentView(LoginRequiredMixin, generic.FormView): + template_name = 'cart/stripe_payment.html' + form_class = StripePaymentForm + + def form_valid(self, form): + payment_method = form.cleaned_data["selectedCard"] + print(payment_method) + if payment_method != "newCard": + try: + order = get_or_set_order_session(self.request) + payment_intent = stripe.PaymentIntent.create( + amount=order.get_raw_total(), + currency='usd', + customer=self.request.user.customer.stripe_customer_id, + payment_method=payment_method, + off_session=True, + confirm=True, + ) + payment_record, created = StripePayment.objects.get_or_create( + order=order + ) + payment_record.payment_intent_id = payment_intent["id"] + payment_record.amount = order.get_total() + payment_record.save() + except stripe.error.CardError as e: + err = e.error + # Error code will be authentication_required if authentication is needed + print("Code is: %s" % err.code) + payment_intent_id = err.payment_intent['id'] + payment_intent = stripe.PaymentIntent.retrieve( + payment_intent_id) + messages.warning(self.request, "Code is: %s" % err.code) + return redirect("/") + + def get_context_data(self, **kwargs): + user = self.request.user + if not user.customer.stripe_customer_id: + stripe_customer = stripe.Customer.create(email=user.email) + user.customer.stripe_customer_id = stripe_customer["id"] + user.customer.save() + + order = get_or_set_order_session(self.request) + + payment_intent = stripe.PaymentIntent.create( + amount=order.get_raw_total(), + currency='usd', + customer=user.customer.stripe_customer_id, + ) + + payment_record, created = StripePayment.objects.get_or_create( + order=order + ) + payment_record.payment_intent_id = payment_intent["id"], + payment_record.amount = order.get_total() + payment_record.save() + + cards = stripe.PaymentMethod.list( + customer=user.customer.stripe_customer_id, + type="card", + ) + payment_methods = [] + for card in cards: + payment_methods.append({ + "last4": card["card"]["last4"], + "brand": card["card"]["brand"], + "exp_month": card["card"]["exp_month"], + "exp_year": card["card"]["exp_year"], + "pm_id": card["id"] + }) + + context = super(StripePaymentView, self).get_context_data(**kwargs) + context["STRIPE_PUBLIC_KEY"] = settings.STRIPE_PUBLIC_KEY + context["client_secret"] = payment_intent["client_secret"] + context["payment_methods"] = payment_methods + return context + + +class ConfirmOrderView(generic.View): + def post(self, request, *args, **kwargs): + order = get_or_set_order_session(request) + body = json.loads(request.body) + payment = Payment.objects.create( + order=order, + successful=True, + raw_response=json.dumps(body), + amount=float(body["purchase_units"][0]["amount"]["value"]), + payment_method='PayPal' + ) + order.ordered = True + order.ordered_date = datetime.date.today() + order.save() + return JsonResponse({"data": "Success"}) + + +class ThankYouView(generic.TemplateView): + template_name = 'cart/thanks.html' + + +class OrderDetailView(LoginRequiredMixin, generic.DetailView): + template_name = 'order.html' + queryset = Order.objects.all() + context_object_name = 'order' + + +# If you are testing your webhook locally with the Stripe CLI you +# can find the endpoint's secret by running `stripe listen` +# Otherwise, find your endpoint's secret in your webhook settings in the Developer Dashboard + +@csrf_exempt +def stripe_webhook_view(request): + endpoint_secret = settings.STRIPE_WEBHOOK_SECRET + payload = request.body + sig_header = request.META['HTTP_STRIPE_SIGNATURE'] + event = None + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + print(event) + except ValueError as e: + # Invalid payload + return HttpResponse(status=400) + except stripe.error.SignatureVerificationError as e: + # Invalid signature + return HttpResponse(status=400) + + # Handle the event + if event.type == 'payment_intent.succeeded': + payment_intent = event.data.object # contains a stripe.PaymentIntent + stripe_payment = StripePayment.objects.get( + payment_intent_id=payment_intent["id"], + ) + stripe_payment.successful = True + stripe_payment.save() + order = stripe_payment.order + order.ordered = True + order.ordered_date = timezone.now() + order.save() + else: + # Unexpected event type + return HttpResponse(status=400) + + return HttpResponse(status=200) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..b903bf0 --- /dev/null +++ b/core/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Customer + + +admin.site.register(Customer) diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..1acf327 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,13 @@ +from django import forms + + +class ContactForm(forms.Form): + name = forms.CharField(max_length=100, widget=forms.TextInput(attrs={ + 'placeholder': "Your name" + })) + email = forms.EmailField(widget=forms.TextInput(attrs={ + 'placeholder': "Your e-mail" + })) + message = forms.CharField(widget=forms.Textarea(attrs={ + 'placeholder': 'Your message' + })) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..a895ea5 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.6 on 2020-05-19 15:21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0002_customer_stripe_customer_id.py b/core/migrations/0002_customer_stripe_customer_id.py new file mode 100644 index 0000000..e59da85 --- /dev/null +++ b/core/migrations/0002_customer_stripe_customer_id.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.6 on 2020-05-19 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='stripe_customer_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..af9ccb6 --- /dev/null +++ b/core/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.db.models.signals import post_save +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class Customer(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + stripe_customer_id = models.CharField(max_length=100) + + def __str__(self): + return self.user.email + + +def post_save_user_receiver(sender, instance, created, **kwargs): + if created: + Customer.objects.create(user=instance) + + +post_save.connect(post_save_user_receiver, sender=User) diff --git a/core/tests.py b/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/core/views.py b/core/views.py new file mode 100644 index 0000000..e6da1db --- /dev/null +++ b/core/views.py @@ -0,0 +1,53 @@ +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.mail import send_mail +from django.shortcuts import reverse +from django.views import generic +from cart.models import Order +from .forms import ContactForm + + +class ProfileView(LoginRequiredMixin, generic.TemplateView): + template_name = 'profile.html' + + def get_context_data(self, **kwargs): + context = super(ProfileView, self).get_context_data(**kwargs) + context.update({ + "orders": Order.objects.filter(user=self.request.user, ordered=True) + }) + return context + + +class HomeView(generic.TemplateView): + template_name = 'index.html' + + +class ContactView(generic.FormView): + form_class = ContactForm + template_name = 'contact.html' + + def get_success_url(self): + return reverse("contact") + + def form_valid(self, form): + messages.info( + self.request, "Thanks for getting in touch. We have received your message.") + name = form.cleaned_data.get('name') + email = form.cleaned_data.get('email') + message = form.cleaned_data.get('message') + + full_message = f""" + Received message below from {name}, {email} + ________________________ + + + {message} + """ + send_mail( + subject="Received contact form submission", + message=full_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[settings.NOTIFY_EMAIL] + ) + return super(ContactView, self).form_valid(form) diff --git a/ecom/.template.env b/ecom/.template.env index 2e8b31f..ae5c036 100644 --- a/ecom/.template.env +++ b/ecom/.template.env @@ -1,2 +1,9 @@ DEBUG= -SECRET_KEY= \ No newline at end of file +SECRET_KEY= +DEFAULT_FROM_EMAIL= +NOTIFY_EMAIL= +PAYPAL_SANDBOX_CLIENT_ID= +PAYPAL_SANDBOX_SECRET_KEY= +STRIPE_PUBLISH_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= \ No newline at end of file diff --git a/ecom/settings.py b/ecom/settings.py index fa1d2b8..527a45e 100644 --- a/ecom/settings.py +++ b/ecom/settings.py @@ -20,8 +20,21 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + 'django.contrib.sites', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'crispy_forms', + + 'cart', + 'core', + 'staff' ] +DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL') +NOTIFY_EMAIL = env('NOTIFY_EMAIL') + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -74,6 +87,18 @@ }, ] +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) +ACCOUNT_AUTHENTICATION_METHOD = 'email' +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_EMAIL_VERIFICATION = 'mandatory' +LOGIN_REDIRECT_URL = '/' +SITE_ID = 1 +CRISPY_TEMPLATE_PACK = 'bootstrap4' + LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True @@ -86,6 +111,13 @@ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, "media") +PAYPAL_CLIENT_ID = env('PAYPAL_SANDBOX_CLIENT_ID') +PAYPAL_SECRET_KEY = env('PAYPAL_SANDBOX_SECRET_KEY') + +STRIPE_PUBLIC_KEY = env("STRIPE_PUBLIC_KEY") +STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY") +STRIPE_WEBHOOK_SECRET = env("STRIPE_WEBHOOK_SECRET") + if DEBUG is False: SESSION_COOKIE_SECURE = True SECURE_BROWSER_XSS_FILTER = True @@ -109,3 +141,6 @@ 'PORT': '' } } + + PAYPAL_CLIENT_ID = env('PAYPAL_LIVE_CLIENT_ID') + PAYPAL_SECRET_KEY = env('PAYPAL_LIVE_SECRET_KEY') diff --git a/ecom/urls.py b/ecom/urls.py index a3c3eca..de78fa7 100644 --- a/ecom/urls.py +++ b/ecom/urls.py @@ -1,12 +1,22 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import path, include + +from core import views urlpatterns = [ path('admin/', admin.site.urls), + path('accounts/', include('allauth.urls')), + path('', views.HomeView.as_view(), name='home'), + path('contact/', views.ContactView.as_view(), name='contact'), + path('cart/', include('cart.urls', namespace='cart')), + path('staff/', include('staff.urls', namespace='staff')), + path('profile/', views.ProfileView.as_view(), name='profile') ] if settings.DEBUG: - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, + document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/requirements.txt b/requirements.txt index 3b99618..88c1b30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,20 @@ asgiref==3.2.7 -Django==3.0.5 +autopep8==1.5.1 +certifi==2020.4.5.1 +chardet==3.0.4 +defusedxml==0.6.0 +Django==3.0.6 +django-allauth==0.41.0 +django-crispy-forms==1.9.0 +django-environ==0.4.5 +idna==2.9 +oauthlib==3.1.0 +Pillow==7.1.0 psycopg2-binary==2.8.4 +pycodestyle==2.5.0 +python3-openid==3.1.0 pytz==2019.3 +requests==2.23.0 +requests-oauthlib==1.3.0 sqlparse==0.3.1 +urllib3==1.25.8 diff --git a/staff/__init__.py b/staff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/staff/admin.py b/staff/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/staff/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/staff/apps.py b/staff/apps.py new file mode 100644 index 0000000..fa420ce --- /dev/null +++ b/staff/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class StaffConfig(AppConfig): + name = 'staff' diff --git a/staff/forms.py b/staff/forms.py new file mode 100644 index 0000000..db14bae --- /dev/null +++ b/staff/forms.py @@ -0,0 +1,16 @@ +from django import forms + +from cart.models import Product + + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = [ + 'title', + 'image', + 'description', + 'price', + 'available_colours', + 'available_sizes', + ] diff --git a/staff/mixins.py b/staff/mixins.py new file mode 100644 index 0000000..6b2527c --- /dev/null +++ b/staff/mixins.py @@ -0,0 +1,8 @@ +from django.shortcuts import redirect + + +class StaffUserMixin(object): + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: + return redirect("home") + return super(StaffUserMixin, self).dispatch(request, *args, **kwargs) diff --git a/staff/models.py b/staff/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/staff/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/staff/tests.py b/staff/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/staff/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/staff/urls.py b/staff/urls.py new file mode 100644 index 0000000..20b3ee2 --- /dev/null +++ b/staff/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from . import views + +app_name = 'staff' + +urlpatterns = [ + path('', views.StaffView.as_view(), name='staff'), + path('create/', views.ProductCreateView.as_view(), name='product-create'), + path('products/', views.ProductListView.as_view(), name='product-list'), + path('products//update/', + views.ProductUpdateView.as_view(), name='product-update'), + path('products//delete/', + views.ProductDeleteView.as_view(), name='product-delete'), +] diff --git a/staff/views.py b/staff/views.py new file mode 100644 index 0000000..cf49992 --- /dev/null +++ b/staff/views.py @@ -0,0 +1,53 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import reverse +from django.views import generic +from cart.models import Order, Product +from .forms import ProductForm +from .mixins import StaffUserMixin + + +class StaffView(LoginRequiredMixin, StaffUserMixin, generic.ListView): + template_name = 'staff/staff.html' + queryset = Order.objects.filter(ordered=True).order_by('-ordered_date') + paginate_by = 20 + context_object_name = 'orders' + + +class ProductListView(LoginRequiredMixin, StaffUserMixin, generic.ListView): + template_name = 'staff/product_list.html' + queryset = Product.objects.all() + paginate_by = 20 + context_object_name = 'products' + + +class ProductCreateView(LoginRequiredMixin, StaffUserMixin, generic.CreateView): + template_name = 'staff/product_create.html' + form_class = ProductForm + + def get_success_url(self): + return reverse("staff:product-list") + + def form_valid(self, form): + form.save() + return super(ProductCreateView, self).form_valid(form) + + +class ProductUpdateView(LoginRequiredMixin, StaffUserMixin, generic.UpdateView): + template_name = 'staff/product_create.html' + form_class = ProductForm + queryset = Product.objects.all() + + def get_success_url(self): + return reverse("staff:product-list") + + def form_valid(self, form): + form.save() + return super(ProductUpdateView, self).form_valid(form) + + +class ProductDeleteView(LoginRequiredMixin, StaffUserMixin, generic.DeleteView): + template_name = 'staff/product_delete.html' + queryset = Product.objects.all() + + def get_success_url(self): + return reverse("staff:product-list") diff --git a/templates/account/account_inactive.html b/templates/account/account_inactive.html new file mode 100644 index 0000000..3347f4f --- /dev/null +++ b/templates/account/account_inactive.html @@ -0,0 +1,11 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Account Inactive" %}{% endblock %} + +{% block content %} +

{% trans "Account Inactive" %}

+ +

{% trans "This account is inactive." %}

+{% endblock %} diff --git a/templates/account/base.html b/templates/account/base.html new file mode 100644 index 0000000..94d9808 --- /dev/null +++ b/templates/account/base.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/templates/account/email.html b/templates/account/email.html new file mode 100644 index 0000000..9a9ed1f --- /dev/null +++ b/templates/account/email.html @@ -0,0 +1,73 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %} + +{% block content %} +

{% trans "E-mail Addresses" %}

+{% if user.emailaddress_set.all %} +

{% trans 'The following e-mail addresses are associated with your account:' %}

+ + + +{% else %} +

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

+ +{% endif %} + + +

{% trans "Add E-mail Address" %}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% endblock %} + + +{% block extra_body %} + +{% endblock %} diff --git a/templates/account/email/email_confirmation_message.txt b/templates/account/email/email_confirmation_message.txt new file mode 100644 index 0000000..2d856f7 --- /dev/null +++ b/templates/account/email/email_confirmation_message.txt @@ -0,0 +1,9 @@ +{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! + +You're receiving this e-mail because user {{ user_display }} has given yours as an e-mail address to connect their account. + +To confirm this is correct, go to {{ activate_url }} +{% endblocktrans %} +{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you from {{ site_name }}! +{{ site_domain }}{% endblocktrans %} +{% endautoescape %} diff --git a/templates/account/email/email_confirmation_signup_message.txt b/templates/account/email/email_confirmation_signup_message.txt new file mode 100644 index 0000000..9996f7e --- /dev/null +++ b/templates/account/email/email_confirmation_signup_message.txt @@ -0,0 +1 @@ +{% include "account/email/email_confirmation_message.txt" %} diff --git a/templates/account/email/email_confirmation_signup_subject.txt b/templates/account/email/email_confirmation_signup_subject.txt new file mode 100644 index 0000000..4c85ebb --- /dev/null +++ b/templates/account/email/email_confirmation_signup_subject.txt @@ -0,0 +1 @@ +{% include "account/email/email_confirmation_subject.txt" %} diff --git a/templates/account/email/email_confirmation_subject.txt b/templates/account/email/email_confirmation_subject.txt new file mode 100644 index 0000000..b0a876f --- /dev/null +++ b/templates/account/email/email_confirmation_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Please Confirm Your E-mail Address{% endblocktrans %} +{% endautoescape %} diff --git a/templates/account/email/password_reset_key_message.txt b/templates/account/email/password_reset_key_message.txt new file mode 100644 index 0000000..d74dfa8 --- /dev/null +++ b/templates/account/email/password_reset_key_message.txt @@ -0,0 +1,12 @@ +{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! + +You're receiving this e-mail because you or someone else has requested a password for your user account. +It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %} + +{{ password_reset_url }} + +{% if username %}{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %} + +{% endif %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using {{ site_name }}! +{{ site_domain }}{% endblocktrans %} +{% endautoescape %} diff --git a/templates/account/email/password_reset_key_subject.txt b/templates/account/email/password_reset_key_subject.txt new file mode 100644 index 0000000..6840c40 --- /dev/null +++ b/templates/account/email/password_reset_key_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Password Reset E-mail{% endblocktrans %} +{% endautoescape %} diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html new file mode 100644 index 0000000..ac0891b --- /dev/null +++ b/templates/account/email_confirm.html @@ -0,0 +1,31 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} + + +{% block content %} +

{% trans "Confirm E-mail Address" %}

+ +{% if confirmation %} + +{% user_display confirmation.email_address.user as user_display %} + +

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

+ +
+{% csrf_token %} + +
+ +{% else %} + +{% url 'account_email' as email_url %} + +

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

+ +{% endif %} + +{% endblock %} diff --git a/templates/account/login.html b/templates/account/login.html new file mode 100644 index 0000000..0f84707 --- /dev/null +++ b/templates/account/login.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} +{% load i18n %} +{% load account %} + +{% block content %} + +
+
+
+
+

Sign in

+
+
+
+ {% csrf_token %} + {% if redirect_field_value %} + + {% endif %} + {{ form|crispy }} + +
+ +
+
+
+
+ +{% endblock content %} diff --git a/templates/account/logout.html b/templates/account/logout.html new file mode 100644 index 0000000..2549a90 --- /dev/null +++ b/templates/account/logout.html @@ -0,0 +1,21 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Out" %}{% endblock %} + +{% block content %} +

{% trans "Sign Out" %}

+ +

{% trans 'Are you sure you want to sign out?' %}

+ +
+ {% csrf_token %} + {% if redirect_field_value %} + + {% endif %} + +
+ + +{% endblock %} diff --git a/templates/account/messages/cannot_delete_primary_email.txt b/templates/account/messages/cannot_delete_primary_email.txt new file mode 100644 index 0000000..de55571 --- /dev/null +++ b/templates/account/messages/cannot_delete_primary_email.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}You cannot remove your primary e-mail address ({{email}}).{% endblocktrans %} diff --git a/templates/account/messages/email_confirmation_sent.txt b/templates/account/messages/email_confirmation_sent.txt new file mode 100644 index 0000000..7a526f8 --- /dev/null +++ b/templates/account/messages/email_confirmation_sent.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Confirmation e-mail sent to {{email}}.{% endblocktrans %} diff --git a/templates/account/messages/email_confirmed.txt b/templates/account/messages/email_confirmed.txt new file mode 100644 index 0000000..3427a4d --- /dev/null +++ b/templates/account/messages/email_confirmed.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}You have confirmed {{email}}.{% endblocktrans %} diff --git a/templates/account/messages/email_deleted.txt b/templates/account/messages/email_deleted.txt new file mode 100644 index 0000000..5cf7cf9 --- /dev/null +++ b/templates/account/messages/email_deleted.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Removed e-mail address {{email}}.{% endblocktrans %} diff --git a/templates/account/messages/logged_in.txt b/templates/account/messages/logged_in.txt new file mode 100644 index 0000000..f49248a --- /dev/null +++ b/templates/account/messages/logged_in.txt @@ -0,0 +1,4 @@ +{% load account %} +{% load i18n %} +{% user_display user as name %} +{% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %} diff --git a/templates/account/messages/logged_out.txt b/templates/account/messages/logged_out.txt new file mode 100644 index 0000000..2cd4627 --- /dev/null +++ b/templates/account/messages/logged_out.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}You have signed out.{% endblocktrans %} diff --git a/templates/account/messages/password_changed.txt b/templates/account/messages/password_changed.txt new file mode 100644 index 0000000..bd5801c --- /dev/null +++ b/templates/account/messages/password_changed.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Password successfully changed.{% endblocktrans %} diff --git a/templates/account/messages/password_set.txt b/templates/account/messages/password_set.txt new file mode 100644 index 0000000..9d224ee --- /dev/null +++ b/templates/account/messages/password_set.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Password successfully set.{% endblocktrans %} diff --git a/templates/account/messages/primary_email_set.txt b/templates/account/messages/primary_email_set.txt new file mode 100644 index 0000000..b6a70dd --- /dev/null +++ b/templates/account/messages/primary_email_set.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Primary e-mail address set.{% endblocktrans %} diff --git a/templates/account/messages/unverified_primary_email.txt b/templates/account/messages/unverified_primary_email.txt new file mode 100644 index 0000000..9c9d0d8 --- /dev/null +++ b/templates/account/messages/unverified_primary_email.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Your primary e-mail address must be verified.{% endblocktrans %} diff --git a/templates/account/password_change.html b/templates/account/password_change.html new file mode 100644 index 0000000..b536579 --- /dev/null +++ b/templates/account/password_change.html @@ -0,0 +1,15 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +

{% trans "Change Password" %}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/templates/account/password_reset.html b/templates/account/password_reset.html new file mode 100644 index 0000000..de23d9e --- /dev/null +++ b/templates/account/password_reset.html @@ -0,0 +1,24 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Password Reset" %}{% endblock %} + +{% block content %} + +

{% trans "Password Reset" %}

+ {% if user.is_authenticated %} + {% include "account/snippets/already_logged_in.html" %} + {% endif %} + +

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

+{% endblock %} diff --git a/templates/account/password_reset_done.html b/templates/account/password_reset_done.html new file mode 100644 index 0000000..e90504f --- /dev/null +++ b/templates/account/password_reset_done.html @@ -0,0 +1,16 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Password Reset" %}{% endblock %} + +{% block content %} +

{% trans "Password Reset" %}

+ + {% if user.is_authenticated %} + {% include "account/snippets/already_logged_in.html" %} + {% endif %} + +

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

+{% endblock %} diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..16f27e9 --- /dev/null +++ b/templates/account/password_reset_from_key.html @@ -0,0 +1,23 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

+ + {% if token_fail %} + {% url 'account_reset_password' as passwd_reset_url %} +

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

+ {% else %} + {% if form %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% else %} +

{% trans 'Your password is now changed.' %}

+ {% endif %} + {% endif %} +{% endblock %} diff --git a/templates/account/password_reset_from_key_done.html b/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..85641c2 --- /dev/null +++ b/templates/account/password_reset_from_key_done.html @@ -0,0 +1,9 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +

{% trans "Change Password" %}

+

{% trans 'Your password is now changed.' %}

+{% endblock %} diff --git a/templates/account/password_set.html b/templates/account/password_set.html new file mode 100644 index 0000000..f561572 --- /dev/null +++ b/templates/account/password_set.html @@ -0,0 +1,15 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Set Password" %}{% endblock %} + +{% block content %} +

{% trans "Set Password" %}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/templates/account/signup.html b/templates/account/signup.html new file mode 100644 index 0000000..1fb1477 --- /dev/null +++ b/templates/account/signup.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} +{% load i18n %} +{% load account %} + +{% block content %} + +
+
+
+
+

Sign in

+
+
+
+ {% csrf_token %} + {% if redirect_field_value %} + + {% endif %} + {{ form|crispy }} + +
+ +
+
+
+
+ +{% endblock content %} diff --git a/templates/account/signup_closed.html b/templates/account/signup_closed.html new file mode 100644 index 0000000..bc83950 --- /dev/null +++ b/templates/account/signup_closed.html @@ -0,0 +1,11 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} + +{% block content %} +

{% trans "Sign Up Closed" %}

+ +

{% trans "We are sorry, but the sign up is currently closed." %}

+{% endblock %} diff --git a/templates/account/snippets/already_logged_in.html b/templates/account/snippets/already_logged_in.html new file mode 100644 index 0000000..00799f0 --- /dev/null +++ b/templates/account/snippets/already_logged_in.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% load account %} + +{% user_display user as user_display %} +

{% trans "Note" %}: {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}

diff --git a/templates/account/verification_sent.html b/templates/account/verification_sent.html new file mode 100644 index 0000000..5f71331 --- /dev/null +++ b/templates/account/verification_sent.html @@ -0,0 +1,12 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} + +{% block content %} +

{% trans "Verify Your E-mail Address" %}

+ +

{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

+ +{% endblock %} diff --git a/templates/account/verified_email_required.html b/templates/account/verified_email_required.html new file mode 100644 index 0000000..8115c48 --- /dev/null +++ b/templates/account/verified_email_required.html @@ -0,0 +1,23 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} + +{% block content %} +

{% trans "Verify Your E-mail Address" %}

+ +{% url 'account_email' as email_url %} + +

{% blocktrans %}This part of the site requires us to verify that +you are who you claim to be. For this purpose, we require that you +verify ownership of your e-mail address. {% endblocktrans %}

+ +

{% blocktrans %}We have sent an e-mail to you for +verification. Please click on the link inside this e-mail. Please +contact us if you do not receive it within a few minutes.{% endblocktrans %}

+ +

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

+ + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 846ceb1..7cdfa40 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,9 +1,12 @@ {% load static %} +{% load cart_template_tags %} Django Ecom {% block head_title %}{% endblock %} + {% block extra_head %} + {% endblock %} @@ -58,16 +61,16 @@
  • {% if request.user.is_authenticated %} - + {% else %} - {% endif %}
  • - + - 0 + {{ request|cart_item_count }}
  • diff --git a/templates/cart/cart.html b/templates/cart/cart.html new file mode 100644 index 0000000..c2ff6b1 --- /dev/null +++ b/templates/cart/cart.html @@ -0,0 +1,139 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    + Home + / + Cart +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + {% for item in order.items.all %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    ImageProductPriceQuantityTotalActions
    + + +

    {{ item.product.title }}

    + Size: {{ item.size.name }} + Colour: {{ item.colour.name }} +
    ${{ item.product.get_price }} +
    + + + +
    +
    + ${{ item.get_total_item_price }} + + X +
    + There are no items in your cart. + Continue shopping +
    +
    +
    +
    + + {% if order.items.count > 0 %} +
    +
    + +
    + +
    +
    +
    +
    +
    +

    Cart Totals

    +
    +
    + +
    +
    + Subtotal +
    +
    + ${{ order.get_subtotal }} +
    +
    + +
    +
    + Total +
    +
    + ${{ order.get_total }} +
    +
    + + + +
    +
    +
    + +
    + {% endif %} + +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/cart/checkout.html b/templates/cart/checkout.html new file mode 100644 index 0000000..3b70ce4 --- /dev/null +++ b/templates/cart/checkout.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + +
    +
    +
    +
    + Home/ + Cart/ + Checkout +
    +
    +
    +
    + + +
    +
    + {% if not request.user.is_authenticated %} +
    +
    + +
    +
    90% complete
    +
    +
    +
    + {% else %} + +
    + {% csrf_token %} +
    +
    +

    Billing Details

    +
    + {{ form|crispy }} +
    + +
    +
    +
    +
    +
    +

    Your Order

    +
    +
    + + + + + + + + + {% for item in order.items.all %} + + + + + {% endfor %} + + + + + + + + + +
    ProductTotal
    + {{ item.size.name }}, {{ item.colour.name }} {{ item.product.title }} + x {{ item.quantity }} + + ${{ item.get_total_item_price }} +
    Subtotal${{ order.get_subtotal }}
    Order Total + + ${{ order.get_total }} + +
    +
    +
    +
    +
    +
    +
    + + {% endif %} +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/cart/payment.html b/templates/cart/payment.html new file mode 100644 index 0000000..c1f3c91 --- /dev/null +++ b/templates/cart/payment.html @@ -0,0 +1,158 @@ +{% extends "base.html" %} + +{% block content %} + + + +
    +
    +
    + + + +
    + +
    + +
    +

    Order total: ${{ order.get_total }}

    +

    Select a payment method

    +
    + Stripe +
    +
    +
    +
    +
    +
    +{% endblock content %} + + +{% block scripts %} + + + + + +{% endblock scripts %} \ No newline at end of file diff --git a/templates/cart/product_detail.html b/templates/cart/product_detail.html new file mode 100644 index 0000000..ae2240b --- /dev/null +++ b/templates/cart/product_detail.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} +{% block content %} + +
    +
    +
    +
    + Home + / + {{ product.title }} +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    {{ product.title }}

    +

    {{ product.description }}

    +

    Available stock: {{ product.stock }} units

    + {% if product.in_stock %} +
    + {% csrf_token %} + {{ form|crispy }} + +
    + {% else %} +

    This item is out of stock

    + {% endif %} +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/cart/product_list.html b/templates/cart/product_list.html new file mode 100644 index 0000000..64760c9 --- /dev/null +++ b/templates/cart/product_list.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    + Home + / + Shop +
    +
    +
    +
    + +
    +
    +
    +
    +
    + {% for product in object_list %} +
    +
    + +
    + +
    + +

    {{ product.title }}

    +
    +

    {{ product.description }}

    +
    +
    + {% endfor %} +
    +
    +
    +
    +

    Categories

    + +
    +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/cart/stripe_payment.html b/templates/cart/stripe_payment.html new file mode 100644 index 0000000..d36bdaa --- /dev/null +++ b/templates/cart/stripe_payment.html @@ -0,0 +1,163 @@ +{% extends "base.html" %} + +{% block extra_head %} + + + + +{% endblock extra_head %} + +{% block content %} + +
    +
    +
    + +
    + {% csrf_token %} +
    + +
    +
    +
    + +
    + + + +
    + + +
    +
    +
    +
    + + + +{% endblock content %} \ No newline at end of file diff --git a/templates/cart/thanks.html b/templates/cart/thanks.html new file mode 100644 index 0000000..c8b5053 --- /dev/null +++ b/templates/cart/thanks.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} + +

    Thanks

    + +{% endblock content %} \ No newline at end of file diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..3eacd11 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + +
    +
    +
    +
    + Home + / + Contact +
    +
    +
    +
    + +
    +
    +
    +
    +

    Get in touch

    +
    +
    +
    + {% csrf_token %} + {{ form|crispy }} + +
    +
    +
    +
    +
    + + +{% endblock content %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..cd356a5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +

    Pay with Paypal

    +

    Use server-side PayPal integration for handling payments.

    +
    +
    +
    + + Django icon + + +
    +

    Made with Django

    +

    Django makes it easy to quickly build awesome web apps.

    +
    +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html index 7de44ee..8be53d8 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -2,10 +2,10 @@
    diff --git a/templates/openid/base.html b/templates/openid/base.html new file mode 100644 index 0000000..671d403 --- /dev/null +++ b/templates/openid/base.html @@ -0,0 +1 @@ +{% extends "socialaccount/base.html" %} diff --git a/templates/openid/login.html b/templates/openid/login.html new file mode 100644 index 0000000..b27ee37 --- /dev/null +++ b/templates/openid/login.html @@ -0,0 +1,18 @@ +{% extends "openid/base.html" %} + +{% load i18n %} + +{% block head_title %}OpenID Sign In{% endblock %} + +{% block content %} + +

    {% trans 'OpenID Sign In' %}

    + + + + +{% endblock %} diff --git a/templates/order.html b/templates/order.html new file mode 100644 index 0000000..97f0267 --- /dev/null +++ b/templates/order.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    + Back to profile +
    +
    +
    + Order: #{{ order.reference_number }} +
    +
    Ordered on: {{ order.ordered_date }}
    +
    +
    + + + + + + + + + {% for item in order.items.all %} + + + + + {% endfor %} + + + + + + + + + +
    Product.Total
    {{ item.size.name }}, {{ item.colour.name }} of {{ item.product.title }} x + {{ item.quantity }}${{ item.get_total_item_price }}
    Subtotal${{ order.get_subtotal }}
    Total${{ order.get_total }}
    +
    +
    +
    +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..6cfbe73 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    +

    Your profile

    +
    +
    +
    + {{ request.user.email }} + + Logout +
    +
    +
    Your orders
    +
    + + + + + + + + + + + {% for order in orders %} + + + + + + + {% empty %} + + + + {% endfor %} + +
    Reference No.DateAmountPayment Status
    #{{ order.reference_number }}{{ order.ordered_date }}${{ order.get_total }}{% if order.ordered %}Paid{% else %}Not paid{% endif %}
    You haven't made any orders
    +
    +
    +
    +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/socialaccount/authentication_error.html b/templates/socialaccount/authentication_error.html new file mode 100644 index 0000000..0300295 --- /dev/null +++ b/templates/socialaccount/authentication_error.html @@ -0,0 +1,11 @@ +{% extends "socialaccount/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} + +{% block content %} +

    {% trans "Social Network Login Failure" %}

    + +

    {% trans "An error occurred while attempting to login via your social network account." %}

    +{% endblock %} diff --git a/templates/socialaccount/base.html b/templates/socialaccount/base.html new file mode 100644 index 0000000..b64fd56 --- /dev/null +++ b/templates/socialaccount/base.html @@ -0,0 +1 @@ +{% extends "account/base.html" %} diff --git a/templates/socialaccount/connections.html b/templates/socialaccount/connections.html new file mode 100644 index 0000000..f7c2729 --- /dev/null +++ b/templates/socialaccount/connections.html @@ -0,0 +1,54 @@ +{% extends "socialaccount/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Account Connections" %}{% endblock %} + +{% block content %} +

    {% trans "Account Connections" %}

    + +{% if form.accounts %} +

    {% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}

    + + +
    +{% csrf_token %} + +
    +{% if form.non_field_errors %} +
    {{ form.non_field_errors }}
    +{% endif %} + +{% for base_account in form.accounts %} +{% with base_account.get_provider_account as account %} +
    + +
    +{% endwith %} +{% endfor %} + +
    + +
    + +
    + +
    + +{% else %} +

    {% trans 'You currently have no social network accounts connected to this account.' %}

    +{% endif %} + +

    {% trans 'Add a 3rd Party Account' %}

    + +
      +{% include "socialaccount/snippets/provider_list.html" with process="connect" %} +
    + +{% include "socialaccount/snippets/login_extra.html" %} + +{% endblock %} diff --git a/templates/socialaccount/login_cancelled.html b/templates/socialaccount/login_cancelled.html new file mode 100644 index 0000000..8d76786 --- /dev/null +++ b/templates/socialaccount/login_cancelled.html @@ -0,0 +1,15 @@ +{% extends "socialaccount/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Login Cancelled" %}{% endblock %} + +{% block content %} + +

    {% trans "Login Cancelled" %}

    + +{% url 'account_login' as login_url %} + +

    {% blocktrans %}You decided to cancel logging in to our site using one of your existing accounts. If this was a mistake, please proceed to sign in.{% endblocktrans %}

    + +{% endblock %} diff --git a/templates/socialaccount/messages/account_connected.txt b/templates/socialaccount/messages/account_connected.txt new file mode 100644 index 0000000..be6aa60 --- /dev/null +++ b/templates/socialaccount/messages/account_connected.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}The social account has been connected.{% endblocktrans %} diff --git a/templates/socialaccount/messages/account_connected_other.txt b/templates/socialaccount/messages/account_connected_other.txt new file mode 100644 index 0000000..e90f6cc --- /dev/null +++ b/templates/socialaccount/messages/account_connected_other.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}The social account is already connected to a different account.{% endblocktrans %} diff --git a/templates/socialaccount/messages/account_connected_updated.txt b/templates/socialaccount/messages/account_connected_updated.txt new file mode 100644 index 0000000..3f7174e --- /dev/null +++ b/templates/socialaccount/messages/account_connected_updated.txt @@ -0,0 +1 @@ +{% extends "socialaccount/messages/account_connected.txt" %} diff --git a/templates/socialaccount/messages/account_disconnected.txt b/templates/socialaccount/messages/account_disconnected.txt new file mode 100644 index 0000000..fd43f30 --- /dev/null +++ b/templates/socialaccount/messages/account_disconnected.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}The social account has been disconnected.{% endblocktrans %} diff --git a/templates/socialaccount/signup.html b/templates/socialaccount/signup.html new file mode 100644 index 0000000..caa2de2 --- /dev/null +++ b/templates/socialaccount/signup.html @@ -0,0 +1,22 @@ +{% extends "socialaccount/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Signup" %}{% endblock %} + +{% block content %} +

    {% trans "Sign Up" %}

    + +

    {% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to +{{site_name}}. As a final step, please complete the following form:{% endblocktrans %}

    + + + +{% endblock %} diff --git a/templates/socialaccount/snippets/login_extra.html b/templates/socialaccount/snippets/login_extra.html new file mode 100644 index 0000000..307def4 --- /dev/null +++ b/templates/socialaccount/snippets/login_extra.html @@ -0,0 +1,3 @@ +{% load socialaccount %} + +{% providers_media_js %} diff --git a/templates/socialaccount/snippets/provider_list.html b/templates/socialaccount/snippets/provider_list.html new file mode 100644 index 0000000..e76a296 --- /dev/null +++ b/templates/socialaccount/snippets/provider_list.html @@ -0,0 +1,20 @@ +{% load socialaccount %} + +{% get_providers as socialaccount_providers %} + +{% for provider in socialaccount_providers %} +{% if provider.id == "openid" %} +{% for brand in provider.get_brands %} +
  • + {{brand.name}} +
  • +{% endfor %} +{% endif %} +
  • + {{provider.name}} +
  • +{% endfor %} diff --git a/templates/staff/product_create.html b/templates/staff/product_create.html new file mode 100644 index 0000000..4bf8642 --- /dev/null +++ b/templates/staff/product_create.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + +
    +
    +
    +
    + Go back to products +
    +

    Create a product +
    + {% csrf_token %} + {{ form|crispy }} + +
    +

    +
    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/templates/staff/product_delete.html b/templates/staff/product_delete.html new file mode 100644 index 0000000..0fbe6c1 --- /dev/null +++ b/templates/staff/product_delete.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + +
    +
    +
    +
    +
    + {% csrf_token %} + {{ form|crispy }} + +
    +
    +
    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/templates/staff/product_list.html b/templates/staff/product_list.html new file mode 100644 index 0000000..4732903 --- /dev/null +++ b/templates/staff/product_list.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    +

    Staff Portal - Products

    + Create a product +
    +
    +
    + + + + + + + + + + {% for product in products %} + + + + + + {% empty %} + + + + {% endfor %} + +
    Title.PriceActions
    {{ product.title }}${{ product.get_price }} + Update + + X + + +
    You don't have any products
    +
    + + {% if page_obj.has_other_pages %} +
    +
    +
    +
      + {% if page_obj.has_previous %} +
    • «
    • + {% else %} +
    • «
    • + {% endif %} + + {% for i in paginator.page_range %} + {% if page_obj.number == i %} +
    • + + {{ i }} + (current) + +
    • + {% else %} +
    • {{ i }}
    • + {% endif %} + {% endfor %} + + {% if page_obj.has_next %} +
    • »
    • + {% else %} +
    • »
    • + {% endif %} +
    +
    +
    +
    + {% endif %} + +
    +
    +
    +
    +
    +
    + +{% endblock content %} + +{% block scripts %} + +{% endblock scripts %} \ No newline at end of file diff --git a/templates/staff/product_update.html b/templates/staff/product_update.html new file mode 100644 index 0000000..0cea3c7 --- /dev/null +++ b/templates/staff/product_update.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + +
    +
    +
    +
    + Go back to products +
    +

    Update product +
    + {% csrf_token %} + {{ form|crispy }} + +
    +

    +
    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/templates/staff/staff.html b/templates/staff/staff.html new file mode 100644 index 0000000..78a3d4f --- /dev/null +++ b/templates/staff/staff.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block content %} + +
    +
    +
    +
    +

    Staff Portal

    +
    +
    +
    + {{ request.user.email }} +
    +
    + Products +
    + +
    Recent orders
    +
    + + + + + + + + + + + + {% for order in orders %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
    Reference No.DateUserAmountPayment Status
    #{{ order.reference_number }}{{ order.ordered_date }}{{ order.user.email }}${{ order.get_total }}{% if order.ordered %}Paid{% else %}Not paid{% endif %}
    You haven't made any orders
    +
    + + {% if page_obj.has_other_pages %} +
    +
    +
    +
      + {% if page_obj.has_previous %} +
    • «
    • + {% else %} +
    • «
    • + {% endif %} + + {% for i in paginator.page_range %} + {% if page_obj.number == i %} +
    • + + {{ i }} + (current) + +
    • + {% else %} +
    • {{ i }}
    • + {% endif %} + {% endfor %} + + {% if page_obj.has_next %} +
    • »
    • + {% else %} +
    • »
    • + {% endif %} +
    +
    +
    +
    + {% endif %} + +
    +
    +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file diff --git a/templates/tests/test_403_csrf.html b/templates/tests/test_403_csrf.html new file mode 100644 index 0000000..86f9aea --- /dev/null +++ b/templates/tests/test_403_csrf.html @@ -0,0 +1,2 @@ +{% load socialaccount %} +Sign In