Skip to content
Snippets Groups Projects
Commit ea867aee authored by Hugo Carrasco's avatar Hugo Carrasco
Browse files

Merge branch 'feature/mathiafl/item-stock-intergration' into 'master'

Feature/mathiafl/item stock intergration

See merge request tdt4242_7/secondExercise!16
parents 4c4744a5 25995b61
No related branches found
No related tags found
No related merge requests found
Pipeline #
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
...@@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ ...@@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
""" """
import os import os
import dj_database_url import dj_database_url
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
...@@ -32,19 +33,20 @@ LOGIN_REDIRECT_URL = 'index' ...@@ -32,19 +33,20 @@ LOGIN_REDIRECT_URL = 'index'
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'webshop',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles',
# Disable Django's own staticfiles handling in favour of WhiteNoise, for # Disable Django's own staticfiles handling in favour of WhiteNoise, for
# greater consistency between gunicorn and `./manage.py runserver`. See: # greater consistency between gunicorn and `./manage.py runserver`. See:
# http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development # http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development
'whitenoise.runserver_nostatic', 'whitenoise.runserver_nostatic',
'django.contrib.staticfiles',
'django_filters', 'django_filters',
'mathfilters', 'mathfilters',
'webshop',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
...@@ -137,3 +139,11 @@ STATICFILES_DIRS = [ ...@@ -137,3 +139,11 @@ STATICFILES_DIRS = [
# Simplified static file serving. # Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/ # https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 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
...@@ -2,9 +2,13 @@ from django.contrib import admin ...@@ -2,9 +2,13 @@ from django.contrib import admin
from .models import * from .models import *
class ItemAdmin(admin.ModelAdmin):
list_display = ('name', 'brand', 'stock', 'price')
# Register your models here. # Register your models here.
admin.site.register(Brand) admin.site.register(Brand)
admin.site.register(Tag) admin.site.register(Tag)
admin.site.register(Item) admin.site.register(Item, ItemAdmin)
admin.site.register(DiscountPercentage) admin.site.register(DiscountPercentage)
admin.site.register(DiscountPackage) admin.site.register(DiscountPackage)
# 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),
),
]
import math import math
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.contrib.auth.models import User
from secondExercise.helper_functions import OutOfStockError
# Create your models here. # Create your models here.
...@@ -32,6 +35,7 @@ class Item(models.Model): ...@@ -32,6 +35,7 @@ class Item(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2) price = models.DecimalField(max_digits=10, decimal_places=2)
brand = models.ForeignKey("Brand", on_delete=models.CASCADE, default=1) brand = models.ForeignKey("Brand", on_delete=models.CASCADE, default=1)
tag = models.ManyToManyField("Tag") tag = models.ManyToManyField("Tag")
stock = models.PositiveIntegerField(default=0)
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -93,8 +97,7 @@ class Cart(models.Model): ...@@ -93,8 +97,7 @@ class Cart(models.Model):
def add_item(self, item): def add_item(self, item):
try: try:
order = self.items.get(item=item) order = self.items.get(item=item)
order.quantity += 1 order.add_quantity(1)
order.save()
except ObjectDoesNotExist: except ObjectDoesNotExist:
self.items.add(Order.objects.create(item=item)) self.items.add(Order.objects.create(item=item))
self.save() self.save()
...@@ -103,8 +106,7 @@ class Cart(models.Model): ...@@ -103,8 +106,7 @@ class Cart(models.Model):
try: try:
order = self.items.get(item=item) order = self.items.get(item=item)
if amount >= 1: if amount >= 1:
order.quantity = amount order.update_quantity(amount)
order.save()
elif amount <= 0: elif amount <= 0:
self.delete_item(item) self.delete_item(item)
self.save() self.save()
...@@ -130,6 +132,11 @@ class Cart(models.Model): ...@@ -130,6 +132,11 @@ class Cart(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
def update_stock(self):
for order in self.items.all():
order.item.stock -= order.quantity
order.item.save()
def get_total(self): def get_total(self):
total = 0 total = 0
for order in self.items.all()[:]: for order in self.items.all()[:]:
...@@ -141,6 +148,21 @@ class Order(models.Model): ...@@ -141,6 +148,21 @@ class Order(models.Model):
item = models.ForeignKey('Item', on_delete=models.CASCADE) item = models.ForeignKey('Item', on_delete=models.CASCADE)
quantity = models.IntegerField(default=1) 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 # Calculates the order total
def get_order_total(self): def get_order_total(self):
try: try:
...@@ -156,13 +178,6 @@ class Order(models.Model): ...@@ -156,13 +178,6 @@ class Order(models.Model):
discount_total = self.item.price * int(self.quantity / discount.buyx) * discount.fory discount_total = self.item.price * int(self.quantity / discount.buyx) * discount.fory
rest_total = self.item.price * (self.quantity % discount.buyx) rest_total = self.item.price * (self.quantity % discount.buyx)
return discount_total + rest_total 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: except ObjectDoesNotExist:
# No discount # No discount
return round(self.item.price * self.quantity, 2) return round(self.item.price * self.quantity, 2)
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.cart_item { .cart_item {
display: grid; display: grid;
grid-template-columns: 3fr 1fr 1fr 1fr 1fr; grid-template-columns: 3fr 1fr 1fr 2fr 1fr 1fr;
justify-items: center; justify-items: center;
margin: 1rem; margin: 1rem;
} }
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
} }
.horizontal_line { .horizontal_line {
height: 0px; height: 0;
border-bottom: 1px solid black; border-bottom: 1px solid black;
} }
...@@ -53,3 +53,11 @@ ...@@ -53,3 +53,11 @@
.strikethrough { .strikethrough {
text-decoration: line-through; 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
...@@ -5,8 +5,13 @@ ...@@ -5,8 +5,13 @@
<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="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' %}"> <link rel="stylesheet" href="{% static 'main.css' %}">
{% block css %}{% endblock %} {% 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 %} {% block js %}{% endblock %}
<title>Web shop</title> <title>Web shop</title>
</head> </head>
<body> <body>
...@@ -46,6 +51,22 @@ ...@@ -46,6 +51,22 @@
</div> </div>
</nav> </nav>
{% block content %}{% endblock content %} {% 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">&times;</span>
</button>
</div>
{% endfor %}
</div>
{% endif %}
</div> </div>
<script>
$('.alert').alert('dispose');
</script>
</body> </body>
</html> </html>
<!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
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!
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<script type="text/javascript"> <script type="text/javascript">
// Allows the user to press the enter key when changing the quantity // Allows the user to press the enter key when changing the quantity
function handle_input(event, item_pk, quantity) { function handle_input(event, item_pk, quantity) {
if (event.key = "Enter") { if (event.key === "Enter") {
updateQuantity(item_pk, quantity); updateQuantity(item_pk, quantity);
} }
} }
...@@ -32,6 +32,24 @@ function updateQuantity(item_pk, quantity) { ...@@ -32,6 +32,24 @@ function updateQuantity(item_pk, quantity) {
let location = "{% url 'update_item_in_cart_base' %}" + item_pk + "/" + quantity + "/"; let location = "{% url 'update_item_in_cart_base' %}" + item_pk + "/" + quantity + "/";
window.location.href = location; 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> </script>
{% endblock %} {% endblock %}
...@@ -40,6 +58,7 @@ function updateQuantity(item_pk, quantity) { ...@@ -40,6 +58,7 @@ function updateQuantity(item_pk, quantity) {
<div class="cart_item"> <div class="cart_item">
<span>Shopping cart</span> <span>Shopping cart</span>
<span>Quantity</span> <span>Quantity</span>
<span>Stock</span>
<span>Price</span> <span>Price</span>
<span>Total</span> <span>Total</span>
<span>Delete</span> <span>Delete</span>
...@@ -58,9 +77,10 @@ function updateQuantity(item_pk, quantity) { ...@@ -58,9 +77,10 @@ function updateQuantity(item_pk, quantity) {
</span> </span>
<div class="item_quantity_selector"> <div class="item_quantity_selector">
<button class="btn_quantity_selector" onclick="subtractQuantity({{order.item.pk}})">-</button> <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> <button class="btn_quantity_selector" onclick="addQuantity({{order.item.pk}})">+</button>
</div> </div>
<span>{{order.item.stock}}</span>
{% if order.item.get_item_percentage_price %} {% if order.item.get_item_percentage_price %}
<span><span class="strikethrough">{{order.item.price}}</span> {{order.item.get_item_percentage_price}} NOK</span> <span><span class="strikethrough">{{order.item.price}}</span> {{order.item.get_item_percentage_price}} NOK</span>
{% else %} {% else %}
...@@ -71,7 +91,41 @@ function updateQuantity(item_pk, quantity) { ...@@ -71,7 +91,41 @@ function updateQuantity(item_pk, quantity) {
</div> </div>
<div class="horizontal_line"></div> <div class="horizontal_line"></div>
{% endfor %} {% 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 %} {% 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">&times;</span>
</button>
</div> </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 %} {% endblock %}
from django.urls import path
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import path
from . import views from . import views
...@@ -13,7 +13,8 @@ urlpatterns = [ ...@@ -13,7 +13,8 @@ urlpatterns = [
url(r'^logout/$', auth_views.logout, {'template_name': 'logged_out.html'}, name='logout'), url(r'^logout/$', auth_views.logout, {'template_name': 'logged_out.html'}, name='logout'),
path('<int:item_id>/', views.detail, name='detail'), path('<int:item_id>/', views.detail, name='detail'),
path('cart/', views.cart, name='cart'), 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('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/', 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'), path('update/<int:item_pk>/<int:amount>/', views.update_item_in_cart, name='update_item_in_cart'),
......
import django_filters
from django import forms
from django.contrib import messages
from django.contrib.auth import login, authenticate 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.forms import UserCreationForm
from django.contrib.auth.views import login from django.contrib.auth.views import login
from django.shortcuts import render, redirect, get_list_or_404, get_object_or_404 from django.shortcuts import redirect, 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 render 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 @login_required
def index(request): 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 = { context = {
'item_list': item_list, 'item_list': item_list,
} }
...@@ -82,14 +81,20 @@ def cart(request): ...@@ -82,14 +81,20 @@ def cart(request):
def add_item_to_cart(request, item_pk): def add_item_to_cart(request, item_pk):
item = get_object_or_404(Item, pk=item_pk) item = get_object_or_404(Item, pk=item_pk)
cart, created = Cart.objects.get_or_create(owner=request.user, active=True) cart, created = Cart.objects.get_or_create(owner=request.user, active=True)
try:
cart.add_item(item) 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')\ return redirect('cart')\
@login_required() @login_required()
def update_item_in_cart(request, item_pk, amount): def update_item_in_cart(request, item_pk, amount):
item = get_object_or_404(Item, pk=item_pk) item = get_object_or_404(Item, pk=item_pk)
cart, created = Cart.objects.get_or_create(owner=request.user, active=True) cart, created = Cart.objects.get_or_create(owner=request.user, active=True)
try:
cart.update_item(item, amount) 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')\ return redirect('cart')\
@login_required() @login_required()
...@@ -109,14 +114,17 @@ def delete_item_from_cart(request, item_pk): ...@@ -109,14 +114,17 @@ def delete_item_from_cart(request, item_pk):
@login_required @login_required
def buy_cart(request): def buy_cart(request, email):
cart = get_object_or_404(Cart, owner=request.user, active=True) cart = get_object_or_404(Cart, owner=request.user, active=True)
if(cart.get_total() <= 0): if(cart.get_total() <= 0):
return redirect('cart') return redirect('cart')
cart.update_stock()
context = { context = {
'total': cart.get_total(), 'total': cart.get_total(),
'cart': cart 'cart': cart,
'user': request.user
} }
send_confirmation_email(email, context)
cart.active = False cart.active = False
cart.save() cart.save()
return render(request, 'webshop/reciept.html', context) return render(request, 'webshop/reciept.html', context)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment