Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#19] Added add to cart functionality with Django logic #20

Merged
merged 18 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions json_data/producten.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"model": "base.product", "pk": 1, "fields": {"name": "SAMSUNG RB34C670DSA/EF", "price": "599.00", "description": "Met de Samsung RB34C670DSA/EF koel-vriescombinatie in de stijlvolle kleur Metal Graphite heb jij ruimte voor al jouw levensmiddelen. Dit vrijstaande model heeft een groot koelgedeelte met een inhoud van 230 liter, verdeeld over 4 koelschappen, 1 koellade voor versproducten en 3 deurvakken voor bijvoorbeeld drinkflessen. Het vriesgedeelte bevindt zich onder het koelgedeelte, bestaat uit 3 ruime vriesvakken en heeft een capaciteit van 114 liter. Het apparaat wordt aangedreven door een krachtige inverter-motor, die niet alleen minder lawaai maakt, maar ook minder energie verbruikt. Dankzij de snelkoel- en snelvriesfuncties worden nieuwe boodschappen bovendien razendsnel gekoeld of ingevroren. Hierdoor blijven voedingsstoffen en smaak beter behouden en hoeft de Samsung minder hard te werken. Ontdooien hoeft niet meer, want met NoFrost doet deze koel-vriescombinatie dat helemaal zelf. Van ijsvorming heb jij dus geen last. Bovendien is deze koel-vriescombinatie via wifi te verbinden met de Smartthings-app. Hierdoor profiteer je van diverse extra functies, zoals de AI Energy Mode waarmee je nauwkeurig het energieverbruik in de gaten houdt. Deze modus analyseert jouw gebruikspatroon, maakt een schatting van je verbruik en helpt je om energie te besparen.", "image": "product_img/SAMSUNG_RB34C670DSAEF.webp", "new": true}}, {"model": "base.product", "pk": 2, "fields": {"name": "LG GBB61DSHMN DoorCooling+", "price": "699.00", "description": "De LG GBB61DSHMN is een vrijstaande koelkast met een vriesvak. Op de deur van het koelgedeelte zit een handig display waarmee je eenvoudig de instellingen beheert, zoals de temperatuur van de koelkast en de vriezer. Ben je niet thuis? Dan pas je de instellingen eenvoudig aan via de ThinQ-app van LG. Zo zorg je ervoor dat de koel-vriescombinatie altijd goed voor jouw boodschappen zorgt. Dankzij de strakke behuizing in mat zilveren kleur ziet deze koel-vriescombinatie er ook stijlvol uit in je keuken.", "image": "product_img/LG_GBB61DSHMN_DoorCooling.webp", "new": true}}, {"model": "base.product", "pk": 3, "fields": {"name": "Combi Koelkast - 293 liter - Inox - Exquisit KGC2", "price": "408.00", "description": "Deze koel- vriescombinatie is 186 cm hoog en biedt maar liefst 293 liter netto inhoud, genoeg voor het hele gezin. de KGC295-80-NF-040EI is luxe afgewerkt. Naast z'n inox-kleurige buitenzijde met geïntegreerde handgreep is de binnenzijde afgewerkt met zilverkleurige accenten.\r\n\r\nHet koelgedeelte van de KGC295-80-NF-040EI heeft een netto inhoud van 210 liter en is voorzien van een viertal hardglazen legplanken. van de 4 legplateaus zijn er 3 verstelbaar. het vriesgedeelte heeft een inhoud van maar liefst 83 liter en is voorzien van 3 handige, doorzichtige lades. Hierin kun je al je in te vriezen levensmiddelen makkelijk ordenen. het vriesgedeelte werkt met een temperatuur van -18°C/3*-sterren aanduiding.", "image": "product_img/combikoelkast.webp", "new": true}}, {"model": "base.product", "pk": 4, "fields": {"name": "Tomado TCR1420S – Koel-vriescombinatie - 173 liter", "price": "299.00", "description": "De Tomado TCR1420S is een koel-vriescombinatie en heeft een totale netto inhoud van 173 liter. Dit is voldoende koel- en vriesruimte voor huishoudens van 3 tot 4 personen. Het koelgedeelte van 121 liter zorgt ervoor dat jij al jouw boodschappen er keurig in kwijt kan. Het koelgedeelte is voorzien van 3 glazen draagplateaus, 1 groentelade en 1 flessenvak in de deur. Zodra je de deur van het koelgedeelte opent, springt de LED binnenverlichting aan zodat je goed zicht hebt. Het 4 sterren vriesgedeelte met een netto inhoud van 52 liter, heeft een invriesvermogen van 2,6 kg/24u. Het vriesgedeelte is voorzien van 2 transparante vrieslades. Dankzij het lage geluidsniveau van 40 dB hoor je nauwelijks dat hij aanstaat!", "image": "product_img/Tomado_TCR1420S__Koel-vriescombinatie_-_173_liter_-_Energieklasse_E_-_RVS_look.jpg", "new": true}}]
31 changes: 31 additions & 0 deletions src/bobvance/base/migrations/0003_auto_20231010_1502.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.21 on 2023-10-10 13:02

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('base', '0002_order_orderproduct_product'),
]

operations = [
migrations.AlterModelOptions(
name='orderproduct',
options={},
),
migrations.AddField(
model_name='order',
name='order_product',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.orderproduct'),
),
migrations.AlterUniqueTogether(
name='orderproduct',
unique_together=set(),
),
migrations.RemoveField(
model_name='orderproduct',
name='order',
),
]
36 changes: 36 additions & 0 deletions src/bobvance/base/migrations/0004_auto_20231010_1508.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 3.2.21 on 2023-10-10 13:08

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('base', '0003_auto_20231010_1502'),
]

operations = [
migrations.AlterModelOptions(
name='orderproduct',
options={'ordering': ['order']},
),
migrations.RemoveField(
model_name='order',
name='order_product',
),
migrations.AddField(
model_name='orderproduct',
name='order',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order_products', to='base.order'),
),
migrations.AlterField(
model_name='orderproduct',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_orders', to='base.product'),
),
migrations.AlterUniqueTogether(
name='orderproduct',
unique_together={('order', 'product')},
),
]
12 changes: 9 additions & 3 deletions src/bobvance/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,25 @@ class Order(models.Model):
def __str__(self):
return f"{self.customer} - {self.id}"

def total_price(self):
return sum(item.total_price() for item in self.order_products.all())


class OrderProduct(models.Model):
order = models.ForeignKey(
Order, related_name="order_products", on_delete=models.CASCADE
Order, related_name="order_products", on_delete=models.CASCADE, null=True, blank=True
)
product = models.ForeignKey(
Product, related_name="order_products", on_delete=models.CASCADE
Product, related_name="product_orders", on_delete=models.CASCADE
)
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)])

def __str__(self):
return f"{self.product.name} - {self.quantity}x"

def total_price(self):
return self.product.price * self.quantity

class Meta:
unique_together = ["order", "product"]
unique_together = ("order", "product")
ordering = ["order"]
6 changes: 3 additions & 3 deletions src/bobvance/base/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.urls import path, include
from bobvance.base.views import Home, ProductsView, ProductDetailView, CartView, AddToCartView, RemoveFromCartView, UpdateCartView
from bobvance.base.views import Home, ProductsView, ProductDetailView, AddToCartView, CartView, UpdateCartView, RemoveFromCartView

urlpatterns = [
path('', Home.as_view(), name='home'),
path('products/', ProductsView.as_view(), name='products'),
path('product/<int:pk>/', ProductDetailView.as_view(), name='product_detail'),
path('add-to-cart/', AddToCartView.as_view(), name='add_to_cart'),
path('cart/', CartView.as_view(), name='cart'),
path('add_to_cart/', AddToCartView.as_view(), name='add_to_cart'),
path('remove-from-cart/', RemoveFromCartView.as_view(), name='remove-from-cart'),
path('update-cart/', UpdateCartView.as_view(), name='update_cart'),
path('remove-from-cart/', RemoveFromCartView.as_view(), name='remove_from_cart'),
]
93 changes: 49 additions & 44 deletions src/bobvance/base/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from bobvance.base.models import Product
from django.views.generic import ListView, DetailView, TemplateView, View
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render, redirect
from django.http import JsonResponse

from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json
from django.contrib import messages

class Home(TemplateView):
template_name = 'base/index.html'
Expand All @@ -28,68 +29,72 @@ def get_context_data(self, **kwargs):
context['more_products'] = Product.objects.exclude(pk=self.object.pk).order_by('?')[:5]
return context

class CartView(View):
def get(self, request, *args, **kwargs):
cart = request.session.get('cart', {})

product_ids = [pid for pid in cart.keys() if pid is not None and pid != 'null' and pid.isdigit()]

cart_items = Product.objects.filter(id__in=product_ids)

total_price = sum([product.price * cart[str(product.id)] for product in cart_items])

context = {
'cart_items': [
{'product': product, 'quantity': cart[str(product.id)]}
for product in cart_items
],
'total_price': total_price,
}

return render(request, 'base/cart.html', context)


class AddToCartView(View):
def post(self, request, *args, **kwargs):
product_id = request.POST.get('product_id')
product = get_object_or_404(Product, id=product_id)
cart = request.session.get('cart', {})
quantity = int(request.POST.get('quantity', 1))

cart = request.session.get('cart', {})
if product_id in cart:
cart[product_id] += 1
cart[product_id] += quantity
else:
cart[product_id] = 1
cart[product_id] = quantity

request.session['cart'] = cart

return JsonResponse({'status': 'success'})
return redirect('cart')

class CartView(TemplateView):
template_name = 'base/cart.html'
class UpdateCartView(View):
def post(self, request, *args, **kwargs):
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity'))

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
cart = self.request.session.get('cart', {})
products_in_cart = Product.objects.filter(id__in=cart.keys())
total_price = sum([product.price * int(cart[str(product.id)]) for product in products_in_cart])

context['cart_items'] = [
{'product': product, 'quantity': cart[str(product.id)]}
for product in products_in_cart
]
context['total_price'] = total_price
return context
cart = request.session.get('cart', {})
if product_id in cart and quantity > 0:
cart[product_id] = quantity
elif product_id in cart and quantity <= 0:
del cart[product_id]

request.session['cart'] = cart

return redirect('cart')

@method_decorator(csrf_exempt, name='dispatch')
class RemoveFromCartView(View):
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
product_id = str(data.get('product_id'))
product_id = request.POST.get('product_id')
cart = request.session.get('cart', {})

if product_id in cart:
del cart[product_id]
request.session['cart'] = cart
return JsonResponse({'status': 'success'})
status = 'success'
message = 'Product successfully removed from cart.'
else:
return JsonResponse({'status': 'error'}, status=400)

class UpdateCartView(View):
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)

def post(self, request, *args, **kwargs):
data = json.loads(request.body)
product_id = data.get('product_id')
quantity = data.get('quantity')
product = get_object_or_404(Product, id=product_id)

cart = request.session.get('cart', {})
cart[product_id] = quantity

request.session['cart'] = cart
status = 'failed'
message = 'Product not found in cart.'

products_in_cart = Product.objects.filter(id__in=cart.keys())
total_price = sum([product.price * int(cart[str(product.id)]) for product in products_in_cart])
if request.is_ajax():
return JsonResponse({'status': status, 'message': message})

return JsonResponse({'status': 'success', 'total_price': str(total_price)})
return redirect('cart')
77 changes: 15 additions & 62 deletions src/bobvance/templates/base/cart.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ <h2 class="text-lg font-bold text-gray-900">{{ item.product.name }}</h2>
</div>
<div class="mt-4 flex justify-between sm:space-y-6 sm:mt-0 sm:block sm:space-x-6 ml-10">
<div class="flex items-center border-gray-100">
<span class="cursor-pointer rounded-l bg-gray-100 py-1 px-3.5 duration-100 hover:bg-blue-600 hover:text-blue-50 quantity-button" data-action="decrease"> - </span>
<input class="h-8 w-8 border bg-white text-center text-xs outline-none quantity-input" type="number" value="{{ item.quantity }}" min="1" data-product-id="{{ item.product.id }}" />
<span class="cursor-pointer rounded-r bg-gray-100 py-1 px-3 duration-100 hover:bg-blue-600 hover:text-blue-50 quantity-button" data-action="increase"> + </span>
<form method="post" action="{% url 'update_cart' %}" class="quantity-form" style="display: flex;">
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ item.product.id }}">
<span class="cursor-pointer rounded-l bg-gray-100 py-1 px-3.5 duration-100 hover:bg-blue-600 hover:text-blue-50 quantity-button" data-action="decrease"> - </span>
<input class="h-8 w-8 border bg-white text-center text-xs outline-none quantity-input" type="number" name="quantity" value="{{ item.quantity }}" min="1" data-product-id="{{ item.product.id }}" />
<span class="cursor-pointer rounded-r bg-gray-100 py-1 px-3 duration-100 hover:bg-blue-600 hover:text-blue-50 quantity-button" data-action="increase"> + </span>
</form>
</div>
<div class="flex items-center space-x-4">
<p class="text-sm">€{{ item.product.price }}</p>
</div>
</div>
<div class="mt-52">
<button type="button" data-product-id="{{ item.product.id }}" class="remove-item focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Verwijderen</button>
<form method="post" action="{% url 'remove_from_cart' %}" class="remove-form">
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ item.product.id }}">
<button type="submit" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Verwijderen</button>
</form>
</div>
</div>
</div>
Expand Down Expand Up @@ -72,38 +80,13 @@ <h2 class="text-lg font-bold text-gray-900">{{ item.product.name }}</h2>
{% endif %}
</div>
</body>

{% include 'components/footer.html' %}

<script>
document.querySelectorAll('.remove-item').forEach(button => {
button.addEventListener('click', function(e) {
const productId = this.dataset.productId;

fetch('/remove-from-cart/', {
method: 'POST',
body: JSON.stringify({
product_id: productId
}),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
if(data.status === 'success') {
location.reload();
} else {
alert('Er ging iets mis bij het verwijderen van het product.');
}
})
});
});
document.querySelectorAll('.quantity-button').forEach(button => {
button.addEventListener('click', function(e) {
const action = e.target.dataset.action;
const input = e.target.parentElement.querySelector('.quantity-input');
const form = e.target.closest('.quantity-form');
const input = form.querySelector('.quantity-input');
let quantity = parseInt(input.value);

if(action === 'increase') {
Expand All @@ -113,37 +96,7 @@ <h2 class="text-lg font-bold text-gray-900">{{ item.product.name }}</h2>
}

input.value = quantity;

input.dispatchEvent(new Event('change'));
});
});

document.querySelectorAll('.quantity-input').forEach(input => {
input.addEventListener('change', function(e) {
const productId = e.target.dataset.productId;
const quantity = e.target.value;

fetch('/update-cart/', {
method: 'POST',
body: JSON.stringify({
product_id: productId,
quantity: quantity,
}),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
}
})
.then(response => response.json())
.then(data => {
if(data.status === 'success') {
document.querySelectorAll('.total-price').forEach(price => {
price.textContent = '€' + data.total_price;
});
} else {
alert('Er ging iets mis bij het updaten van de winkelwagen.');
}
})
form.submit();
});
});
</script>
Expand Down
Loading
Loading