Razmig Sarkissian 4 months ago
commit 6d27c10add
  1. 18
      sync/migrations/0007_dataaccess_store_id.py
  2. 18
      sync/migrations/0008_alter_dataaccess_store_id.py
  3. 194
      sync/model_manager.py
  4. 98
      sync/models/base.py
  5. 78
      sync/models/data_access.py
  6. 13
      sync/registry.py
  7. 164
      sync/signals.py
  8. 44
      sync/utils.py
  9. 115
      sync/views.py
  10. 2
      tournaments/models/event.py
  11. 24
      tournaments/models/match.py
  12. 32
      tournaments/models/team_score.py

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-06-12 13:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0006_alter_modellog_operation'),
]
operations = [
migrations.AddField(
model_name='dataaccess',
name='store_id',
field=models.CharField(default='', max_length=100),
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.1 on 2025-06-26 12:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0007_dataaccess_store_id'),
]
operations = [
migrations.AlterField(
model_name='dataaccess',
name='store_id',
field=models.CharField(blank=True, default='', max_length=100, null=True),
),
]

@ -0,0 +1,194 @@
from django.conf import settings
from typing import List, Dict
from .registry import model_registry
import logging
logger = logging.getLogger(__name__)
class SyncModelChildrenManager:
"""
Manager class for handling model children sharing configuration.
Reads the SYNC_MODEL_CHILDREN_SHARING setting once and builds a bidirectional
relationship graph for efficient lookup.
"""
def __init__(self):
"""Initialize the manager by reading the Django setting and building the relationship graph."""
self._model_relationships = getattr(
settings,
'SYNC_MODEL_CHILDREN_SHARING',
{}
)
self._relationship_graph = {}
def _build_relationship_graph(self) -> Dict[str, List[List[str]]]:
"""
Build a bidirectional relationship graph.
Returns:
Dict[str, List[List[str]]]: Dictionary where keys are model names and values
are lists of relationship paths (arrays of relationship names).
"""
graph = {}
# Add direct relationships (original models to their children)
for model_name, relationships in self._model_relationships.items():
if model_name not in graph:
graph[model_name] = []
graph[model_name].append(relationships)
# Build reverse relationships (children back to original models)
for original_model_name, relationships in self._model_relationships.items():
try:
current_model = model_registry.get_model(original_model_name)
if current_model is None:
print(f'missing {original_model_name}')
continue
current_reverse_path = []
for relationship_name in relationships:
# Get the related model through _meta
try:
field = None
# Try to find the field in the model's _meta
for f in current_model._meta.get_fields():
if hasattr(f, 'related_name') and f.related_name == relationship_name:
field = f
break
elif hasattr(f, 'name') and f.name == relationship_name:
field = f
break
if field is None:
continue
# Get the related model
if hasattr(field, 'related_model'):
related_model = field.related_model
elif hasattr(field, 'model'):
related_model = field.model
else:
continue
related_model_name = related_model.__name__
# Find the reverse relationship name
reverse_relationship_name = self._find_reverse_relationship(
relationship_name, current_model, related_model
)
if reverse_relationship_name:
current_reverse_path.append(reverse_relationship_name)
# Add the reverse path
if related_model_name not in graph:
graph[related_model_name] = []
# The path back is just the reverse relationship name
graph[related_model_name].append(current_reverse_path[::-1]) # make a reverse copy
current_model = related_model
except Exception as e:
logger.info(f'error 2 > {e}')
# Skip problematic relationships
continue
except Exception as e:
logger.info(f'error > {e}')
continue
return graph
def _find_reverse_relationship(self, original_relationship_name, in_model, for_model):
"""
Find the reverse relationship name from from_model to to_model.
Args:
from_model: The model to search relationships from
to_model: The target model to find relationship to
original_relationship_name: The original relationship name for context
Returns:
str or None: The reverse relationship name if found
"""
print(f'reverse of {original_relationship_name} in = {in_model} for {for_model} ')
try:
for field in for_model._meta.get_fields():
# Check ForeignKey, OneToOneField fields
# print(f'{for_model} : field name = {field.name} / field.related_model = {field.related_model == in_model}')
if hasattr(field, 'related_model') and field.related_model == in_model:
return field.name
### possible improvements to do here if multiple relationships of the same type
# Check if this field has a related_name that matches our original relationship
# if hasattr(field, 'related_name') and field.related_name == original_relationship_name:
# # This is the reverse of our original relationship
# return field.name
# elif not hasattr(field, 'related_name') or field.related_name is None:
# default_name = f"{in_model._meta.related_query_name}"
# print(f'no related name: {default_name} / {original_relationship_name.rstrip('s')} ')
# if default_name == original_relationship_name.rstrip('s'): # Simple heuristic
# return field.name
# Check reverse relationships
if hasattr(field, 'field') and hasattr(field.field, 'model'):
if field.field.model == in_model:
if field.get_accessor_name() == original_relationship_name:
return field.field.name
except Exception as e:
print(f'!! ERROR = {e}')
pass
return None
def get_relationships(self, model_name: str) -> List[str]:
"""
Get the list of direct relationships for a given model name.
Args:
model_name (str): The name of the model to look up
Returns:
List[str]: List of relationship names for the model.
Returns empty list if model is not found.
"""
return self._model_relationships.get(model_name, [])
def get_relationship_paths(self, model_name: str) -> List[List[str]]:
"""
Get all relationship paths for a given model name.
This includes both direct relationships and reverse paths.
Args:
model_name (str): The name of the model to look up
Returns:
List[List[str]]: List of relationship paths (each path is a list of relationship names).
Returns empty list if model is not found.
"""
if not self._relationship_graph:
self._relationship_graph = self._build_relationship_graph()
# logger.info(f'self._relationship_graph = {self._relationship_graph}')
return self._relationship_graph.get(model_name, [])
def get_relationship_graph(self) -> Dict[str, List[List[str]]]:
"""
Get the complete relationship graph.
Returns:
Dict[str, List[List[str]]]: The complete relationship graph
"""
return self._relationship_graph.copy()
# Create a singleton instance to use throughout the application
sync_model_manager = SyncModelChildrenManager()

@ -1,9 +1,13 @@
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from typing import List, Set from typing import List, Set
from ..model_manager import sync_model_manager
import logging
from django.apps import apps logger = logging.getLogger(__name__)
class BaseModel(models.Model): class BaseModel(models.Model):
creation_date = models.DateTimeField(default=now, editable=False) creation_date = models.DateTimeField(default=now, editable=False)
@ -28,22 +32,28 @@ class BaseModel(models.Model):
else: else:
return None return None
def data_identifier_dict(self):
return {
'model_id': self.id,
'store_id': None
}
def update_data_access_list(self): def update_data_access_list(self):
related_instances = self.related_instances() related_instances = self.sharing_related_instances()
related_ids = [ri.id for ri in related_instances] data_access_ids = set()
related_ids.append(self.id) for instance in related_instances:
if isinstance(instance, BaseModel) and instance.data_access_ids:
DataAccess = apps.get_model('sync', 'DataAccess') data_access_ids.update(instance.data_access_ids)
data_accesses = DataAccess.objects.filter(model_id__in=related_ids)
for data_access in data_accesses: # print(f'related_instances = {related_instances}')
self.add_data_access_relation(data_access) # data_access_ids = [instance.data_access_ids for instance in related_instances if isinstance(instance, BaseModel)]
# data_access_ids.extend(self.data_access_ids)
# add data_access to children who might not had the relationship self.data_access_ids = list(data_access_ids)
# if data_accesses:
# for child in self.get_children_by_model(): # DataAccess = apps.get_model('sync', 'DataAccess')
# if len(child.data_access_ids) == 0: # data_accesses = DataAccess.objects.filter(model_id__in=related_ids)
# for data_access in data_accesses: # for data_access in data_accesses:
# self.add_data_access_relation(data_access) # self.add_data_access_relation(data_access)
def add_data_access_relation(self, data_access): def add_data_access_relation(self, data_access):
str_id = str(data_access.id) str_id = str(data_access.id)
@ -187,8 +197,64 @@ class BaseModel(models.Model):
return None return None
def sharing_related_instances(self):
"""
Get all related instances (both children and parents) recursively
"""
instances = []
processed_objects = set()
instances.extend(self.get_shared_children(processed_objects))
processed_objects = set()
instances.extend(self.get_recursive_parents(processed_objects))
return instances
def get_shared_children(self, processed_objects):
relationships_arrays = sync_model_manager.get_relationship_paths(self.__class__.__name__)
instances = []
if relationships_arrays:
for relationships in relationships_arrays:
children = self.get_shared_children_from_relationships(relationships, processed_objects)
instances.extend(children)
else:
instances.extend(self.get_recursive_children(processed_objects))
return instances
def get_shared_children_from_relationships(self, relationships, processed_objects):
print(f'>>> {self.__class__.__name__} : relationships = {relationships}')
current = [self]
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():
processed_objects.add(related_obj)
values.extend(value.all())
else:
processed_objects.add(value)
values.append(value)
current = values
# logger.info(f'+++ shared children = {processed_objects}')
return processed_objects
class SideStoreModel(BaseModel): class SideStoreModel(BaseModel):
store_id = models.CharField(max_length=100, default="") # a value matching LeStorage directory sub-stores. Matches the name of the directory. store_id = models.CharField(max_length=100, default="") # a value matching LeStorage directory sub-stores. Matches the name of the directory.
class Meta: class Meta:
abstract = True abstract = True
def data_identifier_dict(self):
return {
'model_id': self.id,
'store_id': self.store_id
}

@ -1,13 +1,13 @@
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
# 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 django.db import transaction
from ..registry import model_registry from ..registry import model_registry
import uuid import uuid
from . import ModelLog, SideStoreModel, BaseModel from . import ModelLog, BaseModel
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,6 +17,7 @@ class DataAccess(BaseModel):
shared_with = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='shared_data') shared_with = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='shared_data')
model_name = models.CharField(max_length=50) model_name = models.CharField(max_length=50)
model_id = models.UUIDField() model_id = models.UUIDField()
store_id = models.CharField(max_length=100, default="", blank=True, null=True) # a value matching LeStorage directory sub-stores. Matches the name of the directory.
granted_at = models.DateTimeField(auto_now_add=True) granted_at = models.DateTimeField(auto_now_add=True)
def delete_dependencies(self): def delete_dependencies(self):
@ -35,33 +36,31 @@ class DataAccess(BaseModel):
"""Create an access log for a list of users """ """Create an access log for a list of users """
model_class = model_registry.get_model(self.model_name) model_class = model_registry.get_model(self.model_name)
if model_class: if model_class:
try: for user in users:
obj = model_class.objects.get(id=self.model_id) logger.info(f'=== create ModelLog for: {operation} > {users}')
store_id = None ModelLog.objects.create(
if isinstance(obj, SideStoreModel): user=user,
store_id = obj.store_id model_id=self.model_id,
model_name=self.model_name,
for user in users: operation=operation,
date=timezone.now(),
store_id=self.store_id
)
logger.info(f'=== create ModelLog for: {operation} > {users}') # existing_log = ModelLog.objects.filter(user=user, model_id=self.model_id, operation=operation).first()
# if existing_log:
existing_log = ModelLog.objects.filter(user=user, model_id=self.model_id, operation=operation).first() # existing_log.date = timezone.now()
if existing_log: # existing_log.model_operation = operation
existing_log.date = timezone.now() # existing_log.save()
existing_log.model_operation = operation # else:
existing_log.save() # ModelLog.objects.create(
else: # user=user,
ModelLog.objects.create( # model_id=self.model_id,
user=user, # model_name=self.model_name,
model_id=self.model_id, # operation=operation,
model_name=self.model_name, # date=timezone.now(),
operation=operation, # store_id=self.store_id
date=timezone.now(), # )
store_id=store_id
)
except ObjectDoesNotExist:
logger.warn(f'!!! object does not exists any more: {self.model_name} : {self.model_id} : {operation}')
pass
else: else:
logger.warn(f'!!!model not found: {self.model_name}') logger.warn(f'!!!model not found: {self.model_name}')
@ -70,12 +69,15 @@ class DataAccess(BaseModel):
if model_class: if model_class:
try: try:
obj = model_class.objects.get(id=self.model_id) obj = model_class.objects.get(id=self.model_id)
related_instance = obj.related_instances() related_instance = obj.sharing_related_instances() # here we want instances granted by the DataAccess, including SYNC_MODEL_CHILDREN_SHARING
related_instance.append(obj) related_instance.append(obj)
for instance in related_instance:
if isinstance(instance, BaseModel): with transaction.atomic():
instance.add_data_access_relation(self) for instance in related_instance:
instance.save() logger.info(f'adds DataAccess to {instance.__class__.__name__}')
if isinstance(instance, BaseModel):
instance.add_data_access_relation(self)
instance.save()
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
@ -86,9 +88,11 @@ class DataAccess(BaseModel):
obj = model_class.objects.get(id=self.model_id) obj = model_class.objects.get(id=self.model_id)
related_instance = obj.related_instances() related_instance = obj.related_instances()
related_instance.append(obj) related_instance.append(obj)
for instance in related_instance:
if isinstance(instance, BaseModel): with transaction.atomic():
instance.remove_data_access_relation(self) for instance in related_instance:
instance.save() if isinstance(instance, BaseModel):
instance.remove_data_access_relation(self)
instance.save()
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass

@ -1,22 +1,25 @@
from django.conf import settings from django.conf import settings
from django.apps import apps from django.apps import apps
from .models import BaseModel
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
import threading import threading
import logging
import importlib
User = get_user_model() logger = logging.getLogger(__name__)
class ModelRegistry: class ModelRegistry:
def __init__(self): def __init__(self):
self._registry = {} self._registry = {}
def load_sync_apps(self): def load_sync_apps(self):
base_model = get_abstract_model_class('BaseModel')
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():
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:
if issubclass(model, BaseModel) or model == User: if issubclass(model, base_model) or model == get_user_model():
model_name = model.__name__ model_name = model.__name__
if self.should_sync_model(model_name, config): if self.should_sync_model(model_name, config):
self.register(model) self.register(model)
@ -39,6 +42,10 @@ class ModelRegistry:
# Global instance # Global instance
model_registry = ModelRegistry() model_registry = ModelRegistry()
def get_abstract_model_class(model_name):
module = importlib.import_module('sync.models')
return getattr(module, model_name)
class DeviceRegistry: class DeviceRegistry:
"""Thread-safe registry to track device IDs associated with model instances.""" """Thread-safe registry to track device IDs associated with model instances."""

@ -11,6 +11,7 @@ from .ws_sender import websocket_sender
from .registry import device_registry, related_users_registry from .registry import device_registry, related_users_registry
import logging import logging
import traceback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,28 +22,25 @@ User = get_user_model()
@receiver([pre_save, pre_delete]) @receiver([pre_save, pre_delete])
def presave_handler(sender, instance, **kwargs): def presave_handler(sender, instance, **kwargs):
# some other classes are excluded in settings_app.py: SYNC_APPS try:
if not isinstance(instance, (BaseModel, User)): # some other classes are excluded in settings_app.py: SYNC_APPS
return if not isinstance(instance, (BaseModel, User)) or isinstance(instance, DataAccess):
return
signal = kwargs.get('signal')
# avoid crash in manage.py createsuperuser + delete user in the admin
if isinstance(instance, User) and (instance._state.db is None or signal == pre_delete):
return
users = related_users(instance)
related_users_registry.register(instance.id, users) signal = kwargs.get('signal')
# user_ids = [user.id for user in users] # avoid crash in manage.py createsuperuser + delete user in the admin
if isinstance(instance, User) and (instance._state.db is None or signal == pre_delete):
return
users = related_users(instance)
related_users_registry.register(instance.id, users)
if signal == pre_save: if signal == pre_save:
detect_foreign_key_changes_for_shared_instances(sender, instance) detect_foreign_key_changes_for_shared_instances(sender, instance)
sig_type = 'pre_save'
else:
sig_type = 'pre_delete'
logger.info(f'* {sig_type} : {instance.__class__.__name__} > impacted users = {users}')
except Exception as e:
logger.info(f'*** presave_handler ERROR: {e}')
raise
@receiver([post_save, post_delete]) @receiver([post_save, post_delete])
def synchronization_notifications(sender, instance, created=False, **kwargs): def synchronization_notifications(sender, instance, created=False, **kwargs):
@ -56,47 +54,31 @@ def synchronization_notifications(sender, instance, created=False, **kwargs):
if not isinstance(instance, BaseModel) and not isinstance(instance, User): if not isinstance(instance, BaseModel) and not isinstance(instance, User):
return return
process_foreign_key_changes(sender, instance, **kwargs) try:
process_foreign_key_changes(sender, instance, **kwargs)
signal = kwargs.get('signal') signal = kwargs.get('signal')
save_model_log_if_possible(instance, signal, created) save_model_log_if_possible(instance, signal, created)
notify_impacted_users(instance) notify_impacted_users(instance)
related_users_registry.unregister(instance.id)
related_users_registry.unregister(instance.id) except Exception as e:
logger.info(f'*** ERROR2: {e}')
logger.info(traceback.format_exc())
raise
def notify_impacted_users(instance): def notify_impacted_users(instance):
# print(f'*** notify_impacted_users for instance: {instance}')
# user_ids = set()
# # add impacted users
# if isinstance(instance, User):
# user_ids.add(instance.id)
# elif isinstance(instance, BaseModel):
# owner = instance.last_updated_by
# if owner:
# user_ids.add(owner.id)
# if isinstance(instance, BaseModel):
# if hasattr(instance, '_users_to_notify'):
# user_ids.update(instance._users_to_notify)
# else:
# print('no users to notify')
device_id = device_registry.get_device_id(instance.id) device_id = device_registry.get_device_id(instance.id)
users = related_users_registry.get_users(instance.id) users = related_users_registry.get_users(instance.id)
if users: if users:
user_ids = [user.id for user in users] user_ids = [user.id for user in users]
websocket_sender.send_message(user_ids, device_id) websocket_sender.send_message(user_ids, device_id)
# print(f'notify device: {device_id}, users = {user_ids}')
# for user_id in user_ids:
# websocket_sender.send_user_message(user_id, device_id)
device_registry.unregister(instance.id) device_registry.unregister(instance.id)
def save_model_log_if_possible(instance, signal, created): def save_model_log_if_possible(instance, signal, created):
users = related_users_registry.get_users(instance.id) users = related_users_registry.get_users(instance.id)
logger.debug(f'*** save_model_log_if_possible >>> users from registry = {users}, instance = {instance}') # logger.info(f'*** save_model_log_if_possible >>> users from registry = {users}, instance = {instance}')
if not users: if not users:
logger.warning(f'!!! Registry returned empty users for instance {instance.id} ({instance.__class__.__name__})') logger.warning(f'!!! Registry returned empty users for instance {instance.id} ({instance.__class__.__name__})')
@ -118,8 +100,8 @@ def save_model_log_if_possible(instance, signal, created):
if isinstance(instance, SideStoreModel): if isinstance(instance, SideStoreModel):
store_id = instance.store_id store_id = instance.store_id
if operation == ModelOperation.DELETE: # delete now unnecessary logs # if operation == ModelOperation.DELETE: # delete now unnecessary logs
ModelLog.objects.filter(model_id=instance.id).delete() # ModelLog.objects.filter(model_id=instance.id).delete()
# user_ids = [user.id for user in users] # user_ids = [user.id for user in users]
# # print(f'users to notify: {user_ids}') # # print(f'users to notify: {user_ids}')
@ -130,16 +112,14 @@ def save_model_log_if_possible(instance, signal, created):
logger.info(f'!!! Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}') logger.info(f'!!! Model Log could not be created because no linked user could be found: {instance.__class__.__name__} {instance}, {signal}')
def save_model_log(users, model_operation, model_name, model_id, store_id): def save_model_log(users, model_operation, model_name, model_id, store_id):
device_id = device_registry.get_device_id(model_id) device_id = device_registry.get_device_id(model_id)
# logger.info(f'*** creating ModelLogs for: {model_operation} {model_name} : {users}')
logger.info(f'*** creating ModelLogs for: {model_operation} {model_name} : {users}')
try: try:
with transaction.atomic(): with transaction.atomic():
created_logs = [] created_logs = []
for user in users: for user in users:
logger.debug(f'Creating ModelLog for user {user.id} ({user.username})') # logger.info(f'Creating ModelLog for user {user.id} - user exists: {User.objects.filter(id=user.id).exists()}')
model_log = ModelLog( model_log = ModelLog(
user=user, user=user,
operation=model_operation, operation=model_operation,
@ -149,20 +129,19 @@ def save_model_log(users, model_operation, model_name, model_id, store_id):
device_id=device_id device_id=device_id
) )
model_log.save() model_log.save()
# logger.info(f'ModelLog saved with ID: {model_log.id}')
created_logs.append(model_log.id) created_logs.append(model_log.id)
logger.debug(f'Successfully created ModelLog {model_log.id}')
logger.info(f'*** Successfully created {len(created_logs)} ModelLogs: {created_logs}') # Immediate verification within transaction
immediate_count = ModelLog.objects.filter(id__in=created_logs).count()
# logger.info(f'*** Within transaction: Created {len(created_logs)}, found {immediate_count}')
# Verify ModelLogs were actually persisted # Verification after transaction commits
persisted_count = ModelLog.objects.filter(id__in=created_logs).count() persisted_count = ModelLog.objects.filter(id__in=created_logs).count()
if persisted_count != len(created_logs): # logger.info(f'*** After transaction: Created {len(created_logs)}, persisted {persisted_count}')
logger.error(f'*** PERSISTENCE VERIFICATION FAILED! Created {len(created_logs)} ModelLogs but only {persisted_count} were persisted to database')
else:
logger.debug(f'*** PERSISTENCE VERIFIED: All {persisted_count} ModelLogs successfully persisted')
except Exception as e: except Exception as e:
logger.error(f'*** FAILED to create ModelLogs for: {model_operation} {model_name}, users: {[u.id for u in users]}, error: {e}', exc_info=True) logger.error(f'*** Exception during ModelLog creation: {e}', exc_info=True)
raise raise
# with transaction.atomic(): # with transaction.atomic():
@ -206,7 +185,6 @@ def detect_foreign_key_changes_for_shared_instances(sender, instance):
return return
data_access_list = related_data_access(instance) data_access_list = related_data_access(instance)
# print(f'FK change > DA count = {len(data_access_list)}')
if data_access_list: if data_access_list:
try: try:
old_instance = sender.objects.get(pk=instance.pk) old_instance = sender.objects.get(pk=instance.pk)
@ -280,7 +258,8 @@ def delete_data_access_if_necessary(sender, instance, **kwargs):
if not isinstance(instance, BaseModel): if not isinstance(instance, BaseModel):
return return
if hasattr(instance, 'id'): if hasattr(instance, 'id'):
DataAccess.objects.filter(model_id=instance.id).delete() for data_access in DataAccess.objects.filter(model_id=instance.id):
data_access.create_revoke_access_log()
@receiver(m2m_changed, sender=DataAccess.shared_with.through) @receiver(m2m_changed, sender=DataAccess.shared_with.through)
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs): def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
@ -306,26 +285,41 @@ def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
@receiver(post_save, sender=DataAccess) @receiver(post_save, sender=DataAccess)
def data_access_post_save(sender, instance, **kwargs): def data_access_post_save(sender, instance, **kwargs):
instance.add_references() # create DataAccess references on hierarchy try:
instance.add_references() # create DataAccess references on hierarchy
if instance.related_user: if instance.related_user:
evaluate_if_user_should_sync(instance.related_user) evaluate_if_user_should_sync(instance.related_user)
except Exception as e:
logger.info(f'*** ERROR3: {e}')
logger.info(traceback.format_exc())
raise
@receiver(pre_delete, sender=DataAccess) @receiver(pre_delete, sender=DataAccess)
def revoke_access_after_delete(sender, instance, **kwargs): def revoke_access_after_delete(sender, instance, **kwargs):
instance.cleanup_references() try:
instance.create_revoke_access_log() instance.cleanup_references()
related_users_registry.register(instance.id, instance.shared_with.all()) instance.create_revoke_access_log()
related_users_registry.register(instance.id, instance.shared_with.all())
instance._user = instance.related_user instance._user = instance.related_user
except Exception as e:
logger.info(f'*** ERROR4: {e}')
logger.info(traceback.format_exc())
raise
@receiver(post_delete, sender=DataAccess) @receiver(post_delete, sender=DataAccess)
def data_access_post_delete(sender, instance, **kwargs): def data_access_post_delete(sender, instance, **kwargs):
notify_impacted_users(instance) try:
notify_impacted_users(instance)
if not hasattr(instance, '_user') or not instance._user: if not hasattr(instance, '_user') or not instance._user:
return return
evaluate_if_user_should_sync(instance._user) evaluate_if_user_should_sync(instance._user)
except Exception as e:
logger.info(f'*** ERROR5: {e}')
logger.info(traceback.format_exc())
raise
def related_users(instance): def related_users(instance):
users = set() users = set()
@ -334,12 +328,10 @@ def related_users(instance):
elif isinstance(instance, BaseModel): elif isinstance(instance, BaseModel):
users.add(instance.related_user) users.add(instance.related_user)
data_access_list = DataAccess.objects.filter(id__in=instance.data_access_ids) data_access_list = DataAccess.objects.filter(id__in=instance.data_access_ids)
# print(f'instance = {instance.__class__.__name__}, data access count = {len(data_access_list)}') # print(f'instance = {instance.__class__.__name__}, data access count = {len(data_access_list)}')
for data_access in data_access_list: for data_access in data_access_list:
users.add(data_access.related_user) users.add(data_access.related_user)
users.update(data_access.shared_with.all()) users.update(data_access.shared_with.all())
if isinstance(instance, DataAccess): if isinstance(instance, DataAccess):
users.update(instance.shared_with.all()) users.update(instance.shared_with.all())
@ -370,9 +362,14 @@ def evaluate_if_user_should_sync(user):
@receiver(post_save, sender=Device) @receiver(post_save, sender=Device)
def device_created(sender, instance, **kwargs): def device_created(sender, instance, **kwargs):
if not instance.user: try:
return if not instance.user:
evaluate_if_user_should_sync(instance.user) return
evaluate_if_user_should_sync(instance.user)
except Exception as e:
logger.info(f'*** ERROR6: {e}')
logger.info(traceback.format_exc())
raise
@receiver(pre_delete, sender=Device) @receiver(pre_delete, sender=Device)
def device_pre_delete(sender, instance, **kwargs): def device_pre_delete(sender, instance, **kwargs):
@ -380,6 +377,11 @@ def device_pre_delete(sender, instance, **kwargs):
@receiver(post_delete, sender=Device) @receiver(post_delete, sender=Device)
def device_post_delete(sender, instance, **kwargs): def device_post_delete(sender, instance, **kwargs):
if not hasattr(instance, '_user') or not instance._user: try:
return if not hasattr(instance, '_user') or not instance._user:
evaluate_if_user_should_sync(instance._user) return
evaluate_if_user_should_sync(instance._user)
except Exception as e:
logger.info(f'*** ERROR7: {e}')
logger.info(traceback.format_exc())
raise

@ -1,7 +1,7 @@
import importlib import importlib
from django.apps import apps
from .registry import model_registry
from collections import defaultdict from collections import defaultdict
from .registry import model_registry
from .models import BaseModel, SideStoreModel
import random import random
import string import string
@ -50,6 +50,7 @@ class HierarchyOrganizer:
def __init__(self): def __init__(self):
self.levels = [] # List of dictionaries, each representing a level self.levels = [] # List of dictionaries, each representing a level
self.item_levels = {} # Keep track of items and their levels: (model_name, id) -> level self.item_levels = {} # Keep track of items and their levels: (model_name, id) -> level
self.children = set()
def add_item(self, model_name, item_data, level): def add_item(self, model_name, item_data, level):
""" """
@ -90,3 +91,42 @@ class HierarchyOrganizer:
if cleaned_dict: if cleaned_dict:
cleaned_levels.append(cleaned_dict) cleaned_levels.append(cleaned_dict)
return cleaned_levels return cleaned_levels
def add_relations(self, instance):
self.add_related_parents(instance)
self.add_related_children(instance)
def add_related_children(self, instance):
instance.get_shared_children(self.children)
def grouped_children(self):
grouped = defaultdict(list)
for instance in self.children:
class_name = instance.__class__.__name__
grouped[class_name].append(instance.data_identifier_dict())
return dict(grouped)
def add_related_parents(self, instance, current_level=0):
"""
Recursively add all parents of an instance to the hierarchy organizer.
Parents are added at a higher level than their children.
"""
parent_models = instance.get_parents_by_model()
for parent_model_name, parent in parent_models.items():
if isinstance(parent, BaseModel):
store_id = None
if isinstance(parent, SideStoreModel):
store_id = parent.store_id
parent_data = {
'model_id': parent.id,
'store_id': store_id
}
# Add parent at the next level
# print(f'*** add parent: {parent_model_name}: {parent.id}')
self.add_item(parent_model_name, parent_data, current_level + 1)
# Recursively process parent's parents
self.add_related_parents(parent, current_level + 1)

@ -24,15 +24,38 @@ from .models import ModelLog, BaseModel, SideStoreModel, DataAccess
from .registry import model_registry, device_registry from .registry import model_registry, device_registry
from .ws_sender import websocket_sender from .ws_sender import websocket_sender
import logging
logger = logging.getLogger(__name__)
# class HierarchyApiView(APIView): # class HierarchyApiView(APIView):
def instances_to_dict(instances, models_dict):
for instance in instances:
child_model_name = instance.__class__.__name__
serializer = get_serializer(instance, child_model_name)
if child_model_name not in models_dict:
models_dict[child_model_name] = {}
models_dict[child_model_name][instance.id] = serializer.data
return models_dict
def instances_to_data_identifier_dict(instances, models_dict):
for instance in instances:
if isinstance(instance, BaseModel):
child_model_name = instance.__class__.__name__
if child_model_name not in models_dict:
models_dict[child_model_name] = {}
models_dict[child_model_name][instance.id] = instance.data_identifier_dict()
return models_dict
def add_children_hierarchy(instance, models_dict): def add_children_hierarchy(instance, models_dict):
sync_models = getattr(settings, 'SYNC_MODEL_CHILDREN_SHARING', {}) sync_models = getattr(settings, 'SYNC_MODEL_CHILDREN_SHARING', {})
if instance.__class__.__name__ in sync_models: if instance.__class__.__name__ in sync_models:
relationships = sync_models[instance.__class__.__name__] relationships = sync_models[instance.__class__.__name__]
# 'Match': {'team_scores', 'team_registration', 'player_registrations'} # 'Match': {'team_scores', 'team_registration', 'player_registrations'}
print(f'relationships = {relationships}') # print(f'relationships = {relationships}')
current = [instance] current = [instance]
for relationship in relationships: for relationship in relationships:
print(f'> relationship = {relationship}') print(f'> relationship = {relationship}')
@ -75,30 +98,30 @@ def add_children_recursively(instance, models_dict):
models_dict[child_model_name][child.id] = serializer.data models_dict[child_model_name][child.id] = serializer.data
add_children_recursively(child, models_dict) add_children_recursively(child, models_dict)
def add_parents_with_hierarchy_organizer(instance, hierarchy_organizer, current_level=0): # def add_parents_with_hierarchy_organizer(instance, hierarchy_organizer, current_level=0):
""" # """
Recursively add all parents of an instance to the hierarchy organizer. # Recursively add all parents of an instance to the hierarchy organizer.
Parents are added at a higher level than their children. # Parents are added at a higher level than their children.
""" # """
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():
if isinstance(parent, BaseModel): # if isinstance(parent, BaseModel):
store_id = None # store_id = None
if isinstance(parent, SideStoreModel): # if isinstance(parent, SideStoreModel):
store_id = parent.store_id # store_id = parent.store_id
parent_data = { # parent_data = {
'model_id': parent.id, # 'model_id': parent.id,
'store_id': store_id # 'store_id': store_id
} # }
# Add parent at the next level # # Add parent at the next level
print(f'*** add parent: {parent_model_name}: {parent.id}') # print(f'*** add parent: {parent_model_name}: {parent.id}')
hierarchy_organizer.add_item(parent_model_name, parent_data, current_level + 1) # hierarchy_organizer.add_item(parent_model_name, parent_data, current_level + 1)
# Recursively process parent's parents # # Recursively process parent's parents
add_parents_with_hierarchy_organizer(parent, hierarchy_organizer, current_level + 1) # add_parents_with_hierarchy_organizer(parent, hierarchy_organizer, current_level + 1)
def add_parents_recursively(instance, dictionary): def add_parents_recursively(instance, dictionary):
""" """
@ -336,14 +359,17 @@ class LogProcessingResult:
shared = defaultdict(dict) shared = defaultdict(dict)
grants = defaultdict(dict) grants = defaultdict(dict)
try:
# Process each grant instance # Process each grant instance
for model_name, instances in self.shared_instances.items(): for model_name, instances in self.shared_instances.items():
for model_id, instance in instances.items(): for model_id, instance in instances.items():
serializer = get_serializer(instance, model_name) serializer = get_serializer(instance, model_name)
shared[model_name][model_id] = serializer.data shared[model_name][model_id] = serializer.data
add_children_hierarchy(instance, grants) add_children_hierarchy(instance, grants)
add_parents_recursively(instance, grants) add_parents_recursively(instance, grants)
except Exception as e:
print(f'ERR = {e}')
# Convert to lists # Convert to lists
for model_name in shared: for model_name in shared:
@ -356,44 +382,50 @@ class LogProcessingResult:
def process_revocations(self): def process_revocations(self):
"""Process revocations and their hierarchies.""" """Process revocations and their hierarchies."""
revocations = defaultdict(list) revocations = defaultdict(list)
revocations_parents_organizer = HierarchyOrganizer() revocated_relations_organizer = HierarchyOrganizer()
# print(f'*** process_revocations: {len(self.revoke_info)}')
# First, collect all revocations # First, collect all revocations
for model_name, items in self.revoke_info.items(): for model_name, items in self.revoke_info.items():
revocations[model_name].extend(items) revocations[model_name].extend(items)
logger.info(f'$$$ process_revocations for {model_name}, items = {len(items)}')
# print(f'*** process_revocations for {model_name}')
# Process parent hierarchies for each revoked item # Process parent hierarchies for each revoked item
model = model_registry.get_model(model_name) model = model_registry.get_model(model_name)
for item in items: for item in items:
logger.info(f'$$$ item revoked = {item}')
try: try:
instance = model.objects.get(id=item['model_id']) instance = model.objects.get(id=item['model_id'])
# print(f'*** process revoked item parents of {model_name} : {item['model_id']}') logger.info(f'$$$ process revoked item parents of {model_name} : {item['model_id']}')
add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer) revocated_relations_organizer.add_relations(instance)
except model.DoesNotExist: except model.DoesNotExist:
pass pass
return revocations, revocations_parents_organizer children = revocated_relations_organizer.grouped_children()
merged_revocations = merge_dicts(revocations, children)
return merged_revocations, revocated_relations_organizer
def get_response_data(self): def get_response_data(self):
"""Construct the complete response data structure.""" """Construct the complete response data structure."""
shared, grants = self.process_shared() shared, grants = self.process_shared()
revocations, revocations_parents_organizer = self.process_revocations() revocations, revocated_relations_organizer = self.process_revocations()
# print(f'self.deletions = {dict(self.deletions)}') # print(f'self.deletions = {dict(self.deletions)}')
# print(f'self.shared_relationship_sets = {self.shared_relationship_sets}') # print(f'self.shared_relationship_sets = {self.shared_relationship_sets}')
# print(f'self.shared_relationship_removals = {self.shared_relationship_removals}') # print(f'self.shared_relationship_removals = {self.shared_relationship_removals}')
# logger.info('--------------------- SYNC')
return { return {
"updates": dict(self.updates), "updates": dict(self.updates),
"deletions": dict(self.deletions), "deletions": dict(self.deletions),
"shared": dict(shared), "shared": dict(shared),
"grants": dict(grants), "grants": dict(grants),
"revocations": dict(revocations), "revocations": dict(revocations),
"revocation_parents": revocations_parents_organizer.get_organized_data(), "revocated_relations": revocated_relations_organizer.get_organized_data(),
"shared_relationship_sets": self.shared_relationship_sets, "shared_relationship_sets": self.shared_relationship_sets,
"shared_relationship_removals": self.shared_relationship_removals, "shared_relationship_removals": self.shared_relationship_removals,
"date": self.last_log_date "date": self.last_log_date
@ -451,3 +483,12 @@ class DataAccessViewSet(viewsets.ModelViewSet):
if self.request.user: if self.request.user:
return self.queryset.filter(Q(related_user=self.request.user) | Q(shared_with__in=[self.request.user])) return self.queryset.filter(Q(related_user=self.request.user) | Q(shared_with__in=[self.request.user]))
return [] return []
def merge_dicts(dict1, dict2):
result = defaultdict(list)
for d in [dict1, dict2]:
for key, value in d.items():
result[key].extend(value)
return dict(result)

@ -26,7 +26,7 @@ class Event(BaseModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.creator: if self.creator:
self.creator_full_name = self.creator.full_name() self.creator_full_name = self.creator.full_name()
super(Event, self).save(*args, **kwargs) super().save(*args, **kwargs)
def display_name(self): def display_name(self):
if self.club and self.club.name and self.name: if self.club and self.club.name and self.name:

@ -1,7 +1,9 @@
from django.db import models from django.db import models
# from tournaments.models import group_stage
from . import TournamentSubModel, Round, GroupStage, FederalMatchCategory
from django.utils import timezone, formats from django.utils import timezone, formats
from django.core.exceptions import ObjectDoesNotExist
from . import TournamentSubModel, Round, GroupStage, FederalMatchCategory
from datetime import datetime, timedelta from datetime import datetime, timedelta
import uuid import uuid
@ -90,14 +92,16 @@ class Match(TournamentSubModel):
return '--' return '--'
def stage_name(self): def stage_name(self):
if self.name: try:
return self.name if self.name:
elif self.round: return self.name
return self.round.name() elif self.round:
elif self.group_stage: return self.round.name()
return self.group_stage.display_name() elif self.group_stage:
else: return self.group_stage.display_name()
return '--' except ObjectDoesNotExist:
pass
return "--"
def get_previous_round(self): def get_previous_round(self):
# Calculate the next index # Calculate the next index

@ -1,4 +1,6 @@
from django.db import models from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from . import TournamentSubModel, Match, TeamRegistration, FederalMatchCategory from . import TournamentSubModel, Match, TeamRegistration, FederalMatchCategory
import uuid import uuid
from .match import Team # Import Team only when needed from .match import Team # Import Team only when needed
@ -15,10 +17,14 @@ class TeamScore(TournamentSubModel):
pass pass
def __str__(self): def __str__(self):
if self.match: try:
return f"{self.match.stage_name()} #{self.match.index}: {self.player_names()}" if self.match and self.team_registration:
else: return f"{self.match.stage_name()} #{self.match.index}: {self.player_names()}"
return "Empty" if self.match:
return f"{self.match.stage_name()} #{self.match.index}"
except ObjectDoesNotExist:
pass
return "--"
def get_tournament(self): # mandatory method for TournamentSubModel def get_tournament(self): # mandatory method for TournamentSubModel
if self.team_registration: if self.team_registration:
@ -48,14 +54,16 @@ class TeamScore(TournamentSubModel):
# return None # return None
def player_names(self): def player_names(self):
if self.team_registration: try:
if self.team_registration.name: if self.team_registration: # this can cause an exception when deleted
return self.team_registration.name if self.team_registration.name:
else: return self.team_registration.name
names = self.team_registration.team_names() else:
return " - ".join(names) names = self.team_registration.team_names()
else: return " - ".join(names)
return "--" except TeamRegistration.DoesNotExist:
pass
return "--"
def shortened_team_names(self, forced=False): def shortened_team_names(self, forced=False):
names = [] names = []

Loading…
Cancel
Save