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 )