diff --git a/api/serializers.py b/api/serializers.py index d51473f..a05f817 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -85,6 +85,11 @@ class CustomUserSerializer(serializers.ModelSerializer): ### the one matching th model = CustomUser fields = CustomUser.fields_for_update() +class ShortUserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ['id', 'first_name', 'last_name'] + class ClubSerializer(serializers.ModelSerializer): class Meta: model = Club diff --git a/api/urls.py b/api/urls.py index afff73d..d94a04e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,10 +3,11 @@ from rest_framework import routers from rest_framework.authtoken.views import obtain_auth_token from . import views -from sync.views import SynchronizationApi, DataAccessViewSet +from sync.views import SynchronizationApi, UserDataAccessApi, DataAccessViewSet router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) +router.register(r'users-search', views.ShortUserViewSet) router.register(r'clubs', views.ClubViewSet) router.register(r'tournaments', views.TournamentViewSet) router.register(r'events', views.EventViewSet) @@ -28,6 +29,7 @@ urlpatterns = [ path('', include(router.urls)), path('data/', SynchronizationApi.as_view(), name="data"), + path('user-data-access/', UserDataAccessApi.as_view(), name="user-data-access"), path('api-token-auth/', obtain_auth_token, name='api_token_auth'), path("user-by-token/", views.user_by_token, name="user_by_token"), diff --git a/api/views.py b/api/views.py index a6907a4..d7c7c8d 100644 --- a/api/views.py +++ b/api/views.py @@ -1,4 +1,4 @@ -from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, CustomUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer +from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, CustomUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, ShortUserSerializer 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 @@ -100,6 +100,23 @@ class UserViewSet(viewsets.ModelViewSet): return UserSerializer return self.serializer_class +class ShortUserViewSet(viewsets.ModelViewSet): + queryset = CustomUser.objects.all() + serializer_class = ShortUserSerializer + permission_classes = [] # Users are public whereas the other requests are only for logged users + + def get_queryset(self): + queryset = CustomUser.objects.all() + search_term = self.request.query_params.get('search', None) + + if search_term: + queryset = queryset.filter( + Q(first_name__icontains=search_term) | + Q(last_name__icontains=search_term) + ) + + return queryset + class ClubViewSet(viewsets.ModelViewSet): queryset = Club.objects.all() serializer_class = ClubSerializer @@ -118,11 +135,7 @@ class EventViewSet(viewsets.ModelViewSet): return [] # return self.queryset.filter(creator=self.request.user) return self.queryset.filter( - Q(creator=self.request.user) | - Q(id__in=DataAccess.objects.filter( - shared_with=self.request.user, - model_name=self.queryset.model.__name__ - ).values_list('model_id', flat=True)) + Q(creator=self.request.user) ) class TournamentViewSet(viewsets.ModelViewSet): @@ -134,11 +147,7 @@ class TournamentViewSet(viewsets.ModelViewSet): return [] return self.queryset.filter( - Q(event__creator=self.request.user) | - Q(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 PurchaseViewSet(viewsets.ModelViewSet): @@ -151,12 +160,8 @@ class PurchaseViewSet(viewsets.ModelViewSet): # return self.queryset.filter(user=self.request.user) return self.queryset.filter( - Q(user=self.request.user) | - Q(id__in=DataAccess.objects.filter( - shared_with=self.request.user, - model_name=self.queryset.model.__name__ - ).values_list('model_id', flat=True)) - ) + Q(user=self.request.user) + ) def patch(self, request, pk): raise MethodNotAllowed('PATCH') diff --git a/sync/models/data_access.py b/sync/models/data_access.py index 020f17d..ee8bf25 100644 --- a/sync/models/data_access.py +++ b/sync/models/data_access.py @@ -4,7 +4,7 @@ from django.apps import apps from django.core.exceptions import ObjectDoesNotExist from django.conf import settings - +from ..registry import sync_registry import uuid from . import ModelLog, SideStoreModel, BaseModel @@ -23,26 +23,29 @@ class DataAccess(BaseModel): def create_access_log(self, users, operation): """Create an access log for a list of users """ - 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 + model_class = sync_registry.get_model(self.model_name) + if model_class: + 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(users__in=users, 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: - model_log = ModelLog.objects.create( - model_id=self.model_id, - model_name=self.model_name, - operation=operation, - date=timezone.now(), - store_id=store_id - ) - model_log.users.set(users) - except ObjectDoesNotExist: - pass + existing_log = ModelLog.objects.filter(users__in=users, 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: + model_log = ModelLog.objects.create( + model_id=self.model_id, + model_name=self.model_name, + operation=operation, + date=timezone.now(), + store_id=store_id + ) + model_log.users.set(users) + except ObjectDoesNotExist: + pass + else: + print(f'model not found: {self.model_name}') diff --git a/sync/registry.py b/sync/registry.py index 0a3af6d..100cd60 100644 --- a/sync/registry.py +++ b/sync/registry.py @@ -5,12 +5,10 @@ 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: @@ -27,10 +25,11 @@ class SyncRegistry: return True def register(self, model): - # print(f'>>> Registers {model.__name__}') self._registry[model.__name__] = model def get_model(self, model_name): + if not self._registry: + self.load_sync_apps() return self._registry.get(model_name) # Create singleton instance diff --git a/sync/views.py b/sync/views.py index 86dd28c..40db54a 100644 --- a/sync/views.py +++ b/sync/views.py @@ -21,7 +21,62 @@ from .models import ModelLog, BaseModel, SideStoreModel, DataAccess from .registry import sync_registry -class SynchronizationApi(APIView): +class HierarchyApiView(APIView): + + def add_children_recursively(self, instance, updates): + """ + Recursively add all children of an instance to the updates dictionary. + """ + # print(f"Instance class: {instance.__class__}") + 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) + # serializer_class = build_serializer_class(child_model_name) + # serializer = serializer_class(child) + updates[child_model_name][child.id] = serializer.data + self.add_children_recursively(child, updates) + + # def add_parents_recursively(self, instance, updates): + # parent_models = instance.get_parents_by_model() + + # for parent_model_name, parent in parent_models.items(): + # # print(f'parent = {parent_model_name}') + # if isinstance(parent, BaseModel): + # serializer_class = build_serializer_class(parent_model_name) + # serializer = serializer_class(parent) + # updates[parent_model_name][parent.id] = serializer.data + # self.add_parents_recursively(parent, updates) + + 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) + # Add full serialized data + # serializer_class = build_serializer_class(parent_model_name) + # serializer = serializer_class(parent) + dictionary[parent_model_name][parent.id] = serializer.data + + self.add_parents_recursively(parent, dictionary, minimal) + +class SynchronizationApi(HierarchyApiView): permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): @@ -74,70 +129,6 @@ class SynchronizationApi(APIView): else: return Response(status=status.HTTP_404_NOT_FOUND) - # def get(self, request, *args, **kwargs): - # last_update_str = request.query_params.get('last_update') - # decoded_last_update = unquote(last_update_str) - - # if not decoded_last_update: - # return Response({"error": "last_update parameter is required"}, status=status.HTTP_400_BAD_REQUEST) - - # try: - # last_update = timezone.datetime.fromisoformat(decoded_last_update) - # except ValueError: - # return Response({"error": f"Invalid date format for last_update: {decoded_last_update}"}, status=status.HTTP_400_BAD_REQUEST) - - # logs = self.query_model_logs(last_update, request.user) - - # updates = defaultdict(dict) - # deletions = defaultdict(list) - # grants = defaultdict(dict) - # revocations = defaultdict(list) - # revocation_parents = defaultdict(dict) - - # last_log_date = None - # for log in logs: - # last_log_date = log.date - # self._process_log(log, updates, deletions, grants, revocations, revocation_parents) - - # response_data = { - # "updates": {k: list(v.values()) for k, v in updates.items()}, - # "deletions": dict(deletions), - # "grants": {k: list(v.values()) for k, v in grants.items()}, - # "revocations": dict(revocations), - # "revocation_parents": {k: list(v.values()) for k, v in revocation_parents.items()}, - # "date": last_log_date - # } - - # return Response(response_data, status=status.HTTP_200_OK) - - # def _process_log(self, log, updates, deletions, grants, revocations, revocation_parents): - # 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 - # 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) - # instance = model.objects.get(id=log.model_id) - # serializer = get_serializer(instance, log.model_name) - # grants[log.model_name][log.model_id] = serializer.data - # self.add_children_recursively(instance, grants) - # self.add_parents_recursively(instance, grants) - # elif log.operation == 'REVOKE_ACCESS': - # revocations[log.model_name].append({ - # 'model_id': log.model_id, - # 'store_id': log.store_id - # }) - # model = apps.get_model('tournaments', model_name=log.model_name) - # try: - # instance = model.objects.get(id=log.model_id) - # self.add_parents_recursively(instance, revocation_parents, minimal=True) - # except model.DoesNotExist: - # pass - # except ObjectDoesNotExist: - # pass - def get(self, request, *args, **kwargs): last_update_str = request.query_params.get('last_update') decoded_last_update = unquote(last_update_str) # Decodes %2B into + @@ -235,58 +226,86 @@ class SynchronizationApi(APIView): log_query = Q(date__gt=last_update) & Q(users=user) return ModelLog.objects.filter(log_query).order_by('date') - def add_children_recursively(self, instance, updates): - """ - Recursively add all children of an instance to the updates dictionary. - """ - # print(f"Instance class: {instance.__class__}") - 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) - # serializer_class = build_serializer_class(child_model_name) - # serializer = serializer_class(child) - updates[child_model_name][child.id] = serializer.data - self.add_children_recursively(child, updates) - - # def add_parents_recursively(self, instance, updates): +class UserDataAccessApi(HierarchyApiView): + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + # Get all GRANT_ACCESS and REVOKE_ACCESS logs for the user, ordered by date + all_logs = ModelLog.objects.filter( + Q(users=request.user) & + (Q(operation='GRANT_ACCESS') | Q(operation='REVOKE_ACCESS')) + ).order_by('date') + + # Track latest status for each (model_name, model_id) + active_grants = {} + + # Process logs chronologically to determine current access state + for log in all_logs: + if log.operation == 'GRANT_ACCESS': + active_grants[log.model_id] = log + elif log.operation == 'REVOKE_ACCESS': + if log.model_id in active_grants and active_grants[log.model_id].date < log.date: + del active_grants[log.model_id] + + # Convert active_grants dict to list of grant logs + active_grants = list(active_grants.values()) + + # Prepare response data structure + data_by_model = defaultdict(dict) + + print(f'>>> grants = {len(active_grants)}') + + for log in active_grants: + try: + model = sync_registry.get_model(log.model_name) + instance = model.objects.get(id=log.model_id) + + # Get the base data + serializer = get_serializer(instance, log.model_name) + data_by_model[log.model_name][log.model_id] = serializer.data + + # Add parents & children recursively + self.add_children_recursively(instance, data_by_model) + self.add_parents_recursively(instance, data_by_model) + + except ObjectDoesNotExist: + continue + + # Convert dictionary values to lists + response_data = { + model_name: list(model_data.values()) + for model_name, model_data in data_by_model.items() + } + + print(f'response_data = {response_data}') + + return Response(response_data, status=status.HTTP_200_OK) + + # def _add_children_recursively(self, instance, data_dict): + # """ + # Recursively add all children of an instance to the data 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) + # data_dict[child_model_name][child.id] = serializer.data + # self._add_children_recursively(child, data_dict) + + # def _add_parents_recursively(self, instance, data_dict): + # """ + # Recursively add all parents of an instance to the data dictionary. + # """ # parent_models = instance.get_parents_by_model() # for parent_model_name, parent in parent_models.items(): - # # print(f'parent = {parent_model_name}') # if isinstance(parent, BaseModel): - # serializer_class = build_serializer_class(parent_model_name) - # serializer = serializer_class(parent) - # updates[parent_model_name][parent.id] = serializer.data - # self.add_parents_recursively(parent, updates) + # serializer = get_serializer(parent, parent_model_name) + # data_dict[parent_model_name][parent.id] = serializer.data + # self._add_parents_recursively(parent, data_dict) - 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) - # Add full serialized data - # serializer_class = build_serializer_class(parent_model_name) - # serializer = serializer_class(parent) - dictionary[parent_model_name][parent.id] = serializer.data - - self.add_parents_recursively(parent, dictionary, minimal) class DataAccessViewSet(viewsets.ModelViewSet): queryset = DataAccess.objects.all()