from django.db.models.fields.related_lookups import RelatedExact 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.db.models import Q from django.core.exceptions import ObjectDoesNotExist from collections import defaultdict from .utils import build_serializer_class, get_data, get_serialized_data from tournaments.models import ModelLog, DataAccess, BaseModel 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) now = timezone.localtime(timezone.now()) try: data_id = data.get('id') instance = get_data('tournaments', model_name, data_id) if model_operation == 'DELETE': parent_model, parent_id = instance.get_parent_reference() return self.delete_and_save_log(request, data_id, model_operation, model_name, store_id, now) else: # PUT 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') return self.save_and_create_log(request, serializer, model_operation, model_name, store_id, now) 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) except model.DoesNotExist: # POST print('>>> insert') serializer = serializer_class(data=data, context={'request': request}) if serializer.is_valid(): return self.save_and_create_log(request, serializer, model_operation, model_name, store_id, now) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def save_and_create_log(self, request, serializer, model_operation, model_name, store_id, date): instance = serializer.save() self.save_model_log( user=request.user, model_operation=model_operation, model_name=model_name, model_id=instance.id, store_id=store_id, date=date ) self.update_linked_data_access(instance, date) return Response(serializer.data, status=status.HTTP_201_CREATED) def delete_and_save_log(self, request, data_id, model_operation, model_name, store_id, date): instance = get_data('tournaments', model_name, data_id) self.update_linked_data_access(instance, date) instance.delete() # We delete all previous logs linked to the instance because they won't be needed anymore ModelLog.objects.filter(model_id=data_id).delete() self.save_model_log( user=request.user, model_operation=model_operation, model_name=model_name, model_id=data_id, store_id=store_id, date=date ) return Response(status=status.HTTP_204_NO_CONTENT) def update_linked_data_access(self, instance, date): related_instances = instance.related_instances() related_ids = [ri.id for ri in instance.related_instances()] related_ids.append(instance.id) data_access_list = DataAccess.objects.filter(model_id__in=related_ids) for data_access in data_access_list: data_access.last_hierarchy_update = date data_access.save() def save_model_log(self, user, model_operation, model_name, model_id, store_id, date): existing_log = ModelLog.objects.filter(user=user, model_id=model_id, operation=model_operation).first() if existing_log: existing_log.date = date existing_log.model_operation = model_operation existing_log.save() else: model_log = ModelLog() model_log.user = user model_log.operation = model_operation model_log.date = date model_log.model_name = model_name model_log.model_id = model_id model_log.store_id = store_id model_log.save() def get(self, request, *args, **kwargs): last_update = request.query_params.get('last_update') if not last_update: return Response({"error": "last_update parameter is required"}, status=status.HTTP_400_BAD_REQUEST) print(f'/data GET: {last_update}') logs = self.query_model_logs(last_update, request.user) updates = defaultdict(dict) deletions = defaultdict(list) # print(f'>>> log count = {len(logs)}') for log in logs: 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_class = build_serializer_class(log.model_name) serializer = serializer_class(instance) # data = get_serialized_data('tournaments', log.model_name, log.model_id) updates[log.model_name][log.model_id] = serializer.data # instance = model.objects.get(id=log.model_id) self.add_children_recursively(instance, updates) self.add_parents_recursively(instance, updates) elif log.operation == 'delete data access signal': print(f'revoke access {log.model_id} - {log.store_id}') deletions[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 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] response_data = { "updates": dict(updates), "deletions": dict(deletions) } return Response(response_data, status=status.HTTP_200_OK) def query_model_logs(self, last_update, user): try: last_update = timezone.datetime.fromisoformat(last_update) except ValueError: return Response({"error": f"Invalid date format for last_update: {last_update}"}, status=status.HTTP_400_BAD_REQUEST) # get recently modified DataAccess data_access_query = Q(last_hierarchy_update__gt=last_update) & (Q(shared_with__in=[user]) | Q(owner=user)) data_access_list = DataAccess.objects.filter(data_access_query) #.values_list('model_id', flat=True) # print(f'>> da count = {len(data_access_list)}') # get ids of all recently updated related instances of each shared data model_ids = [] for data_access in data_access_list: model_ids.append(data_access.model_id) try: instance = get_data('tournaments', data_access.model_name, data_access.model_id) related_instances = instance.related_instances() related_ids = [ri.id for ri in instance.related_instances() if ri.last_update > last_update] model_ids.extend(related_ids) except ObjectDoesNotExist: pass # get all ModelLog list since the last_update, from the user and from the data he has access to log_query = Q(date__gt=last_update) & (Q(user=user) | Q(model_id__in=model_ids)) 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_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 get_data(self, model, log): # instance = model.objects.get(id=log.model_id) # serializer_class = build_serializer_class(log.model_name) # serializer = serializer_class(instance) # return serializer.data