from django.shortcuts import render from .serializers import DataAccessSerializer from django.db.models import Q from rest_framework import viewsets from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status from django.apps import apps from django.utils import timezone from django.core.exceptions import ObjectDoesNotExist from collections import defaultdict from urllib.parse import unquote from .utils import get_serializer, build_serializer_class, get_data, get_serialized_data from .models import ModelLog, BaseModel, SideStoreModel, DataAccess from .registry import sync_registry class DataApi(APIView): permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): # unfold content model_operation = request.data.get('operation') model_name = request.data.get('model_name') data = request.data.get('data') # store_id = request.data.get('store_id') 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) model = sync_registry.get_model(model_name) if model_operation == 'POST': serializer = serializer_class(data=data, context={'request': request}) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif model_operation == 'PUT': data_id = data.get('id') instance = get_data(model_name, data_id) serializer = serializer_class(instance, data=data, context={'request': request}) if serializer.is_valid(): if instance.last_update <= serializer.validated_data.get('last_update'): print('>>> update') serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) else: print('>>> return 203') return Response(serializer.data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif model_operation == 'DELETE': data_id = data.get('id') try: instance = get_data(model_name, data_id) instance.delete() return Response(status=status.HTTP_204_NO_CONTENT) except model.DoesNotExist: # POST delete = ModelLog.objects.filter(model_id=data_id, operation=model_operation).first() if delete: return Response(status=status.HTTP_208_ALREADY_REPORTED) 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 + # print(f'last_update_str = {last_update_str}') # print(f'decoded_last_update = {decoded_last_update}') 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) print(f'/data GET: {last_update}') logs = self.query_model_logs(last_update, request.user) print(f'>>> log count = {len(logs)}') updates = defaultdict(dict) deletions = defaultdict(list) grants = defaultdict(dict) revocations = defaultdict(list) # New dictionary for revocations revocation_parents = defaultdict(dict) last_log_date = None for log in logs: print(f'log date = {log.date}') 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 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 = sync_registry.get_model(log.model_name) # model = apps.get_model('tournaments', model_name=log.model_name) instance = model.objects.get(id=log.model_id) # serializer_class = build_serializer_class(log.model_name) # serializer = serializer_class(instance) serializer = get_serializer(instance, log.model_name) grants[log.model_name][log.model_id] = serializer.data # instance = model.objects.get(id=log.model_id) self.add_children_recursively(instance, grants) self.add_parents_recursively(instance, grants) elif log.operation == 'REVOKE_ACCESS': print(f'revoke access {log.model_id} - {log.store_id}') # Add to revocations instead of deletions revocations[log.model_name].append({ 'model_id': log.model_id, 'store_id': log.store_id }) # Get the model instance and add its parents to revocation_parents model = sync_registry.get_model(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 # Convert updates dict to list for each model for model_name in updates: updates[model_name] = list(updates[model_name].values()) # Convert deletions set to list for each model for model_name in deletions: deletions[model_name] = deletions[model_name] for model_name in grants: grants[model_name] = list(grants[model_name].values()) for model_name in revocation_parents: revocation_parents[model_name] = list(revocation_parents[model_name].values()) response_data = { "updates": dict(updates), "deletions": dict(deletions), "grants": dict(grants), "revocations": dict(revocations), "revocation_parents": dict(revocation_parents), "date": last_log_date } print(f'response_data = {response_data}') return Response(response_data, status=status.HTTP_200_OK) def query_model_logs(self, last_update, user): # print(f'last_update = {last_update}') 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): # 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 DataAccessViewSet(viewsets.ModelViewSet): queryset = DataAccess.objects.all() serializer_class = DataAccessSerializer def get_queryset(self): if self.request.user: return self.queryset.filter(Q(owner=self.request.user) | Q(shared_with__in=[self.request.user])) return []