parent
8d8fa1a7d3
commit
7f31708d86
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 162 KiB |
@ -0,0 +1,14 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
from .models import Product, Color, Size |
||||||
|
|
||||||
|
@admin.register(Product) |
||||||
|
class ProductAdmin(admin.ModelAdmin): |
||||||
|
list_display = ("title", "price") |
||||||
|
|
||||||
|
@admin.register(Color) |
||||||
|
class ColorAdmin(admin.ModelAdmin): |
||||||
|
list_display = ("name",) |
||||||
|
|
||||||
|
@admin.register(Size) |
||||||
|
class SizeAdmin(admin.ModelAdmin): |
||||||
|
list_display = ("name",) |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class ShopConfig(AppConfig): |
||||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||||
|
name = 'shop' |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
from .models import CartItem, Product |
||||||
|
|
||||||
|
def get_or_create_cart_id(request): |
||||||
|
"""Get the cart ID from the session or create a new one""" |
||||||
|
if 'cart_id' not in request.session: |
||||||
|
request.session['cart_id'] = request.session.session_key or request.session.create() |
||||||
|
return request.session['cart_id'] |
||||||
|
|
||||||
|
def get_cart_items(request): |
||||||
|
"""Get all cart items for the current session""" |
||||||
|
cart_id = get_or_create_cart_id(request) |
||||||
|
return CartItem.objects.filter(session_id=cart_id) |
||||||
|
|
||||||
|
def add_to_cart(request, product_id, quantity=1): |
||||||
|
"""Add a product to the cart or update its quantity""" |
||||||
|
product = Product.objects.get(id=product_id) |
||||||
|
cart_id = get_or_create_cart_id(request) |
||||||
|
|
||||||
|
try: |
||||||
|
# Try to get existing cart item |
||||||
|
cart_item = CartItem.objects.get(product=product, session_id=cart_id) |
||||||
|
cart_item.quantity += quantity |
||||||
|
cart_item.save() |
||||||
|
except CartItem.DoesNotExist: |
||||||
|
# Create new cart item |
||||||
|
cart_item = CartItem.objects.create( |
||||||
|
product=product, |
||||||
|
quantity=quantity, |
||||||
|
session_id=cart_id |
||||||
|
) |
||||||
|
|
||||||
|
return cart_item |
||||||
|
|
||||||
|
def remove_from_cart(request, product_id): |
||||||
|
"""Remove a product from the cart""" |
||||||
|
cart_id = get_or_create_cart_id(request) |
||||||
|
CartItem.objects.filter(product_id=product_id, session_id=cart_id).delete() |
||||||
|
|
||||||
|
def update_cart_item(request, product_id, quantity): |
||||||
|
"""Update the quantity of a cart item""" |
||||||
|
cart_id = get_or_create_cart_id(request) |
||||||
|
cart_item = CartItem.objects.get(product_id=product_id, session_id=cart_id) |
||||||
|
|
||||||
|
if quantity > 0: |
||||||
|
cart_item.quantity = quantity |
||||||
|
cart_item.save() |
||||||
|
else: |
||||||
|
cart_item.delete() |
||||||
|
|
||||||
|
def get_cart_total(request): |
||||||
|
"""Calculate the total price of all items in the cart""" |
||||||
|
return sum(item.product.price * item.quantity for item in get_cart_items(request)) |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
# Generated by Django 4.2.11 on 2025-03-17 17:27 |
||||||
|
|
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
initial = True |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Color', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('name', models.CharField(choices=[('Red', 'Red'), ('Blue', 'Blue'), ('Green', 'Green'), ('Black', 'Black'), ('White', 'White')], max_length=10, unique=True)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Size', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('name', models.CharField(choices=[('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'X-Large')], max_length=5, unique=True)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Product', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('title', models.CharField(max_length=200)), |
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='products/')), |
||||||
|
('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), |
||||||
|
('colors', models.ManyToManyField(blank=True, related_name='products', to='shop.color')), |
||||||
|
('sizes', models.ManyToManyField(blank=True, related_name='products', to='shop.size')), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='CartItem', |
||||||
|
fields=[ |
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('quantity', models.PositiveIntegerField(default=1)), |
||||||
|
('session_id', models.CharField(blank=True, max_length=255, null=True)), |
||||||
|
('date_added', models.DateTimeField(auto_now_add=True)), |
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product')), |
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |
||||||
|
], |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
# Generated by Django 4.2.11 on 2025-03-17 17:31 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('shop', '0001_initial'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.RenameModel( |
||||||
|
old_name='Color', |
||||||
|
new_name='ProductColor', |
||||||
|
), |
||||||
|
migrations.RenameModel( |
||||||
|
old_name='Size', |
||||||
|
new_name='ProductSize', |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='product', |
||||||
|
name='colors', |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='product', |
||||||
|
name='sizes', |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='product', |
||||||
|
name='product_colors', |
||||||
|
field=models.ManyToManyField(blank=True, to='shop.productcolor'), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='product', |
||||||
|
name='product_sizes', |
||||||
|
field=models.ManyToManyField(blank=True, to='shop.productsize'), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
# Generated by Django 4.2.11 on 2025-03-17 17:33 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('shop', '0002_rename_color_productcolor_rename_size_productsize_and_more'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.RenameModel( |
||||||
|
old_name='ProductColor', |
||||||
|
new_name='Color', |
||||||
|
), |
||||||
|
migrations.RenameModel( |
||||||
|
old_name='ProductSize', |
||||||
|
new_name='Size', |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='product', |
||||||
|
name='product_colors', |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='product', |
||||||
|
name='product_sizes', |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='product', |
||||||
|
name='colors', |
||||||
|
field=models.ManyToManyField(blank=True, related_name='products', to='shop.color'), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='product', |
||||||
|
name='sizes', |
||||||
|
field=models.ManyToManyField(blank=True, related_name='products', to='shop.size'), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
from django.contrib.auth.models import User |
||||||
|
from django.db import models |
||||||
|
from django.conf import settings |
||||||
|
|
||||||
|
class ColorChoices(models.TextChoices): |
||||||
|
RED = "Red", "Red" |
||||||
|
BLUE = "Blue", "Blue" |
||||||
|
GREEN = "Green", "Green" |
||||||
|
BLACK = "Black", "Black" |
||||||
|
WHITE = "White", "White" |
||||||
|
|
||||||
|
class SizeChoices(models.TextChoices): |
||||||
|
SMALL = "S", "Small" |
||||||
|
MEDIUM = "M", "Medium" |
||||||
|
LARGE = "L", "Large" |
||||||
|
XLARGE = "XL", "X-Large" |
||||||
|
|
||||||
|
class Color(models.Model): |
||||||
|
name = models.CharField(max_length=10, choices=ColorChoices.choices, unique=True) |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.name |
||||||
|
|
||||||
|
class Size(models.Model): |
||||||
|
name = models.CharField(max_length=5, choices=SizeChoices.choices, unique=True) |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.name |
||||||
|
|
||||||
|
class Product(models.Model): |
||||||
|
title = models.CharField(max_length=200) |
||||||
|
image = models.ImageField(upload_to="products/", null=True, blank=True) |
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) |
||||||
|
|
||||||
|
# Use string references to prevent circular imports |
||||||
|
colors = models.ManyToManyField("shop.Color", blank=True, related_name="products") |
||||||
|
sizes = models.ManyToManyField("shop.Size", blank=True, related_name="products") |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.title |
||||||
|
|
||||||
|
class CartItem(models.Model): |
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) |
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE) |
||||||
|
quantity = models.PositiveIntegerField(default=1) |
||||||
|
session_id = models.CharField(max_length=255, null=True, blank=True) |
||||||
|
date_added = models.DateTimeField(auto_now_add=True) |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return f"{self.quantity} x {self.product.title}" |
||||||
|
|
||||||
|
def get_total_price(self): |
||||||
|
return self.product.price * self.quantity |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
.product-image { |
||||||
|
width: 100%; |
||||||
|
height: 180px; |
||||||
|
object-fit: contain; |
||||||
|
display: block; |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
|
||||||
|
.bubble { |
||||||
|
width: 100%; |
||||||
|
height: 365px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.bubble h3 { |
||||||
|
margin-top: 10px; |
||||||
|
font-size: 1.2em; |
||||||
|
flex-grow: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.product-price { |
||||||
|
font-weight: bold; |
||||||
|
color: #f39200; |
||||||
|
margin: 10px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.add-to-cart-form { |
||||||
|
display: flex; |
||||||
|
gap: 10px; |
||||||
|
margin-top: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.quantity-input { |
||||||
|
width: 50px; |
||||||
|
padding: 5px; |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
{% extends 'tournaments/base.html' %} |
||||||
|
|
||||||
|
{% block head_title %}La boutique{% endblock %} |
||||||
|
{% block first_title %}La boutique Padel Club{% endblock %} |
||||||
|
{% block second_title %}Plein de goodies !{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<nav class="margin10"> |
||||||
|
<a href="{% url 'shop:product_list' %}">La Boutique</a> |
||||||
|
<a href="{% url 'index' %}" class="orange">Accueil</a> |
||||||
|
<a href="{% url 'clubs' %}" class="orange">Clubs</a> |
||||||
|
{% if user.is_authenticated %} |
||||||
|
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a> |
||||||
|
<a href="{% url 'profile' %}">Mon compte</a> |
||||||
|
{% else %} |
||||||
|
<a href="{% url 'login' %}">Se connecter</a> |
||||||
|
{% endif %} |
||||||
|
</nav> |
||||||
|
<div class="grid-x"> |
||||||
|
<div class="cell medium-6 large-6 my-block"> |
||||||
|
<h1 class="club my-block topmargin20">Votre panier</h1 > |
||||||
|
<div class="bubble"> |
||||||
|
{% if cart_items %} |
||||||
|
<ul class="cart-items-list"> |
||||||
|
{% for item in cart_items %} |
||||||
|
<li class="cart-item"> |
||||||
|
<div class="cart-item-details"> |
||||||
|
<span class="cart-item-title">{{ item.product.title }}</span> |
||||||
|
<span class="cart-item-quantity">x{{ item.quantity }}</span> |
||||||
|
</div> |
||||||
|
<span class="cart-item-price">{{ item.get_total_price }} €</span> |
||||||
|
</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
|
||||||
|
<div class="cart-summary"> |
||||||
|
<div class="cart-total"> |
||||||
|
<strong>Total:</strong> {{ total }} € |
||||||
|
</div> |
||||||
|
<a href="{% url 'shop:view_cart' %}" class="button">Voir le panier</a> |
||||||
|
</div> |
||||||
|
{% else %} |
||||||
|
<p>Votre panier est vide.</p> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
<div class="cell small-12 medium-4 large-3 my-block"> |
||||||
|
<div class="bubble"> |
||||||
|
{% if product.image %} |
||||||
|
<img src="{{ product.image.url }}" alt="{{ product.title }}" class="product-image"> |
||||||
|
{% else %} |
||||||
|
<div class="no-image">No Image Available</div> |
||||||
|
{% endif %} |
||||||
|
<h3>{{ product.title }}</h3> |
||||||
|
<div class="product-price">{{ product.price }} €</div> |
||||||
|
|
||||||
|
<form method="post" action="{% url 'shop:add_to_cart' product.id %}" class="add-to-cart-form"> |
||||||
|
{% csrf_token %} |
||||||
|
<input type="number" name="quantity" value="1" min="1" max="10" class="quantity-input"> |
||||||
|
<button type="submit" class="btn styled-link">Ajouter au panier</button> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
{% extends 'tournaments/base.html' %} |
||||||
|
|
||||||
|
{% block head_title %}La boutique{% endblock %} |
||||||
|
{% block first_title %}La boutique Padel Club{% endblock %} |
||||||
|
{% block second_title %}Plein de goodies !{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<nav class="margin10"> |
||||||
|
<a href="{% url 'shop:product_list' %}">La Boutique</a> |
||||||
|
<a href="{% url 'index' %}" class="orange">Accueil</a> |
||||||
|
<a href="{% url 'clubs' %}" class="orange">Clubs</a> |
||||||
|
{% if user.is_authenticated %} |
||||||
|
<a href="{% url 'my-tournaments' %}" class="orange">Mes tournois</a> |
||||||
|
<a href="{% url 'profile' %}">Mon compte</a> |
||||||
|
{% else %} |
||||||
|
<a href="{% url 'login' %}">Se connecter</a> |
||||||
|
{% endif %} |
||||||
|
</nav> |
||||||
|
|
||||||
|
<nav class="margin10"> |
||||||
|
<a style="background-color: #90ee90" href="{% url 'shop:view_cart' %}">Voir mon panier ({{ total }} €)</a> |
||||||
|
</nav> |
||||||
|
|
||||||
|
{% if products %} |
||||||
|
<div class="grid-x"> |
||||||
|
{% for product in products %} |
||||||
|
{% include "shop/product_item.html" with product=product %} |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
{% else %} |
||||||
|
<p>No products available.</p> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
from django.test import TestCase |
||||||
|
|
||||||
|
# Create your tests here. |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
from django.urls import path |
||||||
|
from . import views |
||||||
|
|
||||||
|
app_name = 'shop' |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('', views.product_list, name='product_list'), |
||||||
|
|
||||||
|
# Cart URLs |
||||||
|
path('cart/', views.view_cart, name='view_cart'), |
||||||
|
path('cart/add/<int:product_id>/', views.add_to_cart_view, name='add_to_cart'), |
||||||
|
path('cart/update/<int:product_id>/', views.update_cart_view, name='update_cart'), |
||||||
|
path('cart/remove/<int:product_id>/', views.remove_from_cart_view, name='remove_from_cart'), |
||||||
|
|
||||||
|
] |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
from django.shortcuts import render, redirect, get_object_or_404 |
||||||
|
from django.contrib import messages |
||||||
|
from .models import Product, CartItem |
||||||
|
from . import cart |
||||||
|
|
||||||
|
# Create your views here. |
||||||
|
def product_list(request): |
||||||
|
products = Product.objects.all() |
||||||
|
cart_items = cart.get_cart_items(request) |
||||||
|
total = cart.get_cart_total(request) |
||||||
|
return render(request, 'shop/product_list.html', { |
||||||
|
'products': products, |
||||||
|
'cart_items': cart_items, |
||||||
|
'total': total |
||||||
|
}) |
||||||
|
|
||||||
|
def view_cart(request): |
||||||
|
"""Display the shopping cart""" |
||||||
|
cart_items = cart.get_cart_items(request) |
||||||
|
total = cart.get_cart_total(request) |
||||||
|
return render(request, 'shop/cart.html', { |
||||||
|
'cart_items': cart_items, |
||||||
|
'total': total |
||||||
|
}) |
||||||
|
|
||||||
|
def add_to_cart_view(request, product_id): |
||||||
|
"""Add a product to the cart""" |
||||||
|
product = get_object_or_404(Product, id=product_id) |
||||||
|
quantity = int(request.POST.get('quantity', 1)) |
||||||
|
|
||||||
|
cart.add_to_cart(request, product_id, quantity) |
||||||
|
messages.success(request, f'{product.title} added to your cart') |
||||||
|
|
||||||
|
return redirect('shop:product_list') |
||||||
|
|
||||||
|
def update_cart_view(request, product_id): |
||||||
|
"""Update cart item quantity""" |
||||||
|
if request.method == 'POST': |
||||||
|
quantity = int(request.POST.get('quantity', 0)) |
||||||
|
cart.update_cart_item(request, product_id, quantity) |
||||||
|
return redirect('shop:view_cart') |
||||||
|
|
||||||
|
def remove_from_cart_view(request, product_id): |
||||||
|
"""Remove item from cart""" |
||||||
|
cart.remove_from_cart(request, product_id) |
||||||
|
return redirect('shop:view_cart') |
||||||
Loading…
Reference in new issue