├── basket ├── __init__.py ├── tests │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ └── test_views.cpython-39.pyc │ └── test_views.py ├── migrations │ ├── __init__.py │ └── __pycache__ │ │ └── __init__.cpython-39.pyc ├── apps.py ├── context_processors.py ├── __pycache__ │ ├── urls.cpython-39.pyc │ ├── basket.cpython-39.pyc │ ├── models.cpython-39.pyc │ ├── views.cpython-39.pyc │ ├── __init__.cpython-39.pyc │ └── context_processors.cpython-39.pyc ├── urls.py ├── views.py └── basket.py ├── core ├── __init__.py ├── db.sqlite3 ├── settings │ ├── __init__.py │ ├── __pycache__ │ │ ├── base.cpython-39.pyc │ │ ├── core.cpython-39.pyc │ │ ├── __init__.cpython-39.pyc │ │ └── dev_debug.cpython-39.pyc │ ├── dev_debug.py │ └── base.py ├── __pycache__ │ ├── urls.cpython-39.pyc │ ├── wsgi.cpython-39.pyc │ ├── __init__.cpython-39.pyc │ └── settings.cpython-39.pyc ├── asgi.py ├── wsgi.py └── urls.py ├── orders ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── 0001_initial.cpython-39.pyc │ │ └── 0002_remove_order_email.cpython-39.pyc │ └── 0001_initial.py ├── tests.py ├── apps.py ├── __pycache__ │ ├── urls.cpython-39.pyc │ ├── admin.cpython-39.pyc │ ├── models.cpython-39.pyc │ ├── tests.cpython-39.pyc │ ├── views.cpython-39.pyc │ └── __init__.cpython-39.pyc ├── admin.py ├── urls.py ├── views.py └── models.py ├── payment ├── tests.py ├── __init__.py ├── migrations │ ├── __init__.py │ └── __pycache__ │ │ └── __init__.cpython-39.pyc ├── models.py ├── admin.py ├── apps.py ├── __pycache__ │ ├── tests.cpython-39.pyc │ ├── urls.cpython-39.pyc │ ├── views.cpython-39.pyc │ └── __init__.cpython-39.pyc ├── urls.py └── views.py ├── store ├── __init__.py ├── tests │ ├── __init__.py │ ├── __pycache__ │ │ ├── tests.cpython-39.pyc │ │ ├── __init__.cpython-39.pyc │ │ ├── test_models.cpython-39.pyc │ │ └── test_views.cpython-39.pyc │ ├── test_models.py │ └── test_views.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── 0001_initial.cpython-39.pyc │ │ ├── 0002_product_is_active.cpython-39.pyc │ │ ├── 0003_imagealbum_name.cpython-39.pyc │ │ ├── 0002_auto_20201221_1612.cpython-39.pyc │ │ ├── 0002_auto_20210130_0004.cpython-39.pyc │ │ ├── 0003_auto_20201223_1409.cpython-39.pyc │ │ ├── 0004_auto_20201224_0015.cpython-39.pyc │ │ └── 0004_auto_20210129_1731.cpython-39.pyc │ └── 0001_initial.py ├── apps.py ├── __pycache__ │ ├── admin.cpython-39.pyc │ ├── urls.cpython-39.pyc │ ├── views.cpython-39.pyc │ ├── __init__.cpython-39.pyc │ ├── models.cpython-39.pyc │ └── context_processors.cpython-39.pyc ├── context_processors.py ├── urls.py ├── views.py ├── admin.py └── models.py ├── account ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── 0001_initial.cpython-39.pyc │ │ └── 0002_userbase_is_staff.cpython-39.pyc │ └── 0001_initial.py ├── tests.py ├── apps.py ├── admin.py ├── __pycache__ │ ├── admin.cpython-39.pyc │ ├── forms.cpython-39.pyc │ ├── models.cpython-39.pyc │ ├── tests.cpython-39.pyc │ ├── tokens.cpython-39.pyc │ ├── urls.cpython-39.pyc │ ├── views.cpython-39.pyc │ └── __init__.cpython-39.pyc ├── templatetags │ ├── __pycache__ │ │ └── example.cpython-39.pyc │ └── example.py ├── tokens.py ├── urls.py ├── models.py ├── views.py └── forms.py ├── static ├── basket │ └── css │ │ └── basket.css ├── store │ └── css │ │ └── store.css ├── logo.png ├── account │ └── css │ │ └── account.css ├── payment │ ├── css │ │ └── payment.css │ └── index.js └── core │ └── css │ └── base.css ├── .gitattributes ├── db.sqlite3 ├── _documentation ├── stripe.txt ├── commands.txt ├── packages.txt ├── vscode_settings.txt └── database_schema.txt ├── media └── images │ ├── default.png │ ├── img1_DCytNvp.png │ ├── img2_SUlRxwK.png │ ├── default_7j3pLDD.png │ ├── default_AN2mWQr.png │ └── img1_DCytNvp_NlI9SCz.png ├── templates ├── two_factor │ ├── twilio │ │ └── sms_message.html │ ├── _wizard_forms.html │ ├── _base_focus.html │ ├── _base.html │ ├── _wizard_actions.html │ ├── profile │ │ ├── disable.html │ │ └── profile.html │ └── core │ │ ├── otp_required.html │ │ ├── phone_register.html │ │ ├── setup_complete.html │ │ ├── backup_tokens.html │ │ ├── login.html │ │ └── setup.html ├── account │ ├── registration │ │ ├── account_activation_email.html │ │ ├── activation_invalid.html │ │ ├── register_email_confirm.html │ │ └── register.html │ ├── dashboard │ │ ├── delete_confirm.html │ │ ├── edit_addresses.html │ │ ├── edit_details.html │ │ ├── addresses.html │ │ └── dashboard.html │ ├── sub_base.html │ ├── password_reset │ │ ├── reset_status.html │ │ ├── password_reset_email.html │ │ ├── password_reset_form.html │ │ └── password_reset_confirm.html │ └── login.html ├── payment │ ├── error.html │ ├── orderplaced.html │ ├── sub_base.html │ └── payment_form.html ├── store │ ├── category.html │ ├── index.html │ └── single.html ├── basket │ └── summary.html └── base.html ├── requirements.txt ├── README.md ├── .vscode └── settings.json ├── manage.py └── LICENSE /basket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/db.sqlite3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /basket/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/basket/css/basket.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /basket/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orders/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /orders/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /payment/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /payment/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /static/store/css/store.css: -------------------------------------------------------------------------------- 1 | .store-select-dropdown{ 2 | width:50px; 3 | height:40px; 4 | } -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /_documentation/stripe.txt: -------------------------------------------------------------------------------- 1 | No auth: 4242424242424242 2 | Auth: 4000002500003155 3 | Error: 4000000000009995 -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/static/logo.png -------------------------------------------------------------------------------- /store/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StoreConfig(AppConfig): 5 | name = 'store' 6 | -------------------------------------------------------------------------------- /basket/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BasketConfig(AppConfig): 5 | name = 'basket' 6 | -------------------------------------------------------------------------------- /orders/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrdersConfig(AppConfig): 5 | name = 'orders' 6 | -------------------------------------------------------------------------------- /account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = 'account' 6 | -------------------------------------------------------------------------------- /media/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/default.png -------------------------------------------------------------------------------- /payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | name = 'payment' 6 | -------------------------------------------------------------------------------- /account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Customer 4 | 5 | admin.site.register(Customer) 6 | -------------------------------------------------------------------------------- /basket/context_processors.py: -------------------------------------------------------------------------------- 1 | from .basket import Basket 2 | 3 | 4 | def basket(request): 5 | return {'basket': Basket(request)} 6 | -------------------------------------------------------------------------------- /media/images/img1_DCytNvp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/img1_DCytNvp.png -------------------------------------------------------------------------------- /media/images/img2_SUlRxwK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/img2_SUlRxwK.png -------------------------------------------------------------------------------- /media/images/default_7j3pLDD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/default_7j3pLDD.png -------------------------------------------------------------------------------- /media/images/default_AN2mWQr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/default_AN2mWQr.png -------------------------------------------------------------------------------- /_documentation/commands.txt: -------------------------------------------------------------------------------- 1 | # View all migrations 2 | py manage.py makemigrations --check 3 | 4 | # Migrations 5 | python manage.py showmigrations -------------------------------------------------------------------------------- /core/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /core/__pycache__/wsgi.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/__pycache__/wsgi.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /media/images/img1_DCytNvp_NlI9SCz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/media/images/img1_DCytNvp_NlI9SCz.png -------------------------------------------------------------------------------- /orders/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /_documentation/packages.txt: -------------------------------------------------------------------------------- 1 | Package List 2 | #### 3 | 4 | django 5 | black 6 | isort 7 | django-mptt 8 | pillow 9 | stripe 10 | coverage 11 | six -------------------------------------------------------------------------------- /account/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/forms.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/forms.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/tests.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/tests.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/tokens.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/tokens.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/basket.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/basket.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /core/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /core/__pycache__/settings.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/__pycache__/settings.cpython-39.pyc -------------------------------------------------------------------------------- /orders/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /orders/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /orders/__pycache__/tests.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/tests.cpython-39.pyc -------------------------------------------------------------------------------- /orders/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /payment/__pycache__/tests.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/payment/__pycache__/tests.cpython-39.pyc -------------------------------------------------------------------------------- /payment/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/payment/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /payment/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/payment/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /account/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /orders/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /payment/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/payment/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /store/context_processors.py: -------------------------------------------------------------------------------- 1 | from .models import Category 2 | 3 | 4 | def categories(request): 5 | return {"categories": Category.objects.filter(level=0)} 6 | -------------------------------------------------------------------------------- /templates/two_factor/twilio/sms_message.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans trimmed %} 3 | Your OTP token is {{ token }} 4 | {% endblocktrans %} 5 | 6 | -------------------------------------------------------------------------------- /core/settings/__pycache__/base.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/settings/__pycache__/base.cpython-39.pyc -------------------------------------------------------------------------------- /core/settings/__pycache__/core.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/settings/__pycache__/core.cpython-39.pyc -------------------------------------------------------------------------------- /orders/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Order, OrderItem 4 | 5 | admin.site.register(Order) 6 | admin.site.register(OrderItem) -------------------------------------------------------------------------------- /store/tests/__pycache__/tests.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/tests/__pycache__/tests.cpython-39.pyc -------------------------------------------------------------------------------- /basket/tests/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/tests/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /store/tests/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/tests/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /basket/tests/__pycache__/test_views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/tests/__pycache__/test_views.cpython-39.pyc -------------------------------------------------------------------------------- /core/settings/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/settings/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /core/settings/__pycache__/dev_debug.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/core/settings/__pycache__/dev_debug.cpython-39.pyc -------------------------------------------------------------------------------- /store/__pycache__/context_processors.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/__pycache__/context_processors.cpython-39.pyc -------------------------------------------------------------------------------- /store/tests/__pycache__/test_models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/tests/__pycache__/test_models.cpython-39.pyc -------------------------------------------------------------------------------- /store/tests/__pycache__/test_views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/tests/__pycache__/test_views.cpython-39.pyc -------------------------------------------------------------------------------- /basket/__pycache__/context_processors.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/__pycache__/context_processors.cpython-39.pyc -------------------------------------------------------------------------------- /basket/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/basket/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /orders/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /account/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /account/templatetags/__pycache__/example.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/templatetags/__pycache__/example.cpython-39.pyc -------------------------------------------------------------------------------- /payment/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/payment/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /account/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /orders/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /orders/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'orders' 6 | 7 | urlpatterns = [ 8 | path('add/', views.add, name='add'), 9 | ] 10 | -------------------------------------------------------------------------------- /store/migrations/__pycache__/0002_product_is_active.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0002_product_is_active.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0003_imagealbum_name.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0003_imagealbum_name.cpython-39.pyc -------------------------------------------------------------------------------- /account/migrations/__pycache__/0002_userbase_is_staff.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/account/migrations/__pycache__/0002_userbase_is_staff.cpython-39.pyc -------------------------------------------------------------------------------- /orders/migrations/__pycache__/0002_remove_order_email.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/orders/migrations/__pycache__/0002_remove_order_email.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0002_auto_20201221_1612.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0002_auto_20201221_1612.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0002_auto_20210130_0004.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0002_auto_20210130_0004.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0003_auto_20201223_1409.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0003_auto_20201223_1409.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0004_auto_20201224_0015.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0004_auto_20201224_0015.cpython-39.pyc -------------------------------------------------------------------------------- /store/migrations/__pycache__/0004_auto_20210129_1731.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryacademy/YT_Django_Two_Factor_Example/HEAD/store/migrations/__pycache__/0004_auto_20210129_1731.cpython-39.pyc -------------------------------------------------------------------------------- /static/account/css/account.css: -------------------------------------------------------------------------------- 1 | .account-form input { 2 | border: 2px solid #ccc; 3 | height: calc(2em + .75rem + 2px); 4 | } 5 | 6 | .account-form input:focus { 7 | border-color: #1497ff; 8 | box-shadow: none; 9 | } -------------------------------------------------------------------------------- /templates/two_factor/_wizard_forms.html: -------------------------------------------------------------------------------- 1 | {% load example %} 2 | {{ wizard.management_form }} 3 | 4 | {% for field in wizard.form %} 5 | 6 | {{field | add_classes:'form-control mb-2 account-form'}} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /templates/account/registration/account_activation_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Hi {{ user.user_name }}, 3 | 4 | Your account has successfully created. Please click below link to activate your account 5 | 6 | http://{{ domain }}{% url 'account:activate' uidb64=uid token=token %} 7 | {% endautoescape %} -------------------------------------------------------------------------------- /templates/two_factor/_base_focus.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base.html" %} 2 | 3 | {% block content_wrapper %} 4 |
5 |
6 |
7 | {% block content %}{% endblock %} 8 |
9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /static/payment/css/payment.css: -------------------------------------------------------------------------------- 1 | .account-form input{ 2 | border: 2px solid #ccc; 3 | height: calc(2em + .75rem + 2px); 4 | } 5 | 6 | .form-control { 7 | border: 2px solid #ccc; 8 | } 9 | 10 | .account-form input:focus { 11 | border-color: #1497ff; 12 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0.075), 0 0 0px rgba(255, 0, 0, 0.6); 13 | } -------------------------------------------------------------------------------- /store/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'store' 6 | 7 | urlpatterns = [ 8 | path('', views.product_all, name='store_home'), 9 | path('', views.product_detail, name='product_detail'), 10 | path('shop//', views.category_list, name='category_list'), 11 | ] 12 | -------------------------------------------------------------------------------- /templates/account/registration/activation_invalid.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Invalide Activation{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

Error

8 |

Please contact our support team

9 |
10 |
11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/dashboard/delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Delete Account Confirmation{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

Delete Account

8 |

You have now deleted your account

9 |
10 |
11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /payment/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'payment' 6 | 7 | urlpatterns = [ 8 | path('', views.BasketView, name='basket'), 9 | path('orderplaced/', views.order_placed, name='order_placed'), 10 | path('error/', views.Error.as_view(), name='error'), 11 | path('webhook/', views.stripe_webhook), 12 | ] 13 | -------------------------------------------------------------------------------- /templates/two_factor/_base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | 3 | {% block content %} 4 |
5 | 10 |
11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /templates/account/registration/register_email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Account Email Sent{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

Account email sent

8 |

Check your email to activate your account

9 |
10 |
11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /basket/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'basket' 6 | 7 | urlpatterns = [ 8 | path('', views.basket_summary, name='basket_summary'), 9 | path('add/', views.basket_add, name='basket_add'), 10 | path('delete/', views.basket_delete, name='basket_delete'), 11 | path('update/', views.basket_update, name='basket_update'), 12 | ] 13 | -------------------------------------------------------------------------------- /templates/payment/error.html: -------------------------------------------------------------------------------- 1 | {% extends "./sub_base.html" %} 2 | {% block title %}Error{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

Order Error

8 |

Please contact our customer support team

9 | Dashboard 10 |
11 |
12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /templates/payment/orderplaced.html: -------------------------------------------------------------------------------- 1 | {% extends "./sub_base.html" %} 2 | {% block title %}Order Placed{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

Order has been placed

8 |

Check your email for confirmation

9 | See orders 10 |
11 |
12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/sub_base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% load static %} 3 | {% block stylesheet %}{% static 'account/css/account.css' %}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /templates/payment/sub_base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% load static %} 3 | {% block stylesheet %}{% static 'payment/css/payment.css' %}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /account/tokens.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 2 | from six import text_type 3 | 4 | 5 | class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 6 | def _make_hash_value(self, user, timestamp): 7 | return ( 8 | text_type(user.pk) + text_type(timestamp) + 9 | text_type(user.is_active) 10 | ) 11 | 12 | 13 | account_activation_token = AccountActivationTokenGenerator() -------------------------------------------------------------------------------- /core/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for core project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | asgiref==3.3.1 3 | black==20.8b1 4 | certifi==2020.12.5 5 | chardet==4.0.0 6 | click==7.1.2 7 | coverage==5.4 8 | Django==3.1.7 9 | django-js-asset==1.2.2 10 | django-mptt==0.11.0 11 | idna==2.10 12 | isort==5.7.0 13 | mypy-extensions==0.4.3 14 | pathspec==0.8.1 15 | Pillow==8.1.0 16 | pytz==2021.1 17 | regex==2020.11.13 18 | requests==2.25.1 19 | six==1.15.0 20 | sqlparse==0.4.1 21 | stripe==2.56.0 22 | toml==0.10.2 23 | typed-ast==1.4.2 24 | typing-extensions==3.7.4.3 25 | urllib3==1.26.3 26 | django-debug-toolbar 27 | -------------------------------------------------------------------------------- /account/templatetags/example.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name="add_classes") 7 | def add_classes(value, arg): 8 | 9 | classes = value.field.widget.attrs.get("class", "") 10 | 11 | if classes: 12 | classes = classes.split(" ") 13 | else: 14 | classes = [] 15 | 16 | new_classes = arg.split(" ") 17 | for c in new_classes: 18 | if c not in classes: 19 | classes.append(c) 20 | 21 | return value.as_widget(attrs={"class": " ".join(classes)}) 22 | -------------------------------------------------------------------------------- /templates/two_factor/_wizard_actions.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if cancel_url %} 4 | {% trans "Cancel" %} 6 | {% endif %} 7 | {% if wizard.steps.prev %} 8 | 11 | {% else %} 12 | 13 | {% endif %} 14 | 15 | -------------------------------------------------------------------------------- /templates/two_factor/profile/disable.html: -------------------------------------------------------------------------------- 1 | {% extends "../_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block sub_content %} 5 |

{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}

6 |

{% blocktrans trimmed %}You are about to disable two-factor authentication. This 7 | weakens your account security, are you sure?{% endblocktrans %}

8 |
9 | {% csrf_token %} 10 | {{ form }}
11 | 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YT_Django_Project_Ecommerce_v1_Part3 2 | 3 | ## Instructions 4 | 5 | 1. Download 6 | 2. Extract in a folder 7 | 3. Open with visual studio code 8 | 9 | Commands: 10 | 11 | py -m venv venv 12 | venv\Scripts\activate 13 | pip install -r requirements.txt 14 | py manage.py runserver 15 | 16 | 17 | In core /settings.py the stripe is commented out - just put your own details in here (not all of these are connected to the project) 18 | 19 | # Stripe Payment 20 | PUBLISHABLE_KEY = '' 21 | SECRET_KEY = '' 22 | 23 | # Admin login 24 | 1. http://127.0.0.1:8000/admin 25 | 2. username and password = admin -------------------------------------------------------------------------------- /templates/account/password_reset/reset_status.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Reset Status{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 | {% if 'email' in request.get_full_path %} 8 |

Password Reset Success

9 |

Check your email for instructions

10 | {% endif%} 11 | {% if 'complete' in request.get_full_path %} 12 |

Password Reset Success

13 | Login 14 | {% endif%} 15 |
16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.pythonPath": "venv\\Scripts\\python.exe", 4 | "python.sortImports.args": [ 5 | "--profile=black", 6 | ], 7 | "python.formatting.provider": "black", 8 | "python.formatting.blackArgs": [ 9 | "--line-length=119" 10 | ], 11 | "[python]": { 12 | "editor.codeActionsOnSave": { 13 | "source.organizeImports": true 14 | } 15 | }, 16 | // Django Extension 17 | "files.associations": { 18 | "**/*.html": "html", 19 | "**/templates/**/*.html": "django-html", 20 | "**/templates/**/*": "django-txt", 21 | "**/requirements{/**,*}.{txt,in}": "pip-requirements" 22 | }, 23 | } -------------------------------------------------------------------------------- /_documentation/vscode_settings.txt: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.pythonPath": "venv\\Scripts\\python.exe", 4 | "python.sortImports.args": [ 5 | "--profile=black", 6 | ], 7 | "python.formatting.provider": "black", 8 | "python.formatting.blackArgs": [ 9 | "--line-length=119" 10 | ], 11 | "[python]": { 12 | "editor.codeActionsOnSave": { 13 | "source.organizeImports": true 14 | } 15 | }, 16 | // Django Extension 17 | "files.associations": { 18 | "**/*.html": "html", 19 | "**/templates/**/*.html": "django-html", 20 | "**/templates/**/*": "django-txt", 21 | "**/requirements{/**,*}.{txt,in}": "pip-requirements" 22 | }, 23 | } -------------------------------------------------------------------------------- /templates/account/password_reset/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %} 3 | 4 | {% translate "Please go to the following page and choose a new password:" %} 5 | {% block reset_link %} 6 | {{ protocol }}://{{ domain }}{% url 'account:password_reset_confirm' uidb64=uid token=token %} 7 | {% endblock %} 8 | {% translate 'Your username, in case you’ve forgotten:' %} {{ user.get_username }} 9 | 10 | {% translate "Thanks for using our site!" %} 11 | 12 | {% blocktranslate %}The {{ site_name }} team{% endblocktranslate %} 13 | 14 | {% endautoescape %} 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings.base") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | import debug_toolbar 2 | from django.conf import settings 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.urls import include, path 6 | from two_factor.urls import urlpatterns as tf_urls 7 | 8 | urlpatterns = [ 9 | path("admin/", admin.site.urls), 10 | path("", include("store.urls", namespace="store")), 11 | path("basket/", include("basket.urls", namespace="basket")), 12 | path("payment/", include("payment.urls", namespace="payment")), 13 | path("", include(tf_urls)), 14 | path("account/", include("account.urls", namespace="account")), 15 | path("orders/", include("orders.urls", namespace="orders")), 16 | path("__debug__/", include(debug_toolbar.urls)), 17 | ] 18 | 19 | if settings.DEBUG: 20 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 21 | -------------------------------------------------------------------------------- /store/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404, render 2 | 3 | from .models import Category, Product 4 | 5 | 6 | def product_all(request): 7 | products = Product.objects.prefetch_related("product_image").filter(is_active=True) 8 | return render(request, "store/index.html", {"products": products}) 9 | 10 | 11 | def category_list(request, category_slug=None): 12 | category = get_object_or_404(Category, slug=category_slug) 13 | products = Product.objects.filter( 14 | category__in=Category.objects.get(name=category_slug).get_descendants(include_self=True) 15 | ) 16 | return render(request, "store/category.html", {"category": category, "products": products}) 17 | 18 | 19 | def product_detail(request, slug): 20 | product = get_object_or_404(Product, slug=slug, is_active=True) 21 | return render(request, "store/single.html", {"product": product}) 22 | -------------------------------------------------------------------------------- /templates/two_factor/core/otp_required.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% block title %}{% trans "Permission Denied" %}{% endblock %}

6 | 7 |

{% blocktrans trimmed %}The page you requested, enforces users to verify using 8 | two-factor authentication for security reasons. You need to enable these 9 | security features in order to access this page.{% endblocktrans %}

10 | 11 |

{% blocktrans trimmed %}Two-factor authentication is not enabled for your 12 | account. Enable two-factor authentication for enhanced account 13 | security.{% endblocktrans %}

14 |

15 | {% trans "Go back" %} 17 | 18 | {% trans "Enable Two-Factor Authentication" %} 19 |

20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /store/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib import admin 3 | from mptt.admin import MPTTModelAdmin 4 | 5 | from .models import ( 6 | Category, 7 | Product, 8 | ProductImage, 9 | ProductSpecification, 10 | ProductSpecificationValue, 11 | ProductType, 12 | ) 13 | 14 | admin.site.register(Category, MPTTModelAdmin) 15 | 16 | 17 | class ProductSpecificationInline(admin.TabularInline): 18 | model = ProductSpecification 19 | 20 | 21 | @admin.register(ProductType) 22 | class ProductTypeAdmin(admin.ModelAdmin): 23 | inlines = [ 24 | ProductSpecificationInline, 25 | ] 26 | 27 | 28 | class ProductImageInline(admin.TabularInline): 29 | model = ProductImage 30 | 31 | 32 | class ProductSpecificationValueInline(admin.TabularInline): 33 | model = ProductSpecificationValue 34 | 35 | 36 | @admin.register(Product) 37 | class ProductAdmin(admin.ModelAdmin): 38 | inlines = [ 39 | ProductSpecificationValueInline, 40 | ProductImageInline, 41 | ] 42 | -------------------------------------------------------------------------------- /templates/two_factor/core/phone_register.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% block title %}{% trans "Add Backup Phone" %}{% endblock %}

6 | 7 | {% if wizard.steps.current == 'setup' %} 8 |

{% blocktrans trimmed %}You'll be adding a backup phone number to your 9 | account. This number will be used if your primary method of 10 | registration is not available.{% endblocktrans %}

11 | {% elif wizard.steps.current == 'validation' %} 12 |

{% blocktrans trimmed %}We've sent a token to your phone number. Please 13 | enter the token you've received.{% endblocktrans %}

14 | {% endif %} 15 | 16 |
{% csrf_token %} 17 | {% include "two_factor/_wizard_forms.html" %} 18 | 19 | {# hidden submit button to enable [enter] key #} 20 | 21 | 22 | {% include "two_factor/_wizard_actions.html" %} 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/account/password_reset/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Reset your account{% endblock %} 3 | {% block sub_content %} 4 | 5 | {% if request.user.is_authenticated %} 6 | 9 | {% endif %} 10 | 11 | 26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /core/settings/dev_debug.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | # debug_toolbar settings 4 | if DEBUG: 5 | INTERNAL_IPS = ("127.0.0.1",) 6 | MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) 7 | 8 | INSTALLED_APPS += ("debug_toolbar",) 9 | 10 | DEBUG_TOOLBAR_PANELS = [ 11 | "debug_toolbar.panels.versions.VersionsPanel", 12 | "debug_toolbar.panels.timer.TimerPanel", 13 | "debug_toolbar.panels.settings.SettingsPanel", 14 | "debug_toolbar.panels.headers.HeadersPanel", 15 | "debug_toolbar.panels.request.RequestPanel", 16 | "debug_toolbar.panels.sql.SQLPanel", 17 | "debug_toolbar.panels.staticfiles.StaticFilesPanel", 18 | "debug_toolbar.panels.templates.TemplatesPanel", 19 | "debug_toolbar.panels.cache.CachePanel", 20 | "debug_toolbar.panels.signals.SignalsPanel", 21 | "debug_toolbar.panels.logging.LoggingPanel", 22 | "debug_toolbar.panels.redirects.RedirectsPanel", 23 | ] 24 | 25 | DEBUG_TOOLBAR_CONFIG = { 26 | "INTERCEPT_REDIRECTS": False, 27 | } 28 | -------------------------------------------------------------------------------- /templates/two_factor/core/setup_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}

6 | 7 |

{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor 8 | authentication.{% endblocktrans %}

9 | 10 | {% if not phone_methods %} 11 |

{% trans "Back to Account Security" %}

13 | {% else %} 14 |

{% blocktrans trimmed %}However, it might happen that you don't have access to 15 | your primary token device. To enable account recovery, add a phone 16 | number.{% endblocktrans %}

17 | 18 | {% trans "Back to Account Security" %} 20 |

{% trans "Add Phone Number" %}

22 | {% endif %} 23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Very Academy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "./sub_base.html" %} 2 | {% block title %}Log-in{% endblock %} 3 | {% block sub_content %} 4 | 5 | 27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /_documentation/database_schema.txt: -------------------------------------------------------------------------------- 1 | https://dbdiagram.io/d 2 | 3 | 4 | Table category as C { 5 | id int [pk, increment] 6 | name varchar 7 | slug slug 8 | parent varchar 9 | is_active boolean 10 | } 11 | 12 | Table ProductType as PT { 13 | id int [pk, increment] 14 | name varchar 15 | is_active boolean 16 | } 17 | 18 | Table ProductSpecification as PS { 19 | id int [pk, increment] 20 | product_type int 21 | name varchar 22 | } 23 | Ref: PT.id < PS.product_type 24 | 25 | Table Product as P { 26 | id int [pk, increment] 27 | product_type int 28 | category int 29 | description varchar 30 | slug slug 31 | regular_price decimal 32 | discount_price decimal 33 | is_active boolean 34 | created_at timestamp 35 | updated_at timestamp 36 | } 37 | Ref: PT.id < P.product_type 38 | Ref: C.id < P.category 39 | 40 | Table ProductSpecificationValue as PSV { 41 | id int [pk, increment] 42 | product int 43 | specification int 44 | value varchar 45 | } 46 | Ref: P.id < PSV.product 47 | Ref: PS.id < PSV.specification 48 | 49 | Table ProductImage as PI { 50 | id int [pk, increment] 51 | product int 52 | image image 53 | is_feature boolean 54 | created_at timestamp 55 | updated_at timestamp 56 | } 57 | Ref: P.id < PI.product -------------------------------------------------------------------------------- /templates/two_factor/core/backup_tokens.html: -------------------------------------------------------------------------------- 1 | {% extends "../_base.html" %} 2 | {% load i18n %} 3 | 4 | {% block sub_content %} 5 |

{% block title %}{% trans "Backup Tokens" %}{% endblock %}

6 |

{% blocktrans trimmed %}Backup tokens can be used when your primary and backup 7 | phone numbers aren't available. The backup tokens below can be used 8 | for login verification. If you've used up all your backup tokens, you 9 | can generate a new set of backup tokens. Only the backup tokens shown 10 | below will be valid.{% endblocktrans %}

11 | 12 | {% if device.token_set.count %} 13 | 18 |

{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}

19 | {% else %} 20 |

{% trans "You don't have any backup codes yet." %}

21 | {% endif %} 22 | 23 |
{% csrf_token %}{{ form }} 24 | {% trans "Back to Account Security" %} 26 | 27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /orders/views.py: -------------------------------------------------------------------------------- 1 | from django.http.response import JsonResponse 2 | from django.shortcuts import render 3 | 4 | from basket.basket import Basket 5 | 6 | from .models import Order, OrderItem 7 | 8 | 9 | def add(request): 10 | basket = Basket(request) 11 | if request.POST.get('action') == 'post': 12 | 13 | order_key = request.POST.get('order_key') 14 | user_id = request.user.id 15 | baskettotal = basket.get_total_price() 16 | 17 | # Check if order exists 18 | if Order.objects.filter(order_key=order_key).exists(): 19 | pass 20 | else: 21 | order = Order.objects.create(user_id=user_id, full_name='name', address1='add1', 22 | address2='add2', total_paid=baskettotal, order_key=order_key) 23 | order_id = order.pk 24 | 25 | for item in basket: 26 | OrderItem.objects.create(order_id=order_id, product=item['product'], price=item['price'], quantity=item['qty']) 27 | 28 | response = JsonResponse({'success': 'Return something'}) 29 | return response 30 | 31 | 32 | def payment_confirmation(data): 33 | Order.objects.filter(order_key=data).update(billing_status=True) 34 | 35 | 36 | def user_orders(request): 37 | user_id = request.user.id 38 | orders = Order.objects.filter(user_id=user_id).filter(billing_status=True) 39 | return orders 40 | -------------------------------------------------------------------------------- /templates/account/password_reset/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Password Reset Confirm{% endblock %} 3 | {% block sub_content %} 4 | 5 | {% if request.user.is_authenticated %} 6 | 9 | {% endif %} 10 | 11 | 35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/registration/register.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Register Account{% endblock %} 3 | {% block sub_content %} 4 | 5 | 41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /orders/models.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from django.conf import settings 3 | from django.db import models 4 | 5 | from store.models import Product 6 | 7 | 8 | class Order(models.Model): 9 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='order_user') 10 | full_name = models.CharField(max_length=50) 11 | address1 = models.CharField(max_length=250) 12 | address2 = models.CharField(max_length=250) 13 | city = models.CharField(max_length=100) 14 | phone = models.CharField(max_length=100) 15 | post_code = models.CharField(max_length=20) 16 | created = models.DateTimeField(auto_now_add=True) 17 | updated = models.DateTimeField(auto_now=True) 18 | total_paid = models.DecimalField(max_digits=5, decimal_places=2) 19 | order_key = models.CharField(max_length=200) 20 | billing_status = models.BooleanField(default=False) 21 | 22 | class Meta: 23 | ordering = ('-created',) 24 | 25 | def __str__(self): 26 | return str(self.created) 27 | 28 | 29 | class OrderItem(models.Model): 30 | order = models.ForeignKey(Order, 31 | related_name='items', 32 | on_delete=models.CASCADE) 33 | product = models.ForeignKey(Product, 34 | related_name='order_items', 35 | on_delete=models.CASCADE) 36 | price = models.DecimalField(max_digits=5, decimal_places=2) 37 | quantity = models.PositiveIntegerField(default=1) 38 | 39 | def __str__(self): 40 | return str(self.id) 41 | -------------------------------------------------------------------------------- /templates/account/dashboard/edit_addresses.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Edit Addresses{% endblock %} 3 | 4 | {% block sub_content %} 5 |
6 |

Create/Edit Address

7 |
Add a new delivery address and delivery preferences
8 |
9 | 32 | 33 |
34 | 35 | 43 | {% endblock %} -------------------------------------------------------------------------------- /basket/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.shortcuts import get_object_or_404, render 3 | 4 | from store.models import Product 5 | 6 | from .basket import Basket 7 | 8 | 9 | def basket_summary(request): 10 | basket = Basket(request) 11 | return render(request, 'basket/summary.html', {'basket': basket}) 12 | 13 | 14 | def basket_add(request): 15 | basket = Basket(request) 16 | if request.POST.get('action') == 'post': 17 | product_id = int(request.POST.get('productid')) 18 | product_qty = int(request.POST.get('productqty')) 19 | product = get_object_or_404(Product, id=product_id) 20 | basket.add(product=product, qty=product_qty) 21 | 22 | basketqty = basket.__len__() 23 | response = JsonResponse({'qty': basketqty}) 24 | return response 25 | 26 | 27 | def basket_delete(request): 28 | basket = Basket(request) 29 | if request.POST.get('action') == 'post': 30 | product_id = int(request.POST.get('productid')) 31 | basket.delete(product=product_id) 32 | 33 | basketqty = basket.__len__() 34 | baskettotal = basket.get_total_price() 35 | response = JsonResponse({'qty': basketqty, 'subtotal': baskettotal}) 36 | return response 37 | 38 | 39 | def basket_update(request): 40 | basket = Basket(request) 41 | if request.POST.get('action') == 'post': 42 | product_id = int(request.POST.get('productid')) 43 | product_qty = int(request.POST.get('productqty')) 44 | basket.update(product=product_id, qty=product_qty) 45 | 46 | basketqty = basket.__len__() 47 | basketsubtotal = basket.get_subtotal_price() 48 | response = JsonResponse({'qty': basketqty, 'subtotal': basketsubtotal}) 49 | return response 50 | 51 | 52 | -------------------------------------------------------------------------------- /templates/account/dashboard/edit_details.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Edit Profile{% endblock %} 3 | {% block sub_content %} 4 | 5 |
6 | 36 | 42 |
43 | 44 | {% endblock %} -------------------------------------------------------------------------------- /templates/store/category.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %} 3 | {% if category %}{{ category.name }}{% else %}Products{% endif %} 4 | {% endblock %} 5 | {% block content %} 6 | 7 |
8 |
9 |

{{ category.name|title }}

10 |
11 |
12 |
Popular products purchased
13 |
14 |
15 |
16 |
17 |
18 |
19 | {% if not products %} 20 |
There are currently no products active Home
21 | {% else %} 22 |
23 | {% for product in products %} 24 |
25 |
26 | {% for image in product.product_image.all %} 27 | {% if image.is_feature%} 28 | Responsive image 29 | {% endif %} 30 | {% endfor %} 31 |
32 |

33 | {{ product.title }} 34 |

35 |
36 | £{{product.regular_price}} 37 |
38 |
39 |
40 |
41 | {% endfor %} 42 |
43 | {% endif %} 44 |
45 |
46 |
47 | 48 | {% endblock %} -------------------------------------------------------------------------------- /payment/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import stripe 5 | from django.contrib.auth.decorators import login_required 6 | from django.http.response import HttpResponse 7 | from django.shortcuts import render 8 | from django.views.decorators.csrf import csrf_exempt 9 | from django.views.generic.base import TemplateView 10 | from django.conf import settings 11 | 12 | from basket.basket import Basket 13 | from orders.views import payment_confirmation 14 | 15 | 16 | def order_placed(request): 17 | basket = Basket(request) 18 | basket.clear() 19 | return render(request, 'payment/orderplaced.html') 20 | 21 | 22 | class Error(TemplateView): 23 | template_name = 'payment/error.html' 24 | 25 | 26 | @login_required 27 | def BasketView(request): 28 | 29 | basket = Basket(request) 30 | total = str(basket.get_total_price()) 31 | total = total.replace('.', '') 32 | total = int(total) 33 | 34 | stripe.api_key = settings.STRIPE_SECRET_KEY 35 | intent = stripe.PaymentIntent.create( 36 | amount=total, 37 | currency='gbp', 38 | metadata={'userid': request.user.id} 39 | ) 40 | 41 | return render(request, 'payment/payment_form.html', {'client_secret': intent.client_secret, 42 | 'STRIPE_PUBLISHABLE_KEY': os.environ.get('STRIPE_PUBLISHABLE_KEY')}) 43 | 44 | 45 | @csrf_exempt 46 | def stripe_webhook(request): 47 | payload = request.body 48 | event = None 49 | 50 | try: 51 | event = stripe.Event.construct_from( 52 | json.loads(payload), stripe.api_key 53 | ) 54 | except ValueError as e: 55 | print(e) 56 | return HttpResponse(status=400) 57 | 58 | # Handle the event 59 | if event.type == 'payment_intent.succeeded': 60 | payment_confirmation(event.data.object.client_secret) 61 | 62 | else: 63 | print('Unhandled event type {}'.format(event.type)) 64 | 65 | return HttpResponse(status=200) 66 | -------------------------------------------------------------------------------- /static/core/css/base.css: -------------------------------------------------------------------------------- 1 | /* Bootstrap Over-ride */ 2 | @media (min-width: 1000px){ 3 | .container{ 4 | max-width: 1000px; 5 | } 6 | } 7 | @media (max-width: 1200px){ 8 | .container{ 9 | max-width: 100%; 10 | padding:0 24px; 11 | } 12 | } 13 | .dropdown-toggle::after { 14 | display: none; 15 | } 16 | .dropdown-menu{ 17 | top:58px; 18 | left:0px; 19 | padding:10px; 20 | } 21 | 22 | .alert-primary{ 23 | background-color: #E4F1FF; 24 | } 25 | 26 | 27 | /* Fonts */ 28 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); 29 | html, body, p, div{ 30 | letter-spacing: -0.02px; 31 | font-family: 'Roboto', sans-serif; 32 | } 33 | .fs12{ 34 | font-size: 12px; 35 | } 36 | .fs15{ 37 | font-size: 0.9375rem; 38 | } 39 | 40 | .fw300{ 41 | font-weight: 300; 42 | } 43 | .fw500{ 44 | font-weight: 500; 45 | } 46 | 47 | 48 | /* Navigation-Main*/ 49 | 50 | /* xs */ 51 | .logo { 52 | width: 49px; 53 | height: auto; 54 | } 55 | /* sm */ 56 | @media (min-width: 768px) { 57 | .logo { 58 | width: 49px; 59 | } 60 | } 61 | /* md */ 62 | @media (min-width: 992px) { 63 | .logo { 64 | width: 60px; 65 | } 66 | } 67 | /* lg */ 68 | @media (min-width: 1200px) { 69 | .logo { 70 | width: 70px; 71 | } 72 | } 73 | 74 | /* Basket Menu */ 75 | 76 | .basket-qty{ 77 | color: #fff; 78 | background: #d42114; 79 | border: 2px solid #fff; 80 | border-radius: 50%; 81 | text-align: center; 82 | font-size: 11px; 83 | height: 15px; 84 | width: 14px; 85 | position: absolute; 86 | box-sizing: content-box; 87 | font-weight: 600; 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | top: 1px; 92 | right: 10px; 93 | } 94 | 95 | .basket-btn{ 96 | position: relative; 97 | } 98 | 99 | .basket-btn:hover, .basket-btn:focus, .basket-btn:active, .basket-btn.active{ 100 | color:#d42114; 101 | transition-duration: 0.4s; 102 | background-color: #fff; 103 | } 104 | 105 | 106 | /* Footer */ 107 | 108 | .footer > div > div > ul > li{ 109 | padding-bottom:8px; 110 | } -------------------------------------------------------------------------------- /templates/store/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Store - Low Prices in Books & more{% endblock %} 3 | {% block content %} 4 | 5 |
6 | 16 |
17 |
18 |
19 |
20 |

Popular

21 |
22 | {% if not products %} 23 |
There are currently no products active
24 | {% else %} 25 |
26 | {% for product in products %} 27 |
28 |
29 | {% for image in product.product_image.all %} 30 | {% if image.is_feature%} 31 | Responsive image 33 | {% endif %} 34 | {% endfor %} 35 |
36 |

37 | {{ product.title|slice:":50" }}... 39 |

40 |
£{{product.regular_price}}
41 |
42 |
43 |
44 | {% endfor %} 45 |
46 | {% endif %} 47 |
48 |
49 |
50 | 51 | {% endblock %} -------------------------------------------------------------------------------- /templates/two_factor/core/login.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base.html" %} 2 | {% load i18n two_factor %} 3 | 4 | {% block sub_content %} 5 |
6 |

{% block title %}{% trans "Login" %}{% endblock %}

7 | 8 | {% if wizard.steps.current == 'auth' %} 9 |

{% blocktrans %}Enter your credentials.{% endblocktrans %}

10 | {% elif wizard.steps.current == 'token' %} 11 | {% if device.method == 'call' %} 12 |

{% blocktrans trimmed %}We are calling your phone right now, please enter the 13 | digits you hear.{% endblocktrans %}

14 | {% elif device.method == 'sms' %} 15 |

{% blocktrans trimmed %}We sent you a text message, please enter the tokens we 16 | sent.{% endblocktrans %}

17 | {% else %} 18 |

{% blocktrans trimmed %}Please enter the tokens generated by your token 19 | generator.{% endblocktrans %}

20 | {% endif %} 21 | {% elif wizard.steps.current == 'backup' %} 22 |

{% blocktrans trimmed %}Use this form for entering backup tokens for logging in. 23 | These tokens have been generated for you to print and keep safe. Please 24 | enter one of these backup tokens to login to your account.{% endblocktrans %}

25 | {% endif %} 26 | 27 |
{% csrf_token %} 28 | {% include "two_factor/_wizard_forms.html" %} 29 | 30 | {# hidden submit button to enable [enter] key #} 31 | 32 | 33 | {% if other_devices %} 34 |

{% trans "Or, alternatively, use one of your backup phones:" %}

35 |

36 | {% for other in other_devices %} 37 | 41 | {% endfor %}

42 | {% endif %} 43 | {% if backup_tokens %} 44 |

{% trans "As a last resort, you can use a backup token:" %}

45 |

46 | 48 |

49 | {% endif %} 50 | 51 | {% include "two_factor/_wizard_actions.html" %} 52 |
53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-02 14:12 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('store', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Order', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('full_name', models.CharField(max_length=50)), 23 | ('address1', models.CharField(max_length=250)), 24 | ('address2', models.CharField(max_length=250)), 25 | ('city', models.CharField(max_length=100)), 26 | ('phone', models.CharField(max_length=100)), 27 | ('post_code', models.CharField(max_length=20)), 28 | ('created', models.DateTimeField(auto_now_add=True)), 29 | ('updated', models.DateTimeField(auto_now=True)), 30 | ('total_paid', models.DecimalField(decimal_places=2, max_digits=5)), 31 | ('order_key', models.CharField(max_length=200)), 32 | ('billing_status', models.BooleanField(default=False)), 33 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_user', to=settings.AUTH_USER_MODEL)), 34 | ], 35 | options={ 36 | 'ordering': ('-created',), 37 | }, 38 | ), 39 | migrations.CreateModel( 40 | name='OrderItem', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('price', models.DecimalField(decimal_places=2, max_digits=5)), 44 | ('quantity', models.PositiveIntegerField(default=1)), 45 | ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.order')), 46 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_items', to='store.product')), 47 | ], 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /store/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from store.models import Category, Product 6 | 7 | 8 | class TestCategoriesModel(TestCase): 9 | 10 | def setUp(self): 11 | self.data1 = Category.objects.create(name='django', slug='django') 12 | 13 | def test_category_model_entry(self): 14 | """ 15 | Test Category model data insertion/types/field attributes 16 | """ 17 | data = self.data1 18 | self.assertTrue(isinstance(data, Category)) 19 | self.assertEqual(str(data), 'django') 20 | 21 | def test_category_url(self): 22 | """ 23 | Test category model slug and URL reverse 24 | """ 25 | data = self.data1 26 | response = self.client.post( 27 | reverse('store:category_list', args=[data.slug])) 28 | self.assertEqual(response.status_code, 200) 29 | 30 | 31 | class TestProductsModel(TestCase): 32 | def setUp(self): 33 | Category.objects.create(name='django', slug='django') 34 | User.objects.create(username='admin') 35 | self.data1 = Product.objects.create(category_id=1, title='django beginners', created_by_id=1, 36 | slug='django-beginners', price='20.00', image='django') 37 | self.data2 = Product.products.create(category_id=1, title='django advanced', created_by_id=1, 38 | slug='django-advanced', price='20.00', image='django', is_active=False) 39 | 40 | def test_products_model_entry(self): 41 | """ 42 | Test product model data insertion/types/field attributes 43 | """ 44 | data = self.data1 45 | self.assertTrue(isinstance(data, Product)) 46 | self.assertEqual(str(data), 'django beginners') 47 | 48 | def test_products_url(self): 49 | """ 50 | Test product model slug and URL reverse 51 | """ 52 | data = self.data1 53 | url = reverse('store:product_detail', args=[data.slug]) 54 | self.assertEqual(url, '/django-beginners') 55 | response = self.client.post( 56 | reverse('store:product_detail', args=[data.slug])) 57 | self.assertEqual(response.status_code, 200) 58 | 59 | def test_products_custom_manager_basic(self): 60 | """ 61 | Test product model custom manager returns only active products 62 | """ 63 | data = Product.products.all() 64 | self.assertEqual(data.count(), 1) 65 | -------------------------------------------------------------------------------- /store/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from unittest import skip 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.models import User 6 | from django.http import HttpRequest 7 | from django.test import Client, TestCase 8 | from django.urls import reverse 9 | 10 | from store.models import Category, Product 11 | from store.views import product_all 12 | 13 | 14 | @skip("demonstrating skipping") 15 | class TestSkip(TestCase): 16 | def test_skip_exmaple(self): 17 | pass 18 | 19 | 20 | class TestViewResponses(TestCase): 21 | def setUp(self): 22 | self.c = Client() 23 | User.objects.create(username='admin') 24 | Category.objects.create(name='django', slug='django') 25 | Product.objects.create(category_id=1, title='django beginners', created_by_id=1, 26 | slug='django-beginners', price='20.00', image='django') 27 | 28 | def test_url_allowed_hosts(self): 29 | """ 30 | Test allowed hosts 31 | """ 32 | response = self.c.get('/', HTTP_HOST='noaddress.com') 33 | self.assertEqual(response.status_code, 400) 34 | response = self.c.get('/', HTTP_HOST='yourdomain.com') 35 | self.assertEqual(response.status_code, 200) 36 | 37 | def test_homepage_url(self): 38 | """ 39 | Test homepage response status 40 | """ 41 | response = self.c.get('/') 42 | self.assertEqual(response.status_code, 200) 43 | 44 | def test_product_list_url(self): 45 | """ 46 | Test category response status 47 | """ 48 | response = self.c.get( 49 | reverse('store:category_list', args=['django'])) 50 | self.assertEqual(response.status_code, 200) 51 | 52 | def test_product_detail_url(self): 53 | """ 54 | Test items response status 55 | """ 56 | response = self.c.get( 57 | reverse('store:product_detail', args=['django-beginners'])) 58 | self.assertEqual(response.status_code, 200) 59 | 60 | def test_homepage_html(self): 61 | """ 62 | Example: code validation, search HTML for text 63 | """ 64 | request = HttpRequest() 65 | engine = import_module(settings.SESSION_ENGINE) 66 | request.session = engine.SessionStore() 67 | response = product_all(request) 68 | html = response.content.decode('utf8') 69 | self.assertIn('BookStore', html) 70 | self.assertTrue(html.startswith('\n\n')) 71 | self.assertEqual(response.status_code, 200) 72 | -------------------------------------------------------------------------------- /basket/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from store.models import Category, Product 6 | 7 | 8 | class TestBasketView(TestCase): 9 | def setUp(self): 10 | User.objects.create(username='admin') 11 | Category.objects.create(name='django', slug='django') 12 | Product.objects.create(category_id=1, title='django beginners', created_by_id=1, 13 | slug='django-beginners', price='20.00', image='django') 14 | Product.objects.create(category_id=1, title='django intermediate', created_by_id=1, 15 | slug='django-beginners', price='20.00', image='django') 16 | Product.objects.create(category_id=1, title='django advanced', created_by_id=1, 17 | slug='django-beginners', price='20.00', image='django') 18 | self.client.post( 19 | reverse('basket:basket_add'), {"productid": 1, "productqty": 1, "action": "post"}, xhr=True) 20 | self.client.post( 21 | reverse('basket:basket_add'), {"productid": 2, "productqty": 2, "action": "post"}, xhr=True) 22 | 23 | def test_basket_url(self): 24 | """ 25 | Test homepage response status 26 | """ 27 | response = self.client.get(reverse('basket:basket_summary')) 28 | self.assertEqual(response.status_code, 200) 29 | 30 | def test_basket_add(self): 31 | """ 32 | Test adding items to the basket 33 | """ 34 | response = self.client.post( 35 | reverse('basket:basket_add'), {"productid": 3, "productqty": 1, "action": "post"}, xhr=True) 36 | self.assertEqual(response.json(), {'qty': 4}) 37 | response = self.client.post( 38 | reverse('basket:basket_add'), {"productid": 2, "productqty": 1, "action": "post"}, xhr=True) 39 | self.assertEqual(response.json(), {'qty': 3}) 40 | 41 | def test_basket_delete(self): 42 | """ 43 | Test deleting items from the basket 44 | """ 45 | response = self.client.post( 46 | reverse('basket:basket_delete'), {"productid": 2, "action": "post"}, xhr=True) 47 | self.assertEqual(response.json(), {'qty': 1, 'subtotal': '20.00'}) 48 | 49 | def test_basket_update(self): 50 | """ 51 | Test updating items from the basket 52 | """ 53 | response = self.client.post( 54 | reverse('basket:basket_update'), {"productid": 2, "productqty": 1, "action": "post"}, xhr=True) 55 | self.assertEqual(response.json(), {'qty': 2, 'subtotal': '40.00'}) 56 | -------------------------------------------------------------------------------- /static/payment/index.js: -------------------------------------------------------------------------------- 1 | //'use strict'; 2 | 3 | 4 | var stripe = Stripe(STRIPE_PUBLISHABLE_KEY); 5 | 6 | var elem = document.getElementById('submit'); 7 | clientsecret = elem.getAttribute('data-secret'); 8 | 9 | // Set up Stripe.js and Elements to use in checkout form 10 | var elements = stripe.elements(); 11 | var style = { 12 | base: { 13 | color: "#000", 14 | lineHeight: '2.4', 15 | fontSize: '16px' 16 | } 17 | }; 18 | 19 | 20 | var card = elements.create("card", { style: style }); 21 | card.mount("#card-element"); 22 | 23 | card.on('change', function(event) { 24 | var displayError = document.getElementById('card-errors') 25 | if (event.error) { 26 | displayError.textContent = event.error.message; 27 | $('#card-errors').addClass('alert alert-info'); 28 | } else { 29 | displayError.textContent = ''; 30 | $('#card-errors').removeClass('alert alert-info'); 31 | } 32 | }); 33 | 34 | var form = document.getElementById('payment-form'); 35 | 36 | form.addEventListener('submit', function(ev) { 37 | ev.preventDefault(); 38 | 39 | var custName = document.getElementById("custName").value; 40 | var custAdd = document.getElementById("custAdd").value; 41 | var custAdd2 = document.getElementById("custAdd2").value; 42 | var postCode = document.getElementById("postCode").value; 43 | 44 | 45 | $.ajax({ 46 | type: "POST", 47 | url: 'http://127.0.0.1:8000/orders/add/', 48 | data: { 49 | order_key: clientsecret, 50 | csrfmiddlewaretoken: CSRF_TOKEN, 51 | action: "post", 52 | }, 53 | success: function (json) { 54 | console.log(json.success) 55 | 56 | stripe.confirmCardPayment(clientsecret, { 57 | payment_method: { 58 | card: card, 59 | billing_details: { 60 | address:{ 61 | line1:custAdd, 62 | line2:custAdd2 63 | }, 64 | name: custName 65 | }, 66 | } 67 | }).then(function(result) { 68 | if (result.error) { 69 | console.log('payment error') 70 | console.log(result.error.message); 71 | } else { 72 | if (result.paymentIntent.status === 'succeeded') { 73 | console.log('payment processed') 74 | // There's a risk of the customer closing the window before callback 75 | // execution. Set up a webhook or plugin to listen for the 76 | // payment_intent.succeeded event that handles any business critical 77 | // post-payment actions. 78 | window.location.replace("http://127.0.0.1:8000/payment/orderplaced/"); 79 | } 80 | } 81 | }); 82 | 83 | }, 84 | error: function (xhr, errmsg, err) {}, 85 | }); 86 | 87 | 88 | 89 | }); 90 | -------------------------------------------------------------------------------- /templates/account/dashboard/addresses.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Edit Addresses{% endblock %} 3 | 4 | 5 | 6 | {% block sub_content %} 7 |

Your Addresses

8 |
Manage your addresses and delivery preferences
9 |
10 |
11 |
12 | 31 | {% for address in addresses %} 32 |
33 |
34 |
35 | {% if address.default %} 36 | Default 37 | {% endif %} 38 |   39 |
40 |
41 |

{{address.full_name}}

42 |

{{address.address_line}}

43 |

{{address.address_line2}}

44 |

{{address.town_city}}

45 |

{{address.postcode}}

46 |

Phone number: {{address.phone}}

47 |
48 | Edit 49 | | 50 | Delete 51 | {% if not address.default %} 52 | | Set Default 53 | {% endif %} 54 |
55 |
56 |
57 | 58 |
59 | {% endfor %} 60 |
61 |
62 | 63 | {% endblock %} -------------------------------------------------------------------------------- /account/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import path 3 | from django.views.generic import TemplateView 4 | 5 | from . import views 6 | from .forms import PwdResetConfirmForm, PwdResetForm, UserLoginForm 7 | 8 | # https://docs.djangoproject.com/en/3.1/topics/auth/default/ 9 | # https://ccbv.co.uk/projects/Django/3.0/django.contrib.auth.views/PasswordResetConfirmView/ 10 | 11 | app_name = "account" 12 | 13 | urlpatterns = [ 14 | path( 15 | "login/", 16 | auth_views.LoginView.as_view(template_name="account/login.html", form_class=UserLoginForm), 17 | name="login", 18 | ), 19 | path("logout/", auth_views.LogoutView.as_view(next_page="/account/login/"), name="logout"), 20 | path("register/", views.account_register, name="register"), 21 | path("activate//)/", views.account_activate, name="activate"), 22 | # Reset password 23 | path( 24 | "password_reset/", 25 | auth_views.PasswordResetView.as_view( 26 | template_name="account/password_reset/password_reset_form.html", 27 | success_url="password_reset_email_confirm", 28 | email_template_name="account/password_reset/password_reset_email.html", 29 | form_class=PwdResetForm, 30 | ), 31 | name="pwdreset", 32 | ), 33 | path( 34 | "password_reset_confirm//", 35 | auth_views.PasswordResetConfirmView.as_view( 36 | template_name="account/password_reset/password_reset_confirm.html", 37 | success_url="password_reset_complete/", 38 | form_class=PwdResetConfirmForm, 39 | ), 40 | name="password_reset_confirm", 41 | ), 42 | path( 43 | "password_reset/password_reset_email_confirm/", 44 | TemplateView.as_view(template_name="account/password_reset/reset_status.html"), 45 | name="password_reset_done", 46 | ), 47 | path( 48 | "password_reset_confirm/Mg/password_reset_complete/", 49 | TemplateView.as_view(template_name="account/password_reset/reset_status.html"), 50 | name="password_reset_complete", 51 | ), 52 | # User dashboard 53 | path("dashboard/", views.dashboard, name="dashboard"), 54 | path("profile/edit/", views.edit_details, name="edit_details"), 55 | path("profile/delete_user/", views.delete_user, name="delete_user"), 56 | path( 57 | "profile/delete_confirm/", 58 | TemplateView.as_view(template_name="account/dashboard/delete_confirm.html"), 59 | name="delete_confirmation", 60 | ), 61 | path("addresses/", views.view_address, name="addresses"), 62 | path("add_address/", views.add_address, name="add_address"), 63 | path("addresses/edit//", views.edit_address, name="edit_address"), 64 | path("addresses/delete//", views.delete_address, name="delete_address"), 65 | path("addresses/set_default//", views.set_default, name="set_default"), 66 | ] 67 | -------------------------------------------------------------------------------- /templates/store/single.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% load static %} 3 | {% block stylesheet %}{% static 'store/css/store.css' %}{% endblock %} 4 | {% block title %} 5 | {% if product %}{{ product.title }}{% else %}Product{% endif %} 6 | {% endblock %} 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 |
14 |
£{{ product.regular_price }} 15 |

includes tax

16 |
17 |
18 | 19 | 25 |
26 |
27 |
28 | 30 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |

{{ product.title }}

40 |
41 | 42 | {% for image in product.product_image.all %} 43 | {% if image.is_feature %} 44 | Responsive image 45 | {% else %} 46 | Responsive image 47 | {% endif %} 48 | {% endfor %} 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 77 | 78 | {% endblock %} -------------------------------------------------------------------------------- /templates/two_factor/core/setup.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}

6 | {% if wizard.steps.current == 'welcome' %} 7 |

{% blocktrans trimmed %}You are about to take your account security to the 8 | next level. Follow the steps in this wizard to enable two-factor 9 | authentication.{% endblocktrans %}

10 | {% elif wizard.steps.current == 'method' %} 11 |

{% blocktrans trimmed %}Please select which authentication method you would 12 | like to use.{% endblocktrans %}

13 | {% elif wizard.steps.current == 'generator' %} 14 |

{% blocktrans trimmed %}To start using a token generator, please use your 15 | smartphone to scan the QR code below. For example, use Google 16 | Authenticator. Then, enter the token generated by the app. 17 | {% endblocktrans %}

18 |

QR Code

19 | {% elif wizard.steps.current == 'sms' %} 20 |

{% blocktrans trimmed %}Please enter the phone number you wish to receive the 21 | text messages on. This number will be validated in the next step. 22 | {% endblocktrans %}

23 | {% elif wizard.steps.current == 'call' %} 24 |

{% blocktrans trimmed %}Please enter the phone number you wish to be called on. 25 | This number will be validated in the next step. {% endblocktrans %}

26 | {% elif wizard.steps.current == 'validation' %} 27 | {% if challenge_succeeded %} 28 | {% if device.method == 'call' %} 29 |

{% blocktrans trimmed %}We are calling your phone right now, please enter the 30 | digits you hear.{% endblocktrans %}

31 | {% elif device.method == 'sms' %} 32 |

{% blocktrans trimmed %}We sent you a text message, please enter the tokens we 33 | sent.{% endblocktrans %}

34 | {% endif %} 35 | {% else %} 36 | 41 | {% endif %} 42 | {% elif wizard.steps.current == 'yubikey' %} 43 |

{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a 44 | token in the field below. Your YubiKey will be linked to your 45 | account.{% endblocktrans %}

46 | {% endif %} 47 | 48 |
{% csrf_token %} 49 | {% include "two_factor/_wizard_forms.html" %} 50 | 51 | {# hidden submit button to enable [enter] key #} 52 | 53 | 54 | {% include "two_factor/_wizard_actions.html" %} 55 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /templates/two_factor/profile/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "../_base.html" %} 2 | {% load i18n two_factor %} 3 | 4 | {% block sub_content %} 5 |

{% block title %}{% trans "Account Security" %}{% endblock %}

6 | 7 | {% if default_device %} 8 | {% if default_device_type == 'TOTPDevice' %} 9 |

{% trans "Tokens will be generated by your token generator." %}

10 | {% elif default_device_type == 'PhoneDevice' %} 11 |

{% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %}

12 | {% elif default_device_type == 'RemoteYubikeyDevice' %} 13 |

{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}

14 | {% endif %} 15 | 16 | {% if available_phone_methods %} 17 |

{% trans "Backup Phone Numbers" %}

18 |

{% blocktrans trimmed %}If your primary method is not available, we are able to 19 | send backup tokens to the phone numbers listed below.{% endblocktrans %}

20 |
    21 | {% for phone in backup_phones %} 22 |
  • 23 | {{ phone|device_action }} 24 |
    26 | {% csrf_token %} 27 | 29 |
    30 |
  • 31 | {% endfor %} 32 |
33 |

{% trans "Add Phone Number" %}

35 | {% endif %} 36 | 37 |

{% trans "Backup Tokens" %}

38 |

39 | {% blocktrans trimmed %}If you don't have any device with you, you can access 40 | your account using backup tokens.{% endblocktrans %} 41 | {% blocktrans trimmed count counter=backup_tokens %} 42 | You have only one backup token remaining. 43 | {% plural %} 44 | You have {{ counter }} backup tokens remaining. 45 | {% endblocktrans %} 46 |

47 |

{% trans "Show Codes" %}

49 | 50 |

{% trans "Disable Two-Factor Authentication" %}

51 |

{% blocktrans trimmed %}However we strongly discourage you to do so, you can 52 | also disable two-factor authentication for your account.{% endblocktrans %}

53 |

54 | {% trans "Disable Two-Factor Authentication" %}

55 | {% else %} 56 |

{% blocktrans trimmed %}Two-factor authentication is not enabled for your 57 | account. Enable two-factor authentication for enhanced account 58 | security.{% endblocktrans %}

59 |

60 | {% trans "Enable Two-Factor Authentication" %} 61 |

62 | {% endif %} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /templates/payment/payment_form.html: -------------------------------------------------------------------------------- 1 | {% extends "./sub_base.html" %} 2 | {% load static %} 3 | {% block title %}Make Payment{% endblock %} 4 | 5 | {% block sub_content %} 6 | 7 |
8 |

Billing address

9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | Valid first name is required. 17 |
18 |
19 |
20 | 21 | 22 |
23 | Please enter a valid email address for shipping updates. 24 |
25 |
26 |
27 | 28 | 29 |
30 | Please enter your shipping address. 31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 43 |
44 | Please select a valid country. 45 |
46 |
47 |
48 | 49 | 53 |
54 | Please provide a valid state. 55 |
56 |
57 |
58 | 59 | 60 |
61 | Zip code required. 62 |
63 |
64 |
65 |
66 |

Payment

67 | 68 |
69 | 70 |
71 |
72 | 73 |
74 | 75 | 79 | 80 | 81 | 82 | {% endblock %} -------------------------------------------------------------------------------- /account/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.contrib.auth.models import (AbstractBaseUser, BaseUserManager, 4 | PermissionsMixin) 5 | from django.core.mail import send_mail 6 | from django.db import models 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | 10 | class CustomAccountManager(BaseUserManager): 11 | 12 | def create_superuser(self, email, name, password, **other_fields): 13 | 14 | other_fields.setdefault('is_staff', True) 15 | other_fields.setdefault('is_superuser', True) 16 | other_fields.setdefault('is_active', True) 17 | 18 | if other_fields.get('is_staff') is not True: 19 | raise ValueError( 20 | 'Superuser must be assigned to is_staff=True.') 21 | if other_fields.get('is_superuser') is not True: 22 | raise ValueError( 23 | 'Superuser must be assigned to is_superuser=True.') 24 | 25 | return self.create_user(email, name, password, **other_fields) 26 | 27 | def create_user(self, email, name, password, **other_fields): 28 | 29 | if not email: 30 | raise ValueError(_('You must provide an email address')) 31 | 32 | email = self.normalize_email(email) 33 | user = self.model(email=email, name=name, 34 | **other_fields) 35 | user.set_password(password) 36 | user.save() 37 | return user 38 | 39 | 40 | class Customer(AbstractBaseUser, PermissionsMixin): 41 | email = models.EmailField(_('email address'), unique=True) 42 | name = models.CharField(max_length=150) 43 | mobile = models.CharField(max_length=20, blank=True) 44 | is_active = models.BooleanField(default=False) 45 | is_staff = models.BooleanField(default=False) 46 | created = models.DateTimeField(auto_now_add=True) 47 | updated = models.DateTimeField(auto_now=True) 48 | 49 | objects = CustomAccountManager() 50 | 51 | USERNAME_FIELD = 'email' 52 | REQUIRED_FIELDS = ['name'] 53 | 54 | class Meta: 55 | verbose_name = "Accounts" 56 | verbose_name_plural = "Accounts" 57 | 58 | def email_user(self, subject, message): 59 | send_mail( 60 | subject, 61 | message, 62 | 'l@1.com', 63 | [self.email], 64 | fail_silently=False, 65 | ) 66 | 67 | def __str__(self): 68 | return self.name 69 | 70 | class Address(models.Model): 71 | """ 72 | Address 73 | """ 74 | 75 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 76 | customer = models.ForeignKey(Customer, verbose_name=_("Customer"), on_delete=models.CASCADE) 77 | full_name = models.CharField(_("Full Name"), max_length=150) 78 | phone = models.CharField(_("Phone Number"), max_length=50) 79 | postcode = models.CharField(_("Postcode"), max_length=50) 80 | address_line = models.CharField(_("Address Line 1"), max_length=255) 81 | address_line2 = models.CharField(_("Address Line 2"), max_length=255) 82 | town_city = models.CharField(_("Town/City/State"), max_length=150) 83 | delivery_instructions = models.CharField(_("Delivery Instructions"), max_length=255) 84 | created_at = models.DateTimeField(_("Created at"), auto_now_add=True) 85 | updated_at = models.DateTimeField(_("Updated at"), auto_now=True) 86 | default = models.BooleanField(_("Default"), default=False) 87 | 88 | class Meta: 89 | verbose_name = "Address" 90 | verbose_name_plural = "Addresses" 91 | 92 | def __str__(self): 93 | return "Address" -------------------------------------------------------------------------------- /account/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-02 14:12 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Customer', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 26 | ('name', models.CharField(max_length=150)), 27 | ('mobile', models.CharField(blank=True, max_length=20)), 28 | ('is_active', models.BooleanField(default=False)), 29 | ('is_staff', models.BooleanField(default=False)), 30 | ('created', models.DateTimeField(auto_now_add=True)), 31 | ('updated', models.DateTimeField(auto_now=True)), 32 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'verbose_name': 'Accounts', 37 | 'verbose_name_plural': 'Accounts', 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='Address', 42 | fields=[ 43 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 44 | ('full_name', models.CharField(max_length=150, verbose_name='Full Name')), 45 | ('phone', models.CharField(max_length=50, verbose_name='Phone Number')), 46 | ('postcode', models.CharField(max_length=50, verbose_name='Postcode')), 47 | ('address_line', models.CharField(max_length=255, verbose_name='Address Line 1')), 48 | ('address_line2', models.CharField(max_length=255, verbose_name='Address Line 2')), 49 | ('town_city', models.CharField(max_length=150, verbose_name='Town/City/State')), 50 | ('delivery_instructions', models.CharField(max_length=255, verbose_name='Delivery Instructions')), 51 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 52 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), 53 | ('default', models.BooleanField(default=False, verbose_name='Default')), 54 | ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Customer')), 55 | ], 56 | options={ 57 | 'verbose_name': 'Address', 58 | 'verbose_name_plural': 'Addresses', 59 | }, 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /core/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 5 | 6 | SECRET_KEY = "3xk*)i0x#k$btl=(6q)te!19=mp6d)lm1+zl#ts4ewxi3-!vm_" 7 | 8 | DEBUG = True 9 | 10 | ALLOWED_HOSTS = ["yourdomain.com", "127.0.0.1", "localhost"] 11 | 12 | INSTALLED_APPS = [ 13 | "django.contrib.admin", 14 | "django.contrib.auth", 15 | "django.contrib.contenttypes", 16 | "django.contrib.sessions", 17 | "django.contrib.messages", 18 | "django.contrib.staticfiles", 19 | "store", 20 | "basket", 21 | "account", 22 | "orders", 23 | "mptt", 24 | "core", 25 | # 2FA 26 | "django_otp", 27 | "django_otp.plugins.otp_static", 28 | "django_otp.plugins.otp_totp", 29 | "two_factor", 30 | ] 31 | 32 | MIDDLEWARE = [ 33 | "django.middleware.security.SecurityMiddleware", 34 | "django.contrib.sessions.middleware.SessionMiddleware", 35 | "django.middleware.common.CommonMiddleware", 36 | "django.middleware.csrf.CsrfViewMiddleware", 37 | "django.contrib.auth.middleware.AuthenticationMiddleware", 38 | "django_otp.middleware.OTPMiddleware", 39 | "django.contrib.messages.middleware.MessageMiddleware", 40 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 41 | ] 42 | 43 | ROOT_URLCONF = "core.urls" 44 | 45 | TEMPLATES = [ 46 | { 47 | "BACKEND": "django.template.backends.django.DjangoTemplates", 48 | "DIRS": [BASE_DIR / "templates"], 49 | "APP_DIRS": True, 50 | "OPTIONS": { 51 | "context_processors": [ 52 | "django.template.context_processors.debug", 53 | "django.template.context_processors.request", 54 | "django.contrib.auth.context_processors.auth", 55 | "django.contrib.messages.context_processors.messages", 56 | "store.context_processors.categories", 57 | "basket.context_processors.basket", 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | WSGI_APPLICATION = "core.wsgi.application" 64 | 65 | DATABASES = { 66 | "default": { 67 | "ENGINE": "django.db.backends.sqlite3", 68 | "NAME": BASE_DIR / "db.sqlite3", 69 | } 70 | } 71 | 72 | AUTH_PASSWORD_VALIDATORS = [ 73 | { 74 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 75 | }, 76 | { 77 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 78 | }, 79 | { 80 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 81 | }, 82 | { 83 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 84 | }, 85 | ] 86 | 87 | LANGUAGE_CODE = "en-us" 88 | 89 | TIME_ZONE = "UTC" 90 | 91 | USE_I18N = True 92 | 93 | USE_L10N = True 94 | 95 | USE_TZ = True 96 | 97 | STATIC_URL = "/static/" 98 | 99 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 100 | 101 | MEDIA_URL = "/media/" 102 | MEDIA_ROOT = os.path.join(BASE_DIR, "media/") 103 | 104 | # Basket session ID 105 | BASKET_SESSION_ID = "basket" 106 | 107 | # Stripe Payment 108 | os.environ.setdefault( 109 | "STRIPE_PUBLISHABLE_KEY", 110 | "pk_test_51IHxTTJm9Ogh6om63DrCDlFfxcPDTvbUVy1CoSiI1ZS2GKt8UJojUJlbo9CAOCHnNgEqwKlQlnv9TmyzKIUUkr8800w8nNvBfw", 111 | ) 112 | STRIPE_SECRET_KEY = ( 113 | "sk_test_51IHxTTJm9Ogh6om6ryBhjePFWUTXvweI5y5gXjFhgPWVztF83X6Rhae1LGfW8bteV5ebb2KhX9w61Q1117Sw1iHE00gzR7PmNq" 114 | ) 115 | # stripe listen --forward-to localhost:8000/payment/webhook/ 116 | 117 | # Custom user model 118 | AUTH_USER_MODEL = "account.Customer" 119 | LOGIN_REDIRECT_URL = "/account/dashboard" 120 | # LOGIN_URL = "/account/login/" 121 | LOGIN_URL = "two_factor:login" 122 | 123 | # Email setting 124 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 125 | -------------------------------------------------------------------------------- /basket/basket.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.conf import settings 4 | from store.models import Product 5 | 6 | 7 | class Basket: 8 | """ 9 | A base Basket class, providing some default behaviors that 10 | can be inherited or overrided, as necessary. 11 | """ 12 | 13 | def __init__(self, request): 14 | self.session = request.session 15 | basket = self.session.get(settings.BASKET_SESSION_ID) 16 | if settings.BASKET_SESSION_ID not in request.session: 17 | basket = self.session[settings.BASKET_SESSION_ID] = {} 18 | self.basket = basket 19 | 20 | def add(self, product, qty): 21 | """ 22 | Adding and updating the users basket session data 23 | """ 24 | product_id = str(product.id) 25 | 26 | if product_id in self.basket: 27 | self.basket[product_id]["qty"] = qty 28 | else: 29 | self.basket[product_id] = {"price": str(product.regular_price), "qty": qty} 30 | 31 | self.save() 32 | 33 | def __iter__(self): 34 | """ 35 | Collect the product_id in the session data to query the database 36 | and return products 37 | """ 38 | product_ids = self.basket.keys() 39 | products = Product.objects.filter(id__in=product_ids) 40 | basket = self.basket.copy() 41 | 42 | for product in products: 43 | basket[str(product.id)]["product"] = product 44 | 45 | for item in basket.values(): 46 | item["price"] = Decimal(item["price"]) 47 | item["total_price"] = item["price"] * item["qty"] 48 | yield item 49 | 50 | def __len__(self): 51 | """ 52 | Get the basket data and count the qty of items 53 | """ 54 | return sum(item["qty"] for item in self.basket.values()) 55 | 56 | def update(self, product, qty): 57 | """ 58 | Update values in session data 59 | """ 60 | product_id = str(product) 61 | if product_id in self.basket: 62 | self.basket[product_id]["qty"] = qty 63 | self.save() 64 | 65 | def get_subtotal_price(self): 66 | return sum(Decimal(item["price"]) * item["qty"] for item in self.basket.values()) 67 | 68 | def get_total_price(self): 69 | 70 | subtotal = sum(Decimal(item["price"]) * item["qty"] for item in self.basket.values()) 71 | 72 | if subtotal == 0: 73 | shipping = Decimal(0.00) 74 | else: 75 | shipping = Decimal(11.50) 76 | 77 | total = subtotal + Decimal(shipping) 78 | return total 79 | 80 | def delete(self, product): 81 | """ 82 | Delete item from session data 83 | """ 84 | product_id = str(product) 85 | 86 | if product_id in self.basket: 87 | del self.basket[product_id] 88 | self.save() 89 | 90 | def clear(self): 91 | # Remove basket from session 92 | del self.session[settings.BASKET_SESSION_ID] 93 | self.save() 94 | 95 | def save(self): 96 | self.session.modified = True 97 | 98 | 99 | """ 100 | Code in this file has been inspried/reworked from other known works. Plese ensure that 101 | the License below is included in any of your work that is directly copied from 102 | this source file. 103 | 104 | 105 | MIT License 106 | 107 | Copyright (c) 2019 Packt 108 | 109 | Permission is hereby granted, free of charge, to any person obtaining a copy 110 | of this software and associated documentation files (the "Software"), to deal 111 | in the Software without restriction, including without limitation the rights 112 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 113 | copies of the Software, and to permit persons to whom the Software is 114 | furnished to do so, subject to the following conditions: 115 | 116 | The above copyright notice and this permission notice shall be included in all 117 | copies or substantial portions of the Software. 118 | 119 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 120 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 121 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 122 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 123 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 124 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 125 | SOFTWARE. 126 | """ 127 | -------------------------------------------------------------------------------- /templates/account/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "../sub_base.html" %} 2 | {% block title %}Dashboard{% endblock %} 3 | 4 | {% block sub_content %} 5 | 6 |
7 |

Your Account

8 |
9 |
10 |
Manage your orders and personal details
11 | {% comment %} {% endcomment %} 12 |
13 |
14 | 15 | 78 | 79 | {% endblock %} -------------------------------------------------------------------------------- /account/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import login, logout 2 | from django.contrib.auth.decorators import login_required 3 | from django.contrib.sites.shortcuts import get_current_site 4 | from django.http import HttpResponse, HttpResponseRedirect 5 | from django.shortcuts import redirect, render 6 | from django.urls import reverse 7 | from django.template.loader import render_to_string 8 | from django.utils.encoding import force_bytes, force_text 9 | from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode 10 | 11 | from orders.views import user_orders 12 | 13 | from .forms import RegistrationForm, UserEditForm, UserAddressForm 14 | from .models import Customer, Address 15 | from .tokens import account_activation_token 16 | 17 | 18 | @login_required 19 | def dashboard(request): 20 | orders = user_orders(request) 21 | return render(request, 22 | 'account/dashboard/dashboard.html', 23 | {'section': 'profile', 'orders': orders}) 24 | 25 | 26 | @login_required 27 | def edit_details(request): 28 | if request.method == 'POST': 29 | user_form = UserEditForm(instance=request.user, data=request.POST) 30 | 31 | if user_form.is_valid(): 32 | user_form.save() 33 | else: 34 | user_form = UserEditForm(instance=request.user) 35 | 36 | return render(request, 37 | 'account/dashboard/edit_details.html', {'user_form': user_form}) 38 | 39 | 40 | @login_required 41 | def delete_user(request): 42 | user = UserBase.objects.get(user_name=request.user) 43 | user.is_active = False 44 | user.save() 45 | logout(request) 46 | return redirect('account:delete_confirmation') 47 | 48 | 49 | def account_register(request): 50 | 51 | if request.user.is_authenticated: 52 | return redirect('account:dashboard') 53 | 54 | if request.method == 'POST': 55 | registerForm = RegistrationForm(request.POST) 56 | if registerForm.is_valid(): 57 | user = registerForm.save(commit=False) 58 | user.email = registerForm.cleaned_data['email'] 59 | user.set_password(registerForm.cleaned_data['password']) 60 | user.is_active = False 61 | user.save() 62 | current_site = get_current_site(request) 63 | subject = 'Activate your Account' 64 | message = render_to_string('account/registration/account_activation_email.html', { 65 | 'user': user, 66 | 'domain': current_site.domain, 67 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 68 | 'token': account_activation_token.make_token(user), 69 | }) 70 | user.email_user(subject=subject, message=message) 71 | return render(request, 'account/registration/register_email_confirm.html', {'form': registerForm}) 72 | else: 73 | registerForm = RegistrationForm() 74 | return render(request, 'account/registration/register.html', {'form': registerForm}) 75 | 76 | 77 | def account_activate(request, uidb64, token): 78 | try: 79 | uid = force_text(urlsafe_base64_decode(uidb64)) 80 | user = UserBase.objects.get(pk=uid) 81 | except(TypeError, ValueError, OverflowError, user.DoesNotExist): 82 | user = None 83 | if user is not None and account_activation_token.check_token(user, token): 84 | user.is_active = True 85 | user.save() 86 | login(request, user) 87 | return redirect('account:dashboard') 88 | else: 89 | return render(request, 'account/registration/activation_invalid.html') 90 | 91 | # Addresses 92 | 93 | @login_required 94 | def view_address(request): 95 | addresses = Address.objects.filter(customer=request.user) 96 | return render(request, "account/dashboard/addresses.html", {"addresses": addresses}) 97 | 98 | 99 | @login_required 100 | def add_address(request): 101 | if request.method == "POST": 102 | address_form = UserAddressForm(data=request.POST) 103 | if address_form.is_valid(): 104 | address_form = address_form.save(commit=False) 105 | address_form.customer = request.user 106 | address_form.save() 107 | return HttpResponseRedirect(reverse("account:addresses")) 108 | else: 109 | address_form = UserAddressForm() 110 | return render(request, "account/dashboard/edit_addresses.html", {"form": address_form}) 111 | 112 | @login_required 113 | def edit_address(request, id): 114 | if request.method == "POST": 115 | address = Address.objects.get(pk=id, customer=request.user) 116 | address_form = UserAddressForm(instance=address, data=request.POST) 117 | if address_form.is_valid(): 118 | address_form.save() 119 | return HttpResponseRedirect(reverse("account:addresses")) 120 | else: 121 | address = Address.objects.get(pk=id, customer=request.user) 122 | address_form = UserAddressForm(instance=address) 123 | return render(request, "account/dashboard/edit_addresses.html", {"form": address_form}) 124 | 125 | @login_required 126 | def delete_address(request, id): 127 | address = Address.objects.filter(pk=id, customer=request.user).delete() 128 | return redirect("account:addresses") 129 | 130 | @login_required 131 | def set_default(request, id): 132 | Address.objects.filter(customer=request.user, default=True).update(default=False) 133 | Address.objects.filter(pk=id, customer=request.user).update(default=True) 134 | return redirect("account:addresses") -------------------------------------------------------------------------------- /store/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from django.utils.translation import gettext_lazy as _ 4 | from mptt.models import MPTTModel, TreeForeignKey 5 | 6 | 7 | class Category(MPTTModel): 8 | """ 9 | Category Table implimented with MPTT. 10 | """ 11 | 12 | name = models.CharField( 13 | verbose_name=_("Category Name"), 14 | help_text=_("Required and unique"), 15 | max_length=255, 16 | unique=True, 17 | ) 18 | slug = models.SlugField(verbose_name=_("Category safe URL"), max_length=255, unique=True) 19 | parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") 20 | is_active = models.BooleanField(default=True) 21 | 22 | class MPTTMeta: 23 | order_insertion_by = ["name"] 24 | 25 | class Meta: 26 | verbose_name = _("Category") 27 | verbose_name_plural = _("Categories") 28 | 29 | def get_absolute_url(self): 30 | return reverse("store:category_list", args=[self.slug]) 31 | 32 | def __str__(self): 33 | return self.name 34 | 35 | 36 | class ProductType(models.Model): 37 | """ 38 | ProductType Table will provide a list of the different types 39 | of products that are for sale. 40 | """ 41 | 42 | name = models.CharField(verbose_name=_("Product Name"), help_text=_("Required"), max_length=255, unique=True) 43 | is_active = models.BooleanField(default=True) 44 | 45 | class Meta: 46 | verbose_name = _("Product Type") 47 | verbose_name_plural = _("Product Types") 48 | 49 | def __str__(self): 50 | return self.name 51 | 52 | 53 | class ProductSpecification(models.Model): 54 | """ 55 | The Product Specification Table contains product 56 | specifiction or features for the product types. 57 | """ 58 | 59 | product_type = models.ForeignKey(ProductType, on_delete=models.RESTRICT) 60 | name = models.CharField(verbose_name=_("Name"), help_text=_("Required"), max_length=255) 61 | 62 | class Meta: 63 | verbose_name = _("Product Specification") 64 | verbose_name_plural = _("Product Specifications") 65 | 66 | def __str__(self): 67 | return self.name 68 | 69 | 70 | class Product(models.Model): 71 | """ 72 | The Product table contining all product items. 73 | """ 74 | 75 | product_type = models.ForeignKey(ProductType, on_delete=models.RESTRICT) 76 | category = models.ForeignKey(Category, on_delete=models.RESTRICT) 77 | title = models.CharField( 78 | verbose_name=_("title"), 79 | help_text=_("Required"), 80 | max_length=255, 81 | ) 82 | description = models.TextField(verbose_name=_("description"), help_text=_("Not Required"), blank=True) 83 | slug = models.SlugField(max_length=255) 84 | regular_price = models.DecimalField( 85 | verbose_name=_("Regular price"), 86 | help_text=_("Maximum 999.99"), 87 | error_messages={ 88 | "name": { 89 | "max_length": _("The price must be between 0 and 999.99."), 90 | }, 91 | }, 92 | max_digits=5, 93 | decimal_places=2, 94 | ) 95 | discount_price = models.DecimalField( 96 | verbose_name=_("Discount price"), 97 | help_text=_("Maximum 999.99"), 98 | error_messages={ 99 | "name": { 100 | "max_length": _("The price must be between 0 and 999.99."), 101 | }, 102 | }, 103 | max_digits=5, 104 | decimal_places=2, 105 | ) 106 | is_active = models.BooleanField( 107 | verbose_name=_("Product visibility"), 108 | help_text=_("Change product visibility"), 109 | default=True, 110 | ) 111 | created_at = models.DateTimeField(_("Created at"), auto_now_add=True, editable=False) 112 | updated_at = models.DateTimeField(_("Updated at"), auto_now=True) 113 | 114 | class Meta: 115 | ordering = ("-created_at",) 116 | verbose_name = _("Product") 117 | verbose_name_plural = _("Products") 118 | 119 | def get_absolute_url(self): 120 | return reverse("store:product_detail", args=[self.slug]) 121 | 122 | def __str__(self): 123 | return self.title 124 | 125 | 126 | class ProductSpecificationValue(models.Model): 127 | """ 128 | The Product Specification Value table holds each of the 129 | products individual specification or bespoke features. 130 | """ 131 | 132 | product = models.ForeignKey(Product, on_delete=models.CASCADE) 133 | specification = models.ForeignKey(ProductSpecification, on_delete=models.RESTRICT) 134 | value = models.CharField( 135 | verbose_name=_("value"), 136 | help_text=_("Product specification value (maximum of 255 words"), 137 | max_length=255, 138 | ) 139 | 140 | class Meta: 141 | verbose_name = _("Product Specification Value") 142 | verbose_name_plural = _("Product Specification Values") 143 | 144 | def __str__(self): 145 | return self.value 146 | 147 | 148 | class ProductImage(models.Model): 149 | """ 150 | The Product Image table. 151 | """ 152 | 153 | product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="product_image") 154 | image = models.ImageField( 155 | verbose_name=_("image"), 156 | help_text=_("Upload a product image"), 157 | upload_to="images/", 158 | default="images/default.png", 159 | ) 160 | alt_text = models.CharField( 161 | verbose_name=_("Alturnative text"), 162 | help_text=_("Please add alturnative text"), 163 | max_length=255, 164 | null=True, 165 | blank=True, 166 | ) 167 | is_feature = models.BooleanField(default=False) 168 | created_at = models.DateTimeField(auto_now_add=True, editable=False) 169 | updated_at = models.DateTimeField(auto_now=True) 170 | 171 | class Meta: 172 | verbose_name = _("Product Image") 173 | verbose_name_plural = _("Product Images") 174 | -------------------------------------------------------------------------------- /account/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm, 3 | SetPasswordForm) 4 | 5 | from .models import Customer, Address 6 | 7 | class UserAddressForm(forms.ModelForm): 8 | class Meta: 9 | model = Address 10 | fields = ["full_name", "phone", "address_line", "address_line2", "town_city", "postcode"] 11 | 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | self.fields["full_name"].widget.attrs.update( 15 | {"class": "form-control mb-2 account-form", "placeholder": "Full Name"} 16 | ) 17 | self.fields["phone"].widget.attrs.update({"class": "form-control mb-2 account-form", "placeholder": "Phone"}) 18 | self.fields["address_line"].widget.attrs.update( 19 | {"class": "form-control mb-2 account-form", "placeholder": "Full Name"} 20 | ) 21 | self.fields["address_line2"].widget.attrs.update( 22 | {"class": "form-control mb-2 account-form", "placeholder": "Full Name"} 23 | ) 24 | self.fields["town_city"].widget.attrs.update( 25 | {"class": "form-control mb-2 account-form", "placeholder": "Full Name"} 26 | ) 27 | self.fields["postcode"].widget.attrs.update( 28 | {"class": "form-control mb-2 account-form", "placeholder": "Full Name"} 29 | ) 30 | 31 | 32 | 33 | class UserLoginForm(AuthenticationForm): 34 | 35 | username = forms.CharField(widget=forms.TextInput( 36 | attrs={'class': 'form-control mb-3', 'placeholder': 'Username', 'id': 'login-username'})) 37 | password = forms.CharField(widget=forms.PasswordInput( 38 | attrs={ 39 | 'class': 'form-control', 40 | 'placeholder': 'Password', 41 | 'id': 'login-pwd', 42 | } 43 | )) 44 | 45 | 46 | class RegistrationForm(forms.ModelForm): 47 | 48 | user_name = forms.CharField( 49 | label='Enter Username', min_length=4, max_length=50, help_text='Required') 50 | email = forms.EmailField(max_length=100, help_text='Required', error_messages={ 51 | 'required': 'Sorry, you will need an email'}) 52 | password = forms.CharField(label='Password', widget=forms.PasswordInput) 53 | password2 = forms.CharField( 54 | label='Repeat password', widget=forms.PasswordInput) 55 | 56 | class Meta: 57 | model = Customer 58 | fields = ('user_name', 'email',) 59 | 60 | def clean_username(self): 61 | user_name = self.cleaned_data['user_name'].lower() 62 | r = Customer.objects.filter(user_name=user_name) 63 | if r.count(): 64 | raise forms.ValidationError("Username already exists") 65 | return user_name 66 | 67 | def clean_password2(self): 68 | cd = self.cleaned_data 69 | if cd['password'] != cd['password2']: 70 | raise forms.ValidationError('Passwords do not match.') 71 | return cd['password2'] 72 | 73 | def clean_email(self): 74 | email = self.cleaned_data['email'] 75 | if Customer.objects.filter(email=email).exists(): 76 | raise forms.ValidationError( 77 | 'Please use another Email, that is already taken') 78 | return email 79 | 80 | def __init__(self, *args, **kwargs): 81 | super().__init__(*args, **kwargs) 82 | self.fields['user_name'].widget.attrs.update( 83 | {'class': 'form-control mb-3', 'placeholder': 'Username'}) 84 | self.fields['email'].widget.attrs.update( 85 | {'class': 'form-control mb-3', 'placeholder': 'E-mail', 'name': 'email', 'id': 'id_email'}) 86 | self.fields['password'].widget.attrs.update( 87 | {'class': 'form-control mb-3', 'placeholder': 'Password'}) 88 | self.fields['password2'].widget.attrs.update( 89 | {'class': 'form-control', 'placeholder': 'Repeat Password'}) 90 | 91 | 92 | class PwdResetForm(PasswordResetForm): 93 | 94 | email = forms.EmailField(max_length=254, widget=forms.TextInput( 95 | attrs={'class': 'form-control mb-3', 'placeholder': 'Email', 'id': 'form-email'})) 96 | 97 | def clean_email(self): 98 | email = self.cleaned_data['email'] 99 | u = Customer.objects.filter(email=email) 100 | if not u: 101 | raise forms.ValidationError( 102 | 'Unfortunatley we can not find that email address') 103 | return email 104 | 105 | 106 | class PwdResetConfirmForm(SetPasswordForm): 107 | new_password1 = forms.CharField( 108 | label='New password', widget=forms.PasswordInput( 109 | attrs={'class': 'form-control mb-3', 'placeholder': 'New Password', 'id': 'form-newpass'})) 110 | new_password2 = forms.CharField( 111 | label='Repeat password', widget=forms.PasswordInput( 112 | attrs={'class': 'form-control mb-3', 'placeholder': 'New Password', 'id': 'form-new-pass2'})) 113 | 114 | 115 | class UserEditForm(forms.ModelForm): 116 | 117 | email = forms.EmailField( 118 | label='Account email (can not be changed)', max_length=200, widget=forms.TextInput( 119 | attrs={'class': 'form-control mb-3', 'placeholder': 'email', 'id': 'form-email', 'readonly': 'readonly'})) 120 | 121 | user_name = forms.CharField( 122 | label='Firstname', min_length=4, max_length=50, widget=forms.TextInput( 123 | attrs={'class': 'form-control mb-3', 'placeholder': 'Username', 'id': 'form-firstname', 'readonly': 'readonly'})) 124 | 125 | first_name = forms.CharField( 126 | label='Username', min_length=4, max_length=50, widget=forms.TextInput( 127 | attrs={'class': 'form-control mb-3', 'placeholder': 'Firstname', 'id': 'form-lastname'})) 128 | 129 | class Meta: 130 | model = Customer 131 | fields = ('email', 'user_name', 'first_name',) 132 | 133 | def __init__(self, *args, **kwargs): 134 | super().__init__(*args, **kwargs) 135 | self.fields['user_name'].required = True 136 | self.fields['email'].required = True 137 | -------------------------------------------------------------------------------- /templates/basket/summary.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% load static %} 3 | {% block title %}Basket Summary{%endblock %} 4 | {% block content %} 5 | 6 |
7 |
8 |

Your Basket

9 |
10 |
11 |

Manage your items in your basket

12 |
13 |
14 |
15 |
16 |
17 | {% if basket|length == 0 %} 18 |
Your basket is empty Shop
19 | {% else %} 20 |
21 |
22 |
Order
23 | 38 |
39 |
40 |
Sub Total: £ 41 |
{{basket.get_subtotal_price}}
42 |
43 |
Shipping (Next day delivery): £11.50
44 |
Total to pay: £{{basket.get_total_price}}
46 |
47 |
48 |
49 |
50 | Checkout 51 | 52 |
53 |
54 |
55 | {% for item in basket %} 56 | {% with product=item.product %} 57 |
58 |
59 |
60 | {% for image in product.product_image.all %} 61 | {% if image.is_feature%} 62 | Responsive image 63 | {% endif %} 64 | {% endfor %} 65 |
66 |
67 |
68 | 69 |

{{product.title}}

70 |
71 | 72 | 79 | Update 81 | Delete 83 |
84 |
85 |
86 |
87 | {% endwith %} 88 | {% endfor %} 89 |
90 | {% endif %} 91 |
92 |
93 | 94 | 150 | 151 | {% endblock %} -------------------------------------------------------------------------------- /store/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-02 14:12 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import mptt.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(help_text='Required and unique', max_length=255, unique=True, verbose_name='Category Name')), 21 | ('slug', models.SlugField(max_length=255, unique=True, verbose_name='Category safe URL')), 22 | ('is_active', models.BooleanField(default=True)), 23 | ('lft', models.PositiveIntegerField(editable=False)), 24 | ('rght', models.PositiveIntegerField(editable=False)), 25 | ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), 26 | ('level', models.PositiveIntegerField(editable=False)), 27 | ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='store.category')), 28 | ], 29 | options={ 30 | 'verbose_name': 'Category', 31 | 'verbose_name_plural': 'Categories', 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='Product', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('title', models.CharField(help_text='Required', max_length=255, verbose_name='title')), 39 | ('description', models.TextField(blank=True, help_text='Not Required', verbose_name='description')), 40 | ('slug', models.SlugField(max_length=255)), 41 | ('regular_price', models.DecimalField(decimal_places=2, error_messages={'name': {'max_length': 'The price must be between 0 and 999.99.'}}, help_text='Maximum 999.99', max_digits=5, verbose_name='Regular price')), 42 | ('discount_price', models.DecimalField(decimal_places=2, error_messages={'name': {'max_length': 'The price must be between 0 and 999.99.'}}, help_text='Maximum 999.99', max_digits=5, verbose_name='Discount price')), 43 | ('is_active', models.BooleanField(default=True, help_text='Change product visibility', verbose_name='Product visibility')), 44 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 45 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), 46 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='store.category')), 47 | ], 48 | options={ 49 | 'verbose_name': 'Product', 50 | 'verbose_name_plural': 'Products', 51 | 'ordering': ('-created_at',), 52 | }, 53 | ), 54 | migrations.CreateModel( 55 | name='ProductSpecification', 56 | fields=[ 57 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('name', models.CharField(help_text='Required', max_length=255, verbose_name='Name')), 59 | ], 60 | options={ 61 | 'verbose_name': 'Product Specification', 62 | 'verbose_name_plural': 'Product Specifications', 63 | }, 64 | ), 65 | migrations.CreateModel( 66 | name='ProductType', 67 | fields=[ 68 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 69 | ('name', models.CharField(help_text='Required', max_length=255, unique=True, verbose_name='Product Name')), 70 | ('is_active', models.BooleanField(default=True)), 71 | ], 72 | options={ 73 | 'verbose_name': 'Product Type', 74 | 'verbose_name_plural': 'Product Types', 75 | }, 76 | ), 77 | migrations.CreateModel( 78 | name='ProductSpecificationValue', 79 | fields=[ 80 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 81 | ('value', models.CharField(help_text='Product specification value (maximum of 255 words', max_length=255, verbose_name='value')), 82 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.product')), 83 | ('specification', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='store.productspecification')), 84 | ], 85 | options={ 86 | 'verbose_name': 'Product Specification Value', 87 | 'verbose_name_plural': 'Product Specification Values', 88 | }, 89 | ), 90 | migrations.AddField( 91 | model_name='productspecification', 92 | name='product_type', 93 | field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='store.producttype'), 94 | ), 95 | migrations.CreateModel( 96 | name='ProductImage', 97 | fields=[ 98 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 99 | ('image', models.ImageField(default='images/default.png', help_text='Upload a product image', upload_to='images/', verbose_name='image')), 100 | ('alt_text', models.CharField(blank=True, help_text='Please add alturnative text', max_length=255, null=True, verbose_name='Alturnative text')), 101 | ('is_feature', models.BooleanField(default=False)), 102 | ('created_at', models.DateTimeField(auto_now_add=True)), 103 | ('updated_at', models.DateTimeField(auto_now=True)), 104 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_image', to='store.product')), 105 | ], 106 | options={ 107 | 'verbose_name': 'Product Image', 108 | 'verbose_name_plural': 'Product Images', 109 | }, 110 | ), 111 | migrations.AddField( 112 | model_name='product', 113 | name='product_type', 114 | field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='store.producttype'), 115 | ), 116 | ] 117 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}Store - Low Prices in Books & more{% endblock %} 8 | 9 | 11 | 13 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 151 |
152 | 153 |
154 |
{% block content %} {% endblock %}
155 |
156 | 157 | 208 | 209 | 210 | --------------------------------------------------------------------------------