Fixes and improvements

sync
Laurent 11 months ago
parent d50e391d16
commit d0e49971b5
  1. 5
      api/serializers.py
  2. 4
      api/urls.py
  3. 39
      api/views.py
  4. 49
      sync/models/data_access.py
  5. 5
      sync/registry.py
  6. 243
      sync/views.py

@ -85,6 +85,11 @@ class CustomUserSerializer(serializers.ModelSerializer): ### the one matching th
model = CustomUser model = CustomUser
fields = CustomUser.fields_for_update() fields = CustomUser.fields_for_update()
class ShortUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'first_name', 'last_name']
class ClubSerializer(serializers.ModelSerializer): class ClubSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Club model = Club

@ -3,10 +3,11 @@ from rest_framework import routers
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
from . import views from . import views
from sync.views import SynchronizationApi, DataAccessViewSet from sync.views import SynchronizationApi, UserDataAccessApi, DataAccessViewSet
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet) router.register(r'users', views.UserViewSet)
router.register(r'users-search', views.ShortUserViewSet)
router.register(r'clubs', views.ClubViewSet) router.register(r'clubs', views.ClubViewSet)
router.register(r'tournaments', views.TournamentViewSet) router.register(r'tournaments', views.TournamentViewSet)
router.register(r'events', views.EventViewSet) router.register(r'events', views.EventViewSet)
@ -28,6 +29,7 @@ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('data/', SynchronizationApi.as_view(), name="data"), 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('api-token-auth/', obtain_auth_token, name='api_token_auth'),
path("user-by-token/", views.user_by_token, name="user_by_token"), path("user-by-token/", views.user_by_token, name="user_by_token"),

@ -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 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 from rest_framework import viewsets, permissions
@ -100,6 +100,23 @@ class UserViewSet(viewsets.ModelViewSet):
return UserSerializer return UserSerializer
return self.serializer_class 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): class ClubViewSet(viewsets.ModelViewSet):
queryset = Club.objects.all() queryset = Club.objects.all()
serializer_class = ClubSerializer serializer_class = ClubSerializer
@ -118,11 +135,7 @@ class EventViewSet(viewsets.ModelViewSet):
return [] return []
# return self.queryset.filter(creator=self.request.user) # return self.queryset.filter(creator=self.request.user)
return self.queryset.filter( return self.queryset.filter(
Q(creator=self.request.user) | 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))
) )
class TournamentViewSet(viewsets.ModelViewSet): class TournamentViewSet(viewsets.ModelViewSet):
@ -134,11 +147,7 @@ class TournamentViewSet(viewsets.ModelViewSet):
return [] return []
return self.queryset.filter( return self.queryset.filter(
Q(event__creator=self.request.user) | 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))
) )
class PurchaseViewSet(viewsets.ModelViewSet): class PurchaseViewSet(viewsets.ModelViewSet):
@ -151,12 +160,8 @@ class PurchaseViewSet(viewsets.ModelViewSet):
# return self.queryset.filter(user=self.request.user) # return self.queryset.filter(user=self.request.user)
return self.queryset.filter( return self.queryset.filter(
Q(user=self.request.user) | 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))
)
def patch(self, request, pk): def patch(self, request, pk):
raise MethodNotAllowed('PATCH') raise MethodNotAllowed('PATCH')

@ -4,7 +4,7 @@ from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings from django.conf import settings
from ..registry import sync_registry
import uuid import uuid
from . import ModelLog, SideStoreModel, BaseModel from . import ModelLog, SideStoreModel, BaseModel
@ -23,26 +23,29 @@ class DataAccess(BaseModel):
def create_access_log(self, users, operation): def create_access_log(self, users, operation):
"""Create an access log for a list of users """ """Create an access log for a list of users """
model_class = apps.get_model('tournaments', self.model_name) model_class = sync_registry.get_model(self.model_name)
try: if model_class:
obj = model_class.objects.get(id=self.model_id) try:
store_id = None obj = model_class.objects.get(id=self.model_id)
if isinstance(obj, SideStoreModel): store_id = None
store_id = obj.store_id 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() existing_log = ModelLog.objects.filter(users__in=users, model_id=self.model_id, operation=operation).first()
if existing_log: if existing_log:
existing_log.date = timezone.now() existing_log.date = timezone.now()
existing_log.model_operation = operation existing_log.model_operation = operation
existing_log.save() existing_log.save()
else: else:
model_log = ModelLog.objects.create( model_log = ModelLog.objects.create(
model_id=self.model_id, model_id=self.model_id,
model_name=self.model_name, model_name=self.model_name,
operation=operation, operation=operation,
date=timezone.now(), date=timezone.now(),
store_id=store_id store_id=store_id
) )
model_log.users.set(users) model_log.users.set(users)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
else:
print(f'model not found: {self.model_name}')

@ -5,12 +5,10 @@ from .models import BaseModel
class SyncRegistry: class SyncRegistry:
def __init__(self): def __init__(self):
self._registry = {} self._registry = {}
self.load_sync_apps()
def load_sync_apps(self): def load_sync_apps(self):
sync_apps = getattr(settings, 'SYNC_APPS', {}) sync_apps = getattr(settings, 'SYNC_APPS', {})
for app_label, config in sync_apps.items(): for app_label, config in sync_apps.items():
print(f'app_label = {app_label}')
app_models = apps.get_app_config(app_label).get_models() app_models = apps.get_app_config(app_label).get_models()
for model in app_models: for model in app_models:
if hasattr(model, '_meta') and not model._meta.abstract: if hasattr(model, '_meta') and not model._meta.abstract:
@ -27,10 +25,11 @@ class SyncRegistry:
return True return True
def register(self, model): def register(self, model):
# print(f'>>> Registers {model.__name__}')
self._registry[model.__name__] = model self._registry[model.__name__] = model
def get_model(self, model_name): def get_model(self, model_name):
if not self._registry:
self.load_sync_apps()
return self._registry.get(model_name) return self._registry.get(model_name)
# Create singleton instance # Create singleton instance

@ -21,7 +21,62 @@ from .models import ModelLog, BaseModel, SideStoreModel, DataAccess
from .registry import sync_registry 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] permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -74,70 +129,6 @@ class SynchronizationApi(APIView):
else: else:
return Response(status=status.HTTP_404_NOT_FOUND) 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): def get(self, request, *args, **kwargs):
last_update_str = request.query_params.get('last_update') last_update_str = request.query_params.get('last_update')
decoded_last_update = unquote(last_update_str) # Decodes %2B into + 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) log_query = Q(date__gt=last_update) & Q(users=user)
return ModelLog.objects.filter(log_query).order_by('date') return ModelLog.objects.filter(log_query).order_by('date')
def add_children_recursively(self, instance, updates): class UserDataAccessApi(HierarchyApiView):
""" permission_classes = [IsAuthenticated]
Recursively add all children of an instance to the updates dictionary.
""" def get(self, request, *args, **kwargs):
# print(f"Instance class: {instance.__class__}") # Get all GRANT_ACCESS and REVOKE_ACCESS logs for the user, ordered by date
child_models = instance.get_children_by_model() all_logs = ModelLog.objects.filter(
Q(users=request.user) &
for child_model_name, children in child_models.items(): (Q(operation='GRANT_ACCESS') | Q(operation='REVOKE_ACCESS'))
for child in children: ).order_by('date')
if isinstance(child, BaseModel):
serializer = get_serializer(child, child_model_name) # Track latest status for each (model_name, model_id)
# serializer_class = build_serializer_class(child_model_name) active_grants = {}
# serializer = serializer_class(child)
updates[child_model_name][child.id] = serializer.data # Process logs chronologically to determine current access state
self.add_children_recursively(child, updates) for log in all_logs:
if log.operation == 'GRANT_ACCESS':
# def add_parents_recursively(self, instance, updates): 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() # parent_models = instance.get_parents_by_model()
# for parent_model_name, parent in parent_models.items(): # for parent_model_name, parent in parent_models.items():
# # print(f'parent = {parent_model_name}')
# if isinstance(parent, BaseModel): # if isinstance(parent, BaseModel):
# serializer_class = build_serializer_class(parent_model_name) # serializer = get_serializer(parent, parent_model_name)
# serializer = serializer_class(parent) # data_dict[parent_model_name][parent.id] = serializer.data
# updates[parent_model_name][parent.id] = serializer.data # self._add_parents_recursively(parent, data_dict)
# 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): class DataAccessViewSet(viewsets.ModelViewSet):
queryset = DataAccess.objects.all() queryset = DataAccess.objects.all()

Loading…
Cancel
Save