├── api ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── views │ ├── __init__.py │ ├── view_customer.py │ ├── view_product.py │ └── view_order.py ├── apps.py └── urls.py ├── core ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── context_processors.py ├── management │ └── commands │ │ ├── tostaff.py │ │ ├── outstaff.py │ │ └── _basestaff.py ├── utils.py ├── views.py ├── tests.py ├── validators.py ├── admin.py ├── permissions.py └── models.py ├── order ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── urls.py ├── forms.py ├── validators.py ├── serializers.py ├── admin.py └── views.py ├── customer ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_customer.py │ └── test_address.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── validators.py ├── urls.py ├── forms.py ├── admin.py ├── serializers.py ├── models.py └── views.py ├── landing ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── forms.py ├── admin.py ├── models.py └── views.py ├── product ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_category.py │ └── test_discount.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── urls.py ├── validators.py ├── views.py ├── serializers.py └── admin.py ├── shopping ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── static └── shared │ ├── img │ ├── happy.jpg │ ├── sepehr.jpg │ ├── Cart-icon.png │ ├── tooltag-arrowright.svg │ ├── icon-addlink.svg │ ├── tooltag-add.svg │ ├── icon-changelink.svg │ ├── icon-deletelink.svg │ ├── icon-yes.svg │ ├── search.svg │ ├── icon-alert.svg │ ├── icon-no.svg │ ├── inline-delete.svg │ ├── icon-viewlink.svg │ ├── icon-unknown.svg │ ├── icon-unknown-alt.svg │ ├── icon-clock.svg │ ├── icon-calendar.svg │ ├── calendar-icons.svg │ ├── sorting-icons.svg │ └── selector-icons.svg │ ├── fonts │ ├── Vazir.eot │ ├── Vazir.ttf │ ├── Vazir.woff │ ├── Yekan.eot │ ├── Yekan.ttf │ ├── Yekan.woff │ ├── Vazir.woff2 │ ├── Yekan.woff2 │ ├── Gorbeh-Regular.eot │ ├── Gorbeh-Regular.otf │ ├── Gorbeh-Regular.ttf │ ├── Gorbeh-Regular.woff │ ├── Gorbeh-Regular.woff2 │ ├── Roboto-Bold-webfont.woff │ ├── Roboto-Light-webfont.woff │ └── Roboto-Regular-webfont.woff │ ├── js │ ├── plugins.js │ └── modernizr.min.js │ └── css │ ├── fonts.css │ └── normalize.css ├── requirements.txt ├── templates ├── admin │ ├── base_site.html │ ├── login.html │ └── base.html ├── registration │ └── logged_out.html ├── landing │ ├── 404.html │ └── contact.html ├── customer │ ├── address.html │ ├── password.html │ ├── edit.html │ ├── login.html │ ├── register.html │ └── profile.html ├── product │ └── category_list.html ├── order │ ├── orders.html │ └── cart.html └── base.html ├── manage.py ├── README.md └── .gitignore /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /customer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /landing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /product/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shopping/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /customer/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /product/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /customer/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /landing/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /product/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /landing/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /api/views/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Views Package Contains Seprate View Module for API View of Project Apps 3 | """ 4 | -------------------------------------------------------------------------------- /static/shared/img/happy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/img/happy.jpg -------------------------------------------------------------------------------- /static/shared/img/sepehr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/img/sepehr.jpg -------------------------------------------------------------------------------- /static/shared/fonts/Vazir.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Vazir.eot -------------------------------------------------------------------------------- /static/shared/fonts/Vazir.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Vazir.ttf -------------------------------------------------------------------------------- /static/shared/fonts/Vazir.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Vazir.woff -------------------------------------------------------------------------------- /static/shared/fonts/Yekan.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Yekan.eot -------------------------------------------------------------------------------- /static/shared/fonts/Yekan.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Yekan.ttf -------------------------------------------------------------------------------- /static/shared/fonts/Yekan.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Yekan.woff -------------------------------------------------------------------------------- /static/shared/fonts/Vazir.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Vazir.woff2 -------------------------------------------------------------------------------- /static/shared/fonts/Yekan.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Yekan.woff2 -------------------------------------------------------------------------------- /static/shared/img/Cart-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/img/Cart-icon.png -------------------------------------------------------------------------------- /static/shared/fonts/Gorbeh-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Gorbeh-Regular.eot -------------------------------------------------------------------------------- /static/shared/fonts/Gorbeh-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Gorbeh-Regular.otf -------------------------------------------------------------------------------- /static/shared/fonts/Gorbeh-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Gorbeh-Regular.ttf -------------------------------------------------------------------------------- /static/shared/fonts/Gorbeh-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Gorbeh-Regular.woff -------------------------------------------------------------------------------- /static/shared/fonts/Gorbeh-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Gorbeh-Regular.woff2 -------------------------------------------------------------------------------- /static/shared/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /static/shared/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /static/shared/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SepehrBazyar/Shopping/HEAD/static/shared/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.4.1 2 | Django==3.2.5 3 | djangorestframework==3.12.4 4 | psycopg2==2.9.1 5 | pymongo==3.12.0 6 | pytz==2021.1 7 | sqlparse==0.4.1 8 | -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'api' 7 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CoreConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'core' 8 | verbose_name = _("Core") 9 | -------------------------------------------------------------------------------- /order/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class OrderConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'order' 8 | verbose_name = _("Orders") 9 | -------------------------------------------------------------------------------- /landing/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class LandingConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'landing' 8 | verbose_name = _("Home") 9 | -------------------------------------------------------------------------------- /product/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class ProductConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'product' 8 | verbose_name = _("Products") 9 | -------------------------------------------------------------------------------- /customer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CustomerConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'customer' 8 | verbose_name = _("Customers") 9 | -------------------------------------------------------------------------------- /static/shared/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /product/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import * 4 | 5 | app_name = "product" 6 | urlpatterns = [ 7 | path('', ProductsListView.as_view(), name="lists"), 8 | path('category/', CategoryListView.as_view(), name="categories"), 9 | path('', ProductDetailView.as_view(), name="details"), 10 | ] 11 | -------------------------------------------------------------------------------- /static/shared/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/context_processors.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from .utils import readable 4 | from customer.models import Customer 5 | from order.models import Order 6 | 7 | def incoming(request) -> Dict[str, str]: 8 | """ 9 | Function for Pass Total Incoming Context to All Templates 10 | """ 11 | 12 | return {'income': readable(Order.total_income())} 13 | -------------------------------------------------------------------------------- /landing/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .models import Message 5 | 6 | class MessageForm(forms.ModelForm): 7 | """ 8 | Model Form for Write New Message in Contact Page View 9 | """ 10 | 11 | class Meta: 12 | model = Message 13 | exclude = ['deleted', 'delete_timestamp', 'was_read'] 14 | -------------------------------------------------------------------------------- /static/shared/img/icon-changelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /customer/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import * 2 | from django.utils.timezone import now 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | def birthday_validator(birth_day): 6 | """ 7 | Validator Function for Check Validated Birth Day Time Before Present 8 | """ 9 | 10 | if not birth_day < now().date(): 11 | raise ValidationError(_("Your Birth Day Can't be After Present Date Time")) 12 | -------------------------------------------------------------------------------- /order/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import * 4 | 5 | app_name = "order" 6 | urlpatterns = [ 7 | path('cart/', BasketCartView.as_view(), name="cart"), 8 | path('items/', ChangeItemView.as_view(), name="count"), 9 | path('orders/', OrdersCustomerView.as_view(), name="orders"), 10 | path('change/', ChangeCartStatusView.as_view(), name="change"), 11 | path('remove/', RemoveItemView.as_view(), name="remove") 12 | ] 13 | -------------------------------------------------------------------------------- /static/shared/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shopping/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for shopping 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.2/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', 'shopping.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /shopping/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for shopping 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.2/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', 'shopping.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /static/shared/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/management/commands/tostaff.py: -------------------------------------------------------------------------------- 1 | from ._basestaff import BasicCommand 2 | 3 | class Command(BasicCommand): 4 | """ 5 | Command Class to Change User to is Staff 6 | """ 7 | 8 | help = "Choose User for Convert to Staff" 9 | 10 | def handle(self, *args, **options): 11 | user = super().handle(*args, **options) 12 | user.is_staff = True 13 | user.save() 14 | self.stdout.write(self.style.SUCCESS(f"User {user.username} is Staff.")) 15 | -------------------------------------------------------------------------------- /core/management/commands/outstaff.py: -------------------------------------------------------------------------------- 1 | from ._basestaff import BasicCommand 2 | 3 | class Command(BasicCommand): 4 | """ 5 | Command Class to Change User to is not Staff 6 | """ 7 | 8 | help = "Choose User for Leave from Staffs" 9 | 10 | def handle(self, *args, **options): 11 | user = super().handle(*args, **options) 12 | user.is_staff = False 13 | user.save() 14 | self.stdout.write(self.style.WARNING(f"User {user.username} is not Staff.")) 15 | -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | def readable(price: int) -> str: 2 | """ 3 | Make Read Able Number for Show Prices in Template Pages 4 | """ 5 | 6 | digits, answer = str(price), [] 7 | for digit in range(len(digits) - 1, -1, -3): 8 | result = "" 9 | if digit - 2 >= 0: result += digits[digit - 2] 10 | if digit - 1 >= 0: result += digits[digit - 1] 11 | if digit >= 0: result += digits[digit] 12 | answer.append(result) 13 | return '/'.join(answer[::-1]) 14 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.utils import translation 3 | 4 | # Create your views here. 5 | def change_language(request): 6 | """ 7 | Function to Change Language Automatic by Check Current LANGUAGE CODE 8 | """ 9 | 10 | language = 'en' if translation.get_language() == 'fa' else 'fa' 11 | translation.activate(language) 12 | request.session[translation.LANGUAGE_SESSION_KEY] = language 13 | return redirect(request.GET.get('current', '/')) 14 | -------------------------------------------------------------------------------- /static/shared/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %} 5 | 6 | {% endblock %} 7 | 8 | {% block title %} 9 | {% trans "Admin Panel" %} 10 | {% endblock %} 11 | 12 | {% block branding %} 13 |

{% trans "Sepehr Shopping System" %}

14 | {% endblock %} 15 | 16 | {% block nav-global %}{% endblock %} 17 | -------------------------------------------------------------------------------- /static/shared/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/icon-viewlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/shared/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /customer/urls.py: -------------------------------------------------------------------------------- 1 | from os import name 2 | from django.urls import path 3 | 4 | from .views import * 5 | 6 | app_name = "customer" 7 | urlpatterns = [ 8 | path('register/', CustomerRegisterView.as_view(), name="register"), 9 | path('login/', CustomerLoginView.as_view(), name="login"), 10 | path('logout/', CustomerLogoutView.as_view(), name="logout"), 11 | path('profile/', CustomerProfileView.as_view(), name="profile"), 12 | path('password/', ChangePasswordView.as_view(), name="password"), 13 | path('change/', CustomerEditProfileView.as_view(), name="edit"), 14 | path('address/', CreateNewAddressView.as_view(), name="address"), 15 | path('address//', EditAddressView.as_view(), name="change"), 16 | ] 17 | -------------------------------------------------------------------------------- /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', 'shopping.settings') 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/management/commands/_basestaff.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | 3 | from argparse import ArgumentParser 4 | 5 | from core.models import User 6 | 7 | class BasicCommand(BaseCommand): 8 | """ 9 | Basic Class for Inheritanced to Change is_staff of Users 10 | """ 11 | 12 | def add_arguments(self, parser: ArgumentParser): 13 | parser.add_argument('username', metavar="Username", help="Please Enter Username") 14 | 15 | def handle(self, *args, **options): 16 | username = options['username'] 17 | try: 18 | user = User.objects.get(username=username) 19 | except User.DoesNotExist: 20 | raise CommandError(f"User {username} Does Not Exist!") 21 | else: 22 | return user 23 | -------------------------------------------------------------------------------- /static/shared/js/plugins.js: -------------------------------------------------------------------------------- 1 | // Avoid `console` errors in browsers that lack a console. 2 | (function() { 3 | var method; 4 | var noop = function () {}; 5 | var methods = [ 6 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 7 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 8 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 9 | 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' 10 | ]; 11 | var length = methods.length; 12 | var console = (window.console = window.console || {}); 13 | 14 | while (length--) { 15 | method = methods[length]; 16 | 17 | // Only stub undefined methods. 18 | if (!console[method]) { 19 | console[method] = noop; 20 | } 21 | } 22 | }()); 23 | 24 | // Place any jQuery/helper plugins in here. 25 | -------------------------------------------------------------------------------- /templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block breadcrumbs %}{% endblock %} 5 | 6 | {% block extrastyle %} 7 | 8 | 15 | {% endblock %} 16 | 17 | {% block nav-sidebar %}{% endblock %} 18 | 19 | {% block content %} 20 | 21 |

{% translate "Thanks for spending some quality time with the Web site today." %}

22 | 23 |

{% translate 'Log in again' %}

24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .models import TestModel 4 | 5 | # Create your tests here. 6 | class TestBasicModel(TestCase): 7 | def setUp(self): 8 | self.t1 = TestModel.objects.create() 9 | self.t2 = TestModel.objects.create() 10 | self.t2.delete() 11 | 12 | def test_not_deleted_all(self): 13 | self.assertIn(self.t1, TestModel.objects.all()) 14 | 15 | def test_not_deleted_filter(self): 16 | self.assertIn(self.t1, TestModel.objects.filter()) 17 | 18 | def test_not_deleted_archive(self): 19 | self.assertIn(self.t1, TestModel.objects.archive()) 20 | 21 | def test_deleted_all(self): 22 | self.assertNotIn(self.t2, TestModel.objects.all()) 23 | 24 | def test_deleted_filter(self): 25 | self.assertNotIn(self.t2, TestModel.objects.filter()) 26 | 27 | def test_deleted_archive(self): 28 | self.assertIn(self.t2, TestModel.objects.archive()) 29 | -------------------------------------------------------------------------------- /customer/tests/test_customer.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils.timezone import now, timedelta 3 | 4 | from ..models import Customer 5 | 6 | # Create your tests here. 7 | class TestCustomerModel(TestCase): 8 | def test_age_zero(self): 9 | self.customer = Customer.objects.create(username="test", password="test4321") 10 | self.customer.birth_day = now().date() - timedelta(days=364) 11 | self.customer.save() 12 | self.assertEqual(self.customer.age, 0) 13 | 14 | def test_age_positive(self): 15 | self.customer = Customer.objects.create(username="test", password="test4321") 16 | self.customer.birth_day = now().date() - timedelta(days=7559) 17 | self.customer.save() 18 | self.assertEqual(self.customer.age, 20) 19 | 20 | def test_age_null(self): 21 | self.customer = Customer.objects.create(username="test", password="test4321") 22 | self.assertIsNone(self.customer.age) 23 | -------------------------------------------------------------------------------- /landing/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Message 4 | 5 | # Register your models here. 6 | @admin.register(Message) 7 | class MessageAdmin(admin.ModelAdmin): 8 | """ 9 | Message Admin Class to Show Received Message for Admin in Panel with Info 10 | """ 11 | 12 | readonly_fields = ('first_name', 'last_name', 'phone_number', 'email', 'text') 13 | fields = ('was_read', ('first_name', 'last_name'), ('phone_number', 'email'), 'text') 14 | list_display = ('phone_number', 'first_name', 'last_name', 'email', 'was_read') 15 | list_filter = ('was_read', 'create_timestamp') 16 | search_fields = ('text', 'first_name', 'last_name', 'phone_number', 'email') 17 | ordering = ('-create_timestamp',) 18 | 19 | # staff admin can't anything edit on messages add change or delete just see 20 | def has_add_permission(self, request) -> bool: 21 | return False 22 | 23 | def has_delete_permission(self, request, obj=None) -> bool: 24 | return False 25 | -------------------------------------------------------------------------------- /core/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import * 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | import re as regex 5 | 6 | class Validators: 7 | """ 8 | Check Validation of the All the Input Values 9 | """ 10 | 11 | PHONE_NUMBER_PATTERN = r"^(09)([0-9]{9})$" 12 | POSTAL_CODE_PATTERN = r"^([0-9]{10})$" 13 | 14 | @classmethod 15 | def check_phone_number(cls, phone_number: str): 16 | """ 17 | Check Validation of Phone Number\n 18 | Example Valid Number: 09123456789 19 | """ 20 | 21 | if not regex.search(cls.PHONE_NUMBER_PATTERN, phone_number): 22 | raise ValidationError(_("Phone Number Must Start with 09 & its Length is 11 Char.")) 23 | 24 | @classmethod 25 | def check_postal_code(cls, postal_code: str): 26 | """ 27 | Check Validation Zip Code 10 Digits 28 | """ 29 | 30 | if not regex.search(cls.POSTAL_CODE_PATTERN, postal_code): 31 | raise ValidationError(_("Zip Code Must be 10 Digits.")) 32 | -------------------------------------------------------------------------------- /order/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .models import * 5 | from .validators import * 6 | 7 | class OrderItemForm(forms.ModelForm): 8 | """ 9 | Form by Order Item Model to Change Count and Check Available Inventory 10 | """ 11 | 12 | class Meta: 13 | model = OrderItem 14 | exclude = ['deleted', 'delete_timestamp'] 15 | widgets = { 16 | 'order': forms.HiddenInput(), 17 | 'product': forms.HiddenInput(), 18 | } 19 | 20 | 21 | class OrderForm(forms.ModelForm): 22 | """ 23 | Create Form by Order Model Just Get Code for Check Discount Code 24 | """ 25 | 26 | class Meta: 27 | model = Order 28 | exclude = ['deleted', 'delete_timestamp', 'address'] 29 | widgets = { 30 | 'status': forms.HiddenInput(), 31 | 'customer': forms.HiddenInput(), 32 | 'total_price': forms.HiddenInput(), 33 | 'final_price': forms.HiddenInput(), 34 | 'discount': forms.HiddenInput(), 35 | } 36 | -------------------------------------------------------------------------------- /product/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import * 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from typing import Optional 5 | 6 | class DiscountValidator: 7 | """ 8 | Validator Callable Class for Check Percent Discount Between 0 and 100 Numbers 9 | """ 10 | 11 | def __init__(self, unit: str): 12 | self.unit = unit 13 | 14 | def __call__(self, amount: int, roof: Optional[int]): 15 | if self.unit == 'P' and amount > 100: 16 | raise ValidationError(_("Percentage Rate Must be Between 0 & 100")) 17 | if self.unit == 'T' and roof: 18 | raise ValidationError(_("Ceiling is Just Available in Percent Unit")) 19 | 20 | 21 | class DatesValidator: 22 | """ 23 | Validator Callable Class for Check End Date of Discount After the Start Date 24 | """ 25 | 26 | def __init__(self, start_date): 27 | self.start_date = start_date 28 | 29 | def __call__(self, end_date): 30 | if end_date and end_date < self.start_date: 31 | raise ValidationError(_("End Date Should be After the Start Date")) 32 | -------------------------------------------------------------------------------- /static/shared/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/shared/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /static/shared/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/landing/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Page Not Found" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | {% comment %} * { 8 | line-height: 1.2; 9 | margin: 0; 10 | } 11 | 12 | html { 13 | color: #888; 14 | display: table; 15 | font-family: sans-serif; 16 | height: 100%; 17 | text-align: center; 18 | width: 100%; 19 | } 20 | 21 | body { 22 | display: table-cell; 23 | vertical-align: middle; 24 | margin: 2em auto; 25 | } {% endcomment %} 26 | 27 | #subject { 28 | color: #555; 29 | font-size: 2em; 30 | font-weight: 400; 31 | } 32 | 33 | #context { 34 | margin: 0 auto; 35 | width: 280px; 36 | } 37 | 38 | @media only screen and (max-width: 280px) { 39 | 40 | body, 41 | #context { 42 | width: 95%; 43 | } 44 | 45 | #subject { 46 | font-size: 1.5em; 47 | margin: 0 0 0.3em; 48 | } 49 | 50 | } 51 | {% endblock %} 52 | 53 | {% block body %} 54 |
55 |

{% trans "Page Not Found" %}!

56 | 57 |

58 | {% trans "Sorry, but the page you were trying to view does not exist." %} 59 |

60 |
61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .models import * 6 | 7 | # Register your models here. 8 | @admin.register(User) 9 | class MyUserAdmin(UserAdmin): 10 | """ 11 | Customization Admin Pane of User Model for Show Phone Number & Sync with User Name 12 | """ 13 | 14 | fieldsets = ( 15 | (None, {'fields': ('username', 'password')}), 16 | (_('Personal info'), {'fields': ('first_name', 'last_name', 'phone_number', 'email')}), 17 | (_('Permissions'), { 18 | 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), 19 | }), 20 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 21 | ) 22 | list_display = ('phone_number', 'first_name', 'last_name', 'email', 'is_staff') 23 | list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') 24 | search_fields = ('username', 'first_name', 'last_name', 'phone_number', 'email') 25 | ordering = ('-id',) 26 | filter_horizontal = ('groups', 'user_permissions',) 27 | 28 | 29 | class BasicAdmin(admin.ModelAdmin): 30 | """ 31 | Excluded logical Delete Fields for All Model in Panel Admin 32 | """ 33 | 34 | exclude = ['deleted', 'delete_timestamp'] 35 | search_fields = ['title_en', 'title_fa'] 36 | -------------------------------------------------------------------------------- /templates/customer/address.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Address" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #A9CCE3; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 |
33 |
34 | {{ form.non_field_errors }} 35 |
36 | {% csrf_token %} 37 |
38 | {% for field in form %} 39 | {{ field.errors }} 40 |

{{ field.label_tag }}

{{ field }} 41 | {% if field.help_text %} 42 |

{{ field.help_text|safe }}

43 | {% endif %} 44 | {% endfor %} 45 |

46 | 47 |

48 |
49 |
50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/customer/password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #A9CCE3; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 |
33 |
34 | {{ form.non_field_errors }} 35 |
36 | {% csrf_token %} 37 |
38 | {% for field in form %} 39 | {{ field.errors }} 40 |

{{ field.label_tag }}

{{ field }} 41 | {% if field.help_text %} 42 |

{{ field.help_text|safe }}

43 | {% endif %} 44 | {% endfor %} 45 |

46 | 47 |

48 |
49 |
50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /static/shared/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Yekan; 3 | src: url( '../fonts/Yekan.eot' ); 4 | src: url( '../fonts/Yekan.eot?#iefix' ) format( 'embedded-opentype' ), 5 | url( '../fonts/Yekan.woff2' ) format( 'woff2' ), 6 | url( '../fonts/Yekan.woff' ) format( 'woff' ), 7 | url( '../fonts/Yekan.ttf' ) format( 'truetype' ), 8 | url( '../fonts/Yekan.svg#Yekan' ) format( 'svg' ); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: Vazir; 15 | src: url( '../fonts/Vazir.eot' ); 16 | src: url( '../fonts/Vazir.eot?#iefix' ) format( 'embedded-opentype' ), 17 | url( '../fonts/Vazir.woff2' ) format( 'woff2' ), 18 | url( '../fonts/Vazir.woff' ) format( 'woff' ), 19 | url( '../fonts/Vazir.ttf' ) format( 'truetype' ); 20 | font-weight: normal; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: 'Aviny'; 26 | src: url("http://rangvorang.ir/Aviny.woff") format("woff"); 27 | font-weight: bold; 28 | font-style: normal; 29 | } 30 | 31 | @font-face { 32 | font-family: 'Roboto'; 33 | src: url('../fonts/Roboto-Bold-webfont.woff'); 34 | font-weight: 700; 35 | font-style: normal; 36 | } 37 | 38 | @font-face { 39 | font-family: 'Roboto'; 40 | src: url('../fonts/Roboto-Regular-webfont.woff'); 41 | font-weight: 400; 42 | font-style: normal; 43 | } 44 | 45 | @font-face { 46 | font-family: 'Roboto'; 47 | src: url('../fonts/Roboto-Light-webfont.woff'); 48 | font-weight: 300; 49 | font-style: normal; 50 | } 51 | -------------------------------------------------------------------------------- /order/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import * 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class CountValidator: 5 | """ 6 | Validator Callable Class for Check Count Order Item Available Product Inventory 7 | """ 8 | 9 | def __init__(self, product_item): 10 | self.product_item = product_item 11 | 12 | def __call__(self, count: int): 13 | if count > self.product_item.inventory: 14 | raise ValidationError(_("The Product Item Inventory isn't Enough!")) 15 | 16 | 17 | class DiscountCodeValidator: 18 | """ 19 | Validator Callable Class for Check Validate & Usable Discount Code for Customer 20 | """ 21 | 22 | def __init__(self, discount_code): 23 | self.discount_code = discount_code 24 | 25 | def __call__(self, customer): 26 | if self.discount_code.count() > 0: 27 | if customer in self.discount_code[0].users.all(): 28 | raise ValidationError(_("This Discount Code has been Expired for You!")) 29 | else: 30 | raise ValidationError(_("Not Found Any Matching Discount Code!")) 31 | 32 | 33 | class CustomerAddressValidator: 34 | """ 35 | Validator Callable Class for Check Owner of Address is Customer of Order 36 | """ 37 | 38 | def __init__(self, customer): 39 | self.customer = customer 40 | 41 | def __call__(self, address): 42 | if address.customer != self.customer: 43 | raise ValidationError(_("Owner of Address & Order Must be One Person")) 44 | -------------------------------------------------------------------------------- /templates/customer/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Edit Profile" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #A9CCE3; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 |
33 |
34 | {{ form.non_field_errors }} 35 |
36 | {% csrf_token %} 37 |
38 | {% for field in form %} 39 | {{ field.errors }} 40 |

{{ field.label_tag }}

{{ field }} 41 | {% if field.help_text %} 42 |

{{ field.help_text|safe }}

43 | {% endif %} 44 | {% endfor %} 45 |

46 | 47 |

48 |
49 |
50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /landing/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | from django.core.validators import EmailValidator 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from core.models import BasicModel 7 | from core.validators import Validators 8 | 9 | # Create your models here. 10 | class Message(BasicModel): 11 | """ 12 | Message Model for Create Contact Form and Show Messages in Admin Panel 13 | """ 14 | 15 | class Meta: 16 | verbose_name, verbose_name_plural = _("Message"), _("Messages") 17 | 18 | was_read = models.BooleanField(default=False, verbose_name=_("Was Read"), 19 | help_text=_("Please if Read this Message Select the Checkbox to Mark as Read")) 20 | first_name = models.CharField(max_length=100, verbose_name=_("First Name"), 21 | help_text=_("Please Enter Your First Name.")) 22 | last_name = models.CharField(max_length=100, verbose_name=_("Last Name"), 23 | help_text=_("Please Enter Your Last Name.")) 24 | phone_number = models.CharField(max_length=11, verbose_name=_("Phone Number"), 25 | validators=[Validators.check_phone_number], help_text=_("Please Enter Your Phone Number")) 26 | email = models.EmailField(null=True, blank=True, verbose_name=_("Email Address"), 27 | help_text=_("Please Enter Your Email Address(Optional).")) 28 | text = models.TextField(verbose_name=_("Message Text"), 29 | help_text=_("Please Write Your Message Text...")) 30 | 31 | def __str__(self) -> str: 32 | return f"{self.first_name} {self.last_name}" 33 | -------------------------------------------------------------------------------- /customer/tests/test_address.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.db import transaction 3 | from django.db.utils import IntegrityError 4 | 5 | from ..models import Customer, Address 6 | 7 | # Create your tests here. 8 | class TestAddressModel(TestCase): 9 | def setUp(self): 10 | self.customer = Customer.objects.create(username="test", password="test4321") 11 | self.home = Address.objects.create(customer=self.customer, name="Home", 12 | zip_code="1234567890", province="Tehran", city="Tehran", 13 | rest="...", lat=35.56, lng=54.35) 14 | 15 | def test_successfully_add_address(self): 16 | self.assertEqual(self.home.customer, self.customer) 17 | 18 | def test_successfully_add_new_address(self): 19 | work = Address.objects.create(customer=self.customer, name="Work", 20 | zip_code="1234567891", province="Tehran", city="Tehran", 21 | rest="...", lat=35.65, lng=54.53) 22 | self.assertIn(work, self.customer.addresses.all()) 23 | 24 | def test_duplicated_address_adding(self): 25 | try: # try to add duplicate address for check unique together lat & lng 26 | # This is caused by a quirk in how transactions are handled in Django 27 | with transaction.atomic(): 28 | Address.objects.create(customer=self.customer, name="House", 29 | zip_code="1234567892", province="Tehran", city="Tehran", 30 | rest="...", lat=35.56, lng=54.35) 31 | except IntegrityError: 32 | pass 33 | self.assertEqual(self.customer.addresses.all().count(), 1) # not appending 34 | -------------------------------------------------------------------------------- /landing/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, HttpResponse 2 | from django.views.decorators.csrf import csrf_exempt 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from product.views import ProductsListView, CategoryListView 6 | from customer.views import CustomerLoginView, CustomerProfileView, CustomerRegisterView 7 | from order.views import BasketCartView 8 | from .forms import MessageForm 9 | 10 | # Create your views here. 11 | def page_not_found(request, exception): 12 | """ 13 | Handling My Custom 404 Error View Page Not Found 14 | """ 15 | 16 | data = {} 17 | return render(request, "landing/404.html", data, status=404) 18 | 19 | def send_message(request): 20 | """ 21 | Function View for Show Form Model by Message & Get Text with AJAX Single Page 22 | """ 23 | 24 | if request.method == 'GET': 25 | form = MessageForm() 26 | return render(request, "landing/contact.html", { 27 | 'form': form 28 | }) 29 | 30 | elif request.method == 'POST': 31 | msg, status = _("Your Message was Successfully Received"), 200 32 | form = MessageForm(request.POST) 33 | if form.is_valid(): form.save() 34 | else: msg, status = _("Please be Careful & Try Again..."), 400 35 | return HttpResponse(msg, status=status) 36 | 37 | # Duplicate Page Views Import in Landing App for Easy URL Routing out of Apps 38 | home = ProductsListView.as_view() 39 | category = CategoryListView.as_view() 40 | login = CustomerLoginView.as_view() 41 | register = CustomerRegisterView.as_view() 42 | profile = CustomerProfileView.as_view() 43 | cart = BasketCartView.as_view() 44 | -------------------------------------------------------------------------------- /templates/customer/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Login" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #A9CCE3; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 |
33 |
34 |

{% trans "Login" %}

35 |
36 |
37 |
38 | {{ form.non_field_errors }} 39 |
40 | {% csrf_token %} 41 |
42 | {% for field in form %} 43 | {{ field.errors }} 44 |

{{ field.label_tag }}

{{ field }} 45 | {% endfor %} 46 |

47 | 48 |

49 |

{% trans "Do You Haven't Account Yet?" %} 50 | {% trans "Register" %} 51 |

52 |
53 |
54 |
55 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /templates/product/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Categories" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | .card{ 8 | margin: 5% 0%; 9 | } 10 | 11 | .card-body{ 12 | margin: 0% 0% 0% 3%; 13 | padding: 6% 0%; 14 | } 15 | {% endblock %} 16 | 17 | {% block body %} 18 | 19 |
20 |
21 | {% for parent in parents %} 22 | {% if forloop.counter|divisibleby:"2" %} 23 |
24 | {% else %} 25 |
26 | {% endif %} 27 |
28 |

29 | 30 | {{ parent.title }} 31 | 32 |

33 |
34 |
35 |
36 | {% for child in parent.subcategories.all %} 37 |

38 | 39 | {{ child.title }} 40 |
41 |

42 | {% endfor %} 43 |
44 |
45 |
46 | {% endfor %} 47 |
48 |
49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /shopping/urls.py: -------------------------------------------------------------------------------- 1 | """shopping URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from django.conf.urls.i18n import i18n_patterns 21 | 22 | from core.views import * 23 | from landing.views import * 24 | 25 | handler404 = "landing.views.page_not_found" 26 | 27 | urlpatterns = [ 28 | path('', home, name="index"), 29 | path('category/', category, name="category"), 30 | path('login/', login, name="login"), 31 | path('register/', register, name="register"), 32 | path('profile/', profile, name="profile"), 33 | path('cart/', cart, name="cart"), 34 | path('contact/', send_message, name="contact"), 35 | path('admin/', admin.site.urls), 36 | path('language/', change_language, name="change_language"), 37 | path('product/', include('product.urls')), 38 | path('customer/', include('customer.urls')), 39 | path('order/', include('order.urls')), 40 | path('api/', include('api.urls')), 41 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 42 | -------------------------------------------------------------------------------- /templates/customer/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Register" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #B3B6B7; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 |
33 |
34 |

{% trans "Register" %}

35 |
36 |
37 |
38 |
39 | {% csrf_token %} 40 |
41 | {% for field in form %} 42 | {{ field.errors }} 43 |

{{ field.label_tag }}

{{ field }} 44 | {% if field.help_text %} 45 |

{{ field.help_text|safe }}

46 | {% endif %} 47 | {% endfor %} 48 |

49 | 50 |

51 |

{% trans "Do You Have Account?" %} 52 | {% trans "Login" %} 53 |

54 |
55 |
56 |
57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /customer/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.contrib.auth.forms import UserCreationForm, \ 4 | AuthenticationForm, PasswordChangeForm 5 | 6 | from .models import * 7 | 8 | class CustomerRegisterForm(UserCreationForm): 9 | """ 10 | Created Form for Register New Customers by Model Fields 11 | """ 12 | 13 | class Meta: 14 | model = Customer 15 | fields = ('username', 'password1', 'password2') 16 | labels = { 17 | 'username': _("Phone Number"), 18 | } 19 | help_texts = { 20 | 'username': _("Please Enter Your Phone Number") 21 | } 22 | 23 | 24 | class CustomerLoginForm(AuthenticationForm): 25 | """ 26 | Sign In Customer by Enter Phone Number & Password to Order 27 | """ 28 | 29 | class Meta: 30 | model = Customer 31 | fields = ('username', 'password') 32 | labels = { 33 | 'username': _("Phone Number"), 34 | } 35 | 36 | error_messages = { 37 | 'invalid_login': _( 38 | "Please Enter a Valid Phone Number and Password." 39 | ), 40 | } 41 | 42 | 43 | class CustomerChangePassword(PasswordChangeForm): 44 | """ 45 | Inheritanced from Built-in Change Password Form 46 | """ 47 | 48 | pass 49 | 50 | 51 | class CustomerEditProfileForm(forms.ModelForm): 52 | """ 53 | Model Form for Change Customer Information Optionals 54 | """ 55 | 56 | class Meta: 57 | model = Customer 58 | fields = ('first_name', 'last_name', 'email', 'photo', 'gender', 'birth_day') 59 | widgets = { 60 | 'birth_day': forms.DateInput(attrs={'type':'date'}), 61 | } 62 | 63 | 64 | class AdderssForm(forms.ModelForm): 65 | """ 66 | Model Form for Create New Address for Customers 67 | """ 68 | 69 | class Meta: 70 | model = Address 71 | exclude = ['deleted', 'delete_timestamp', 'customer'] 72 | -------------------------------------------------------------------------------- /landing/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-08-21 15:48 2 | 3 | import core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Message', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('deleted', models.BooleanField(db_index=True, default=False)), 20 | ('create_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Create TimeStamp')), 21 | ('modify_timestamp', models.DateTimeField(auto_now=True, verbose_name='Modify TimeStamp')), 22 | ('delete_timestamp', models.DateTimeField(blank=True, default=None, null=True)), 23 | ('was_read', models.BooleanField(default=False, help_text='Please if Read this Message Select the Checkbox to Mark as Read', verbose_name='Was Read')), 24 | ('first_name', models.CharField(help_text='Please Enter Your First Name.', max_length=100, verbose_name='First Name')), 25 | ('last_name', models.CharField(help_text='Please Enter Your Last Name.', max_length=100, verbose_name='Last Name')), 26 | ('phone_number', models.CharField(help_text='Please Enter Your Phone Number', max_length=11, validators=[core.validators.Validators.check_phone_number], verbose_name='Phone Number')), 27 | ('email', models.EmailField(blank=True, help_text='Please Enter Your Email Address(Optional).', max_length=254, null=True, verbose_name='Email Address')), 28 | ('text', models.TextField(help_text='Please Write Your Message Text...', verbose_name='Message Text')), 29 | ], 30 | options={ 31 | 'verbose_name': 'Message', 32 | 'verbose_name_plural': 'Messages', 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /product/tests/test_category.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from pymongo import MongoClient 4 | from bson.objectid import ObjectId 5 | 6 | from ..models import * 7 | 8 | # Create your tests here. 9 | class TestCategoryModel(TestCase): 10 | def setUp(self): 11 | self.c = Category.objects.create(title_en="Test", title_fa="تست", slug="test") 12 | 13 | # tests for read and add and delete in property list category in mongodb nosql 14 | def test_read_1(self): 15 | self.assertListEqual(self.c.property_list(), []) 16 | 17 | def test_add_1_default(self): 18 | self.c.add_property("Test", "تست") 19 | self.assertListEqual(self.c.property_list(), ["تست"]) 20 | 21 | def test_add_2_fa(self): 22 | self.c.add_property("Test", "تست") 23 | self.assertListEqual(self.c.property_list('fa'), ["تست"]) 24 | 25 | def test_add_3_en(self): 26 | self.c.add_property("Test", "تست") 27 | self.assertListEqual(self.c.property_list('en'), ["Test"]) 28 | 29 | def test_delete_1_default(self): 30 | self.c.add_property("Test", "تست") 31 | self.c.delete_property("تست") 32 | self.assertListEqual(self.c.property_list(), []) 33 | 34 | def test_delete_2_fa(self): 35 | self.c.add_property("Test", "تست") 36 | self.c.delete_property("تست", 'fa') 37 | self.assertListEqual(self.c.property_list('fa'), []) 38 | 39 | def test_delete_3_en(self): 40 | self.c.add_property("Test", "تست") 41 | self.c.delete_property("Test", 'en') 42 | self.assertListEqual(self.c.property_list('en'), []) 43 | 44 | def test_delete_4_except(self): 45 | self.c.add_property("Test", "تست") 46 | self.c.delete_property("Akbar") 47 | self.assertListEqual(self.c.property_list('fa'), ["تست"]) 48 | 49 | def tearDown(self): # end of any test function for delete in db 50 | with MongoClient('mongodb://localhost:27017/') as client: 51 | categories = client.shopping.categories 52 | result = categories.delete_one({ 53 | "_id": ObjectId(self.c.properties) 54 | }) 55 | self.assertEqual(result.deleted_count, 1) 56 | -------------------------------------------------------------------------------- /customer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from core.admin import BasicAdmin, MyUserAdmin 5 | from .models import * 6 | 7 | # Register your models here. 8 | class AddressInline(admin.StackedInline): 9 | """ 10 | Create New Address Instance in Customer Admin Page with Stacked Inline Model 11 | """ 12 | 13 | model = Address 14 | exclude = ['deleted', 'delete_timestamp'] 15 | verbose_name_plural = _("Addresses") 16 | fields = ('name', ('zip_code', 'country'), ('lat', 'lng'), ('province', 'city'), 'rest') 17 | extra = 1 18 | 19 | 20 | @admin.register(Customer) 21 | class CustomerAdmin(MyUserAdmin): 22 | """ 23 | Customization Admin Panel for Show Details of Customer List Informations 24 | """ 25 | 26 | fieldsets = ( 27 | (None, {'fields': ('username', 'password')}), 28 | (_('Personal info'), {'fields': ('first_name', 'last_name', 'phone_number', 'email')}), 29 | (_('Extra info'), { 30 | 'classes': ('collapse',), 31 | 'fields': ('photo', ('gender', 'birth_day')) 32 | }), 33 | (_('Permissions'), { 34 | 'classes': ('collapse',), 35 | 'fields': ('is_active', 'is_staff'), 36 | }), 37 | (_('Important dates'), { 38 | 'classes': ('collapse',), 39 | 'fields': ('last_login', 'date_joined') 40 | }), 41 | ) 42 | list_display = ('phone_number', 'first_name', 'last_name', 'email', 'is_active') 43 | list_filter = ('is_active', 'gender') 44 | inlines = [AddressInline] 45 | 46 | 47 | @admin.register(Address) 48 | class AddressAdmin(BasicAdmin): 49 | """ 50 | Manage Address Class Model and Show Fields in Panel Admin 51 | """ 52 | 53 | fieldsets = ( 54 | (None, { 55 | 'fields': ('customer', 'name', ('zip_code', 'country'), 56 | ('lat', 'lng'), ('province', 'city'), 'rest') 57 | }), 58 | ) 59 | list_display = ('zip_code', 'province', 'city', 'country') 60 | list_filter = ('country', 'province', 'city') 61 | search_fields = ('zip_code', 'country', 'province', 'city') 62 | ordering = ('-id',) 63 | -------------------------------------------------------------------------------- /api/views/view_customer.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from rest_framework import generics 4 | from rest_framework.views import APIView 5 | from rest_framework.response import Response 6 | from rest_framework.decorators import api_view 7 | from rest_framework.permissions import IsAuthenticated 8 | 9 | from core.permissions import * 10 | from customer.serializers import * 11 | 12 | # Create your views here. 13 | class CustomerListAPIView(generics.ListAPIView): 14 | """ 15 | View for Just See List of Customers Just is Staff User 16 | """ 17 | 18 | serializer_class = CustomerBriefSerializer 19 | queryset = Customer.objects.all() 20 | permission_classes = [ 21 | IsOwnerSite 22 | ] 23 | 24 | 25 | class CustomerDetailAPIView(generics.RetrieveUpdateAPIView): 26 | """ 27 | View for See and Edit(not Delete) Details of a Customer Just is Owner or Staff User 28 | """ 29 | 30 | serializer_class = CustomerSerializer 31 | queryset = Customer.objects.all() 32 | lookup_field = 'username' 33 | lookup_url_kwarg = 'phone' 34 | permission_classes = [ 35 | IsCustomerUser 36 | ] 37 | 38 | 39 | class AddressListAPIView(generics.ListCreateAPIView): 40 | """ 41 | View for Just See List of Addresses Just is Staff User 42 | """ 43 | 44 | serializer_class = AddressBriefSerializer 45 | queryset = Address.objects.all() 46 | permission_classes = [ 47 | IsCustomerOwner 48 | ] 49 | 50 | def get_queryset(self): 51 | user = self.request.user 52 | result = super().get_queryset() 53 | if not user.is_staff: 54 | result = result.filter(customer__username=user.username) 55 | return result 56 | 57 | def perform_create(self, serializer): 58 | customer = Customer.objects.get(username=self.request.user.username) 59 | serializer.save(customer=customer) 60 | 61 | 62 | class AddressDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 63 | """ 64 | View for See and Edit Details of a Address Just is Owner or Staff User 65 | """ 66 | 67 | serializer_class = AddressSerializer 68 | queryset = Address.objects.all() 69 | lookup_field = 'zip_code' 70 | lookup_url_kwarg = 'code' 71 | permission_classes = [ 72 | IsOwnerUser 73 | ] 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopping 2 | Bilingual Online Shopping System Project with Python & Django Framework & REST API 3 | 4 | ## Photos: 5 | home-page-fa 6 | product-list-en 7 | cart-page-fa 8 | 9 | ### Responsive Mode: 10 |
11 | responsive-category-list 12 | responsive-404-not-found 13 |
14 | 15 | ## Tools: 16 | 1. Back-End: Python, Django, REST API 17 | 2. Data Base: PostgreSQL, MongoDB 18 | 3. Front-End: HTML5, CSS3, JavaScript, Bootstrap4, jQuery, AJAX 19 | 20 | ## How to Run? 21 | 1. Clone the Project 22 | * `git clone https://github.com/SepehrBazyar/Shopping.git` 23 | 2. Create a Virtual Environment("venv" is a Selective Name). 24 | * `virtualenv venv` 25 | 3. Activate the Interpreter of the Virtual Environment 26 | * Windows: `venv\Script\active` 27 | * Linux: `source venv/bin/active` 28 | 4. Install the Requirements 29 | * `pip install -r requirements.txt` 30 | 5. Adjust the Data Base Amount in `settings.py` File in `shopping` Directory 31 | 6. Write the Following Command to Create Media Directory 32 | * `mkdir media` 33 | 7. Write the Following Command to Compile the Translations 34 | * `python manage.py compilemessages -l fa` 35 | 8. Write the Following Command to Create Your Tables 36 | * `python manage.py migrate` 37 | 9. Write the Following Command to Create a Superuser 38 | * `python manage.py createsuperuser` 39 | 10. Run the MongoDB 40 | 11. Write the Following Command to Run the Server 41 | * `python manage.py runserver` 42 | 43 | ## Features: 44 | * Its Language Changes with One Click 45 | * "Cart" & "Contact Me" Pages are Single Page Application 46 | * Set Cookie to Add Product to Cart without Logging in 47 | * Animated Features in the "Cart Icon", "Category List Page" & "Carousel" 48 | -------------------------------------------------------------------------------- /templates/customer/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Profile" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 |
13 |
14 | {{ customer.username }} 16 |

17 | 18 | {% trans "Name" %}: {{ customer.first_name }} {{ customer.last_name }} 19 | 20 |

21 |

22 | {% trans "Gender" %}: {{ customer.gender_type }}
23 | {% trans "Age" %}: 24 | {% if customer.age %} 25 | {{ customer.age }} 26 | {% else %} 27 | - 28 | {% endif %} 29 | {% trans "Year" %} 30 |

31 | 32 |
33 | 34 |

35 | 36 | {% trans "Orders List" %} 37 | 38 |

39 | 40 |


41 | 42 |

{% trans "Addresses" %}:

43 | {% for address in customer.addresses.all %} 44 |

45 | {{ forloop.counter }}. {{ address.name }}: 46 | {{ address.province }} - {{ address.city }} 47 | ({% trans "Zip Code" %}: {{ address.zip_code }}) 48 | 49 | 50 | 51 |

52 | {% endfor %} 53 | 54 |
55 | 56 | 57 | {% trans "Add New Address" %} 58 | 59 | 60 |
61 |
62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from .views.view_product import * 6 | from .views.view_customer import * 7 | from .views.view_order import * 8 | 9 | app_name = "api" 10 | 11 | # viewsets 12 | order_list = OrderListAPIView.as_view({ 13 | 'get': 'list', 14 | }) 15 | order_detail = OrderDetailAPIView.as_view({ 16 | 'get': 'retrieve', 17 | 'put': 'update', 18 | 'patch': 'partial_update', 19 | 'delete': 'destroy', 20 | }) 21 | order_item_list = OrderItemListAPIView.as_view({ 22 | 'get': 'list', 23 | 'post': 'create', 24 | }) 25 | order_item_detail = OrderItemDetailAPIView.as_view({ 26 | 'get': 'retrieve', 27 | 'put': 'update', 28 | 'patch': 'partial_update', 29 | 'delete': 'destroy', 30 | }) 31 | 32 | urlpatterns = [ 33 | # product app 34 | path('product/brand/', BrandListAPIView.as_view(), name="brand_list"), 35 | path('product/brand//', BrandDetailAPIView.as_view(), name="brand_detail"), 36 | path('product/discount/', DiscountListAPIView.as_view(), name="discount_list"), 37 | path('product/discount//', DiscountDetailAPIView.as_view(), name="discount_detail"), 38 | path('product/category/', CategoryListAPIView.as_view(), name="category_list"), 39 | path('product/category//', CategoryDetailAPIView.as_view(), name="category_detail"), 40 | path('product/product/', ProductListAPIView.as_view(), name="product_list"), 41 | path('product/product//', ProductDetailAPIView.as_view(), name="product_detail"), 42 | 43 | # customer app 44 | path('customer/customer/', CustomerListAPIView.as_view(), name="customer_list"), 45 | path('customer/customer//', CustomerDetailAPIView.as_view(), name="customer_detail"), 46 | path('customer/address/', AddressListAPIView.as_view(), name="address_list"), 47 | path('customer/address//', AddressDetailAPIView.as_view(), name="address_detail"), 48 | 49 | # order app 50 | path('order/discountcode/', DiscountCodeListAPIView.as_view(), name="discountcode_list"), 51 | path('order/discountcode//', DiscountCodeDetailAPIView.as_view(), name="discountcode_detail"), 52 | path('order/order/', order_list, name="order_list"), 53 | path('order/order//', order_detail, name="order_detail"), 54 | path('order/orderitem/', order_item_list, name="orderitem_list"), 55 | path('order/orderitem//', order_item_detail, name="orderitem_detail") 56 | ] 57 | -------------------------------------------------------------------------------- /templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }} 5 | {{ form.media }} 6 | 18 | {% endblock %} 19 | 20 | {% block bodyclass %}{{ block.super }} login{% endblock %} 21 | 22 | {% block usertools %}{% endblock %} 23 | 24 | {% block nav-global %}{% endblock %} 25 | 26 | {% block nav-sidebar %}{% endblock %} 27 | 28 | {% block content_title %}{% endblock %} 29 | 30 | {% block breadcrumbs %}{% endblock %} 31 | 32 | {% block content %} 33 | {% if form.errors and not form.non_field_errors %} 34 |

35 | {% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} 36 |

37 | {% endif %} 38 | 39 | {% if form.non_field_errors %} 40 | {% for error in form.non_field_errors %} 41 |

42 | {{ error }} 43 |

44 | {% endfor %} 45 | {% endif %} 46 | 47 |
48 | 49 | {% if user.is_authenticated %} 50 |

51 | {% blocktranslate trimmed %} 52 | You are authenticated as {{ username }}, but are not authorized to 53 | access this page. Would you like to login to a different account? 54 | {% endblocktranslate %} 55 |

56 | {% endif %} 57 | 58 |
{% csrf_token %} 59 |
60 | {{ form.username.errors }} 61 | {{ form.username.label_tag }} {{ form.username }} 62 |
63 |
64 | {{ form.password.errors }} 65 | {{ form.password.label_tag }} {{ form.password }} 66 | 67 |
68 | {% url 'admin_password_reset' as password_reset_url %} 69 | {% if password_reset_url %} 70 | 73 | {% endif %} 74 |
75 | 76 |
77 |
78 | 79 |
80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /customer/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import * 4 | 5 | class AddressBriefSerializer(serializers.ModelSerializer): 6 | """ 7 | Brief Serializer for Address Model Show Important Fields 8 | """ 9 | 10 | customer = serializers.HiddenField(default=None) 11 | 12 | class Meta: 13 | model = Address 14 | fields = ( 15 | "id", "customer", "zip_code", "name", "lat", "lng", 16 | "country", "province", "city", "rest" 17 | ) 18 | extra_kwargs = { 19 | 'country': { 20 | "write_only": True, 21 | }, 22 | 'province': { 23 | "write_only": True, 24 | }, 25 | 'city': { 26 | "write_only": True, 27 | }, 28 | 'rest': { 29 | "write_only": True, 30 | }, 31 | } 32 | 33 | 34 | class AddressSerializer(serializers.ModelSerializer): 35 | """ 36 | Serializer for Address Model Show All of the Fields 37 | """ 38 | 39 | customer = serializers.HyperlinkedRelatedField(view_name="api:customer_detail", 40 | read_only=True, lookup_field='username', lookup_url_kwarg='phone') 41 | 42 | class Meta: 43 | model = Address 44 | fields = ( 45 | "id", "customer", "zip_code", "name", "lat", "lng", 46 | "country", "province", "city", "rest" 47 | ) 48 | 49 | 50 | class CustomerBriefSerializer(serializers.ModelSerializer): 51 | """ 52 | Brief Serializer for Customer Model Show Important Fields 53 | """ 54 | 55 | class Meta: 56 | model = Customer 57 | fields = ( 58 | "id", "username", "first_name", "last_name", "phone_number", "email" 59 | ) 60 | 61 | 62 | class CustomerSerializer(serializers.ModelSerializer): 63 | """ 64 | Serializer for Customer Model Show All of the Fields 65 | """ 66 | 67 | staff = serializers.ReadOnlyField(source='is_staff') 68 | addresses = AddressBriefSerializer(read_only=True, many=True) 69 | codes = serializers.PrimaryKeyRelatedField(read_only=True, many=True) 70 | orders = serializers.HyperlinkedRelatedField(view_name="api:order_detail", 71 | many=True, read_only=True, lookup_field='id', lookup_url_kwarg='number') 72 | 73 | class Meta: 74 | model = Customer 75 | fields = ( 76 | "id", "username", "first_name", "last_name", "phone_number", "email", 77 | "staff", "gender", "birth_day", "photo", "addresses", "codes", "orders" 78 | ) 79 | -------------------------------------------------------------------------------- /templates/landing/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Contact Me" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | ul > li { 8 | list-style-type: none; 9 | padding: 1px 0; 10 | } 11 | 12 | li, dt, dd { 13 | color: var(--body-quiet-color); 14 | font-size: 13px; 15 | line-height: 20px; 16 | } 17 | 18 | ul.errorlist li { 19 | color: red; 20 | background-color: #A9CCE3; 21 | font-size: 13px; 22 | display: block; 23 | margin-bottom: 4px; 24 | overflow-wrap: break-word; 25 | } 26 | {% endblock %} 27 | 28 | {% block body %} 29 |
30 |
31 |
32 | 33 | Sepehr Bazyar 35 | 36 |
37 |

{% trans "Hi, My Name is Sepehr Bazyar." %}

38 |

{% trans "I'm a Python Back-End Developer." %}

39 |
40 | 41 | bazyar.sepehr@gmail.com 42 |
43 |
44 | 45 |
46 | 47 | {{ form.non_field_errors }} 48 |
49 | {% csrf_token %} 50 |
51 | {% for field in form %} 52 | {{ field.errors }} 53 |

{{ field.label_tag }}

{{ field }} 54 | {% if field.help_text %} 55 |

{{ field.help_text|safe }}

56 | {% endif %} 57 | {% endfor %} 58 |

59 |

60 | 63 |
64 |
65 |
66 | {% endblock %} 67 | 68 | {% block extra_js %} 69 | 85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /core/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | class IsStaffUser(permissions.BasePermission): 4 | """ 5 | Permission for Staff Users to Can Change in Items of Product or etc... 6 | """ 7 | 8 | def has_permission(self, request, view): # list 9 | if request.method in permissions.SAFE_METHODS: # get, head, option 10 | return True 11 | return request.user.is_staff 12 | 13 | def has_object_permission(self, request, view, obj): # one object 14 | if request.method in permissions.SAFE_METHODS: 15 | return True 16 | return request.user.is_staff 17 | 18 | 19 | class IsOwnerSite(permissions.BasePermission): 20 | """ 21 | Permission for Check User is Owner Admin for Show List of Private Data 22 | """ 23 | 24 | def has_permission(self, request, view): 25 | return request.user.is_staff 26 | 27 | def has_object_permission(self, request, view, obj): 28 | return request.user.is_staff 29 | 30 | 31 | class IsCustomerUser(permissions.BasePermission): 32 | """ 33 | Permission for Check User Send Request is Self if not Forbidden 34 | """ 35 | 36 | def has_object_permission(self, request, view, obj): 37 | return request.user.username == obj.username or request.user.is_staff 38 | 39 | 40 | class IsOwnerUser(permissions.BasePermission): 41 | """ 42 | Permission for Check User Send Request is Owner of Object if not Forbidden 43 | """ 44 | 45 | def has_object_permission(self, request, view, obj): 46 | return request.user.username == obj.customer.username or request.user.is_staff 47 | 48 | 49 | class IsCustomerOwnerParent(permissions.BasePermission): 50 | """ 51 | Permission for Check User Send Request is Owner of Parent of tis Object 52 | """ 53 | 54 | def has_object_permission(self, request, view, obj): 55 | return request.user.username == obj.order.customer.username or request.user.is_staff 56 | 57 | 58 | class IsStaffAuthenticated(permissions.BasePermission): 59 | """ 60 | Access for Create Just Staff User and Safe Methods Just for Authentiated Users 61 | """ 62 | 63 | def has_permission(self, request, view): 64 | if request.user.is_authenticated: 65 | if request.user.is_staff: 66 | return True 67 | if request.method in permissions.SAFE_METHODS: 68 | return True 69 | return False 70 | 71 | 72 | class IsCustomerOwner(permissions.BasePermission): 73 | """ 74 | Access for Create New Object for Only Customers & Staff Users Just Can See List 75 | """ 76 | 77 | def has_permission(self, request, view): 78 | if request.user.is_authenticated: 79 | if not request.user.is_staff: 80 | return True 81 | elif request.method in permissions.SAFE_METHODS: 82 | return True 83 | return False 84 | -------------------------------------------------------------------------------- /product/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, HttpResponse, get_object_or_404 2 | from django.http import JsonResponse 3 | from django.views import View, generic 4 | from django.urls import reverse, reverse_lazy 5 | from django.db.models import Q 6 | from django.utils.translation import get_language, gettext_lazy as _ 7 | 8 | from .models import * 9 | 10 | # Create your views here. 11 | class ProductsListView(generic.ListView): 12 | """ 13 | Generic Class Based View for Show List of All Active Products Item 14 | """ 15 | 16 | context_object_name = "products" 17 | paginate_by = 12 18 | 19 | def get_queryset(self): 20 | result = Product.objects.all() 21 | kwargs = self.request.GET 22 | if "category" in kwargs: 23 | result = result.filter(category__slug=kwargs["category"]) 24 | if "brand" in kwargs: 25 | result = result.filter(brand__slug=kwargs["brand"]) 26 | if "search" in kwargs: 27 | text = kwargs["search"] 28 | result = result.filter( 29 | Q(title_en__icontains=text) | Q(title_fa__icontains=text) | Q(slug__icontains=text) | 30 | Q(category__title_en__icontains=text) | Q(category__title_fa__icontains=text) | 31 | Q(brand__title_en__icontains=text) | Q(brand__title_fa__icontains=text) | 32 | Q(category__slug__icontains=text) | Q(brand__slug__icontains=text) 33 | ) 34 | return result 35 | 36 | def get_context_data(self, **kwargs): 37 | context = super().get_context_data(**kwargs) 38 | context.update({ 39 | 'slides': Product.objects.exclude(image='Unknown.jpg').order_by('?')[:3], 40 | }) 41 | if get_language() == 'en': context['prev'], context['next'] = "prev", "next" 42 | else: context['prev'], context['next'] = "next", "prev" 43 | return context 44 | 45 | 46 | class ProductDetailView(generic.DetailView): 47 | """ 48 | Generic Class Based View for Show Detail of a Product Item 49 | """ 50 | 51 | model = Product 52 | context_object_name = "product" 53 | 54 | def post(self, request, *args, **kwargs): 55 | resp = JsonResponse({'msg': _("Product Item has Successfully been Added to the Cart")}) 56 | cart = request.COOKIES.get("cart", "") 57 | resp.set_cookie("cart", cart + request.POST["product"] + ',') 58 | return resp 59 | 60 | 61 | class CategoryListView(generic.ListView): 62 | """ 63 | Generic Class Based View for Show List of All Categories in Collapsed Cards 64 | """ 65 | 66 | context_object_name = "parents" 67 | 68 | def get_queryset(self): 69 | parents = Category.objects.filter(root=None) 70 | return parents 71 | 72 | def get_context_data(self, **kwargs): 73 | context = super().get_context_data(**kwargs) 74 | if get_language() == 'en': context['Right'], context['Left'] = "Right", "Left" 75 | else: context['Right'], context['Left'] = "Left", "Right" 76 | return context 77 | -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | from django.contrib.auth.models import UserManager, AbstractUser 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from .validators import Validators 7 | 8 | # Create your models here. 9 | class MyUserManager(UserManager): 10 | """ 11 | Customizing Manager of User for Change Auth Field to Phone Number for Default Username 12 | """ 13 | 14 | def create_superuser(self, username=None, email=None, password=None, **extra_fields): 15 | username = extra_fields["phone_number"] 16 | return super().create_superuser(username, email, password, **extra_fields) 17 | 18 | 19 | class User(AbstractUser): 20 | """ 21 | Customization User Model for Change Default User Name to Phone Number for Auth Pages 22 | """ 23 | 24 | class Meta: 25 | verbose_name, verbose_name_plural = _("User"), _("Users") 26 | 27 | objects = MyUserManager() 28 | USERNAME_FIELD = 'phone_number' 29 | 30 | phone_number = models.CharField(max_length=11, unique=True, verbose_name=_("Phone Number"), 31 | validators=[Validators.check_phone_number], help_text=_("Please Enter Your Phone Number")) 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | self.phone_number = self.username 36 | 37 | 38 | class BasicManager(models.Manager): 39 | """ 40 | Basic Class Manager for Customize the Query Set and Filter by Deleted 41 | """ 42 | 43 | # override get_queryset method to hide logical deleted in functions 44 | def get_queryset(self): 45 | return super().get_queryset().exclude(deleted=True) 46 | 47 | # override delete method to change deleted field to logical delete 48 | def delete(self): 49 | for obj in self: 50 | obj.delete() 51 | 52 | # create new method to set new all() function for show deleted item 53 | def archive(self): 54 | return super().get_queryset() 55 | 56 | 57 | class BasicModel(models.Model): 58 | """ 59 | Basic Class Model for Inheritance All Other Class Model from this 60 | """ 61 | 62 | class Meta: 63 | abstract = True # can't create instance object from this class 64 | 65 | objects = BasicManager() 66 | 67 | deleted = models.BooleanField(default=False, db_index=True) # column indexing 68 | create_timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Create TimeStamp")) 69 | modify_timestamp = models.DateTimeField(auto_now=True, verbose_name=_("Modify TimeStamp")) 70 | delete_timestamp = models.DateTimeField(default=None, null=True, blank=True) 71 | 72 | def delete(self): # logical delete 73 | """ 74 | Overrided Delete Method to Logical Delete Save the Record without Show 75 | """ 76 | 77 | self.deleted = True 78 | self.delete_timestamp = timezone.now() 79 | self.save() 80 | 81 | 82 | class TestModel(BasicModel): 83 | """ 84 | This Class Just Written to Unit Test Basic Test Class Model 85 | """ 86 | 87 | pass 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # If you are using PyCharm # 132 | .idea/**/workspace.xml 133 | .idea/**/tasks.xml 134 | .idea/dictionaries 135 | .idea/**/dataSources/ 136 | .idea/**/dataSources.ids 137 | .idea/**/dataSources.xml 138 | .idea/**/dataSources.local.xml 139 | .idea/**/sqlDataSources.xml 140 | .idea/**/dynamic.xml 141 | .idea/**/uiDesigner.xml 142 | .idea/**/gradle.xml 143 | .idea/**/libraries 144 | *.iws /out/ 145 | 146 | # Visual Studio Code # 147 | .vscode/* 148 | !.vscode/settings.json 149 | !.vscode/tasks.json 150 | !.vscode/launch.json 151 | !.vscode/extensions.json 152 | .history 153 | 154 | media/ 155 | -------------------------------------------------------------------------------- /order/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from product.serializers import * 4 | from customer.serializers import * 5 | from .models import * 6 | 7 | class DiscountCodeBriefSerializer(serializers.ModelSerializer): 8 | """ 9 | Brief Serializer for Discount Code Model Show Important Fields 10 | """ 11 | 12 | class Meta: 13 | model = DiscountCode 14 | fields = ( 15 | "id", "code", "title_en", "title_fa", "slug", "unit", "amount", "roof" 16 | ) 17 | 18 | 19 | class DiscountCodeSerializer(serializers.ModelSerializer): 20 | """ 21 | Serializer for Discount Code Model Show All of the Fields 22 | """ 23 | 24 | users = serializers.HyperlinkedRelatedField(view_name="api:customer_detail", 25 | read_only=True, many=True, lookup_field='username', lookup_url_kwarg='phone') 26 | 27 | class Meta: 28 | model = DiscountCode 29 | fields = ( 30 | "id", "code", "title_en", "title_fa", "slug", 31 | "unit", "amount", "roof", "start_date", "end_date", "users" 32 | ) 33 | 34 | 35 | class OrderItemBriefSerializer(serializers.ModelSerializer): 36 | """ 37 | Brief Serializer for Order Item Model Show Important Fields 38 | """ 39 | 40 | order = serializers.HiddenField(default=None) 41 | 42 | class Meta: 43 | model = OrderItem 44 | fields = ( 45 | "id", "order", "product", "count" 46 | ) 47 | 48 | 49 | class OrderItemSerializer(serializers.ModelSerializer): 50 | """ 51 | Serializer for Order Item Model Show All of the Fields 52 | """ 53 | 54 | order = serializers.HyperlinkedRelatedField(view_name="api:order_detail", 55 | read_only=True, lookup_field='id', lookup_url_kwarg='number') 56 | 57 | class Meta: 58 | model = OrderItem 59 | fields = ( 60 | "id", "order", "product", "count" 61 | ) 62 | 63 | 64 | class OrderBriefSerializer(serializers.ModelSerializer): 65 | """ 66 | Brief Serializer for Order Model Show Important Fields 67 | """ 68 | 69 | customer = serializers.HyperlinkedRelatedField(view_name="api:customer_detail", 70 | read_only=True, lookup_field='username', lookup_url_kwarg='phone') 71 | 72 | class Meta: 73 | model = Order 74 | fields = ( 75 | "id", "status", "customer", "total_price", "final_price" 76 | ) 77 | 78 | 79 | class OrderSerializer(serializers.ModelSerializer): 80 | """ 81 | Serializer for Order Model Show All of the Fields 82 | """ 83 | 84 | customer = CustomerBriefSerializer(read_only=True) 85 | address = serializers.HyperlinkedRelatedField(view_name="api:address_detail", 86 | read_only=True, lookup_field='zip_code', lookup_url_kwarg='code') 87 | discount = DiscountCodeBriefSerializer(read_only=True) 88 | items = OrderItemBriefSerializer(read_only=True, many=True) 89 | 90 | class Meta: 91 | model = Order 92 | fields = ( 93 | "id", "status", "customer", "address", 94 | "total_price", "final_price", "code", "discount", "items" 95 | ) 96 | -------------------------------------------------------------------------------- /api/views/view_product.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from rest_framework import generics 4 | from rest_framework.views import APIView 5 | from rest_framework.response import Response 6 | from rest_framework.decorators import api_view 7 | 8 | from core.permissions import * 9 | from product.serializers import * 10 | 11 | # Create your views here. 12 | class BrandListAPIView(generics.ListCreateAPIView): 13 | """ 14 | View for See List of Brands & Create New if User is Staff 15 | """ 16 | 17 | serializer_class = BrandBriefSerializer 18 | queryset = Brand.objects.all() 19 | permission_classes = [ 20 | IsStaffUser 21 | ] 22 | 23 | 24 | class BrandDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 25 | """ 26 | View for See Details of a Brand by Slug Field for Staff Users 27 | """ 28 | 29 | serializer_class = BrandSerializer 30 | queryset = Brand.objects.all() 31 | lookup_field = 'slug' 32 | lookup_url_kwarg = 'name' 33 | permission_classes = [ 34 | IsStaffUser 35 | ] 36 | 37 | 38 | class DiscountListAPIView(generics.ListCreateAPIView): 39 | """ 40 | View for See List of Discount & Create New if User is Staff 41 | """ 42 | 43 | serializer_class = DiscountBriefSerializer 44 | queryset = Discount.objects.exclude(has_code=True) 45 | permission_classes = [ 46 | IsStaffUser 47 | ] 48 | 49 | 50 | class DiscountDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 51 | """ 52 | View for See Details of a Discount by Slug Field for Staff Users 53 | """ 54 | 55 | serializer_class = DiscountSerializer 56 | queryset = Discount.objects.exclude(has_code=True) 57 | lookup_field = 'slug' 58 | lookup_url_kwarg = 'name' 59 | permission_classes = [ 60 | IsStaffUser 61 | ] 62 | 63 | 64 | class CategoryListAPIView(generics.ListCreateAPIView): 65 | """ 66 | Show Breif List Information of All Categories 67 | """ 68 | 69 | serializer_class = CategoryBriefSerializer 70 | queryset = Category.objects.all() 71 | permission_classes = [ 72 | IsStaffUser 73 | ] 74 | 75 | 76 | class CategoryDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 77 | """ 78 | View for Show Completely of Informations of a Category 79 | """ 80 | 81 | serializer_class = CategorySerializer 82 | queryset = Category.objects.all() 83 | lookup_field = 'slug' 84 | lookup_url_kwarg = 'name' 85 | permission_classes = [ 86 | IsStaffUser 87 | ] 88 | 89 | 90 | class ProductListAPIView(generics.ListCreateAPIView): 91 | """ 92 | Show Breif List Information of All Product Items 93 | """ 94 | 95 | serializer_class = ProductBriefSerializer 96 | queryset = Product.objects.all() 97 | permission_classes = [ 98 | IsStaffUser 99 | ] 100 | 101 | 102 | class ProductDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 103 | """ 104 | View for Show Completely of Informations of a Product Item 105 | """ 106 | 107 | serializer_class = ProductSerializer 108 | queryset = Product.objects.all() 109 | lookup_field = 'slug' 110 | lookup_url_kwarg = 'name' 111 | permission_classes = [ 112 | IsStaffUser 113 | ] 114 | -------------------------------------------------------------------------------- /static/shared/img/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /api/views/view_order.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from rest_framework import generics, viewsets 4 | from rest_framework.views import APIView 5 | from rest_framework.response import Response 6 | from rest_framework.decorators import api_view 7 | 8 | from core.permissions import * 9 | from order.serializers import * 10 | 11 | # Create your views here. 12 | class DiscountCodeListAPIView(generics.ListCreateAPIView): 13 | """ 14 | View for See List of Discount Code & Create New if User is Staff 15 | """ 16 | 17 | serializer_class = DiscountCodeBriefSerializer 18 | queryset = DiscountCode.objects.all() 19 | permission_classes = [ 20 | IsOwnerSite 21 | ] 22 | 23 | 24 | class DiscountCodeDetailAPIView(generics.RetrieveUpdateDestroyAPIView): 25 | """ 26 | View for See Details of a Discount Code by Code Field for Staff Users 27 | """ 28 | 29 | serializer_class = DiscountCodeSerializer 30 | queryset = DiscountCode.objects.all() 31 | lookup_field = 'code' 32 | lookup_url_kwarg = 'code' 33 | permission_classes = [ 34 | IsOwnerSite 35 | ] 36 | 37 | 38 | class OrderListAPIView(viewsets.ModelViewSet): 39 | """ 40 | View for See List of Orders Just if User is Staff 41 | """ 42 | 43 | serializer_class = OrderBriefSerializer 44 | queryset = Order.objects.all() 45 | permission_classes = [ 46 | IsStaffAuthenticated 47 | ] 48 | 49 | def get_queryset(self): 50 | user = self.request.user 51 | result = super().get_queryset() 52 | if not user.is_staff: 53 | result = result.filter(customer__username=user.username) 54 | return result 55 | 56 | 57 | class OrderDetailAPIView(viewsets.ModelViewSet): 58 | """ 59 | View for See Details of a Order by Recepite Number Just is Onwer or Staff 60 | """ 61 | 62 | serializer_class = OrderSerializer 63 | queryset = Order.objects.all() 64 | lookup_field = 'id' 65 | lookup_url_kwarg = 'number' 66 | permission_classes = [ 67 | IsOwnerUser 68 | ] 69 | 70 | 71 | class OrderItemListAPIView(viewsets.ModelViewSet): 72 | """ 73 | View for See List of Order Items Just if User is Staff 74 | """ 75 | 76 | serializer_class = OrderItemBriefSerializer 77 | queryset = OrderItem.objects.all() 78 | permission_classes = [ 79 | IsCustomerOwner 80 | ] 81 | 82 | def get_queryset(self): 83 | user = self.request.user 84 | result = super().get_queryset() 85 | if not user.is_staff: 86 | result = result.filter(order__customer__username=user.username) 87 | return result 88 | 89 | def perform_create(self, serializer): 90 | customer = Customer.objects.get(username=self.request.user.username) 91 | try: 92 | order = customer.orders.get(status__exact='U') 93 | except Order.DoesNotExist: 94 | order = Order.objects.create( 95 | customer=customer, 96 | address=customer.addresses.first()) 97 | serializer.save(order=order) 98 | 99 | 100 | class OrderItemDetailAPIView(viewsets.ModelViewSet): 101 | """ 102 | View for See Details of a Order Item Just User is Onwer of Order or is Staff 103 | """ 104 | 105 | serializer_class = OrderItemSerializer 106 | queryset = OrderItem.objects.all() 107 | lookup_field = 'id' 108 | lookup_url_kwarg = 'number' 109 | permission_classes = [ 110 | IsCustomerOwnerParent 111 | ] 112 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-08-21 15:48 2 | 3 | import core.models 4 | import core.validators 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('auth', '0012_alter_user_first_name_max_length'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='TestModel', 21 | fields=[ 22 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('deleted', models.BooleanField(db_index=True, default=False)), 24 | ('create_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Create TimeStamp')), 25 | ('modify_timestamp', models.DateTimeField(auto_now=True, verbose_name='Modify TimeStamp')), 26 | ('delete_timestamp', models.DateTimeField(blank=True, default=None, null=True)), 27 | ], 28 | options={ 29 | 'abstract': False, 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='User', 34 | fields=[ 35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('password', models.CharField(max_length=128, verbose_name='password')), 37 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 38 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 39 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 40 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 41 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 42 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 43 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 44 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 45 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 46 | ('phone_number', models.CharField(help_text='Please Enter Your Phone Number', max_length=11, unique=True, validators=[core.validators.Validators.check_phone_number], verbose_name='Phone Number')), 47 | ('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')), 48 | ('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')), 49 | ], 50 | options={ 51 | 'verbose_name': 'User', 52 | 'verbose_name_plural': 'Users', 53 | }, 54 | managers=[ 55 | ('objects', core.models.MyUserManager()), 56 | ], 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /customer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-08-21 15:48 2 | 3 | import core.models 4 | import core.validators 5 | import customer.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('core', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Customer', 21 | fields=[ 22 | ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.user')), 23 | ('photo', models.FileField(blank=True, default='Unknown.jpg', help_text='Please Upload Uour Image if You Wish.', upload_to='customer/customers/', verbose_name='Profile Picture')), 24 | ('gender', models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female'), ('N', 'Non-Binary')], default=None, help_text='Please Select Your Gender if You Wish.', max_length=1, null=True, verbose_name='Gender')), 25 | ('birth_day', models.DateField(blank=True, default=None, help_text='Please Enter Your Birth Day if You Wish.', null=True, validators=[customer.validators.birthday_validator], verbose_name='Birth Day')), 26 | ], 27 | options={ 28 | 'verbose_name': 'Customer', 29 | 'verbose_name_plural': 'Customers', 30 | }, 31 | bases=('core.user',), 32 | managers=[ 33 | ('objects', core.models.MyUserManager()), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='Address', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('deleted', models.BooleanField(db_index=True, default=False)), 41 | ('create_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Create TimeStamp')), 42 | ('modify_timestamp', models.DateTimeField(auto_now=True, verbose_name='Modify TimeStamp')), 43 | ('delete_timestamp', models.DateTimeField(blank=True, default=None, null=True)), 44 | ('name', models.CharField(help_text="Please Enter Your Address' Name", max_length=50, verbose_name="Address' Name")), 45 | ('zip_code', models.CharField(help_text='Please Enter Your Zip Code', max_length=10, unique=True, validators=[core.validators.Validators.check_postal_code], verbose_name='Zip Code')), 46 | ('country', models.CharField(default='ایران', help_text='Please Enter Your Country(By Default Iran is Considered).', max_length=10, verbose_name='Country')), 47 | ('province', models.CharField(help_text='Please Enter Your Province For Example Tehran or Alborz or ...', max_length=10, verbose_name='Province')), 48 | ('city', models.CharField(help_text='Please Enter Your City For Example Tehran or Karaj or ...', max_length=10, verbose_name='City')), 49 | ('rest', models.TextField(help_text='Please Enter the Rest of Your Address Accurately', verbose_name='Continue Address(Street and Alley)')), 50 | ('lat', models.FloatField(help_text='Please Enter Your Address Latitude', verbose_name='Latitude')), 51 | ('lng', models.FloatField(help_text='Please Enter Your Address Longitude', verbose_name='Longitude')), 52 | ('customer', models.ForeignKey(help_text='Please Select Customer Owner this Address', on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='customer.customer', verbose_name='Customer')), 53 | ], 54 | options={ 55 | 'verbose_name': 'Address', 56 | 'verbose_name_plural': 'Addresses', 57 | 'unique_together': {('lat', 'lng')}, 58 | }, 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /customer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.timezone import now 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from core.models import BasicModel, User 6 | from core.validators import Validators 7 | from .validators import birthday_validator 8 | 9 | # Create your models here. 10 | class Customer(User): 11 | """ 12 | Customer Model is Like User But Can't Enter Admin Panel.\n 13 | Customer Just Can See Products and Categories and Create New Orders. 14 | """ 15 | 16 | class Meta: 17 | verbose_name, verbose_name_plural = _("Customer"), _("Customers") 18 | 19 | GENDERS = { 20 | 'M': _("Male"), 21 | 'F': _("Female"), 22 | 'N': _("Non-Binary"), 23 | } 24 | 25 | photo = models.FileField(upload_to="customer/customers/", verbose_name=_("Profile Picture"), 26 | default="Unknown.jpg", blank=True, help_text=_("Please Upload Uour Image if You Wish.")) 27 | gender = models.CharField(max_length=1, default=None, null=True, blank=True, 28 | choices=[(key, value) for key, value in GENDERS.items()], 29 | verbose_name=_("Gender"), help_text=_("Please Select Your Gender if You Wish.")) 30 | birth_day = models.DateField(default=None, null=True, blank=True, verbose_name=_("Birth Day"), 31 | validators=[birthday_validator], help_text=_("Please Enter Your Birth Day if You Wish.")) 32 | 33 | @property 34 | def age(self) -> int: 35 | """ 36 | Property Method to Calculate & Save the Age of Customer from Birth Day's 37 | """ 38 | 39 | if self.birth_day is not None: 40 | return (now().date() - self.birth_day).days // 365 41 | 42 | @property 43 | def gender_type(self): 44 | """ 45 | Get Readable Gender Name for Show in Profile Page View so Property Method 46 | """ 47 | 48 | return self.__class__.GENDERS[self.gender] if self.gender is not None else '-' 49 | 50 | def delete(self): 51 | self.is_active = False 52 | self.save() 53 | 54 | 55 | class Address(BasicModel): 56 | """ 57 | Address Model Includes Country, Province & City and belongs to Customers 58 | """ 59 | 60 | class Meta: 61 | verbose_name, verbose_name_plural = _("Address"), _("Addresses") 62 | unique_together = ('lat', 'lng') 63 | 64 | customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="addresses", 65 | verbose_name=_("Customer"), help_text=_("Please Select Customer Owner this Address")) 66 | name = models.CharField(max_length=50, verbose_name=_("Address' Name"), 67 | help_text=_("Please Enter Your Address' Name")) 68 | zip_code = models.CharField(max_length=10, unique=True, verbose_name=_("Zip Code"), 69 | validators=[Validators.check_postal_code], help_text=_("Please Enter Your Zip Code")) 70 | country = models.CharField(max_length=10, default="ایران", verbose_name=_("Country"), 71 | help_text=_("Please Enter Your Country(By Default Iran is Considered).")) 72 | province = models.CharField(max_length=10, verbose_name=_("Province"), 73 | help_text=_("Please Enter Your Province For Example Tehran or Alborz or ...")) 74 | city = models.CharField(max_length=10, verbose_name=_("City"), 75 | help_text=_("Please Enter Your City For Example Tehran or Karaj or ...")) 76 | rest = models.TextField(verbose_name=_("Continue Address(Street and Alley)"), 77 | help_text=_("Please Enter the Rest of Your Address Accurately")) 78 | lat = models.FloatField(verbose_name=_("Latitude"), 79 | help_text=_("Please Enter Your Address Latitude")) 80 | lng = models.FloatField(verbose_name=_("Longitude"), 81 | help_text=_("Please Enter Your Address Longitude")) 82 | 83 | def __str__(self) -> str: 84 | zipcode_trans = _("Zip Code") 85 | return f"{self.name}: {self.province} - {self.city}({zipcode_trans}: {self.zip_code})" 86 | -------------------------------------------------------------------------------- /product/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import * 4 | 5 | class BrandBriefSerializer(serializers.ModelSerializer): 6 | """ 7 | Brief Serializer for Brand Model Show Important Fields 8 | """ 9 | 10 | class Meta: 11 | model = Brand 12 | fields = ( 13 | "id", "title_en", "title_fa", "slug" 14 | ) 15 | 16 | 17 | class BrandSerializer(serializers.ModelSerializer): 18 | """ 19 | Serializer for Brand Model Show All of the Fields 20 | """ 21 | 22 | products = serializers.StringRelatedField(read_only=True, many=True) 23 | 24 | class Meta: 25 | model = Brand 26 | fields = ( 27 | "id", "title_en", "title_fa", "slug", "logo", "link", "products" 28 | ) 29 | 30 | 31 | class DiscountBriefSerializer(serializers.ModelSerializer): 32 | """ 33 | Brief Serializer for Discount Model Show Important Fields 34 | """ 35 | 36 | class Meta: 37 | model = Discount 38 | fields = ( 39 | "id", "title_en", "title_fa", "slug", "unit", "amount", "roof" 40 | ) 41 | 42 | 43 | class DiscountSerializer(serializers.ModelSerializer): 44 | """ 45 | Serializer for Discount Model Show All of the Fields 46 | """ 47 | 48 | class Meta: 49 | model = Discount 50 | fields = ( 51 | "id", "title_en", "title_fa", "slug", 52 | "unit", "amount", "roof", "start_date", "end_date" 53 | ) 54 | 55 | 56 | class CategoryBriefSerializer(serializers.ModelSerializer): 57 | """ 58 | Show Breif Information of Category for Use in Other Models 59 | """ 60 | 61 | class Meta: 62 | model = Category 63 | fields = ("id", "title_en", "title_fa", "slug") 64 | 65 | class CategorySerializer(serializers.ModelSerializer): 66 | """ 67 | Show Full Information and Fields of Category for Use in Self 68 | """ 69 | 70 | properties = serializers.ReadOnlyField(source='property_dict') 71 | root = CategoryBriefSerializer(read_only=True) 72 | subcategories = CategoryBriefSerializer(read_only=True, many=True) 73 | 74 | class Meta: 75 | model = Category 76 | fields = ( 77 | "id", "title_en", "title_fa", "slug", "root", "properties", "subcategories" 78 | ) 79 | 80 | 81 | class ProductBriefSerializer(serializers.ModelSerializer): 82 | """ 83 | Show Breif Information of Product for Show in List Page 84 | """ 85 | 86 | class Meta: 87 | model = Product 88 | fields = ( 89 | "id", "title_en", "title_fa", "slug", "category", "brand", 90 | "price", "inventory", "image", "discount", 91 | ) 92 | extra_kwargs = { 93 | 'category': { 94 | "write_only": True, 95 | }, 96 | 'brand': { 97 | "write_only": True, 98 | }, 99 | 'inventory': { 100 | "write_only": True, 101 | }, 102 | 'image': { 103 | "write_only": True, 104 | }, 105 | 'discount': { 106 | "write_only": True, 107 | }, 108 | } 109 | 110 | 111 | class ProductSerializer(serializers.ModelSerializer): 112 | """ 113 | Show Full Information and Fields of Products for Use in Detail Page 114 | """ 115 | 116 | category = serializers.HyperlinkedRelatedField(view_name="api:category_detail", 117 | read_only=True, lookup_field='slug', lookup_url_kwarg='name') 118 | brand = serializers.HyperlinkedRelatedField(view_name="api:brand_detail", 119 | read_only=True, lookup_field='slug', lookup_url_kwarg='name') 120 | properties = serializers.ReadOnlyField(source='property_dict') 121 | 122 | class Meta: 123 | model = Product 124 | fields = ( 125 | "id", "title_en", "title_fa", "slug", "category", "brand", "price", 126 | "inventory", "image", "discount", "properties" 127 | ) 128 | -------------------------------------------------------------------------------- /product/tests/test_discount.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils.timezone import now, timedelta 3 | 4 | from ..models import * 5 | 6 | # Create your tests here. 7 | class TestDiscountModel(TestCase): 8 | def setUp(self): 9 | self.info = { 10 | 'title_en': "Test", 11 | 'title_fa': "تست", 12 | 'slug': "test", 13 | } 14 | 15 | # tests for discount with toman unit and discount < price 16 | def test_toman_positive_1(self): 17 | d = Discount.objects.create(**self.info, unit='T', amount=5_000) 18 | self.assertEqual(d.calculate_price(75_000), 70_000) 19 | 20 | def test_toman_positive_2(self): 21 | d = Discount.objects.create(**self.info, unit='T', amount=25_000) 22 | self.assertEqual(d.calculate_price(55_000), 30_000) 23 | 24 | def test_toman_positive_3(self): 25 | d = Discount.objects.create(**self.info, unit='T', amount=7_000) 26 | self.assertEqual(d.calculate_price(17_500), 10_500) 27 | 28 | # tests for discount with toman unit and discount > price 29 | def test_toman_zero_1(self): 30 | d = Discount.objects.create(**self.info, unit='T', amount=12_000) 31 | self.assertEqual(d.calculate_price(10_000), 0) 32 | 33 | # tests for discount with percent unit and without ceiling value 34 | def test_percent_without_roof_1(self): 35 | d = Discount.objects.create(**self.info, unit='P', amount=12) 36 | self.assertEqual(d.calculate_price(250_000), 220_000) 37 | 38 | def test_percent_without_roof_2(self): 39 | d = Discount.objects.create(**self.info, unit='P', amount=50) 40 | self.assertEqual(d.calculate_price(100_000), 50_000) 41 | 42 | def test_percent_without_roof_3(self): 43 | d = Discount.objects.create(**self.info, unit='P', amount=2) 44 | self.assertEqual(d.calculate_price(12_000), 11_760) 45 | 46 | def test_percent_without_roof_4(self): 47 | d = Discount.objects.create(**self.info, unit='P', amount=15) 48 | self.assertEqual(d.calculate_price(1_000_000), 850_000) 49 | 50 | # tests for discount with percent unit and with ceiling value but less than 51 | def test_percent_with_roof_fewer_1(self): 52 | d = Discount.objects.create(**self.info, unit='P', amount=10, roof=20_000) 53 | self.assertEqual(d.calculate_price(120_000), 108_000) 54 | 55 | def test_percent_with_roof_fewer_2(self): 56 | d = Discount.objects.create(**self.info, unit='P', amount=5, roof=10_000) 57 | self.assertEqual(d.calculate_price(40_000), 38_000) 58 | 59 | def test_percent_with_roof_fewer_3(self): 60 | d = Discount.objects.create(**self.info, unit='P', amount=50, roof=25_000) 61 | self.assertEqual(d.calculate_price(20_000), 10_000) 62 | 63 | # tests for discount with percent unit and with ceiling value but not less than 64 | def test_percent_with_roof_bigger_1(self): 65 | d = Discount.objects.create(**self.info, unit='P', amount=25, roof=15_000) 66 | self.assertEqual(d.calculate_price(80_000), 65_000) 67 | 68 | # tests for discount with start date time in the future so dont apply on price 69 | def test_datetime_start_future_1(self): 70 | d = Discount.objects.create(**self.info, unit='T', amount=12_000, 71 | start_date=now() + timedelta(days=1)) 72 | self.assertEqual(d.calculate_price(10_000), 10_000) 73 | 74 | # tests for discount with end date time in the past so dont apply on price 75 | def test_datetime_end_past_1(self): 76 | d = Discount.objects.create(**self.info, unit='P', amount=25, roof=15_000, 77 | start_date=now(), end_date=now() - timedelta(days=1)) 78 | self.assertEqual(d.calculate_price(80_000), 80_000) 79 | 80 | # tests for discount with has end time in the future and similar to null value 81 | def test_datetime_end_not_null_1(self): 82 | d = Discount.objects.create(**self.info, unit='P', amount=10, roof=20_000, 83 | start_date=now(), end_date=now() + timedelta(days=1)) 84 | self.assertEqual(d.calculate_price(120_000), 108_000) 85 | -------------------------------------------------------------------------------- /order/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from core.admin import * 5 | from product.admin import DiscountAdmin 6 | from .models import * 7 | 8 | # Register your models here. 9 | class OrderItemInlineAdmin(admin.TabularInline): 10 | """ 11 | Create New Order Item Instance in Order Admin Page with Tabular Inline Model 12 | """ 13 | 14 | model = OrderItem 15 | fields = [('product', 'count')] 16 | exclude = ['deleted', 'delete_timestamp'] 17 | verbose_name_plural = _("Order Items") 18 | extra = 1 19 | 20 | 21 | @admin.register(DiscountCode) 22 | class DiscountCodeAdmin(DiscountAdmin): 23 | """ 24 | Manage Discount Code Class Model and Show Fields in Panel Admin 25 | """ 26 | 27 | exclude = DiscountAdmin.exclude + ['users'] 28 | fieldsets = [(None, {'fields': ('code',)}),] + DiscountAdmin.fieldsets 29 | list_display = DiscountAdmin.list_display + ['code'] 30 | list_filter = DiscountAdmin.list_filter + ['code'] 31 | 32 | @admin.action(description=_("Beginning Selected Discount Codes")) 33 | def beginning(self, request, queryset): 34 | """ 35 | Action for Change Start Date of Selected Discount Codes to Now 36 | """ 37 | 38 | updated = queryset.update(start_date=timezone.now()) 39 | if updated == 1: 40 | message = _(" Discount Code was Successfully Beginning.") 41 | else: 42 | message = _(" Discount Codes were Successfully Beginning.") 43 | self.message_user(request, str(updated) + message) 44 | 45 | @admin.action(description=_("Finishing Selected Discount Codes")) 46 | def finishing(self, request, queryset): 47 | """ 48 | Action for Change End Date of Selected Discount Codes to Now 49 | """ 50 | 51 | updated = queryset.update(end_date=timezone.now()) 52 | if updated == 1: 53 | message = _(" Discount Code was Successfully Finishing.") 54 | else: 55 | message = _(" Discount Codes were Successfully Finishing.") 56 | self.message_user(request, str(updated) + message) 57 | 58 | 59 | @admin.register(Order) 60 | class OrderAdmin(BasicAdmin): 61 | """ 62 | Manage Order Class Model and Show Fields in Panel Admin 63 | """ 64 | 65 | exclude = BasicAdmin.exclude + ['discount'] 66 | fieldsets = ( 67 | (None, { 68 | 'fields': ( 69 | ('customer', 'address'), 70 | ('total_price', 'final_price'), 71 | ('code', 'status') 72 | ), 73 | }), 74 | ) 75 | readonly_fields = ['total_price', 'final_price'] 76 | list_display = ('__str__', 'customer', 'total_price', 'final_price', 'status', 'discount') 77 | list_filter = ('status',) 78 | search_fields = ('customer',) 79 | ordering = ('-id',) 80 | inlines = [OrderItemInlineAdmin] 81 | actions = ['paymenting', 'canceling'] 82 | 83 | @admin.action(description=_("Paymenting Selected Order")) 84 | def paymenting(self, request, queryset): 85 | """ 86 | Action for Change Status of Selected Orders to Paid 87 | """ 88 | 89 | updated = queryset.count() 90 | if updated == 1: message = _(" Order was Successfully Paiding.") 91 | else: message = _(" Orders were Successfully Paiding.") 92 | for order in queryset: order.payment() 93 | self.message_user(request, str(updated) + message) 94 | 95 | @admin.action(description=_("Canceling Selected Order")) 96 | def canceling(self, request, queryset): 97 | """ 98 | Action for Change Status of Selected Orders to Canceled 99 | """ 100 | 101 | updated = queryset.count() 102 | if updated == 1: message = _(" Order was Successfully Canceling.") 103 | else: message = _(" Orders were Successfully Canceling.") 104 | for order in queryset: order.cancel() 105 | self.message_user(request, str(updated) + message) 106 | 107 | 108 | @admin.register(OrderItem) 109 | class OrderItemAdmin(BasicAdmin): 110 | """ 111 | Manage Order Items Class Model and Show Fields in Panel Admin 112 | """ 113 | 114 | fieldsets = ( 115 | (None, { 116 | 'fields': (('order'), ('product', 'count')) 117 | }), 118 | ) 119 | list_display = ('__str__', 'order', 'product', 'count') 120 | search_fields = ('order', 'product') 121 | ordering = ('-id',) 122 | -------------------------------------------------------------------------------- /templates/order/orders.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Orders" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | font-size: 13px; 22 | display: block; 23 | margin-bottom: 4px; 24 | overflow-wrap: break-word; 25 | } 26 | 27 | {% endblock %} 28 | 29 | {% block body %} 30 |
31 |

32 | 33 | {% trans "All Orders List" %} 34 | 35 | 36 | 37 | {% trans "Paid Orders List" %} 38 | 39 | 40 | 41 | {% trans "Canceled Orders List" %} 42 | 43 |

44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for order in orders %} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 114 | 115 | {% endfor %} 116 | 117 |
#{% trans "Total Price" %}({% trans "Toman" %}){% trans "Final Price" %}({% trans "Toman" %}){% trans "Discount Code" %}{% trans "Status" %}{% trans "Details" %}
{{ forloop.counter }}{{ order.readable_total_price }}{{ order.readable_final_price }}{% if order.code %}{{ order.code }}{% else %}-{% endif %}{{ order.status_name }} 67 | 70 | 71 | 72 | 112 | 113 |
118 | 119 |
120 | {% endblock %} 121 | -------------------------------------------------------------------------------- /order/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-08-21 15:48 2 | 3 | import django.core.validators 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 | ('product', '0001_initial'), 14 | ('customer', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='DiscountCode', 20 | fields=[ 21 | ('discount_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='product.discount')), 22 | ('code', models.CharField(help_text='Please Enter Identification Code to Check Validation in Order Model', max_length=10, unique=True, verbose_name='Identification Code')), 23 | ('users', models.ManyToManyField(default=None, help_text='Show Which Users have Used this Code?', null=True, related_name='codes', to='customer.Customer', verbose_name='Users Used')), 24 | ], 25 | options={ 26 | 'verbose_name': 'Discount Code', 27 | 'verbose_name_plural': 'Discount Codes', 28 | }, 29 | bases=('product.discount',), 30 | ), 31 | migrations.CreateModel( 32 | name='Order', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('deleted', models.BooleanField(db_index=True, default=False)), 36 | ('create_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Create TimeStamp')), 37 | ('modify_timestamp', models.DateTimeField(auto_now=True, verbose_name='Modify TimeStamp')), 38 | ('delete_timestamp', models.DateTimeField(blank=True, default=None, null=True)), 39 | ('status', models.CharField(choices=[('P', 'Paid'), ('U', 'Unpaid'), ('C', 'Canceled')], default='U', help_text='Please Select the Status of Order(By Default Unpaid is Considered)', max_length=1, verbose_name='Status of Order')), 40 | ('total_price', models.PositiveBigIntegerField(default=0, help_text='Total Price is Sum of Pure Price of Product Items for this Order', verbose_name='Total Price')), 41 | ('final_price', models.PositiveBigIntegerField(default=0, help_text='Final Price is Sum of Price of Product Items for this Order After Dicount', verbose_name='Final Price')), 42 | ('code', models.CharField(blank=True, default=None, help_text='If You have a Discount Code Please Enter it', max_length=10, null=True, verbose_name='Discount Code')), 43 | ('address', models.ForeignKey(help_text='Please Select Address to Send Order There', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='customer.address', verbose_name='Address')), 44 | ('customer', models.ForeignKey(help_text='Please Select Customer Owner this Order', on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='customer.customer', verbose_name='Customer')), 45 | ('discount', models.ForeignKey(blank=True, default=None, help_text='Please Select Discount from Discount Codes to Apply on Price', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='order.discountcode', verbose_name='Discount Value')), 46 | ], 47 | options={ 48 | 'verbose_name': 'Order', 49 | 'verbose_name_plural': 'Orders', 50 | }, 51 | ), 52 | migrations.CreateModel( 53 | name='OrderItem', 54 | fields=[ 55 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 56 | ('deleted', models.BooleanField(db_index=True, default=False)), 57 | ('create_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Create TimeStamp')), 58 | ('modify_timestamp', models.DateTimeField(auto_now=True, verbose_name='Modify TimeStamp')), 59 | ('delete_timestamp', models.DateTimeField(blank=True, default=None, null=True)), 60 | ('count', models.PositiveIntegerField(default=1, help_text='Please Selcet the Count of this Order Item(Minimum Value is 1).', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Count of Order Item')), 61 | ('order', models.ForeignKey(help_text='Please Select Your Order', on_delete=django.db.models.deletion.CASCADE, related_name='items', to='order.order', verbose_name='Recepite Order')), 62 | ('product', models.ForeignKey(help_text='Please Select Product Item to Add', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='product.product', verbose_name='Product Item')), 63 | ], 64 | options={ 65 | 'verbose_name': 'Order Item', 66 | 'verbose_name_plural': 'Order Items', 67 | }, 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 3 | 4 | 5 | {% trans "Sepehr Shopping" %} | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | {% if not is_popup and is_nav_sidebar_enabled %} 10 | 11 | 12 | {% endif %} 13 | {% block extrastyle %}{% endblock %} 14 | {% if LANGUAGE_BIDI %}{% endif %} 15 | {% block extrahead %}{% endblock %} 16 | {% block responsive %} 17 | 18 | 19 | {% if LANGUAGE_BIDI %}{% endif %} 20 | {% endblock %} 21 | {% block blockbots %}{% endblock %} 22 | 23 | {% load i18n %} 24 | 25 | 27 | 28 | 29 |
30 | 31 | {% if not is_popup %} 32 | 33 | 73 | 74 | {% block breadcrumbs %} 75 | 79 | {% endblock %} 80 | {% endif %} 81 | 82 |
83 | {% if not is_popup and is_nav_sidebar_enabled %} 84 | {% block nav-sidebar %} 85 | {% include "admin/nav_sidebar.html" %} 86 | {% endblock %} 87 | {% endif %} 88 |
89 | {% block messages %} 90 | {% if messages %} 91 |
    {% for message in messages %} 92 | {{ message|capfirst }} 93 | {% endfor %}
94 | {% endif %} 95 | {% endblock messages %} 96 | 97 |
98 | {% block pretitle %}{% endblock %} 99 | {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} 100 | {% block content_subtitle %}{% if subtitle %}

{{ subtitle }}

{% endif %}{% endblock %} 101 | {% block content %} 102 | {% block object-tools %}{% endblock %} 103 | {{ content }} 104 | {% endblock %} 105 | {% block sidebar %}{% endblock %} 106 |
107 |
108 | 109 | {% block footer %}{% endblock %} 110 |
111 |
112 |
113 | 114 | 115 | 116 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /product/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils import timezone 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from core.admin import * 6 | from .models import * 7 | 8 | # Register your models here. 9 | class BasicTabularInline(admin.TabularInline): 10 | """ 11 | Basic Tabular Model Admin for Inheritance All Other Models 12 | """ 13 | 14 | fields = [('title_fa', 'title_en', 'slug')] 15 | exclude = ['deleted', 'delete_timestamp'] 16 | prepopulated_fields = { 17 | 'slug': ('title_en',), 18 | } 19 | extra = 1 20 | 21 | 22 | class BasicStackedInline(admin.StackedInline): 23 | """ 24 | Basic Stacked Model Admin for Inheritance All Other Models 25 | """ 26 | 27 | fields = ['title_fa', 'title_en', 'slug'] 28 | exclude = ['deleted', 'delete_timestamp'] 29 | prepopulated_fields = { 30 | 'slug': ('title_en',), 31 | } 32 | extra = 1 33 | 34 | 35 | class CategoryInline(BasicTabularInline): 36 | """ 37 | Create New Category Instance in Category Admin Page with Tabular Inline Model 38 | """ 39 | 40 | model = Category 41 | exclude = BasicTabularInline.exclude + ['properties'] 42 | verbose_name_plural = _("Subcategories") 43 | 44 | 45 | class ProductInline(BasicStackedInline): 46 | """ 47 | Create New Product Instance in Brand Admin Page with Stacked Inline Model 48 | """ 49 | 50 | model = Product 51 | exclude = BasicStackedInline.exclude + ['properties'] 52 | verbose_name_plural = _("Manufactured Products") 53 | fields = BasicTabularInline.fields + [ 54 | 'category', 55 | ('image', 'inventory'), 56 | ('price', 'discount') 57 | ] 58 | 59 | 60 | class TranslateAdmin(BasicAdmin): 61 | """ 62 | Tranlation Admin Class to Fix Titles & Slug in All Inheritanced Class 63 | """ 64 | 65 | fieldsets = [ 66 | (_("Detail Information"), { 67 | 'fields': ('title_fa', ('title_en', 'slug')), 68 | }), 69 | ] 70 | list_display = ['__str__', 'title_en', 'title_fa'] 71 | ordering = ['id'] 72 | prepopulated_fields = { 73 | 'slug': ('title_en',), 74 | } 75 | 76 | 77 | @admin.register(Category) 78 | class CategoryAdmin(TranslateAdmin): 79 | """ 80 | Manage Category Class Model and Show Fields in Panel Admin 81 | """ 82 | 83 | exclude = TranslateAdmin.exclude + ['properties'] 84 | fieldsets = TranslateAdmin.fieldsets + [ 85 | (_("Optional Information"), { 86 | 'fields': ('root',), 87 | }), 88 | ] 89 | list_display = TranslateAdmin.list_display + ['root'] 90 | inlines = [CategoryInline] 91 | 92 | 93 | @admin.register(Brand) 94 | class BrandAdmin(TranslateAdmin): 95 | """ 96 | Manage Brand Class Model and Show Fields in Panel Admin 97 | """ 98 | 99 | fieldsets = TranslateAdmin.fieldsets + [ 100 | (_("Optional Information"), { 101 | 'fields': ('logo', 'link'), 102 | }), 103 | ] 104 | list_display = TranslateAdmin.list_display + ['link'] 105 | inlines = [ProductInline] 106 | 107 | 108 | @admin.register(Discount) 109 | class DiscountAdmin(TranslateAdmin): 110 | """ 111 | Manage Discount Class Model and Show Fields in Panel Admin 112 | """ 113 | 114 | fieldsets = TranslateAdmin.fieldsets + [ 115 | (_("Value Information"), { 116 | 'fields': [('unit', 'amount', 'roof')], 117 | }), 118 | (_("Date Information"), { 119 | 'classes': ('collapse',), 120 | 'fields': ['has_code', ('start_date', 'end_date')], 121 | }), 122 | ] 123 | list_display = TranslateAdmin.list_display + ['start_date', 'end_date', 'has_code'] 124 | list_filter = ['unit', 'start_date', 'end_date', 'has_code'] 125 | actions = ['beginning', 'finishing'] 126 | 127 | @admin.action(description=_("Beginning Selected Discounts")) 128 | def beginning(self, request, queryset): 129 | """ 130 | Action for Change Start Date of Selected Discounts to Now 131 | """ 132 | 133 | updated = queryset.update(start_date=timezone.now()) 134 | if updated == 1: 135 | message = _(" Discount was Successfully Beginning.") 136 | else: 137 | message = _(" Discounts were Successfully Beginning.") 138 | self.message_user(request, str(updated) + message) 139 | 140 | @admin.action(description=_("Finishing Selected Discounts")) 141 | def finishing(self, request, queryset): 142 | """ 143 | Action for Change End Date of Selected Discounts to Now 144 | """ 145 | 146 | updated = queryset.update(end_date=timezone.now()) 147 | if updated == 1: 148 | message = _(" Discount was Successfully Finishing.") 149 | else: 150 | message = _(" Discounts were Successfully Finishing.") 151 | self.message_user(request, str(updated) + message) 152 | 153 | 154 | @admin.register(Product) 155 | class ProductAdmin(TranslateAdmin): 156 | """ 157 | Manage Product Item Class Model and Show Fields in Panel Admin 158 | """ 159 | 160 | exclude = TranslateAdmin.exclude + ['properties'] 161 | fieldsets = TranslateAdmin.fieldsets + [ 162 | (_("Further information"), { 163 | 'fields': ( 164 | ('image', 'inventory'), 165 | ('category', 'brand'), 166 | ('price', 'discount'), 167 | ), 168 | }), 169 | ] 170 | list_display = TranslateAdmin.list_display + \ 171 | ['category', 'brand', 'price', 'discount', 'final_price'] 172 | list_filter = ('category', 'brand', 'discount') 173 | list_editable = ['price'] 174 | -------------------------------------------------------------------------------- /order/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, HttpResponse 2 | from django.http import JsonResponse 3 | from django.contrib.auth.mixins import LoginRequiredMixin 4 | from django.urls import reverse, reverse_lazy 5 | from django.views import generic, View 6 | from django.utils.translation import gettext_lazy as _ 7 | from django.contrib import messages 8 | 9 | from .models import * 10 | from .forms import * 11 | 12 | # Create your views here. 13 | class BasketCartView(LoginRequiredMixin, View): 14 | """ 15 | View for Show Detail of Basket Cart for Customer 16 | """ 17 | 18 | def get(self, request, *args, **kwargs): 19 | msgs = messages.get_messages(request) 20 | try: customer = Customer.objects.get(id=request.user.id) 21 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 22 | if customer.addresses.count() == 0: 23 | return redirect(reverse("customer:address")) 24 | new_items = set(request.COOKIES.get("cart", "").split(',')[:-1]) 25 | order = customer.orders.filter(status__exact='U') 26 | if order.count() == 1: 27 | order = order[0] 28 | for item in new_items: 29 | product = Product.objects.get(id=int(item)) 30 | if order.items.filter(product=product).count() == 0: 31 | OrderItem.objects.create(order=order, product=product, count=1) 32 | else: 33 | if new_items: 34 | order = Order.objects.create( 35 | customer=customer, address=customer.addresses.first()) 36 | for item in new_items: 37 | product = Product.objects.get(id=int(item)) 38 | OrderItem.objects.create(order=order, product=product, count=1) 39 | else: order = None 40 | form = OrderForm(instance=order) 41 | addresses = customer.addresses.all() 42 | items = [] 43 | if order is not None: 44 | for item in order.items.all(): 45 | items.append((OrderItemForm(instance=item), item)) 46 | resp = render(request, "order/cart.html", { 47 | 'order': order, 'form': form, 'addresses': addresses, 'items': items, 'msgs': msgs, 48 | }) 49 | resp.set_cookie("cart", '') 50 | return resp 51 | 52 | def post(self, request, *args, **kwargs): 53 | try: customer = Customer.objects.get(id=request.user.id) 54 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 55 | order = customer.orders.filter(status='U')[0] 56 | order.address = Address.objects.get(id=int(request.POST["address"])) 57 | form = OrderForm(request.POST) 58 | if form.is_valid(): 59 | order.code = request.POST["code"] or None 60 | order.save() 61 | data, code = order.readable_final_price, 1 62 | else: 63 | order.code = None 64 | order.save() 65 | data, code = form.errors['__all__'].as_data()[0].messages[0], 0 66 | return JsonResponse({'code': code, 'data': data}) 67 | 68 | 69 | class ChangeItemView(LoginRequiredMixin, View): 70 | """ 71 | View for Return Form Order Item Check Validated Change Count 72 | """ 73 | 74 | def post(self, request, *args, **kwargs): 75 | item = OrderItem.objects.get(id=request.GET['item']) 76 | form = OrderItemForm(request.POST) 77 | if form.is_valid(): 78 | item.count = int(request.POST["count"]) 79 | item.save() 80 | code, data = 1, { 81 | 'total': item.order.readable_total_price, 82 | 'final': item.order.readable_final_price, 83 | 'price': item.total_price, 84 | } 85 | else: 86 | key = 'count' if request.POST["count"] == '0' else '__all__' 87 | code, data = 0, form.errors[key].as_data()[0].messages[0] 88 | return JsonResponse({'code': code, 'data': data, 'count': item.count}) 89 | 90 | 91 | class RemoveItemView(LoginRequiredMixin, View): 92 | """ 93 | View for Remove an Item from Basket Cart by Logical Delete 94 | """ 95 | 96 | def get(self, request, *args, **kwargs): 97 | try: customer = Customer.objects.get(id=request.user.id) 98 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 99 | item = OrderItem.objects.get(id=request.GET['item']) 100 | if item.order.customer == customer: item.delete() 101 | return redirect(reverse("order:cart")) 102 | 103 | 104 | class OrdersCustomerView(LoginRequiredMixin, generic.ListView): 105 | """ 106 | Show History of Orders List for Customer Can Filter by Status 107 | """ 108 | 109 | context_object_name = "orders" 110 | template_name = "order/orders.html" 111 | 112 | def get_queryset(self): 113 | try: customer = Customer.objects.get(id=self.request.user.id) 114 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 115 | result = customer.orders.exclude(status__exact='U').order_by("-id") 116 | kwargs = self.request.GET 117 | if "status" in kwargs: 118 | result = result.filter(status__exact=kwargs["status"]) 119 | return result 120 | 121 | 122 | class ChangeCartStatusView(LoginRequiredMixin, View): 123 | """ 124 | View for Change Status of Order to Paid or Canceling 125 | """ 126 | 127 | def get(self, request, *args, **kwargs): 128 | try: customer = Customer.objects.get(id=request.user.id) 129 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 130 | order = customer.orders.get(id=request.GET['order']) 131 | if order.customer == customer: 132 | if request.GET['status'] == 'P': 133 | order.payment() 134 | order.save() 135 | if request.GET['status'] == 'C': 136 | order.cancel() 137 | order.save() 138 | return redirect(reverse("order:orders")) 139 | -------------------------------------------------------------------------------- /customer/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import auth 2 | from django.shortcuts import render, redirect, HttpResponse 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.contrib.auth import authenticate, login, logout 5 | from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView 6 | from django.contrib.auth.mixins import LoginRequiredMixin 7 | from django.urls import reverse, reverse_lazy 8 | from django.views import generic, View 9 | 10 | from .models import Customer 11 | from .forms import * 12 | 13 | # Create your views here. 14 | class CustomerLoginView(LoginView): 15 | """ 16 | Customize Built-in View for Login Customers 17 | """ 18 | 19 | authentication_form = CustomerLoginForm 20 | template_name = "customer/login.html" 21 | 22 | def get(self, request, *args, **kwargs): 23 | if request.user.is_authenticated: 24 | return redirect(reverse("index")) 25 | return super().get(request, *args, **kwargs) 26 | 27 | 28 | class CustomerLogoutView(LogoutView): 29 | """ 30 | Customize Built-in View for Logout Customers 31 | """ 32 | 33 | pass 34 | 35 | 36 | class CustomerRegisterView(generic.FormView): 37 | """ 38 | Generic Class Based View for Register New Customer 39 | """ 40 | 41 | form_class = CustomerRegisterForm 42 | template_name = "customer/register.html" 43 | success_url = reverse_lazy("profile") 44 | 45 | def get(self, request, *args, **kwargs): 46 | if request.user.is_authenticated: 47 | return redirect(reverse("profile")) 48 | return super().get(request, *args, **kwargs) 49 | 50 | def form_valid(self, form): 51 | user = Customer.objects.create_user(username=form.data["username"], 52 | password=form.data["password1"]) 53 | login(self.request, user) 54 | return redirect(reverse("profile")) 55 | 56 | 57 | class ChangePasswordView(LoginRequiredMixin, PasswordChangeView): 58 | """ 59 | Inheritanced from Built-in View for Change Customer Password in New Template 60 | """ 61 | 62 | form_class = CustomerChangePassword 63 | success_url = reverse_lazy("customer:profile") 64 | template_name = "customer/password.html" 65 | 66 | 67 | class CustomerProfileView(LoginRequiredMixin, View): 68 | """ 69 | View for Customer See Profile Detials and Edit it 70 | """ 71 | 72 | def get(self, request, *args, **kwargs): 73 | try: customer = Customer.objects.get(id=request.user.id) 74 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 75 | return render(request, "customer/profile.html", { 76 | 'customer': customer, 77 | }) 78 | 79 | 80 | class CustomerEditProfileView(LoginRequiredMixin, View): 81 | """ 82 | View for Change Customer Information Edit Name & Photo 83 | """ 84 | 85 | def get(self, request, *args, **kwargs): 86 | try: customer = Customer.objects.get(id=request.user.id) 87 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 88 | form = CustomerEditProfileForm(instance=customer) 89 | return render(request, "customer/edit.html", { 90 | 'form': form, 91 | }) 92 | 93 | def post(self, request, *args, **kwargs): 94 | try: customer = Customer.objects.get(id=request.user.id) 95 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 96 | form = CustomerEditProfileForm(instance=customer, data=request.POST, files=request.FILES) 97 | if form.is_valid(): 98 | form.save() 99 | return redirect(reverse("customer:profile")) 100 | return render(request, "customer/edit.html", { 101 | 'form': form, 102 | }) 103 | 104 | 105 | class CreateNewAddressView(LoginRequiredMixin, View): 106 | """ 107 | Authenticated Customer Can Add New Address for Orders in this View 108 | """ 109 | 110 | def get(self, request, *args, **kwargs): 111 | form = AdderssForm() 112 | return render(request, "customer/address.html", { 113 | 'form': form 114 | }) 115 | 116 | def post(self, request, *args, **kwargs): 117 | form = AdderssForm(request.POST) 118 | if form.is_valid(): 119 | new_address = form.save(commit=False) 120 | try: customer = Customer.objects.get(id=request.user.id) 121 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 122 | new_address.customer = customer 123 | new_address.save() 124 | if "cart" in request.GET: 125 | try: cart = customer.orders.get(status__exact='U') 126 | except ObjectDoesNotExist: pass 127 | else: 128 | cart.address = new_address 129 | cart.save() 130 | return redirect(reverse("order:cart")) 131 | return redirect(reverse("customer:profile")) 132 | else: 133 | return render(request, "customer/address.html", { 134 | 'form': form 135 | }) 136 | 137 | 138 | class EditAddressView(LoginRequiredMixin, View): 139 | """ 140 | View for Change Written Addresses of a Customer 141 | """ 142 | 143 | def get(self, request, *args, **kwargs): 144 | try: customer = Customer.objects.get(id=request.user.id) 145 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 146 | address = Address.objects.get(zip_code__exact=kwargs["zip_code"]) 147 | if address.customer == customer: 148 | form = AdderssForm(instance=address) 149 | return render(request, "customer/address.html", { 150 | 'form': form 151 | }) 152 | return redirect(reverse("customer:profile")) 153 | 154 | def post(self, request, *args, **kwargs): 155 | try: customer = Customer.objects.get(id=request.user.id) 156 | except Customer.DoesNotExist: return redirect(reverse("customer:logout")) 157 | address = Address.objects.get(zip_code__exact=kwargs["zip_code"]) 158 | if address.customer == customer: 159 | form = AdderssForm(data=request.POST, instance=address) 160 | if form.is_valid(): 161 | form.save() 162 | return redirect(reverse("customer:profile")) 163 | else: 164 | return render(request, "customer/address.html", { 165 | 'form': form 166 | }) 167 | return redirect(reverse("customer:profile")) 168 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% trans "Sepehr Shopping" %} | {% block title %}{% endblock %} 24 | 25 | 26 | 91 | 92 | 93 | {% block extra_head %}{% endblock %} 94 | 95 | 96 | 97 | 98 | 99 | 148 | 149 | 150 | {% block body %}{% endblock %} 151 | 152 | 153 | {% comment %}
154 |
155 |

© {% trans "Copyright 2021, All Right Reserved Sepehr Bazyar" %}

156 |
157 |
{% endcomment %} 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 168 | 169 | 170 | {% block extra_js %}{% endblock %} 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /shopping/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for shopping project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from django.utils.translation import gettext_lazy as _ 14 | 15 | from pathlib import Path 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'django-insecure-(tm+8z@9likc@2it0qj4!a*^1b*^a4temh*g1)*_z@1)tr27fk' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | # DEBUG = False 33 | 34 | # ALLOWED_HOSTS = ['localhost', '127.0.0.1'] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | # django rest framework api 47 | 'rest_framework', 48 | # my project apps 49 | 'api', 50 | 'core', 51 | 'customer', 52 | 'landing', 53 | 'order', 54 | 'product', 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.common.CommonMiddleware', 61 | 'django.middleware.csrf.CsrfViewMiddleware', 62 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 63 | 'django.contrib.messages.middleware.MessageMiddleware', 64 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 65 | ] 66 | 67 | ROOT_URLCONF = 'shopping.urls' 68 | 69 | TEMPLATES = [ 70 | { 71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 72 | 'DIRS': [BASE_DIR / 'templates'], 73 | 'APP_DIRS': True, 74 | 'OPTIONS': { 75 | 'context_processors': [ 76 | 'django.template.context_processors.debug', 77 | 'django.template.context_processors.request', 78 | 'django.contrib.auth.context_processors.auth', 79 | 'django.contrib.messages.context_processors.messages', 80 | 'core.context_processors.incoming', # custom processor for calculate icoming 81 | ], 82 | }, 83 | }, 84 | ] 85 | 86 | WSGI_APPLICATION = 'shopping.wsgi.application' 87 | 88 | 89 | # Database 90 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 91 | 92 | DATABASES = { 93 | # 'default': { 94 | # 'ENGINE': 'django.db.backends.sqlite3', 95 | # 'NAME': BASE_DIR / 'db.sqlite3', 96 | # } 97 | 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.postgresql', 100 | 'NAME': 'shopdb', 101 | 'USER': 'postgres', 102 | 'PASSWORD': 'sepibzyr79', 103 | 'HOST': 'localhost', 104 | 'PORT': '5432', 105 | } 106 | 107 | } 108 | 109 | 110 | # Password validation 111 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 112 | 113 | AUTH_PASSWORD_VALIDATORS = [ 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 122 | }, 123 | { 124 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 125 | }, 126 | ] 127 | 128 | # Logging 129 | # LOGGING = { 130 | # 'version': 1, 131 | # 'disable_existing_loggers': False, 132 | # 'formatters': { 133 | # "my-style": { 134 | # 'format': "%(asctime)s - %(levelname)-10s - %(message)s", 135 | # 'style': '%', 136 | # }, 137 | # }, 138 | # 'filters': { 139 | # "my-cb-length": { 140 | # '()': 'django.utils.log.CallbackFilter', 141 | # 'callback': lambda record: len(record.getMessage()) <= 100, 142 | # }, 143 | # }, 144 | # 'handlers': { 145 | # 'console': { 146 | # 'class': 'logging.StreamHandler', 147 | # 'formatter': "my-style", 148 | # }, 149 | # 'file': { 150 | # 'class': 'logging.FileHandler', 151 | # 'filename': BASE_DIR / "logs.log", 152 | # 'formatter': "my-style", 153 | # 'filters': ["my-cb-length"], 154 | # }, 155 | # }, 156 | # 'root': { 157 | # 'handlers': ["console"], 158 | # 'level': 'DEBUG', 159 | # }, 160 | # 'loggers': { 161 | # 'clogger': { 162 | # 'handlers': ["console"], 163 | # 'level': "WARNING", 164 | # 'propagate': False, 165 | # }, 166 | # 'flogger': { 167 | # 'handlers': ["file"], 168 | # 'level': "INFO", 169 | # 'propagate': False, 170 | # }, 171 | # }, 172 | # } 173 | 174 | 175 | # Internationalization 176 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 177 | 178 | # LANGUAGE_CODE = 'en' 179 | LANGUAGE_CODE = 'fa' 180 | 181 | LANGUAGES = [ 182 | ('fa', _('Persian')), 183 | ('en', _('English')), 184 | ] 185 | 186 | # TIME_ZONE = 'UTC' 187 | TIME_ZONE = 'Asia/Tehran' 188 | 189 | USE_I18N = True 190 | 191 | USE_L10N = True 192 | 193 | USE_TZ = True 194 | 195 | 196 | # Static files (CSS, JavaScript, Images) 197 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 198 | 199 | STATIC_URL = '/static/' 200 | 201 | STATICFILES_DIRS = [ 202 | BASE_DIR / 'static', 203 | ] 204 | 205 | # Default primary key field type 206 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 207 | 208 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 209 | 210 | LOCALE_PATHS = [ 211 | BASE_DIR / 'locale', 212 | ] 213 | 214 | MEDIA_URL = '/media/' 215 | 216 | MEDIA_ROOT = BASE_DIR / 'media' 217 | # MEDIA_ROOT = "/home/admin/Shopping/media/" 218 | 219 | AUTH_USER_MODEL = 'core.User' 220 | LOGIN_URL = "customer:login" 221 | LOGIN_REDIRECT_URL = "customer:profile" 222 | LOGOUT_REDIRECT_URL = "customer:login" 223 | 224 | REST_FRAMEWORK = { 225 | 'DEFAULT_PAGINATION_CLASS': "rest_framework.pagination.PageNumberPagination", 226 | 'PAGE_SIZE': 10, 227 | } 228 | -------------------------------------------------------------------------------- /static/shared/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | } 89 | 90 | /** 91 | * Add the correct font weight in Chrome, Edge, and Safari. 92 | */ 93 | 94 | b, 95 | strong { 96 | font-weight: bolder; 97 | } 98 | 99 | /** 100 | * 1. Correct the inheritance and scaling of font size in all browsers. 101 | * 2. Correct the odd `em` font sizing in all browsers. 102 | */ 103 | 104 | code, 105 | kbd, 106 | samp { 107 | font-family: monospace, monospace; /* 1 */ 108 | font-size: 1em; /* 2 */ 109 | } 110 | 111 | /** 112 | * Add the correct font size in all browsers. 113 | */ 114 | 115 | small { 116 | font-size: 80%; 117 | } 118 | 119 | /** 120 | * Prevent `sub` and `sup` elements from affecting the line height in 121 | * all browsers. 122 | */ 123 | 124 | sub, 125 | sup { 126 | font-size: 75%; 127 | line-height: 0; 128 | position: relative; 129 | vertical-align: baseline; 130 | } 131 | 132 | sub { 133 | bottom: -0.25em; 134 | } 135 | 136 | sup { 137 | top: -0.5em; 138 | } 139 | 140 | /* Embedded content 141 | ========================================================================== */ 142 | 143 | /** 144 | * Remove the border on images inside links in IE 10. 145 | */ 146 | 147 | img { 148 | border-style: none; 149 | } 150 | 151 | /* Forms 152 | ========================================================================== */ 153 | 154 | /** 155 | * 1. Change the font styles in all browsers. 156 | * 2. Remove the margin in Firefox and Safari. 157 | */ 158 | 159 | button, 160 | input, 161 | optgroup, 162 | select, 163 | textarea { 164 | font-family: inherit; /* 1 */ 165 | font-size: 100%; /* 1 */ 166 | line-height: 1.15; /* 1 */ 167 | margin: 0; /* 2 */ 168 | } 169 | 170 | /** 171 | * Show the overflow in IE. 172 | * 1. Show the overflow in Edge. 173 | */ 174 | 175 | button, 176 | input { /* 1 */ 177 | overflow: visible; 178 | } 179 | 180 | /** 181 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 182 | * 1. Remove the inheritance of text transform in Firefox. 183 | */ 184 | 185 | button, 186 | select { /* 1 */ 187 | text-transform: none; 188 | } 189 | 190 | /** 191 | * Correct the inability to style clickable types in iOS and Safari. 192 | */ 193 | 194 | button, 195 | [type="button"], 196 | [type="reset"], 197 | [type="submit"] { 198 | -webkit-appearance: button; 199 | } 200 | 201 | /** 202 | * Remove the inner border and padding in Firefox. 203 | */ 204 | 205 | button::-moz-focus-inner, 206 | [type="button"]::-moz-focus-inner, 207 | [type="reset"]::-moz-focus-inner, 208 | [type="submit"]::-moz-focus-inner { 209 | border-style: none; 210 | padding: 0; 211 | } 212 | 213 | /** 214 | * Restore the focus styles unset by the previous rule. 215 | */ 216 | 217 | button:-moz-focusring, 218 | [type="button"]:-moz-focusring, 219 | [type="reset"]:-moz-focusring, 220 | [type="submit"]:-moz-focusring { 221 | outline: 1px dotted ButtonText; 222 | } 223 | 224 | /** 225 | * Correct the padding in Firefox. 226 | */ 227 | 228 | fieldset { 229 | padding: 0.35em 0.75em 0.625em; 230 | } 231 | 232 | /** 233 | * 1. Correct the text wrapping in Edge and IE. 234 | * 2. Correct the color inheritance from `fieldset` elements in IE. 235 | * 3. Remove the padding so developers are not caught out when they zero out 236 | * `fieldset` elements in all browsers. 237 | */ 238 | 239 | legend { 240 | box-sizing: border-box; /* 1 */ 241 | color: inherit; /* 2 */ 242 | display: table; /* 1 */ 243 | max-width: 100%; /* 1 */ 244 | padding: 0; /* 3 */ 245 | white-space: normal; /* 1 */ 246 | } 247 | 248 | /** 249 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 250 | */ 251 | 252 | progress { 253 | vertical-align: baseline; 254 | } 255 | 256 | /** 257 | * Remove the default vertical scrollbar in IE 10+. 258 | */ 259 | 260 | textarea { 261 | overflow: auto; 262 | } 263 | 264 | /** 265 | * 1. Add the correct box sizing in IE 10. 266 | * 2. Remove the padding in IE 10. 267 | */ 268 | 269 | [type="checkbox"], 270 | [type="radio"] { 271 | box-sizing: border-box; /* 1 */ 272 | padding: 0; /* 2 */ 273 | } 274 | 275 | /** 276 | * Correct the cursor style of increment and decrement buttons in Chrome. 277 | */ 278 | 279 | [type="number"]::-webkit-inner-spin-button, 280 | [type="number"]::-webkit-outer-spin-button { 281 | height: auto; 282 | } 283 | 284 | /** 285 | * 1. Correct the odd appearance in Chrome and Safari. 286 | * 2. Correct the outline style in Safari. 287 | */ 288 | 289 | [type="search"] { 290 | -webkit-appearance: textfield; /* 1 */ 291 | outline-offset: -2px; /* 2 */ 292 | } 293 | 294 | /** 295 | * Remove the inner padding in Chrome and Safari on macOS. 296 | */ 297 | 298 | [type="search"]::-webkit-search-decoration { 299 | -webkit-appearance: none; 300 | } 301 | 302 | /** 303 | * 1. Correct the inability to style clickable types in iOS and Safari. 304 | * 2. Change font properties to `inherit` in Safari. 305 | */ 306 | 307 | ::-webkit-file-upload-button { 308 | -webkit-appearance: button; /* 1 */ 309 | font: inherit; /* 2 */ 310 | } 311 | 312 | /* Interactive 313 | ========================================================================== */ 314 | 315 | /* 316 | * Add the correct display in Edge, IE 10+, and Firefox. 317 | */ 318 | 319 | details { 320 | display: block; 321 | } 322 | 323 | /* 324 | * Add the correct display in all browsers. 325 | */ 326 | 327 | summary { 328 | display: list-item; 329 | } 330 | 331 | /* Misc 332 | ========================================================================== */ 333 | 334 | /** 335 | * Add the correct display in IE 10+. 336 | */ 337 | 338 | template { 339 | display: none; 340 | } 341 | 342 | /** 343 | * Add the correct display in IE 10. 344 | */ 345 | 346 | [hidden] { 347 | display: none; 348 | } 349 | -------------------------------------------------------------------------------- /templates/order/cart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}{% trans "Cart" %}{% endblock %} 5 | 6 | {% block extra_style %} 7 | 8 | ul > li { 9 | list-style-type: none; 10 | padding: 1px 0; 11 | } 12 | 13 | li, dt, dd { 14 | color: var(--body-quiet-color); 15 | font-size: 13px; 16 | line-height: 20px; 17 | } 18 | 19 | ul.errorlist li { 20 | color: red; 21 | background-color: #B3B6B7; 22 | font-size: 13px; 23 | display: block; 24 | margin-bottom: 4px; 25 | overflow-wrap: break-word; 26 | } 27 | 28 | {% endblock %} 29 | 30 | {% block body %} 31 |
32 | 33 | {% comment %} {% if msgs %} 34 |
    35 | {% for msg in msgs %} 36 | {% if msg.level == DEFAULT_MESSAGE_LEVELS.ERROR %} 37 |
  • {{ msg }}
  • 38 | {% endif %} 39 | {% endfor %} 40 |
41 | 42 |
43 | 44 | {% endif %} {% endcomment %} 45 | 46 | {% if order %} 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% for item in items %} 61 | 62 | {{ item.0.non_field_errors }} 63 | 64 | {% csrf_token %} 65 |
66 | {% for hidden in item.0.hidden_fields %} 67 | {{ hidden }} 68 | {% endfor %} 69 |
70 | 71 | {% for field in item.0.visible_fields %}{% endfor %} 72 | {% comment %} {% endcomment %} 79 | 80 | 81 | 82 | 83 | 92 | 93 | {% endfor %} 94 | 95 |
#{% trans "Product" %}{% trans "Count of Order Item" %}{% trans "Unit Price" %}({% trans "Toman" %}){% trans "Total Price" %}({% trans "Toman" %}){% trans "Change Item" %}
{{ forloop.counter }}{{ item.1.product.title }}{{ field }} 73 | 74 | 76 | {% trans "Remove" %} 77 | 78 | {{ item.1.product.readable_final_price }}{{ item.1.total_price }} 84 | 87 | 89 | {% trans "Remove" %} 90 | 91 |
96 | 97 |

98 | 99 |

{% trans "Total Price" %}: 100 | {{ order.readable_total_price }} {% trans "Toman" %} 101 |

102 |

{% trans "Final Price" %}: 103 | {{ order.readable_final_price }} {% trans "Toman" %} 104 |

105 |
106 | 107 | {{ form.non_field_errors }} 108 |
109 | {% csrf_token %} 110 |
111 | {% for hidden in form.hidden_fields %} 112 | {{ hidden }} 113 | {% endfor %} 114 | 115 |

116 | 125 |

{% trans "Please Select Address to Send Order There" %}

126 | 127 | 128 | {% trans "Add New Address" %} 129 | 130 | 131 | {% for field in form.visible_fields %} 132 | {{ field.errors }} 133 |

{{ field.label_tag }}

{{ field }} 134 | {% if field.help_text %} 135 |

{{ field.help_text|safe }}

136 | {% endif %} 137 | {% endfor %} 138 |

139 | {% comment %} {% endcomment %} 140 |
141 | 142 |
145 | 146 | 148 | {% trans "Payment" %} 149 | 150 | 152 | {% trans "Cancel" %} 153 | 154 |
155 | {% else %} 156 |

{% trans "Empty Basket" %}!

157 | 158 |

{% trans "Hope You Buy Again" %}...

159 | {% endif %} 160 |
161 | {% endblock %} 162 | 163 | {% block extra_js %} 164 | 190 | {% endblock %} 191 | -------------------------------------------------------------------------------- /static/shared/js/modernizr.min.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.11.2 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-cssanimations-csscolumns-customelements-flexbox-history-picture-pointerevents-postmessage-sizes-srcset-webgl-websockets-webworkers-addtest-domprefixes-hasevent-mq-prefixedcssvalue-prefixes-setclasses-testallprops-testprop-teststyles !*/ 3 | !function(e,t,n,r){function o(e,t){return typeof e===t}function i(e){var t=_.className,n=Modernizr._config.classPrefix||"";if(S&&(t=t.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(e.length>0&&(t+=" "+n+e.join(" "+n)),S?_.className.baseVal=t:_.className=t)}function s(e,t){if("object"==typeof e)for(var n in e)k(e,n)&&s(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),o=Modernizr[r[0]];if(2===r.length&&(o=o[r[1]]),void 0!==o)return Modernizr;t="function"==typeof t?t():t,1===r.length?Modernizr[r[0]]=t:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=t),i([(t&&!1!==t?"":"no-")+r.join("-")]),Modernizr._trigger(e,t)}return Modernizr}function a(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):S?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function l(){var e=n.body;return e||(e=a(S?"svg":"body"),e.fake=!0),e}function u(e,t,r,o){var i,s,u,f,c="modernizr",d=a("div"),p=l();if(parseInt(r,10))for(;r--;)u=a("div"),u.id=o?o[r]:c+(r+1),d.appendChild(u);return i=a("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(n.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=_.style.overflow,_.style.overflow="hidden",_.appendChild(p)),s=t(d,e),p.fake?(p.parentNode.removeChild(p),_.style.overflow=f,_.offsetHeight):d.parentNode.removeChild(d),!!s}function f(e,n,r){var o;if("getComputedStyle"in t){o=getComputedStyle.call(t,e,n);var i=t.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&e.currentStyle&&e.currentStyle[r];return o}function c(e,t){return!!~(""+e).indexOf(t)}function d(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function p(e,n){var o=e.length;if("CSS"in t&&"supports"in t.CSS){for(;o--;)if(t.CSS.supports(d(e[o]),n))return!0;return!1}if("CSSSupportsRule"in t){for(var i=[];o--;)i.push("("+d(e[o])+":"+n+")");return i=i.join(" or "),u("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"===f(e,null,"position")})}return r}function m(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function h(e,t,n,i){function s(){u&&(delete N.style,delete N.modElem)}if(i=!o(i,"undefined")&&i,!o(n,"undefined")){var l=p(e,n);if(!o(l,"undefined"))return l}for(var u,f,d,h,A,v=["modernizr","tspan","samp"];!N.style&&v.length;)u=!0,N.modElem=a(v.shift()),N.style=N.modElem.style;for(d=e.length,f=0;f