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 }}

+ + + Back to Orders + +

Items Summary

+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
ProductColorSizeQuantityOrders
{{ 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 %} +
No items to prepare
+ +

Order Details

+ + + + + + + + + + + {% for order in orders %} + + + + + + + {% empty %} + + + + {% endfor %} + +
Order #DateCustomerItems
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 %} +
No orders found
+ + +
+{% 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.
- Créer le tout de suite ! + Créez le tout de suite !

{% 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