diff --git a/authentication/views.py b/authentication/views.py index 2236557..f8ba5c0 100644 --- a/authentication/views.py +++ b/authentication/views.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) diff --git a/sync/models/base.py b/sync/models/base.py index 95ee41c..cf90f58 100644 --- a/sync/models/base.py +++ b/sync/models/base.py @@ -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) diff --git a/sync/models/model_log.py b/sync/models/model_log.py index 31ed0a9..18ebf7e 100644 --- a/sync/models/model_log.py +++ b/sync/models/model_log.py @@ -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) diff --git a/sync/signals.py b/sync/signals.py index c654b1a..ae3650d 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -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 diff --git a/sync/views.py b/sync/views.py index 350735a..37557f3 100644 --- a/sync/views.py +++ b/sync/views.py @@ -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 diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index e68c5eb..6b5696e 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -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():