diff --git a/authentication/views.py b/authentication/views.py
index 0bc6974..2236557 100644
--- a/authentication/views.py
+++ b/authentication/views.py
@@ -17,8 +17,10 @@ from .utils import is_valid_email
from .models import Device, LoginLog
from .serializers import ChangePasswordSerializer
+import logging
CustomUser=get_user_model()
+logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name='dispatch')
class CustomAuthToken(APIView):
@@ -29,6 +31,7 @@ class CustomAuthToken(APIView):
password = request.data.get('password')
device_id = request.data.get('device_id')
+ logger.info(f'Login attempt from {username}')
user = authenticate(username=username, password=password)
if user is None and is_valid_email(username) == True:
diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py
index 1e95c73..e2ef565 100644
--- a/padelclub_backend/settings.py
+++ b/padelclub_backend/settings.py
@@ -161,10 +161,70 @@ AUTHENTICATION_BACKENDS = [
]
CSRF_COOKIE_SECURE = True # if using HTTPS
-if DEBUG: # Development environment
- SESSION_COOKIE_SECURE = False
-else: # Production environment
- SESSION_COOKIE_SECURE = True
+SESSION_COOKIE_SECURE = True
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'verbose': {
+ 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
+ 'style': '{',
+ },
+ 'simple': {
+ 'format': '{levelname} {message}',
+ 'style': '{',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'level': 'INFO',
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose',
+ },
+ 'file': {
+ 'level': 'DEBUG',
+ 'class': 'logging.FileHandler',
+ 'filename': os.path.join(BASE_DIR, 'django.log'),
+ 'formatter': 'verbose',
+ },
+ 'rotating_file': {
+ 'level': 'INFO',
+ 'class': 'logging.handlers.RotatingFileHandler',
+ 'filename': os.path.join(BASE_DIR, 'django.log'),
+ 'maxBytes': 10 * 1024 * 1024,
+ 'backupCount': 10,
+ 'formatter': 'verbose',
+ },
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ 'tournaments': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ 'propagate': False,
+ },
+ 'authentication': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ 'propagate': False,
+ },
+ 'sync': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ 'propagate': False,
+ },
+ 'api': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ 'propagate': False,
+ },
+ },
+}
-from .settings_local import *
from .settings_app import *
+from .settings_local import *
diff --git a/shop/admin.py b/shop/admin.py
index 62fa1b3..72d69c6 100644
--- a/shop/admin.py
+++ b/shop/admin.py
@@ -1,5 +1,6 @@
from django.contrib import admin
-from .models import Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage
+from django.shortcuts import render
+from .models import Product, Color, Size, Order, OrderItem, GuestUser, Coupon, CouponUsage, OrderStatus
from django.utils.html import format_html
@admin.register(Product)
@@ -40,6 +41,67 @@ class OrderItemInline(admin.TabularInline):
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'date_ordered', 'status', 'total_price')
inlines = [OrderItemInline]
+ list_filter = ('status', 'payment_status')
+
+ def changelist_view(self, request, extra_context=None):
+ # If 'show_preparation' parameter is in the request, show the preparation view
+ if 'show_preparation' in request.GET:
+ return self.preparation_view(request)
+
+ # Otherwise show the normal change list
+ extra_context = extra_context or {}
+ paid_orders_count = Order.objects.filter(status=OrderStatus.PAID).count()
+ extra_context['paid_orders_count'] = paid_orders_count
+ return super().changelist_view(request, extra_context=extra_context)
+
+ def preparation_view(self, request):
+ """View for items that need to be prepared"""
+ # Get paid orders
+ orders = Order.objects.filter(status=OrderStatus.PAID).order_by('-date_ordered')
+
+ # Group items by product, color, size
+ items_by_variant = {}
+ all_items = OrderItem.objects.filter(order__status=OrderStatus.PAID)
+
+ for item in all_items:
+ # Create a key for grouping items
+ key = (
+ str(item.product.id),
+ str(item.color.id) if item.color else 'none',
+ str(item.size.id) if item.size else 'none'
+ )
+
+ if key not in items_by_variant:
+ items_by_variant[key] = {
+ 'product': item.product,
+ 'color': item.color,
+ 'size': item.size,
+ 'quantity': 0,
+ 'orders': set()
+ }
+
+ items_by_variant[key]['quantity'] += item.quantity
+ items_by_variant[key]['orders'].add(item.order.id)
+
+ # Convert to list and sort
+ items_list = list(items_by_variant.values())
+ items_list.sort(key=lambda x: x['product'].title)
+
+ context = {
+ 'title': 'Orders to Prepare',
+ 'app_label': 'shop',
+ 'opts': Order._meta,
+ 'orders': orders,
+ 'items': items_list,
+ 'total_orders': orders.count(),
+ 'total_items': sum(i['quantity'] for i in items_list)
+ }
+
+ return render(
+ request,
+ 'admin/shop/order/preparation_view.html',
+ context
+ )
class GuestUserOrderInline(admin.TabularInline):
model = Order
diff --git a/shop/signals.py b/shop/signals.py
index 025c7a5..f449550 100644
--- a/shop/signals.py
+++ b/shop/signals.py
@@ -1,4 +1,4 @@
-from django.db.models.signals import post_save, post_delete
+from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver
from django.core.mail import send_mail
from django.conf import settings
@@ -8,18 +8,38 @@ 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([pre_save, post_delete], sender=Order)
def send_order_notification(sender, instance, **kwargs):
"""Send an email notification when an order is created, updated, or deleted."""
- transaction.on_commit(lambda: _send_order_email(instance, **kwargs))
+ # For pre_save, we need to check if the instance exists in the database
+ if kwargs.get('signal', None) == pre_save:
+ try:
+ # Get the current instance from the database
+ old_instance = Order.objects.get(pk=instance.pk)
+ # Only send notification if status has changed
+ if old_instance.status != instance.status:
+ # Execute on commit to ensure DB consistency
+ transaction.on_commit(lambda: _send_order_email(instance, old_status=old_instance.status, **kwargs))
+ except Order.DoesNotExist:
+ # This is a new instance (creation)
+ # You might want to handle creation differently or just pass
+ pass
+ else:
+ # Handle post_delete
+ transaction.on_commit(lambda: _send_order_email(instance, **kwargs))
-def _send_order_email(instance, **kwargs):
+def _send_order_email(instance, old_status=None, **kwargs):
# Skip processing for PENDING orders
if instance.status == OrderStatus.PENDING:
return
# Determine action type
- action = _determine_action_type(kwargs)
+ if 'signal' in kwargs and kwargs['signal'] == post_delete:
+ action = "DELETED"
+ elif old_status is None:
+ action = "CREATED"
+ else:
+ action = "UPDATED"
if action in ["DELETED", "CREATED"]:
return # No emails for these actions
@@ -34,15 +54,6 @@ def _send_order_email(instance, **kwargs):
if order_details['customer_email']:
_send_customer_notification(instance, order_details, items_list)
-def _determine_action_type(kwargs):
- """Determine the action type from signal kwargs."""
- if 'signal' in kwargs and kwargs['signal'] == post_delete:
- return "DELETED"
- elif kwargs.get('created', False):
- return "CREATED"
- else:
- return "UPDATED"
-
def _get_order_details(instance):
"""Extract and build order details dictionary."""
# Get customer info
@@ -122,14 +133,17 @@ def _send_internal_notification(instance, action, order_details, items_list):
# Build price information with coupon details if applicable
price_info = f"Prix total: {order_details['total_price']}€"
+ server = ""
+ if settings.DEBUG:
+ server = "DEBUG: "
+
if order_details['has_coupon']:
price_info = f"""
Prix total: {order_details['total_price']}€
{order_details['coupon_info']}
Réduction: -{order_details['discount_amount']}€
Montant payé: {order_details['final_price']}€"""
-
- subject = f"Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}"
+ subject = f"{server}Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}"
message = f"""
La commande #{order_details['order_id']} a été {action_fr.lower()}
diff --git a/shop/templates/admin/shop/order/change_list.html b/shop/templates/admin/shop/order/change_list.html
new file mode 100644
index 0000000..e3e7fdc
--- /dev/null
+++ b/shop/templates/admin/shop/order/change_list.html
@@ -0,0 +1,18 @@
+{% extends "admin/change_list.html" %}
+
+{% block object-tools %}
+
+{% endblock %}
diff --git a/shop/templates/admin/shop/order/preparation_view.html b/shop/templates/admin/shop/order/preparation_view.html
new file mode 100644
index 0000000..63213a5
--- /dev/null
+++ b/shop/templates/admin/shop/order/preparation_view.html
@@ -0,0 +1,103 @@
+{% extends "admin/base_site.html" %}
+
+{% block content %}
+
+
Total orders with status PAID: {{ total_orders }}
+
Total items to prepare: {{ total_items }}
+
+
Print This Page
+
Back to Orders
+
+
Items Summary
+
+
+
+ Product
+ Color
+ Size
+ Quantity
+ Orders
+
+
+
+ {% for item in items %}
+
+ {{ item.product.title }}
+
+ {% if item.color %}
+
+ {{ item.color.name }}
+ {% else %}
+ -
+ {% endif %}
+
+ {{ item.size.name|default:"-" }}
+ {{ item.quantity }}
+
+ {% for order_id in item.orders %}
+ Order #{{ order_id }} {% if not forloop.last %}, {% endif %}
+ {% endfor %}
+
+
+ {% empty %}
+
+ No items to prepare
+
+ {% endfor %}
+
+
+
+
Order Details
+
+
+
+ Order #
+ Date
+ Customer
+ Items
+
+
+
+ {% for order in orders %}
+
+ Order #{{ order.id }}
+ {{ order.date_ordered|date:"Y-m-d H:i" }}
+
+ {% if order.user %}
+ {{ order.user.email }}
+ {% elif order.guest_user %}
+ {{ order.guest_user.email }} (Guest)
+ {% else %}
+ Unknown
+ {% endif %}
+
+
+ {% for item in order.items.all %}
+ {{ item.quantity }}x {{ item.product.title }}
+ {% if item.color %} ({{ item.color.name }}){% endif %}
+ {% if item.size %} [{{ item.size.name }}]{% endif %}
+
+ {% endfor %}
+
+
+ {% empty %}
+
+ No orders found
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/tournaments/admin.py b/tournaments/admin.py
index 1248c10..e06bb99 100644
--- a/tournaments/admin.py
+++ b/tournaments/admin.py
@@ -13,7 +13,7 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
- search_fields = ('username', 'email', 'phone', 'first_name', 'last_name', 'licence_id')
+ search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id']
list_display = ['email', 'first_name', 'last_name', 'username', 'licence_id', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin']
list_filter = ['is_active', 'origin']
@@ -83,7 +83,7 @@ class RoundAdmin(SyncedObjectAdmin):
class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank']
- search_fields = ('id', 'first_name', 'last_name', 'licence_id__icontains')
+ search_fields = ['id', 'first_name', 'last_name', 'licence_id__icontains']
list_filter = ['registered_online', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name']
raw_id_fields = ['team_registration'] # Add this line
@@ -111,9 +111,9 @@ class GroupStageAdmin(SyncedObjectAdmin):
class ClubAdmin(SyncedObjectAdmin):
list_display = ['name', 'acronym', 'city', 'creator', 'events_count', 'broadcast_code']
- search_fields = ('name', 'acronym', 'city')
+ search_fields = ['name', 'acronym', 'city']
ordering = ['creator']
- raw_id_fields = ['creator']
+ raw_id_fields = ['creator', 'related_user']
class PurchaseAdmin(SyncedObjectAdmin):
list_display = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date']
@@ -150,7 +150,7 @@ class UnregisteredTeamAdmin(admin.ModelAdmin):
class UnregisteredPlayerAdmin(admin.ModelAdmin):
list_display = ['first_name', 'last_name', 'licence_id']
- search_fields = ('first_name', 'last_name')
+ search_fields = ['first_name', 'last_name']
list_filter = []
ordering = ['last_name', 'first_name']
diff --git a/tournaments/repositories.py b/tournaments/repositories.py
index 634a835..d30f636 100644
--- a/tournaments/repositories.py
+++ b/tournaments/repositories.py
@@ -1,4 +1,3 @@
-from django.utils import timezone
from .models import TeamRegistration, PlayerRegistration
from .models.player_enums import PlayerSexType, PlayerDataSource
from .models.enums import FederalCategory
diff --git a/tournaments/services/tournament_registration.py b/tournaments/services/tournament_registration.py
index 258b69f..019ef29 100644
--- a/tournaments/services/tournament_registration.py
+++ b/tournaments/services/tournament_registration.py
@@ -72,13 +72,10 @@ class TournamentRegistrationService:
if self._is_already_registered(licence_id):
return
- if self.request.user.is_authenticated and self.request.user.licence_id is None:
+ if self.request.user.is_authenticated and self.request.user.licence_id is None and len(self.context['current_players']) == 0:
if self._update_user_license(player_data.get('licence_id')) == False:
- return
-
- if self.request.user.licence_id is None and len(self.context['current_players']) == 0:
- # if no licence id for authentificated user and trying to add him as first player of the team, we check his federal data
- self._handle_invalid_names(licence_id, player_data)
+ # if no licence id for authentificated user and trying to add him as first player of the team, we check his federal data
+ self._handle_invalid_names(licence_id, player_data)
else:
# Handle player data
if self.context['add_player_form'].names_is_valid():
diff --git a/tournaments/templates/registration/login.html b/tournaments/templates/registration/login.html
index 2188b41..d4ac8e7 100644
--- a/tournaments/templates/registration/login.html
+++ b/tournaments/templates/registration/login.html
@@ -43,7 +43,7 @@
- Pas encore de compte ? Créer le tout de suite !
+ Pas encore de compte ? Créez le tout de suite !
diff --git a/tournaments/templates/tournaments/tournament_info.html b/tournaments/templates/tournaments/tournament_info.html
index b7c8427..c1fab19 100644
--- a/tournaments/templates/tournaments/tournament_info.html
+++ b/tournaments/templates/tournaments/tournament_info.html
@@ -213,7 +213,7 @@
Vous avez besoin d'un compte Padel Club pour pouvoir vous inscrire en ligne.
{% endif %}
diff --git a/tournaments/views.py b/tournaments/views.py
index f6c3c72..a69b4f7 100644
--- a/tournaments/views.py
+++ b/tournaments/views.py
@@ -54,6 +54,8 @@ from .services.tournament_unregistration import TournamentUnregistrationService
from django.core.exceptions import ValidationError
from .forms import (
ProfileUpdateForm,
+ SimpleCustomUserCreationForm,
+ SimpleForm
)
from .utils.apns import send_push_notification
from .utils.licence_validator import LicenseValidator