Files
MTCBD/backend/analytics/signals.py
2026-02-23 20:31:53 +05:30

163 lines
5.3 KiB
Python

from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from django.contrib.contenttypes.models import ContentType
from .models import ActivityLog, AuditLog
import json
from django.core.serializers.json import DjangoJSONEncoder
import inspect
# Try to import models dynamically to avoid circular imports during startup
# We'll use get_model where possible, but for signals we need the actual models
from projects.models import Project, Task
def get_current_request():
"""
A hacky way to get the current request in signals.
In a real production app, you'd use a middleware like django-crum
or pass the user explicitly.
For this prototype, we'll try to find the request in the stack.
"""
for f in inspect.stack():
if 'request' in f[0].f_locals:
return f[0].f_locals['request']
return None
def get_user_and_tenant_from_instance_or_request(instance):
request = get_current_request()
user = getattr(request, 'user', None) if request else None
# If user is anonymous, try to get from instance
if user and user.is_authenticated:
pass
elif hasattr(instance, 'created_by') and instance.created_by:
user = instance.created_by
elif hasattr(instance, 'user') and instance.user:
user = instance.user
tenant = getattr(request, 'tenant', None) if request else getattr(instance, 'tenant', None)
return user, tenant
@receiver(post_save, sender=Project)
@receiver(post_save, sender=Task)
def log_activity_on_save(sender, instance, created, **kwargs):
user, tenant = get_user_and_tenant_from_instance_or_request(instance)
if not tenant:
return # Cannot log without tenant
action = "created" if created else "updated"
target_type = sender.__name__
metadata = {}
if hasattr(instance, 'name'):
metadata['name'] = instance.name
elif hasattr(instance, 'title'):
metadata['title'] = instance.title
ActivityLog.objects.create(
tenant=tenant,
user=user if (user and user.is_authenticated) else None,
action=action,
target_type=target_type,
target_id=str(instance.id),
metadata=metadata
)
@receiver(post_delete, sender=Project)
@receiver(post_delete, sender=Task)
def log_activity_on_delete(sender, instance, **kwargs):
user, tenant = get_user_and_tenant_from_instance_or_request(instance)
if not tenant:
return
action = "deleted"
target_type = sender.__name__
metadata = {}
if hasattr(instance, 'name'):
metadata['name'] = instance.name
elif hasattr(instance, 'title'):
metadata['title'] = instance.title
ActivityLog.objects.create(
tenant=tenant,
user=user if (user and user.is_authenticated) else None,
action=action,
target_type=target_type,
target_id=str(instance.id),
metadata=metadata
)
# --- Audit Logs ---
# Small hack: Store the original state before saving
@receiver(pre_save, sender=Project)
@receiver(pre_save, sender=Task)
def store_original_state(sender, instance, **kwargs):
if instance.pk:
try:
old_instance = sender.objects.get(pk=instance.pk)
# Store it on the instance for the post_save signal
instance._old_state = {f.name: getattr(old_instance, f.name) for f in sender._meta.fields}
except sender.DoesNotExist:
pass
@receiver(post_save, sender=Project)
@receiver(post_save, sender=Task)
def log_audit_on_save(sender, instance, created, **kwargs):
user, tenant = get_user_and_tenant_from_instance_or_request(instance)
request = get_current_request()
if not tenant:
return
action = "created" if created else "updated"
changes = {}
if not created and hasattr(instance, '_old_state'):
for f in sender._meta.fields:
old_val = instance._old_state.get(f.name)
new_val = getattr(instance, f.name)
if old_val != new_val:
# Basic stringification for JSON serialization
changes[f.name] = {
'old': str(old_val),
'new': str(new_val)
}
elif created:
for f in sender._meta.fields:
changes[f.name] = {'new': str(getattr(instance, f.name))}
AuditLog.objects.create(
tenant=tenant,
user=user if (user and user.is_authenticated) else None,
action=action,
model_name=sender.__name__,
object_id=str(instance.id),
changes=changes,
ip_address=request.META.get('REMOTE_ADDR') if request else None
)
@receiver(post_delete, sender=Project)
@receiver(post_delete, sender=Task)
def log_audit_on_delete(sender, instance, **kwargs):
user, tenant = get_user_and_tenant_from_instance_or_request(instance)
request = get_current_request()
if not tenant:
return
changes = {f.name: {'old': str(getattr(instance, f.name))} for f in sender._meta.fields}
AuditLog.objects.create(
tenant=tenant,
user=user if (user and user.is_authenticated) else None,
action="deleted",
model_name=sender.__name__,
object_id=str(instance.id),
changes=changes,
ip_address=request.META.get('REMOTE_ADDR') if request else None
)