This commit is contained in:
Anna Sudnitsina 2018-09-10 23:16:35 +03:00
parent 6c7f355b52
commit 53b6fba4c2
83 changed files with 1541 additions and 0 deletions

0
accounts/__init__.py Normal file
View File

3
accounts/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
accounts/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

14
accounts/forms.py Normal file
View 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

View 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)),
],
),
]

View File

11
accounts/models.py Normal file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
accounts/urls.py Normal file
View 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
View 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
View File

4
addresses/admin.py Normal file
View File

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Address
admin.site.register(Address)

5
addresses/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AddressesConfig(AppConfig):
name = 'addresses'

8
addresses/forms.py Normal file
View File

@ -0,0 +1,8 @@
from django import forms
from .models import Address
class AddressForm(forms.ModelForm):
class Meta:
model = Address
exclude = ['billing_profile']

View 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')),
],
),
]

View File

27
addresses/models.py Normal file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
addresses/urls.py Normal file
View 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
View 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
View File

5
billing/admin.py Normal file
View 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
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class BillingConfig(AppConfig):
name = 'billing'

View 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)),
],
),
]

View 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),
),
]

View File

50
billing/models.py Normal file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
billing/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
carts/__init__.py Normal file
View File

5
carts/admin.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Cart
admin.site.register(Cart)

5
carts/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CartsConfig(AppConfig):
name = 'carts'

View 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)),
],
),
]

View 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),
),
]

View File

68
carts/models.py Normal file
View 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

View 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 %}

View 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 %}

View 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>

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
carts/urls.py Normal file
View 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
View 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

Binary file not shown.

0
ecommerce/__init__.py Normal file
View File

131
ecommerce/settings.py Normal file
View 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")

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View File

4
orders/admin.py Normal file
View File

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Order
admin.site.register(Order)

5
orders/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'orders'

View 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')),
],
),
]

View 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),
),
]

View 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'),
),
]

View 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'),
),
]

View 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',
),
]

View File

102
orders/models.py Normal file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
orders/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
products/__init__.py Normal file
View File

9
products/admin.py Normal file
View 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
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProductsConfig(AppConfig):
name = 'products'

View 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)),
],
),
]

View 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/'),
),
]

View 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/'),
),
]

View 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),
),
]

View 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),
),
]

View 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,
),
]

View File

44
products/models.py Normal file
View 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)

View 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 %}

View 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 %}

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
products/urls.py Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
Django==1.11.4
Pillow==5.2.0
pytz==2018.5