Separate the sync from tournaments by creating a new app

sync
Laurent 11 months ago
parent 9b81dc49e8
commit 7bdf38b78d
  1. 12
      api/serializers.py
  2. 7
      api/urls.py
  3. 34
      api/utils.py
  4. 21
      api/views.py
  5. 1
      padelclub_backend/settings.py
  6. 5
      padelclub_backend/settings_app.py
  7. 0
      sync/__init__.py
  8. 33
      sync/admin.py
  9. 9
      sync/apps.py
  10. 48
      sync/migrations/0001_initial.py
  11. 0
      sync/migrations/__init__.py
  12. 3
      sync/models/__init__.py
  13. 3
      sync/models/base.py
  14. 6
      sync/models/data_access.py
  15. 13
      sync/models/model_log.py
  16. 37
      sync/registry.py
  17. 8
      sync/serializers.py
  18. 167
      sync/signals.py
  19. 3
      sync/tests.py
  20. 42
      sync/utils.py
  21. 35
      sync/views.py
  22. 33
      tournaments/admin.py
  23. 23
      tournaments/migrations/0103_remove_modellog_users_delete_dataaccess_and_more.py
  24. 5
      tournaments/models/__init__.py
  25. 182
      tournaments/signals.py

@ -11,7 +11,7 @@ from django.core.mail import EmailMessage
from django.contrib.sites.shortcuts import get_current_site
from api.tokens import account_activation_token
from tournaments.models.data_access import DataAccess
# from tournaments.models.data_access import DataAccess
class UserSerializer(serializers.ModelSerializer):
@ -227,8 +227,8 @@ class DeviceTokenSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = ['user']
class DataAccessSerializer(serializers.ModelSerializer):
class Meta:
model = DataAccess
fields = '__all__'
read_only_fields = ['user']
# class DataAccessSerializer(serializers.ModelSerializer):
# class Meta:
# model = DataAccess
# fields = '__all__'
# read_only_fields = ['user']

@ -2,7 +2,8 @@ from django.urls import include, path
from rest_framework import routers
from rest_framework.authtoken.views import obtain_auth_token
from . import views, sync
from . import views
from sync.views import DataApi, DataAccessViewSet
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
@ -21,12 +22,12 @@ router.register(r'date-intervals', views.DateIntervalViewSet)
router.register(r'failed-api-calls', views.FailedApiCallViewSet)
router.register(r'logs', views.LogViewSet)
router.register(r'device-token', views.DeviceTokenViewSet)
router.register(r'data-access', views.DataAccessViewSet)
router.register(r'data-access', DataAccessViewSet)
urlpatterns = [
path('', include(router.urls)),
path('data/', sync.DataApi.as_view(), name="data"),
path('data/', 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"),

@ -1,39 +1,5 @@
import re
import importlib
from django.apps import apps
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(model_name):
# Remove the 's' character at the end if present
if model_name.endswith('s') and not model_name.endswith('ss'):
model_name = model_name[:-1]
# Capitalize words separated by a dash
words = model_name.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)
def get_serializer(instance, model_name):
serializer = build_serializer_class(model_name)
return serializer(instance)
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)
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 tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DataAccess
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
from rest_framework import viewsets, permissions
from rest_framework.authtoken.models import Token
@ -20,7 +20,7 @@ from django.apps import apps
from collections import defaultdict
from .permissions import IsClubOwner
from .utils import is_valid_email, build_serializer_class
from .utils import is_valid_email
class CustomAuthToken(APIView):
permission_classes = []
@ -249,11 +249,7 @@ class DateIntervalViewSet(viewsets.ModelViewSet):
return []
return self.queryset.filter(
Q(event__creator=self.request.user) |
Q(event__id__in=DataAccess.objects.filter(
shared_with=self.request.user,
model_name=self.queryset.model.__name__
).values_list('model_id', flat=True))
Q(event__creator=self.request.user)
)
class FailedApiCallViewSet(viewsets.ModelViewSet):
@ -306,12 +302,3 @@ class DeviceTokenViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class DataAccessViewSet(viewsets.ModelViewSet):
queryset = DataAccess.objects.all()
serializer_class = DataAccessSerializer
def get_queryset(self):
if self.request.user:
return self.queryset.filter(Q(owner=self.request.user) | Q(shared_with__in=[self.request.user]))
return []

@ -33,6 +33,7 @@ ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'daphne',
'sync',
'tournaments',
'django.contrib.admin',
'django.contrib.auth',

@ -48,3 +48,8 @@ CHANNEL_LAYERS = {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
SYNC_APPS = {
'sync': {},
'tournaments': { 'exclude': ['Log', 'FailedApiCall'] }
}

@ -0,0 +1,33 @@
from django.contrib import admin
from .models import BaseModel, ModelLog, DataAccess
from django.utils import timezone
class AutoUpdateAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if isinstance(obj, BaseModel):
obj.last_updated_by = request.user
obj.last_update = timezone.now()
super().save_model(request, obj, form, change)
class ModelLogAdmin(admin.ModelAdmin):
list_display = ['get_users', 'date', 'operation', 'model_id', 'model_name']
list_filter = ['users']
ordering = ['-date']
@admin.display(description='Users')
def get_users(self, obj):
return ", ".join([str(item) for item in obj.users.all()])
class DataAccessAdmin(AutoUpdateAdmin):
list_display = ['owner', 'get_shared_users', 'model_name', 'model_id']
list_filter = ['owner', 'shared_with']
ordering = ['-granted_at']
@admin.display(description='Shared with')
def get_shared_users(self, obj):
return ", ".join([str(item) for item in obj.shared_with.all()])
# Register your models here.
admin.site.register(ModelLog, ModelLogAdmin)
admin.site.register(DataAccess, DataAccessAdmin)

@ -0,0 +1,9 @@
from django.apps import AppConfig
class SyncConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sync'
def ready(self):
# Import signals when Django starts
import sync.signals

@ -0,0 +1,48 @@
# Generated by Django 5.1 on 2024-12-02 15:39
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DataAccess',
fields=[
('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('last_update', models.DateTimeField(default=django.utils.timezone.now)),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('model_name', models.CharField(max_length=50)),
('model_id', models.UUIDField()),
('granted_at', models.DateTimeField(auto_now_add=True)),
('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_data', to=settings.AUTH_USER_MODEL)),
('shared_with', models.ManyToManyField(related_name='shared_data', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ModelLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('model_id', models.UUIDField()),
('operation', models.CharField(choices=[('POST', 'POST'), ('PUT', 'PUT'), ('DELETE', 'DELETE'), ('GRANT_ACCESS', 'GRANT_ACCESS'), ('REVOKE_ACCESS', 'REVOKE_ACCESS')], max_length=50)),
('date', models.DateTimeField()),
('model_name', models.CharField(max_length=50)),
('store_id', models.CharField(blank=True, max_length=200, null=True)),
('users', models.ManyToManyField(blank=True, related_name='model_logs', to=settings.AUTH_USER_MODEL)),
],
),
]

@ -0,0 +1,3 @@
from .base import BaseModel, SideStoreModel
from .model_log import ModelLog, ModelOperation
from .data_access import DataAccess

@ -1,11 +1,12 @@
from django.db import models
from django.utils.timezone import now
from typing import List, Set
from django.conf import settings
class BaseModel(models.Model):
creation_date = models.DateTimeField(default=now, editable=False)
last_update = models.DateTimeField(default=now)
last_updated_by = models.ForeignKey('CustomUser', blank=True, null=True, on_delete=models.SET_NULL)
last_updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL)
class Meta:
abstract = True

@ -2,6 +2,8 @@ from django.db import models
from django.utils import timezone
from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
import uuid
@ -9,8 +11,8 @@ from . import ModelLog, SideStoreModel, BaseModel
class DataAccess(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
owner = models.ForeignKey('CustomUser', related_name='owned_data', on_delete=models.CASCADE)
shared_with = models.ManyToManyField('CustomUser', related_name='shared_data')
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='owned_data', on_delete=models.CASCADE)
shared_with = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='shared_data')
model_name = models.CharField(max_length=50)
model_id = models.UUIDField()
granted_at = models.DateTimeField(auto_now_add=True)

@ -1,10 +1,19 @@
from django.db import models
from django.conf import settings
import uuid
from . import ModelOperation
class ModelOperation(models.TextChoices):
POST = 'POST', 'POST'
PUT = 'PUT', 'PUT'
DELETE = 'DELETE', 'DELETE'
GRANT_ACCESS = 'GRANT_ACCESS', 'GRANT_ACCESS'
REVOKE_ACCESS = 'REVOKE_ACCESS', 'REVOKE_ACCESS'
class ModelLog(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
users = models.ManyToManyField('CustomUser', related_name='model_logs', blank=True)
users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='model_logs', blank=True)
model_id = models.UUIDField()
operation = models.CharField(choices=ModelOperation.choices, max_length=50)
date = models.DateTimeField()

@ -0,0 +1,37 @@
from django.conf import settings
from django.apps import apps
from .models import BaseModel
class SyncRegistry:
def __init__(self):
self._registry = {}
self.load_sync_apps()
def load_sync_apps(self):
sync_apps = getattr(settings, 'SYNC_APPS', {})
for app_label, config in sync_apps.items():
print(f'app_label = {app_label}')
app_models = apps.get_app_config(app_label).get_models()
for model in app_models:
if hasattr(model, '_meta') and not model._meta.abstract:
if issubclass(model, BaseModel):
model_name = model.__name__
if self.should_sync_model(model_name, config):
self.register(model)
def should_sync_model(self, model_name, config):
if 'exclude' in config and model_name in config['exclude']:
return False
if 'models' in config and config['models']:
return model_name in config['models']
return True
def register(self, model):
print(f'>>> Registers {model.__name__}')
self._registry[model.__name__] = model
def get_model(self, model_name):
return self._registry.get(model_name)
# Create singleton instance
sync_registry = SyncRegistry()

@ -0,0 +1,8 @@
from rest_framework import serializers
from .models import DataAccess
class DataAccessSerializer(serializers.ModelSerializer):
class Meta:
model = DataAccess
fields = '__all__'
read_only_fields = ['user']

@ -0,0 +1,167 @@
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver
from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel
from django.contrib.auth import get_user_model
from django.utils import timezone
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from threading import Timer
from functools import partial
# Synchronization
User = get_user_model()
@receiver([pre_save, pre_delete])
def synchronization_prepare(sender, instance, created=False, **kwargs):
# some classes are excluded in settings_app.py: SYNC_APPS
if not isinstance(instance, BaseModel):
return
save_model_log_if_possible(instance, kwargs.get('signal'), created)
@receiver([post_save, post_delete])
def synchronization_notifications(sender, instance, created=False, **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.
"""
# some classes are excluded in settings_app.py: SYNC_APPS
if not isinstance(instance, BaseModel):
return
# print(f'*** signals {sender}')
notify_impacted_users(instance, kwargs.get('signal'))
def notify_impacted_users(instance, signal):
user_ids = set()
# add impacted users
if isinstance(instance, User):
user_ids.add(instance.id)
elif isinstance(instance, BaseModel):
owner = instance.last_updated_by
if owner:
user_ids.add(owner.id)
if isinstance(instance, BaseModel):
if instance._users_to_notify is not None:
user_ids.update(instance._users_to_notify)
else:
print('no users to notify')
print(f'notify: {user_ids}')
for user_id in user_ids:
send_user_message(user_id)
def save_model_log_if_possible(instance, signal, created):
user = instance.last_updated_by
if user:
if signal == post_save or signal == pre_save:
if created:
operation = ModelOperation.POST
else:
operation = ModelOperation.PUT
else:
operation = ModelOperation.DELETE
model_name = instance.__class__.__name__
store_id = None
if isinstance(instance, SideStoreModel):
store_id = instance.store_id
if operation == ModelOperation.DELETE: # delete now unnecessary logs
ModelLog.objects.filter(model_id=instance.id).delete()
users = {user}
data_access_list = related_data_access(instance)
for data_access in data_access_list:
users.add(data_access.owner)
users.update(data_access.shared_with.all())
if isinstance(instance, DataAccess):
users.add(instance.owner)
users.update(instance.shared_with.all())
user_ids = [user.id for user in users]
print(f'users to notify: {user_ids}')
instance._users_to_notify = user_ids # save this for the post_save signal
save_model_log(users, operation, model_name, instance.id, store_id)
else:
print('>>> Model Log could not be created because instance.last_updated_by is None')
def save_model_log(users, model_operation, model_name, model_id, store_id):
now = timezone.now()
existing_log = ModelLog.objects.filter(users__in=users, model_id=model_id, operation=model_operation).first()
if existing_log:
# print(f'update existing log {existing_log.users} ')
existing_log.date = now
existing_log.model_operation = model_operation
existing_log.save()
existing_log.users.set(users)
else:
model_log = ModelLog()
model_log.operation = model_operation
model_log.date = now
model_log.model_name = model_name
model_log.model_id = model_id
model_log.store_id = store_id
model_log.save()
model_log.users.set(users)
def related_data_access(instance):
related_instances = instance.related_instances()
related_ids = [ri.id for ri in instance.related_instances()]
related_ids.append(instance.id)
return DataAccess.objects.filter(model_id__in=related_ids)
def delete_data_access_if_necessary(model_id):
DataAccess.objects.filter(model_id=model_id).delete()
@receiver(m2m_changed, sender=DataAccess.shared_with.through)
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
users = User.objects.filter(id__in=pk_set)
if action == "post_add":
instance.create_access_log(users, 'GRANT_ACCESS')
elif action == "post_remove":
instance.create_access_log(users, 'REVOKE_ACCESS')
for user_id in pk_set:
send_user_message(user_id)
def send_user_message(user_id):
if not hasattr(send_user_message, '_buffer'):
send_user_message._buffer = set()
send_user_message._timer = None
send_user_message._buffer.add(user_id)
if send_user_message._timer:
send_user_message._timer.cancel()
def send_buffered_messages():
channel_layer = get_channel_layer()
for buffered_id in send_user_message._buffer:
group_name = f"sync_{buffered_id}"
print(f">>> send to group {group_name}")
async_to_sync(channel_layer.group_send)(
group_name, {"type": "sync.update", "message": "hello"}
)
send_user_message._buffer.clear()
send_user_message._timer = None
send_user_message._timer = Timer(0.1, send_buffered_messages)
send_user_message._timer.start()
@receiver(pre_delete, sender=DataAccess)
def revoke_access_after_delete(sender, instance, **kwargs):
instance.create_revoke_access_log()

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,42 @@
import importlib
from django.apps import apps
from .registry import sync_registry
def build_serializer_class(model_name):
# Remove the 's' character at the end if present
if model_name.endswith('s') and not model_name.endswith('ss'):
model_name = model_name[:-1]
# Capitalize words separated by a dash
words = model_name.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'
# Try to find serializer in current directory first
try:
module = importlib.import_module('api.serializers')
return getattr(module, transformed_string)
except (ImportError, AttributeError):
module = importlib.import_module('.serializers', package=__package__)
return getattr(module, transformed_string)
def get_serializer(instance, model_name):
serializer = build_serializer_class(model_name)
return serializer(instance)
def get_data(model_name, model_id):
model = sync_registry.get_model(model_name)
# model = apps.get_model(app_label=app_label, model_name=model_name)
return model.objects.get(id=model_id)
def get_serialized_data(model_name, model_id):
print(f'model_name = {model_name}')
model = sync_registry.get_model(model_name)
instance = model.objects.get(id=model_id)
serializer_class = build_serializer_class(model_name)
serializer = serializer_class(instance)
return serializer.data

@ -1,3 +1,8 @@
from django.shortcuts import render
from .serializers import DataAccessSerializer
from django.db.models import Q
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@ -5,7 +10,6 @@ from rest_framework import status
from django.apps import apps
from django.utils import timezone
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from collections import defaultdict
@ -13,7 +17,9 @@ from urllib.parse import unquote
from .utils import get_serializer, build_serializer_class, get_data, get_serialized_data
from tournaments.models import ModelLog, BaseModel, SideStoreModel
from .models import ModelLog, BaseModel, SideStoreModel, DataAccess
from .registry import sync_registry
class DataApi(APIView):
permission_classes = [IsAuthenticated]
@ -31,7 +37,8 @@ class DataApi(APIView):
serializer_class = build_serializer_class(model_name)
data['last_updated_by'] = request.user.id # always refresh the user performing the operation
model = apps.get_model(app_label='tournaments', model_name=model_name)
# model = apps.get_model(app_label='tournaments', model_name=model_name)
model = sync_registry.get_model(model_name)
if model_operation == 'POST':
serializer = serializer_class(data=data, context={'request': request})
@ -42,7 +49,7 @@ class DataApi(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif model_operation == 'PUT':
data_id = data.get('id')
instance = get_data('tournaments', model_name, data_id)
instance = get_data(model_name, data_id)
serializer = serializer_class(instance, data=data, context={'request': request})
if serializer.is_valid():
if instance.last_update <= serializer.validated_data.get('last_update'):
@ -57,7 +64,7 @@ class DataApi(APIView):
elif model_operation == 'DELETE':
data_id = data.get('id')
try:
instance = get_data('tournaments', model_name, data_id)
instance = get_data(model_name, data_id)
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except model.DoesNotExist: # POST
@ -161,19 +168,20 @@ class DataApi(APIView):
last_log_date = log.date
try:
if log.operation in ['POST', 'PUT']:
data = get_serialized_data('tournaments', log.model_name, log.model_id)
data = get_serialized_data(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)
model = sync_registry.get_model(log.model_name)
# 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)
serializer = get_serializer(instance, log.model_name)
# data = get_serialized_data('tournaments', log.model_name, log.model_id)
grants[log.model_name][log.model_id] = serializer.data
# instance = model.objects.get(id=log.model_id)
self.add_children_recursively(instance, grants)
@ -187,7 +195,7 @@ class DataApi(APIView):
})
# Get the model instance and add its parents to revocation_parents
model = apps.get_model('tournaments', model_name=log.model_name)
model = sync_registry.get_model(log.model_name)
try:
instance = model.objects.get(id=log.model_id)
self.add_parents_recursively(instance, revocation_parents, minimal=True)
@ -279,3 +287,12 @@ class DataApi(APIView):
dictionary[parent_model_name][parent.id] = serializer.data
self.add_parents_recursively(parent, dictionary, minimal)
class DataAccessViewSet(viewsets.ModelViewSet):
queryset = DataAccess.objects.all()
serializer_class = DataAccessSerializer
def get_queryset(self):
if self.request.user:
return self.queryset.filter(Q(owner=self.request.user) | Q(shared_with__in=[self.request.user]))
return []

@ -1,24 +1,15 @@
from django.contrib import admin
from django.utils import timezone
from tournaments.models import team_registration
from tournaments.models.data_access import DataAccess
from tournaments.models.device_token import DeviceToken
from .models import BaseModel, Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, ModelLog
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter
class AutoUpdateAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if isinstance(obj, BaseModel):
obj.last_updated_by = request.user
obj.last_update = timezone.now()
super().save_model(request, obj, form, change)
from sync.admin import AutoUpdateAdmin
class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm
@ -102,24 +93,6 @@ class LogAdmin(AutoUpdateAdmin):
class DeviceTokenAdmin(AutoUpdateAdmin):
list_display = ['user', 'value']
class ModelLogAdmin(admin.ModelAdmin):
list_display = ['get_users', 'date', 'operation', 'model_id', 'model_name']
list_filter = ['users']
ordering = ['-date']
@admin.display(description='Users')
def get_users(self, obj):
return ", ".join([str(item) for item in obj.users.all()])
class DataAccessAdmin(AutoUpdateAdmin):
list_display = ['owner', 'get_shared_users', 'model_name', 'model_id']
list_filter = ['owner', 'shared_with']
ordering = ['-granted_at']
@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(Club, ClubAdmin)
admin.site.register(Event, EventAdmin)
@ -136,5 +109,3 @@ 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)
admin.site.register(DataAccess, DataAccessAdmin)

@ -0,0 +1,23 @@
# Generated by Django 5.1 on 2024-12-02 15:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0102_remove_dataaccess_last_hierarchy_update'),
]
operations = [
migrations.RemoveField(
model_name='modellog',
name='users',
),
migrations.DeleteModel(
name='DataAccess',
),
migrations.DeleteModel(
name='ModelLog',
),
]

@ -1,4 +1,5 @@
from .base import BaseModel, SideStoreModel
from sync.models import BaseModel, SideStoreModel
from .custom_user import CustomUser
from .club import Club
from .court import Court
@ -17,5 +18,3 @@ from .purchase import Purchase
from .failed_api_call import FailedApiCall
from .log import Log
from .device_token import DeviceToken
from .model_log import ModelLog
from .data_access import DataAccess

@ -1,187 +1,15 @@
import random
import string
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.db.transaction import DatabaseError
from django.db.models.signals import post_save
# from django.db.transaction import DatabaseError
from django.dispatch import receiver
from django.conf import settings
from django.apps import apps
from django.utils import timezone
from django.db.models import Q
# from django.apps import apps
# from django.db.models import Q
from .models import Club, FailedApiCall, CustomUser, Log, DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel
from .models import Club, FailedApiCall, Log
import requests
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from threading import Timer
from functools import partial
# Synchronization
@receiver([pre_save, pre_delete])
def synchronization_prepare(sender, instance, created=False, **kwargs):
if not isinstance(instance, BaseModel):
return
if sender in [FailedApiCall, Log, ModelLog]:
return
save_model_log_if_possible(instance, kwargs.get('signal'), created)
# if not isinstance(instance, DataAccess):
# update_data_access(instance)
@receiver([post_save, post_delete])
def synchronization_notifications(sender, instance, created=False, **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 not isinstance(instance, BaseModel):
return
if sender in [FailedApiCall, Log, ModelLog]:
return
# print(f'*** signals {sender}')
notify_impacted_users(instance, kwargs.get('signal'))
def notify_impacted_users(instance, signal):
user_ids = set()
# add impacted users
if isinstance(instance, CustomUser):
user_ids.add(instance.id)
elif isinstance(instance, BaseModel):
owner = instance.last_updated_by
if owner:
user_ids.add(owner.id)
if isinstance(instance, BaseModel):
if instance._users_to_notify is not None:
user_ids.update(instance._users_to_notify)
else:
print('no users to notify')
print(f'notify: {user_ids}')
for user_id in user_ids:
send_user_message(user_id)
def save_model_log_if_possible(instance, signal, created):
user = instance.last_updated_by
if user:
if signal == post_save or signal == pre_save:
if created:
operation = ModelOperation.POST
else:
operation = ModelOperation.PUT
else:
operation = ModelOperation.DELETE
model_name = instance.__class__.__name__
store_id = None
if isinstance(instance, SideStoreModel):
store_id = instance.store_id
if operation == ModelOperation.DELETE: # delete now unnecessary logs
ModelLog.objects.filter(model_id=instance.id).delete()
users = {user}
data_access_list = related_data_access(instance)
for data_access in data_access_list:
users.add(data_access.owner)
users.update(data_access.shared_with.all())
if isinstance(instance, DataAccess):
users.add(instance.owner)
users.update(instance.shared_with.all())
user_ids = [user.id for user in users]
print(f'users to notify: {user_ids}')
instance._users_to_notify = user_ids # save this for the post_save signal
save_model_log(users, operation, model_name, instance.id, store_id)
else:
print('>>> Model Log could not be created because instance.last_updated_by is None')
def save_model_log(users, model_operation, model_name, model_id, store_id):
now = timezone.now()
existing_log = ModelLog.objects.filter(users__in=users, model_id=model_id, operation=model_operation).first()
if existing_log:
# print(f'update existing log {existing_log.users} ')
existing_log.date = now
existing_log.model_operation = model_operation
existing_log.save()
existing_log.users.set(users)
else:
model_log = ModelLog()
model_log.operation = model_operation
model_log.date = now
model_log.model_name = model_name
model_log.model_id = model_id
model_log.store_id = store_id
model_log.save()
model_log.users.set(users)
def related_data_access(instance):
related_instances = instance.related_instances()
related_ids = [ri.id for ri in instance.related_instances()]
related_ids.append(instance.id)
return DataAccess.objects.filter(model_id__in=related_ids)
# def update_data_access(instance):
# data_access_list = related_data_access(instance)
# for data_access in data_access_list:
# date = timezone.now() if instance.last_update is None else instance.last_update
# data_access.last_hierarchy_update = date
# data_access.save()
def delete_data_access_if_necessary(model_id):
DataAccess.objects.filter(model_id=model_id).delete()
@receiver(m2m_changed, sender=DataAccess.shared_with.through)
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
users = CustomUser.objects.filter(id__in=pk_set)
if action == "post_add":
instance.create_access_log(users, 'GRANT_ACCESS')
elif action == "post_remove":
instance.create_access_log(users, 'REVOKE_ACCESS')
for user_id in pk_set:
send_user_message(user_id)
def send_user_message(user_id):
if not hasattr(send_user_message, '_buffer'):
send_user_message._buffer = set()
send_user_message._timer = None
send_user_message._buffer.add(user_id)
if send_user_message._timer:
send_user_message._timer.cancel()
def send_buffered_messages():
channel_layer = get_channel_layer()
for buffered_id in send_user_message._buffer:
group_name = f"sync_{buffered_id}"
print(f">>> send to group {group_name}")
async_to_sync(channel_layer.group_send)(
group_name, {"type": "sync.update", "message": "hello"}
)
send_user_message._buffer.clear()
send_user_message._timer = None
send_user_message._timer = Timer(0.1, send_buffered_messages)
send_user_message._timer.start()
@receiver(pre_delete, sender=DataAccess)
def revoke_access_after_delete(sender, instance, **kwargs):
instance.create_revoke_access_log()
# Others
def generate_unique_code():

Loading…
Cancel
Save