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. 6
      tournaments/views.py

@ -52,8 +52,7 @@ SYNC_APPS = {
STRIPE_CURRENCY = 'eur'
# Add managers who should receive internal emails
SHOP_MANAGERS = [
('Razmig Sarkissian', 'razmig@padelclub.app'),
# ('Shop Admin', 'shop-admin@padelclub.app'),
('Shop Admin', 'shop-admin@padelclub.app'),
# ('Laurent Morvillier', 'laurent@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)
except CartItem.DoesNotExist:
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 .models import Order, OrderItem, OrderStatus
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)
def send_order_notification(sender, instance, **kwargs):
@ -307,3 +309,17 @@ Merci de votre confiance.
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"
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"""
if self.is_test_mode:
logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}")
session = stripe.checkout.Session.create(
customer_email=customer_email,
payment_method_types=['card'],
line_items=line_items,
mode='payment',
@ -67,24 +68,3 @@ class StripeService:
# Create a singleton instance for import and use throughout the app
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="bubble">
{% 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 %}
<div class="no-image">No Image Available</div>
{% 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 %}
<div class="options-container">
<div class="option-element product-title">
@ -33,6 +37,7 @@
title="{{ color.name }}"
data-color-id="{{ color.id }}"
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>
{% endfor %}
</div>
@ -66,7 +71,7 @@
</div>
<div class="option-element total-price form-group"><span id="total-price-{{ product.id }}">{{ product.price }}</span></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>
</div>
</div>
@ -123,6 +128,88 @@ function selectColor(productId, colorId, colorName, element) {
// Add selected class to clicked color
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>

@ -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),
}
# Set up customer information
customer_email = None
# Add user info to metadata if available
if request.user.is_authenticated:
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:
metadata['guest_email'] = request.session.get('guest_email', '')
customer_email = request.session.get('guest_email', '')
try:
# Use the service to create the session
checkout_session = stripe_service.create_checkout_session(
customer_email=customer_email,
line_items=line_items,
success_url=success_url,
cancel_url=cancel_url,
@ -114,8 +123,21 @@ def add_to_cart_view(request, product_id):
size_id = request.POST.get('size')
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')
def update_cart_view(request, product_id):

@ -424,13 +424,16 @@ class Match(SideStoreModel):
time_indication = self.time_indication()
court = self.court_name(self.court_index)
group_stage_name = None
bracket_name = None
if self.group_stage:
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
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():
livematch.add_team(team)
@ -491,7 +494,7 @@ class Team:
}
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.title = title
self.date = date
@ -506,6 +509,7 @@ class LiveMatch:
self.disabled = disabled
self.start_date = start_date
self.court_index = court_index
self.bracket_name = bracket_name
def add_team(self, team):
self.teams.append(team)
@ -526,7 +530,8 @@ class LiveMatch:
"group_stage_name": self.group_stage_name,
"format": self.format,
"disabled": self.disabled,
"court_index": self.court_index
"court_index": self.court_index,
"bracket_name": self.bracket_name
}
def show_time_indication(self):

@ -902,6 +902,23 @@ h-margin {
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 {
background-color: #90ee90; /* Light green color */
}

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

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

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

@ -2,6 +2,17 @@
<div class="cell medium-12 large-3 my-block">
<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>
{% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}">
@ -53,5 +64,14 @@
{% endfor %}
</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>

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

Loading…
Cancel
Save