First working occurence of sync and websockets

sync
Laurent 12 months ago
parent 2d2e17eb70
commit 41ba44df98
  1. 227
      api/sync.py
  2. 4
      api/urls.py
  3. 15
      api/utils.py
  4. 261
      api/views.py
  5. 8
      tournaments/admin.py
  6. 38
      tournaments/consumers.py
  7. 190
      tournaments/migrations/0097_club_last_updated_by_court_last_updated_by_and_more.py
  8. 108
      tournaments/migrations/0098_remove_club_last_updated_by_and_more.py
  9. 19
      tournaments/migrations/0099_dataaccess_last_hierarchy_update.py
  10. 83
      tournaments/models/base.py
  11. 2
      tournaments/models/club.py
  12. 70
      tournaments/models/data_access.py
  13. 4
      tournaments/models/date_interval.py
  14. 2
      tournaments/models/device_token.py
  15. 4
      tournaments/models/event.py
  16. 4
      tournaments/models/group_stage.py
  17. 4
      tournaments/models/match.py
  18. 4
      tournaments/models/model_log.py
  19. 4
      tournaments/models/player_registration.py
  20. 4
      tournaments/models/purchase.py
  21. 4
      tournaments/models/round.py
  22. 8
      tournaments/models/team_score.py
  23. 68
      tournaments/models/tournament.py
  24. 1
      tournaments/routing.py
  25. 111
      tournaments/signals.py

@ -0,0 +1,227 @@
from django.db.models.fields.related_lookups import RelatedExact
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.apps import apps
from django.utils import timezone
from django.db.models import Q
from collections import defaultdict
from .utils import build_serializer_class, get_data, get_serialized_data
from tournaments.models import ModelLog, DataAccess, BaseModel
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')
store_id = request.data.get('store_id')
print(f"DataApi post > {model_operation} {model_name}")
serializer_class = build_serializer_class(model_name)
model = apps.get_model(app_label='tournaments', model_name=model_name)
now = timezone.localtime(timezone.now())
try:
data_id = data.get('id')
# instance = model.objects.get(id=data_id)
instance = get_data('tournaments', model_name, data_id)
# update the possible data access objects with the current date
if model_operation == 'DELETE':
parent_model, parent_id = instance.get_parent_reference()
return self.delete_and_save_log(request, data_id, model_operation, model_name, store_id, now)
else: # 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, store_id, now)
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: # POST
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, store_id, now)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def save_and_create_log(self, request, serializer, model_operation, model_name, store_id, date):
instance = serializer.save()
self.create_and_save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
model_id=instance.id,
store_id=store_id,
date=date
)
self.update_linked_data_access(instance, date)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def delete_and_save_log(self, request, data_id, model_operation, model_name, store_id, date):
instance = get_data('tournaments', model_name, data_id)
self.update_linked_data_access(instance, date)
instance.delete()
# we delete all previous logs linked to the instance because they won't be needed anymore
ModelLog.objects.filter(model_id=data_id).delete()
self.create_and_save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
model_id=data_id,
store_id=store_id,
date=date
)
return Response(status=status.HTTP_204_NO_CONTENT)
def update_linked_data_access(self, instance, date):
related_instances = instance.related_instances()
related_ids = [ri.id for ri in instance.related_instances()]
related_ids.append(instance.id)
data_access_list = DataAccess.objects.filter(model_id__in=related_ids)
for data_access in data_access_list:
data_access.last_hierarchy_update = date
data_access.save()
def create_and_save_model_log(self, user, model_operation, model_name, model_id, store_id, date):
model_log = ModelLog()
model_log.user = user
model_log.operation = model_operation
model_log.date = date
model_log.model_name = model_name
model_log.model_id = model_id
model_log.store_id = store_id
model_log.save()
def get(self, request, *args, **kwargs):
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(f'/data GET: {last_update}')
logs = self.query_model_logs(last_update, request.user)
updates = defaultdict(dict)
deletions = defaultdict(list)
print(f'>>> log count = {len(logs)}')
for log in logs:
if log.operation in ['POST', 'PUT']:
data = get_serialized_data('tournaments', log.model_name, log.model_id)
updates[log.model_name][log.model_id] = data
elif log.operation == 'DELETE':
deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_id})
elif log.operation == 'GRANT_ACCESS':
model = apps.get_model('tournaments', model_name=log.model_name)
instance = model.objects.get(id=log.model_id)
serializer_class = build_serializer_class(log.model_name)
serializer = serializer_class(instance)
# data = get_serialized_data('tournaments', log.model_name, log.model_id)
updates[log.model_name][log.model_id] = serializer.data
# instance = model.objects.get(id=log.model_id)
self.add_children_recursively(instance, updates)
self.add_parents_recursively(instance, updates)
elif log.operation == 'delete data access signal':
print(f'revoke access {log.model_id} - {log.store_id}')
deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_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] = deletions[model_name]
response_data = {
"updates": dict(updates),
"deletions": dict(deletions)
}
return Response(response_data, status=status.HTTP_200_OK)
def query_model_logs(self, last_update, user):
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)
# get recently modified DataAccess
data_access_query = Q(last_hierarchy_update__gt=last_update) & (Q(shared_with__in=[user]) | Q(owner=user))
data_access_list = DataAccess.objects.filter(data_access_query) #.values_list('model_id', flat=True)
print(f'>> da count = {len(data_access_list)}')
# get ids of all recently updated related instances of each shared data
model_ids = []
for data_access in data_access_list:
instance = get_data('tournaments', data_access.model_name, data_access.model_id)
related_instances = instance.related_instances()
related_ids = [ri.id for ri in instance.related_instances() if ri.last_update > last_update]
model_ids.extend(related_ids)
model_ids.append(instance.id)
# get all ModelLog list since the last_update, from the user and from the data he has access to
log_query = Q(date__gt=last_update) & (Q(user=user) | Q(model_id__in=model_ids))
return ModelLog.objects.filter(log_query).order_by('date')
def add_children_recursively(self, instance, updates):
"""
Recursively add all children of an instance to the updates dictionary.
"""
# print(f"Instance class: {instance.__class__}")
child_models = instance.get_children_by_model()
for child_model_name, children in child_models.items():
for child in children:
if isinstance(child, BaseModel):
serializer_class = build_serializer_class(child_model_name)
serializer = serializer_class(child)
updates[child_model_name][child.id] = serializer.data
self.add_children_recursively(child, updates)
def add_parents_recursively(self, instance, updates):
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
# print(f'parent = {parent_model_name}')
if isinstance(parent, BaseModel):
serializer_class = build_serializer_class(parent_model_name)
serializer = serializer_class(parent)
updates[parent_model_name][parent.id] = serializer.data
self.add_parents_recursively(parent, updates)
# def get_data(self, model, log):
# instance = model.objects.get(id=log.model_id)
# serializer_class = build_serializer_class(log.model_name)
# serializer = serializer_class(instance)
# return serializer.data

@ -2,7 +2,7 @@ from django.urls import include, path
from rest_framework import routers from rest_framework import routers
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
from . import views from . import views, sync
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet) router.register(r'users', views.UserViewSet)
@ -26,7 +26,7 @@ router.register(r'data-access', views.DataAccessViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('data/', views.DataApi.as_view(), name="data"), path('data/', sync.DataApi.as_view(), name="data"),
path('api-token-auth/', obtain_auth_token, name='api_token_auth'), path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
path("user-by-token/", views.user_by_token, name="user_by_token"), path("user-by-token/", views.user_by_token, name="user_by_token"),

@ -1,5 +1,6 @@
import re import re
import importlib import importlib
from django.apps import apps
def is_valid_email(email): def is_valid_email(email):
email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
@ -21,3 +22,17 @@ def build_serializer_class(source):
module = importlib.import_module('api.serializers') module = importlib.import_module('api.serializers')
return getattr(module, transformed_string) return getattr(module, transformed_string)
def get_data(app_label, model_name, model_id):
model = apps.get_model(app_label=app_label, model_name=model_name)
return model.objects.get(id=model_id)
# serializer_class = build_serializer_class(model_name)
# serializer = serializer_class(instance)
# return serializer.data
def get_serialized_data(app_label, model_name, model_id):
model = apps.get_model(app_label=app_label, model_name=model_name)
instance = model.objects.get(id=model_id)
serializer_class = build_serializer_class(model_name)
serializer = serializer_class(instance)
return serializer.data

@ -1,5 +1,5 @@
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, CustomUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, DataAccessSerializer from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, CustomUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, DataAccessSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, ModelLog, DataAccess, BaseModel from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DataAccess
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
@ -22,197 +22,6 @@ from collections import defaultdict
from .permissions import IsClubOwner from .permissions import IsClubOwner
from .utils import is_valid_email, build_serializer_class 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')
store_id = request.data.get('store_id')
print(f"DataApi post > {model_operation} {model_name}")
serializer_class = build_serializer_class(model_name)
model = apps.get_model(app_label='tournaments', model_name=model_name)
try:
data_id = data.get('id')
instance = model.objects.get(id=data_id)
if model_operation == 'DELETE':
parent_model, parent_id = instance.get_parent_reference()
return self.delete_and_save_log(request, data_id, model_operation, model_name, store_id, parent_id, parent_model)
else: # 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, store_id)
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: # POST
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, store_id)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def save_and_create_log(self, request, serializer, model_operation, model_name, store_id):
instance = serializer.save()
parent_model, parent_id = instance.get_parent_reference()
self.create_and_save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
model_id=instance.id,
store_id=store_id,
parent_id=parent_id,
parent_model=parent_model
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def delete_and_save_log(self, request, data_id, model_operation, model_name, store_id, parent_id, parent_model):
model = apps.get_model(app_label='tournaments', model_name=model_name)
print(model)
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=data_id).delete()
self.create_and_save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
model_id=data_id,
store_id=store_id,
parent_id=parent_id,
parent_model=parent_model
)
return Response(status=status.HTTP_204_NO_CONTENT)
def create_and_save_model_log(self, user, model_operation, model_name, model_id, store_id, parent_id, parent_model):
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.store_id = store_id
model_log.parent_model_id = parent_id
model_log.parent_model_name = parent_model
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)
data_access_query = Q(shared_with=request.user) | Q(owner=request.user)
data_access = DataAccess.objects.filter(data_access_query).values_list('model_id', flat=True)
log_query = Q(date__gt=last_update) & (Q(user=request.user) | Q(model_id__in=data_access) | Q(parent_model_id__in=data_access))
logs = ModelLog.objects.filter(log_query).order_by('date')
updates = defaultdict(dict)
deletions = defaultdict(list)
print(f'>>> log count = {len(logs)}')
for log in logs:
model = apps.get_model(app_label='tournaments', model_name=log.model_name)
if log.operation in ['POST', 'PUT']:
try:
data = self.get_data(model, log)
updates[log.model_name][log.model_id] = 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].append({'model_id': log.model_id, 'store_id': log.store_id})
elif log.operation == 'GRANT_ACCESS':
data = self.get_data(model, log)
updates[log.model_name][log.model_id] = data
instance = model.objects.get(id=log.model_id)
self.add_children_recursively(instance, updates)
self.add_parents_recursively(instance, updates)
elif log.operation == 'REVOKE_ACCESS':
print(f'revoke access {log.model_id} - {log.store_id}')
# data = self.get_data(model, log)
deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_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] = deletions[model_name]
date = logs.last().date.astimezone().isoformat(timespec='seconds') if logs else None
response_data = {
"updates": dict(updates),
"deletions": dict(deletions),
"date": date
}
return Response(response_data, status=status.HTTP_200_OK)
def add_children_recursively(self, instance, updates):
"""
Recursively add all children of an instance to the updates dictionary.
"""
child_models = instance.get_children_by_model()
for child_model_name, children in child_models.items():
for child in children:
if isinstance(child, BaseModel):
serializer_class = build_serializer_class(child_model_name)
serializer = serializer_class(child)
updates[child_model_name][child.id] = serializer.data
self.add_children_recursively(child, updates)
def add_parents_recursively(self, instance, updates):
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
# print(f'parent = {parent_model_name}')
if isinstance(parent, BaseModel):
serializer_class = build_serializer_class(parent_model_name)
serializer = serializer_class(parent)
updates[parent_model_name][parent.id] = serializer.data
self.add_parents_recursively(parent, updates)
def get_data(self, model, log):
instance = model.objects.get(id=log.model_id)
serializer_class = build_serializer_class(log.model_name)
serializer = serializer_class(instance)
return serializer.data
class CustomAuthToken(APIView): class CustomAuthToken(APIView):
permission_classes = [] permission_classes = []
@ -261,6 +70,20 @@ class Logout(APIView):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)
@api_view(['GET']) @api_view(['GET'])
def user_by_token(request): def user_by_token(request):
serializer = UserSerializer(request.user) serializer = UserSerializer(request.user)
@ -283,8 +106,25 @@ class ClubViewSet(viewsets.ModelViewSet):
permission_classes = [IsClubOwner] # Clubs are public whereas the other requests are only for logged users permission_classes = [IsClubOwner] # Clubs are public whereas the other requests are only for logged users
def perform_create(self, serializer): def perform_create(self, serializer):
super.perform_create()
serializer.save(creator=self.request.user) serializer.save(creator=self.request.user)
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
# return self.queryset.filter(creator=self.request.user)
return self.queryset.filter(
Q(creator=self.request.user) |
Q(id__in=DataAccess.objects.filter(
shared_with=self.request.user,
model_name=self.queryset.model.__name__
).values_list('model_id', flat=True))
)
class TournamentViewSet(viewsets.ModelViewSet): class TournamentViewSet(viewsets.ModelViewSet):
queryset = Tournament.objects.all() queryset = Tournament.objects.all()
serializer_class = TournamentSerializer serializer_class = TournamentSerializer
@ -318,49 +158,12 @@ class PurchaseViewSet(viewsets.ModelViewSet):
).values_list('model_id', flat=True)) ).values_list('model_id', flat=True))
) )
def put(self, request, pk):
raise MethodNotAllowed('PUT')
def patch(self, request, pk): def patch(self, request, pk):
raise MethodNotAllowed('PATCH') raise MethodNotAllowed('PATCH')
def delete(self, request, pk): def delete(self, request, pk):
raise MethodNotAllowed('DELETE') raise MethodNotAllowed('DELETE')
# class ExpandedTournamentViewSet(viewsets.ModelViewSet):
# queryset = Tournament.objects.all()
# serializer_class = ExpandedTournamentSerializer
class ChangePasswordView(UpdateAPIView):
serializer_class = ChangePasswordSerializer
def update(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# if using drf authtoken, create a new token
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token, created = Token.objects.get_or_create(user=user)
# return new token
return Response({'token': token.key}, status=status.HTTP_200_OK)
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
def get_queryset(self):
if self.request.user.is_anonymous:
return []
# return self.queryset.filter(creator=self.request.user)
return self.queryset.filter(
Q(creator=self.request.user) |
Q(id__in=DataAccess.objects.filter(
shared_with=self.request.user,
model_name=self.queryset.model.__name__
).values_list('model_id', flat=True))
)
class RoundViewSet(viewsets.ModelViewSet): class RoundViewSet(viewsets.ModelViewSet):
queryset = Round.objects.all() queryset = Round.objects.all()
serializer_class = RoundSerializer serializer_class = RoundSerializer

@ -100,9 +100,13 @@ class ModelLogAdmin(admin.ModelAdmin):
ordering = ['-date'] ordering = ['-date']
class DataAccessAdmin(admin.ModelAdmin): class DataAccessAdmin(admin.ModelAdmin):
list_display = ['owner', 'shared_with', 'model_name', 'model_id', 'granted_at'] list_display = ['owner', 'get_shared_users', 'model_name', 'model_id', 'last_hierarchy_update']
list_filter = ['owner', 'shared_with'] list_filter = ['owner', 'shared_with']
ordering = ['-granted_at'] ordering = ['-last_hierarchy_update']
@admin.display(description='Shared with')
def get_shared_users(self, obj):
return ", ".join([str(item) for item in obj.shared_with.all()])
admin.site.register(CustomUser, CustomUserAdmin) admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Club, ClubAdmin) admin.site.register(Club, ClubAdmin)

@ -3,6 +3,44 @@ import json
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
class UserConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["user_id"]
self.room_group_name = f"sync_{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, data):
# text_data_json = json.loads(text_data)
# message = text_data_json["message"]
print(f'received {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": "sync.update", "message": "hello"} # sync.update calls the method below
)
# Receive message from room group
def sync_update(self, event):
message = event["message"]
# Send message to WebSocket
self.send(text_data=message)
class ChatConsumer(WebsocketConsumer): class ChatConsumer(WebsocketConsumer):
def connect(self): def connect(self):
self.room_name = 'main' self.room_name = 'main'

@ -0,0 +1,190 @@
# Generated by Django 5.1 on 2024-11-12 16:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0096_alter_modellog_operation_and_more'),
]
operations = [
migrations.AddField(
model_name='club',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='court',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='dateinterval',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='devicetoken',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='event',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='failedapicall',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='groupstage',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='log',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='match',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='playerregistration',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='purchase',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='round',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='teamregistration',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='teamscore',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='tournament',
name='last_updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='club',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='creator_clubs', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='court',
name='club',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courts', to='tournaments.club'),
),
migrations.AlterField(
model_name='customuser',
name='clubs',
field=models.ManyToManyField(blank=True, related_name='creators', to='tournaments.club'),
),
migrations.AlterField(
model_name='dateinterval',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='date_intervals', to='tournaments.event'),
),
migrations.AlterField(
model_name='devicetoken',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_tokens', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='event',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to='tournaments.club'),
),
migrations.AlterField(
model_name='event',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='failedapicall',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='failed_api_calls', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='groupstage',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_stages', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='log',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='match',
name='group_stage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='tournaments.groupstage'),
),
migrations.AlterField(
model_name='match',
name='round',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='tournaments.round'),
),
migrations.AlterField(
model_name='modellog',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='model_logs', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='playerregistration',
name='team_registration',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='player_registrations', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='purchase',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchases', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='round',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rounds', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamregistration',
name='group_stage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_registrations', to='tournaments.groupstage'),
),
migrations.AlterField(
model_name='teamregistration',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_registrations', to='tournaments.tournament'),
),
migrations.AlterField(
model_name='teamscore',
name='team_registration',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_scores', to='tournaments.teamregistration'),
),
migrations.AlterField(
model_name='tournament',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tournaments', to='tournaments.event'),
),
]

@ -0,0 +1,108 @@
# Generated by Django 5.1 on 2024-11-15 14:45
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0097_club_last_updated_by_court_last_updated_by_and_more'),
]
operations = [
migrations.RemoveField(
model_name='club',
name='last_updated_by',
),
migrations.RemoveField(
model_name='court',
name='last_updated_by',
),
migrations.RemoveField(
model_name='dateinterval',
name='last_updated_by',
),
migrations.RemoveField(
model_name='devicetoken',
name='last_updated_by',
),
migrations.RemoveField(
model_name='event',
name='last_updated_by',
),
migrations.RemoveField(
model_name='failedapicall',
name='last_updated_by',
),
migrations.RemoveField(
model_name='groupstage',
name='last_updated_by',
),
migrations.RemoveField(
model_name='log',
name='last_updated_by',
),
migrations.RemoveField(
model_name='match',
name='last_updated_by',
),
migrations.RemoveField(
model_name='modellog',
name='parent_model_id',
),
migrations.RemoveField(
model_name='modellog',
name='parent_model_name',
),
migrations.RemoveField(
model_name='playerregistration',
name='last_updated_by',
),
migrations.RemoveField(
model_name='purchase',
name='last_updated_by',
),
migrations.RemoveField(
model_name='round',
name='last_updated_by',
),
migrations.RemoveField(
model_name='teamregistration',
name='last_updated_by',
),
migrations.RemoveField(
model_name='teamscore',
name='last_updated_by',
),
migrations.RemoveField(
model_name='tournament',
name='last_updated_by',
),
migrations.AddField(
model_name='dataaccess',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AddField(
model_name='dataaccess',
name='last_update',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.RemoveField(
model_name='dataaccess',
name='shared_with',
),
migrations.AlterField(
model_name='tournament',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tournaments.event'),
),
migrations.AddField(
model_name='dataaccess',
name='shared_with',
field=models.ManyToManyField(related_name='shared_data', to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-11-18 12:54
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0098_remove_club_last_updated_by_and_more'),
]
operations = [
migrations.AddField(
model_name='dataaccess',
name='last_hierarchy_update',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

@ -1,15 +1,20 @@
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from typing import List, Set
class BaseModel(models.Model): class BaseModel(models.Model):
creation_date = models.DateTimeField(default=now, editable=False) creation_date = models.DateTimeField(default=now, editable=False)
last_update = models.DateTimeField(default=now) last_update = models.DateTimeField(default=now)
# last_updated_by = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL)
class Meta: class Meta:
abstract = True abstract = True
def get_owner(self):
return None
def get_parent_reference(self): def get_parent_reference(self):
"""Override in child models to provide parent reference""" """Return a tuple: model_name, model_id"""
return None, None return None, None
def get_children_by_model(self): def get_children_by_model(self):
@ -24,6 +29,7 @@ class BaseModel(models.Model):
for child in children: for child in children:
if (child.one_to_many or child.one_to_one) and child.auto_created: if (child.one_to_many or child.one_to_one) and child.auto_created:
model_name = child.related_model.__name__ model_name = child.related_model.__name__
# print(f'>>> add children for {model_name}')
related_objects[model_name] = getattr(self, child.name).all() related_objects[model_name] = getattr(self, child.name).all()
return related_objects return related_objects
@ -57,6 +63,81 @@ class BaseModel(models.Model):
return parents return parents
def get_recursive_children(self, processed_objects: Set = None) -> List:
"""
Recursively get all children objects through the hierarchy
"""
if processed_objects is None:
processed_objects = set()
# Skip if we've already processed this object to avoid infinite recursion
if self.pk in processed_objects:
return []
processed_objects.add(self.pk)
children = []
# Get immediate children
children_by_model = self.get_children_by_model()
for queryset in children_by_model.values():
for child in queryset:
children.append(child)
# Recursively get children of children
if isinstance(child, BaseModel):
children.extend(child.get_recursive_children(processed_objects))
return children
def get_recursive_parents(self, processed_objects: Set = None) -> List:
"""
Recursively get all parent objects through the hierarchy
"""
if processed_objects is None:
processed_objects = set()
# Skip if we've already processed this object to avoid infinite recursion
if self.pk in processed_objects:
return []
processed_objects.add(self.pk)
parents = []
# Get immediate parents
parents_by_model = self.get_parents_by_model()
for parent in parents_by_model.values():
parents.append(parent)
# Recursively get parents of parents
if isinstance(parent, BaseModel):
parents.extend(parent.get_recursive_parents(processed_objects))
return parents
def related_instances(self):
"""
Get all related instances (both children and parents) recursively
"""
instances = []
processed_objects = set()
instances.extend(self.get_recursive_children(processed_objects))
processed_objects = set()
instances.extend(self.get_recursive_parents(processed_objects))
return instances
# def related_instances(self):
# instances = []
# children_by_model = self.get_children_by_model()
# all_children = [item for sublist in children_by_model.values() for item in sublist]
# instances.extend(all_children)
# parents_by_model = self.get_parents_by_model()
# instances.extend(parents_by_model.values())
# return instances
class SideStoreModel(BaseModel): class SideStoreModel(BaseModel):
store_id = models.CharField(max_length=100) store_id = models.CharField(max_length=100)

@ -4,7 +4,7 @@ from . import BaseModel
class Club(BaseModel): class Club(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
creator = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL) creator = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL, related_name='creator_clubs')
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
acronym = models.CharField(max_length=10) acronym = models.CharField(max_length=10)
phone = models.CharField(max_length=15, null=True, blank=True) phone = models.CharField(max_length=15, null=True, blank=True)

@ -3,45 +3,77 @@ from django.utils import timezone
from django.apps import apps from django.apps import apps
import uuid import uuid
from . import ModelLog, SideStoreModel from . import ModelLog, SideStoreModel, BaseModel
class DataAccess(models.Model): class DataAccess(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4) id = models.UUIDField(primary_key=True, default=uuid.uuid4)
owner = models.ForeignKey('CustomUser', related_name='owned_data', on_delete=models.CASCADE) owner = models.ForeignKey('CustomUser', related_name='owned_data', on_delete=models.CASCADE)
shared_with = models.ForeignKey('CustomUser', related_name='shared_data', on_delete=models.CASCADE) shared_with = models.ManyToManyField('CustomUser', related_name='shared_data')
model_name = models.CharField(max_length=50) model_name = models.CharField(max_length=50)
model_id = models.UUIDField() model_id = models.UUIDField()
granted_at = models.DateTimeField(auto_now_add=True) granted_at = models.DateTimeField(auto_now_add=True)
last_hierarchy_update = models.DateTimeField(default=timezone.now)
def save(self, *args, **kwargs): # def save(self, *args, **kwargs):
is_new = self._state.adding # Check if this is a new DataAccess # is_new = self._state.adding
super().save(*args, **kwargs)
if is_new: # print('>>> save DA')
self.create_initial_log() # if not is_new:
# # Store old shared_with users before save
# old_instance = DataAccess.objects.get(pk=self.pk)
# self._old_shared_with = set(old_instance.shared_with.all())
# super().save(*args, **kwargs)
# if is_new:
# # For new instances, create logs for all shared users
# self.create_access_logs('GRANT_ACCESS')
# else:
# # For updates, handle differences
# new_shared_with = set(self.shared_with.all())
# # Users that were added
# added_users = new_shared_with - self._old_shared_with
# for user in added_users:
# self.create_access_log(user, 'GRANT_ACCESS')
# # Users that were removed
# removed_users = self._old_shared_with - new_shared_with
# for user in removed_users:
# self.create_access_log(user, 'REVOKE_ACCESS')
# def delete(self, *args, **kwargs): # def delete(self, *args, **kwargs):
# print('>>> delete data access') # # Store users before deletion
# # Create deletion sync logs before deleting the access # users_to_revoke = list(self.shared_with.all())
# self.create_deletion_log()
# return super().delete(*args, **kwargs) # # First delete the instance
# super().delete(*args, **kwargs)
# # Then create revoke logs for all users
# for user in users_to_revoke:
# self.create_access_log(user, 'REVOKE_ACCESS')
# def create_access_logs(self, operation):
# """Create logs for all shared users"""
# users = self.shared_with.all()
# print(f'>>> create logs for users = {len(users)}')
def create_initial_log(self): # for user in self.shared_with.all():
# self.create_access_log(user, operation)
def create_access_log(self, user, operation):
"""Create a single access log for a specific user"""
model_class = apps.get_model('tournaments', self.model_name) model_class = apps.get_model('tournaments', self.model_name)
obj = model_class.objects.get(id=self.model_id) obj = model_class.objects.get(id=self.model_id)
parent_model, parent_id = obj.get_parent_reference()
store_id = None store_id = None
if isinstance(obj, SideStoreModel): if isinstance(obj, SideStoreModel):
store_id = obj.store_id store_id = obj.store_id
ModelLog.objects.create( ModelLog.objects.create(
user=self.shared_with, # The user receiving access user=user,
model_id=self.model_id, model_id=self.model_id,
model_name=self.model_name, model_name=self.model_name,
operation='GRANT_ACCESS', operation=operation,
date=timezone.now(), date=timezone.now(),
store_id=store_id, store_id=store_id
parent_model_id=parent_id,
parent_model_name=parent_model
) )

@ -9,6 +9,10 @@ class DateInterval(BaseModel):
start_date = models.DateTimeField() start_date = models.DateTimeField()
end_date = models.DateTimeField() end_date = models.DateTimeField()
# Required for sync web sockets update
def get_owner(self):
return self.event.creator
# Data Access # Data Access
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.event.id return 'Event', self.event.id

@ -4,7 +4,7 @@ import uuid
from . import BaseModel from . import BaseModel
class DeviceToken(BaseModel): class DeviceToken(BaseModel):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='device_tokens')
value = models.TextField() value = models.TextField()
def __str__(self): def __str__(self):

@ -15,6 +15,10 @@ class Event(BaseModel):
def __str__(self): def __str__(self):
return self.display_name() return self.display_name()
# Required for sync web sockets update
def get_owner(self):
return self.creator
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.creator: if self.creator:
self.creator_full_name = self.creator.full_name() self.creator_full_name = self.creator.full_name()

@ -19,6 +19,10 @@ class GroupStage(SideStoreModel):
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.tournament.event.id return 'Event', self.tournament.event.id
# Required for sync web sockets update
def get_owner(self):
return self.tournament.event.creator
def __str__(self): def __str__(self):
return self.display_name() return self.display_name()
# return f"{self.tournament.display_name()} - {self.display_name()}" # return f"{self.tournament.display_name()} - {self.display_name()}"

@ -30,6 +30,10 @@ class Match(SideStoreModel):
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.tournament().event.id return 'Event', self.tournament().event.id
# Required for sync web sockets update
def get_owner(self):
return self.tournament().event.creator
def __str__(self): def __str__(self):
names = " / ".join(self.player_names()) names = " / ".join(self.player_names())
return f"{self.stage_name()} #{self.index}: {names}" return f"{self.stage_name()} #{self.index}: {names}"

@ -10,5 +10,5 @@ class ModelLog(models.Model):
date = models.DateTimeField() date = models.DateTimeField()
model_name = models.CharField(max_length=50) model_name = models.CharField(max_length=50)
store_id = models.CharField(max_length=200, blank=True, null=True) store_id = models.CharField(max_length=200, blank=True, null=True)
parent_model_id = models.UUIDField(blank=True, null=True) # parent_model_id = models.UUIDField(blank=True, null=True)
parent_model_name = models.CharField(max_length=50, blank=True, null=True) # parent_model_name = models.CharField(max_length=50, blank=True, null=True)

@ -38,6 +38,10 @@ class PlayerRegistration(SideStoreModel):
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.team_registration.tournament.event.id return 'Event', self.team_registration.tournament.event.id
# Required for sync web sockets update
def get_owner(self):
return self.team_registration.tournament.event.creator
def __str__(self): def __str__(self):
return self.name() return self.name()

@ -13,3 +13,7 @@ class Purchase(BaseModel):
def __str__(self): def __str__(self):
return f"{self.identifier} > {self.product_id} - {self.purchase_date} - {self.user.username}" return f"{self.identifier} > {self.product_id} - {self.purchase_date} - {self.user.username}"
# Required for sync web sockets update
def get_owner(self):
return self.user

@ -14,6 +14,10 @@ class Round(SideStoreModel):
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.tournament.event.id return 'Event', self.tournament.event.id
# Required for sync web sockets update
def get_owner(self):
return self.tournament.event.creator
def __str__(self): def __str__(self):
if self.parent: if self.parent:
return f"LB: {self.name()}" return f"LB: {self.name()}"

@ -13,6 +13,14 @@ class TeamScore(SideStoreModel):
def __str__(self): def __str__(self):
return f"{self.match.stage_name()} #{self.match.index}: {self.player_names()}" return f"{self.match.stage_name()} #{self.match.index}: {self.player_names()}"
# Data Access
def get_parent_reference(self):
return 'Event', self.tournament().event.id
# Required for sync web sockets update
def get_owner(self):
return self.tournament().event.creator
def tournament(self): def tournament(self):
if self.team_registration: if self.team_registration:
return self.team_registration.tournament return self.team_registration.tournament

@ -17,7 +17,7 @@ class TeamSortingType(models.IntegerChoices):
class Tournament(BaseModel): class Tournament(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE, related_name='tournaments') event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE, related_name="events")
name = models.CharField(max_length=200, null=True, blank=True) name = models.CharField(max_length=200, null=True, blank=True)
start_date = models.DateTimeField() start_date = models.DateTimeField()
end_date = models.DateTimeField(null=True, blank=True) end_date = models.DateTimeField(null=True, blank=True)
@ -60,9 +60,17 @@ class Tournament(BaseModel):
hide_points_earned = models.BooleanField(default=False) hide_points_earned = models.BooleanField(default=False)
publish_rankings = models.BooleanField(default=False) publish_rankings = models.BooleanField(default=False)
# Data Access
def get_parent_reference(self): def get_parent_reference(self):
return 'Event', self.event.id return 'Event', self.event.id
def get_child_models(self):
return {
'Round': 'round_set',
'GroupStage': 'groupstage_set',
'TeamRegistration': 'teamregistration_set'
}
def __str__(self): def __str__(self):
if self.name: if self.name:
return self.name return self.name
@ -196,7 +204,7 @@ class Tournament(BaseModel):
def team_summons(self): def team_summons(self):
summons = [] summons = []
for team_registration in self.team_registrations.all(): for team_registration in self.teamregistration_set.all():
if team_registration.is_valid_for_summon(): if team_registration.is_valid_for_summon():
next_match = team_registration.next_match() next_match = team_registration.next_match()
if next_match and next_match.start_date is not None: if next_match and next_match.start_date is not None:
@ -211,7 +219,7 @@ class Tournament(BaseModel):
def rankings(self): def rankings(self):
rankings = [] rankings = []
for team_registration in self.team_registrations.all(): for team_registration in self.teamregistration_set.all():
if team_registration.walk_out is False and team_registration.final_ranking is not None: if team_registration.walk_out is False and team_registration.final_ranking is not None:
names = team_registration.team_names() names = team_registration.team_names()
ranking = team_registration.final_ranking ranking = team_registration.final_ranking
@ -232,7 +240,7 @@ class Tournament(BaseModel):
complete_teams = [] complete_teams = []
closed_registration_date = self.closed_registration_date closed_registration_date = self.closed_registration_date
for team_registration in self.team_registrations.all(): for team_registration in self.teamregistration_set.all():
is_valid = False is_valid = False
if closed_registration_date is not None and team_registration.registration_date is not None and team_registration.registration_date <= closed_registration_date: if closed_registration_date is not None and team_registration.registration_date is not None and team_registration.registration_date <= closed_registration_date:
is_valid = True is_valid = True
@ -329,10 +337,10 @@ class Tournament(BaseModel):
match_groups = [] match_groups = []
if group_stage_id: if group_stage_id:
group_stage = self.group_stages.filter(id=group_stage_id).first() group_stage = self.groupstage_set.filter(id=group_stage_id).first()
match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False)) match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False))
elif round_id: elif round_id:
round = self.rounds.filter(id=round_id).first() round = self.round_set.filter(id=round_id).first()
if round: if round:
match_groups = self.round_match_groups(round, broadcasted, hide_empty_matches=False) match_groups = self.round_match_groups(round, broadcasted, hide_empty_matches=False)
else: else:
@ -342,11 +350,11 @@ class Tournament(BaseModel):
def all_groups(self, broadcasted): def all_groups(self, broadcasted):
groups = [] groups = []
for round in self.rounds.filter(parent=None).all().order_by('index'): for round in self.round_set.filter(parent=None).all().order_by('index'):
groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True)) groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))
if self.display_group_stages(): if self.display_group_stages():
for group_stage in self.group_stages.all().order_by('index'): for group_stage in self.groupstage_set.all().order_by('index'):
group = self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=True) group = self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=True)
if group: if group:
groups.append(group) groups.append(group)
@ -354,7 +362,7 @@ class Tournament(BaseModel):
return groups return groups
def group_stage_match_group(self, group_stage, broadcasted, hide_empty_matches): def group_stage_match_group(self, group_stage, broadcasted, hide_empty_matches):
matches = group_stage.matches.all() matches = group_stage.match_set.all()
if hide_empty_matches: if hide_empty_matches:
matches = [m for m in matches if m.should_appear()] matches = [m for m in matches if m.should_appear()]
else: else:
@ -368,7 +376,7 @@ class Tournament(BaseModel):
def round_match_groups(self, round, broadcasted, hide_empty_matches): def round_match_groups(self, round, broadcasted, hide_empty_matches):
groups = [] groups = []
matches = round.matches.order_by('index').all() matches = round.match_set.order_by('index').all()
if hide_empty_matches: if hide_empty_matches:
matches = [m for m in matches if m.should_appear()] matches = [m for m in matches if m.should_appear()]
else: else:
@ -397,7 +405,7 @@ class Tournament(BaseModel):
return MatchGroup(name, live_matches) return MatchGroup(name, live_matches)
def live_group_stages(self): def live_group_stages(self):
group_stages = list(self.group_stages.all()) group_stages = list(self.groupstage_set.all())
group_stages.sort(key=lambda gs: gs.index) group_stages.sort(key=lambda gs: gs.index)
return [gs.live_group_stages() for gs in group_stages] return [gs.live_group_stages() for gs in group_stages]
@ -435,7 +443,7 @@ class Tournament(BaseModel):
matches = [] matches = []
group_stages = [] group_stages = []
if len(self.group_stages.all()) > 0 and self.no_bracket_match_has_started(): if len(self.groupstage_set.all()) > 0 and self.no_bracket_match_has_started():
group_stages = self.live_group_stages() group_stages = self.live_group_stages()
matches = self.broadcasted_group_stages_matches() matches = self.broadcasted_group_stages_matches()
first_round = self.first_round() first_round = self.first_round()
@ -463,18 +471,18 @@ class Tournament(BaseModel):
def no_bracket_match_has_started(self): def no_bracket_match_has_started(self):
matches = [] matches = []
for round in self.rounds.all(): for round in self.round_set.all():
for match in round.matches.all(): for match in round.match_set.all():
if match.started(): if match.started():
return False return False
return True return True
def all_matches(self, hide_empty_matches): def all_matches(self, hide_empty_matches):
matches = [] matches = []
for round in self.rounds.all(): for round in self.round_set.all():
matches.extend(round.all_matches(hide_empty_matches)) matches.extend(round.all_matches(hide_empty_matches))
for group_stage in self.group_stages.all(): for group_stage in self.groupstage_set.all():
matches.extend(group_stage.matches.all()) matches.extend(group_stage.match_set.all())
matches = [m for m in matches if m.should_appear()] matches = [m for m in matches if m.should_appear()]
@ -482,12 +490,12 @@ class Tournament(BaseModel):
def group_stage_matches(self): def group_stage_matches(self):
matches = [] matches = []
for group_stage in self.group_stages.all(): for group_stage in self.groupstage_set.all():
matches.extend(group_stage.matches.all()) matches.extend(group_stage.match_set.all())
return matches return matches
def group_stages_running(self): def group_stages_running(self):
if len(self.group_stages.all()) > 0: if len(self.groupstage_set.all()) > 0:
# check le debut des match de Round # check le debut des match de Round
matches = self.group_stage_matches() matches = self.group_stage_matches()
running_group_stage_matches = [m for m in matches if m.end_date is None] running_group_stage_matches = [m for m in matches if m.end_date is None]
@ -509,7 +517,7 @@ class Tournament(BaseModel):
current_round = last_started_match.round.root_round() current_round = last_started_match.round.root_round()
if current_round: if current_round:
return current_round return current_round
main_rounds = list(self.rounds.filter(parent=None).all()) main_rounds = list(self.round_set.filter(parent=None).all())
main_rounds.sort(key=lambda r: r.index) main_rounds.sort(key=lambda r: r.index)
if main_rounds: if main_rounds:
return main_rounds[0] return main_rounds[0]
@ -522,10 +530,10 @@ class Tournament(BaseModel):
return matches[0] return matches[0]
def round_for_index(self, index): def round_for_index(self, index):
return self.rounds.filter(index=index, parent=None).first() return self.round_set.filter(index=index, parent=None).first()
def first_round(self): def first_round(self):
main_rounds = list(self.rounds.filter(parent=None)) main_rounds = list(self.round_set.filter(parent=None))
main_rounds.sort(key=lambda r: r.index, reverse=True) main_rounds.sort(key=lambda r: r.index, reverse=True)
return main_rounds[0] return main_rounds[0]
@ -534,13 +542,13 @@ class Tournament(BaseModel):
group_stages = self.elected_broadcast_group_stages() group_stages = self.elected_broadcast_group_stages()
group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date)) group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date))
for group_stage in group_stages: for group_stage in group_stages:
matches.extend(group_stage.matches.all()) matches.extend(group_stage.match_set.all())
matches = [m for m in matches if m.should_appear()] matches = [m for m in matches if m.should_appear()]
matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index)) matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
return matches return matches
def elected_broadcast_group_stages(self): def elected_broadcast_group_stages(self):
group_stages = list(self.group_stages.all()) group_stages = list(self.groupstage_set.all())
started = [gs for gs in group_stages if gs.starts_soon()] started = [gs for gs in group_stages if gs.starts_soon()]
if len(started) > 0: if len(started) > 0:
return started return started
@ -588,7 +596,7 @@ class Tournament(BaseModel):
def display_group_stages(self): def display_group_stages(self):
if self.end_date is not None: if self.end_date is not None:
return True return True
if len(self.group_stages.all()) == 0: if len(self.groupstage_set.all()) == 0:
return False return False
if self.publish_group_stages: if self.publish_group_stages:
return True return True
@ -600,7 +608,7 @@ class Tournament(BaseModel):
return timezone.now() >= first_group_stage_start_date return timezone.now() >= first_group_stage_start_date
def group_stage_start_date(self): def group_stage_start_date(self):
group_stages = [gs for gs in self.group_stages.all() if gs.start_date is not None] group_stages = [gs for gs in self.groupstage_set.all() if gs.start_date is not None]
if len(group_stages) == 0: if len(group_stages) == 0:
return None return None
@ -630,7 +638,7 @@ class Tournament(BaseModel):
def bracket_matches(self): def bracket_matches(self):
matches = [] matches = []
for round in self.rounds.all(): for round in self.round_set.all():
matches.extend(round.all_matches(False)) matches.extend(round.all_matches(False))
return matches return matches
@ -655,7 +663,7 @@ class Tournament(BaseModel):
return self.federal_level_category == FederalLevelCategory.UNLISTED return self.federal_level_category == FederalLevelCategory.UNLISTED
def is_build_and_not_empty(self): def is_build_and_not_empty(self):
return (len(self.group_stages.all()) > 0 or len(self.rounds.all()) > 0) and len(self.team_registrations.all()) >= 4 return (len(self.groupstage_set.all()) > 0 or len(self.round_set.all()) > 0) and len(self.teamregistration_set.all()) >= 4
def day_duration_formatted(self): def day_duration_formatted(self):
return plural_format("jour", self.day_duration) return plural_format("jour", self.day_duration)
@ -667,7 +675,7 @@ class Tournament(BaseModel):
return False return False
def has_all_group_stages_started(self): def has_all_group_stages_started(self):
for group_stage in self.group_stages.all(): for group_stage in self.groupstage_set.all():
if group_stage.has_at_least_one_started_match() == False: if group_stage.has_at_least_one_started_match() == False:
return False return False
return True return True

@ -4,6 +4,7 @@ from django.urls import re_path
from . import consumers from . import consumers
websocket_urlpatterns = [ websocket_urlpatterns = [
re_path(r"ws/user/(?P<user_id>[\w-]+)/$", consumers.UserConsumer.as_asgi()),
re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()), re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()),
# re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()), # re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
] ]

@ -1,14 +1,90 @@
import random import random
import string import string
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete, post_delete, m2m_changed
from django.db.transaction import DatabaseError
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.apps import apps from django.apps import apps
from django.utils import timezone from django.utils import timezone
from django.db.models import Q
from .models import Club, FailedApiCall, CustomUser, Log, DataAccess, ModelLog from .models import Club, FailedApiCall, CustomUser, Log, DataAccess, ModelLog, BaseModel
import requests import requests
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
# Synchronization
@receiver([post_save, post_delete])
def synchronization_notifications(sender, instance, **kwargs):
"""
Signal handler that sends notifications through WebSocket channels when model instances are saved or deleted.
The function creates a WebSocket group name for each affected user and sends a sync update message
to all clients connected to that group.
"""
if sender in [FailedApiCall, Log, ModelLog]:
return
channel_layer = get_channel_layer()
user_ids = set()
if isinstance(instance, CustomUser):
user_ids.add(instance.id)
elif isinstance(instance, DataAccess):
for shared_user in instance.shared_with.all():
user_ids.add(shared_user.id)
elif isinstance(instance, BaseModel):
owner = instance.get_owner()
if owner is not None:
user_ids.add(owner.id)
if isinstance(instance, BaseModel):
parent_model, data_access_reference_id = instance.get_parent_reference()
data_access_query = Q(model_id=instance.id)
if data_access_reference_id is not None:
data_access_query |= Q(model_id=data_access_reference_id)
data_access_list = DataAccess.objects.filter(data_access_query)
for data_access in data_access_list:
user_ids.add(data_access.owner.id)
for shared_user in data_access.shared_with.all():
user_ids.add(shared_user.id)
for user_id in user_ids:
group_name = f"sync_{user_id}"
print(f">>> send to group {group_name}")
# Send to all clients in the sync group
async_to_sync(channel_layer.group_send)(
group_name, {"type": "sync.update", "message": "hello"}
)
@receiver(m2m_changed, sender=DataAccess.shared_with.through)
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
for user_id in pk_set:
user = CustomUser.objects.get(id=user_id)
instance.create_access_log(user, 'GRANT_ACCESS')
elif action == "post_remove":
for user_id in pk_set:
user = CustomUser.objects.get(id=user_id)
instance.create_access_log(user, 'REVOKE_ACCESS')
@receiver(pre_delete, sender=DataAccess)
def store_users_before_delete(sender, instance, **kwargs):
# Store the users in a temporary attribute that we can access after deletion
instance._users_to_revoke = list(instance.shared_with.all())
@receiver(post_delete, sender=DataAccess)
def revoke_access_after_delete(sender, instance, **kwargs):
# Create revoke logs for all previously stored users
if hasattr(instance, '_users_to_revoke'):
for user in instance._users_to_revoke:
instance.create_access_log(user, 'REVOKE_ACCESS')
# Others
def generate_unique_code(): def generate_unique_code():
characters = string.ascii_letters + string.digits characters = string.ascii_letters + string.digits
while True: while True:
@ -29,10 +105,6 @@ DISCORD_LOGS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1257987637449588736
def notify_discord_on_create(sender, instance, created, **kwargs): def notify_discord_on_create(sender, instance, created, **kwargs):
notify_object_creation_on_discord(created, instance, DISCORD_FAILED_CALLS_WEBHOOK_URL) notify_object_creation_on_discord(created, instance, DISCORD_FAILED_CALLS_WEBHOOK_URL)
# @receiver(post_save, sender=CustomUser)
# def notify_user_creation_on_discord(sender, instance, created, **kwargs):
# notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL)
@receiver(post_save, sender=Log) @receiver(post_save, sender=Log)
def notify_log_creation_on_discord(sender, instance, created, **kwargs): def notify_log_creation_on_discord(sender, instance, created, **kwargs):
notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL) notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL)
@ -42,7 +114,10 @@ def notify_object_creation_on_discord(created, instance, webhook_url):
if created: if created:
default_db_engine = settings.DATABASES['default']['ENGINE'] default_db_engine = settings.DATABASES['default']['ENGINE']
if default_db_engine != 'django.db.backends.sqlite3': if default_db_engine != 'django.db.backends.sqlite3':
if hasattr(instance, 'discord_string'):
message = f'New {instance.__class__.__name__} created: {instance.discord_string()}' message = f'New {instance.__class__.__name__} created: {instance.discord_string()}'
else:
message = "no message. Please configure 'discord_string' on your instance"
send_discord_message(webhook_url, message) send_discord_message(webhook_url, message)
def send_discord_message(webhook_url, content): def send_discord_message(webhook_url, content):
@ -54,27 +129,3 @@ def send_discord_message(webhook_url, content):
raise ValueError( raise ValueError(
f'Error sending message to Discord webhook: {response.status_code}, {response.text}' f'Error sending message to Discord webhook: {response.status_code}, {response.text}'
) )
@receiver(pre_delete, sender=DataAccess)
def data_access_pre_delete(sender, instance, **kwargs):
print('>>> delete data access signal')
try:
model_class = apps.get_model('tournaments', instance.model_name)
obj = model_class.objects.get(id=instance.model_id)
parent_model, parent_id = obj.get_parent_reference()
store_id = None
if hasattr(obj, 'store_id'):
store_id = obj.store_id
ModelLog.objects.create(
user=instance.shared_with,
model_id=instance.model_id,
model_name=instance.model_name,
operation='REVOKE_ACCESS',
date=timezone.now(),
store_id=store_id,
parent_model_id=parent_id,
parent_model_name=parent_model
)
except Exception as e:
print(f"Error in data_access_pre_delete signal: {e}")

Loading…
Cancel
Save