20 |
21 | {% for parent in parents %}
22 | {% if forloop.counter|divisibleby:"2" %}
23 |
24 | {% else %}
25 |
26 | {% endif %}
27 |
34 |
35 |
36 | {% for child in parent.subcategories.all %}
37 |
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 |
36 |
37 |
38 |
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 |
6 |
7 |
8 |
9 | ### Responsive Mode:
10 |
11 |
12 |
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 |
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 |
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 |
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 | {% trans "Total Price" %}({% trans "Toman" %})
52 | {% trans "Final Price" %}({% trans "Toman" %})
53 | {% trans "Discount Code" %}
54 | {% trans "Status" %}
55 | {% trans "Details" %}
56 |
57 |
58 |
59 | {% for order in orders %}
60 |
61 | {{ forloop.counter }}
62 | {{ order.readable_total_price }}
63 | {{ order.readable_final_price }}
64 | {% if order.code %}{{ order.code }}{% else %}-{% endif %}
65 | {{ order.status_name }}
66 |
67 |
68 | {% trans "Click Me" %}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
85 |
86 | {% trans "Date" %}: {{ order.modify_timestamp }}
87 |
88 |
89 |
90 |
91 | {% for item in order.items.all %}
92 |
93 | {{ item.product.title }} ×
94 | {{ item.count }} {% trans "Number" %}
95 |
96 | {% endfor %}
97 |
98 |
99 |
100 | {% if order.paid %}
101 |
107 | {% endif %}
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | {% endfor %}
116 |
117 |
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 |
100 |
147 |
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 | {% trans "Product" %}
53 | {% trans "Count of Order Item" %}
54 | {% trans "Unit Price" %}({% trans "Toman" %})
55 | {% trans "Total Price" %}({% trans "Toman" %})
56 | {% trans "Change Item" %}
57 |
58 |
59 |
60 | {% for item in items %}
61 |
62 | {{ item.0.non_field_errors }}
63 |
83 |
84 |
85 | {% trans "Change Count" %}
86 |
87 |
89 | {% trans "Remove" %}
90 |
91 |
92 |
93 | {% endfor %}
94 |
95 |
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 |
141 |
142 |
143 | {% trans "Apply Changes" %}
144 |
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