initial commit

shop
Raz 8 months ago
parent 8d8fa1a7d3
commit 7f31708d86
  1. BIN
      media/products/Capture_décran_2025-03-17_à_17.11.10.png
  2. BIN
      media/products/IMG_4957.jpg
  3. BIN
      media/products/IMG_4957_36DeRZt.jpg
  4. BIN
      media/products/IMG_4957_gKJ3yDG.jpg
  5. BIN
      media/products/IMG_4957_korPKtN.jpg
  6. BIN
      media/products/WhatsApp_Image_2025-03-02_at_12.02.15.jpeg
  7. 5
      padelclub_backend/settings.py
  8. 0
      shop/__init__.py
  9. 14
      shop/admin.py
  10. 6
      shop/apps.py
  11. 52
      shop/cart.py
  12. 53
      shop/migrations/0001_initial.py
  13. 39
      shop/migrations/0002_rename_color_productcolor_rename_size_productsize_and_more.py
  14. 39
      shop/migrations/0003_rename_productcolor_color_rename_productsize_size_and_more.py
  15. 0
      shop/migrations/__init__.py
  16. 53
      shop/models.py
  17. 37
      shop/static/shop/css/shop.css
  18. 48
      shop/templates/shop/cart.html
  19. 17
      shop/templates/shop/product_item.html
  20. 35
      shop/templates/shop/product_list.html
  21. 3
      shop/tests.py
  22. 15
      shop/urls.py
  23. 46
      shop/views.py
  24. 2
      tournaments/admin.py
  25. 4
      tournaments/templates/tournaments/base.html
  26. 7
      tournaments/urls.py

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

@ -15,6 +15,8 @@ import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
@ -32,6 +34,7 @@ ALLOWED_HOSTS = ['*']
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'shop',
'tournaments', 'tournaments',
# 'crm', # 'crm',
'django.contrib.admin', 'django.contrib.admin',
@ -64,7 +67,7 @@ ROOT_URLCONF = 'padelclub_backend.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'templates')], # Project-level templates
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [

@ -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')

@ -19,7 +19,7 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm form = CustomUserChangeForm
add_form = CustomUserCreationForm add_form = CustomUserCreationForm
model = CustomUser model = CustomUser
list_display = ['first_name', 'last_name', 'email', 'latest_event_club_name', 'username', 'is_active', 'date_joined', 'event_count', 'origin'] list_display = ['email', 'first_name', 'last_name', 'latest_event_club_name', 'username', 'is_active', 'date_joined', 'event_count', 'origin']
list_filter = ['is_active', 'origin'] list_filter = ['is_active', 'origin']
ordering = ['-date_joined'] ordering = ['-date_joined']
fieldsets = [ fieldsets = [

@ -13,7 +13,9 @@
/> />
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" /> <link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" /> <link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" />
{% block extra_css %}{% endblock %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'shop/css/shop.css' %}">
{% endblock %}
<link <link
rel="icon" rel="icon"

@ -1,5 +1,9 @@
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include, path from django.urls import include, path
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm from .forms import EmailOrUsernameAuthenticationForm, CustomPasswordChangeForm
from . import views from . import views
@ -75,5 +79,8 @@ urlpatterns = [
path('admin/users-export/', views.UserListExportView.as_view(), name='users_export'), path('admin/users-export/', views.UserListExportView.as_view(), name='users_export'),
path('activation-success/', views.activation_success, name='activation_success'), path('activation-success/', views.activation_success, name='activation_success'),
path('activation-failed/', views.activation_failed, name='activation_failed'), path('activation-failed/', views.activation_failed, name='activation_failed'),
path('shop/', include('shop.urls')),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Loading…
Cancel
Save