├── core
├── __init__.py
├── admin.py
├── tests.py
├── static
│ └── core
│ │ ├── logo.jpg
│ │ └── styles.css
├── apps.py
├── urls.py
├── models.py
├── serializers.py
└── templates
│ └── core
│ └── index.html
├── likes
├── __init__.py
├── tests.py
├── admin.py
├── apps.py
└── models.py
├── store
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── store_seed.py
├── signals
│ ├── __init__.py
│ └── handlers.py
├── tests.py
├── admin.py
├── apps.py
├── validators.py
├── tests
│ ├── conftest.py
│ └── test_collections.py
├── permissions.py
├── tasks.py
├── urls.py
├── templates
│ └── emails
│ │ └── verification_email.html
├── factories.py
├── models.py
├── serializers.py
└── views.py
├── tags
├── __init__.py
├── admin.py
├── tests.py
├── views.py
├── apps.py
└── models.py
├── e_commerce
├── __init__.py
├── celery.py
├── asgi.py
├── wsgi.py
├── urls.py
└── settings.py
├── pytest.ini
├── scripts
├── docker-entrypoint.sh
└── wait-for-it.sh
├── Dockerfile
├── Pipfile
├── manage.py
├── LICENSE
├── docker-compose.yml
├── .gitignore
└── README.md
/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/likes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/store/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/store/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/store/signals/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/store/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/e_commerce/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import celery
2 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = e_commerce.settings
3 |
4 |
--------------------------------------------------------------------------------
/core/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/core/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/likes/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/store/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/tags/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/tags/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/tags/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/likes/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/store/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/core/static/core/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amirhossein2831/E_Commerce_App/HEAD/core/static/core/logo.jpg
--------------------------------------------------------------------------------
/core/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CoreConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'core'
7 |
--------------------------------------------------------------------------------
/tags/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TagsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'tags'
7 |
--------------------------------------------------------------------------------
/likes/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class LikesConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'likes'
7 |
--------------------------------------------------------------------------------
/core/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.views.generic import TemplateView
3 |
4 |
5 | urlpatterns = [
6 | path('', TemplateView.as_view(template_name='core/index.html')),
7 | ]
--------------------------------------------------------------------------------
/store/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class StoreConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'store'
7 |
8 | def ready(self):
9 | import store.signals.handlers
--------------------------------------------------------------------------------
/scripts/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Apply database migrations
4 | echo "Apply database migrations"
5 | python manage.py makemigrations
6 | python manage.py migrate
7 |
8 | # Start server
9 | echo "Starting server"
10 | python manage.py runserver 0.0.0.0:8000
--------------------------------------------------------------------------------
/store/validators.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError
2 |
3 |
4 | def validate_file_size(file):
5 | max_size_kb = 500
6 |
7 | if file.size > max_size_kb * 1024:
8 | raise ValidationError(f'the file size should be less than {max_size_kb} KB')
9 |
--------------------------------------------------------------------------------
/e_commerce/celery.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery import Celery
4 |
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'e_commerce.settings')
6 |
7 | celery = Celery('e_commerce')
8 | celery.config_from_object('django.conf:settings', namespace='CELERY')
9 | celery.autodiscover_tasks()
10 |
--------------------------------------------------------------------------------
/store/signals/handlers.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db.models.signals import post_save
3 | from django.dispatch import receiver
4 |
5 | from store.models import Customer
6 |
7 |
8 | @receiver(post_save, sender=settings.AUTH_USER_MODEL)
9 | def create_customer_for_new_user(sender, instance, created, **kwargs):
10 | if created:
11 | Customer.objects.create(user=instance)
12 |
--------------------------------------------------------------------------------
/core/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractUser
2 | from django.db import models
3 |
4 |
5 | class User(AbstractUser):
6 | # add extra field here
7 | email = models.EmailField(unique=True)
8 |
9 | class Meta:
10 | indexes = [
11 | models.Index(fields=['first_name'], name='first_name_idx'),
12 | models.Index(fields=['last_name'], name='last_name_idx'),
13 | ]
14 |
--------------------------------------------------------------------------------
/e_commerce/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for e_commerce project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'e_commerce.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/e_commerce/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for e_commerce project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'e_commerce.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/store/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.contrib.auth.models import User
3 | from rest_framework.test import APIClient
4 |
5 |
6 | @pytest.fixture
7 | def api_client():
8 | return APIClient()
9 |
10 |
11 | @pytest.fixture
12 | def authenticated_client(api_client):
13 | def do_authenticate(is_staff=False, is_superuser=False):
14 | return api_client.force_authenticate(user=User(is_staff=is_staff, is_superuser=is_superuser))
15 |
16 | return do_authenticate
17 |
--------------------------------------------------------------------------------
/store/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 |
4 | class IsAuthAdminUserOrAuthReadOnly(permissions.BasePermission):
5 | def has_permission(self, request, view):
6 | if request.method in permissions.SAFE_METHODS:
7 | return bool(request.user and request.user.is_authenticated)
8 | return bool(request.user and request.user.is_authenticated and request.user.is_staff)
9 |
10 |
11 | class CanViewHistory(permissions.BasePermission):
12 | def has_permission(self, request, view):
13 | return request.user.has_perm('store.view_history')
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10
2 |
3 | ENV PYTHONUNBUFFERED=1
4 | WORKDIR /app
5 |
6 | # Required to install mysqlclient with Pip
7 | RUN apt-get update \
8 | && apt-get install python3-dev default-libmysqlclient-dev gcc -y
9 |
10 |
11 | # Install pipenv
12 | RUN pip install --upgrade pip
13 | RUN pip install pipenv
14 |
15 | # Install application dependencies
16 | COPY Pipfile Pipfile.lock /app/
17 | # We use the --system flag so packages are installed into the system python
18 | # and not into a virtualenv. Docker containers don't need virtual environments.
19 | RUN pipenv install --system --dev
20 |
21 | # Copy the application files into the image
22 | COPY . /app/
23 |
24 | # Expose port 8000 on the container
25 | EXPOSE 8000
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | django = "*"
8 | django-debug-toolbar = "*"
9 | djangorestframework = "*"
10 | markdown = "*"
11 | django-filter = "*"
12 | mysqlclient = "*"
13 | python-dotenv = "*"
14 | djoser = "*"
15 | djangorestframework-simplejwt = "*"
16 | drf-nested-routers = "*"
17 | drf-yasg = "*"
18 | factory-boy = "*"
19 | pillow = "*"
20 | django-cors-headers = "*"
21 | django-templated-mail = "*"
22 | redis = "*"
23 | celery = "*"
24 | flower = "*"
25 | django-redis = "*"
26 |
27 | [dev-packages]
28 | pytest = "*"
29 | pytest-django = "*"
30 | model-bakery = "*"
31 | pytest-watch = "*"
32 |
33 | [requires]
34 | python_version = "3.10"
35 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'e_commerce.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/store/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 | from django.core.mail import BadHeaderError
3 | from django.db.models import Count
4 | from templated_mail.mail import BaseEmailMessage
5 |
6 | from store.models import Cart
7 |
8 |
9 | @shared_task
10 | def send_verification_email(code):
11 | print("Sending verification email ...")
12 | try:
13 | email = BaseEmailMessage(template_name='emails/verification_email.html', context={'verification_code': code})
14 | email.send(['amirmemool2@gmail.com'])
15 | except BadHeaderError as e:
16 | print(f'Bad header exception occur {e}')
17 | print("Email Send Successfully")
18 |
19 |
20 | @shared_task
21 | def remove_empty_cart():
22 | print('Removing empty cart ...')
23 | Cart.objects.annotate(item_count=Count('items')).filter(item_count=0).delete()
24 | print('Empty Cart Removed successfully')
--------------------------------------------------------------------------------
/likes/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.contenttypes.fields import GenericForeignKey
2 | from django.contrib.contenttypes.models import ContentType
3 | from django.db import models
4 |
5 | from core.models import User
6 |
7 |
8 | class LikedItemManager(models.Manager):
9 |
10 | @staticmethod
11 | def get_like_for(content_type, object_id):
12 | content_type = ContentType.objects.get_for_model(content_type)
13 |
14 | return LikedItem.objects.filter(
15 | content_type=content_type,
16 | object_id=object_id
17 | )
18 |
19 |
20 | class LikedItem(models.Model):
21 | objects = LikedItemManager()
22 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='likes')
23 |
24 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='likes')
25 | object_id = models.PositiveIntegerField()
26 | content_object = GenericForeignKey()
27 |
--------------------------------------------------------------------------------
/tags/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.contenttypes.fields import GenericForeignKey
2 | from django.db import models
3 | from django.contrib.contenttypes.models import ContentType
4 |
5 |
6 | class TaggedItemManager(models.Manager):
7 |
8 | @staticmethod
9 | def get_tags_for(content_type, object_id):
10 | content_type = ContentType.objects.get_for_model(content_type)
11 |
12 | return TaggedItem.objects.select_related('tag').filter(
13 | content_type=content_type,
14 | object_id=object_id
15 | )
16 |
17 |
18 | class Tag(models.Model):
19 | label = models.CharField(max_length=255)
20 |
21 |
22 | class TaggedItem(models.Model):
23 | objects = TaggedItemManager()
24 | tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
25 |
26 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
27 | object_id = models.PositiveIntegerField()
28 | content_object = GenericForeignKey()
--------------------------------------------------------------------------------
/store/management/commands/store_seed.py:
--------------------------------------------------------------------------------
1 | from django.core.management import BaseCommand
2 | from django.db import transaction
3 |
4 | from store import factories
5 |
6 |
7 | class Command(BaseCommand):
8 | help = 'Seed Store tables with sample data'
9 |
10 | def handle(self, *args, **kwargs):
11 | try:
12 | with transaction.atomic():
13 | self.stdout.write(self.style.SUCCESS('Seeding database...'))
14 |
15 | factories.AddressFactory.create_user_with_profile_addresses(user_size=10)
16 | factories.ProductFactory.create_collection_product_promotions_reviews(collections_size=10)
17 | factories.CartItemFactory.create_cart_cart_items(10)
18 | factories.OrderItemFactory.create_order_order_items(10)
19 |
20 | self.stdout.write(self.style.SUCCESS('Database seeded successfully'))
21 | except Exception as e:
22 | self.stdout.write(self.style.ERROR(f'Database seeding failed: {e}'))
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 AmirHossein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/core/serializers.py:
--------------------------------------------------------------------------------
1 | from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer, UserSerializer as BaseUserSerializer
2 | from rest_framework import serializers
3 |
4 |
5 | class UserCreateSerializer(BaseUserCreateSerializer):
6 | class Meta(BaseUserCreateSerializer.Meta):
7 | fields = ['id', 'username', 'first_name', 'last_name', 'email', 'password', 'is_superuser', 'is_staff']
8 |
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | staff = self.context["request"].user.is_staff
13 | superuser = self.context["request"].user.is_superuser
14 |
15 | if not staff and not superuser:
16 | self.fields.pop('is_staff')
17 | self.fields.pop('is_superuser')
18 | elif staff and not superuser:
19 | self.fields.pop('is_superuser')
20 |
21 |
22 | class UserSerializer(BaseUserSerializer):
23 | is_superuser = serializers.BooleanField(read_only=True)
24 | is_staff = serializers.BooleanField(read_only=True)
25 |
26 | class Meta(BaseUserSerializer.Meta):
27 | fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff']
28 |
--------------------------------------------------------------------------------
/core/templates/core/index.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | My E-Commerce Store
9 |
10 |
11 |
12 | My E-Commerce Store
13 |
21 |
22 |
23 |
24 |
25 | Welcome to our store!
26 | Discover amazing deals on our latest products.
27 | Shop Now
28 |
29 |
30 |
31 |
32 |

33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | web:
5 | container_name: web
6 | build: .
7 | command: ./scripts/wait-for-it.sh mysql:3306 -- ./scripts/docker-entrypoint.sh
8 | ports:
9 | - "8000:8000"
10 | depends_on:
11 | - redis
12 | - mysql
13 | restart: on-failure
14 | volumes:
15 | - .:/app
16 |
17 | mysql:
18 | image: mysql:latest
19 | restart: always
20 | ports:
21 | - "${PORT:-3306}:3306"
22 | volumes:
23 | - ./data/db:/var/lib/mysql
24 | environment:
25 | MYSQL_DATABASE: ${NAME:-app_mysql}
26 | MYSQL_ROOT_PASSWORD: ${PASSWORD:-password}
27 |
28 | redis:
29 | image: redis:6.2-alpine
30 | restart: always
31 | ports:
32 | - "${REDIS_PORT:-6379}:6379"
33 | volumes:
34 | - ./redis_data:/data
35 |
36 | celery:
37 | build: .
38 | command: celery -A e_commerce worker --loglevel=info
39 | depends_on:
40 | - redis
41 | volumes:
42 | - .:/app
43 |
44 | celery-beat:
45 | build: .
46 | command: celery -A e_commerce beat --loglevel=info
47 | depends_on:
48 | - redis
49 | volumes:
50 | - .:/app
51 |
52 | flower:
53 | build: .
54 | command: celery -A e_commerce flower
55 | depends_on:
56 | - web
57 | - redis
58 | - celery
59 | environment:
60 | - DEBUG=1
61 | - CELERY_BROKER=redis://redis:${REDIS_PORT:-6379}/1
62 | - CELERY_BACKEND=redis://redis:${REDIS_PORT:-6379}/1
63 | ports:
64 | - "${FLOWER_PORT:-5555}:5555"
65 |
66 | volumes:
67 | data:
68 | redis_data:
--------------------------------------------------------------------------------
/store/urls.py:
--------------------------------------------------------------------------------
1 | from rest_framework_nested import routers
2 |
3 | from . import views
4 |
5 | route = routers.DefaultRouter()
6 | route.register('customers', views.CustomerViewSet)
7 | route.register('collections', views.CollectionViewSet)
8 | route.register('products', views.ProductViewSet)
9 | route.register('promotions', views.PromotionViewSet)
10 | route.register('carts', views.CartViewSet)
11 | route.register('orders', views.OrderViewSet)
12 | route.register('customers/me/addresses', views.LoginCustomerAddressViewSet)
13 |
14 | promotions_router = routers.NestedDefaultRouter(route, 'products', lookup='products')
15 | promotions_router.register('promotions', views.ProductPromotionViewSet, basename='promotions')
16 |
17 | reviews_router = routers.NestedDefaultRouter(route, 'products', lookup='products')
18 | reviews_router.register('reviews', views.ProductReviewViewSet, basename='reviews')
19 |
20 | addresses_router = routers.NestedDefaultRouter(route, 'customers', lookup='customers')
21 | addresses_router.register('addresses', views.CustomerAddressViewSet, basename='addresses')
22 |
23 | cart_items_router = routers.NestedDefaultRouter(route, 'carts', lookup='carts')
24 | cart_items_router.register('items', views.CartItemViewSet, 'items')
25 |
26 | product_image_router = routers.NestedDefaultRouter(route, 'products', lookup='products')
27 | product_image_router.register('images', views.ProductImageViewSet, basename='images')
28 |
29 | urlpatterns = (
30 | route.urls +
31 | promotions_router.urls +
32 | reviews_router.urls +
33 | addresses_router.urls +
34 | cart_items_router.urls +
35 | product_image_router.urls
36 | )
37 |
--------------------------------------------------------------------------------
/store/templates/emails/verification_email.html:
--------------------------------------------------------------------------------
1 | {% block subject %}E commerce Verification code{% endblock %}
2 | {% block text_body %}use verification code to verify your email{% endblock %}
3 |
4 | {% block html_body %}
5 |
6 |
7 |
8 |
9 |
10 | Email Verification
11 |
39 |
40 |
41 |
42 |
Email Verification
43 |
Please use the following verification code to complete your registration:
44 |
{{ verification_code }}
45 |
If you did not request this verification, you can safely ignore this email.
46 |
47 |
48 |
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/e_commerce/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | URL configuration for e_commerce project.
3 |
4 | The `urlpatterns` list routes URLs to views. For more information please see:
5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/
6 | Examples:
7 | Function views
8 | 1. Add an import: from my_app import views
9 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
10 | Class-based views
11 | 1. Add an import: from other_app.views import Home
12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13 | Including another URLconf
14 | 1. Import the include() function: from django.urls import include, path
15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16 | """
17 | from django.conf.urls.static import static
18 | from django.contrib import admin
19 | from django.conf import settings
20 | from drf_yasg.views import get_schema_view
21 | from drf_yasg import openapi
22 | from django.urls import path, include
23 | from rest_framework import permissions
24 |
25 | schema_view = get_schema_view(
26 | openapi.Info(
27 | title="E Commerce API",
28 | default_version='v1',
29 | description="this is the document for the for E Commerce site",
30 | terms_of_service="https://www.example.com/terms/",
31 | contact=openapi.Contact(email="contact@example.com"),
32 | license=openapi.License(name="Awesome License"),
33 | ),
34 | public=True,
35 | permission_classes=[permissions.AllowAny]
36 | )
37 |
38 | urlpatterns = [
39 | path('',include('core.urls')),
40 | path('admin/', admin.site.urls),
41 | path('__debug__/', include("debug_toolbar.urls")),
42 | path('auth/', include('djoser.urls')),
43 | path('auth/', include('djoser.urls.jwt')),
44 | path('store/', include('store.urls')),
45 | path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
46 | path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
47 | ]
48 |
49 | if settings.DEBUG is True:
50 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
51 |
--------------------------------------------------------------------------------
/core/static/core/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | font-family: Arial, sans-serif;
9 | line-height: 1.6;
10 | }
11 |
12 | header {
13 | background-color: #333;
14 | color: #fff;
15 | padding: 1rem;
16 | text-align: center;
17 | }
18 |
19 | nav ul {
20 | list-style: none;
21 | padding: 0;
22 | }
23 |
24 | nav ul li {
25 | display: inline;
26 | margin: 0 10px;
27 | }
28 |
29 | nav ul li a {
30 | color: #fff;
31 | text-decoration: none;
32 | }
33 |
34 | main {
35 | padding: 2rem;
36 | }
37 |
38 | .hero {
39 | text-align: center;
40 | margin-bottom: 2rem;
41 | }
42 |
43 | .hero h2 {
44 | font-size: 2.5rem;
45 | margin-bottom: 1rem;
46 | }
47 |
48 | .hero p {
49 | font-size: 1.2rem;
50 | margin-bottom: 1.5rem;
51 | }
52 |
53 | .btn {
54 | display: inline-block;
55 | background-color: #007bff;
56 | color: #fff;
57 | padding: 0.5rem 1rem;
58 | text-decoration: none;
59 | border-radius: 5px;
60 | }
61 |
62 | .btn:hover {
63 | background-color: #0056b3;
64 | }
65 |
66 | .products {
67 | display: grid;
68 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
69 | gap: 1rem;
70 | margin-bottom: 100px;
71 | }
72 |
73 | .product {
74 | border: 1px solid #ccc;
75 | padding: 1rem;
76 | text-align: center;
77 | }
78 |
79 | .product img {
80 | max-width: 100%;
81 | height: auto;
82 | }
83 |
84 | .product h3 {
85 | margin-top: 0.5rem;
86 | }
87 |
88 | footer {
89 | background-color: #333;
90 | color: #fff;
91 | text-align: center;
92 | padding: 1rem 0;
93 | position: fixed;
94 | bottom: 0;
95 | width: 100%;
96 | }
--------------------------------------------------------------------------------
/store/tests/test_collections.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from model_bakery import baker
3 | from rest_framework import status
4 |
5 | from store.models import Collection, Product
6 |
7 |
8 | @pytest.fixture
9 | def get_collections(api_client):
10 | def do_get_collections(pk=None):
11 | if pk is not None:
12 | return api_client.get(f'/store/collections/{pk}/')
13 | else:
14 | return api_client.get('/store/collections/')
15 |
16 | return do_get_collections
17 |
18 |
19 | @pytest.fixture
20 | def create_collection(api_client):
21 | def do_create_collection(collection):
22 | return api_client.post('/store/collections/', collection)
23 |
24 | return do_create_collection
25 |
26 |
27 | class TestQueryCollection:
28 | def test_if_user_is_anonymous_return_401(self, get_collections):
29 | response = get_collections()
30 |
31 | assert response.status_code == status.HTTP_401_UNAUTHORIZED
32 |
33 | @pytest.mark.django_db
34 | def test_if_user_is_authenticate_return_200_list(self, authenticated_client, get_collections):
35 | authenticated_client()
36 | collections_number = 4
37 | baker.make(Collection, _quantity=collections_number)
38 |
39 | response = get_collections()
40 |
41 | assert response.status_code == status.HTTP_200_OK
42 | assert response.data['count'] == collections_number
43 |
44 | @pytest.mark.django_db
45 | def test_if_user_is_authenticate_return_200_detail(self, authenticated_client, get_collections):
46 | authenticated_client()
47 | collection = baker.make(Collection)
48 |
49 | response = get_collections(pk=collection.id)
50 |
51 | assert response.status_code == status.HTTP_200_OK
52 | assert response.data['id'] == collection.id
53 |
54 |
55 | class TestCommandCollection:
56 | def test_if_user_is_anonymous_return_401(self, api_client, create_collection):
57 | response = create_collection({'title': 'test'})
58 |
59 | assert response.status_code == status.HTTP_401_UNAUTHORIZED
60 |
61 | def test_if_user_is_not_admin_return_403(self, authenticated_client, create_collection):
62 | authenticated_client()
63 |
64 | response = create_collection({'title': 'test'})
65 |
66 | assert response.status_code == status.HTTP_403_FORBIDDEN
67 |
68 | def test_create_if_data_is_invalid_return_400(self, authenticated_client, create_collection):
69 | authenticated_client(is_staff=True)
70 |
71 | response = create_collection({'title': ''})
72 |
73 | assert response.status_code == status.HTTP_400_BAD_REQUEST
74 | assert response.data['title'] is not None
75 |
76 | @pytest.mark.django_db
77 | def test_create_if_data_is_valid_return_200(self, authenticated_client, create_collection):
78 | authenticated_client(is_staff=True)
79 |
80 | response = create_collection({'title': 'test'})
81 |
82 | assert response.status_code == status.HTTP_201_CREATED
83 | assert response.data['id'] > 0
84 |
85 | @pytest.mark.django_db
86 | def test_update_if_user_is_admin_return_204(self, authenticated_client, api_client):
87 | authenticated_client(is_staff=True)
88 | updated_title = 'updated'
89 | collection = baker.make(Collection)
90 |
91 | response = api_client.patch(f'/store/collections/{collection.id}/', {'title': updated_title})
92 |
93 | assert response.status_code == status.HTTP_200_OK
94 | assert response.data['title'] == updated_title
95 |
96 | @pytest.mark.django_db
97 | def test_delete_if_data_is_invalid_return_400(self, authenticated_client, api_client):
98 | authenticated_client(is_staff=True)
99 | products = baker.make(Product, _quantity=2, slug='test')
100 | collection = baker.make(Collection, products=products)
101 |
102 | response = api_client.delete(f'/store/collections/{collection.id}/')
103 |
104 | assert response.status_code == status.HTTP_400_BAD_REQUEST
105 |
106 | @pytest.mark.django_db
107 | def test_delete_if_user_is_admin_return_204(self, authenticated_client, api_client):
108 | authenticated_client(is_staff=True)
109 | collection = baker.make(Collection)
110 |
111 | response = api_client.delete(f'/store/collections/{collection.id}/')
112 |
113 | assert response.status_code == status.HTTP_204_NO_CONTENT
114 | assert not Collection.objects.filter(pk=collection.id).exists()
115 |
--------------------------------------------------------------------------------
/store/factories.py:
--------------------------------------------------------------------------------
1 | import random
2 | import factory
3 | from decimal import Decimal
4 | from store.models import *
5 |
6 |
7 | class UserFactory(factory.django.DjangoModelFactory):
8 | class Meta:
9 | model = settings.AUTH_USER_MODEL
10 |
11 | username = factory.Faker('name')
12 | first_name = factory.Faker('first_name')
13 | last_name = factory.Faker('last_name')
14 | email = factory.Faker('email')
15 | password = factory.Faker('password')
16 |
17 |
18 | class AddressFactory(factory.django.DjangoModelFactory):
19 | class Meta:
20 | model = Address
21 |
22 | street = factory.Faker('street_address')
23 | city = factory.Faker('city')
24 | zip_code = factory.Faker('zipcode')
25 |
26 | @staticmethod
27 | def create_user_with_profile_addresses(user_size, address_size=2):
28 | customers = UserFactory.create_batch(user_size)
29 | [AddressFactory.create_batch(address_size, customer_id=customer.id) for customer in customers]
30 |
31 |
32 | class CollectionFactory(factory.django.DjangoModelFactory):
33 | class Meta:
34 | model = Collection
35 |
36 | title = factory.Faker('sentence', nb_words=4)
37 | featured_product = None
38 |
39 |
40 | class PromotionFactory(factory.django.DjangoModelFactory):
41 | class Meta:
42 | model = Promotion
43 |
44 | description = factory.Faker('text')
45 | discount = factory.Faker('random_int', min=1, max=50)
46 |
47 |
48 | class ReviewFactory(factory.django.DjangoModelFactory):
49 | class Meta:
50 | model = Review
51 |
52 | title = factory.Faker('sentence', nb_words=4)
53 | description = factory.Faker('text')
54 |
55 |
56 | class ProductFactory(factory.django.DjangoModelFactory):
57 | class Meta:
58 | model = Product
59 |
60 | title = factory.Faker('sentence', nb_words=4)
61 | slug = factory.Faker('slug')
62 | description = factory.Faker('text')
63 | unit_price = factory.Faker('random_int', min=10, max=1000)
64 | inventory = factory.Faker('random_int', min=0, max=1000)
65 |
66 | @staticmethod
67 | def create_collection_product_promotions_reviews(collections_size, product_size=5, promotions_size=2,
68 | review_size=2):
69 | # create collection
70 | collections = CollectionFactory.create_batch(collections_size)
71 | # create product for each collection
72 | products_list = [ProductFactory.create_batch(product_size, collection=collection) for collection in collections]
73 |
74 | for products in products_list:
75 | for product in products:
76 | # create review for each product
77 | ReviewFactory.create_batch(review_size, product=product)
78 | # create promotions for each product
79 | promotions = PromotionFactory.create_batch(promotions_size)
80 | product.promotions.set(promotions)
81 | product.save()
82 |
83 |
84 | class CartFactory(factory.django.DjangoModelFactory):
85 | class Meta:
86 | model = Cart
87 |
88 |
89 | class CartItemFactory(factory.django.DjangoModelFactory):
90 | class Meta:
91 | model = CartItem
92 |
93 | quantity = random.randint(0, 10)
94 |
95 | @staticmethod
96 | def create_cart_cart_items(cart_size, item_size=5):
97 | product_ids = Product.objects.only('id').values_list('id', flat=True)
98 |
99 | carts = CartFactory.create_batch(cart_size)
100 | for cart in carts:
101 | for _ in range(item_size):
102 | CartItemFactory.create(cart=cart, product_id=random.choice(product_ids))
103 |
104 |
105 | class OrderFactory(factory.django.DjangoModelFactory):
106 | class Meta:
107 | model = Order
108 |
109 | created_at = factory.Faker('date_time_this_month', before_now=True)
110 | payment_status = factory.Faker('random_element', elements=['P', 'C', 'F'])
111 |
112 |
113 | class OrderItemFactory(factory.django.DjangoModelFactory):
114 | class Meta:
115 | model = OrderItem
116 |
117 | quantity = factory.Faker('random_int', min=1, max=10)
118 | unit_price = Decimal('10.00')
119 |
120 | @staticmethod
121 | def create_order_order_items(order_size, item_size=5):
122 | customer_ids = Customer.objects.only('user_id').values_list('user_id', flat=True)
123 | product_ids = Product.objects.only('id').values_list('id', flat=True)
124 |
125 | orders = OrderFactory.create_batch(order_size, customer_id=random.choice(customer_ids))
126 |
127 | for order in orders:
128 | for _ in range(item_size):
129 | OrderItemFactory.create(order=order, product_id=random.choice(product_ids))
130 |
--------------------------------------------------------------------------------
/store/models.py:
--------------------------------------------------------------------------------
1 | import time
2 | from uuid import uuid4
3 |
4 | from django.conf import settings
5 | from django.core.validators import MinValueValidator, MaxValueValidator
6 | from django.db import models
7 | from django.utils.text import slugify
8 | from .validators import validate_file_size
9 |
10 |
11 | class AuditableModel(models.Model):
12 | class Meta:
13 | abstract = True
14 |
15 | created_at = models.DateTimeField(auto_now_add=True)
16 | created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="+")
17 |
18 | updated_at = models.DateTimeField(auto_now=True)
19 | updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="+")
20 |
21 | deleted_at = models.DateTimeField(blank=True, null=True)
22 | deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="+")
23 |
24 |
25 | class Customer(AuditableModel):
26 | BRONZE_MEMBERSHIP = 'B'
27 | SILVER_MEMBERSHIP = 'S'
28 | GOLD_MEMBERSHIP = 'G'
29 |
30 | MEMBERSHIP_PLAN = [
31 | (BRONZE_MEMBERSHIP, 'BRONZE MEMBERSHIP'),
32 | (SILVER_MEMBERSHIP, 'SILVER MEMBERSHIP'),
33 | (GOLD_MEMBERSHIP, 'GOLD MEMBERSHIP'),
34 | ]
35 |
36 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
37 | phone = models.CharField(max_length=11, null=True)
38 | birth_date = models.DateField(null=True)
39 | membership = models.CharField(max_length=1, choices=MEMBERSHIP_PLAN, default=BRONZE_MEMBERSHIP)
40 |
41 | class Meta:
42 | permissions = [
43 | ('view_history', 'Can view history')
44 | ]
45 |
46 |
47 | class Collection(AuditableModel):
48 | title = models.CharField(max_length=255)
49 | featured_product = models.ForeignKey('Product', on_delete=models.SET_NULL, related_name="+", null=True)
50 |
51 |
52 | class Promotion(AuditableModel):
53 | description = models.TextField()
54 | discount = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(100)])
55 |
56 |
57 | class Product(AuditableModel):
58 | title = models.CharField(max_length=255)
59 | slug = models.SlugField(null=True)
60 | description = models.TextField()
61 | unit_price = models.DecimalField(max_digits=6, decimal_places=2)
62 | inventory = models.PositiveIntegerField()
63 | collection = models.ForeignKey(Collection, on_delete=models.PROTECT, related_name='products')
64 | promotions = models.ManyToManyField(Promotion, related_name='products')
65 |
66 | def save(self, *args, **kwargs):
67 | if not self.slug:
68 | self.slug = slugify(self.title)
69 | super().save(*args, **kwargs)
70 |
71 |
72 | class ProductImage(AuditableModel):
73 | product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
74 | image = models.ImageField(upload_to='store/images', validators=[validate_file_size])
75 |
76 |
77 | class Review(AuditableModel):
78 | title = models.CharField(max_length=255)
79 | description = models.TextField()
80 | product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
81 |
82 |
83 | class Cart(AuditableModel):
84 | id = models.UUIDField(primary_key=True, default=uuid4)
85 |
86 |
87 | class CartItem(AuditableModel):
88 | cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
89 | product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='cart_items')
90 | quantity = models.PositiveSmallIntegerField(validators=[MinValueValidator(1)])
91 |
92 | class Meta:
93 | unique_together = [['cart', 'product']]
94 |
95 |
96 | class Address(AuditableModel):
97 | customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='addresses', null=True)
98 | street = models.CharField(max_length=255)
99 | city = models.CharField(max_length=255)
100 | zip_code = models.CharField(max_length=5, default='')
101 |
102 |
103 | class Order(AuditableModel):
104 | PAYMENT_STATUS_PENDING = 'P'
105 | PAYMENT_STATUS_COMPLETE = 'C'
106 | PAYMENT_STATUS_FAILED = 'F'
107 |
108 | PAYMENT_STATUS_CHOICES = [
109 | (PAYMENT_STATUS_PENDING, 'Pending'),
110 | (PAYMENT_STATUS_COMPLETE, 'Complete'),
111 | (PAYMENT_STATUS_FAILED, 'Failed'),
112 | ]
113 | payment_status = models.CharField(max_length=1, choices=PAYMENT_STATUS_CHOICES, default=PAYMENT_STATUS_PENDING)
114 | customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
115 |
116 |
117 | class OrderItem(AuditableModel):
118 | order = models.ForeignKey(Order, on_delete=models.PROTECT, related_name='items')
119 | product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='order_items')
120 | quantity = models.PositiveSmallIntegerField()
121 | unit_price = models.DecimalField(max_digits=6, decimal_places=2)
122 |
--------------------------------------------------------------------------------
/scripts/wait-for-it.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to test if a given TCP host/port are available
3 |
4 | WAITFORIT_cmdname=${0##*/}
5 |
6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
7 |
8 | usage()
9 | {
10 | cat << USAGE >&2
11 | Usage:
12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
13 | -h HOST | --host=HOST Host or IP under test
14 | -p PORT | --port=PORT TCP port under test
15 | Alternatively, you specify the host and port as host:port
16 | -s | --strict Only execute subcommand if the test succeeds
17 | -q | --quiet Don't output any status messages
18 | -t TIMEOUT | --timeout=TIMEOUT
19 | Timeout in seconds, zero for no timeout
20 | -- COMMAND ARGS Execute command with args after the test finishes
21 | USAGE
22 | exit 1
23 | }
24 |
25 | wait_for()
26 | {
27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
29 | else
30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
31 | fi
32 | WAITFORIT_start_ts=$(date +%s)
33 | while :
34 | do
35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT
37 | WAITFORIT_result=$?
38 | else
39 | (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
40 | WAITFORIT_result=$?
41 | fi
42 | if [[ $WAITFORIT_result -eq 0 ]]; then
43 | WAITFORIT_end_ts=$(date +%s)
44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
45 | break
46 | fi
47 | sleep 1
48 | done
49 | return $WAITFORIT_result
50 | }
51 |
52 | wait_for_wrapper()
53 | {
54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then
56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
57 | else
58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
59 | fi
60 | WAITFORIT_PID=$!
61 | trap "kill -INT -$WAITFORIT_PID" INT
62 | wait $WAITFORIT_PID
63 | WAITFORIT_RESULT=$?
64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then
65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
66 | fi
67 | return $WAITFORIT_RESULT
68 | }
69 |
70 | # process arguments
71 | while [[ $# -gt 0 ]]
72 | do
73 | case "$1" in
74 | *:* )
75 | WAITFORIT_hostport=(${1//:/ })
76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]}
77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]}
78 | shift 1
79 | ;;
80 | --child)
81 | WAITFORIT_CHILD=1
82 | shift 1
83 | ;;
84 | -q | --quiet)
85 | WAITFORIT_QUIET=1
86 | shift 1
87 | ;;
88 | -s | --strict)
89 | WAITFORIT_STRICT=1
90 | shift 1
91 | ;;
92 | -h)
93 | WAITFORIT_HOST="$2"
94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi
95 | shift 2
96 | ;;
97 | --host=*)
98 | WAITFORIT_HOST="${1#*=}"
99 | shift 1
100 | ;;
101 | -p)
102 | WAITFORIT_PORT="$2"
103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi
104 | shift 2
105 | ;;
106 | --port=*)
107 | WAITFORIT_PORT="${1#*=}"
108 | shift 1
109 | ;;
110 | -t)
111 | WAITFORIT_TIMEOUT="$2"
112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
113 | shift 2
114 | ;;
115 | --timeout=*)
116 | WAITFORIT_TIMEOUT="${1#*=}"
117 | shift 1
118 | ;;
119 | --)
120 | shift
121 | WAITFORIT_CLI=("$@")
122 | break
123 | ;;
124 | --help)
125 | usage
126 | ;;
127 | *)
128 | echoerr "Unknown argument: $1"
129 | usage
130 | ;;
131 | esac
132 | done
133 |
134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
135 | echoerr "Error: you need to provide a host and port to test."
136 | usage
137 | fi
138 |
139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
143 |
144 | # Check to see if timeout is from busybox?
145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
147 |
148 | WAITFORIT_BUSYTIMEFLAG=""
149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
150 | WAITFORIT_ISBUSY=1
151 | # Check if busybox timeout uses -t flag
152 | # (recent Alpine versions don't support -t anymore)
153 | if timeout &>/dev/stdout | grep -q -e '-t '; then
154 | WAITFORIT_BUSYTIMEFLAG="-t"
155 | fi
156 | else
157 | WAITFORIT_ISBUSY=0
158 | fi
159 |
160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then
161 | wait_for
162 | WAITFORIT_RESULT=$?
163 | exit $WAITFORIT_RESULT
164 | else
165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
166 | wait_for_wrapper
167 | WAITFORIT_RESULT=$?
168 | else
169 | wait_for
170 | WAITFORIT_RESULT=$?
171 | fi
172 | fi
173 |
174 | if [[ $WAITFORIT_CLI != "" ]]; then
175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
177 | exit $WAITFORIT_RESULT
178 | fi
179 | exec "${WAITFORIT_CLI[@]}"
180 | else
181 | exit $WAITFORIT_RESULT
182 | fi
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Django ###
2 | *.log
3 | *.pot
4 | *.pyc
5 | __pycache__/
6 | local_settings.py
7 | db.sqlite3
8 | db.sqlite3-journal
9 | media
10 |
11 | # configuration environnement
12 | .env*
13 | devenv
14 |
15 | # Django
16 | .pot
17 | .pyc
18 | __pycache__
19 | migrations
20 |
21 | # Docker generated files/folder from config
22 | mysql
23 | data
24 | redis_data
25 | /static
26 |
27 | # prod files
28 | staticfiles
29 |
30 | # Ne pas ignorer ces fichiers
31 | !.env.dev-exemple
32 |
33 | ### Django.Python Stack ###
34 | # Byte-compiled / optimized / DLL files
35 | *.py[cod]
36 | *$py.class
37 |
38 | # C extensions
39 | *.so
40 |
41 | # Distribution / packaging
42 | .Python
43 | build/
44 | develop-eggs/
45 | dist/
46 | downloads/
47 | eggs/
48 | .eggs/
49 | lib/
50 | lib64/
51 | parts/
52 | sdist/
53 | var/
54 | wheels/
55 | share/python-wheels/
56 | *.egg-info/
57 | .installed.cfg
58 | *.egg
59 | MANIFEST
60 |
61 | # PyInstaller
62 | # Usually these files are written by a python script from a template
63 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
64 | *.manifest
65 | *.spec
66 |
67 | # Installer logs
68 | pip-log.txt
69 | pip-delete-this-directory.txt
70 |
71 | # Unit test / coverage reports
72 | htmlcov/
73 | .tox/
74 | .nox/
75 | .coverage
76 | .coverage.*
77 | .cache
78 | nosetests.xml
79 | coverage.xml
80 | *.cover
81 | *.py,cover
82 | .hypothesis/
83 | .pytest_cache/
84 | cover/
85 |
86 | # Translations
87 | *.mo
88 |
89 | # Django stuff:
90 |
91 | # Flask stuff:
92 | instance/
93 | .webassets-cache
94 |
95 | # Scrapy stuff:
96 | .scrapy
97 |
98 | # Sphinx documentation
99 | docs/_build/
100 |
101 | # PyBuilder
102 | .pybuilder/
103 | target/
104 |
105 | # Jupyter Notebook
106 | .ipynb_checkpoints
107 |
108 | # IPython
109 | profile_default/
110 | ipython_config.py
111 |
112 |
113 | # pipenv
114 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
115 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
116 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
117 | # install all needed dependencies.
118 | Pipfile.lock
119 |
120 |
121 |
122 | # pdm
123 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
124 | #pdm.lock
125 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
126 | # in version control.
127 | # https://pdm.fming.dev/#use-with-ide
128 | .pdm.toml
129 |
130 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131 | __pypackages__/
132 |
133 | # Celery stuff
134 | celerybeat-schedule
135 | celerybeat.pid
136 |
137 | # SageMath parsed files
138 | *.sage.py
139 |
140 | # Environments
141 | .env
142 | .venv
143 | env/
144 | venv/
145 | ENV/
146 | env.bak/
147 | venv.bak/
148 |
149 | # Spyder project settings
150 | .spyderproject
151 | .spyproject
152 |
153 | # Rope project settings
154 | .ropeproject
155 |
156 | # mkdocs documentation
157 | /site
158 |
159 | # mypy
160 | .mypy_cache/
161 | .dmypy.json
162 | dmypy.json
163 |
164 | # Pyre type checker
165 | .pyre/
166 |
167 | # pytype static type analyzer
168 | .pytype/
169 |
170 | # Cython debug symbols
171 | cython_debug/
172 |
173 | # PyCharm
174 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
175 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
176 | # and can be added to the global gitignore or merged into this file. For a more nuclear
177 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
178 | .idea/
179 |
180 |
181 | # User-specific stuff
182 | .idea/**/workspace.xml
183 | .idea/**/tasks.xml
184 | .idea/**/usage.statistics.xml
185 | .idea/**/dictionaries
186 | .idea/**/shelf
187 |
188 | # AWS User-specific
189 | .idea/**/aws.xml
190 |
191 | # Generated files
192 | .idea/**/contentModel.xml
193 |
194 | # Sensitive or high-churn files
195 | .idea/**/dataSources/
196 | .idea/**/dataSources.ids
197 | .idea/**/dataSources.local.xml
198 | .idea/**/sqlDataSources.xml
199 | .idea/**/dynamic.xml
200 | .idea/**/uiDesigner.xml
201 | .idea/**/dbnavigator.xml
202 |
203 |
204 | # File-based project format
205 | *.iws
206 |
207 |
208 | # mpeltonen/sbt-idea plugin
209 | .idea_modules/
210 |
211 | # JIRA plugin
212 | atlassian-ide-plugin.xml
213 |
214 | # Cursive Clojure plugin
215 | .idea/replstate.xml
216 |
217 | # SonarLint plugin
218 | .idea/sonarlint/
219 |
220 | # Crashlytics plugin (for Android Studio and IntelliJ)
221 | com_crashlytics_export_strings.xml
222 | crashlytics.properties
223 | crashlytics-build.properties
224 | fabric.properties
225 |
226 | # Editor-based Rest Client
227 | .idea/httpRequests
228 |
229 | ### PyCharm Patch ###
230 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
231 |
232 | # *.iml
233 | # modules.xml
234 | # .idea/misc.xml
235 | # *.ipr
236 |
237 | # Sonarlint plugin
238 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
239 | .idea/**/sonarlint/
240 |
241 | # SonarQube Plugin
242 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
243 | .idea/**/sonarIssues.xml
244 |
245 | # Markdown Navigator plugin
246 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
247 | .idea/**/markdown-navigator.xml
248 | .idea/**/markdown-navigator-enh.xml
249 | .idea/**/markdown-navigator/
250 |
251 | # Cache file creation bug
252 | # See https://youtrack.jetbrains.com/issue/JBR-2257
253 | .idea/$CACHE_FILE$
254 |
255 | # CodeStream plugin
256 | # https://plugins.jetbrains.com/plugin/12206-codestream
257 | .idea/codestream.xml
258 |
259 | # Azure Toolkit for IntelliJ plugin
260 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
261 | .idea/**/azureSettings.xml
262 |
263 | ### Python Patch ###
264 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
265 | poetry.toml
266 |
267 | # ruff
268 | .ruff_cache/
269 |
270 | # LSP config files
271 | pyrightconfig.json
272 |
273 | # End of https://www.toptal.com/developers/gitignore/api/python,django,pycharm
274 |
--------------------------------------------------------------------------------
/store/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from django.db import transaction
3 | from rest_framework.serializers import ModelSerializer
4 |
5 | from store.models import *
6 |
7 |
8 | class CustomerSerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = Customer
11 | fields = ['phone', 'birth_date', 'membership']
12 |
13 |
14 | class CustomerAddressSerializer(serializers.ModelSerializer):
15 | zip_code = serializers.CharField(required=False, allow_blank=True)
16 |
17 | class Meta:
18 | model = Address
19 | fields = ['id', 'street', 'city', 'zip_code']
20 |
21 |
22 | class CollectionSerializer(serializers.ModelSerializer):
23 | products = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
24 | featured_product = serializers.PrimaryKeyRelatedField(
25 | queryset=Product.objects.all(), allow_null=True, required=False)
26 |
27 | class Meta:
28 | model = Collection
29 | fields = ['id', 'title', 'featured_product', 'products']
30 |
31 |
32 | class ProductImageSerializer(ModelSerializer):
33 | class Meta:
34 | model = ProductImage
35 | fields = ['id', 'image']
36 |
37 |
38 | class ProductSerializer(serializers.ModelSerializer):
39 | slug = serializers.CharField(read_only=True)
40 | images = ProductImageSerializer(many=True, read_only=True)
41 | promotions = serializers.PrimaryKeyRelatedField(
42 | queryset=Promotion.objects.all(), many=True, allow_empty=True, required=False)
43 |
44 | class Meta:
45 | model = Product
46 | fields = ['id', 'title', 'slug', 'description', 'unit_price', 'inventory', 'collection', 'promotions', 'images']
47 |
48 |
49 | class PromotionSerializer(serializers.ModelSerializer):
50 | products = serializers.PrimaryKeyRelatedField(
51 | queryset=Product.objects.all(), many=True, allow_empty=True, required=False)
52 |
53 | class Meta:
54 | model = Promotion
55 | fields = ['id', 'description', 'discount', 'products']
56 |
57 |
58 | class ProductPromotionSerializer(serializers.ModelSerializer):
59 | class Meta:
60 | model = Promotion
61 | fields = ['id', 'description', 'discount']
62 |
63 |
64 | class ProductReviewSerializer(serializers.ModelSerializer):
65 | class Meta:
66 | model = Review
67 | fields = ['id', 'title', 'description']
68 |
69 |
70 | class CartItemSerializer(serializers.ModelSerializer):
71 | product = ProductSerializer(read_only=True)
72 | total_price = serializers.SerializerMethodField(method_name='get_total_price')
73 |
74 | @staticmethod
75 | def get_total_price(cart_item: CartItem):
76 | return cart_item.product.unit_price * cart_item.quantity
77 |
78 | class Meta:
79 | model = CartItem
80 | fields = ['id', 'product', 'quantity', 'total_price']
81 |
82 |
83 | class AddCartItemSerializer(serializers.ModelSerializer):
84 | product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all())
85 |
86 | def save(self, **kwargs):
87 | product = self.validated_data['product']
88 | quantity = self.validated_data['quantity']
89 | cart = self.context.get('cart')
90 |
91 | try:
92 | cart_item = CartItem.objects.get(cart_id=cart, product_id=product)
93 | cart_item.quantity += quantity
94 | cart_item.save()
95 | self.instance = cart_item
96 | except CartItem.DoesNotExist:
97 | self.instance = CartItem.objects.create(cart_id=cart, **self.validated_data)
98 |
99 | return self.instance
100 |
101 | class Meta:
102 | model = CartItem
103 | fields = ['id', 'product', 'quantity']
104 |
105 |
106 | class UpdateCartSerializer(serializers.ModelSerializer):
107 | class Meta:
108 | model = CartItem
109 | fields = ['quantity']
110 |
111 |
112 | class CartSerializer(serializers.ModelSerializer):
113 | id = serializers.UUIDField(read_only=True)
114 | items = CartItemSerializer(many=True, read_only=True)
115 | total_price = serializers.SerializerMethodField(method_name='get_total_price')
116 |
117 | @staticmethod
118 | def get_total_price(cart: Cart):
119 | return sum([item.quantity * item.product.unit_price for item in cart.items.all()])
120 |
121 | class Meta:
122 | model = Cart
123 | fields = ['id', 'items', 'total_price']
124 |
125 |
126 | class OrderItemSerializer(serializers.ModelSerializer):
127 | product = ProductSerializer(read_only=True)
128 |
129 | class Meta:
130 | model = OrderItem
131 | fields = ['id', 'product', 'quantity', 'unit_price']
132 |
133 |
134 | class OrderSerializer(serializers.ModelSerializer):
135 | customer = serializers.PrimaryKeyRelatedField(read_only=True)
136 | payment_status = serializers.CharField(read_only=True)
137 | placed_at = serializers.DateTimeField(source='created_at', read_only=True)
138 | items = OrderItemSerializer(many=True, read_only=True)
139 |
140 | class Meta:
141 | model = Order
142 | fields = ['id', 'customer', 'payment_status', 'placed_at', 'items']
143 |
144 |
145 | class CreateOrderSerializer(serializers.Serializer):
146 | cart = serializers.PrimaryKeyRelatedField(queryset=Cart.objects.all())
147 |
148 | @staticmethod
149 | def validate_cart(value):
150 | if CartItem.objects.filter(cart=value).count() == 0:
151 | raise serializers.ValidationError('the cart is empty')
152 | return value
153 |
154 | @transaction.atomic
155 | def save(self, **kwargs):
156 | cart = self.validated_data['cart']
157 | customer = Customer.objects.get(user=self.context.get('user'))
158 | order = Order.objects.create(customer=customer)
159 | cart_items = CartItem.objects.filter(cart=cart).select_related('product')
160 |
161 | order_items = [
162 | OrderItem(
163 | order=order,
164 | product=item.product,
165 | quantity=item.quantity,
166 | unit_price=item.product.unit_price
167 | )
168 | for item in cart_items
169 | ]
170 | OrderItem.objects.bulk_create(order_items)
171 | cart.delete()
172 |
173 | return order
174 |
175 |
176 | class UpdateOrderSerializer(serializers.ModelSerializer):
177 | class Meta:
178 | model = Order
179 | fields = ['payment_status']
180 |
--------------------------------------------------------------------------------
/e_commerce/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for e_commerce project.
3 |
4 | Generated by 'django-admin startproject' using Django 5.0.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/5.0/ref/settings/
11 | """
12 |
13 | import os
14 | from datetime import timedelta
15 | from pathlib import Path
16 |
17 | from celery.schedules import crontab
18 | from dotenv import load_dotenv
19 |
20 | # Load and extract from .env file
21 | load_dotenv()
22 |
23 | # DB VAR
24 | CONNECTION = os.getenv('CONNECTION')
25 | NAME = os.getenv('NAME')
26 | USER = os.getenv('ROOT')
27 | PASSWORD = os.getenv('PASSWORD')
28 | HOST = os.getenv('HOST')
29 | PORT = os.getenv('PORT')
30 |
31 | # CELERY VAR
32 | REDIS_PORT = os.getenv('REDIS_PORT')
33 |
34 | # EMAIL VAR
35 | TYPE = os.getenv('EMAIL_TYPE')
36 |
37 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
38 | BASE_DIR = Path(__file__).resolve().parent.parent
39 |
40 | # Quick-start development settings - unsuitable for production
41 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
42 |
43 | # SECURITY WARNING: keep the secret key used in production secret!
44 | SECRET_KEY = 'django-insecure-gx5wnf%w@p^&850i(9uhk%z8_ry7kxva_o8v7-bx+33mdq#h)8'
45 |
46 | # SECURITY WARNING: don't run with debug turned on in production!
47 |
48 | DEBUG = True
49 |
50 | DEBUG_TOOLBAR_CONFIG = {
51 | 'SHOW_TOOLBAR_CALLBACK': lambda request: True
52 | }
53 |
54 | # USER AND HOST
55 |
56 | AUTH_USER_MODEL = 'core.User'
57 |
58 | ADMINS = [
59 | ('', ''),
60 | ]
61 |
62 | ALLOWED_HOSTS = [
63 | 'localhost', '127.0.0.1', '0.0.0.0'
64 | ]
65 |
66 | INTERNAL_IPS = [
67 | "127.0.0.1",
68 | ]
69 |
70 | # Application definition
71 |
72 | INSTALLED_APPS = [
73 | 'django.contrib.admin',
74 | 'django.contrib.auth',
75 | 'django.contrib.contenttypes',
76 | 'django.contrib.sessions',
77 | 'django.contrib.messages',
78 | 'django.contrib.staticfiles',
79 | 'corsheaders',
80 | 'django_filters',
81 | 'rest_framework',
82 | 'debug_toolbar',
83 | 'djoser',
84 | 'drf_yasg',
85 | 'core',
86 | 'store',
87 | 'likes',
88 | 'tags'
89 | ]
90 |
91 | MIDDLEWARE = [
92 | 'corsheaders.middleware.CorsMiddleware',
93 | 'django.middleware.security.SecurityMiddleware',
94 | 'django.contrib.sessions.middleware.SessionMiddleware',
95 | 'django.middleware.common.CommonMiddleware',
96 | 'django.middleware.csrf.CsrfViewMiddleware',
97 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
98 | 'django.contrib.messages.middleware.MessageMiddleware',
99 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
100 | "debug_toolbar.middleware.DebugToolbarMiddleware",
101 | ]
102 |
103 | ROOT_URLCONF = 'e_commerce.urls'
104 |
105 | TEMPLATES = [
106 | {
107 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
108 | 'DIRS': [],
109 | 'APP_DIRS': True,
110 | 'OPTIONS': {
111 | 'context_processors': [
112 | 'django.template.context_processors.debug',
113 | 'django.template.context_processors.request',
114 | 'django.contrib.auth.context_processors.auth',
115 | 'django.contrib.messages.context_processors.messages',
116 | ],
117 | },
118 | },
119 | ]
120 |
121 | WSGI_APPLICATION = 'e_commerce.wsgi.application'
122 | # Database
123 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
124 |
125 | DATABASES = {
126 | 'default': {
127 | 'ENGINE': f'django.db.backends.{CONNECTION}',
128 | 'NAME': NAME,
129 | 'USER': USER,
130 | 'PASSWORD': PASSWORD,
131 | 'HOST': HOST,
132 | 'PORT': PORT,
133 | }
134 | }
135 |
136 | # CELERY SETTINGS
137 |
138 | CELERY_BROKER_URL = f'redis://redis:{REDIS_PORT}/1'
139 |
140 | CELERY_BEAT_SCHEDULE = {
141 | 'remove_empty_cart': {
142 | 'task': 'store.tasks.remove_empty_cart',
143 | 'schedule': crontab(day_of_week=1, hour=12, minute=1),
144 | # 'args: [args],
145 | # 'kwargs': {}
146 | }
147 | }
148 |
149 | CACHES = {
150 | "default": {
151 | "BACKEND": "django_redis.cache.RedisCache",
152 | "LOCATION": f'redis://redis:{REDIS_PORT}/2',
153 | "OPTIONS": {
154 | "CLIENT_CLASS": "django_redis.client.DefaultClient",
155 | }
156 | }
157 | }
158 |
159 | # Password validation
160 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
161 |
162 | AUTH_PASSWORD_VALIDATORS = [
163 | {
164 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
165 | },
166 | {
167 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
168 | },
169 | {
170 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
171 | },
172 | {
173 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
174 | },
175 | ]
176 |
177 | # Internationalization
178 | # https://docs.djangoproject.com/en/5.0/topics/i18n/
179 |
180 | LANGUAGE_CODE = 'en-us'
181 |
182 | TIME_ZONE = 'UTC'
183 |
184 | USE_I18N = True
185 |
186 | USE_TZ = True
187 |
188 | # Static files (CSS, JavaScript, Images)
189 | # https://docs.djangoproject.com/en/5.0/howto/static-files/
190 |
191 | STATIC_URL = 'static/'
192 | STATIC_ROOT = BASE_DIR / 'static'
193 |
194 | MEDIA_URL = 'media/'
195 | MEDIA_ROOT = BASE_DIR / 'media'
196 |
197 | # Default primary key field type
198 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
199 |
200 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
201 |
202 | # DRF
203 |
204 | REST_FRAMEWORK = {
205 | 'COERCE_DECIMAL_PLACES': False,
206 | 'DEFAULT_AUTHENTICATION_CLASSES': (
207 | 'rest_framework_simplejwt.authentication.JWTAuthentication',
208 | ),
209 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
210 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
211 | 'PAGE_SIZE': 10
212 | }
213 |
214 | # AUTH
215 |
216 | DJOSER = {
217 | 'SERIALIZERS': {
218 | 'user': 'core.serializers.UserCreateSerializer',
219 | 'user_create': 'core.serializers.UserCreateSerializer',
220 | 'current_user': 'core.serializers.UserSerializer'
221 | }
222 | }
223 |
224 | SIMPLE_JWT = {
225 | 'AUTH_HEADER_TYPES': ('JWT',),
226 | "ACCESS_TOKEN_LIFETIME": timedelta(days=5),
227 | }
228 |
229 | # SWAGGER
230 |
231 | SWAGGER_SETTINGS = {
232 | 'SECURITY_DEFINITIONS': {
233 | 'Basic': {
234 | 'type': 'basic'
235 | }
236 | }
237 | }
238 |
239 | # ALLOWED CORS
240 |
241 | CORS_ALLOWED_ORIGINS = [
242 | 'http://localhost:8001',
243 | 'http://127.0.0.1:8001',
244 | 'http://localhost:8080',
245 | 'http://127.0.0.1:8080',
246 |
247 | ]
248 |
249 | # EMAIL SETTING
250 |
251 | EMAIL_BACKEND = f'django.core.mail.backends.{TYPE}.EmailBackend'
252 | EMAIL_HOST = os.getenv('EMAIL_HOST')
253 | EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
254 | EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
255 | EMAIL_PORT = os.getenv('EMAIL_PORT')
256 | DEFAULT_FROM_EMAIL = 'Amirmemool12@gmail.com'
257 |
--------------------------------------------------------------------------------
/store/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import get_object_or_404
2 | from rest_framework import exceptions
3 | from rest_framework import status
4 | from rest_framework.decorators import action
5 | from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, DestroyModelMixin
6 | from rest_framework.permissions import IsAuthenticated, IsAdminUser
7 | from rest_framework.request import Request
8 | from rest_framework.response import Response
9 | from rest_framework.viewsets import ModelViewSet, GenericViewSet
10 |
11 | from . import serializers
12 | from .models import *
13 | from .permissions import IsAuthAdminUserOrAuthReadOnly
14 |
15 |
16 | class CustomerViewSet(ModelViewSet):
17 | queryset = Customer.objects.all()
18 | serializer_class = serializers.CustomerSerializer
19 | permission_classes = [IsAdminUser]
20 | http_method_names = ['head', 'options', 'get', 'put', 'patch', 'delete']
21 |
22 | @action(detail=False, methods=['GET', 'PUT'], permission_classes=[IsAuthenticated])
23 | def me(self, request: Request) -> Response:
24 | customer = get_object_or_404(Customer, user=request.user)
25 | if request.method == 'GET':
26 | serializer = serializers.CustomerSerializer(customer)
27 | return Response(serializer.data, status=status.HTTP_200_OK)
28 | elif request.method == 'PUT':
29 | serializer = serializers.CustomerSerializer(customer, request.data)
30 | serializer.is_valid(raise_exception=True)
31 | serializer.save(user=request.user)
32 | return Response(serializer.data, status=status.HTTP_200_OK)
33 |
34 |
35 | class LoginCustomerAddressViewSet(ModelViewSet):
36 | queryset = Address.objects.all()
37 | serializer_class = serializers.CustomerAddressSerializer
38 | permission_classes = [IsAuthenticated]
39 |
40 | def get_queryset(self):
41 | return Address.objects.filter(customer_id=self.request.user.id).all()
42 |
43 | def perform_create(self, serializer):
44 | serializer.save(customer_id=self.request.user.id)
45 |
46 |
47 | class CustomerAddressViewSet(ModelViewSet):
48 | queryset = Address.objects.all()
49 | serializer_class = serializers.CustomerAddressSerializer
50 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
51 |
52 | def get_queryset(self):
53 | return Address.objects.filter(customer=self.kwargs['customers_pk']).all()
54 |
55 | def perform_create(self, serializer):
56 | serializer.save(customer=Customer(pk=self.kwargs['customers_pk']))
57 |
58 |
59 | class CollectionViewSet(ModelViewSet):
60 | queryset = Collection.objects.prefetch_related('products').all()
61 | serializer_class = serializers.CollectionSerializer
62 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
63 |
64 |
65 | def perform_destroy(self, instance):
66 | if instance.products.count() > 0:
67 | raise exceptions.ValidationError("Cannot delete a collection with associated products.")
68 | instance.delete()
69 |
70 |
71 |
72 | class ProductViewSet(ModelViewSet):
73 | queryset = Product.objects.prefetch_related('promotions', 'images').all()
74 | serializer_class = serializers.ProductSerializer
75 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
76 |
77 |
78 | class PromotionViewSet(ModelViewSet):
79 | queryset = Promotion.objects.prefetch_related('products').all()
80 | serializer_class = serializers.PromotionSerializer
81 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
82 |
83 |
84 | class ProductPromotionViewSet(ModelViewSet):
85 | queryset = Promotion.objects.all()
86 | serializer_class = serializers.ProductPromotionSerializer
87 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
88 |
89 | def get_queryset(self):
90 | return Promotion.objects.filter(products=self.kwargs['products_pk'])
91 |
92 | def perform_create(self, serializer):
93 | serializer.save(products=[self.kwargs['products_pk']])
94 |
95 |
96 | class ProductReviewViewSet(ModelViewSet):
97 | queryset = Review.objects.all()
98 | serializer_class = serializers.ProductReviewSerializer
99 | permission_classes = [IsAuthAdminUserOrAuthReadOnly]
100 |
101 | def get_queryset(self):
102 | return Review.objects.filter(product=self.kwargs['products_pk']).all()
103 |
104 | def perform_create(self, serializer):
105 | serializer.save(product=Product(pk=self.kwargs['products_pk']))
106 |
107 |
108 | class CartViewSet(DestroyModelMixin, RetrieveModelMixin, CreateModelMixin, GenericViewSet):
109 | queryset = Cart.objects.prefetch_related('items', 'items__product', 'items__product__promotions').all()
110 | serializer_class = serializers.CartSerializer
111 |
112 |
113 | class CartItemViewSet(ModelViewSet):
114 | queryset = CartItem.objects.all()
115 | http_method_names = ['head', 'options', 'get', 'post', 'patch', 'delete']
116 |
117 | def get_queryset(self):
118 | return CartItem.objects.filter(cart=self.kwargs['carts_pk']).select_related('product').all()
119 |
120 | def get_serializer_context(self):
121 | return {'cart': self.kwargs['carts_pk']}
122 |
123 | def get_serializer_class(self):
124 | if self.request.method == 'POST':
125 | return serializers.AddCartItemSerializer
126 | if self.request.method in ['PATCH']:
127 | return serializers.UpdateCartSerializer
128 | return serializers.CartItemSerializer
129 |
130 |
131 | class OrderViewSet(ModelViewSet):
132 | queryset = Order.objects.all()
133 | http_method_names = ['head', 'options', 'get', 'post', 'patch', 'delete']
134 |
135 | def get_permissions(self):
136 | if self.request.method in ['PATCH', 'DELETE']:
137 | return [IsAdminUser()]
138 | return [IsAuthenticated()]
139 |
140 | def get_queryset(self):
141 | if self.request.user.is_staff or self.request.user.is_superuser:
142 | return (Order.objects.prefetch_related(
143 | 'items', 'items__product',
144 | 'items__product__promotions',
145 | 'items__product__images').all())
146 | return (Order.objects.filter(customer_id=self.request.user.id).prefetch_related(
147 | 'items', 'items__product',
148 | 'items__product__promotions',
149 | 'items__product__images').all())
150 |
151 | def get_serializer_class(self):
152 | if self.request.method == 'POST':
153 | return serializers.CreateOrderSerializer
154 | elif self.request.method == 'PATCH':
155 | return serializers.UpdateOrderSerializer
156 | return serializers.OrderSerializer
157 |
158 | def get_serializer_context(self, **kwargs):
159 | return {'user': self.request.user}
160 |
161 | def create(self, request, *args, **kwargs):
162 | serializer = self.get_serializer(data=request.data)
163 | serializer.is_valid(raise_exception=True)
164 | order = serializer.save()
165 | serializer = serializers.OrderSerializer(order)
166 | headers = self.get_success_headers(serializer.data)
167 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
168 |
169 |
170 | class ProductImageViewSet(ModelViewSet):
171 | queryset = ProductImage.objects.all()
172 | serializer_class = serializers.ProductImageSerializer
173 |
174 | def get_queryset(self):
175 | return ProductImage.objects.filter(product=self.kwargs['products_pk']).all()
176 |
177 | def perform_create(self, serializer):
178 | serializer.save(product_id=self.kwargs['products_pk'])
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :fire: Django E-Commerce Hub :fire:
2 |
3 | ## Empowering Your Online Shopping Experience :bento: :bento:
4 |
5 | Welcome to the Django E-Commerce Hub, where we redefine your online shopping journey! Our platform offers a seamless and
6 | secure environment for buyers and sellers alike. Explore a wide range of products, manage your shopping cart
7 | effortlessly, and enjoy a smooth checkout process. Join us as we revolutionize the way you shop online!
8 |
9 | ## Acknowledgements
10 |
11 | - [Virtual Environments](#virtual_env)
12 | - [Env file](#env_file)
13 | - [DB && Docker](#db)
14 | - [Generate Fake DB Record](#factory)
15 | - [Email, Celery with Flower](#celery)
16 | - [Run Server](#run)
17 | - [Automation Test](#test)
18 | - [Swagger](#swagger)
19 | - [Authors](#author)
20 | - [Skills](#skill)
21 | - [License](#license)
22 |
23 | # Virtual Environments
24 |
25 | I prefer to use Pipenv which is a tool that simplifies Python dependency management by automatically creating and
26 | managing virtual environments for your projects. It combines the functionality of pip and virtualenv into a single
27 | package, making it easier to manage dependencies and ensure project consistency.
28 |
29 | - **Installation:** Install Pipenv using pip:
30 | if you already have pipenv jump this one
31 | ```bash
32 | pip install pipenv
33 | ```
34 |
35 | - **Creating a Virtual Environment:** Create a virtual environment for your project:
36 | ```bash
37 | pipenv install
38 | ```
39 | - [More about pipenv](https://pipenv.pypa.io/en/latest/)
40 |
41 | # Env File
42 |
43 | Environment variables are a way to store configuration settings or sensitive information that your application needs to
44 | function properly. They are typically stored in a file named `.env` in the root directory of your project. Here's a
45 | summary of how to use and manage environment variables in your project:
46 |
47 | - **Creating an Environment File**
48 |
49 | Create a file named `.env` in the root directory of your project. This file will store your environment variables.
50 | you need to create variable for DB, redis, email connection
51 |
52 | - **Adding Variables**
53 |
54 | Add your environment variables to the `.env` file using the `KEY=VALUE` format. For example:
55 |
56 | ```plaintext
57 | # DB Connection
58 |
59 | CONNECTION=mysql,sql,..
60 | NAME=appname
61 | ROOT=username
62 | PASSWORD=password
63 | HOST=hostname
64 | PORT=portname
65 |
66 | # REDIS Connection
67 |
68 | REDIS_PORT=redisport
69 | REDIS_PASSWORD=redispassword
70 | FLOWER_PORT=flowerport
71 |
72 | # SMTP Connection
73 |
74 | EMAIL_TYPE=smtp,..
75 | EMAIL_HOST=Emailhost
76 | EMAIL_HOST_USER=username
77 | EMAIL_HOST_PASSWORD=password
78 | EMAIL_PORT=port
79 |
80 | ```
81 |
82 | # DB && Dokcer
83 |
84 | fortunately We utilize Docker to manage our database environment. Docker enables containerization, simplifying
85 | deployment across different environments.
86 |
87 | We leverage Docker Compose to define and run multi-container Docker applications. Docker Compose reads environment
88 | variables from a `.env` file and build from Dockerfile, allowing flexible configuration. feel free to update the docker
89 | compose file as you need
90 |
91 | To start the database and related services, execute:
92 |
93 | ```bash
94 | docker compose up --build
95 | ```
96 |
97 | once you run it with `--build` just run `docker compose up` unless you changed the docker file
98 |
99 | for running any command inside your container simple use `docker comopse run web command` EX:
100 |
101 | ```bash
102 | docker compose exec web python manage.py store_seed
103 | ```
104 |
105 | # Generate Fake DB Record
106 |
107 | Factory Boy is a Python library that simplifies the creation of object instances in tests or database seeding. It
108 | provides a convenient and flexible way to define factory classes for generating test data.
109 |
110 | - **Data Generation:** Automatically generate values for fields based on predefined strategies or custom functions.
111 | - **Seeding Databases:** Use Factory Boy to populate databases with test data, making it easier to set up and tear down
112 | test environments.
113 |
114 | ```bash
115 | docker compose exec web python manage.py store_seed
116 | ```
117 |
118 | - some time may an error occur while seeding db but don't worry we use transaction so all record rollback
119 | and run it again
120 |
121 | # Email, Celery with Flower
122 |
123 | so app also support sending email for make it work you need to add your email server for that just add variable
124 | to `.env` file
125 |
126 | ```plaintext
127 |
128 | # SMTP Connection
129 |
130 | EMAIL_TYPE=smtp,..
131 | EMAIL_HOST=Emailhost
132 | EMAIL_HOST_USER=username
133 | EMAIL_HOST_PASSWORD=password
134 | EMAIL_PORT=port
135 |
136 | ```
137 |
138 | as you now sending email or some task may take too much time so it's time to set up celery
139 |
140 | ```plaintext
141 |
142 | # REDIS Connection
143 |
144 | REDIS_PORT=redisport
145 | REDIS_PASSWORD=redispassword
146 | FLOWER_PORT=flowerport
147 |
148 | ```
149 |
150 | add the variable to you `.env` file. you also have access to flower to find out about jobs
151 |
152 | make sure your dokcer compose is up with no error to enjoy celery
153 |
154 | # Run Server
155 |
156 | for running server you don't need to run `python manage.py runserver` case we create a script that run migration and
157 | runserver you just need dto make sure the docker compose is up
158 |
159 | # Automation Test
160 |
161 | automation test is a solid way to test the api without too much pain.I prefer to pytest for testing which is a very
162 | simple and full feature framework for testing app
163 |
164 | ```bash
165 | docker compose exec web pytest -W ingore
166 | ```
167 |
168 | simply use for testing and for current deprecation of a package in pytest we get a warning so simply ignore it
169 |
170 | # Swagger for API Documentation
171 |
172 | We use Swagger for API documentation, providing a clear overview of our API endpoints and their functionalities.
173 |
174 | **Accessing Swagger Documentation:**
175 |
176 | Visit `host:port/swagger` or `host:port/redoc` to explore our API documentation and understand how to interact with our
177 | endpoints.
178 |
179 | # Authors
180 |
181 | This project was developed by our dedicated team of developers, committed to delivering high-quality software solutions:
182 |
183 | - [Amir Hossein Motaghian](https://github.com/johndoe)
184 |
185 | For inquiries or collaboration opportunities, feel free to reach out to any of our team members via their respective
186 | GitHub profiles.
187 |
188 | # Skills
189 |
190 | - **Backend Development:** Experienced in building robust backend systems using Python, Django
191 | - **Database Management:** Skilled in database design, optimization, and management MySQL
192 | - **API Development:** Expertise in designing and implementing RESTFUL documenting with Swagger, and testing with
193 | Postman.
194 | - **Security:** Well-versed in web application security principles, including authentication, authorization, and
195 | protection against common vulnerabilities.
196 | - **Agile Methodologies:** Practiced in Agile development methodologies such as Scrum and Kanban, fostering
197 | collaboration and delivering incremental value to stakeholders.
198 | - **Continuous Learning:** Committed to staying up-to-date with the latest technologies and best practices through
199 | continuous learning and participation in community events.
200 | - **Packages:** we give a thanks to the authors of these packages also:
201 | 1_django-debug-toolbar, 2_djangorestframework, 3_mysqlclient, 4_python-dotenv, 5_djoser
202 | 6_djangorestframework-simplejwt, 7_drf-nested-routers, 8_drf-yasg, 9_factory_boy,
203 | 10_pillow, 11_django-cors-headers, 12_django-templated-mail, 13_redis, 14_celery, 15_flower,
204 | 12_pytest, 13_pytest-django, 14_model-bakery, 15_pytest-watch, 16_django-redis
205 |
206 | Our diverse skill set enables us to tackle complex challenges and deliver innovative solutions to meet our clients'
207 | needs.
208 |
209 | # License
210 |
211 | This project is licensed under the [MIT License](LICENSE), granting you the freedom to use, modify, and distribute the
212 | code for both commercial and non-commercial purposes. See the [LICENSE](LICENSE) file for more details.
213 |
--------------------------------------------------------------------------------