From 53b6fba4c2100c315ccaa052ca6d4fe1dbdec1b3 Mon Sep 17 00:00:00 2001 From: anya Date: Mon, 10 Sep 2018 23:16:35 +0300 Subject: [PATCH] initial --- accounts/__init__.py | 0 accounts/admin.py | 3 + accounts/apps.py | 5 + accounts/forms.py | 14 ++ accounts/migrations/0001_initial.py | 26 ++++ accounts/migrations/__init__.py | 0 accounts/models.py | 11 ++ accounts/tests.py | 3 + accounts/urls.py | 7 + accounts/views.py | 25 ++++ addresses/__init__.py | 0 addresses/admin.py | 4 + addresses/apps.py | 5 + addresses/forms.py | 8 ++ addresses/migrations/0001_initial.py | 31 +++++ addresses/migrations/__init__.py | 0 addresses/models.py | 27 ++++ addresses/tests.py | 3 + addresses/urls.py | 11 ++ addresses/views.py | 46 ++++++ billing/__init__.py | 0 billing/admin.py | 5 + billing/apps.py | 5 + billing/migrations/0001_initial.py | 30 ++++ billing/migrations/0002_auto_20180801_1339.py | 22 +++ billing/migrations/__init__.py | 0 billing/models.py | 50 +++++++ billing/tests.py | 3 + billing/views.py | 3 + carts/__init__.py | 0 carts/admin.py | 5 + carts/apps.py | 5 + carts/migrations/0001_initial.py | 31 +++++ carts/migrations/0002_cart_subtotal.py | 20 +++ carts/migrations/__init__.py | 0 carts/models.py | 68 +++++++++ carts/templates/carts/checkout-done.html | 8 ++ carts/templates/carts/checkout.html | 61 ++++++++ carts/templates/carts/form.html | 8 ++ carts/templates/carts/home.html | 35 +++++ carts/tests.py | 3 + carts/urls.py | 10 ++ carts/views.py | 71 ++++++++++ db.sqlite3 | Bin 0 -> 208896 bytes ecommerce/__init__.py | 0 ecommerce/settings.py | 131 ++++++++++++++++++ ecommerce/templates/base.html | 26 ++++ ecommerce/templates/base/nav.html | 50 +++++++ ecommerce/templates/bootstrap/example.html | 22 +++ ecommerce/urls.py | 34 +++++ ecommerce/utils.py | 43 ++++++ ecommerce/wsgi.py | 16 +++ manage.py | 22 +++ orders/__init__.py | 0 orders/admin.py | 4 + orders/apps.py | 5 + orders/migrations/0001_initial.py | 29 ++++ orders/migrations/0002_auto_20180801_1332.py | 20 +++ orders/migrations/0003_auto_20180802_0823.py | 27 ++++ .../migrations/0004_order_shipping_address.py | 22 +++ orders/migrations/0005_auto_20180802_1436.py | 20 +++ orders/migrations/__init__.py | 0 orders/models.py | 102 ++++++++++++++ orders/tests.py | 3 + orders/views.py | 3 + products/__init__.py | 0 products/admin.py | 9 ++ products/apps.py | 5 + products/migrations/0001_initial.py | 25 ++++ products/migrations/0002_product_image.py | 20 +++ .../migrations/0003_auto_20180729_0956.py | 20 +++ products/migrations/0004_product_slug.py | 20 +++ .../migrations/0005_auto_20180729_1104.py | 20 +++ products/migrations/0006_product_time.py | 22 +++ products/migrations/__init__.py | 0 products/models.py | 44 ++++++ products/templates/products/details.html | 18 +++ products/templates/products/list.html | 19 +++ .../templates/products/snippets/card.html | 10 ++ products/tests.py | 3 + products/urls.py | 7 + products/views.py | 45 ++++++ requirements.txt | 3 + 83 files changed, 1541 insertions(+) create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/forms.py create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/tests.py create mode 100644 accounts/urls.py create mode 100644 accounts/views.py create mode 100644 addresses/__init__.py create mode 100644 addresses/admin.py create mode 100644 addresses/apps.py create mode 100644 addresses/forms.py create mode 100644 addresses/migrations/0001_initial.py create mode 100644 addresses/migrations/__init__.py create mode 100644 addresses/models.py create mode 100644 addresses/tests.py create mode 100644 addresses/urls.py create mode 100644 addresses/views.py create mode 100644 billing/__init__.py create mode 100644 billing/admin.py create mode 100644 billing/apps.py create mode 100644 billing/migrations/0001_initial.py create mode 100644 billing/migrations/0002_auto_20180801_1339.py create mode 100644 billing/migrations/__init__.py create mode 100644 billing/models.py create mode 100644 billing/tests.py create mode 100644 billing/views.py create mode 100644 carts/__init__.py create mode 100644 carts/admin.py create mode 100644 carts/apps.py create mode 100644 carts/migrations/0001_initial.py create mode 100644 carts/migrations/0002_cart_subtotal.py create mode 100644 carts/migrations/__init__.py create mode 100644 carts/models.py create mode 100644 carts/templates/carts/checkout-done.html create mode 100644 carts/templates/carts/checkout.html create mode 100644 carts/templates/carts/form.html create mode 100644 carts/templates/carts/home.html create mode 100644 carts/tests.py create mode 100644 carts/urls.py create mode 100644 carts/views.py create mode 100644 db.sqlite3 create mode 100644 ecommerce/__init__.py create mode 100644 ecommerce/settings.py create mode 100644 ecommerce/templates/base.html create mode 100644 ecommerce/templates/base/nav.html create mode 100644 ecommerce/templates/bootstrap/example.html create mode 100644 ecommerce/urls.py create mode 100644 ecommerce/utils.py create mode 100644 ecommerce/wsgi.py create mode 100755 manage.py create mode 100644 orders/__init__.py create mode 100644 orders/admin.py create mode 100644 orders/apps.py create mode 100644 orders/migrations/0001_initial.py create mode 100644 orders/migrations/0002_auto_20180801_1332.py create mode 100644 orders/migrations/0003_auto_20180802_0823.py create mode 100644 orders/migrations/0004_order_shipping_address.py create mode 100644 orders/migrations/0005_auto_20180802_1436.py create mode 100644 orders/migrations/__init__.py create mode 100644 orders/models.py create mode 100644 orders/tests.py create mode 100644 orders/views.py create mode 100644 products/__init__.py create mode 100644 products/admin.py create mode 100644 products/apps.py create mode 100644 products/migrations/0001_initial.py create mode 100644 products/migrations/0002_product_image.py create mode 100644 products/migrations/0003_auto_20180729_0956.py create mode 100644 products/migrations/0004_product_slug.py create mode 100644 products/migrations/0005_auto_20180729_1104.py create mode 100644 products/migrations/0006_product_time.py create mode 100644 products/migrations/__init__.py create mode 100644 products/models.py create mode 100644 products/templates/products/details.html create mode 100644 products/templates/products/list.html create mode 100644 products/templates/products/snippets/card.html create mode 100644 products/tests.py create mode 100644 products/urls.py create mode 100644 products/views.py create mode 100644 requirements.txt diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..41201c6 --- /dev/null +++ b/accounts/forms.py @@ -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 diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..aa2ca68 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..505ddbe --- /dev/null +++ b/accounts/models.py @@ -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 diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..d6af201 --- /dev/null +++ b/accounts/urls.py @@ -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"), +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..1ecdfd2 --- /dev/null +++ b/accounts/views.py @@ -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 + diff --git a/addresses/__init__.py b/addresses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/addresses/admin.py b/addresses/admin.py new file mode 100644 index 0000000..98cf5a2 --- /dev/null +++ b/addresses/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Address + +admin.site.register(Address) diff --git a/addresses/apps.py b/addresses/apps.py new file mode 100644 index 0000000..7582c6e --- /dev/null +++ b/addresses/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AddressesConfig(AppConfig): + name = 'addresses' diff --git a/addresses/forms.py b/addresses/forms.py new file mode 100644 index 0000000..6c59885 --- /dev/null +++ b/addresses/forms.py @@ -0,0 +1,8 @@ +from django import forms + +from .models import Address + +class AddressForm(forms.ModelForm): + class Meta: + model = Address + exclude = ['billing_profile'] \ No newline at end of file diff --git a/addresses/migrations/0001_initial.py b/addresses/migrations/0001_initial.py new file mode 100644 index 0000000..5de7d78 --- /dev/null +++ b/addresses/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/addresses/migrations/__init__.py b/addresses/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/addresses/models.py b/addresses/models.py new file mode 100644 index 0000000..81f908e --- /dev/null +++ b/addresses/models.py @@ -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 + ) + diff --git a/addresses/tests.py b/addresses/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/addresses/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/addresses/urls.py b/addresses/urls.py new file mode 100644 index 0000000..ca9c65b --- /dev/null +++ b/addresses/urls.py @@ -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"), +] \ No newline at end of file diff --git a/addresses/views.py b/addresses/views.py new file mode 100644 index 0000000..f078d8a --- /dev/null +++ b/addresses/views.py @@ -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") +""" \ No newline at end of file diff --git a/billing/__init__.py b/billing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/billing/admin.py b/billing/admin.py new file mode 100644 index 0000000..a72c3b1 --- /dev/null +++ b/billing/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import BillingProfile + +admin.site.register(BillingProfile) +# Register your models here. diff --git a/billing/apps.py b/billing/apps.py new file mode 100644 index 0000000..21d70f8 --- /dev/null +++ b/billing/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BillingConfig(AppConfig): + name = 'billing' diff --git a/billing/migrations/0001_initial.py b/billing/migrations/0001_initial.py new file mode 100644 index 0000000..e5a8c4f --- /dev/null +++ b/billing/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/billing/migrations/0002_auto_20180801_1339.py b/billing/migrations/0002_auto_20180801_1339.py new file mode 100644 index 0000000..ac52d06 --- /dev/null +++ b/billing/migrations/0002_auto_20180801_1339.py @@ -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), + ), + ] diff --git a/billing/migrations/__init__.py b/billing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/billing/models.py b/billing/models.py new file mode 100644 index 0000000..84a4227 --- /dev/null +++ b/billing/models.py @@ -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) + diff --git a/billing/tests.py b/billing/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/billing/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/billing/views.py b/billing/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/billing/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/carts/__init__.py b/carts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/carts/admin.py b/carts/admin.py new file mode 100644 index 0000000..5933914 --- /dev/null +++ b/carts/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import Cart + +admin.site.register(Cart) diff --git a/carts/apps.py b/carts/apps.py new file mode 100644 index 0000000..42ca295 --- /dev/null +++ b/carts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CartsConfig(AppConfig): + name = 'carts' diff --git a/carts/migrations/0001_initial.py b/carts/migrations/0001_initial.py new file mode 100644 index 0000000..57e9ea5 --- /dev/null +++ b/carts/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/carts/migrations/0002_cart_subtotal.py b/carts/migrations/0002_cart_subtotal.py new file mode 100644 index 0000000..23588ec --- /dev/null +++ b/carts/migrations/0002_cart_subtotal.py @@ -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), + ), + ] diff --git a/carts/migrations/__init__.py b/carts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/carts/models.py b/carts/models.py new file mode 100644 index 0000000..85e86d2 --- /dev/null +++ b/carts/models.py @@ -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 + + diff --git a/carts/templates/carts/checkout-done.html b/carts/templates/carts/checkout-done.html new file mode 100644 index 0000000..3566a32 --- /dev/null +++ b/carts/templates/carts/checkout-done.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+

Tank you for your order!

+
+ +{% endblock %} \ No newline at end of file diff --git a/carts/templates/carts/checkout.html b/carts/templates/carts/checkout.html new file mode 100644 index 0000000..eb0b068 --- /dev/null +++ b/carts/templates/carts/checkout.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} + +{% block content %} + +{{ object }} -- {{ object.cart }} + +{% if not billing_profile %} +
+
+

Login

+
+
+ Continue as guest +
+ {% csrf_token %} + {{ guest_form.as_p }} +
+
+
+{% else %} + + {% if not object.address %} +
+
+

Adress

+
+
+
+
{% csrf_token %} + {% for addr in address_qs %} +
+ {% endfor %} + +
+
+ {% csrf_token %} + {{ address_form.as_p }} + + +
+
+
+
+ {% else %} + +

Finalize checkout

+

Shipping Address: {{ object.address.get_address}}

+

Cart items: {% for item in object.cart.products.all %}{{ item }}{% if not forloop.last %}, {% endif %}{% endfor %}

+ +

Cart total: {{ object.cart.total }}

+

Shipping Total: {{ object.shipping_total }}

+

Order Total: {{ object.total }}

+
{% csrf_token %} + +
+{% endif %} +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/carts/templates/carts/form.html b/carts/templates/carts/form.html new file mode 100644 index 0000000..00bbb9c --- /dev/null +++ b/carts/templates/carts/form.html @@ -0,0 +1,8 @@ +
{% csrf_token %} + + {% if object in cart.products.all %} + + {% else %} + + {% endif %} +
\ No newline at end of file diff --git a/carts/templates/carts/home.html b/carts/templates/carts/home.html new file mode 100644 index 0000000..8222b1f --- /dev/null +++ b/carts/templates/carts/home.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

Cart

+ + + + + + + + + + {% for product in cart.products.all %} + + + + + + {% endfor %} + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/carts/tests.py b/carts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/carts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/carts/urls.py b/carts/urls.py new file mode 100644 index 0000000..6a6548c --- /dev/null +++ b/carts/urls.py @@ -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") +] diff --git a/carts/views.py b/carts/views.py new file mode 100644 index 0000000..3322934 --- /dev/null +++ b/carts/views.py @@ -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") \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..88220fbec81791fe5b80ca40e9caab2de1ea1216 GIT binary patch literal 208896 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 + + + + + + + + + + + + + {% include 'base/nav.html' with brand_name='Brand name'%} +
+ {% block content %}{% endblock %} +
+ + + + + + + \ No newline at end of file diff --git a/ecommerce/templates/base/nav.html b/ecommerce/templates/base/nav.html new file mode 100644 index 0000000..3eb44b3 --- /dev/null +++ b/ecommerce/templates/base/nav.html @@ -0,0 +1,50 @@ +{% url 'products:list' as prod_list %} +{% url 'carts:home' as cart_url %} + +
\ No newline at end of file diff --git a/ecommerce/templates/bootstrap/example.html b/ecommerce/templates/bootstrap/example.html new file mode 100644 index 0000000..8aacf07 --- /dev/null +++ b/ecommerce/templates/bootstrap/example.html @@ -0,0 +1,22 @@ + + + + + + + + + + + Hello, world! + + +

Hello, world!

+ + + + + + + + \ No newline at end of file diff --git a/ecommerce/urls.py b/ecommerce/urls.py new file mode 100644 index 0000000..c35c5bc --- /dev/null +++ b/ecommerce/urls.py @@ -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) \ No newline at end of file diff --git a/ecommerce/utils.py b/ecommerce/utils.py new file mode 100644 index 0000000..b262ed4 --- /dev/null +++ b/ecommerce/utils.py @@ -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 \ No newline at end of file diff --git a/ecommerce/wsgi.py b/ecommerce/wsgi.py new file mode 100644 index 0000000..ccf146c --- /dev/null +++ b/ecommerce/wsgi.py @@ -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() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..561bf0f --- /dev/null +++ b/manage.py @@ -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) diff --git a/orders/__init__.py b/orders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orders/admin.py b/orders/admin.py new file mode 100644 index 0000000..6b1c02d --- /dev/null +++ b/orders/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Order + +admin.site.register(Order) diff --git a/orders/apps.py b/orders/apps.py new file mode 100644 index 0000000..384ab43 --- /dev/null +++ b/orders/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrdersConfig(AppConfig): + name = 'orders' diff --git a/orders/migrations/0001_initial.py b/orders/migrations/0001_initial.py new file mode 100644 index 0000000..097d16c --- /dev/null +++ b/orders/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/orders/migrations/0002_auto_20180801_1332.py b/orders/migrations/0002_auto_20180801_1332.py new file mode 100644 index 0000000..fc76e94 --- /dev/null +++ b/orders/migrations/0002_auto_20180801_1332.py @@ -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), + ), + ] diff --git a/orders/migrations/0003_auto_20180802_0823.py b/orders/migrations/0003_auto_20180802_0823.py new file mode 100644 index 0000000..a1ef2f1 --- /dev/null +++ b/orders/migrations/0003_auto_20180802_0823.py @@ -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'), + ), + ] diff --git a/orders/migrations/0004_order_shipping_address.py b/orders/migrations/0004_order_shipping_address.py new file mode 100644 index 0000000..d57c20f --- /dev/null +++ b/orders/migrations/0004_order_shipping_address.py @@ -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'), + ), + ] diff --git a/orders/migrations/0005_auto_20180802_1436.py b/orders/migrations/0005_auto_20180802_1436.py new file mode 100644 index 0000000..6f9a41b --- /dev/null +++ b/orders/migrations/0005_auto_20180802_1436.py @@ -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', + ), + ] diff --git a/orders/migrations/__init__.py b/orders/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orders/models.py b/orders/models.py new file mode 100644 index 0000000..4a59331 --- /dev/null +++ b/orders/models.py @@ -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() + + diff --git a/orders/tests.py b/orders/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/orders/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/orders/views.py b/orders/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/orders/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/products/__init__.py b/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/admin.py b/products/admin.py new file mode 100644 index 0000000..7f7988f --- /dev/null +++ b/products/admin.py @@ -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',) diff --git a/products/apps.py b/products/apps.py new file mode 100644 index 0000000..864c43e --- /dev/null +++ b/products/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + name = 'products' diff --git a/products/migrations/0001_initial.py b/products/migrations/0001_initial.py new file mode 100644 index 0000000..3f467ae --- /dev/null +++ b/products/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/products/migrations/0002_product_image.py b/products/migrations/0002_product_image.py new file mode 100644 index 0000000..0b6537c --- /dev/null +++ b/products/migrations/0002_product_image.py @@ -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/'), + ), + ] diff --git a/products/migrations/0003_auto_20180729_0956.py b/products/migrations/0003_auto_20180729_0956.py new file mode 100644 index 0000000..d5137d5 --- /dev/null +++ b/products/migrations/0003_auto_20180729_0956.py @@ -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/'), + ), + ] diff --git a/products/migrations/0004_product_slug.py b/products/migrations/0004_product_slug.py new file mode 100644 index 0000000..c05e806 --- /dev/null +++ b/products/migrations/0004_product_slug.py @@ -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), + ), + ] diff --git a/products/migrations/0005_auto_20180729_1104.py b/products/migrations/0005_auto_20180729_1104.py new file mode 100644 index 0000000..1d869ca --- /dev/null +++ b/products/migrations/0005_auto_20180729_1104.py @@ -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), + ), + ] diff --git a/products/migrations/0006_product_time.py b/products/migrations/0006_product_time.py new file mode 100644 index 0000000..828cbab --- /dev/null +++ b/products/migrations/0006_product_time.py @@ -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, + ), + ] diff --git a/products/migrations/__init__.py b/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/models.py b/products/models.py new file mode 100644 index 0000000..a66ad5d --- /dev/null +++ b/products/models.py @@ -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 ''.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) \ No newline at end of file diff --git a/products/templates/products/details.html b/products/templates/products/details.html new file mode 100644 index 0000000..6f0b3d9 --- /dev/null +++ b/products/templates/products/details.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {{ object.title }}
+ {{ object.time|timesince}} ago + {{ object.description|linebreaks }}
+ {% if object.image %} + + {% endif %} +
+
+ {{ cart }} + {% include "carts/form.html" %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/products/templates/products/list.html b/products/templates/products/list.html new file mode 100644 index 0000000..a317130 --- /dev/null +++ b/products/templates/products/list.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block content %} + +
+ {% for obj in object_list %} +
+ {{ forloop.counter }} + {% include 'products/snippets/card.html' with instance=obj %} + {% if forloop.counter|divisibleby:2 %} +
+ {% else %} +
+ {% endif %} + {% endfor %} + + + +{% endblock %} \ No newline at end of file diff --git a/products/templates/products/snippets/card.html b/products/templates/products/snippets/card.html new file mode 100644 index 0000000..f4fcd81 --- /dev/null +++ b/products/templates/products/snippets/card.html @@ -0,0 +1,10 @@ +
+ {% if instance.image %} + {{ instance.title}} logo + {% endif%} +
+
{{ instance.title}}
+

{{ instance.description|linebreaks|truncatewords:20 }}

+ View +
+
\ No newline at end of file diff --git a/products/tests.py b/products/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/products/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/products/urls.py b/products/urls.py new file mode 100644 index 0000000..2913136 --- /dev/null +++ b/products/urls.py @@ -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[\w-]+)/?$', ProductDetailView.as_view(), name='details') +] \ No newline at end of file diff --git a/products/views.py b/products/views.py new file mode 100644 index 0000000..16b02ea --- /dev/null +++ b/products/views.py @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..89f43fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Django==1.11.4 +Pillow==5.2.0 +pytz==2018.5
#Product NamePrice
{{ forloop.counter }}{{ product.title }}{% include "carts/form.html" with object=product in_cart=True %} + {{ product.price }}
Subtotal {{ cart.subtotal }}
Total {{ cart.total }}
Checkout