You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
8.2 KiB
222 lines
8.2 KiB
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
|
from django.db import models
|
|
from django.dispatch import receiver
|
|
|
|
from .models import DataAccess, ModelLog, ModelOperation, BaseModel, SideStoreModel
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
|
|
from .ws_sender import websocket_sender
|
|
|
|
User = get_user_model()
|
|
|
|
@receiver([pre_save, pre_delete])
|
|
def synchronization_prepare(sender, instance, created=False, **kwargs):
|
|
|
|
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
|
|
|
|
# some other classes are excluded in settings_app.py: SYNC_APPS
|
|
if not isinstance(instance, BaseModel) and not isinstance(instance, User):
|
|
return
|
|
|
|
device_id = None
|
|
if hasattr(instance, '_device_id'):
|
|
device_id = instance._device_id
|
|
|
|
# print(f'kwargs = {kwargs}')
|
|
save_model_log_if_possible(instance, signal, created, device_id)
|
|
|
|
if signal == pre_save:
|
|
# print('yes')
|
|
detect_foreign_key_changes(sender, instance, device_id)
|
|
|
|
@receiver([post_save, post_delete])
|
|
def synchronization_notifications(sender, instance, created=False, **kwargs):
|
|
"""
|
|
Signal handler that sends notifications through WebSocket channels when model instances are saved or deleted.
|
|
The function creates a WebSocket group name for each affected user and sends a sync update message
|
|
to all clients connected to that group.
|
|
"""
|
|
|
|
# some classes are excluded in settings_app.py: SYNC_APPS
|
|
if not isinstance(instance, BaseModel) and not isinstance(instance, User):
|
|
return
|
|
|
|
# print(f'*** instance._state.db: {instance._state.db}')
|
|
notify_impacted_users(instance)
|
|
|
|
def notify_impacted_users(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 = None
|
|
if hasattr(instance, '_device_id'):
|
|
device_id = instance._device_id
|
|
|
|
print(f'notify: {user_ids}')
|
|
for user_id in user_ids:
|
|
websocket_sender.send_user_message(user_id, device_id)
|
|
# send_user_message(user_id)
|
|
|
|
def save_model_log_if_possible(instance, signal, created, device_id):
|
|
|
|
if isinstance(instance, User):
|
|
users = {instance}
|
|
else:
|
|
users = related_users(instance)
|
|
|
|
# print(f'users = {users}')
|
|
if users:
|
|
if signal == post_save or signal == pre_save:
|
|
if created:
|
|
operation = ModelOperation.POST
|
|
else:
|
|
operation = ModelOperation.PUT
|
|
else:
|
|
operation = ModelOperation.DELETE
|
|
|
|
model_name = instance.__class__.__name__
|
|
store_id = None
|
|
if isinstance(instance, SideStoreModel):
|
|
store_id = instance.store_id
|
|
|
|
if operation == ModelOperation.DELETE: # delete now unnecessary logs
|
|
ModelLog.objects.filter(model_id=instance.id).delete()
|
|
|
|
user_ids = [user.id for user in users]
|
|
|
|
print(f'users to notify: {user_ids}')
|
|
instance._users_to_notify = user_ids # save this for the post_save signal
|
|
save_model_log(users, operation, model_name, instance.id, store_id, device_id)
|
|
|
|
else:
|
|
print('>>> Model Log could not be created because instance.last_updated_by is None')
|
|
|
|
def save_model_log(users, model_operation, model_name, model_id, store_id, device_id):
|
|
now = timezone.now()
|
|
existing_log = ModelLog.objects.filter(users__in=users, model_id=model_id, operation=model_operation).first()
|
|
if existing_log:
|
|
# print(f'update existing log {existing_log.users} ')
|
|
existing_log.date = now
|
|
existing_log.device_id = device_id
|
|
# existing_log.operation = model_operation
|
|
existing_log.save()
|
|
existing_log.users.set(users)
|
|
else:
|
|
model_log = ModelLog()
|
|
model_log.operation = model_operation
|
|
model_log.date = now
|
|
model_log.model_name = model_name
|
|
model_log.model_id = model_id
|
|
model_log.store_id = store_id
|
|
model_log.device_id = device_id
|
|
model_log.save()
|
|
model_log.users.set(users)
|
|
|
|
def detect_foreign_key_changes(sender, instance, device_id):
|
|
if not hasattr(instance, 'pk') or not instance.pk:
|
|
return
|
|
if not isinstance(instance, BaseModel):
|
|
return
|
|
|
|
data_access_list = related_data_access(instance)
|
|
if data_access_list:
|
|
try:
|
|
old_instance = sender.objects.get(pk=instance.pk)
|
|
except sender.DoesNotExist:
|
|
return
|
|
|
|
# Check foreign key fields
|
|
for field in sender._meta.get_fields():
|
|
if isinstance(field, models.ForeignKey) and not field.related_model == User:
|
|
# print(f'field.related_model = {field.related_model}')
|
|
old_value = getattr(old_instance, field.name, None)
|
|
new_value = getattr(instance, field.name, None)
|
|
if old_value != new_value:
|
|
for data_access in data_access_list:
|
|
if old_value:
|
|
model_name = old_value.__class__.__name__
|
|
save_model_log(data_access.shared_with.all(), 'REVOKE_ACCESS', model_name, old_value.id, old_value.get_store_id(), device_id)
|
|
if new_value:
|
|
model_name = new_value.__class__.__name__
|
|
save_model_log(data_access.shared_with.all(), 'GRANT_ACCESS', model_name, new_value.id, new_value.get_store_id(), device_id)
|
|
|
|
# REVOKE access for old_value and GRANT new_value
|
|
print(f"Foreign key changed in {sender.__name__}: "
|
|
f"{field.name} from {old_value} to {new_value}")
|
|
|
|
@receiver(post_delete)
|
|
def delete_data_access_if_necessary(sender, instance, **kwargs):
|
|
if hasattr(instance, 'id'):
|
|
DataAccess.objects.filter(model_id=instance.id).delete()
|
|
|
|
@receiver(m2m_changed, sender=DataAccess.shared_with.through)
|
|
def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs):
|
|
|
|
users = User.objects.filter(id__in=pk_set)
|
|
|
|
if action == "post_add":
|
|
instance.create_access_log(users, 'GRANT_ACCESS')
|
|
elif action == "post_remove":
|
|
instance.create_access_log(users, 'REVOKE_ACCESS')
|
|
|
|
device_id = None
|
|
if hasattr(instance, '_device_id'):
|
|
device_id = instance._device_id
|
|
|
|
for user_id in pk_set:
|
|
websocket_sender.send_user_message(user_id, device_id)
|
|
|
|
@receiver(pre_delete, sender=DataAccess)
|
|
def revoke_access_after_delete(sender, instance, **kwargs):
|
|
instance.create_revoke_access_log()
|
|
|
|
def related_users(instance):
|
|
users = set()
|
|
if isinstance(instance, User):
|
|
users.add(instance)
|
|
elif isinstance(instance, BaseModel):
|
|
users.add(instance.related_user)
|
|
users.add(instance.last_updated_by)
|
|
|
|
# look in hierarchy
|
|
related_instances = instance.related_instances()
|
|
related_users = [ri.related_user for ri in related_instances if isinstance(ri, BaseModel)]
|
|
users.update(related_users)
|
|
|
|
# look in related DataAccess
|
|
data_access_list = instances_related_data_access(instance, related_instances)
|
|
for data_access in data_access_list:
|
|
users.add(data_access.related_user)
|
|
users.update(data_access.shared_with.all())
|
|
|
|
if isinstance(instance, DataAccess):
|
|
users.update(instance.shared_with.all())
|
|
|
|
return {user for user in users if user is not None}
|
|
|
|
def related_data_access(instance):
|
|
related_instances = instance.related_instances()
|
|
return instances_related_data_access(instance, related_instances)
|
|
|
|
def instances_related_data_access(instance, related_instances):
|
|
# related_instances = instance.related_instances()
|
|
related_ids = [ri.id for ri in related_instances]
|
|
related_ids.append(instance.id)
|
|
return DataAccess.objects.filter(model_id__in=related_ids)
|
|
|