sync
Laurent 12 months ago
parent 41ba44df98
commit 1f4687f78a
  1. 33
      api/sync.py
  2. 3
      api/utils.py
  3. 100
      tournaments/migrations/0100_club_last_updated_by_court_last_updated_by_and_more.py
  4. 5
      tournaments/models/base.py
  5. 58
      tournaments/models/data_access.py
  6. 4
      tournaments/models/date_interval.py
  7. 4
      tournaments/models/event.py
  8. 4
      tournaments/models/group_stage.py
  9. 4
      tournaments/models/match.py
  10. 4
      tournaments/models/player_registration.py
  11. 4
      tournaments/models/purchase.py
  12. 4
      tournaments/models/round.py
  13. 4
      tournaments/models/team_score.py
  14. 2
      tournaments/models/tournament.py
  15. 46
      tournaments/signals.py

@ -7,6 +7,7 @@ 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
@ -28,17 +29,14 @@ class DataApi(APIView):
print(f"DataApi post > {model_operation} {model_name}")
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)
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)
@ -62,9 +60,10 @@ class DataApi(APIView):
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(
self.save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
@ -84,10 +83,10 @@ class DataApi(APIView):
instance.delete()
# we delete all previous logs linked to the instance because they won't be needed anymore
# 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(
self.save_model_log(
user=request.user,
model_operation=model_operation,
model_name=model_name,
@ -108,7 +107,13 @@ class DataApi(APIView):
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):
def save_model_log(self, user, model_operation, model_name, model_id, store_id, date):
existing_log = ModelLog.objects.filter(user=user, model_id=model_id, operation=model_operation).first()
if existing_log:
existing_log.date = date
existing_log.model_operation = model_operation
existing_log.save()
else:
model_log = ModelLog()
model_log.user = user
model_log.operation = model_operation
@ -130,9 +135,10 @@ class DataApi(APIView):
updates = defaultdict(dict)
deletions = defaultdict(list)
print(f'>>> log count = {len(logs)}')
# print(f'>>> log count = {len(logs)}')
for log in logs:
try:
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
@ -153,6 +159,8 @@ class DataApi(APIView):
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})
except ObjectDoesNotExist:
pass
# Convert updates dict to list for each model
for model_name in updates:
@ -179,16 +187,19 @@ class DataApi(APIView):
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)}')
# 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:
model_ids.append(data_access.model_id)
try:
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)
except ObjectDoesNotExist:
pass
# 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))

@ -26,9 +26,6 @@ def build_serializer_class(source):
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)

@ -0,0 +1,100 @@
# Generated by Django 5.1 on 2024-11-20 14:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0099_dataaccess_last_hierarchy_update'),
]
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='dataaccess',
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='tournament',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tournaments', to='tournaments.event'),
),
]

@ -5,14 +5,11 @@ from typing import List, Set
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('CustomUser', blank=True, null=True, on_delete=models.SET_NULL)
class Meta:
abstract = True
def get_owner(self):
return None
def get_parent_reference(self):
"""Return a tuple: model_name, model_id"""
return None, None

@ -1,6 +1,8 @@
from django.db import models
from django.utils import timezone
from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist
import uuid
from . import ModelLog, SideStoreModel, BaseModel
@ -14,61 +16,21 @@ class DataAccess(BaseModel):
granted_at = models.DateTimeField(auto_now_add=True)
last_hierarchy_update = models.DateTimeField(default=timezone.now)
# def save(self, *args, **kwargs):
# is_new = self._state.adding
# print('>>> save DA')
# 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):
# # Store users before deletion
# users_to_revoke = list(self.shared_with.all())
# # 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)}')
# 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)
try:
obj = model_class.objects.get(id=self.model_id)
store_id = None
if isinstance(obj, SideStoreModel):
store_id = obj.store_id
existing_log = ModelLog.objects.filter(user=user, model_id=self.model_id, operation=operation).first()
if existing_log:
existing_log.date = timezone.now()
existing_log.model_operation = operation
existing_log.save()
else:
ModelLog.objects.create(
user=user,
model_id=self.model_id,
@ -77,3 +39,5 @@ class DataAccess(BaseModel):
date=timezone.now(),
store_id=store_id
)
except ObjectDoesNotExist:
pass

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

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

@ -19,10 +19,6 @@ class GroupStage(SideStoreModel):
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 __str__(self):
return self.display_name()
# return f"{self.tournament.display_name()} - {self.display_name()}"

@ -30,10 +30,6 @@ class Match(SideStoreModel):
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 __str__(self):
names = " / ".join(self.player_names())
return f"{self.stage_name()} #{self.index}: {names}"

@ -38,10 +38,6 @@ class PlayerRegistration(SideStoreModel):
def get_parent_reference(self):
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):
return self.name()

@ -13,7 +13,3 @@ class Purchase(BaseModel):
def __str__(self):
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,10 +14,6 @@ class Round(SideStoreModel):
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 __str__(self):
if self.parent:
return f"LB: {self.name()}"

@ -17,10 +17,6 @@ class TeamScore(SideStoreModel):
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):
if self.team_registration:
return self.team_registration.tournament

@ -17,7 +17,7 @@ class TeamSortingType(models.IntegerChoices):
class Tournament(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE, related_name="events")
event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE, related_name="tournaments")
name = models.CharField(max_length=200, null=True, blank=True)
start_date = models.DateTimeField()
end_date = models.DateTimeField(null=True, blank=True)

@ -27,24 +27,30 @@ def synchronization_notifications(sender, instance, **kwargs):
if sender in [FailedApiCall, Log, ModelLog]:
return
channel_layer = get_channel_layer()
if kwargs.get('signal') == post_delete:
delete_data_access_if_necessary(instance.id)
print(f'*** signals {sender}')
user_ids = set()
# add impacted users
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()
owner = instance.last_updated_by
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 kwargs.get('signal') != post_delete:
# when deleting objects, accessing reference generates DoesNotExist exceptions
parent_model, data_access_reference_id = instance.get_parent_reference()
if data_access_reference_id is not None:
data_access_query |= Q(model_id=data_access_reference_id)
# look for users through data access objects
data_access_list = DataAccess.objects.filter(data_access_query)
for data_access in data_access_list:
user_ids.add(data_access.owner.id)
@ -52,10 +58,17 @@ def synchronization_notifications(sender, instance, **kwargs):
user_ids.add(shared_user.id)
for user_id in user_ids:
send_user_message(user_id)
def delete_data_access_if_necessary(model_id):
DataAccess.objects.filter(model_id=model_id).delete()
def send_user_message(user_id):
group_name = f"sync_{user_id}"
print(f">>> send to group {group_name}")
# Send to all clients in the sync group
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
group_name, {"type": "sync.update", "message": "hello"}
)
@ -66,23 +79,28 @@ def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
for user_id in pk_set:
user = CustomUser.objects.get(id=user_id)
instance.create_access_log(user, 'GRANT_ACCESS')
send_user_message(user_id)
elif action == "post_remove":
for user_id in pk_set:
user = CustomUser.objects.get(id=user_id)
instance.create_access_log(user, 'REVOKE_ACCESS')
send_user_message(user_id)
@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:
for user in instance.shared_with.all():
instance.create_access_log(user, 'REVOKE_ACCESS')
# # 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():

Loading…
Cancel
Save