initial
This commit is contained in:
parent
6c7f355b52
commit
53b6fba4c2
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
3
accounts/admin.py
Normal file
3
accounts/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
accounts/apps.py
Normal file
5
accounts/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
14
accounts/forms.py
Normal file
14
accounts/forms.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class GuestForm(forms.Form):
|
||||
email = forms.EmailField()
|
||||
|
||||
|
||||
# class LoginForm(forms.Form):
|
||||
# username = forms.CharField()
|
||||
# password = forms.CharField(widget=forms.PasswordInput)
|
||||
|
||||
# delete guest session if logged in
|
26
accounts/migrations/0001_initial.py
Normal file
26
accounts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-02 08:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GuestEmail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('update', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
11
accounts/models.py
Normal file
11
accounts/models.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class GuestEmail(models.Model):
|
||||
email = models.EmailField()
|
||||
active = models.BooleanField(default=True)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
update = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
3
accounts/tests.py
Normal file
3
accounts/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
7
accounts/urls.py
Normal file
7
accounts/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
from .views import guest_login_page
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url("^guest_login", guest_login_page, name="guest_login"),
|
||||
]
|
25
accounts/views.py
Normal file
25
accounts/views.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django.shortcuts import render
|
||||
from .forms import GuestForm
|
||||
from .models import GuestEmail
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
def guest_login_page(request):
|
||||
form = GuestForm(request.POST or None)
|
||||
# context = {"form": form}
|
||||
# TODO: next stuff
|
||||
if form.is_valid():
|
||||
print('form is valid')
|
||||
email = form.cleaned_data.get("email")
|
||||
new_guest_email = GuestEmail.objects.create(email=email)
|
||||
request.session['guest_email_id'] = new_guest_email.id
|
||||
|
||||
return redirect("carts:checkout")
|
||||
|
||||
def login_page(resuest):
|
||||
pass
|
||||
|
||||
|
||||
def logout_page(resuest):
|
||||
pass
|
||||
|
0
addresses/__init__.py
Normal file
0
addresses/__init__.py
Normal file
4
addresses/admin.py
Normal file
4
addresses/admin.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from .models import Address
|
||||
|
||||
admin.site.register(Address)
|
5
addresses/apps.py
Normal file
5
addresses/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AddressesConfig(AppConfig):
|
||||
name = 'addresses'
|
8
addresses/forms.py
Normal file
8
addresses/forms.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django import forms
|
||||
|
||||
from .models import Address
|
||||
|
||||
class AddressForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Address
|
||||
exclude = ['billing_profile']
|
31
addresses/migrations/0001_initial.py
Normal file
31
addresses/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-02 13:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('billing', '0002_auto_20180801_1339'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Address',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('address_type', models.CharField(choices=[('billing', 'Billing'), ('shipping', 'Shipping')], max_length=120)),
|
||||
('adress_line_1', models.CharField(max_length=120)),
|
||||
('adress_line_2', models.CharField(blank=True, max_length=120, null=True)),
|
||||
('city', models.CharField(max_length=120)),
|
||||
('country', models.CharField(max_length=120)),
|
||||
('code', models.CharField(max_length=120)),
|
||||
('billing_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='billing.BillingProfile')),
|
||||
],
|
||||
),
|
||||
]
|
0
addresses/migrations/__init__.py
Normal file
0
addresses/migrations/__init__.py
Normal file
27
addresses/models.py
Normal file
27
addresses/models.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.db import models
|
||||
from billing.models import BillingProfile
|
||||
|
||||
ADDRESS_TYPE = (
|
||||
('billing', 'Billing'),
|
||||
('shipping', 'Shipping')
|
||||
)
|
||||
class Address(models.Model):
|
||||
billing_profile = models.ForeignKey(BillingProfile)
|
||||
address_type = models.CharField(max_length=120, choices=ADDRESS_TYPE)
|
||||
adress_line_1 = models.CharField(max_length=120)
|
||||
adress_line_2 = models.CharField(max_length=120, null=True, blank=True)
|
||||
city = models.CharField(max_length=120)
|
||||
country = models.CharField(max_length=120)
|
||||
code = models.CharField(max_length=120)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.billing_profile)
|
||||
|
||||
def get_address(self):
|
||||
return "{line1}\n{line2}\n{city}\n{country}\n".format(
|
||||
line1=self.adress_line_1,
|
||||
line2=self.adress_line_2 or "",
|
||||
city=self.city,
|
||||
country=self.country
|
||||
)
|
||||
|
3
addresses/tests.py
Normal file
3
addresses/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
11
addresses/urls.py
Normal file
11
addresses/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
from .views import (
|
||||
checkout_address_create,
|
||||
checkout_address_use
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^create/$", checkout_address_create, name="checkout"),
|
||||
url(r"^reuse/$", checkout_address_use, name="reuse"),
|
||||
url(r"^reuse/$", checkout_address_use, name="reuse"),
|
||||
]
|
46
addresses/views.py
Normal file
46
addresses/views.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from django.shortcuts import redirect
|
||||
from .forms import AddressForm
|
||||
from billing.models import BillingProfile
|
||||
from .models import Address
|
||||
|
||||
|
||||
def checkout_address_create(request):
|
||||
form = AddressForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
inst = form.save(commit=False)
|
||||
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
|
||||
if billing_profile is not None:
|
||||
inst.billing_profile = billing_profile
|
||||
inst.address_type = "shipping"
|
||||
inst.save()
|
||||
request.session["address_id"] = inst.id
|
||||
else:
|
||||
print("error")
|
||||
return redirect("carts:checkout")
|
||||
|
||||
def checkout_address_use(request):
|
||||
if request.user.is_authenticated():
|
||||
context = {}
|
||||
if request.method == "POST":
|
||||
print(request.POST)
|
||||
address = request.POST.get("address", None)
|
||||
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
|
||||
if address is not None:
|
||||
qs = Address.objects.filter(billing_profile=billing_profile, id=address)
|
||||
if qs.exists():
|
||||
request.session["address_id"] = address
|
||||
return redirect("carts:checkout")
|
||||
|
||||
"""
|
||||
def guest_login_page(request):
|
||||
form = GuestForm(request.POST or None)
|
||||
# context = {"form": form}
|
||||
# TODO: next stuff
|
||||
if form.is_valid():
|
||||
print('form is valid')
|
||||
email = form.cleaned_data.get("email")
|
||||
new_guest_email = GuestEmail.objects.create(email=email)
|
||||
request.session['guest_email_id'] = new_guest_email.id
|
||||
|
||||
return redirect("carts:checkout")
|
||||
"""
|
0
billing/__init__.py
Normal file
0
billing/__init__.py
Normal file
5
billing/admin.py
Normal file
5
billing/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from .models import BillingProfile
|
||||
|
||||
admin.site.register(BillingProfile)
|
||||
# Register your models here.
|
5
billing/apps.py
Normal file
5
billing/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BillingConfig(AppConfig):
|
||||
name = 'billing'
|
30
billing/migrations/0001_initial.py
Normal file
30
billing/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-01 13:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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='BillingProfile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('update', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
22
billing/migrations/0002_auto_20180801_1339.py
Normal file
22
billing/migrations/0002_auto_20180801_1339.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-01 13:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='billingprofile',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique=True),
|
||||
),
|
||||
]
|
0
billing/migrations/__init__.py
Normal file
0
billing/migrations/__init__.py
Normal file
50
billing/models.py
Normal file
50
billing/models.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from accounts.models import GuestEmail
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BillingProfileManager(models.Manager):
|
||||
def new_or_get(self, request):
|
||||
created = False
|
||||
obj = None
|
||||
guest_email_id = request.session.get("guest_email_id")
|
||||
if request.user.is_authenticated():
|
||||
'logged in user checkout'
|
||||
obj, created = self.model.objects.get_or_create(
|
||||
user=request.user,
|
||||
email=request.user.email
|
||||
)
|
||||
elif guest_email_id is not None:
|
||||
'guest user checkout'
|
||||
print(guest_email_id)
|
||||
guest_email_obj = GuestEmail.objects.get(id=guest_email_id)
|
||||
obj, created = self.model.objects.get_or_create(
|
||||
email=guest_email_obj.email
|
||||
)
|
||||
else:
|
||||
pass
|
||||
return obj, created
|
||||
|
||||
|
||||
class BillingProfile(models.Model):
|
||||
user = models.ForeignKey(User, unique=True, null=True, blank=True)
|
||||
email = models.EmailField()
|
||||
active = models.BooleanField(default=True)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
update = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
objects = BillingProfileManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_create(sender, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
BillingProfile.objects.get_or_create(user=instance)
|
||||
|
3
billing/tests.py
Normal file
3
billing/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
billing/views.py
Normal file
3
billing/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
carts/__init__.py
Normal file
0
carts/__init__.py
Normal file
5
carts/admin.py
Normal file
5
carts/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Cart
|
||||
|
||||
admin.site.register(Cart)
|
5
carts/apps.py
Normal file
5
carts/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CartsConfig(AppConfig):
|
||||
name = 'carts'
|
31
carts/migrations/0001_initial.py
Normal file
31
carts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-31 12:53
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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),
|
||||
('products', '0006_product_time'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Cart',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('total', models.DecimalField(decimal_places=2, default=0.0, max_digits=100)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('products', models.ManyToManyField(blank=True, null=True, to='products.Product')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
20
carts/migrations/0002_cart_subtotal.py
Normal file
20
carts/migrations/0002_cart_subtotal.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-31 15:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('carts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cart',
|
||||
name='subtotal',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=100),
|
||||
),
|
||||
]
|
0
carts/migrations/__init__.py
Normal file
0
carts/migrations/__init__.py
Normal file
68
carts/models.py
Normal file
68
carts/models.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from products.models import Product
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import m2m_changed, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class CartManager(models.Manager):
|
||||
def new_or_get(self, request):
|
||||
cart_id = request.session.get("cart_id", None)
|
||||
qs = self.get_queryset().filter(id=cart_id)
|
||||
if qs.count() == 1:
|
||||
|
||||
new_obj = False
|
||||
cart_obj = qs.first()
|
||||
if request.user.is_authenticated() and cart_obj.user is None:
|
||||
cart_obj.user = request.user
|
||||
cart_obj.save()
|
||||
else:
|
||||
cart_obj = Cart.objects.new(user=request.user)
|
||||
new_obj = True
|
||||
request.session["cart_id"] = cart_obj.id
|
||||
return cart_obj, new_obj
|
||||
|
||||
def new(self, user=None, products=None):
|
||||
if user is not None:
|
||||
if user.is_authenticated():
|
||||
return self.model.objects.create(user=user)
|
||||
return self.model.objects.create(user=None)
|
||||
|
||||
|
||||
class Cart(models.Model):
|
||||
user = models.ForeignKey(User, null=True, blank=True)
|
||||
products = models.ManyToManyField(Product, null=True, blank=True)
|
||||
total = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
|
||||
subtotal = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = CartManager()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Cart.products.through)
|
||||
def cart_change(sender, instance, action, **kwargs):
|
||||
if action in ("post_add", "post_remove", "post_clear"):
|
||||
|
||||
print('change')
|
||||
prs = instance.products.all()
|
||||
t = 0
|
||||
for i in prs:
|
||||
t += i.price
|
||||
if instance.subtotal != t:
|
||||
|
||||
instance.subtotal = t
|
||||
instance.save()
|
||||
# cart_obj.save()
|
||||
|
||||
@receiver(pre_save, sender=Cart)
|
||||
def cart_save(sender, instance, **kwargs):
|
||||
instance.total = float(instance.subtotal) * float(0.95) # 5% discount
|
||||
|
||||
|
8
carts/templates/carts/checkout-done.html
Normal file
8
carts/templates/carts/checkout-done.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-6 mx-auto py-5 text-center">
|
||||
<h1 class="display-1">Tank you for your order!</h1>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
61
carts/templates/carts/checkout.html
Normal file
61
carts/templates/carts/checkout.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ object }} -- {{ object.cart }}
|
||||
|
||||
{% if not billing_profile %}
|
||||
<div class="row text-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<p class="lead">Login</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
Continue as guest
|
||||
<form action={% url "accounts:guest_login" %} method="POST">
|
||||
{% csrf_token %}
|
||||
{{ guest_form.as_p }} <button type='submit' class='btn btn-default'>Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% if not object.address %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="lead">Adress</p>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<form method="POST" action={% url "addresses:reuse" %} > {% csrf_token %}
|
||||
{% for addr in address_qs %}
|
||||
<label for="{{ addr.id }}">
|
||||
<input id="address-{{ addr.id }}" type="radio" name="address" value="{{ addr.id }}">
|
||||
{{ addr.adress_line_1 }}</label><br/>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-success">Use Address</button>
|
||||
</form>
|
||||
<form action={% url "addresses:checkout" %} method="POST" >
|
||||
{% csrf_token %}
|
||||
{{ address_form.as_p }}
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
|
||||
</form></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<h1>Finalize checkout</h1>
|
||||
<p>Shipping Address: {{ object.address.get_address}}</p>
|
||||
<p>Cart items: {% for item in object.cart.products.all %}{{ item }}{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
|
||||
<p>Cart total: {{ object.cart.total }}</p>
|
||||
<p>Shipping Total: {{ object.shipping_total }}</p>
|
||||
<p>Order Total: {{ object.total }}</p>
|
||||
<form class="form" method="POST" action="">{% csrf_token %}
|
||||
<button type=submit" class="btn btn-success">Checkout</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
8
carts/templates/carts/form.html
Normal file
8
carts/templates/carts/form.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<form method="POST" action={% url "carts:update" %} class="form"> {% csrf_token %}
|
||||
<input type="hidden" name="product_id" value="{{ object.id }}" />
|
||||
{% if object in cart.products.all %}
|
||||
<button class="btn btn-link" style="padding: 0px">Remove from cart?</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success">ADD to cart</button>
|
||||
{% endif %}
|
||||
</form>
|
35
carts/templates/carts/home.html
Normal file
35
carts/templates/carts/home.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Cart</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Product Name</th>
|
||||
<th scope="col">Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product in cart.products.all %}
|
||||
<tr>
|
||||
<th scope="row">{{ forloop.counter }}</th>
|
||||
<td><a href="{{ product.get_absolute_url }}" {{ product.title }}>{{ product.title }}</a>{% include "carts/form.html" with object=product in_cart=True %}
|
||||
</td>
|
||||
<td>{{ product.price }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td>Subtotal {{ cart.subtotal }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td>Total {{ cart.total }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td><a href={% url "carts:checkout" %} class="btn btn-lg btn-block btn-success">Checkout</a></td>
|
||||
</tr>
|
||||
|
||||
{% endblock %}
|
3
carts/tests.py
Normal file
3
carts/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
carts/urls.py
Normal file
10
carts/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from .views import cart_home, cart_update, checkout_home, checkout_done
|
||||
from django.conf.urls import url
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url("^$", cart_home, name="home"),
|
||||
url("^checkout/$", checkout_home, name="checkout"),
|
||||
url("^update/$", cart_update, name="update"),
|
||||
url("^checkout/success/$", checkout_done, name="success")
|
||||
]
|
71
carts/views.py
Normal file
71
carts/views.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
|
||||
from products.models import Product
|
||||
from .models import Cart
|
||||
from orders.models import Order
|
||||
from billing.models import BillingProfile
|
||||
from accounts.forms import GuestForm
|
||||
from accounts.models import GuestEmail
|
||||
from addresses.forms import AddressForm
|
||||
from addresses.models import Address
|
||||
|
||||
|
||||
def cart_home(request):
|
||||
cart_obj, new_obj = Cart.objects.new_or_get(request)
|
||||
return render(request, 'carts/home.html', {"cart": cart_obj})
|
||||
|
||||
|
||||
def cart_update(request):
|
||||
product_id = request.POST.get("product_id")
|
||||
product_obj = get_object_or_404(Product, id=product_id)
|
||||
cart_obj, new_obj = Cart.objects.new_or_get(request)
|
||||
if product_obj in cart_obj.products.all():
|
||||
cart_obj.products.remove(product_obj)
|
||||
else:
|
||||
cart_obj.products.add(product_obj)
|
||||
request.session['cart_items'] = cart_obj.products.count()
|
||||
return redirect("carts:home")
|
||||
|
||||
|
||||
def checkout_home(request):
|
||||
cart_obj, cart_created = Cart.objects.new_or_get(request)
|
||||
order_obj = None
|
||||
if cart_created or cart_obj.products.count() == 0:
|
||||
return redirect("carts:home")
|
||||
guest_form = GuestForm()
|
||||
address_form = AddressForm()
|
||||
address_id = request.session.get("address_id", None)
|
||||
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
|
||||
address_qs = None
|
||||
if billing_profile is not None:
|
||||
if request.user.is_authenticated():
|
||||
|
||||
address_qs = Address.objects.filter(billing_profile=billing_profile)
|
||||
order_obj, order_obj_created = Order.objects.new_or_get(billing_profile, cart_obj)
|
||||
if address_id:
|
||||
order_obj.address = Address.objects.get(id=address_id)
|
||||
request.session['cart_items'] = 0
|
||||
del request.session["address_id"]
|
||||
order_obj.save()
|
||||
|
||||
if request.method == 'POST':
|
||||
'check that order is done'
|
||||
is_done = order_obj.check_done()
|
||||
if is_done:
|
||||
order_obj.mark_paid()
|
||||
del request.session['cart_id']
|
||||
return redirect('carts:success')
|
||||
|
||||
|
||||
context = {
|
||||
"object": order_obj,
|
||||
"billing_profile": billing_profile,
|
||||
"guest_form" : guest_form,
|
||||
"address_form": address_form,
|
||||
"address_qs": address_qs
|
||||
}
|
||||
return render(request, "carts/checkout.html", context)
|
||||
|
||||
|
||||
def checkout_done(request):
|
||||
return render(request, "carts/checkout-done.html")
|
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
0
ecommerce/__init__.py
Normal file
0
ecommerce/__init__.py
Normal file
131
ecommerce/settings.py
Normal file
131
ecommerce/settings.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
"""
|
||||
Django settings for ecommerce project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.11.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'o0!mhcklo#cz1zrlmr)nfr^8!@$kz56*vv##!hb0(^j$k3iy9='
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'products',
|
||||
'carts',
|
||||
'orders',
|
||||
'billing',
|
||||
'accounts',
|
||||
'addresses'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'ecommerce.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(PROJECT_ROOT, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ecommerce.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
|
26
ecommerce/templates/base.html
Normal file
26
ecommerce/templates/base.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% load static %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<!-- Fontawesome CSS -->
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
{% include 'base/nav.html' with brand_name='Brand name'%}
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!-- Optional JavaScript -->
|
||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
50
ecommerce/templates/base/nav.html
Normal file
50
ecommerce/templates/base/nav.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% url 'products:list' as prod_list %}
|
||||
{% url 'carts:home' as cart_url %}
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">{% if brand_name%} {{ brand_name}} {% else %} eCommerce {% endif %}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item {% if request.path == prod_list %}active{% endif %}">
|
||||
<a class="nav-link" href={{ prod_list }}>Products</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item {% if request.path == cart_url %}active{% endif %}">
|
||||
<a class="nav-link" href={{ cart_url }}>{{ request.session.cart_items }} <i class="fas fa-shopping-cart"></i></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Dropdown
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if request.user.is_authenticated %}
|
||||
<a class="nav-link" href="/admin/logout/">Logout</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="/admin/login/">Login</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
22
ecommerce/templates/bootstrap/example.html
Normal file
22
ecommerce/templates/bootstrap/example.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
|
||||
<title>Hello, world!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
<!-- Optional JavaScript -->
|
||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
34
ecommerce/urls.py
Normal file
34
ecommerce/urls.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
"""ecommerce URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.11/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^bootstrap/?$', TemplateView.as_view(template_name='bootstrap/example.html')),
|
||||
url(r'^products/', include('products.urls', namespace="products")),
|
||||
url(r'^cart/', include('carts.urls', namespace="carts")),
|
||||
url(r'^accounts/', include('accounts.urls', namespace="accounts")),
|
||||
url(r'^address/', include('addresses.urls', namespace="addresses")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
43
ecommerce/utils.py
Normal file
43
ecommerce/utils.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import string
|
||||
import random
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
|
||||
def unique_order_id_generator(inst):
|
||||
"""
|
||||
Generate unique order_id.
|
||||
Model should have an order_id field to use this function.
|
||||
"""
|
||||
print('i am here')
|
||||
order_new_id = random_string_generator()
|
||||
|
||||
Klass = inst.__class__
|
||||
qs_exists = Klass.objects.filter(order_id =order_new_id).exists()
|
||||
if qs_exists:
|
||||
return unique_slug_generator(inst)
|
||||
return order_new_id
|
||||
|
||||
|
||||
def unique_slug_generator(inst, new_slug=None):
|
||||
"""
|
||||
Generate unique slug from title or based on provided slug.
|
||||
Model should have a slug field and a title (char) field to use this function.
|
||||
"""
|
||||
if new_slug is not None:
|
||||
slug = new_slug
|
||||
else:
|
||||
slug = slugify(inst.title)
|
||||
|
||||
Klass = inst.__class__
|
||||
qs_exists = Klass.objects.filter(slug=slug).exists()
|
||||
if qs_exists:
|
||||
new_slug = "{slug}-{randstr}".format(
|
||||
slug=slug,
|
||||
randstr=random_string_generator(size=4)
|
||||
)
|
||||
return unique_slug_generator(inst, new_slug=new_slug)
|
||||
return slug
|
16
ecommerce/wsgi.py
Normal file
16
ecommerce/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for ecommerce project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ecommerce.settings")
|
||||
|
||||
application = get_wsgi_application()
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ecommerce.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
0
orders/__init__.py
Normal file
0
orders/__init__.py
Normal file
4
orders/admin.py
Normal file
4
orders/admin.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from .models import Order
|
||||
|
||||
admin.site.register(Order)
|
5
orders/apps.py
Normal file
5
orders/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrdersConfig(AppConfig):
|
||||
name = 'orders'
|
29
orders/migrations/0001_initial.py
Normal file
29
orders/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-01 11:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('carts', '0002_cart_subtotal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order_id', models.CharField(max_length=100)),
|
||||
('status', models.CharField(choices=[('created', 'Created'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('refunded', 'Refunded')], default='created', max_length=120)),
|
||||
('shipping_total', models.DecimalField(decimal_places=2, default=5.99, max_digits=100)),
|
||||
('total', models.DecimalField(decimal_places=2, default=0.0, max_digits=100)),
|
||||
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='carts.Cart')),
|
||||
],
|
||||
),
|
||||
]
|
20
orders/migrations/0002_auto_20180801_1332.py
Normal file
20
orders/migrations/0002_auto_20180801_1332.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-01 13:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='order_id',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
27
orders/migrations/0003_auto_20180802_0823.py
Normal file
27
orders/migrations/0003_auto_20180802_0823.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-02 08:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0002_auto_20180801_1339'),
|
||||
('orders', '0002_auto_20180801_1332'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='billing_profile',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='billing.BillingProfile'),
|
||||
),
|
||||
]
|
22
orders/migrations/0004_order_shipping_address.py
Normal file
22
orders/migrations/0004_order_shipping_address.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-02 14:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('addresses', '0001_initial'),
|
||||
('orders', '0003_auto_20180802_0823'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='shipping_address',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='addresses.Address'),
|
||||
),
|
||||
]
|
20
orders/migrations/0005_auto_20180802_1436.py
Normal file
20
orders/migrations/0005_auto_20180802_1436.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-08-02 14:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0004_order_shipping_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='order',
|
||||
old_name='shipping_address',
|
||||
new_name='address',
|
||||
),
|
||||
]
|
0
orders/migrations/__init__.py
Normal file
0
orders/migrations/__init__.py
Normal file
102
orders/models.py
Normal file
102
orders/models.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
from math import fsum
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from billing.models import BillingProfile
|
||||
from carts.models import Cart
|
||||
from ecommerce.utils import unique_order_id_generator
|
||||
from addresses.models import Address
|
||||
|
||||
ORDER_STATUS_CHOICES = (
|
||||
('created', 'Created'),
|
||||
('paid', 'Paid'),
|
||||
('shipped', 'Shipped'),
|
||||
('refunded', 'Refunded')
|
||||
)
|
||||
|
||||
|
||||
class OrderManager(models.Manager):
|
||||
def new_or_get(self, billing_profile, cart_obj):
|
||||
created = False
|
||||
qs = self.get_queryset().filter(billing_profile=billing_profile,
|
||||
cart=cart_obj, active=True,
|
||||
status='created')
|
||||
if qs.count() == 1:
|
||||
obj = qs.first()
|
||||
else:
|
||||
obj = self.model.objects.create(billing_profile=billing_profile,
|
||||
cart=cart_obj)
|
||||
created = True
|
||||
return obj, created
|
||||
|
||||
class Order(models.Model):
|
||||
billing_profile = models.ForeignKey(BillingProfile, null=True, blank=True)
|
||||
order_id = models.CharField(max_length=100, blank=True)
|
||||
address = models.ForeignKey(Address, null=True, blank=True)
|
||||
cart = models.ForeignKey(Cart)
|
||||
status = models.CharField(max_length=120, default='created', choices=ORDER_STATUS_CHOICES)
|
||||
shipping_total = models.DecimalField(default=5.99, max_digits=100, decimal_places=2)
|
||||
total = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
objects = OrderManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.order_id
|
||||
|
||||
def update_total(self):
|
||||
cart_total = self.cart.total
|
||||
shipping_total = self.shipping_total
|
||||
total = fsum([cart_total, shipping_total])
|
||||
total = format(total, '.2f')
|
||||
self.total = total
|
||||
self.save()
|
||||
return total
|
||||
|
||||
def check_done(self):
|
||||
billing_profile = self.billing_profile
|
||||
address = self.address
|
||||
total = self.total
|
||||
if billing_profile and address and total > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def mark_paid(self):
|
||||
if self.check_done():
|
||||
self.status = "paid"
|
||||
self.save()
|
||||
return self.status
|
||||
|
||||
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Order)
|
||||
def create_order_id(sender, instance, *args, **kwargs):
|
||||
print('ord_id')
|
||||
if not instance.order_id:
|
||||
instance.order_id = unique_order_id_generator(instance)
|
||||
qs = Order.objects.filter(cart=instance.cart).exclude(billing_profile=instance.billing_profile)
|
||||
if qs.exists():
|
||||
qs.update(active=False)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Cart)
|
||||
def create_cart_total(sender, instance, created, *args, **kwargs):
|
||||
if not created:
|
||||
cart_obj = instance
|
||||
cart_total = cart_obj.total
|
||||
cart_id = cart_obj.id
|
||||
qs = Order.objects.filter(cart__id=cart_id)
|
||||
if qs.count() == 1:
|
||||
order_obj = qs.first()
|
||||
order_obj.update_total()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Order)
|
||||
def create_order(sender, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
instance.update_total()
|
||||
|
||||
|
3
orders/tests.py
Normal file
3
orders/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
orders/views.py
Normal file
3
orders/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
products/__init__.py
Normal file
0
products/__init__.py
Normal file
9
products/admin.py
Normal file
9
products/admin.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . models import Product
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
# fields = ('image_tag',)
|
||||
list_display = ('title', 'slug', 'time', 'image_tag')
|
||||
readonly_fields = ('image_tag',)
|
5
products/apps.py
Normal file
5
products/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProductsConfig(AppConfig):
|
||||
name = 'products'
|
25
products/migrations/0001_initial.py
Normal file
25
products/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 08:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=120)),
|
||||
('description', models.TextField()),
|
||||
('price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
],
|
||||
),
|
||||
]
|
20
products/migrations/0002_product_image.py
Normal file
20
products/migrations/0002_product_image.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 09:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='image',
|
||||
field=models.FileField(blank=True, null=True, upload_to='products/'),
|
||||
),
|
||||
]
|
20
products/migrations/0003_auto_20180729_0956.py
Normal file
20
products/migrations/0003_auto_20180729_0956.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 09:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0002_product_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='products/'),
|
||||
),
|
||||
]
|
20
products/migrations/0004_product_slug.py
Normal file
20
products/migrations/0004_product_slug.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 10:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0003_auto_20180729_0956'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True),
|
||||
),
|
||||
]
|
20
products/migrations/0005_auto_20180729_1104.py
Normal file
20
products/migrations/0005_auto_20180729_1104.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 11:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0004_product_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, unique=True),
|
||||
),
|
||||
]
|
22
products/migrations/0006_product_time.py
Normal file
22
products/migrations/0006_product_time.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2018-07-29 13:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0005_auto_20180729_1104'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='time',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
0
products/migrations/__init__.py
Normal file
0
products/migrations/__init__.py
Normal file
44
products/models.py
Normal file
44
products/models.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from ecommerce.utils import unique_slug_generator
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ProductManager(models.Manager):
|
||||
def get_by_id(self, id):
|
||||
return self.get_queryset().filter(id=id).first() or None
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
title = models.CharField(max_length=120)
|
||||
slug = models.SlugField(blank=True, unique=True)
|
||||
description = models.TextField()
|
||||
price = models.DecimalField(decimal_places=2, max_digits=10, default=0)
|
||||
image = models.ImageField(upload_to='products/', null=True, blank=True)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
objects = ProductManager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("products:details", kwargs={'slug': self.slug})
|
||||
# return "/{slug}/".format(slug=self.slug)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def image_tag(self):
|
||||
if self.image:
|
||||
return '<img src={} style="height: 200px">'.format(self.image.url)
|
||||
return 'No file'
|
||||
|
||||
image_tag.short_description = 'Image_preview'
|
||||
image_tag.allow_tags = True
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Product)
|
||||
def product_save(sender, instance, **kwargs):
|
||||
if not instance.slug:
|
||||
instance.slug = unique_slug_generator(instance)
|
18
products/templates/products/details.html
Normal file
18
products/templates/products/details.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
{{ object.title }}<br>
|
||||
{{ object.time|timesince}} ago
|
||||
{{ object.description|linebreaks }}<br>
|
||||
{% if object.image %}
|
||||
<img src={{ object.image.url }}>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
{{ cart }}
|
||||
{% include "carts/form.html" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
19
products/templates/products/list.html
Normal file
19
products/templates/products/list.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
{% for obj in object_list %}
|
||||
<div class="col">
|
||||
{{ forloop.counter }}
|
||||
{% include 'products/snippets/card.html' with instance=obj %}
|
||||
{% if forloop.counter|divisibleby:2 %}
|
||||
</div></div><div class="row">
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
10
products/templates/products/snippets/card.html
Normal file
10
products/templates/products/snippets/card.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="card" style="width: 18rem;">
|
||||
{% if instance.image %}
|
||||
<img class="card-img-top" src="{{ instance.image.url }}" alt="{{ instance.title}} logo">
|
||||
{% endif%}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ instance.title}}</h5>
|
||||
<p class="card-text">{{ instance.description|linebreaks|truncatewords:20 }}</p>
|
||||
<a href="{{ instance.get_absolute_url }}" class="btn btn-primary">View</a>
|
||||
</div>
|
||||
</div>
|
3
products/tests.py
Normal file
3
products/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
7
products/urls.py
Normal file
7
products/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
from .views import ProductListView, ProductDetailView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', ProductListView.as_view(), name='list'),
|
||||
url(r'^(?P<slug>[\w-]+)/?$', ProductDetailView.as_view(), name='details')
|
||||
]
|
45
products/views.py
Normal file
45
products/views.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from django.shortcuts import render
|
||||
from django.views.generic import ListView, DetailView
|
||||
from .models import Product
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import Http404
|
||||
from carts.models import Cart
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class ProductListView(ListView):
|
||||
queryset = Product.objects.all()
|
||||
template_name = "products/list.html"
|
||||
|
||||
# def get_context_data(self, *args, **kwargs):
|
||||
# context = super().get_context_data(*args, **kwargs)
|
||||
# return context
|
||||
|
||||
|
||||
class ProductDetailView(DetailView):
|
||||
queryset = Product.objects.all()
|
||||
template_name = "products/details.html"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
||||
cart_obj, new_cart = Cart.objects.new_or_get(self.request)
|
||||
context["cart"] = cart_obj
|
||||
return context
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
# request = self.request
|
||||
slug = self.kwargs.get('slug')
|
||||
inst = get_object_or_404(Product, slug=slug)
|
||||
# if inst is None:
|
||||
# raise Http404("Product doesn't exist")
|
||||
# return inst
|
||||
# try:
|
||||
# inst = Product.objects.get(slug=slug)
|
||||
#
|
||||
# except ObjectDoesNotExist:
|
||||
# raise Http404("not found...")
|
||||
# except Product.MultipleObjectsReturned:
|
||||
# inst = Product.objects.filter(slug=slug)[0]
|
||||
return inst
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
Django==1.11.4
|
||||
Pillow==5.2.0
|
||||
pytz==2018.5
|
Loading…
Reference in New Issue
Block a user