diff --git a/cart/cart.py b/cart/cart.py index fde5a26..b52499a 100644 --- a/cart/cart.py +++ b/cart/cart.py @@ -3,6 +3,7 @@ from django.conf import settings from shop.models import Product +from coupons.models import Coupon from common.constants import (KEY_PRICE, KEY_QUANTITY, KEY_PRODUCT, KEY_TOTAL_PRICE) @@ -15,11 +16,29 @@ def __init__(self, request): """ self.session = request.session + self.coupon_id = self.session.get('coupon_id') cart = self.session.get(settings.SESSION_CART_ID) if not cart: cart = self.session[settings.SESSION_CART_ID] = {} self.cart = cart + @property + def coupon(self): + if self.coupon_id: + try: + return Coupon.objects.get(id=self.coupon_id) + except Coupon.DoesNotExist: + pass + return None + + def get_discount(self): + if self.coupon: + return (self.coupon.discount / Decimal(100)) * self.get_total_price() + return Decimal(0) + + def get_price_after_coupon(self): + return self.get_total_price() - self.get_discount() + def add(self, product, quantity=1, override_quantity=False): """ Add Item in cart or update its quantity diff --git a/cart/forms.py b/cart/forms.py index e7dfc32..3547b66 100644 --- a/cart/forms.py +++ b/cart/forms.py @@ -1,9 +1,11 @@ from django import forms +from django.utils.translation import gettext_lazy as _ PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)] class CartAddProductForm(forms.Form): - quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int) + quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int, + label=_('Quantity')) override = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) diff --git a/cart/templates/cart/cart_details.html b/cart/templates/cart/cart_details.html index 9acb99f..ee873d7 100644 --- a/cart/templates/cart/cart_details.html +++ b/cart/templates/cart/cart_details.html @@ -1,19 +1,20 @@ {% extends 'shop/base.html' %} {% load static %} +{% load i18n %} -{% block title %} Your shopping Cart {% endblock %} +{% block title %} {% trans 'Your shopping Cart' %} {% endblock %} {% block content %} -

Your shopping cart

+

{% trans 'Your shopping cart' %}

- - - - - - + + + + + + @@ -36,24 +37,50 @@

Your shopping cart

+ {% if cart.coupon %} + + + + + + + + + + + {% endif %} {% endwith %} {% endfor %} - + - +
ImageProductQuantityRemoveUnit PricePrice{% trans 'Image' %}{% trans 'Product' %}{% trans 'Quantity' %}{% trans 'Remove' %}{% trans 'Unit Price' %}{% trans 'Price' %}
- + {% csrf_token %}
{{item.price}} {{item.total_price}}
{% trans 'Subtotal' %}${{cart.get_total_price|floatformat:2}}
+ "{{cart.coupon.code}}" coupon + ({{cart.coupon.discount}} off) + + - ${{cart.get_discount|floatformat:2}} +
Total{% trans 'Total' %} ${{cart.get_total_price}}${{cart.get_price_after_coupon|floatformat:2}}
+
+

{% trans 'Apply a Coupon' %}

+
+ {{coupon_apply_form}} + {% csrf_token %} + +
+
+

- Continue Shopping - Checkout + {% trans 'Continue Shopping' %} + {% trans 'Checkout' %}

{% endblock %} \ No newline at end of file diff --git a/cart/views.py b/cart/views.py index 54d5020..0b2f311 100644 --- a/cart/views.py +++ b/cart/views.py @@ -4,6 +4,7 @@ from .cart import Cart from .forms import CartAddProductForm from shop.models import Product +from coupons.forms import CouponApplyForm from common.constants import KEY_QUANTITY @@ -30,8 +31,10 @@ def cart_remove(request, product_id): def cart_details(request): cart = Cart(request) + coupon_apply_form = CouponApplyForm() for item in cart: item['product_update_form'] = CartAddProductForm(initial={ KEY_QUANTITY: item.get(KEY_QUANTITY), 'override': True }) - return render(request, 'cart/cart_details.html', {'cart': cart}) + return render(request, 'cart/cart_details.html', {'cart': cart, + 'coupon_apply_form': coupon_apply_form}) diff --git a/coupons/__init__.py b/coupons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coupons/admin.py b/coupons/admin.py new file mode 100644 index 0000000..3717dfb --- /dev/null +++ b/coupons/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import Coupon + + +@admin.register(Coupon) +class CouponAdmin(admin.ModelAdmin): + list_display = ['code', 'valid_from', 'valid_to', 'discount', 'active', 'total_coupons', 'availed_coupons'] + list_filter = ['active', 'valid_to', 'valid_from'] + search_fields = ['code'] \ No newline at end of file diff --git a/coupons/apps.py b/coupons/apps.py new file mode 100644 index 0000000..1190536 --- /dev/null +++ b/coupons/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CouponsConfig(AppConfig): + name = 'coupons' diff --git a/coupons/forms.py b/coupons/forms.py new file mode 100644 index 0000000..9c4421f --- /dev/null +++ b/coupons/forms.py @@ -0,0 +1,6 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class CouponApplyForm(forms.Form): + code = forms.CharField(label=_('Code')) diff --git a/coupons/migrations/0001_initial.py b/coupons/migrations/0001_initial.py new file mode 100644 index 0000000..7dd1596 --- /dev/null +++ b/coupons/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.3 on 2021-12-31 18:47 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Coupon', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=32, unique=True)), + ('valid_from', models.DateTimeField()), + ('valid_to', models.DateTimeField()), + ('discount', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), + ('active', models.BooleanField()), + ('total_coupons', models.PositiveIntegerField(default=3)), + ('availed_coupons', models.PositiveIntegerField(default=0)), + ], + ), + ] diff --git a/coupons/migrations/__init__.py b/coupons/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coupons/models.py b/coupons/models.py new file mode 100644 index 0000000..be63841 --- /dev/null +++ b/coupons/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator + + +class Coupon(models.Model): + code = models.CharField(max_length=32, unique=True, db_index=True) + valid_from = models.DateTimeField() + valid_to = models.DateTimeField() + discount = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(100)]) + active = models.BooleanField() + total_coupons = models.PositiveIntegerField(default=3) + availed_coupons = models.PositiveIntegerField(default=0) + + def __str__(self): + return f"Coupon code:{self.code}" diff --git a/coupons/tests.py b/coupons/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/coupons/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/coupons/urls.py b/coupons/urls.py new file mode 100644 index 0000000..eca8d9b --- /dev/null +++ b/coupons/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import coupon_apply + +app_name = 'coupons' + + +urlpatterns = [ + path('apply/', coupon_apply, name='apply') +] diff --git a/coupons/views.py b/coupons/views.py new file mode 100644 index 0000000..de6da20 --- /dev/null +++ b/coupons/views.py @@ -0,0 +1,21 @@ +from django.utils import timezone +from django.shortcuts import render, redirect +from django.views.decorators.http import require_POST + +from .models import Coupon +from .forms import CouponApplyForm + + +@require_POST +def coupon_apply(request): + now = timezone.now() + form = CouponApplyForm(request.POST) + if form.is_valid(): + code = form.cleaned_data['code'] + try: + coupon = Coupon.objects.get(code=code, valid_from__lte=now, valid_to__gte=now, active=True) + request.session['coupon_id'] = coupon.id + except Coupon.DoesNotExist: + request.session['coupon_id'] = None + + return redirect('cart:cart_details') diff --git a/eShop/settings.py b/eShop/settings.py index 197abb6..2f321cf 100644 --- a/eShop/settings.py +++ b/eShop/settings.py @@ -12,6 +12,7 @@ import os import braintree +from django.utils.translation import gettext_lazy as _ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -39,11 +40,13 @@ 'cart.apps.CartConfig', 'orders.apps.OrdersConfig', 'payment.apps.PaymentConfig', + 'coupons.apps.CouponsConfig' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -106,7 +109,12 @@ # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'en' +LANGUAGES = ( + ('en', _('English')), + ('es', _('Spanish')), + ('ur', _('Urdu')), +) TIME_ZONE = 'UTC' @@ -119,6 +127,9 @@ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') STATIC_ROOT = os.path.join(BASE_DIR, 'static/') +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale/'), +) # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ @@ -133,6 +144,10 @@ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +REDIS_HOST = 'localhost' +REDIS_PORT = '6379' +REDIS_DB = 1 + try: from .local_settings import * # pylint: disable=all except: # pylint: disable=bare-except diff --git a/eShop/urls.py b/eShop/urls.py index 315030c..42577f9 100644 --- a/eShop/urls.py +++ b/eShop/urls.py @@ -17,13 +17,15 @@ from django.conf import settings from django.urls import path, include from django.conf.urls.static import static +from django.conf.urls.i18n import i18n_patterns -urlpatterns = [ +urlpatterns = i18n_patterns( path('admin/', admin.site.urls), path('payment/', include('payment.urls', namespace='payment')), path('cart/', include('cart.urls', namespace='cart')), path('orders/', include('orders.urls', namespace='orders')), + path('coupons/', include('coupons.urls', namespace='coupons')), path('', include('shop.urls', namespace='shop')), -] +) if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..aca0f3a --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,124 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: cart/forms.py:9 cart/templates/cart/cart_details.html:14 +msgid "Quantity" +msgstr "" + +#: cart/templates/cart/cart_details.html:5 +msgid "Your shopping Cart" +msgstr "" + +#: cart/templates/cart/cart_details.html:8 +msgid "Your shopping cart" +msgstr "" + +#: cart/templates/cart/cart_details.html:12 +msgid "Image" +msgstr "" + +#: cart/templates/cart/cart_details.html:13 +msgid "Product" +msgstr "" + +#: cart/templates/cart/cart_details.html:15 +msgid "Remove" +msgstr "" + +#: cart/templates/cart/cart_details.html:16 +msgid "Unit Price" +msgstr "" + +#: cart/templates/cart/cart_details.html:17 +msgid "Price" +msgstr "" + +#: cart/templates/cart/cart_details.html:40 +msgid "Remove Item" +msgstr "" + +#: cart/templates/cart/cart_details.html:49 +msgid "Subtotal" +msgstr "" + +#: cart/templates/cart/cart_details.html:67 +msgid "Total" +msgstr "" + +#: cart/templates/cart/cart_details.html:74 +msgid "Apply a Coupon" +msgstr "" + +#: cart/templates/cart/cart_details.html:78 +msgid "Apply" +msgstr "" + +#: cart/templates/cart/cart_details.html:83 +msgid "Continue Shopping" +msgstr "" + +#: cart/templates/cart/cart_details.html:84 +msgid "Checkout" +msgstr "" + +#: coupons/forms.py:6 +msgid "Code" +msgstr "" + +#: eShop/settings.py:114 +msgid "English" +msgstr "" + +#: eShop/settings.py:115 +msgid "Spanish" +msgstr "" + +#: eShop/settings.py:116 +msgid "Urdu" +msgstr "" + +#: shop/templates/shop/base.html:7 shop/templates/shop/base.html:12 +msgid "eShop by Zee" +msgstr "" + +#: shop/templates/shop/base.html:18 +msgid "Your Cart" +msgstr "" + +#: shop/templates/shop/base.html:20 +#, python-format +msgid "" +"\n" +" %(items)s item, $%(total)s\n" +" " +msgid_plural "" +"\n" +" %(items)s items, $%(total)s\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: shop/templates/shop/base.html:27 +msgid "Your cart is empty." +msgstr "" + +#: shop/templates/shop/product/details.html:19 +msgid "Add to Cart" +msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..bd62c28 --- /dev/null +++ b/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,124 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: cart/forms.py:9 cart/templates/cart/cart_details.html:14 +msgid "Quantity" +msgstr "" + +#: cart/templates/cart/cart_details.html:5 +msgid "Your shopping Cart" +msgstr "" + +#: cart/templates/cart/cart_details.html:8 +msgid "Your shopping cart" +msgstr "" + +#: cart/templates/cart/cart_details.html:12 +msgid "Image" +msgstr "" + +#: cart/templates/cart/cart_details.html:13 +msgid "Product" +msgstr "" + +#: cart/templates/cart/cart_details.html:15 +msgid "Remove" +msgstr "" + +#: cart/templates/cart/cart_details.html:16 +msgid "Unit Price" +msgstr "" + +#: cart/templates/cart/cart_details.html:17 +msgid "Price" +msgstr "" + +#: cart/templates/cart/cart_details.html:40 +msgid "Remove Item" +msgstr "" + +#: cart/templates/cart/cart_details.html:49 +msgid "Subtotal" +msgstr "" + +#: cart/templates/cart/cart_details.html:67 +msgid "Total" +msgstr "" + +#: cart/templates/cart/cart_details.html:74 +msgid "Apply a Coupon" +msgstr "" + +#: cart/templates/cart/cart_details.html:78 +msgid "Apply" +msgstr "" + +#: cart/templates/cart/cart_details.html:83 +msgid "Continue Shopping" +msgstr "" + +#: cart/templates/cart/cart_details.html:84 +msgid "Checkout" +msgstr "" + +#: coupons/forms.py:6 +msgid "Code" +msgstr "" + +#: eShop/settings.py:114 +msgid "English" +msgstr "Inglés" + +#: eShop/settings.py:115 +msgid "Spanish" +msgstr "Español" + +#: eShop/settings.py:116 +msgid "Urdu" +msgstr "Urdau" + +#: shop/templates/shop/base.html:7 shop/templates/shop/base.html:12 +msgid "eShop by Zee" +msgstr "" + +#: shop/templates/shop/base.html:18 +msgid "Your Cart" +msgstr "" + +#: shop/templates/shop/base.html:20 +#, python-format +msgid "" +"\n" +" %(items)s item, $%(total)s\n" +" " +msgid_plural "" +"\n" +" %(items)s items, $%(total)s\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: shop/templates/shop/base.html:27 +msgid "Your cart is empty." +msgstr "" + +#: shop/templates/shop/product/details.html:19 +msgid "Add to Cart" +msgstr "" diff --git a/locale/ur/LC_MESSAGES/django.po b/locale/ur/LC_MESSAGES/django.po new file mode 100644 index 0000000..7520d9d --- /dev/null +++ b/locale/ur/LC_MESSAGES/django.po @@ -0,0 +1,128 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: cart/forms.py:9 cart/templates/cart/cart_details.html:14 +msgid "Quantity" +msgstr "مقدار" + +#: cart/templates/cart/cart_details.html:5 +#, fuzzy +#| msgid "Your Cart" +msgid "Your shopping Cart" +msgstr "آپکی ٹوکری" + +#: cart/templates/cart/cart_details.html:8 +msgid "Your shopping cart" +msgstr "آپکی ٹوکری" + +#: cart/templates/cart/cart_details.html:12 +msgid "Image" +msgstr "تصویر" + +#: cart/templates/cart/cart_details.html:13 +msgid "Product" +msgstr "چیز" + +#: cart/templates/cart/cart_details.html:15 +msgid "Remove" +msgstr "ہٹائیں" + +#: cart/templates/cart/cart_details.html:16 +msgid "Unit Price" +msgstr "ایک چیزکی قیمت" + +#: cart/templates/cart/cart_details.html:17 +msgid "Price" +msgstr "قیمت" + +#: cart/templates/cart/cart_details.html:40 +#, fuzzy +#| msgid "Remove" +msgid "Remove Item" +msgstr "ہٹائیں" + +#: cart/templates/cart/cart_details.html:49 +msgid "Subtotal" +msgstr "آدھا میزان" + +#: cart/templates/cart/cart_details.html:67 +msgid "Total" +msgstr "میزان" + +#: cart/templates/cart/cart_details.html:74 +msgid "Apply a Coupon" +msgstr "کوپن لگائیں" + +#: cart/templates/cart/cart_details.html:78 +msgid "Apply" +msgstr "لگائیے" + +#: cart/templates/cart/cart_details.html:83 +msgid "Continue Shopping" +msgstr "شاپنگ کرتے رہیں" + +#: cart/templates/cart/cart_details.html:84 +msgid "Checkout" +msgstr "پوراکریں" + +#: coupons/forms.py:6 +msgid "Code" +msgstr "کوڈ" + +#: eShop/settings.py:114 +msgid "English" +msgstr "انگریزی" + +#: eShop/settings.py:115 +msgid "Spanish" +msgstr "اسپینش" + +#: eShop/settings.py:116 +msgid "Urdu" +msgstr "اردو" + +#: shop/templates/shop/base.html:7 shop/templates/shop/base.html:12 +msgid "eShop by Zee" +msgstr "آئن لائن دکان" + +#: shop/templates/shop/base.html:18 +msgid "Your Cart" +msgstr "آپکی ٹوکری" + +#: shop/templates/shop/base.html:20 +#, python-format +msgid "" +"\n" +" %(items)s چیز, $%(total)s\n" +" " +msgid_plural "" +"\n" +" %(items)s چیزیں, $%(total)s\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: shop/templates/shop/base.html:27 +msgid "Your cart is empty." +msgstr "آپکی ٹوکری خالی ہے" + +#: shop/templates/shop/product/details.html:19 +msgid "Add to Cart" +msgstr "ٹوکری میں ڈالیں" diff --git a/orders/locale/en/LC_MESSAGES/django.po b/orders/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..6012aff --- /dev/null +++ b/orders/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,96 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: orders/models.py:11 +msgid "first name" +msgstr "" + +#: orders/models.py:12 +msgid "last name" +msgstr "" + +#: orders/models.py:13 +msgid "email" +msgstr "" + +#: orders/models.py:14 +msgid "address" +msgstr "" + +#: orders/models.py:15 +msgid "city" +msgstr "" + +#: orders/models.py:16 +msgid "created" +msgstr "" + +#: orders/models.py:17 +msgid "updated" +msgstr "" + +#: orders/models.py:18 +msgid "paid" +msgstr "" + +#: orders/models.py:19 +msgid "braintree id" +msgstr "" + +#: orders/models.py:22 +msgid "Discount" +msgstr "" + +#: orders/templates/orders/order/order_create.html:4 +#: orders/templates/orders/order/order_create.html:7 +msgid "Checkout" +msgstr "" + +#: orders/templates/orders/order/order_create.html:10 +msgid "Your Order" +msgstr "" + +#: orders/templates/orders/order/order_create.html:20 +#, python-format +msgid "" +"\n" +" \"%(code)s\" %(discount)s %%off\n" +" " +msgstr "" + +#: orders/templates/orders/order/order_create.html:27 +msgid "Total" +msgstr "" + +#: orders/templates/orders/order/order_create.html:31 +msgid "Place Order" +msgstr "" + +#: orders/templates/orders/order/order_created.html:3 +msgid "Thank you" +msgstr "" + +#: orders/templates/orders/order/order_created.html:6 +msgid "Thank you!" +msgstr "" + +#: orders/templates/orders/order/order_created.html:7 +msgid "Your order has been successfully completed. Your order number is" +msgstr "" diff --git a/orders/locale/es/LC_MESSAGES/django.po b/orders/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..95599d5 --- /dev/null +++ b/orders/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,96 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: orders/models.py:11 +msgid "first name" +msgstr "nombre" + +#: orders/models.py:12 +msgid "last name" +msgstr "apellidos" + +#: orders/models.py:13 +msgid "email" +msgstr "email" + +#: orders/models.py:14 +msgid "address" +msgstr "dirección" + +#: orders/models.py:15 +msgid "city" +msgstr "ciudad" + +#: orders/models.py:16 +msgid "created" +msgstr "created" + +#: orders/models.py:17 +msgid "updated" +msgstr "updated" + +#: orders/models.py:18 +msgid "paid" +msgstr "paid" + +#: orders/models.py:19 +msgid "braintree id" +msgstr "" + +#: orders/models.py:22 +msgid "Discount" +msgstr "" + +#: orders/templates/orders/order/order_create.html:4 +#: orders/templates/orders/order/order_create.html:7 +msgid "Checkout" +msgstr "" + +#: orders/templates/orders/order/order_create.html:10 +msgid "Your Order" +msgstr "" + +#: orders/templates/orders/order/order_create.html:20 +#, python-format +msgid "" +"\n" +" \"%(code)s\" %(discount)s %%off\n" +" " +msgstr "" + +#: orders/templates/orders/order/order_create.html:27 +msgid "Total" +msgstr "" + +#: orders/templates/orders/order/order_create.html:31 +msgid "Place Order" +msgstr "" + +#: orders/templates/orders/order/order_created.html:3 +msgid "Thank you" +msgstr "" + +#: orders/templates/orders/order/order_created.html:6 +msgid "Thank you!" +msgstr "" + +#: orders/templates/orders/order/order_created.html:7 +msgid "Your order has been successfully completed. Your order number is" +msgstr "" diff --git a/orders/locale/ur/LC_MESSAGES/django.po b/orders/locale/ur/LC_MESSAGES/django.po new file mode 100644 index 0000000..108d1fd --- /dev/null +++ b/orders/locale/ur/LC_MESSAGES/django.po @@ -0,0 +1,96 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-03 22:29+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: orders/models.py:11 +msgid "first name" +msgstr "نام کا پہلا حصہ" + +#: orders/models.py:12 +msgid "last name" +msgstr "آخری نام" + +#: orders/models.py:13 +msgid "email" +msgstr "ای میل" + +#: orders/models.py:14 +msgid "address" +msgstr "پتہ" + +#: orders/models.py:15 +msgid "city" +msgstr "شہر" + +#: orders/models.py:16 +msgid "created" +msgstr "آرڈرکی تاریخ" + +#: orders/models.py:17 +msgid "updated" +msgstr "آرڈرمیں ترمیم" + +#: orders/models.py:18 +msgid "paid" +msgstr "مکمل" + +#: orders/models.py:19 +msgid "braintree id" +msgstr "آئی ڈی" + +#: orders/models.py:22 +msgid "Discount" +msgstr "رعایت" + +#: orders/templates/orders/order/order_create.html:4 +#: orders/templates/orders/order/order_create.html:7 +msgid "Checkout" +msgstr "پیسے نکالیں" + +#: orders/templates/orders/order/order_create.html:10 +msgid "Your Order" +msgstr "آپکاآرڈر" + +#: orders/templates/orders/order/order_create.html:20 +#, python-format +msgid "" +"\n" +" \"%(code)s\" %(discount)s %%off\n" +" " +msgstr "" + +#: orders/templates/orders/order/order_create.html:27 +msgid "Total" +msgstr "میزان" + +#: orders/templates/orders/order/order_create.html:31 +msgid "Place Order" +msgstr "آرڈدیں" + +#: orders/templates/orders/order/order_created.html:3 +msgid "Thank you" +msgstr "شکریہ" + +#: orders/templates/orders/order/order_created.html:6 +msgid "Thank you!" +msgstr "شکریہ!" + +#: orders/templates/orders/order/order_created.html:7 +msgid "Your order has been successfully completed. Your order number is" +msgstr "آپکاآرڈررکھ دیاگیا ہے" diff --git a/orders/migrations/0003_auto_20220103_0503.py b/orders/migrations/0003_auto_20220103_0503.py new file mode 100644 index 0000000..ee13258 --- /dev/null +++ b/orders/migrations/0003_auto_20220103_0503.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.3 on 2022-01-03 05:03 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('coupons', '0001_initial'), + ('orders', '0002_order_braintree_id'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='coupon', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='coupons.coupon'), + ), + migrations.AddField( + model_name='order', + name='discount', + field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]), + ), + ] diff --git a/orders/models.py b/orders/models.py index ede3b41..3075f7f 100644 --- a/orders/models.py +++ b/orders/models.py @@ -1,18 +1,25 @@ +from decimal import Decimal from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.validators import MinValueValidator, MaxValueValidator +from coupons.models import Coupon from shop.models import Product class Order(models.Model): - first_name = models.CharField(max_length=32) - last_name = models.CharField(max_length=32) - email = models.EmailField() - address = models.CharField(max_length=64) - city = models.CharField(max_length=16) - created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) - paid = models.BooleanField(default=False) - braintree_id = models.CharField(max_length=156, blank=True) + first_name = models.CharField(_('first name'), max_length=32) + last_name = models.CharField(_('last name'), max_length=32) + email = models.EmailField(_('email')) + address = models.CharField(_('address'), max_length=64) + city = models.CharField(_('city'), max_length=16) + created = models.DateTimeField(_('created'), auto_now_add=True) + updated = models.DateTimeField(_('updated'), auto_now=True) + paid = models.BooleanField(_('paid'), default=False) + braintree_id = models.CharField(_('braintree id'), max_length=156, blank=True) + coupon = models.ForeignKey(Coupon, related_name='orders', null=True, blank=True, + on_delete=models.SET_NULL) + discount = models.IntegerField(_('Discount'), default=0, validators=[MinValueValidator(0), MaxValueValidator(100)]) class Meta: ordering = ('-created',) @@ -20,9 +27,13 @@ class Meta: def __str__(self): return f'Order ID: {self.id} by {self.first_name}' - def get_total_cost(self): + def get_total_without_coupon(self): return sum([item.get_cost() for item in self.items.all()]) + def get_total_cost(self): + total_cost = self.get_total_without_coupon() + return total_cost - total_cost * (self.discount/Decimal(100)) + class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) diff --git a/orders/templates/admin/orders/order/detail.html b/orders/templates/admin/orders/order/detail.html index 03388c3..254f72a 100644 --- a/orders/templates/admin/orders/order/detail.html +++ b/orders/templates/admin/orders/order/detail.html @@ -69,8 +69,14 @@

Items bought

{% endfor %} Total - ${{order.get_total_cost}} + ${{order.get_total_without_coupon}} + {% if order.coupon %} + + Coupon ("{{order.coupon.discount}}" off) + ${{order.get_total_cost}}> + + {% endif %} diff --git a/orders/templates/orders/order/order_create.html b/orders/templates/orders/order/order_create.html index f901d21..699b150 100644 --- a/orders/templates/orders/order/order_create.html +++ b/orders/templates/orders/order/order_create.html @@ -1,25 +1,34 @@ {% extends 'shop/base.html' %} +{% load i18n %} -{% block title %} Checkout {% endblock %} +{% block title %}{% trans 'Checkout' %} {% endblock %} {% block content %} -

Checkout

+

{% trans 'Checkout' %}

-

Your Order

+

{% trans 'Your Order' %}

    {%for item in cart %}
  • {{item.quantity}}x {{item.product.name}} - ${{item.total_price}} + ${{item.total_price|floatformat:2}}
  • {% endfor %} + {% if cart.coupon %} +
  • + {% blocktrans with code=cart.coupon.code discount=cart.coupon.code %} + "{{code}}" {{discount}} %off + {% endblocktrans %} + - ${{cart.get_discount|floatformat:2}} +
  • + {%endif%}
-

Total: ${{cart.get_total_price}}

+

{% trans 'Total' %}: ${{cart.get_price_after_coupon|floatformat:2}}

{{form.as_p}} -

+

{% csrf_token %}
{% endblock %} diff --git a/orders/templates/orders/order/order_created.html b/orders/templates/orders/order/order_created.html index e7698a4..5670b89 100644 --- a/orders/templates/orders/order/order_created.html +++ b/orders/templates/orders/order/order_created.html @@ -1,8 +1,8 @@ {% extends 'shop/base.html' %} - -{% block title %}Thank you {% endblock %} +{% load i18n %} +{% block title %}{% trans 'Thank you' %} {% endblock %} {% block content %} -

Thank you!

-

Your order has been successfully completed. Your order number is {{order.id}}.

+

{% trans 'Thank you!' %}

+

{% trans 'Your order has been successfully completed. Your order number is' %} {{order.id}}.

{% endblock %} \ No newline at end of file diff --git a/orders/views.py b/orders/views.py index aeae4f4..dfc3e41 100644 --- a/orders/views.py +++ b/orders/views.py @@ -19,7 +19,11 @@ def order_create(request): if request.method == 'POST': form = OrderCreateForm(request.POST) if form.is_valid(): - order = form.save() + order = form.save(commit=False) + if cart.coupon: + order.coupon = cart.coupon + order.discount = cart.coupon.discount + order.save() log_info("Order saved!!!") for item in cart: OrderItem.objects.create(order=order, product=item['product'], diff --git a/shop/recommender.py b/shop/recommender.py new file mode 100644 index 0000000..7aeddf4 --- /dev/null +++ b/shop/recommender.py @@ -0,0 +1,59 @@ +import redis +from django.conf import settings +from .models import Product + + +# connect to redis +r = redis.Redis(host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=settings.REDIS_DB) + + +class Recommender(object): + + def get_product_key(self, id): + return f'product:{id}:purchased_with' + + def products_bought(self, products): + product_ids = [p.id for p in products] + for product_id in product_ids: + for with_id in product_ids: + # get the other products bought with each product + if product_id != with_id: + # increment score for product purchased together + r.zincrby(self.get_product_key(product_id), + 1, + with_id) + + def suggest_products_for(self, products, max_results=6): + product_ids = [p.id for p in products] + if len(products) == 1: + # only 1 product + suggestions = r.zrange( + self.get_product_key(product_ids[0]), + 0, -1, desc=True)[:max_results] + else: + # generate a temporary key + flat_ids = ''.join([str(id) for id in product_ids]) + tmp_key = f'tmp_{flat_ids}' + # multiple products, combine scores of all products + # store the resulting sorted set in a temporary key + keys = [self.get_product_key(id) for id in product_ids] + r.zunionstore(tmp_key, keys) + # remove ids for the products the recommendation is for + r.zrem(tmp_key, *product_ids) + # get the product ids by their score, descendant sort + suggestions = r.zrange(tmp_key, 0, -1, + desc=True)[:max_results] + # remove the temporary key + r.delete(tmp_key) + suggested_products_ids = [int(id) for id in suggestions] + + # get suggested products and sort by order of appearance + suggested_products = list(Product.objects.filter(id__in=suggested_products_ids)) + suggested_products.sort(key=lambda x: suggested_products_ids.index(x.id)) + return suggested_products + + def clear_purchases(self): + for id in Product.objects.values_list('id', flat=True): + r.delete(self.get_product_key(id)) diff --git a/shop/templates/shop/base.html b/shop/templates/shop/base.html index c044e75..18d74d9 100644 --- a/shop/templates/shop/base.html +++ b/shop/templates/shop/base.html @@ -1,26 +1,45 @@ {% load static %} +{% load i18n %} - {% block title %} eShop by Zee {% endblock %} + {% block title %}{% trans 'eShop by Zee' %}{% endblock %}
{% with total_items=cart|length%} {% if total_items > 0 %} - Your Cart: + {% trans 'Your Cart' %}: - {{total_items}} item{{total_items|pluralize}} - ${{cart.get_total_price}} + {% blocktrans with total=cart.get_total_price count items=total_items %} + {{items}} item, ${{total}} + {% plural %} + {{items}} items, ${{total}} + {% endblocktrans %} {% else %} - Your cart is empty. + {% trans "Your cart is empty." %} {% endif %} {% endwith %}
diff --git a/shop/templates/shop/product/details.html b/shop/templates/shop/product/details.html index b089981..199270a 100644 --- a/shop/templates/shop/product/details.html +++ b/shop/templates/shop/product/details.html @@ -1,5 +1,6 @@ {% extends 'shop/base.html' %} {% load static %} +{% load i18n %} {% block title %}{{product.name}}{% endblock%} @@ -15,7 +16,7 @@

{{ cart_add_form }} {% csrf_token %} - + {{product.description|linebreaks}}