Merge branch 'main' into timetoconfirm

timetoconfirm
Raz 7 months ago
commit 33c084ba0e
  1. 3
      authentication/views.py
  2. 68
      padelclub_backend/settings.py
  3. 64
      shop/admin.py
  4. 44
      shop/signals.py
  5. 18
      shop/templates/admin/shop/order/change_list.html
  6. 103
      shop/templates/admin/shop/order/preparation_view.html
  7. 10
      tournaments/admin.py
  8. 1
      tournaments/repositories.py
  9. 5
      tournaments/services/tournament_registration.py
  10. 2
      tournaments/templates/registration/login.html
  11. 2
      tournaments/templates/tournaments/tournament_info.html
  12. 2
      tournaments/views.py

@ -17,8 +17,10 @@ from .utils import is_valid_email
from .models import Device, LoginLog from .models import Device, LoginLog
from .serializers import ChangePasswordSerializer from .serializers import ChangePasswordSerializer
import logging
CustomUser=get_user_model() CustomUser=get_user_model()
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class CustomAuthToken(APIView): class CustomAuthToken(APIView):
@ -29,6 +31,7 @@ class CustomAuthToken(APIView):
password = request.data.get('password') password = request.data.get('password')
device_id = request.data.get('device_id') device_id = request.data.get('device_id')
logger.info(f'Login attempt from {username}')
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user is None and is_valid_email(username) == True: if user is None and is_valid_email(username) == True:

@ -161,10 +161,70 @@ AUTHENTICATION_BACKENDS = [
] ]
CSRF_COOKIE_SECURE = True # if using HTTPS 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
from .settings_local import * 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_app import * from .settings_app import *
from .settings_local import *

@ -1,5 +1,6 @@
from django.contrib import admin 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 from django.utils.html import format_html
@admin.register(Product) @admin.register(Product)
@ -40,6 +41,67 @@ class OrderItemInline(admin.TabularInline):
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'date_ordered', 'status', 'total_price') list_display = ('id', 'date_ordered', 'status', 'total_price')
inlines = [OrderItemInline] 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): class GuestUserOrderInline(admin.TabularInline):
model = Order model = Order

@ -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.dispatch import receiver
from django.core.mail import send_mail from django.core.mail import send_mail
from django.conf import settings from django.conf import settings
@ -8,18 +8,38 @@ from django.db import transaction
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from .cart import transfer_cart 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): def send_order_notification(sender, instance, **kwargs):
"""Send an email notification when an order is created, updated, or deleted.""" """Send an email notification when an order is created, updated, or deleted."""
# 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)) 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 # Skip processing for PENDING orders
if instance.status == OrderStatus.PENDING: if instance.status == OrderStatus.PENDING:
return return
# Determine action type # 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"]: if action in ["DELETED", "CREATED"]:
return # No emails for these actions return # No emails for these actions
@ -34,15 +54,6 @@ def _send_order_email(instance, **kwargs):
if order_details['customer_email']: if order_details['customer_email']:
_send_customer_notification(instance, order_details, items_list) _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): def _get_order_details(instance):
"""Extract and build order details dictionary.""" """Extract and build order details dictionary."""
# Get customer info # 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 # Build price information with coupon details if applicable
price_info = f"Prix total: {order_details['total_price']}" price_info = f"Prix total: {order_details['total_price']}"
server = ""
if settings.DEBUG:
server = "DEBUG: "
if order_details['has_coupon']: if order_details['has_coupon']:
price_info = f""" price_info = f"""
Prix total: {order_details['total_price']} Prix total: {order_details['total_price']}
{order_details['coupon_info']} {order_details['coupon_info']}
Réduction: -{order_details['discount_amount']} Réduction: -{order_details['discount_amount']}
Montant payé: {order_details['final_price']}""" Montant payé: {order_details['final_price']}"""
subject = f"{server}Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}"
subject = f"Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}"
message = f""" message = f"""
La commande #{order_details['order_id']} a été {action_fr.lower()} La commande #{order_details['order_id']} a été {action_fr.lower()}

@ -0,0 +1,18 @@
{% extends "admin/change_list.html" %}
{% block object-tools %}
<ul class="object-tools">
<li>
<a href="?show_preparation=1" class="viewlink">
Orders to Prepare ({{ paid_orders_count }})
</a>
</li>
{% if has_add_permission %}
<li>
<a href="{% url 'admin:shop_order_add' %}" class="addlink">
Add Order
</a>
</li>
{% endif %}
</ul>
{% endblock %}

@ -0,0 +1,103 @@
{% extends "admin/base_site.html" %}
{% block content %}
<div id="content-main">
<p>Total orders with status PAID: {{ total_orders }}</p>
<p>Total items to prepare: {{ total_items }}</p>
<button onclick="window.print()" style="margin-bottom: 20px">Print This Page</button>
<a href="?" class="button" style="margin-left: 10px">Back to Orders</a>
<h2>Items Summary</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Product</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Color</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Size</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Quantity</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Orders</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{{ item.product.title }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% if item.color %}
<span style="display: inline-block; width: 15px; height: 15px; border-radius: 50%; background-color: {{ item.color.colorHex }}; margin-right: 5px;"></span>
{{ item.color.name }}
{% else %}
-
{% endif %}
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{{ item.size.name|default:"-" }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; font-weight: bold;">{{ item.quantity }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% for order_id in item.orders %}
<a href="../{{ order_id }}/change/">Order #{{ order_id }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" style="padding: 8px; text-align: center;">No items to prepare</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2 style="margin-top: 30px;">Order Details</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Order #</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Date</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Customer</th>
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Items</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><a href="../{{ order.id }}/change/">Order #{{ order.id }}</a></td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{{ order.date_ordered|date:"Y-m-d H:i" }}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% if order.user %}
{{ order.user.email }}
{% elif order.guest_user %}
{{ order.guest_user.email }} (Guest)
{% else %}
Unknown
{% endif %}
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
{% 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 %}
<br>
{% endfor %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" style="padding: 8px; text-align: center;">No orders found</td>
</tr>
{% endfor %}
</tbody>
</table>
<style>
@media print {
button, .button, #header, .breadcrumbs {
display: none !important;
}
body {
padding: 0;
margin: 0;
}
}
</style>
</div>
{% endblock %}

@ -13,7 +13,7 @@ class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm form = CustomUserChangeForm
add_form = CustomUserCreationForm add_form = CustomUserCreationForm
model = CustomUser 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_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'] list_filter = ['is_active', 'origin']
@ -83,7 +83,7 @@ class RoundAdmin(SyncedObjectAdmin):
class PlayerRegistrationAdmin(SyncedObjectAdmin): class PlayerRegistrationAdmin(SyncedObjectAdmin):
list_display = ['first_name', 'last_name', 'licence_id', 'rank'] 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] list_filter = ['registered_online', TeamScoreTournamentListFilter]
ordering = ['last_name', 'first_name'] ordering = ['last_name', 'first_name']
raw_id_fields = ['team_registration'] # Add this line raw_id_fields = ['team_registration'] # Add this line
@ -111,9 +111,9 @@ class GroupStageAdmin(SyncedObjectAdmin):
class ClubAdmin(SyncedObjectAdmin): class ClubAdmin(SyncedObjectAdmin):
list_display = ['name', 'acronym', 'city', 'creator', 'events_count', 'broadcast_code'] list_display = ['name', 'acronym', 'city', 'creator', 'events_count', 'broadcast_code']
search_fields = ('name', 'acronym', 'city') search_fields = ['name', 'acronym', 'city']
ordering = ['creator'] ordering = ['creator']
raw_id_fields = ['creator'] raw_id_fields = ['creator', 'related_user']
class PurchaseAdmin(SyncedObjectAdmin): class PurchaseAdmin(SyncedObjectAdmin):
list_display = ['id', 'user', 'product_id', 'quantity', 'purchase_date', 'revocation_date', 'expiration_date'] 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): class UnregisteredPlayerAdmin(admin.ModelAdmin):
list_display = ['first_name', 'last_name', 'licence_id'] list_display = ['first_name', 'last_name', 'licence_id']
search_fields = ('first_name', 'last_name') search_fields = ['first_name', 'last_name']
list_filter = [] list_filter = []
ordering = ['last_name', 'first_name'] ordering = ['last_name', 'first_name']

@ -1,4 +1,3 @@
from django.utils import timezone
from .models import TeamRegistration, PlayerRegistration from .models import TeamRegistration, PlayerRegistration
from .models.player_enums import PlayerSexType, PlayerDataSource from .models.player_enums import PlayerSexType, PlayerDataSource
from .models.enums import FederalCategory from .models.enums import FederalCategory

@ -72,11 +72,8 @@ class TournamentRegistrationService:
if self._is_already_registered(licence_id): if self._is_already_registered(licence_id):
return 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: 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 # 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) self._handle_invalid_names(licence_id, player_data)
else: else:

@ -43,7 +43,7 @@
</p> </p>
</form> </form>
<p>Pas encore de compte ? <a href="{% url 'signup' %}" class="styled-link">Créer le tout de suite !</a></p> <p>Pas encore de compte ? <a href="{% url 'signup' %}" class="styled-link">Créez le tout de suite !</a></p>
</div> </div>
</div> </div>
</div> </div>

@ -213,7 +213,7 @@
Vous avez besoin d'un compte Padel Club pour pouvoir vous inscrire en ligne. Vous avez besoin d'un compte Padel Club pour pouvoir vous inscrire en ligne.
</div> </div>
<div> <div>
<a href="{% url 'signup' %}?next={{ request.path }}" class="styled-link">Créer le tout de suite !</a> <a href="{% url 'signup' %}?next={{ request.path }}" class="styled-link">Créez le tout de suite !</a>
</div> </div>
</p> </p>
{% endif %} {% endif %}

@ -54,6 +54,8 @@ from .services.tournament_unregistration import TournamentUnregistrationService
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .forms import ( from .forms import (
ProfileUpdateForm, ProfileUpdateForm,
SimpleCustomUserCreationForm,
SimpleForm
) )
from .utils.apns import send_push_notification from .utils.apns import send_push_notification
from .utils.licence_validator import LicenseValidator from .utils.licence_validator import LicenseValidator

Loading…
Cancel
Save