import json from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.db import database_sync_to_async from django.core.serializers.json import DjangoJSONEncoder from .basic import execute_data_query class DataConsumer(AsyncJsonWebsocketConsumer): async def encode_json(self, content): """ Encode the given content as JSON, using Django's encoder to handle dates, decimals, etc. """ return json.dumps(content, cls=DjangoJSONEncoder) async def connect(self): self.subscribed_groups = set() self.subscription_params = {} # e.g., {'Product': {'filter': '...', 'values': '...'}} await self.accept() async def disconnect(self, close_code): for group_name in self.subscribed_groups: await self.channel_layer.group_discard(group_name, self.channel_name) async def receive_json(self, content): action = content.get("action") if action == "subscribe": await self.handle_subscribe(content.get("payload", {}), content.get("request_id")) async def handle_subscribe(self, payload, request_id): model_name = payload.get("name") params = payload.get("params", {}) if not model_name: await self.send_json({"type": "error", "request_id": request_id, "message": "Model name is required."}) return # Store subscription params for this client self.subscription_params[model_name] = params # Run the initial data query data = await database_sync_to_async(execute_data_query)(model_name, params) # Send the initial result back to the client await self.send_json({ "type": "subscription_response", "request_id": request_id, "data": data }) # Join the group using a lowercase model name to match the signal group_name = f"model_{model_name.lower()}_updates" if group_name not in self.subscribed_groups: await self.channel_layer.group_add(group_name, self.channel_name) self.subscribed_groups.add(group_name) async def realtime_update(self, event): # Move imports inside the method to prevent AppRegistryNotReady error on startup import ast from django.db.models import Q from .views import get_serializer payload = event["payload"] record = payload["record"] model_name_lower = payload["name"] model_name_capitalized = model_name_lower.capitalize() # 1. Get this client's subscription parameters for the specific model client_params = self.subscription_params.get(model_name_capitalized) if not client_params: return # This client is not subscribed to this model. # 2. Check if the updated record matches the client's filter filter_str = client_params.get('filter') if filter_str: try: Model, _ = get_serializer(model_name_lower) if not Model: return filter_q = Q() filter_dict = ast.literal_eval(filter_str) for key, value in filter_dict.items(): filter_q.add(Q(**{key: value}), Q.AND) matches = await database_sync_to_async( Model.objects.filter(pk=record["id"]).filter(filter_q).exists )() if not matches: return # Record does not match the client's filter, so don't send. except Exception: return # Fail silently if filter is invalid or DB check fails. # 3. Create a tailored payload, respecting the 'values' parameter payload_for_client = payload.copy() values_str = client_params.get('values') if values_str: requested_values = values_str.split(',') # The record from the signal contains all fields. Filter it down. filtered_record = {key: record.get(key) for key in requested_values if key in record} payload_for_client['record'] = filtered_record # 4. Send the final, tailored payload to the client await self.send_json({ "type": "realtime_update", "payload": payload_for_client })