From 94580cdf7304cda95672098ea12132d34d801299 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 21 May 2025 10:57:53 +0200 Subject: [PATCH] sync improvements and custom children sharing --- padelclub_backend/settings_app.py | 5 ++ sync/models/data_access.py | 2 +- sync/models/model_log.py | 4 +- sync/signals.py | 4 +- sync/views.py | 89 ++++++++++++++++++++----------- tournaments/models/__init__.py | 2 +- tournaments/models/enums.py | 12 ++--- 7 files changed, 76 insertions(+), 42 deletions(-) diff --git a/padelclub_backend/settings_app.py b/padelclub_backend/settings_app.py index f0239b3..42daeb4 100644 --- a/padelclub_backend/settings_app.py +++ b/padelclub_backend/settings_app.py @@ -41,6 +41,11 @@ SYNC_APPS = { 'tournaments': { 'exclude': ['Log', 'FailedApiCall', 'DeviceToken'] } } +SYNC_MODEL_CHILDREN_SHARING = { + 'Match': {'team_scores', 'team_registration', 'player_registrations'} +} + + STRIPE_CURRENCY = 'eur' # Add managers who should receive internal emails SHOP_MANAGERS = [ diff --git a/sync/models/data_access.py b/sync/models/data_access.py index 53039a5..dedf04f 100644 --- a/sync/models/data_access.py +++ b/sync/models/data_access.py @@ -22,7 +22,7 @@ class DataAccess(BaseModel): pass def create_revoke_access_log(self): - self.create_access_log(self.shared_with.all(), 'REVOKE_ACCESS') + self.create_access_log(self.shared_with.all(), 'REVOKED_ACCESS') def concerned_users(self): users = list(self.shared_with.all()) diff --git a/sync/models/model_log.py b/sync/models/model_log.py index cd4e801..daab4d0 100644 --- a/sync/models/model_log.py +++ b/sync/models/model_log.py @@ -5,8 +5,8 @@ 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' + SHARED_ACCESS = 'SHARED_ACCESS', 'SHARED_ACCESS' + REVOKED_ACCESS = 'REVOKED_ACCESS', 'REVOKED_ACCESS' SHARED_RELATIONSHIP_SET = 'SHARED_RELATIONSHIP_SET', 'SHARED_RELATIONSHIP_SET' SHARED_RELATIONSHIP_REMOVED = 'SHARED_RELATIONSHIP_REMOVED', 'SHARED_RELATIONSHIP_REMOVED' diff --git a/sync/signals.py b/sync/signals.py index bf4570c..4154091 100644 --- a/sync/signals.py +++ b/sync/signals.py @@ -259,9 +259,9 @@ 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') + instance.create_access_log(users, 'SHARED_ACCESS') elif action == "post_remove": - instance.create_access_log(users, 'REVOKE_ACCESS') + instance.create_access_log(users, 'REVOKED_ACCESS') device_id = device_registry.get_device_id(instance.id) for user_id in pk_set: diff --git a/sync/views.py b/sync/views.py index 2ab7108..d6b4697 100644 --- a/sync/views.py +++ b/sync/views.py @@ -1,7 +1,6 @@ # from django.shortcuts import render import re import json -from .serializers import DataAccessSerializer from rest_framework import viewsets from rest_framework.views import APIView @@ -9,6 +8,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status +from django.conf import settings from django.utils import timezone from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q @@ -16,6 +16,7 @@ from django.db.models import Q from collections import defaultdict from urllib.parse import unquote +from .serializers import DataAccessSerializer from .utils import get_serializer, build_serializer_class, get_data, get_serialized_data, HierarchyOrganizer from .models import ModelLog, BaseModel, SideStoreModel, DataAccess @@ -24,7 +25,42 @@ from .registry import model_registry, device_registry # class HierarchyApiView(APIView): -def add_children_recursively(instance, updates): +def add_children_hierarchy(instance, models_dict): + sync_models = getattr(settings, 'SYNC_MODEL_CHILDREN_SHARING', {}) + if instance.__class__.__name__ in sync_models: + relationships = sync_models[instance.__class__.__name__] + # 'Match': {'team_scores', 'team_registration', 'player_registrations'} + + # print(f'relationships = {relationships}') + current = [instance] + for relationship in relationships: + # print(f'relationship = {relationship}') + values = [] + for item in current: + value = getattr(item, relationship) + + if hasattr(value, 'all') and callable(value.all): + # This is a queryset from a reverse relationship + for related_obj in value.all(): + child_model_name = related_obj.__class__.__name__ + serializer = get_serializer(related_obj, child_model_name) + models_dict[child_model_name][related_obj.id] = serializer.data + # print(f'>>> 1 Added child for {relationship}: {child_model_name}') + values.extend(value.all()) + else: + # This is a single object + child_model_name = value.__class__.__name__ + serializer = get_serializer(value, child_model_name) + models_dict[child_model_name][value.id] = serializer.data + # print(f'>>> 2 Added child for {relationship}: {child_model_name}') + + values.append(value) + current = values + + else: + add_children_recursively(instance, models_dict) + +def add_children_recursively(instance, models_dict): """ Recursively add all children of an instance to the updates dictionary. """ @@ -33,10 +69,10 @@ def add_children_recursively(instance, updates): for child_model_name, children in child_models.items(): for child in children: if isinstance(child, BaseModel): - print(f'add_children_recursively: {child_model_name}') + # print(f'add_children_recursively: {child_model_name}') serializer = get_serializer(child, child_model_name) - updates[child_model_name][child.id] = serializer.data - add_children_recursively(child, updates) + models_dict[child_model_name][child.id] = serializer.data + add_children_recursively(child, models_dict) def add_parents_with_hierarchy_organizer(instance, hierarchy_organizer, current_level=0): """ @@ -63,28 +99,17 @@ def add_parents_with_hierarchy_organizer(instance, hierarchy_organizer, current_ # Recursively process parent's parents add_parents_with_hierarchy_organizer(parent, hierarchy_organizer, current_level + 1) -def add_parents_recursively(instance, dictionary, minimal=False): +def add_parents_recursively(instance, dictionary): """ 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 - - add_parents_recursively(parent, dictionary, minimal) + serializer = get_serializer(parent, parent_model_name) + dictionary[parent_model_name][parent.id] = serializer.data + add_parents_recursively(parent, dictionary) class SynchronizationApi(APIView): permission_classes = [IsAuthenticated] @@ -224,6 +249,7 @@ class LogProcessingResult: # Initialize all the collections self.updates = defaultdict(dict) self.deletions = defaultdict(list) + self.shared_instances = defaultdict(dict) # {model_name: {model_id: instance}} self.grant_instances = defaultdict(dict) # {model_name: {model_id: instance}} self.revoke_info = defaultdict(list) # {model_name: [{model_id, store_id}]} self.shared_relationship_sets = defaultdict(dict) @@ -240,7 +266,7 @@ class LogProcessingResult: self.updates[log.model_name][log.model_id] = data elif log.operation == 'DELETE': self.deletions[log.model_name].append(log.data_identifier_dict()) - elif log.operation == 'GRANT_ACCESS': + elif log.operation == 'SHARED_ACCESS': # Remove any existing revocations for this model_id self._remove_revocation(log.model_name, log.model_id) @@ -249,10 +275,10 @@ class LogProcessingResult: model = model_registry.get_model(log.model_name) try: instance = model.objects.get(id=log.model_id) - self.grant_instances[log.model_name][log.model_id] = instance + self.shared_instances[log.model_name][log.model_id] = instance except model.DoesNotExist: pass - elif log.operation == 'REVOKE_ACCESS': + elif log.operation == 'REVOKED_ACCESS': print(f'revoke access {log.model_id} - {log.store_id}') # Remove any existing grants for this model_id @@ -300,25 +326,27 @@ class LogProcessingResult: if not self.grant_instances[model_name]: del self.grant_instances[model_name] - def process_grants(self): + def process_shared(self): """Process grants and their hierarchies.""" + shared = defaultdict(dict) grants = defaultdict(dict) # Process each grant instance - for model_name, instances in self.grant_instances.items(): + for model_name, instances in self.shared_instances.items(): for model_id, instance in instances.items(): serializer = get_serializer(instance, model_name) - grants[model_name][model_id] = serializer.data + shared[model_name][model_id] = serializer.data - # Add hierarchies only once per instance - add_children_recursively(instance, grants) + add_children_hierarchy(instance, grants) add_parents_recursively(instance, grants) # Convert to lists + for model_name in shared: + shared[model_name] = list(shared[model_name].values()) for model_name in grants: grants[model_name] = list(grants[model_name].values()) - return grants + return shared, grants def process_revocations(self): """Process revocations and their hierarchies.""" @@ -347,7 +375,7 @@ class LogProcessingResult: def get_response_data(self): """Construct the complete response data structure.""" - grants = self.process_grants() + shared, grants = self.process_shared() revocations, revocations_parents_organizer = self.process_revocations() # print(f'self.deletions = {dict(self.deletions)}') @@ -357,6 +385,7 @@ class LogProcessingResult: return { "updates": dict(self.updates), "deletions": dict(self.deletions), + "shared": dict(shared), "grants": dict(grants), "revocations": dict(revocations), "revocation_parents": revocations_parents_organizer.get_organized_data(), diff --git a/tournaments/models/__init__.py b/tournaments/models/__init__.py index b093bda..f75a1fa 100644 --- a/tournaments/models/__init__.py +++ b/tournaments/models/__init__.py @@ -5,7 +5,7 @@ from .custom_user import CustomUser from .club import Club from .court import Court from .date_interval import DateInterval -from .enums import UserOrigin, TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory, OnlineRegistrationStatus, RegistrationStatus, ModelOperation +from .enums import UserOrigin, TournamentPayment, FederalCategory, FederalLevelCategory, FederalAgeCategory, FederalMatchCategory, OnlineRegistrationStatus, RegistrationStatus from .player_enums import PlayerSexType, PlayerDataSource, PlayerPaymentType from .event import Event from .tournament import Tournament, TeamSummon, TeamSortingType, TeamItem diff --git a/tournaments/models/enums.py b/tournaments/models/enums.py index 22abf9e..29746e0 100644 --- a/tournaments/models/enums.py +++ b/tournaments/models/enums.py @@ -168,12 +168,12 @@ class FederalMatchCategory(models.IntegerChoices): else: return 3 -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 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 OnlineRegistrationStatus(models.IntegerChoices): OPEN = 1, 'Open'