From bfbe0a40611820d2239d4e5da67a1d6e745c879c Mon Sep 17 00:00:00 2001 From: anhduy-tech Date: Wed, 31 Dec 2025 09:22:13 +0700 Subject: [PATCH] changes --- api/__pycache__/settings.cpython-313.pyc | Bin 3436 -> 3451 bytes api/settings.py | 2 +- app/__pycache__/basic.cpython-313.pyc | Bin 7060 -> 7742 bytes app/__pycache__/consumers.cpython-313.pyc | Bin 5532 -> 5394 bytes app/__pycache__/views.cpython-313.pyc | Bin 73628 -> 73546 bytes app/basic.py | 53 +++++++++++------ app/consumers.py | 68 ++++++++++++---------- app/views.py | 18 +++--- requirements.txt | 2 +- 9 files changed, 83 insertions(+), 60 deletions(-) diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index 8a9b292cb731fdbe523e4d626378a6c89d94f2ef..58280aa7f8afa4487cf74d5aeea7d9a3a6f73f18 100644 GIT binary patch delta 65 zcmaDO^;?SfGcPX}0}w3g2+4fDk=Ku1UM}1`EX*l1uh1o^(%e5RJ3K8V#K|Nr*uW#u T(9F0z#nF6S?TBFXJV9S8E9S+vbl;~iIq`x@?4&K E0CFl0u>b%7 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 401d43211f7313be0c11196c042b5df7d96115d6..8df3dc909d1459b52f46666a1494a8273ed5478e 100644 GIT binary patch delta 2553 zcmcIkOKclO7@qYjj@OU%bG>WFuO!afO&_L8+NP~&5hNi&-b{G}FssBZ4(=xG+Rz@5 zjS!+-pkY7>0;#ElP$Gd^Bo1(amJ1h__e|?4Q!6UpoMJK90VkM+eWwp)Lj>NKym?KtTt< z%G=Pqfc-ND7(YM>MjkAgB@5cMvFNDPg8Hm}RJ2*pW7dA!+?*#_NuKIN?^}oT7J)|J zSqISLwoY`%I;4I3ZM~EY&Di!*W>m68X?ts?lgy+YE#HOxDEiIjr<}-akD@8N1?{){ zDL0z7b72dv*44uLp})UgI1ncQfPS>gc#z#-MxWVfBPa2YMHlTJRJI$P;ZXoN$d6Jd zsD~(MN(IqXXII}a05pgINO@(BvI=gGb+}%Oqj?`EX-(0QvbybDYE? zlBY#+0#op)Z1Z|O%_Riy_)`FgdvK;zB3&=B1DC?1YYvu*krIvRcFcX-&@i2E*0(X= zi)NTE)|`shD`>Z=0mL|Y(<~2h8c9ri9J0-7q@X5%yXAMmu2%I|@cO10NH)l%5bXLt zJ;r|<3`#M9!?BnYob}@juVwHgfv#s} zZP2Kd@>^2hC3uqSn}9ata39>lmk`{oZdQ!v=vBJ35q#&k^&R}q8Vc;!$+-vb6BDChpENN$PN zL-cdw_x6|AOi&+y`BoN!`Nl5+z=6*4Dfd0AFz;k=X=&uPwpPBpHGvUpb2i%MQ08;_La zPQu{EaV3-1vg!PCnsBRoGUqZ2g?uKxDCWiV%Y}@5K5;m=yddV3iNpgYJd2r>XRx_j zEPHt2J2 zh)Sa$yd(S>f-vTm#YIImE*8$ND5@csRq_LU$n6_OCwyE{h6H0hoz3A!)Mb>PBvelm zLo`uM%hHN?CLEW~jEzxrJO|vC#+l1p;!N&Puqm?aZpX9=nI0Bu%9o z7*d*QzsIee6LRg!jY8V7a+a_z5%VfBL?}&V zr4Nq~#j=QD&G_vYI95MruKSOa{0_zv#g)tWUb%~ovT^Sp2D8z%>jNHsZG3fn{l%)g zuQZJc?4_(PxVE&qv=ONKMoP0aoS$5sTt8p+43r+Px%_K`tAp#$Rb4}+sTvo)#DBdAhYnxUSBD<1F@ZJ5s^c*Z zP>2hfIzb?D$BvG1qs3qFvaeO$$HlO%Us|R0?o?Q#F^U)Ek z12)7fl4F OEiX{iG$qrx$A1G$U}Yiz delta 1959 zcmb_cO>7%Q6rPRk{CNGd{)yK+_WCDrep{z0mD@T=e?n8*rit8oGqfVAQWLw8i`k}Y zhqegG7F=2((O83ILl2|~p;SnazyZnuwWp#=NHHtL?utW?aH18Q-~hA!OAAzRU=DBI zd-LA=zIktFzZrXXkLeeqQ4h%4`^U-Tuw~u!F|GZ)4FLQ#J?s(NQA-y>_f= z7{ozi2aX#DNURoj7RIcwm$I$p2cVYUyy#PFJqXg~gx z>BTDyoh5pe!c!s|e9EjEh;7_XgveI!#TNwm$w|1L(S2JZH|l!2GWJ-^|5Kk_wJH8f zpXyQn+^1~4PY6ay=l{Bpt8oj`0S%c0JpCQAgtWCd^?is?bR(^Lg6yahIS><4he?(Q z1xn-vR`ejF;H=J;JTwG}&qjQ_2qR8p%nDrfh$IR6+EK)abXBOILz-HtC9;dW=piKB z^$*8NO6k=o!6v|POLVvv`J^NO&k2;EJ8gO#lpd@rSnW@ma^4+>D~lTv<#=Q>22TJC{tP;x9;Q zlkxA48On=Ku%i=-CYH`9nuSD0(WMttNunpkjA9^KLW-s460gNlQny+_6N|@{mPl05 zC(}tWor#I_2}K)E&S&s#)_|mJ3X+@h#rZWS6>~(H$k>dEM z)BUodKbK6A`g7@wB$59IkRqx$p(eLp5Eo+$35h&1APK4gq`rTzFFF3=}+5M8k*E+^)fGKsl(EEAi%xRj8t zNLg~AvPl=n%<)66mwD6*tj+?=!{Sld(vzFSF8D!~aTN4moQX$cU`N85D*}5%vykg@DBl(e{DBFU$sfryICi9cUtZd(rn||C1YyPq&?w=U1f zZQNGkhzxg^;aCZdZNmMv#2?IDvF9VZZ0=nR$mXGn%~^Eh_pKa~ZG9EHw-CukZjP4% zrhT%>kS{a75`*7xbFGBV*dqhJ t;r25RXtt;=zOp*@75}YPrl;{w?uS$s-{*NMhkxX!^bv}hq9mG3{!bTMy^8<< diff --git a/app/__pycache__/consumers.cpython-313.pyc b/app/__pycache__/consumers.cpython-313.pyc index d73c16aa06934efa644cf86248cf6eda31fb5fee..62d1510e586a85ff4df6416b629f5148ded979ef 100644 GIT binary patch delta 1423 zcmZuvO>7!R6rKTg*<}|N*ah3z0gU4x`~!Au?9{bvw{?*7qpD%Ml~pT37}n{^ctHzp znqFM>QhJL!s!F0r^`TNN^#a)9Cf69`{sM^ zeczjT^Kk65QO6&4yA9y=u=KX_i|fc?W_L4Y00ux1d=5k?ny!OuP&EaCXcjHkP1nq- zIS3>u5(F+%Es{kf4KG0(BdWqZ|0utE~-!TD@e<0B><(NVs)+SdOiTd9I&>aqyUFSNT zNbI^30RQPWhRKc9Ax|v;Ah8J^^*eZc2&EmoMQQi4bkCK#7^aSEh37J*QGwfJQ&q!;nK=jwO#%a(2YqS4$=O z+w#=3OJpQs5=t(S9>(Qo9P6PDTjz{e&nmfMCX}bVg6fvs9om!^OS0JJ35f>e#%&dGpg?e<7MRilQ`Nw9h3c!1Mh@vpcpkguHJuvz)iZWHzIg0esw}@# zS<6;E+cj>xWcix}PN6*W<>mXA_v5wRF}-)}l+80(_wv5->|S<%YA@F$%zj&i^1Wcu zCLg*n0eWQEbOWafn?5tm{97=_{P9XKJ_#R(V0_f_cwhnhPXaJLZg~=X&4M>iFT!}l z@^p|i{Bda52)3w8NM5*NJOocNiJw(bNhedqqNZ%@6y-uPsl^czmWWpgM%zR+;f^nk lbG1#wxQr@lVLO-E$;yk`7JAPUa%UlAD*VJ>z;If5{{iC{LIMB) delta 1500 zcmZ`(O>7%Q6n?X7uh)NB|E!ZVacswd#BQ3OlB7}7Qm4>5l@k-{)gnYI+{Jc^i@P?v zZb^$!fKx7j!w5B~NNvS|aBEL+0EwbDHzaQ@)n=tuRGdHprKA)pRf5?hE>MX%%=_le z``&!-&Fp@-{geKdb*Iw?WK8~iIRC5bN(=2sRA_)62*F1nAOWU9rii7cQG$}`5D2tj znx+#b*%Sg131%}P%VyCmFdK*=$g+Vfq6NR>=t3;M;Ale}UUGUD*fC@rMi^Gn8rg)9 zCBEYzT2tJX<6lPjhhY+*DyMOb|H~Ywc>|tM{5X@ zI{@sYQAf5l6^nNKz!`Nig4Jk&VB3vSUB-FHiF;aNZEn&1UwNbmbE?NUeN^`RPY3Le zqK85WRRO=iBLcw9kvY7hd30Y8X_CRMQDi{7Yo^5Gh$wRgzrvi}zC-Hxc8S ztTdJE+AKr%$(-mDJp$857$T$$^VkraWWQyQ!ZO=2aU589uF?%hLd_@(NaXQ^AV@BB z4xXliFo#qsMC8caoDuCGr{_@Ah2P|c!pb0_>C9ZdP|hhjmCr_zPL*EMnbT6?R8G~= z4&2~E46oCYTE-8!o+or$Mk?jYQXzjfr?eBc5;amd3VD)2Dx`CSi4V9!&+p&JoSV<9 zWmUHxoX_M+<$O`rO_^fpjBZv-grSU+plGXNygJ>SJDrou${~Uk(pper#$+@=nIscl zaZT85n@ZDaS;2Q)gGtLtshoM02=$btGlim*P0tmTbf%CenRMG`5k|1?QgW}8g37sU zdaE>UGZk?txlB>Xs;xNfZnK6$irY9cj^A?k_I^jci=W_a_>uM8Vta3~(H}f*%T~=3 zUbHma?aS?&JG{s=aNRw=u*N`ZPu-X!Zrfk^}wJO7+g793rtk(jqpG{Jfek1ZidH}5|w9{eGT4!Zf0rb zl3VM3>SAVP?BY=ZF7N;1;FW_n`H8zU^z6FFK=*iKf!}s6xs^n_eth)& z%T>0c;R`=rQX?3x2Zyxa&}F(79IZP1cVe@(fHZs~Hv1)Aed(Ac9IFLKmetP^ml7-L zr?WSLBU)^Bakd(e8vPShM__qgb41n{9Q1}4`iOu(?>T_)S_ecre@{iu063{HY-gMMI%jSzJx!XBTV>`9ozAa_QYveqb7e|G2x%Bnib8~1KayEw z=_|BK)X@AON#zwrWT_woBIaqf8LbZ@6vAR=^{b>aA}Z)d(evZx;pV=d_u_3E6Hh%5 zt5;MiITGfIEC2Y?(y;nPBGlk_iLvsHAy=5A6WExfr<@eNIjF-yjf~Tx-7N@JQwW_b zEs}muA>@=6$?y?#q|_eT^AuPk#AlQ;wS!765YE^Ho6b|=+&V2jV3ljd7UYx^F+b}L z5zh5!fx_$sLrbn2ekC8b>T$=Q z{y(v0|BC%f;g8tRikqxaI48w`wLA22o;NcXV zxKLQ}WJ*u{9zf_UdLa!DFcEH@mx*DdstopWDK7TYTKs#=Wkc0VdKN z)2;I}CY~|%B@Np8Z9@mWvFYO?Z|u%_cHLE`ndt`Jn9=DNi0;c8GWXlPo6}}Dqzl&Q zsj=$2)jl@s)z4f)XT76yA8$+;N$?~MC;3?0au`|{ACx0jclk`?8F!kOF^7P3fwd91 z%B7s=%ziG8=i+=^{9gbW{H&2@jolhPIH#TT`mSY zd40M@1jXcV?RgPYke6qdX{aN7!x|cHk;RLP6x<_^n!FTDldk5E0MCdU2SxCPG+sRi z07P-k1VE8(*VO4 o08M(oD#H9$M{4Q(4WegFUf delta 993 zcmb`GYet%ek$en`XVHrvd!k}jrg18EA0!4k{FOC=&2L~cZB zoh~a&1W8k*kRuE#Z+!|2LsuKb6hB!Al9vpEg5KAZ`Vi5Fo)5qCaL)hyIsd~siQ3GDAFzIo7y8Q1k zBG+OtPTB}~wjn<6f8-EO9EnxEnt#c%X)Vj|JN}$U>P1M349JO&#o*FDHBL6^aD1m0 zmzX5hhQZD+iC+`Tv!`}%RJIBuMhD_Q+(2+-2vVd;%$?>Ey(Zj9`xj3{g@tC zjb`R|9-cI$4yO8z1IA#Kc}a@mvSCq%qBN}y-UeT0K$#%gYJ5z9(}iPVag9gz%hnp; z%^^N(lFthA+3U8&xX7a91fRWv4>@D8slZJ74_Q^vM&^s(NFYd_x)UNex$w4Xq5+S( z6N#!ahk``nteRk87ujA{!9W3Nt3OA>3EWn{g%}!7(@;gOHZQTzOqwolW}%1Ny7Ga7 zA<}p)K*0nly#5(rn(W7mG|Z8VI~ReYg&&Y##^Tf)8WQly02nfRR{FP(J|MauGiO 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