sync_v2
Laurent 7 months ago
parent aacb64f0f0
commit d19fdc3bd0
  1. 33
      authentication/views.py
  2. 1
      sync/models/base.py
  3. 4
      sync/models/model_log.py
  4. 37
      sync/signals.py
  5. 395
      sync/views.py
  6. 2
      tournaments/models/tournament.py

@ -39,26 +39,29 @@ class CustomAuthToken(APIView):
user = authenticate(username=true_username, password=password)
if user:
# user.device_id = device_id
# user.save()
# self.create_or_update_device(user, device_id)
user.device_id = device_id
user.save()
# token, created = Token.objects.get_or_create(user=user)
# return Response({'token': token.key})
device_model = request.data.get('device_model')
device = self.create_or_update_device(user, device_id, device_model)
self.create_login_log(user, device)
if user.device_id is None or user.device_id == device_id or user.username == 'apple-test':
user.device_id = device_id
user.save()
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
device_model = request.data.get('device_model')
# if user.device_id is None or user.device_id == device_id or user.username == 'apple-test':
# user.device_id = device_id
# user.save()
device = self.create_or_update_device(user, device_id, device_model)
self.create_login_log(user, device)
# device_model = request.data.get('device_model')
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
else:
return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN)
# device = self.create_or_update_device(user, device_id, device_model)
# self.create_login_log(user, device)
# token, created = Token.objects.get_or_create(user=user)
# return Response({'token': token.key})
# else:
# return Response({'error': 'Vous ne pouvez pour l\'instant vous connecter sur plusieurs appareils en même temps. Veuillez vous déconnecter du précédent appareil. Autrement, veuillez contacter le support.'}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({'error': 'L\'utilisateur et le mot de passe de correspondent pas'}, status=status.HTTP_401_UNAUTHORIZED)

@ -146,6 +146,7 @@ class BaseModel(models.Model):
for parent in parents_by_model.values():
if isinstance(parent, BaseModel):
if parent.related_user:
print(f'related_user found in {parent}')
return parent.related_user
else:
return parent.find_related_user(processed_objects)

@ -7,6 +7,10 @@ class ModelOperation(models.TextChoices):
DELETE = 'DELETE', 'DELETE'
GRANT_ACCESS = 'GRANT_ACCESS', 'GRANT_ACCESS'
REVOKE_ACCESS = 'REVOKE_ACCESS', 'REVOKE_ACCESS'
RELATIONSHIP_SET = 'RELATIONSHIP_SET', 'RELATIONSHIP_SET'
RELATIONSHIP_REMOVED = 'RELATIONSHIP_REMOVED', 'RELATIONSHIP_REMOVED'
SHARED_RELATIONSHIP_SET = 'SHARED_RELATIONSHIP_SET', 'SHARED_RELATIONSHIP_SET'
SHARED_RELATIONSHIP_REMOVED = 'SHARED_RELATIONSHIP_REMOVED', 'SHARED_RELATIONSHIP_REMOVED'
class ModelLog(models.Model):
# id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)

@ -204,19 +204,44 @@ def detect_foreign_key_changes(sender, instance):
def process_foreign_key_changes(sender, instance, **kwargs):
### TODO : we want to avoid creating ModelLog for the user making the change, but how?
if hasattr(instance, '_fk_changes'):
for change in instance._fk_changes:
for data_access in change['data_access_list']:
shared = data_access.shared_with.all()
owner = {data_access.related_user}
## exclude last_updated_by from extra notifications
if instance.last_updated_by:
shared = shared.exclude(id=instance.last_updated_by.id)
owner = owner.discard(instance.last_updated_by)
if change['old_value']:
model_name = change['old_value'].__class__.__name__
save_model_log(data_access.concerned_users(), 'REVOKE_ACCESS',
model_name, change['old_value'].id,
change['old_value'].get_store_id())
if shared:
print(f"SHARED_RELATIONSHIP_REMOVED: shared={shared}, model_name={model_name}")
save_model_log(shared, 'SHARED_RELATIONSHIP_REMOVED',
model_name, change['old_value'].id,
change['old_value'].get_store_id())
if owner:
print(f"RELATIONSHIP_REMOVED: owner={owner}, model_name={model_name}")
save_model_log(owner, 'RELATIONSHIP_REMOVED',
model_name, change['old_value'].id,
change['old_value'].get_store_id())
if change['new_value']:
model_name = change['new_value'].__class__.__name__
save_model_log(data_access.concerned_users(), 'GRANT_ACCESS',
model_name, change['new_value'].id,
change['new_value'].get_store_id())
if shared:
print(f"SHARED_RELATIONSHIP_SET: shared={shared}, model_name={model_name}")
save_model_log(shared, 'SHARED_RELATIONSHIP_SET',
model_name, change['new_value'].id,
change['new_value'].get_store_id())
if owner:
print(f"RELATIONSHIP_SET: owner={owner}, model_name={model_name}")
save_model_log(owner, 'RELATIONSHIP_SET',
model_name, change['old_value'].id,
change['old_value'].get_store_id())
### Data Access

@ -11,7 +11,6 @@ from rest_framework import status
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from collections import defaultdict
@ -23,69 +22,69 @@ from .models import ModelLog, BaseModel, SideStoreModel, DataAccess
from .registry import model_registry, device_registry
class HierarchyApiView(APIView):
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 = get_serializer(child, child_model_name)
updates[child_model_name][child.id] = serializer.data
self.add_children_recursively(child, updates)
def add_parents_with_hierarchy_organizer(self, instance, hierarchy_organizer, current_level=0):
"""
Recursively add all parents of an instance to the hierarchy organizer.
Parents are added at a higher level than their children.
"""
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
if isinstance(parent, BaseModel):
# class HierarchyApiView(APIView):
def add_children_recursively(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 = get_serializer(child, child_model_name)
updates[child_model_name][child.id] = serializer.data
add_children_recursively(child, updates)
def add_parents_with_hierarchy_organizer(instance, hierarchy_organizer, current_level=0):
"""
Recursively add all parents of an instance to the hierarchy organizer.
Parents are added at a higher level than their children.
"""
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
if isinstance(parent, BaseModel):
store_id = None
if isinstance(parent, SideStoreModel):
store_id = parent.store_id
parent_data = {
'model_id': parent.id,
'store_id': store_id
}
# Add parent at the next level
hierarchy_organizer.add_item(parent_model_name, parent_data, current_level + 1)
# Recursively process parent's parents
add_parents_with_hierarchy_organizer(parent, hierarchy_organizer, current_level + 1)
def add_parents_recursively(instance, dictionary, minimal=False):
"""
Recursively add all parents of an instance to the updates dictionary.
If minimal=True, only add id and store_id.
"""
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
if isinstance(parent, BaseModel):
if minimal:
store_id = None
if isinstance(parent, SideStoreModel):
store_id = parent.store_id
parent_data = {
dictionary[parent_model_name][parent.id] = {
'model_id': parent.id,
'store_id': store_id
}
else:
serializer = get_serializer(parent, parent_model_name)
dictionary[parent_model_name][parent.id] = serializer.data
add_parents_recursively(parent, dictionary, minimal)
# Add parent at the next level
hierarchy_organizer.add_item(parent_model_name, parent_data, current_level + 1)
# Recursively process parent's parents
self.add_parents_with_hierarchy_organizer(parent, hierarchy_organizer, current_level + 1)
def add_parents_recursively(self, instance, dictionary, minimal=False):
"""
Recursively add all parents of an instance to the updates dictionary.
If minimal=True, only add id and store_id.
"""
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
if isinstance(parent, BaseModel):
if minimal:
store_id = None
if isinstance(parent, SideStoreModel):
store_id = parent.store_id
dictionary[parent_model_name][parent.id] = {
'model_id': parent.id,
'store_id': store_id
}
else:
serializer = get_serializer(parent, parent_model_name)
dictionary[parent_model_name][parent.id] = serializer.data
self.add_parents_recursively(parent, dictionary, minimal)
class SynchronizationApi(HierarchyApiView):
class SynchronizationApi(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
@ -297,61 +296,215 @@ class SynchronizationApi(HierarchyApiView):
logs = self.query_model_logs(last_update, request.user, device_id)
print(f'>>> log count = {len(logs)}')
# First pass: Process logs to collect basic operations
updates, deletions, grant_instances, revoke_info, last_log_date = self.process_logs(logs)
# Process all logs and get response data
result = LogProcessingResult()
result.process_logs(logs)
response_data = result.get_response_data()
# Second pass: Process hierarchies for grants and revocations
grants = self.process_grants(grant_instances)
revocations_parents_organizer = self.process_revocations(revoke_info)
return Response(response_data, status=status.HTTP_200_OK)
response_data = {
"updates": dict(updates),
"deletions": dict(deletions),
"grants": dict(grants),
"revocations": dict(revocations_parents_organizer[0]),
"revocation_parents": revocations_parents_organizer[1].get_organized_data(),
"date": last_log_date
}
# def process_logs(self, logs):
# """Process logs to collect basic operations and handle grant/revoke efficiently."""
# # Create an instance of the LogProcessingResult class
# result = LogProcessingResult()
# last_log_date = None
return Response(response_data, status=status.HTTP_200_OK)
# for log in logs:
# last_log_date = log.date
# try:
# if log.operation in ['POST', 'PUT']:
# data = get_serialized_data(log.model_name, log.model_id)
# result.updates[log.model_name][log.model_id] = data
# elif log.operation == 'DELETE':
# result.deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_id})
# elif log.operation == 'GRANT_ACCESS':
# # Remove any existing revocations for this model_id
# self._remove_revocation(result.revoke_info, log.model_name, log.model_id)
# # Add to grant instances if not already there
# if log.model_id not in result.grant_instances[log.model_name]:
# model = model_registry.get_model(log.model_name)
# try:
# instance = model.objects.get(id=log.model_id)
# result.grant_instances[log.model_name][log.model_id] = instance
# except model.DoesNotExist:
# pass
# elif log.operation == 'REVOKE_ACCESS':
# print(f'revoke access {log.model_id} - {log.store_id}')
def process_logs(self, logs):
"""Process logs to collect basic operations and handle grant/revoke efficiently."""
updates = defaultdict(dict)
deletions = defaultdict(list)
grant_instances = defaultdict(dict) # {model_name: {model_id: instance}}
revoke_info = defaultdict(list) # {model_name: [{model_id, store_id}]}
# # Remove any existing grants for this model_id
# self._remove_grant(result.grant_instances, log.model_name, log.model_id)
# # Add to revocations
# result.revoke_info[log.model_name].append({
# 'model_id': log.model_id,
# 'store_id': log.store_id
# })
# elif log.operation == 'RELATIONSHIP_SET':
# data = get_serialized_data(log.model_name, log.model_id)
# result.relationship_sets[log.model_name][log.model_id] = data
# elif log.operation == 'RELATIONSHIP_REMOVED':
# result.relationship_removals[log.model_name].append({
# 'model_id': log.model_id,
# 'store_id': log.store_id
# })
# elif log.operation == 'SHARED_RELATIONSHIP_SET':
# data = get_serialized_data(log.model_name, log.model_id)
# result.shared_relationship_sets[log.model_name][log.model_id] = data
# elif log.operation == 'SHARED_RELATIONSHIP_REMOVED':
# result.shared_relationship_removals[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 result.updates:
# result.updates[model_name] = list(result.updates[model_name].values())
# return result, last_log_date
# def _remove_revocation(self, revoke_info, model_name, model_id):
# """Remove any revocation entries for the specified model and ID."""
# if model_name in revoke_info:
# revoke_info[model_name] = [
# r for r in revoke_info[model_name]
# if r['model_id'] != model_id
# ]
# # Clean up empty lists
# if not revoke_info[model_name]:
# del revoke_info[model_name]
# def _remove_grant(self, grant_instances, model_name, model_id):
# """Remove any grant entries for the specified model and ID."""
# if model_name in grant_instances and model_id in grant_instances[model_name]:
# del grant_instances[model_name][model_id]
# # Clean up empty dictionaries
# if not grant_instances[model_name]:
# del grant_instances[model_name]
# def process_grants(self, grant_instances):
# """Process grants and their hierarchies."""
# grants = defaultdict(dict)
# # Process each grant instance
# for model_name, instances in grant_instances.items():
# for model_id, instance in instances.items():
# serializer = get_serializer(instance, model_name)
# grants[model_name][model_id] = serializer.data
# # Add hierarchies only once per instance
# self.add_children_recursively(instance, grants)
# self.add_parents_recursively(instance, grants)
# # Convert to lists
# for model_name in grants:
# grants[model_name] = list(grants[model_name].values())
# return grants
# def process_revocations(self, revoke_info):
# """Process revocations and their hierarchies."""
# revocations = defaultdict(list)
# revocations_parents_organizer = HierarchyOrganizer()
# # First, collect all revocations
# for model_name, items in revoke_info.items():
# revocations[model_name].extend(items)
# # Process parent hierarchies for each revoked item
# model = model_registry.get_model(model_name)
# for item in items:
# try:
# instance = model.objects.get(id=item['model_id'])
# self.add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer)
# except model.DoesNotExist:
# pass
# return revocations, revocations_parents_organizer
def query_model_logs(self, last_update, user, device_id):
log_query = Q(date__gt=last_update, user=user)
if device_id:
log_query &= ~Q(device_id=device_id) # exclude query
return ModelLog.objects.filter(log_query).order_by('date')
last_log_date = None
# class LogProcessingResult:
# """Class to hold all the results from log processing."""
# def __init__(self):
# # Initialize all the collections
# self.updates = defaultdict(dict)
# self.deletions = defaultdict(list)
# self.grant_instances = defaultdict(dict) # {model_name: {model_id: instance}}
# self.revoke_info = defaultdict(list) # {model_name: [{model_id, store_id}]}
# self.relationship_sets = defaultdict(dict)
# self.relationship_removals = defaultdict(list)
# self.shared_relationship_sets = defaultdict(dict)
# self.shared_relationship_removals = defaultdict(list)
class LogProcessingResult:
"""Class to handle all log processing and organize the results."""
def __init__(self):
# Initialize all the collections
self.updates = defaultdict(dict)
self.deletions = defaultdict(list)
self.grant_instances = defaultdict(dict) # {model_name: {model_id: instance}}
self.revoke_info = defaultdict(list) # {model_name: [{model_id, store_id}]}
self.relationship_sets = defaultdict(dict)
self.relationship_removals = defaultdict(list)
self.shared_relationship_sets = defaultdict(dict)
self.shared_relationship_removals = defaultdict(list)
self.last_log_date = None
def process_logs(self, logs):
"""Process logs to collect basic operations and handle grant/revoke efficiently."""
for log in logs:
last_log_date = log.date
self.last_log_date = log.date
try:
if log.operation in ['POST', 'PUT']:
data = get_serialized_data(log.model_name, log.model_id)
updates[log.model_name][log.model_id] = data
self.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})
self.deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_id})
elif log.operation == 'GRANT_ACCESS':
# Remove any existing revocations for this model_id
self._remove_revocation(revoke_info, log.model_name, log.model_id)
self._remove_revocation(log.model_name, log.model_id)
# Add to grant instances if not already there
if log.model_id not in grant_instances[log.model_name]:
if log.model_id not in self.grant_instances[log.model_name]:
model = model_registry.get_model(log.model_name)
try:
instance = model.objects.get(id=log.model_id)
grant_instances[log.model_name][log.model_id] = instance
self.grant_instances[log.model_name][log.model_id] = instance
except model.DoesNotExist:
pass
elif log.operation == 'REVOKE_ACCESS':
print(f'revoke access {log.model_id} - {log.store_id}')
# Remove any existing grants for this model_id
self._remove_grant(grant_instances, log.model_name, log.model_id)
self._remove_grant(log.model_name, log.model_id)
# Add to revocations
revoke_info[log.model_name].append({
self.revoke_info[log.model_name].append({
'model_id': log.model_id,
'store_id': log.store_id
})
elif log.operation == 'RELATIONSHIP_SET':
data = get_serialized_data(log.model_name, log.model_id)
self.relationship_sets[log.model_name][log.model_id] = data
elif log.operation == 'RELATIONSHIP_REMOVED':
self.relationship_removals[log.model_name].append({
'model_id': log.model_id,
'store_id': log.store_id
})
elif log.operation == 'SHARED_RELATIONSHIP_SET':
data = get_serialized_data(log.model_name, log.model_id)
self.shared_relationship_sets[log.model_name][log.model_id] = data
elif log.operation == 'SHARED_RELATIONSHIP_REMOVED':
self.shared_relationship_removals[log.model_name].append({
'model_id': log.model_id,
'store_id': log.store_id
})
@ -359,43 +512,43 @@ class SynchronizationApi(HierarchyApiView):
pass
# Convert updates dict to list for each model
for model_name in updates:
updates[model_name] = list(updates[model_name].values())
for model_name in self.updates:
self.updates[model_name] = list(self.updates[model_name].values())
return updates, deletions, grant_instances, revoke_info, last_log_date
# return self
def _remove_revocation(self, revoke_info, model_name, model_id):
def _remove_revocation(self, model_name, model_id):
"""Remove any revocation entries for the specified model and ID."""
if model_name in revoke_info:
revoke_info[model_name] = [
r for r in revoke_info[model_name]
if model_name in self.revoke_info:
self.revoke_info[model_name] = [
r for r in self.revoke_info[model_name]
if r['model_id'] != model_id
]
# Clean up empty lists
if not revoke_info[model_name]:
del revoke_info[model_name]
if not self.revoke_info[model_name]:
del self.revoke_info[model_name]
def _remove_grant(self, grant_instances, model_name, model_id):
def _remove_grant(self, model_name, model_id):
"""Remove any grant entries for the specified model and ID."""
if model_name in grant_instances and model_id in grant_instances[model_name]:
del grant_instances[model_name][model_id]
if model_name in self.grant_instances and model_id in self.grant_instances[model_name]:
del self.grant_instances[model_name][model_id]
# Clean up empty dictionaries
if not grant_instances[model_name]:
del grant_instances[model_name]
if not self.grant_instances[model_name]:
del self.grant_instances[model_name]
def process_grants(self, grant_instances):
def process_grants(self):
"""Process grants and their hierarchies."""
grants = defaultdict(dict)
# Process each grant instance
for model_name, instances in grant_instances.items():
for model_name, instances in self.grant_instances.items():
for model_id, instance in instances.items():
serializer = get_serializer(instance, model_name)
grants[model_name][model_id] = serializer.data
# Add hierarchies only once per instance
self.add_children_recursively(instance, grants)
self.add_parents_recursively(instance, grants)
add_children_recursively(instance, grants)
add_parents_recursively(instance, grants)
# Convert to lists
for model_name in grants:
@ -403,13 +556,13 @@ class SynchronizationApi(HierarchyApiView):
return grants
def process_revocations(self, revoke_info):
def process_revocations(self):
"""Process revocations and their hierarchies."""
revocations = defaultdict(list)
revocations_parents_organizer = HierarchyOrganizer()
# First, collect all revocations
for model_name, items in revoke_info.items():
for model_name, items in self.revoke_info.items():
revocations[model_name].extend(items)
# Process parent hierarchies for each revoked item
@ -417,19 +570,31 @@ class SynchronizationApi(HierarchyApiView):
for item in items:
try:
instance = model.objects.get(id=item['model_id'])
self.add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer)
add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer)
except model.DoesNotExist:
pass
return revocations, revocations_parents_organizer
def query_model_logs(self, last_update, user, device_id):
log_query = Q(date__gt=last_update, user=user)
if device_id:
log_query &= ~Q(device_id=device_id) # exclude query
return ModelLog.objects.filter(log_query).order_by('date')
def get_response_data(self):
"""Construct the complete response data structure."""
grants = self.process_grants()
revocations, revocations_parents_organizer = self.process_revocations()
return {
"updates": dict(self.updates),
"deletions": dict(self.deletions),
"grants": dict(grants),
"revocations": dict(revocations),
"revocation_parents": revocations_parents_organizer.get_organized_data(),
"relationship_sets": dict(self.relationship_sets),
"relationship_removals": dict(self.relationship_removals),
"shared_relationship_sets": dict(self.shared_relationship_sets),
"shared_relationship_removals": dict(self.shared_relationship_removals),
"date": self.last_log_date
}
class UserDataAccessApi(HierarchyApiView):
class UserDataAccessApi(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
@ -454,8 +619,8 @@ class UserDataAccessApi(HierarchyApiView):
data_by_model[data_access.model_name][data_access.model_id] = serializer.data
# Add parents & children recursively
self.add_children_recursively(instance, data_by_model)
self.add_parents_recursively(instance, data_by_model)
add_children_recursively(instance, data_by_model)
add_parents_recursively(instance, data_by_model)
except ObjectDoesNotExist:
continue

@ -86,7 +86,7 @@ class Tournament(BaseModel):
for gs in self.group_stages.all():
gs.delete_dependencies()
gs.delete()
for round in self.rounds.all():
for round in self.rounds.filter(parent=None).all():
round.delete_dependencies()
round.delete()
for draw_log in self.draw_logs.all():

Loading…
Cancel
Save