diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index 8a9b292c..58280aa7 100644 Binary files a/api/__pycache__/settings.cpython-313.pyc and b/api/__pycache__/settings.cpython-313.pyc differ diff --git a/api/settings.py b/api/settings.py index edb86e8a..ef54c173 100644 --- a/api/settings.py +++ b/api/settings.py @@ -89,7 +89,7 @@ DATABASES = { 'ENGINE': 'django_db_geventpool.backends.postgresql_psycopg3', # Hoặc psycopg3/postgis 'NAME': 'utopia', 'USER': 'postgres', - 'PASSWORD': 'V59yNLN42a9Q7xT', + 'PASSWORD': 'W7VVBUnqDRy7OVkWfdTB4fS0HQ1615', 'HOST': DBHOST, 'PORT': '5423', 'CONN_MAX_AGE': 0, # Tắt persistent connection mặc định của Django diff --git a/app/__pycache__/basic.cpython-313.pyc b/app/__pycache__/basic.cpython-313.pyc index 401d4321..8df3dc90 100644 Binary files a/app/__pycache__/basic.cpython-313.pyc and b/app/__pycache__/basic.cpython-313.pyc differ diff --git a/app/__pycache__/consumers.cpython-313.pyc b/app/__pycache__/consumers.cpython-313.pyc index d73c16aa..62d1510e 100644 Binary files a/app/__pycache__/consumers.cpython-313.pyc and b/app/__pycache__/consumers.cpython-313.pyc differ diff --git a/app/__pycache__/views.cpython-313.pyc b/app/__pycache__/views.cpython-313.pyc index 903a6240..a17f7263 100644 Binary files a/app/__pycache__/views.cpython-313.pyc and b/app/__pycache__/views.cpython-313.pyc differ diff --git a/app/basic.py b/app/basic.py index 8aa0244f..6269b965 100755 --- a/app/basic.py +++ b/app/basic.py @@ -89,8 +89,18 @@ def execute_data_query(name, params): if Model is None: return None + def parse_param_to_dict(param): + if isinstance(param, dict): + return param + if isinstance(param, str): + try: + return ast.literal_eval(param) + except (ValueError, SyntaxError): + return {} + return {} + # Lấy các tham số từ dict `params` - filter_str = params.get('filter') + filter_param = params.get('filter') values = params.get('values') values = values if values==None else values.split(',') summary = params.get('summary') @@ -99,40 +109,47 @@ def execute_data_query(name, params): sort = params.get('sort') sort = None if sort==None else sort.split(',') distinct_values = params.get('distinct_values') - filter_or = params.get('filter_or') - exclude = params.get('exclude') + filter_or_param = params.get('filter_or') + exclude_param = params.get('exclude') calculation = params.get('calculation') - final_filter = params.get('final_filter') - final_exclude = params.get('final_exclude') + final_filter_param = params.get('final_filter') + final_exclude_param = params.get('final_exclude') # Xây dựng filter_list filter_list = Q() - if filter_or != None: - for key, value in ast.literal_eval(filter_or).items(): + filter_or_dict = parse_param_to_dict(filter_or_param) + if filter_or_dict: + for key, value in filter_or_dict.items(): filter_list.add(Q(**{key: value}), Q.OR) - if filter_str != None: - for key, value in ast.literal_eval(filter_str).items(): - if isinstance(value, dict) == True: - if value['type'] == 'F': - filter_list.add(Q(**{key: F(value['field'])}), Q.AND) + filter_dict = parse_param_to_dict(filter_param) + if filter_dict: + for key, value in filter_dict.items(): + if isinstance(value, dict) and value.get('type') == 'F': + filter_list.add(Q(**{key: F(value['field'])}), Q.AND) else: filter_list.add(Q(**{key: value}), Q.AND) # Thực thi query rows = Model.objects.all() if len(filter_list) == 0 else Model.objects.filter(filter_list) - if exclude != None: + + exclude_dict = parse_param_to_dict(exclude_param) + if exclude_dict: exclude_list = Q() - for key, value in ast.literal_eval(exclude).items(): - if isinstance(value, dict) == True: - if value['type'] == 'F': - exclude_list.add(Q(**{key: F(value['field'])}), Q.AND) + for key, value in exclude_dict.items(): + if isinstance(value, dict) and value.get('type') == 'F': + exclude_list.add(Q(**{key: F(value['field'])}), Q.AND) else: exclude_list.add(Q(**{key: value}), Q.AND) rows = rows.exclude(exclude_list) rows, need_serializer = base_query(rows, values, summary, distinct_values) - rows = final_result(rows, calculation, final_filter, final_exclude, sort) + + # We need to parse final_filter and final_exclude here as they are applied in final_result + final_filter_dict = parse_param_to_dict(final_filter_param) + final_exclude_dict = parse_param_to_dict(final_exclude_param) + + rows = final_result(rows, calculation, final_filter_dict, final_exclude_dict, sort) # Initialize total_rows and full_data total_rows = 0 diff --git a/app/consumers.py b/app/consumers.py index 9e516203..b9dfdb87 100644 --- a/app/consumers.py +++ b/app/consumers.py @@ -14,7 +14,7 @@ class DataConsumer(AsyncJsonWebsocketConsumer): async def connect(self): self.subscribed_groups = set() - self.subscription_params = {} # e.g., {'Product': {'filter': '...', 'values': '...'}} + self.subscription_params = {} await self.accept() async def disconnect(self, close_code): @@ -55,12 +55,14 @@ class DataConsumer(AsyncJsonWebsocketConsumer): 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"] + record_id = payload["record"].get("id") + if not record_id: + return + model_name_lower = payload["name"] model_name_capitalized = model_name_lower.capitalize() @@ -69,38 +71,44 @@ class DataConsumer(AsyncJsonWebsocketConsumer): 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 + # 2. Check if the updated record ID could possibly match the client's filter + 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) + # Build a Q object from the client's filter dictionary + filter_q = Q() + filter_dict = client_params.get('filter') + if isinstance(filter_dict, dict): + for key, value in filter_dict.items(): + filter_q.add(Q(**{key: value}), Q.AND) + + # Combine the client's filter with the specific record's ID + combined_filter = Q(pk=record_id) & filter_q - matches = await database_sync_to_async( - Model.objects.filter(pk=record["id"]).filter(filter_q).exists - )() + # Check if the object actually exists with the combined filter + record_exists = await database_sync_to_async(Model.objects.filter(combined_filter).exists)() + if not record_exists: + return # The record does not match the client's filter, so don't send. - 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. Re-run the original query for just this single object. + # This correctly applies 'values', 'distinct_values', 'summary', etc. + single_record_params = client_params.copy() + single_record_params['filter'] = {'pk': record_id} + + data = await database_sync_to_async(execute_data_query)(model_name_capitalized, single_record_params) - # 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 + # If the query returns no data (e.g., record was deleted or no longer matches), do nothing. + if not data or not data.get('rows'): + return - # 4. Send the final, tailored payload to the client + # 4. Build the final payload with the correctly-shaped record + payload_for_client = { + "name": model_name_lower, + "record": data['rows'][0] + } + + # 5. Send the final, tailored payload to the client await self.send_json({ "type": "realtime_update", "payload": payload_for_client diff --git a/app/views.py b/app/views.py index f93336c9..e26bbfe9 100644 --- a/app/views.py +++ b/app/views.py @@ -285,22 +285,20 @@ def final_result(rows, calculation=None, final_filter=None, final_exclude=None, if calculation: rows = calculate(rows, calculation) - if final_filter != None: + if final_filter: filter_list = Q() - for key, value in ast.literal_eval(final_filter).items(): - if isinstance(value, dict) == True: - if value['type'] == 'F': - filter_list.add(Q(**{key: F(value['field'])}), Q.AND) + for key, value in final_filter.items(): + if isinstance(value, dict) and value.get('type') == 'F': + filter_list.add(Q(**{key: F(value['field'])}), Q.AND) else: filter_list.add(Q(**{key: value}), Q.AND) rows = rows.filter(filter_list) - if final_exclude != None: + if final_exclude: exclude_list = Q() - for key, value in ast.literal_eval(final_exclude).items(): - if isinstance(value, dict) == True: - if value['type'] == 'F': - exclude_list.add(Q(**{key: F(value['field'])}), Q.AND) + for key, value in final_exclude.items(): + if isinstance(value, dict) and value.get('type') == 'F': + exclude_list.add(Q(**{key: F(value['field'])}), Q.AND) else: exclude_list.add(Q(**{key: value}), Q.AND) rows = rows.exclude(exclude_list) diff --git a/requirements.txt b/requirements.txt index 0a6e4718..f5a1692b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,4 @@ num2words mammoth paramiko channels -uvicorn \ No newline at end of file +uvicorn[standard] \ No newline at end of file