Merge branch 'main' into sync

sync
Laurent 8 months ago
commit 861d92505c
  1. 3
      padelclub_backend/settings_app.py
  2. 18
      shop/cart.py
  3. 16
      shop/signals.py
  4. BIN
      shop/static/shop/images/products/noir_hat.png.avif
  5. 24
      shop/stripe_utils.py
  6. 93
      shop/templates/shop/product_item.html
  7. 0
      shop/templatetags/__init__.py
  8. 82
      shop/templatetags/shop_extras.py
  9. 24
      shop/views.py
  10. 11
      tournaments/models/match.py
  11. BIN
      tournaments/static/rules/padel-guide-cdc.pdf
  12. BIN
      tournaments/static/rules/padel-guide-general.pdf
  13. BIN
      tournaments/static/rules/padel-guide-rankings.pdf
  14. BIN
      tournaments/static/rules/padel-rules-2024.pdf
  15. BIN
      tournaments/static/rules/padel-rules.pdf
  16. 17
      tournaments/static/tournaments/css/style.css
  17. 6
      tournaments/static/tournaments/css/tournament_bracket.css
  18. 4
      tournaments/static/tournaments/js/tournament_bracket.js
  19. 4
      tournaments/templates/registration/signup.html
  20. 20
      tournaments/templates/tournaments/bracket_match_cell.html
  21. 4
      tournaments/views.py

@ -52,8 +52,7 @@ SYNC_APPS = {
STRIPE_CURRENCY = 'eur' STRIPE_CURRENCY = 'eur'
# Add managers who should receive internal emails # Add managers who should receive internal emails
SHOP_MANAGERS = [ SHOP_MANAGERS = [
('Razmig Sarkissian', 'razmig@padelclub.app'), ('Shop Admin', 'shop-admin@padelclub.app'),
# ('Shop Admin', 'shop-admin@padelclub.app'),
# ('Laurent Morvillier', 'laurent@padelclub.app'), # ('Laurent Morvillier', 'laurent@padelclub.app'),
# ('Xavier Rousset', 'xavier@padelclub.app'), # ('Xavier Rousset', 'xavier@padelclub.app'),
] ]

@ -74,3 +74,21 @@ def get_cart_item(request, item_id):
return CartItem.objects.get(id=item_id, session_id=cart_id) return CartItem.objects.get(id=item_id, session_id=cart_id)
except CartItem.DoesNotExist: except CartItem.DoesNotExist:
raise Exception("Cart item not found") raise Exception("Cart item not found")
def transfer_cart(request, old_session_key):
"""
Transfer cart items from an anonymous session to an authenticated user's session
"""
from django.contrib.sessions.models import Session
from django.contrib.sessions.backends.db import SessionStore
# Get the old session
try:
old_session = SessionStore(session_key=old_session_key)
# Check if there are cart items in the old session
if 'cart_items' in old_session:
# Transfer cart items to the new session
request.session['cart_items'] = old_session['cart_items']
request.session.modified = True
except Session.DoesNotExist:
pass

@ -5,6 +5,8 @@ from django.conf import settings
from django.urls import reverse from django.urls import reverse
from .models import Order, OrderItem, OrderStatus from .models import Order, OrderItem, OrderStatus
from django.db import transaction from django.db import transaction
from django.contrib.auth.signals import user_logged_in
from .cart import transfer_cart
@receiver([post_save, post_delete], sender=Order) @receiver([post_save, post_delete], sender=Order)
def send_order_notification(sender, instance, **kwargs): def send_order_notification(sender, instance, **kwargs):
@ -307,3 +309,17 @@ Merci de votre confiance.
L'équipe PadelClub L'équipe PadelClub
""" """
@receiver(user_logged_in)
def user_logged_in_handler(sender, request, user, **kwargs):
"""
When a user logs in, transfer any cart items from their anonymous session
"""
# Get the anonymous session key
if hasattr(request, 'session') and not request.session.is_empty():
anonymous_session_key = request.session.session_key
# After the user logs in, the session key changes
# So we transfer cart from the old session to the new session
if anonymous_session_key:
transfer_cart(request, anonymous_session_key)

@ -25,12 +25,13 @@ class StripeService:
mode_str = "TEST" if self.is_test_mode else "LIVE" mode_str = "TEST" if self.is_test_mode else "LIVE"
logger.debug(f"Initialized StripeService in {mode_str} mode") logger.debug(f"Initialized StripeService in {mode_str} mode")
def create_checkout_session(self, line_items, success_url, cancel_url, metadata=None): def create_checkout_session(self, customer_email, line_items, success_url, cancel_url, metadata=None):
"""Create a Stripe Checkout Session for one-time payments""" """Create a Stripe Checkout Session for one-time payments"""
if self.is_test_mode: if self.is_test_mode:
logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}") logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}")
session = stripe.checkout.Session.create( session = stripe.checkout.Session.create(
customer_email=customer_email,
payment_method_types=['card'], payment_method_types=['card'],
line_items=line_items, line_items=line_items,
mode='payment', mode='payment',
@ -67,24 +68,3 @@ class StripeService:
# Create a singleton instance for import and use throughout the app # Create a singleton instance for import and use throughout the app
stripe_service = StripeService() stripe_service = StripeService()
# For backward compatibility, expose some functions directly
def create_payment_intent(amount, currency=None, metadata=None):
"""Legacy function for backward compatibility"""
if currency is None:
currency = stripe_service.currency
return stripe.PaymentIntent.create(
amount=amount,
currency=currency,
metadata=metadata or {},
)
def create_checkout_session(line_items, success_url, cancel_url, metadata=None):
"""Legacy function for backward compatibility"""
return stripe_service.create_checkout_session(
line_items=line_items,
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata or {},
)

@ -1,11 +1,15 @@
{% load shop_extras %}
<div class="small-12 medium-6 large-3 my-block"> <div class="small-12 medium-6 large-3 my-block">
<div class="bubble"> <div class="bubble">
{% if product.image %} {% if product.image %}
<img src="{{ product.image }}" alt="{{ product.title }}" class="product-image"> <img id="product-image-{{ product.id }}"
src="{{ product.image|color_image_url:product.colors.all.0.name }}"
alt="{{ product.title }}"
class="product-image">
{% else %} {% else %}
<div class="no-image">No Image Available</div> <div class="no-image">No Image Available</div>
{% endif %} {% endif %}
<form method="post" action="{% url 'shop:add_to_cart' product.id %}" class="add-to-cart-form"> <form method="post" action="{% url 'shop:add_to_cart' product.id %}" class="add-to-cart-form" id="cart-form-{{ product.id }}">
{% csrf_token %} {% csrf_token %}
<div class="options-container"> <div class="options-container">
<div class="option-element product-title"> <div class="option-element product-title">
@ -33,6 +37,7 @@
title="{{ color.name }}" title="{{ color.name }}"
data-color-id="{{ color.id }}" data-color-id="{{ color.id }}"
data-color-name="{{ color.name }}" data-color-name="{{ color.name }}"
data-color-image="{{ product.image|color_image_url:color.name }}"
onclick="selectColor('{{ product.id }}', '{{ color.id }}', '{{ color.name }}', this)"></div> onclick="selectColor('{{ product.id }}', '{{ color.id }}', '{{ color.name }}', this)"></div>
{% endfor %} {% endfor %}
</div> </div>
@ -66,7 +71,7 @@
</div> </div>
<div class="option-element total-price form-group"><span id="total-price-{{ product.id }}">{{ product.price }}</span></div> <div class="option-element total-price form-group"><span id="total-price-{{ product.id }}">{{ product.price }}</span></div>
</div> </div>
<button type="submit" class="add-to-cart-button">Ajouter au panier</button> <button type="button" class="add-to-cart-button" onclick="addToCartAjax('{{ product.id }}')">Ajouter au panier</button>
</form> </form>
</div> </div>
</div> </div>
@ -123,6 +128,88 @@ function selectColor(productId, colorId, colorName, element) {
// Add selected class to clicked color // Add selected class to clicked color
element.classList.add('selected'); element.classList.add('selected');
// Update product image based on selected color
const productImage = document.getElementById(`product-image-${productId}`);
if (productImage) {
const colorImage = element.getAttribute('data-color-image');
if (colorImage) {
productImage.src = colorImage;
}
}
} }
function addToCartAjax(productId) {
// Get the form
const form = document.getElementById(`cart-form-${productId}`);
const formData = new FormData(form);
// Add CSRF token
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
// Create notification element
const notification = document.createElement('div');
notification.className = 'add-to-cart-notification';
notification.style.position = 'fixed';
notification.style.bottom = '20px';
notification.style.right = '20px';
notification.style.padding = '20px';
notification.style.backgroundColor = '#90ee90';
notification.style.color = '#707070';
notification.style.borderRadius = '12px';
notification.style.zIndex = '9999';
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.3s';
// Send AJAX request
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrftoken,
}
})
.then(response => response.json())
.then(data => {
// Show success message
notification.textContent = data.message;
document.body.appendChild(notification);
// Update cart total in the navigation
const cartTotalElements = document.querySelectorAll('.confirm-nav-button');
cartTotalElements.forEach(element => {
element.textContent = `Voir mon panier (${data.cart_total} €)`;
});
// Show notification
setTimeout(() => { notification.style.opacity = '1'; }, 100);
// Hide notification after 3 seconds
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300); // Wait for fade-out animation
}, 3000);
})
.catch(error => {
console.error('Error:', error);
notification.textContent = "Erreur lors de l'ajout au panier";
notification.style.backgroundColor = '#F44336';
document.body.appendChild(notification);
// Show notification
setTimeout(() => { notification.style.opacity = '1'; }, 100);
// Hide notification after 3 seconds
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300); // Wait for fade-out animation
}, 3000);
});
}
</script> </script>

@ -0,0 +1,82 @@
from django import template
import os
register = template.Library()
@register.filter
def color_image_url(product_image, color_name):
"""
Returns color-specific image URL with any supported extension.
Falls back to the original image if no color variant exists.
"""
if not product_image or not color_name:
return product_image
# Generate color suffix
suffix = generate_color_suffix(color_name)
# Split path
directory, filename = os.path.split(product_image)
base_name, original_ext = os.path.splitext(filename)
# List of supported image extensions to check
supported_extensions = ['.png.avif', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif']
# Check for the color image with original extension first
color_filename = f"{suffix}_{base_name}{original_ext}"
color_image = os.path.join(directory, color_filename)
# Extract the path after /static/
static_prefix = '/static/'
if color_image.startswith(static_prefix):
rel_path = color_image[len(static_prefix):]
else:
rel_path = color_image.lstrip('/')
# Check if file with original extension exists
from django.conf import settings
app_static_path = os.path.join(settings.BASE_DIR, 'shop', 'static', rel_path)
if os.path.exists(app_static_path):
return color_image
# If not found with original extension, try other extensions
for ext in supported_extensions:
if ext == original_ext:
continue # Skip the original extension as we already checked it
color_filename = f"{suffix}_{base_name}{ext}"
color_image = os.path.join(directory, color_filename)
if color_image.startswith(static_prefix):
rel_path = color_image[len(static_prefix):]
else:
rel_path = color_image.lstrip('/')
app_static_path = os.path.join(settings.BASE_DIR, 'shop', 'static', rel_path)
if os.path.exists(app_static_path):
return color_image
# If no color variant is found with any extension, return the original image
return product_image
def generate_color_suffix(color_name):
"""
Generates a URL-friendly suffix from a color name
Example: "Noir / Gris Foncé Chiné" becomes "noir_gris_fonce_chine"
"""
import unicodedata
import re
# Convert to lowercase and replace accents
value = color_name.lower()
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
# Replace slashes and spaces with underscores
value = re.sub(r'[/\s]+', '_', value)
# Remove any remaining non-alphanumeric characters
value = re.sub(r'[^\w_]', '', value)
return value

@ -50,15 +50,24 @@ def _create_stripe_checkout_session(request, order, line_items):
'order_id': str(order.id), 'order_id': str(order.id),
} }
# Set up customer information
customer_email = None
# Add user info to metadata if available # Add user info to metadata if available
if request.user.is_authenticated: if request.user.is_authenticated:
metadata['user_id'] = str(request.user.id) metadata['user_id'] = str(request.user.id)
customer_email = request.user.email
elif order.guest_user:
metadata['guest_email'] = order.guest_user.email
customer_email = order.guest_user.email
elif 'guest_email' in request.session: elif 'guest_email' in request.session:
metadata['guest_email'] = request.session.get('guest_email', '') metadata['guest_email'] = request.session.get('guest_email', '')
customer_email = request.session.get('guest_email', '')
try: try:
# Use the service to create the session # Use the service to create the session
checkout_session = stripe_service.create_checkout_session( checkout_session = stripe_service.create_checkout_session(
customer_email=customer_email,
line_items=line_items, line_items=line_items,
success_url=success_url, success_url=success_url,
cancel_url=cancel_url, cancel_url=cancel_url,
@ -114,8 +123,21 @@ def add_to_cart_view(request, product_id):
size_id = request.POST.get('size') size_id = request.POST.get('size')
cart_item = cart.add_to_cart(request, product_id, quantity, color_id, size_id) cart_item = cart.add_to_cart(request, product_id, quantity, color_id, size_id)
messages.success(request, f'{cart_item.quantity} x {product.title} added to your cart') message = f'{cart_item.quantity} x {product.title} dans le panier',
# Check if this is an AJAX request
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
cart_items = cart.get_cart_items(request)
total = cart.get_cart_total(request)
return JsonResponse({
'success': True,
'message': message,
'cart_total': total,
'cart_count': cart_items.count()
})
# For non-AJAX requests, fall back to the original behavior
messages.success(request, message)
return redirect('shop:product_list') return redirect('shop:product_list')
def update_cart_view(request, product_id): def update_cart_view(request, product_id):

@ -424,13 +424,16 @@ class Match(SideStoreModel):
time_indication = self.time_indication() time_indication = self.time_indication()
court = self.court_name(self.court_index) court = self.court_name(self.court_index)
group_stage_name = None group_stage_name = None
bracket_name = None
if self.group_stage: if self.group_stage:
group_stage_name = self.group_stage.display_name() group_stage_name = self.group_stage.display_name()
else:
bracket_name = f"Match n˚{self.index_in_round() + 1}"
ended = self.end_date is not None ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short live_format = "Format " + FederalMatchCategory(self.format).format_label_short
livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled) livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled, bracket_name)
for team in self.live_teams(): for team in self.live_teams():
livematch.add_team(team) livematch.add_team(team)
@ -491,7 +494,7 @@ class Team:
} }
class LiveMatch: class LiveMatch:
def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled, bracket_name):
self.index = index self.index = index
self.title = title self.title = title
self.date = date self.date = date
@ -506,6 +509,7 @@ class LiveMatch:
self.disabled = disabled self.disabled = disabled
self.start_date = start_date self.start_date = start_date
self.court_index = court_index self.court_index = court_index
self.bracket_name = bracket_name
def add_team(self, team): def add_team(self, team):
self.teams.append(team) self.teams.append(team)
@ -526,7 +530,8 @@ class LiveMatch:
"group_stage_name": self.group_stage_name, "group_stage_name": self.group_stage_name,
"format": self.format, "format": self.format,
"disabled": self.disabled, "disabled": self.disabled,
"court_index": self.court_index "court_index": self.court_index,
"bracket_name": self.bracket_name
} }
def show_time_indication(self): def show_time_indication(self):

@ -902,6 +902,23 @@ h-margin {
border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */ border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
} }
.status-container-bracket {
margin: 0px -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */
padding: 10px 20px 20px 20px; /* Add padding back to maintain text alignment, including bottom */
border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
}
.status-container-bracket-header {
height: 30px;
text-align: left;
}
.status-container-bracket-header-bottom {
height: 30px;
text-align: left;
}
.status-container-bracket.running,
.status-container.running { .status-container.running {
background-color: #90ee90; /* Light green color */ background-color: #90ee90; /* Light green color */
} }

@ -43,7 +43,7 @@
transform: translateX(-50%); /* Center it exactly */ transform: translateX(-50%); /* Center it exactly */
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
width: auto; /* Change from 100% to auto */ width: 100%; /* Change from 100% to auto */
padding: 5px 10px; padding: 5px 10px;
white-space: nowrap; /* Prevent text from wrapping */ white-space: nowrap; /* Prevent text from wrapping */
@ -53,6 +53,10 @@
justify-content: center; justify-content: center;
} }
.round-title.broadcast-mode {
width: auto; /* Change from 100% to auto */
}
.round-name { .round-name {
color: #707070; color: #707070;
font-size: 1.5em; font-size: 1.5em;

@ -66,7 +66,9 @@ function renderBracket(options) {
// Create title // Create title
const titleDiv = document.createElement("div"); const titleDiv = document.createElement("div");
titleDiv.className = "round-title"; titleDiv.className = "round-title";
if (isBroadcast) {
titleDiv.className = "round-title broadcast-mode";
}
// Get the match group name and format // Get the match group name and format
const firstMatchTemplate = roundMatches[0].closest(".match-template"); const firstMatchTemplate = roundMatches[0].closest(".match-template");
const matchGroupName = firstMatchTemplate.dataset.matchGroupName; const matchGroupName = firstMatchTemplate.dataset.matchGroupName;

@ -36,10 +36,6 @@
<button type="submit" class="rounded-button">Créer votre compte</button> <button type="submit" class="rounded-button">Créer votre compte</button>
</form> </form>
</div> </div>
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div> </div>
</div> </div>

@ -2,6 +2,17 @@
<div class="cell medium-12 large-3 my-block"> <div class="cell medium-12 large-3 my-block">
<div class="bubble"> <div class="bubble">
<div class="status-container-bracket-header">
{% if match.bracket_name %}
<label class="minor-info bold">{{ match.bracket_name }}</label>
{% endif %}
{% if not match.ended %}
<label class="right-label minor-info bold">{{ match.court }}</label>
{% endif %}
</div>
<div> <div>
{% for team in match.teams %} {% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}"> <div class="match-result {% cycle 'bottom-border' '' %}">
@ -53,5 +64,14 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="status-container-bracket-header-bottom">
<div class="status-container-bracket {% if not match.ended and match.started %}running{% endif %}">
<label class="left-label minor-info bold">
{% if match.show_time_indication %}
{{ match.time_indication }}
{% endif %}
</label>
</div>
</div>
</div> </div>
</div> </div>

@ -111,7 +111,11 @@ def index(request):
thirty_days_future = now + timedelta(days=30) thirty_days_future = now + timedelta(days=30)
club_id = request.GET.get('club') club_id = request.GET.get('club')
if club_id:
tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, 50)
else:
tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50) tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50)
display_tournament = [t for t in tournaments if t.display_tournament()] display_tournament = [t for t in tournaments if t.display_tournament()]
live = [] live = []
future = [] future = []

Loading…
Cancel
Save