diff --git a/api/serializers.py b/api/serializers.py index ec03749..c483f47 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -34,6 +34,7 @@ class UserSerializer(serializers.ModelSerializer): user = CustomUser.objects.create_user( username=validated_data['username'], + last_update=validated_data.get('last_update'), email=validated_data['email'], password=validated_data['password'], first_name=validated_data['first_name'], @@ -78,7 +79,7 @@ class UserSerializer(serializers.ModelSerializer): model = CustomUser fields = '__all__' # ['id', 'username', 'password', 'umpire_code', 'clubs', 'phone', 'first_name', 'last_name', 'licence_id'] -class UserUpdateSerializer(serializers.ModelSerializer): +class CustomUserSerializer(serializers.ModelSerializer): ### the one matching the CustomUser class and used for sync class Meta: model = CustomUser fields = CustomUser.fields_for_update() @@ -86,7 +87,12 @@ class UserUpdateSerializer(serializers.ModelSerializer): class ClubSerializer(serializers.ModelSerializer): class Meta: model = Club - fields = '__all__' # ['id', 'name', 'acronym', 'phone', 'code', 'federal_club_data', 'address', 'city', 'zip_code', 'latitude', 'longitude'] + fields = '__all__' + + def create(self, validated_data): + user = self.context['request'].user + validated_data['creator'] = user + return super().create(validated_data) class TournamentSerializer(serializers.ModelSerializer): class Meta: @@ -153,10 +159,16 @@ class PlayerRegistrationSerializer(serializers.ModelSerializer): # ['id', 'team_registration_id', 'first_name', 'last_name', 'licence_id', 'rank', 'has_paid'] class PurchaseSerializer(serializers.ModelSerializer): + class Meta: model = Purchase fields = '__all__' + def create(self, validated_data): + user = self.context['request'].user + validated_data['user'] = user + return super().create(validated_data) + class ChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField(max_length=128, write_only=True, required=True) new_password1 = serializers.CharField(max_length=128, write_only=True, required=True) diff --git a/api/urls.py b/api/urls.py index ff61351..6c894fb 100644 --- a/api/urls.py +++ b/api/urls.py @@ -25,6 +25,8 @@ router.register(r'device-token', views.DeviceTokenViewSet) urlpatterns = [ path('', include(router.urls)), + path('data/', views.DataApi.as_view(), name="data"), + path('api-token-auth/', obtain_auth_token, name='api_token_auth'), path("user-by-token/", views.user_by_token, name="user_by_token"), path("change-password/", views.ChangePasswordView.as_view(), name="change_password"), diff --git a/api/utils.py b/api/utils.py index bbe5dc2..c8c571d 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,5 +1,23 @@ import re +import importlib def is_valid_email(email): email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' return re.match(email_regex, email) is not None + +def build_serializer_class(source): + + # Remove the 's' character at the end if present + if source.endswith('s'): + source = source[:-1] + + # Capitalize words separated by a dash + words = source.split('-') + capitalized_words = [word[0].upper() + word[1:] for word in words] + transformed_string = ''.join(capitalized_words) + + # Add 'Serializer' at the end + transformed_string += 'Serializer' + + module = importlib.import_module('api.serializers') + return getattr(module, transformed_string) diff --git a/api/views.py b/api/views.py index ec5d64e..d07cb6c 100644 --- a/api/views.py +++ b/api/views.py @@ -1,5 +1,5 @@ -from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, UserUpdateSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer -from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken +from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, CustomUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer +from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, ModelLog from rest_framework import viewsets, permissions from rest_framework.authtoken.models import Token @@ -14,9 +14,152 @@ from rest_framework.views import APIView from django.contrib.auth import authenticate from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone +from django.apps import apps + +from collections import defaultdict from .permissions import IsClubOwner -from .utils import is_valid_email +from .utils import is_valid_email, build_serializer_class + +class DataApi(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + + # unfold content + model_operation = request.data.get('operation') + model_name = request.data.get('model_name') + data = request.data.get('data') + + print(f"/data {model_operation} {model_name}") + + serializer_class = build_serializer_class(model_name) + + model = apps.get_model(app_label='tournaments', model_name=model_name) + try: + instance = model.objects.get(id=data.get('id')) + + if model_operation == 'DELETE': + return self.delete_and_save_log(request, data, model_operation, model_name) + else: # POST/PUT + serializer = serializer_class(instance, data=data, context={'request': request}) + if serializer.is_valid(): + if instance.last_update <= serializer.validated_data.get('last_update'): + print('>>> update') + return self.save_and_create_log(request, serializer, model_operation, model_name) + else: + print('>>> return 203') + return Response(serializer.data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except model.DoesNotExist: + # If the instance doesn't exist, we should be in a POST situation + print('>>> insert') + serializer = serializer_class(data=data, context={'request': request}) + if serializer.is_valid(): + return self.save_and_create_log(request, serializer, model_operation, model_name) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def save_and_create_log(self, request, serializer, model_operation, model_name): + instance = serializer.save() + + self.create_and_save_model_log( + user=request.user, + model_operation=model_operation, + model_name=model_name, + model_id=instance.id + ) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def delete_and_save_log(self, request, data, model_operation, model_name): + + model = apps.get_model(app_label='tournaments', model_name=model_name) + try: + instance = model.objects.get(id=data.id) + instance.delete() + except model.DoesNotExist: + return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND) + + # we delete all logs linked to the instance because they won't be needed anymore + ModelLog.objects.filter(model_id=instance.id).delete() + + self.create_and_save_model_log( + user=request.user, + model_operation=model_operation, + model_name=model_name, + model_id=instance.id + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def create_and_save_model_log(self, user, model_operation, model_name, model_id): + model_log = ModelLog() + model_log.user = user + model_log.operation = model_operation + model_log.date = timezone.localtime(timezone.now()) + model_log.model_name = model_name + model_log.model_id = model_id + model_log.save() + + def get(self, request, *args, **kwargs): + print('/data GET YEAH!') + + last_update = request.query_params.get('last_update') + if not last_update: + return Response({"error": "last_update parameter is required"}, status=status.HTTP_400_BAD_REQUEST) + + print(last_update) + + try: + last_update = timezone.datetime.fromisoformat(last_update) + except ValueError: + return Response({"error": f"Invalid date format for last_update: {last_update}"}, status=status.HTTP_400_BAD_REQUEST) + + logs = ModelLog.objects.filter(date__gt=last_update).order_by('date') + + updates = defaultdict(dict) + deletions = defaultdict(set) + + for log in logs: + model = apps.get_model(app_label='tournaments', model_name=log.model_name) + + if log.operation in ['POST', 'PUT']: + try: + instance = model.objects.get(id=log.model_id) + serializer_class = build_serializer_class(log.model_name) + serializer = serializer_class(instance) + updates[log.model_name][log.model_id] = serializer.data + except model.DoesNotExist: + # If the instance doesn't exist, it might have been deleted after this log was created + pass + elif log.operation == 'DELETE': + deletions[log.model_name].add(log.model_id) + + # Convert updates dict to list for each model + for model_name in updates: + updates[model_name] = list(updates[model_name].values()) + + # Convert deletions set to list for each model + for model_name in deletions: + deletions[model_name] = list(deletions[model_name]) + + # local_time = timezone.localtime(timezone.now()) + # print(local_time.isoformat(timespec='seconds')) + + + date = logs.last().date.astimezone().isoformat(timespec='seconds') if logs else None + print(date) + + response_data = { + "updates": dict(updates), + "deletions": dict(deletions), + "date": date + } + + return Response(response_data, status=status.HTTP_200_OK) class CustomAuthToken(APIView): permission_classes = [] @@ -34,14 +177,15 @@ class CustomAuthToken(APIView): if user is not None: - if user.device_id is None or user.device_id == device_id or user.username == 'apple-test': + # if user.device_id is None or user.device_id == device_id or user.username == 'apple-test': + # if user.device_id is None or user.device_id == device_id or user.username == 'apple-test': user.device_id = device_id user.save() token, created = Token.objects.get_or_create(user=user) return Response({'token': token.key}) - else: - return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN) + # else: + # return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN) else: return Response({'error': 'L\'utilisateur et le mot de passe de correspondent pas'}, status=status.HTTP_401_UNAUTHORIZED) @@ -72,7 +216,7 @@ def user_by_token(request): class UserViewSet(viewsets.ModelViewSet): queryset = CustomUser.objects.all() - serializer_class = UserUpdateSerializer + serializer_class = CustomUserSerializer permission_classes = [] # Users are public whereas the other requests are only for logged users def get_serializer_class(self): diff --git a/asgi.py b/asgi.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/asgi.py @@ -0,0 +1 @@ + diff --git a/padelclub_backend/asgi.py b/padelclub_backend/asgi.py index 4cdfc99..00fb082 100644 --- a/padelclub_backend/asgi.py +++ b/padelclub_backend/asgi.py @@ -9,8 +9,30 @@ https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ import os +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'padelclub_backend.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "padelclub_backend.settings") -application = get_asgi_application() +django_asgi_app = get_asgi_application() + +from tournaments.routing import websocket_urlpatterns + +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + } +) + +# import os + +# from django.core.asgi import get_asgi_application + +# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'padelclub_backend.settings') + +# application = get_asgi_application() diff --git a/padelclub_backend/routing.py b/padelclub_backend/routing.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/padelclub_backend/routing.py @@ -0,0 +1 @@ + diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index deff1f2..2b6f00b 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -32,6 +32,7 @@ ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ + 'daphne', 'tournaments', 'django.contrib.admin', 'django.contrib.auth', @@ -77,6 +78,7 @@ TEMPLATES = [ WSGI_APPLICATION = 'padelclub_backend.wsgi.application' +ASGI_APPLICATION = "padelclub_backend.asgi.application" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases diff --git a/padelclub_backend/settings_app.py b/padelclub_backend/settings_app.py index 54d6d79..8acd72a 100644 --- a/padelclub_backend/settings_app.py +++ b/padelclub_backend/settings_app.py @@ -41,3 +41,9 @@ CACHES = { } QR_CODE_CACHE_ALIAS = 'qr-code' + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer" + } +} diff --git a/requirements.txt b/requirements.txt index c7d4319..d0e8158 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -Django==4.2.11 +Django==5.1 djangorestframework==3.14.0 psycopg2-binary==2.9.9 -dj-rest-auth==5.1.0 +dj-rest-auth==6.0.0 django-qr-code==4.0.1 pycryptodome==3.20.0 requests==2.31.0 PyJWT==2.8.0 httpx[http2]==0.27.0 +channels[daphne]==4.1.0 diff --git a/tournaments/admin.py b/tournaments/admin.py index 90cc714..50d1f19 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from tournaments.models import team_registration from tournaments.models.device_token import DeviceToken -from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log +from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, ModelLog from django.contrib.auth.admin import UserAdmin from django.contrib.auth.forms import UserCreationForm, UserChangeForm @@ -93,6 +93,10 @@ class LogAdmin(admin.ModelAdmin): class DeviceTokenAdmin(admin.ModelAdmin): list_display = ['user', 'value'] +class ModelLogAdmin(admin.ModelAdmin): + list_display = ['user', 'date', 'operation', 'model_id', 'model_name'] + list_filter = ['user'] + admin.site.register(CustomUser, CustomUserAdmin) admin.site.register(Club, ClubAdmin) admin.site.register(Event, EventAdmin) @@ -109,3 +113,4 @@ admin.site.register(DateInterval, DateIntervalAdmin) admin.site.register(FailedApiCall, FailedApiCallAdmin) admin.site.register(Log, LogAdmin) admin.site.register(DeviceToken, DeviceTokenAdmin) +admin.site.register(ModelLog, ModelLogAdmin) diff --git a/tournaments/consumers.py b/tournaments/consumers.py new file mode 100644 index 0000000..6c4fd03 --- /dev/null +++ b/tournaments/consumers.py @@ -0,0 +1,41 @@ +import json + +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + +class ChatConsumer(WebsocketConsumer): + def connect(self): + self.room_name = 'main' + self.room_group_name = f"chat_{self.room_name}" + + # Join room group + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, self.channel_name + ) + + self.accept() + + def disconnect(self, close_code): + # Leave room group + async_to_sync(self.channel_layer.group_discard)( + self.room_group_name, self.channel_name + ) + + # Receive message from WebSocket + def receive(self, text_data): + # text_data_json = json.loads(text_data) + # message = text_data_json["message"] + print(f'received {text_data}') + + # Send message to room group + # chat.message calls the chat_message method + async_to_sync(self.channel_layer.group_send)( + self.room_group_name, {"type": "chat.message", "message": text_data} + ) + + # Receive message from room group + def chat_message(self, event): + message = event["message"] + + # Send message to WebSocket + self.send(text_data=message) diff --git a/tournaments/migrations/0080_modellog.py b/tournaments/migrations/0080_modellog.py new file mode 100644 index 0000000..ffac350 --- /dev/null +++ b/tournaments/migrations/0080_modellog.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1 on 2024-09-12 13:49 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0079_alter_event_creator'), + ] + + operations = [ + migrations.CreateModel( + name='ModelLog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('model_id', models.UUIDField()), + ('operation', models.IntegerField(choices=[(0, 'POST'), (1, 'PUT'), (2, 'DELETE')])), + ('date', models.DateTimeField()), + ('model_name', models.CharField(max_length=50)), + ], + ), + ] diff --git a/tournaments/migrations/0081_alter_modellog_operation.py b/tournaments/migrations/0081_alter_modellog_operation.py new file mode 100644 index 0000000..99624a0 --- /dev/null +++ b/tournaments/migrations/0081_alter_modellog_operation.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-12 15:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0080_modellog'), + ] + + operations = [ + migrations.AlterField( + model_name='modellog', + name='operation', + field=models.IntegerField(choices=[('POST', 'POST'), ('PUT', 'PUT'), ('DELETE', 'DELETE')]), + ), + ] diff --git a/tournaments/migrations/0082_alter_modellog_operation.py b/tournaments/migrations/0082_alter_modellog_operation.py new file mode 100644 index 0000000..a59ead3 --- /dev/null +++ b/tournaments/migrations/0082_alter_modellog_operation.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-12 15:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0081_alter_modellog_operation'), + ] + + operations = [ + migrations.AlterField( + model_name='modellog', + name='operation', + field=models.CharField(choices=[('POST', 'POST'), ('PUT', 'PUT'), ('DELETE', 'DELETE')], max_length=50), + ), + ] diff --git a/tournaments/migrations/0083_modellog_creator.py b/tournaments/migrations/0083_modellog_creator.py new file mode 100644 index 0000000..6acdf2c --- /dev/null +++ b/tournaments/migrations/0083_modellog_creator.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1 on 2024-10-09 08:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0082_alter_modellog_operation'), + ] + + operations = [ + migrations.AddField( + model_name='modellog', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tournaments/migrations/0084_rename_creator_modellog_user.py b/tournaments/migrations/0084_rename_creator_modellog_user.py new file mode 100644 index 0000000..70771c7 --- /dev/null +++ b/tournaments/migrations/0084_rename_creator_modellog_user.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-10-09 08:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0083_modellog_creator'), + ] + + operations = [ + migrations.RenameField( + model_name='modellog', + old_name='creator', + new_name='user', + ), + ] diff --git a/tournaments/migrations/0085_modellog_store_id.py b/tournaments/migrations/0085_modellog_store_id.py new file mode 100644 index 0000000..3c51e7e --- /dev/null +++ b/tournaments/migrations/0085_modellog_store_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-10-09 11:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0084_rename_creator_modellog_user'), + ] + + operations = [ + migrations.AddField( + model_name='modellog', + name='store_id', + field=models.CharField(blank=True, max_length=200, null=True), + ), + ] diff --git a/tournaments/migrations/0086_club_creation_date_club_last_update_and_more.py b/tournaments/migrations/0086_club_creation_date_club_last_update_and_more.py new file mode 100644 index 0000000..b5b6866 --- /dev/null +++ b/tournaments/migrations/0086_club_creation_date_club_last_update_and_more.py @@ -0,0 +1,154 @@ +# Generated by Django 5.1 on 2024-10-15 14:42 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0085_modellog_store_id'), + ] + + operations = [ + migrations.AddField( + model_name='club', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='club', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='court', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='court', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='dateinterval', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='dateinterval', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='devicetoken', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='devicetoken', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='event', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='failedapicall', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='failedapicall', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='groupstage', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='groupstage', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='log', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='log', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='match', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='match', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='playerregistration', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='playerregistration', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='purchase', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='purchase', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='round', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='round', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='teamregistration', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='teamregistration', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='teamscore', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AddField( + model_name='teamscore', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='tournament', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/tournaments/migrations/0087_remove_modellog_store_id.py b/tournaments/migrations/0087_remove_modellog_store_id.py new file mode 100644 index 0000000..29cdf4f --- /dev/null +++ b/tournaments/migrations/0087_remove_modellog_store_id.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1 on 2024-10-16 12:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0086_club_creation_date_club_last_update_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='modellog', + name='store_id', + ), + ] diff --git a/tournaments/migrations/0088_customuser_last_update.py b/tournaments/migrations/0088_customuser_last_update.py new file mode 100644 index 0000000..e561ac2 --- /dev/null +++ b/tournaments/migrations/0088_customuser_last_update.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1 on 2024-10-17 10:36 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0087_remove_modellog_store_id'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='last_update', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/tournaments/migrations/0089_groupstage_store_id_match_store_id_and_more.py b/tournaments/migrations/0089_groupstage_store_id_match_store_id_and_more.py new file mode 100644 index 0000000..adbb556 --- /dev/null +++ b/tournaments/migrations/0089_groupstage_store_id_match_store_id_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1 on 2024-10-17 13:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0088_customuser_last_update'), + ] + + operations = [ + migrations.AddField( + model_name='groupstage', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='match', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='playerregistration', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='round', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='teamregistration', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='teamscore', + name='store_id', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/tournaments/migrations/0090_dynamic_store_id.py b/tournaments/migrations/0090_dynamic_store_id.py new file mode 100644 index 0000000..93240da --- /dev/null +++ b/tournaments/migrations/0090_dynamic_store_id.py @@ -0,0 +1,79 @@ +# Generated by Django 5.1 on 2024-10-17 13:02 + +from ast import Match +from django.db import migrations + +from tournaments.models.player_registration import PlayerRegistration + +def update_group_stage_store_id(apps): + GroupStage = apps.get_model('tournaments', 'GroupStage') + + for group_stage in GroupStage.objects.all(): + group_stage.store_id = str(group_stage.tournament.id) + group_stage.save() + +def update_round_store_id(apps): + Round = apps.get_model('tournaments', 'Round') + + for round in Round.objects.all(): + round.store_id = str(round.tournament.id) + round.save() + +def update_team_registration_store_id(apps): + TeamRegistration = apps.get_model('tournaments', 'TeamRegistration') + + for tr in TeamRegistration.objects.all(): + tr.store_id = str(tr.tournament.id) + tr.save() + +def update_player_registration_store_id(apps): + PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration') + + for pr in PlayerRegistration.objects.all(): + pr.store_id = str(pr.team_registration.tournament.id) + pr.save() + +def update_match_store_id(apps): + Match = apps.get_model('tournaments', 'Match') + + for match in Match.objects.all(): + if match.round: + tournament = match.round.tournament + else: + tournament = match.group_stage.tournament + match.store_id = str(tournament.id) + match.save() + +def update_team_score_store_id(apps): + TeamScore = apps.get_model('tournaments', 'TeamScore') + + for team_score in TeamScore.objects.all(): + tournament = None + if team_score.team_registration: + tournament = team_score.team_registration.tournament + elif team_score.match: + if team_score.match.round: + tournament = team_score.match.round.tournament + else: + tournament = team_score.team_registration.tournament + + team_score.store_id = str(tournament.id) + team_score.save() + +def update_models(apps, schema_editor): + update_group_stage_store_id(apps) + update_round_store_id(apps) + update_team_registration_store_id(apps) + update_player_registration_store_id(apps) + update_match_store_id(apps) + update_team_score_store_id(apps) + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0089_groupstage_store_id_match_store_id_and_more'), + ] + + operations = [ + migrations.RunPython(update_models), + ] diff --git a/tournaments/models/__init__.py b/tournaments/models/__init__.py index ced18b9..3fd8708 100644 --- a/tournaments/models/__init__.py +++ b/tournaments/models/__init__.py @@ -1,8 +1,9 @@ +from .base import BaseModel, SideStoreModel from .custom_user import CustomUser from .club import Club from .court import Court from .date_interval import DateInterval -from .enums import TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory +from .enums import TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory, ModelOperation from .player_enums import PlayerSexType, PlayerDataSource, PlayerPaymentType from .event import Event from .tournament import Tournament, TeamSummon, TeamSortingType, TeamList @@ -16,3 +17,4 @@ from .purchase import Purchase from .failed_api_call import FailedApiCall from .log import Log from .device_token import DeviceToken +from .model_log import ModelLog diff --git a/tournaments/models/base.py b/tournaments/models/base.py new file mode 100644 index 0000000..6c85543 --- /dev/null +++ b/tournaments/models/base.py @@ -0,0 +1,15 @@ +from django.db import models +from django.utils.timezone import now + +class BaseModel(models.Model): + creation_date = models.DateTimeField(default=now, editable=False) + last_update = models.DateTimeField(default=now) + + class Meta: + abstract = True + +class SideStoreModel(BaseModel): + store_id = models.CharField(max_length=100) + + class Meta: + abstract = True diff --git a/tournaments/models/club.py b/tournaments/models/club.py index 0fc4f0d..74321e9 100644 --- a/tournaments/models/club.py +++ b/tournaments/models/club.py @@ -1,7 +1,8 @@ from django.db import models import uuid +from . import BaseModel -class Club(models.Model): +class Club(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) creator = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL) # string to avoid circular import name = models.CharField(max_length=50) diff --git a/tournaments/models/court.py b/tournaments/models/court.py index e8df8a7..eea8dd4 100644 --- a/tournaments/models/court.py +++ b/tournaments/models/court.py @@ -1,8 +1,8 @@ from django.db import models import uuid -from . import Club +from . import BaseModel, Club -class Court(models.Model): +class Court(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) index = models.IntegerField(default=0) club = models.ForeignKey(Club, on_delete=models.CASCADE) diff --git a/tournaments/models/custom_user.py b/tournaments/models/custom_user.py index a05a246..0629b0c 100644 --- a/tournaments/models/custom_user.py +++ b/tournaments/models/custom_user.py @@ -1,11 +1,14 @@ from django.db import models from django.contrib.auth.models import AbstractUser +from django.utils.timezone import now + from . import club, enums import uuid class CustomUser(AbstractUser): pass id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + last_update = models.DateTimeField(default=now) email = models.EmailField(unique=True) umpire_code = models.CharField(max_length=50, blank=True, null=True) clubs = models.ManyToManyField(club.Club, blank=True) @@ -37,7 +40,7 @@ class CustomUser(AbstractUser): def fields_for_update(): # returns the list of fields to update without password - return ['id', 'username', 'email', 'umpire_code', 'clubs', 'phone', 'first_name', 'last_name', + return ['id', 'last_update', 'username', 'email', 'umpire_code', 'clubs', 'phone', 'first_name', 'last_name', 'licence_id', 'country', 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_display_format', 'summons_display_entry_fee', diff --git a/tournaments/models/date_interval.py b/tournaments/models/date_interval.py index 87ef7c0..9763445 100644 --- a/tournaments/models/date_interval.py +++ b/tournaments/models/date_interval.py @@ -1,7 +1,8 @@ from django.db import models import uuid +from . import BaseModel -class DateInterval(models.Model): +class DateInterval(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) event = models.ForeignKey('Event', on_delete=models.CASCADE) court_index = models.IntegerField() diff --git a/tournaments/models/device_token.py b/tournaments/models/device_token.py index 7442d5d..fc5a711 100644 --- a/tournaments/models/device_token.py +++ b/tournaments/models/device_token.py @@ -1,8 +1,9 @@ from django.db import models from . import CustomUser import uuid +from . import BaseModel -class DeviceToken(models.Model): +class DeviceToken(BaseModel): user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) value = models.TextField() diff --git a/tournaments/models/enums.py b/tournaments/models/enums.py index 76a340b..71c0d82 100644 --- a/tournaments/models/enums.py +++ b/tournaments/models/enums.py @@ -56,3 +56,8 @@ class FederalMatchCategory(models.IntegerChoices): return 1 else: return 3 + +class ModelOperation(models.TextChoices): + POST = 'POST', 'POST' + PUT = 'PUT', 'PUT' + DELETE = 'DELETE', 'DELETE' diff --git a/tournaments/models/event.py b/tournaments/models/event.py index bed7bef..2351354 100644 --- a/tournaments/models/event.py +++ b/tournaments/models/event.py @@ -1,8 +1,8 @@ from django.db import models -from . import Club, CustomUser +from . import BaseModel, Club, CustomUser import uuid -class Event(models.Model): +class Event(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) club = models.ForeignKey(Club, on_delete=models.SET_NULL, null=True, blank=True) creation_date = models.DateTimeField() diff --git a/tournaments/models/failed_api_call.py b/tournaments/models/failed_api_call.py index fd53e7a..4ad2e94 100644 --- a/tournaments/models/failed_api_call.py +++ b/tournaments/models/failed_api_call.py @@ -1,8 +1,8 @@ from django.db import models -from . import CustomUser +from . import BaseModel, CustomUser import uuid -class FailedApiCall(models.Model): +class FailedApiCall(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) date = models.DateTimeField() user = models.ForeignKey(CustomUser, blank=True, null=True, on_delete=models.SET_NULL) diff --git a/tournaments/models/group_stage.py b/tournaments/models/group_stage.py index 85415a9..4af2608 100644 --- a/tournaments/models/group_stage.py +++ b/tournaments/models/group_stage.py @@ -1,11 +1,12 @@ from django.db import models -from . import Tournament, FederalMatchCategory + +from . import SideStoreModel, Tournament, FederalMatchCategory import uuid from ..utils.extensions import format_seconds from datetime import datetime, timedelta from django.utils import timezone -class GroupStage(models.Model): +class GroupStage(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) index = models.IntegerField(default=0) @@ -18,6 +19,9 @@ class GroupStage(models.Model): return self.display_name() # return f"{self.tournament.display_name()} - {self.display_name()}" + def tournament_str_id(self): + return str(self.tournament.id) + def display_name(self): if self.name: return self.name diff --git a/tournaments/models/log.py b/tournaments/models/log.py index dec2d94..4ecc756 100644 --- a/tournaments/models/log.py +++ b/tournaments/models/log.py @@ -1,8 +1,8 @@ from django.db import models -from . import CustomUser +from . import BaseModel, CustomUser import uuid -class Log(models.Model): +class Log(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) date = models.DateTimeField() user = models.ForeignKey(CustomUser, blank=True, null=True, on_delete=models.SET_NULL) diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 4e4695d..70bffa6 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -1,13 +1,13 @@ from django.db import models from tournaments.models import group_stage -from . import Round, GroupStage, FederalMatchCategory +from . import SideStoreModel, Round, GroupStage, FederalMatchCategory from django.utils import timezone, formats from datetime import timedelta import uuid from ..utils.extensions import format_seconds -class Match(models.Model): +class Match(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) round = models.ForeignKey(Round, null=True, blank=True, on_delete=models.CASCADE) group_stage = models.ForeignKey(GroupStage, null=True, blank=True, on_delete=models.CASCADE) @@ -36,6 +36,9 @@ class Match(models.Model): else: return self.group_stage.tournament + def tournament_id(self): + return self.tournament().id + def court_name(self, index): club = None if self.tournament().event: diff --git a/tournaments/models/model_log.py b/tournaments/models/model_log.py new file mode 100644 index 0000000..d29a84e --- /dev/null +++ b/tournaments/models/model_log.py @@ -0,0 +1,12 @@ +from django.db import models +import uuid +from . import ModelOperation + +class ModelLog(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) + user = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL) + model_id = models.UUIDField() + operation = models.CharField(choices=ModelOperation.choices, max_length=50) + date = models.DateTimeField() + model_name = models.CharField(max_length=50) + # store_id = models.CharField(max_length=200, blank=True, null=True) diff --git a/tournaments/models/player_registration.py b/tournaments/models/player_registration.py index 560bc54..433c53e 100644 --- a/tournaments/models/player_registration.py +++ b/tournaments/models/player_registration.py @@ -1,8 +1,8 @@ from django.db import models -from . import TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType +from . import SideStoreModel, TeamRegistration, PlayerSexType, PlayerDataSource, PlayerPaymentType import uuid -class PlayerRegistration(models.Model): +class PlayerRegistration(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) team_registration = models.ForeignKey(TeamRegistration, on_delete=models.CASCADE) first_name = models.CharField(max_length=50, blank=True) @@ -37,6 +37,9 @@ class PlayerRegistration(models.Model): def __str__(self): return self.name() + def tournament_id(self): + return self.team_registration.tournament.id + def name(self): return f"{self.first_name} {self.last_name}" diff --git a/tournaments/models/purchase.py b/tournaments/models/purchase.py index 6dc72e4..a1fb4d0 100644 --- a/tournaments/models/purchase.py +++ b/tournaments/models/purchase.py @@ -1,8 +1,8 @@ from django.db import models import uuid -from . import CustomUser +from . import BaseModel, CustomUser -class Purchase(models.Model): +class Purchase(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) identifier = models.BigIntegerField() diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 5797f80..f830239 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -1,8 +1,8 @@ from django.db import models -from . import Tournament, FederalMatchCategory +from . import SideStoreModel, Tournament, FederalMatchCategory import uuid -class Round(models.Model): +class Round(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) index = models.IntegerField(default=0) @@ -20,6 +20,9 @@ class Round(models.Model): # else: # return f"{self.tournament.display_name()} - {self.name()}" + def tournament_id(self): + return self.tournament.id + def name(self): if self.parent: return "Matchs de classement" diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index 7736713..5d93414 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -1,10 +1,10 @@ from django.db import models from django.db.models.sql.query import Q -from . import Tournament, GroupStage, Match +from . import SideStoreModel, Tournament, GroupStage, Match import uuid from django.utils import timezone -class TeamRegistration(models.Model): +class TeamRegistration(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) group_stage = models.ForeignKey(GroupStage, null=True, blank=True, on_delete=models.SET_NULL) @@ -37,6 +37,9 @@ class TeamRegistration(models.Model): # return f"{self.name}: {self.player_names()}" return self.player_names() + def tournament_id(self): + return self.tournament.id + def team_names(self): if self.name: return [self.name] diff --git a/tournaments/models/team_score.py b/tournaments/models/team_score.py index 41852f0..4dbd00b 100644 --- a/tournaments/models/team_score.py +++ b/tournaments/models/team_score.py @@ -1,8 +1,8 @@ from django.db import models -from . import Match, TeamRegistration, PlayerRegistration, FederalMatchCategory +from . import SideStoreModel, Match, TeamRegistration, PlayerRegistration, FederalMatchCategory import uuid -class TeamScore(models.Model): +class TeamScore(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) match = models.ForeignKey(Match, on_delete=models.CASCADE, related_name="team_scores") team_registration = models.ForeignKey(TeamRegistration, on_delete=models.CASCADE, null=True, blank=True) @@ -13,6 +13,17 @@ class TeamScore(models.Model): def __str__(self): return f"{self.match.stage_name()} #{self.match.index}: {self.player_names()}" + def tournament(self): + if self.team_registration: + return self.team_registration.tournament + elif self.match: + return self.match.tournament() + else: + return None + + def tournament_id(self): + return self.tournament().id + def player_names(self): if self.team_registration: if self.team_registration.name: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index ba76ed3..ba2fffc 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from tournaments.models import group_stage -from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory +from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory import uuid from django.utils import timezone, formats from datetime import datetime, timedelta @@ -15,7 +15,7 @@ class TeamSortingType(models.IntegerChoices): RANK = 1, 'Rank' INSCRIPTION_DATE = 2, 'Inscription Date' -class Tournament(models.Model): +class Tournament(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE) name = models.CharField(max_length=200, null=True, blank=True) diff --git a/tournaments/routing.py b/tournaments/routing.py new file mode 100644 index 0000000..b644e48 --- /dev/null +++ b/tournaments/routing.py @@ -0,0 +1,9 @@ +# chat/routing.py +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()), + # re_path(r"ws/chat/(?P\w+)/$", consumers.ChatConsumer.as_asgi()), +] diff --git a/tournaments/static/tournaments/ja.html b/tournaments/static/tournaments/ja.html new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tournaments/static/tournaments/ja.html @@ -0,0 +1 @@ +