├── 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 | Product 1 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 | --------------------------------------------------------------------------------