diff --git a/secondExercise/helper_functions.py b/secondExercise/helper_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..d196cf6e4aa7b3f0a24e7381ebd13e6343645147 --- /dev/null +++ b/secondExercise/helper_functions.py @@ -0,0 +1,20 @@ +from django.core.mail import send_mail +from django.template.loader import render_to_string + + +class OutOfStockError(Exception): + pass + + +def send_confirmation_email(email, context): + msg_plain = render_to_string('email/confirmation.txt', context) + msg_html = render_to_string('email/confirmation.html', context) + + send_mail( + "Receipt on Order id " + context['cart'].pk, + msg_plain, + 'donotreply@group7webshop.com', + [email], + fail_silently=True, + html_message=msg_html, + ) \ No newline at end of file diff --git a/secondExercise/settings.py b/secondExercise/settings.py index 680501713a7b449896a3fa929f48c6bfdc1c44e5..0f93a1dd87e4f1042bb7247d88a539f817f8d86b 100644 --- a/secondExercise/settings.py +++ b/secondExercise/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ """ import os + import dj_database_url # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -22,7 +23,7 @@ TEMPLATES_PATH = os.path.join(PROJECT_PATH, "templates") # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('SECRET_KEY') +SECRET_KEY = os.environ.get('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,19 +33,20 @@ LOGIN_REDIRECT_URL = 'index' # Application definition INSTALLED_APPS = [ - 'webshop', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', + 'django.contrib.staticfiles', # Disable Django's own staticfiles handling in favour of WhiteNoise, for # greater consistency between gunicorn and `./manage.py runserver`. See: # http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development 'whitenoise.runserver_nostatic', - 'django.contrib.staticfiles', 'django_filters', 'mathfilters', + + 'webshop', ] MIDDLEWARE = [ @@ -137,3 +139,11 @@ STATICFILES_DIRS = [ # Simplified static file serving. # https://warehouse.python.org/project/whitenoise/ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +EMAIL_USE_TLS = True +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = os.environ.get('EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 diff --git a/webshop/admin.py b/webshop/admin.py index 594414fd378246eb7b3dd8aa64786c71ea3ed6ac..62c5c3a9087d42929c500916d929bf7dddfe5005 100644 --- a/webshop/admin.py +++ b/webshop/admin.py @@ -2,9 +2,13 @@ from django.contrib import admin from .models import * + +class ItemAdmin(admin.ModelAdmin): + list_display = ('name', 'brand', 'stock', 'price') + # Register your models here. admin.site.register(Brand) admin.site.register(Tag) -admin.site.register(Item) +admin.site.register(Item, ItemAdmin) admin.site.register(DiscountPercentage) admin.site.register(DiscountPackage) diff --git a/webshop/migrations/0005_item_stock.py b/webshop/migrations/0005_item_stock.py new file mode 100644 index 0000000000000000000000000000000000000000..e88b1ef70fe1a7c2ec0687bb2ededcbf1f5f0ac9 --- /dev/null +++ b/webshop/migrations/0005_item_stock.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-03-28 15:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webshop', '0004_auto_20180304_2228'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='stock', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/webshop/models.py b/webshop/models.py index 60abfef980526b4dc6b98605157ab384434416bb..6705283ed4d821b8276b081cd9b72692e6456df6 100644 --- a/webshop/models.py +++ b/webshop/models.py @@ -1,7 +1,10 @@ import math + +from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db import models -from django.contrib.auth.models import User + +from secondExercise.helper_functions import OutOfStockError # Create your models here. @@ -32,6 +35,7 @@ class Item(models.Model): price = models.DecimalField(max_digits=10, decimal_places=2) brand = models.ForeignKey("Brand", on_delete=models.CASCADE, default=1) tag = models.ManyToManyField("Tag") + stock = models.PositiveIntegerField(default=0) def __str__(self): return self.name @@ -93,8 +97,7 @@ class Cart(models.Model): def add_item(self, item): try: order = self.items.get(item=item) - order.quantity += 1 - order.save() + order.add_quantity(1) except ObjectDoesNotExist: self.items.add(Order.objects.create(item=item)) self.save() @@ -103,11 +106,10 @@ class Cart(models.Model): try: order = self.items.get(item=item) if amount >= 1: - order.quantity = amount - order.save() + order.update_quantity(amount) elif amount <= 0: self.delete_item(item) - self.save() + self.save() except ObjectDoesNotExist: self.items.add(Order.objects.create(item=item)) self.save() @@ -130,6 +132,11 @@ class Cart(models.Model): except ObjectDoesNotExist: pass + def update_stock(self): + for order in self.items.all(): + order.item.stock -= order.quantity + order.item.save() + def get_total(self): total = 0 for order in self.items.all()[:]: @@ -141,6 +148,21 @@ class Order(models.Model): item = models.ForeignKey('Item', on_delete=models.CASCADE) quantity = models.IntegerField(default=1) + # Increases quantity by amount if in stock. + def add_quantity(self, amount): + if self.item.stock < (self.quantity + amount): + raise OutOfStockError() + self.quantity += amount + self.save() + + # Sets quantity by amount if in stock, or deletes order. + def update_quantity(self, quantity): + if self.item.stock < quantity: + raise OutOfStockError() + self.quantity = quantity + self.save() + + # Calculates the order total def get_order_total(self): try: @@ -156,13 +178,6 @@ class Order(models.Model): discount_total = self.item.price * int(self.quantity / discount.buyx) * discount.fory rest_total = self.item.price * (self.quantity % discount.buyx) return discount_total + rest_total - temp_quantity = self.quantity - total = 0 - while temp_quantity - discount.buyx >= 0: - total += self.item.price * discount.fory - temp_quantity -= discount.buyx - total += self.item.price * temp_quantity - return round(total, 2) except ObjectDoesNotExist: # No discount return round(self.item.price * self.quantity, 2) diff --git a/webshop/static/webshop/css/cart.css b/webshop/static/webshop/css/cart.css index 7e1a5c56c5aa7bd4ffd43e1e4b9f8c1e203a32f5..d754f6678b7a8aaad7b5d823f543e344c4c8418f 100644 --- a/webshop/static/webshop/css/cart.css +++ b/webshop/static/webshop/css/cart.css @@ -16,7 +16,7 @@ .cart_item { display: grid; - grid-template-columns: 3fr 1fr 1fr 1fr 1fr; + grid-template-columns: 3fr 1fr 1fr 2fr 1fr 1fr; justify-items: center; margin: 1rem; } @@ -27,7 +27,7 @@ } .horizontal_line { - height: 0px; + height: 0; border-bottom: 1px solid black; } @@ -53,3 +53,11 @@ .strikethrough { text-decoration: line-through; } + +.is-invalid{ + border-color: rgba(175, 17, 14, 0.87) !important; +} + +.invalid-help-text { + color: rgba(175, 17, 14, 0.87) !important;; +} \ No newline at end of file diff --git a/webshop/templates/base.html b/webshop/templates/base.html index 79a6905e389fec9e29bfc97a2677be5af1b454f8..f0b025b92ea8677264f82f16df1329586a1f5f46 100644 --- a/webshop/templates/base.html +++ b/webshop/templates/base.html @@ -2,11 +2,16 @@ {% load staticfiles %} <html> <head> - <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous"> - <link rel="stylesheet" href="{% static 'main.css' %}"> - {% block css %}{% endblock %} - {% block js %}{% endblock %} - <title>Web shop</title> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous"> + <link rel="stylesheet" href="{% static 'main.css' %}"> + {% block css %}{% endblock %} + <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> + <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.bundle.min.js" integrity="sha384-feJI7QwhOS+hwpX2zkaeJQjeiwlhOP+SdQDqhgvvo1DsjtiSQByFdThsxO669S2D" crossorigin="anonymous"></script> + {% block js %}{% endblock %} + <title>Web shop</title> + + </head> <body> @@ -46,6 +51,22 @@ </div> </nav> {% block content %}{% endblock content %} + {% if messages %} + <div class="message-list"> + {% for message in messages %} + <div class="alert alert-danger alert-dismissible fade show" role="alert"> + {{ message.message }} + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + {% endfor %} + </div> + + {% endif %} </div> + <script> + $('.alert').alert('dispose'); + </script> </body> </html> diff --git a/webshop/templates/email/confirmation.html b/webshop/templates/email/confirmation.html new file mode 100644 index 0000000000000000000000000000000000000000..cc2d8918120c2f7025af7436b9fb0ef9215ddbcc --- /dev/null +++ b/webshop/templates/email/confirmation.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Confirmation</title> +</head> +<body> + <h3>Hello, dear {{ user.username }}</h3> + + <h4>Your order has been completed and it consist of:</h4> + + <table> + <tr> + <td>Item</td> + <td>Amount</td> + </tr> + {% for item in cart.items.all %} + <tr> + <td> + {{ item.item.name }} + </td> + <td> + {{ item.quantity }} + </td> + </tr> + {% endfor %} + </table> + <p> For a total of {{ total }}NOK</p> + + <p>------</p> + <strong>Best regards, webshopguys!</strong> +</body> +</html> \ No newline at end of file diff --git a/webshop/templates/email/confirmation.txt b/webshop/templates/email/confirmation.txt new file mode 100644 index 0000000000000000000000000000000000000000..94f1d0bf76afe14a533e078097b2fb8313d13fbf --- /dev/null +++ b/webshop/templates/email/confirmation.txt @@ -0,0 +1,17 @@ +Hello {{user.username}}! + +Your order at Webshop has been completed + +Your order consist of: + +{% for item in cart.items.all %} + + {{ item.item.name }} - {{ item.quantity }} + +{% endfor %} + +For a total of {{ total }}NOK + +Best regards, Webshop! + + diff --git a/webshop/templates/webshop/cart.html b/webshop/templates/webshop/cart.html index bc9fdd11c2d6099e7b2190356696799072b81793..5052132cbdb33d15f012b2a69ac572fb2a39a853 100644 --- a/webshop/templates/webshop/cart.html +++ b/webshop/templates/webshop/cart.html @@ -11,7 +11,7 @@ <script type="text/javascript"> // Allows the user to press the enter key when changing the quantity function handle_input(event, item_pk, quantity) { - if (event.key = "Enter") { + if (event.key === "Enter") { updateQuantity(item_pk, quantity); } } @@ -32,6 +32,24 @@ function updateQuantity(item_pk, quantity) { let location = "{% url 'update_item_in_cart_base' %}" + item_pk + "/" + quantity + "/"; window.location.href = location; } + +function buyCart () { + let email = document.getElementById('InputEmail1'); + if (validateEmail(email.value)) { + window.location.href = "{% url 'buy_cart_base' %}" + email.value + '/'; + } + else { + email.classList.toggle('is-invalid'); + document.getElementById('emailHelp').innerHTML = 'Please provide a valid email address'; + document.getElementById('emailHelp').classList.add('invalid-help-text'); + } +} + +function validateEmail(email) { + const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); +} + </script> {% endblock %} @@ -40,6 +58,7 @@ function updateQuantity(item_pk, quantity) { <div class="cart_item"> <span>Shopping cart</span> <span>Quantity</span> + <span>Stock</span> <span>Price</span> <span>Total</span> <span>Delete</span> @@ -58,9 +77,10 @@ function updateQuantity(item_pk, quantity) { </span> <div class="item_quantity_selector"> <button class="btn_quantity_selector" onclick="subtractQuantity({{order.item.pk}})">-</button> - <input class="input_quantity_selector" type="number" maxlength="3" min="1" name="{{order.item.pk}}-quantity" value={{order.quantity}} onsubmit="updateQuantity({{order.item.pk}}, this.value)" onkeydown="handle_input(event, {{order.item.pk}}, this.value)"> + <input class="input_quantity_selector" type="number" maxlength="3" min="1" name="{{order.item.pk}}-quantity" value={{order.quantity}} onkeydown="handle_input(event, {{order.item.pk}}, this.value)"> <button class="btn_quantity_selector" onclick="addQuantity({{order.item.pk}})">+</button> </div> + <span>{{order.item.stock}}</span> {% if order.item.get_item_percentage_price %} <span><span class="strikethrough">{{order.item.price}}</span> {{order.item.get_item_percentage_price}} NOK</span> {% else %} @@ -71,7 +91,41 @@ function updateQuantity(item_pk, quantity) { </div> <div class="horizontal_line"></div> {% endfor %} - <h4>Subtotal ({{ item_amount }} items): {{ total_price }} NOK <a href="{% url 'buy_cart' %}">[Buy Cart]</a></h4> + <h4>Subtotal ({{ item_amount }} items): {{ total_price }} NOK </h4> + <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal"> + Buy cart + </button> {% endif %} + </div> + +<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">Modal title</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <form> + <div class="form-group"> + + <label for="InputEmail1">Please specify email adress to send reciept</label> + <input type="email" class="form-control" id="InputEmail1" aria-describedby="emailHelp" + placeholder="Enter email" required data-error-msg="The email is required in valid format!"> + <small id="emailHelp" class="form-text">We'll never share your email with anyone else.</small> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Abort</button> + <button type="submit" class="btn btn-primary" onclick="buyCart()">Send reciept</button> + </div> + </div> + </div> +</div> + + {% endblock %} diff --git a/webshop/urls.py b/webshop/urls.py index 36a3e9155c6e722c32895ef32493b0f4f849c0b1..384d6ea14beb3a704c5363a7d3544f4241f2d8de 100644 --- a/webshop/urls.py +++ b/webshop/urls.py @@ -1,6 +1,6 @@ -from django.urls import path from django.conf.urls import url from django.contrib.auth import views as auth_views +from django.urls import path from . import views @@ -13,7 +13,8 @@ urlpatterns = [ url(r'^logout/$', auth_views.logout, {'template_name': 'logged_out.html'}, name='logout'), path('<int:item_id>/', views.detail, name='detail'), path('cart/', views.cart, name='cart'), - path('cart/buy/', views.buy_cart, name='buy_cart'), + path('cart/buy/', views.buy_cart, name='buy_cart_base'), + path('cart/buy/<str:email>/', views.buy_cart, name='buy_cart'), path('add/<int:item_pk>/', views.add_item_to_cart, name='add_item_to_cart'), path('update/', views.update_item_in_cart, name='update_item_in_cart_base'), path('update/<int:item_pk>/<int:amount>/', views.update_item_in_cart, name='update_item_in_cart'), diff --git a/webshop/views.py b/webshop/views.py index 47e7fbc55677d4c3c0bd1c6f9d5bc6483397ea50..22ddc544fdcea6bdce2f7b6c470b0d1367e6d127 100644 --- a/webshop/views.py +++ b/webshop/views.py @@ -1,21 +1,20 @@ +import django_filters +from django import forms +from django.contrib import messages from django.contrib.auth import login, authenticate +from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.views import login -from django.shortcuts import render, redirect, get_list_or_404, get_object_or_404 -from django.template import Template, Context, loader -from django.contrib.auth.decorators import login_required -from django import forms -import django_filters -from .models import Item, Brand, Tag, Cart -from django.http import Http404 +from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import render -from django.http import HttpResponse +from secondExercise.helper_functions import OutOfStockError, send_confirmation_email +from .models import Item, Brand, Tag, Cart @login_required def index(request): - item_list = ItemsFilter(request.GET, queryset=Item.objects.all()[:5]) + item_list = ItemsFilter(request.GET, queryset=Item.objects.filter(stock__gt=0)[:5]) context = { 'item_list': item_list, } @@ -82,14 +81,20 @@ def cart(request): def add_item_to_cart(request, item_pk): item = get_object_or_404(Item, pk=item_pk) cart, created = Cart.objects.get_or_create(owner=request.user, active=True) - cart.add_item(item) + try: + cart.add_item(item) + except OutOfStockError: + messages.add_message(request, messages.ERROR, 'The stock of this item can\'t support your desired order') return redirect('cart')\ @login_required() def update_item_in_cart(request, item_pk, amount): item = get_object_or_404(Item, pk=item_pk) cart, created = Cart.objects.get_or_create(owner=request.user, active=True) - cart.update_item(item, amount) + try: + cart.update_item(item, amount) + except OutOfStockError: + messages.add_message(request, messages.ERROR, 'The stock of this item can\'t support your desired order') return redirect('cart')\ @login_required() @@ -109,14 +114,17 @@ def delete_item_from_cart(request, item_pk): @login_required -def buy_cart(request): +def buy_cart(request, email): cart = get_object_or_404(Cart, owner=request.user, active=True) if(cart.get_total() <= 0): return redirect('cart') + cart.update_stock() context = { 'total': cart.get_total(), - 'cart': cart + 'cart': cart, + 'user': request.user } + send_confirmation_email(email, context) cart.active = False cart.save() return render(request, 'webshop/reciept.html', context)