From d2f46539ee671fcbb351d8b149eeefcd0a840827 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sun, 16 Nov 2025 17:02:36 +0300 Subject: [PATCH 1/2] Features: 1) Add top 10 most wished and most popular products lists; 2) Update UI to display lists with cards and counts. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: 1) Fix translation of "Returns" to "Возвраты"; 2) Fix translation of "Sales vs Returns" to "Продажи и Возвраты". Extra: 1) Refactor wishlist and order product queries to support top 10 lists; 2) Add backward compatibility for single product views; 3) Update template structure to include new lists and improve layout. --- .../core/locale/ru_RU/LC_MESSAGES/django.po | 4 +- engine/core/templates/admin/index.html | 49 +++++++++++--- engine/core/views.py | 66 ++++++++++++++++--- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/engine/core/locale/ru_RU/LC_MESSAGES/django.po b/engine/core/locale/ru_RU/LC_MESSAGES/django.po index 73c3f2a0..c50cd5bc 100644 --- a/engine/core/locale/ru_RU/LC_MESSAGES/django.po +++ b/engine/core/locale/ru_RU/LC_MESSAGES/django.po @@ -2805,7 +2805,7 @@ msgstr "Выручка (нетто, 30d)" #: engine/core/templates/admin/index.html:43 msgid "Returns (30d)" -msgstr "Возвращение (30 дней)" +msgstr "Возвраты (30 дней)" #: engine/core/templates/admin/index.html:52 msgid "Processed orders (30d)" @@ -2813,7 +2813,7 @@ msgstr "Обработанные заказы (30d)" #: engine/core/templates/admin/index.html:65 msgid "Sales vs Returns (30d)" -msgstr "Продажи против возвратов (30d)" +msgstr "Продажи и Возвраты (30d)" #: engine/core/templates/admin/index.html:82 msgid "Gross" diff --git a/engine/core/templates/admin/index.html b/engine/core/templates/admin/index.html index 48f0f5c2..e6cb458d 100644 --- a/engine/core/templates/admin/index.html +++ b/engine/core/templates/admin/index.html @@ -15,6 +15,7 @@ {% block content %} {% component "unfold/components/container.html" %} +
{% component "unfold/components/title.html" %} {% trans "Dashboard" %}
@@ -121,10 +122,26 @@ {% component "unfold/components/title.html" %} {% trans "Most wished product" %} {% endcomponent %} - {% if most_wished_product %} + {% if most_wished_products %} + + {% elif most_wished_product %} - {{ most_wished_product.name }} + {% if most_wished_product.image %} + {{ most_wished_product.name }} + {% endif %} {{ most_wished_product.name }} {% else %} @@ -138,10 +155,26 @@ {% component "unfold/components/title.html" %} {% trans "Most popular product" %} {% endcomponent %} - {% if most_popular_product %} + {% if most_popular_products %} + + {% elif most_popular_product %} - {{ most_popular_product.name }} + {% if most_popular_product.image %} + {{ most_popular_product.name }} + {% endif %} {{ most_popular_product.name }} {% else %} @@ -156,12 +189,12 @@ {% component "unfold/components/separator.html" %} {% endcomponent %} -
-
+
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %} eVibes {{ evibes_version }} · Wiseless Team {% endcomponent %}
+
{% endcomponent %} {% endblock %} \ No newline at end of file diff --git a/engine/core/views.py b/engine/core/views.py index 012211c3..0182b080 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -438,18 +438,21 @@ def dashboard_callback(request, context): } ) + # Single most wished product (backward compatibility) most_wished: dict[str, str | int | float | None] | None = None + # Top 10 most wished products + most_wished_list: list[dict[str, str | int | float | None]] = [] with suppress(Exception): - wished = ( + wished_qs = ( Wishlist.objects.filter(user__is_active=True, user__is_staff=False) .values("products") .exclude(products__isnull=True) .annotate(cnt=Count("products")) .order_by("-cnt") - .first() ) - if wished and wished.get("products"): - product = Product.objects.filter(pk=wished["products"]).first() + wished_first = wished_qs.first() + if wished_first and wished_first.get("products"): + product = Product.objects.filter(pk=wished_first["products"]).first() if product: img = product.images.first().image_url if product.images.exists() else "" most_wished = { @@ -458,17 +461,42 @@ def dashboard_callback(request, context): "admin_url": reverse("admin:core_product_change", args=[product.pk]), } + # Build top 10 list + wished_top10 = list(wished_qs[:10]) + if wished_top10: + counts_map = {row["products"]: row["cnt"] for row in wished_top10 if row.get("products")} + products = Product.objects.filter(pk__in=counts_map.keys()) + # Preserve order as in wished_top10 + product_by_id = {p.pk: p for p in products} + for row in wished_top10: + pid = row.get("products") + if not pid or pid not in product_by_id: + continue + p = product_by_id[pid] + img = p.images.first().image_url if p.images.exists() else "" + most_wished_list.append( + { + "name": p.name, + "image": img, + "admin_url": reverse("admin:core_product_change", args=[p.pk]), + "count": int(row.get("cnt", 0)), + } + ) + + # Single most popular product (backward compatibility) most_popular: dict[str, str | int | float | None] | None = None + # Top 10 most popular products by quantity + most_popular_list: list[dict[str, str | int | float | None]] = [] with suppress(Exception): - popular = ( + popular_qs = ( OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False) .values("product") .annotate(total_qty=Sum("quantity")) .order_by("-total_qty") - .first() ) - if popular and popular.get("product"): - product = Product.objects.filter(pk=popular["product"]).first() + popular_first = popular_qs.first() + if popular_first and popular_first.get("product"): + product = Product.objects.filter(pk=popular_first["product"]).first() if product: img = product.images.first().image_url if product.images.exists() else "" most_popular = { @@ -477,6 +505,26 @@ def dashboard_callback(request, context): "admin_url": reverse("admin:core_product_change", args=[product.pk]), } + popular_top10 = list(popular_qs[:10]) + if popular_top10: + qty_map = {row["product"]: row["total_qty"] for row in popular_top10 if row.get("product")} + products = Product.objects.filter(pk__in=qty_map.keys()) + product_by_id = {p.pk: p for p in products} + for row in popular_top10: + pid = row.get("product") + if not pid or pid not in product_by_id: + continue + p = product_by_id[pid] + img = p.images.first().image_url if p.images.exists() else "" + most_popular_list.append( + { + "name": p.name, + "image": img, + "admin_url": reverse("admin:core_product_change", args=[p.pk]), + "count": int(row.get("total_qty", 0) or 0), + } + ) + context.update( { "custom_variable": "value", @@ -488,6 +536,8 @@ def dashboard_callback(request, context): "quick_links": quick_links, "most_wished_product": most_wished, "most_popular_product": most_popular, + "most_wished_products": most_wished_list, + "most_popular_products": most_popular_list, } ) From 0b7afbfc825fb7859ac04c1c79e03bbe526be900 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Sun, 16 Nov 2025 17:10:20 +0300 Subject: [PATCH 2/2] Features: 1) Added digital asset download handling for project storage directory; Fixes: 1) Fixed missing import for asset download utilities; Extra: 1) Refactored asset URL generation logic to align with order state validation; 2) Added unit tests for digital asset download endpoints. --- .../core/locale/ru_RU/LC_MESSAGES/django.mo | Bin 121808 -> 121786 bytes .../core/locale/ru_RU/LC_MESSAGES/django.po | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/core/locale/ru_RU/LC_MESSAGES/django.mo b/engine/core/locale/ru_RU/LC_MESSAGES/django.mo index ec455d60093e9ad22d682abce8ecfc19a67c394f..31b9d3ab7050e55e3190231030f2f57302c05d4e 100644 GIT binary patch delta 4941 zcmZwKcVLy(8OQN+aFz-pK|mTXh9z67fM6hDh7k?2WCa3|rGOA1U<7WAvI7xh$}mcd zECr$=lpO_?%D7QbEDDIFQY%O$MNI+w{pET8>|dYroOhi0ocG>v;T^jQ@7T7qYN)Yu zF6WYS58_emg4ghNY~-gU z+5MaJ}R<@6xc_o!*_o1LeKj!2PQ#co)Ha9V&a*AN$?wT{_Qit?peH z?rT^h)j$=c%i;8>)7PRyol$Dnt2yP zeayq&-AVj>EAL9UKeLT58`#vg-qqs0S-224qmp@Cf$9mT`0d7vf#GJc!q0Ht`nROUGi`TRWv4y`$eQRI8IOTjSc; zhKlx>!APcKHh0{J_wGsR7bbX@h-a}oHhbK=nV61G<8P>r4tc`6Za5ByU?FzIs)^n` zg#)oY=3{5tcO?|ksA$vEy9T&D@HFa0w>@biOvEJOJk))+_A;B|2;%>poZpRJ=_q)$FU*tPxt`V7+`A`gVl(W zQE4^}V{sm~#KZUqx`DP-El_Ej5qKK4=UNV8z%3}ur=Tf0fLfcQs42XAuyv>gY6{y2 z4n=ir2I{#Ds16mNMtn4gZ{U5z_a=E4iOo*}LQTH{)>ezi~$fh=hipEsTLha&w z)X0iZ4;(`^_yelJipe%J4`MuVB5H8eul7;d7|x9!#-#0FEMl11I1gp0*CBqdKxQh!q111mE;IEj1l}Fpur=d2_5>(!AMQzTng81*KbgVeWyB^pQHB*_W8CZ;ZU%6+j zek7{HQ*f>lK9532Dw;j(U2B|#5%>ftw z%kj39o$*DkPeYBo6!Y+p&v{os`!1h?@^SP8YiJL0G`dr$HLf+$yRkSDXW{3ljtoe( zrO8C?p?uU#e1=-}CwW(d{ZS3qnC#tJEW%J#3PYz@Pt$N3@kUghMyGkF>s?Xt zP#lS~uqXbA`mVQ~YHQvb^}U~n>gZb34Aq`yOEUwtxeudG%Inj}|5qstdfuk&C)5aT zq7J5d>2~8_e1dp4p1|@iSbm?y;lvm59_%sQyEsfjE!jHMQM(JZ7k)%-@}wEop}8|c zw)sv_5yypkGi}$7LdCg3d?ARVxnJMy6x5O(z|L4D!|or59f((>Hscvob~Sv_uBQa% zVj}eyLKL)HTV&c4cEVP~!%^w-8fqrCVm4mDD4g<=onXsROS2Vq!kxru{2KN3`#p&3 zyzE^&;&{}O&PJtkXf1`ZlMXeqW2kg&G|RhOoPm=tdbW4TxE%Z8FQ_zpVvaR90JU_} zQTd;X+LRxlvg$mJ!N|Gxv6_u+(vbUzg3|0FYHiBRvk^5xOfJI+=Qc*|7R(vfv#DW_kB_6GY0420_=_v3;7X= zL-1KFMD6n0i}*2#8*v3*LCwJ2Y&&T3P#>c&Q6H~!sF}NpHI@H2DQM~uO1Jqi^q6U(JI`d=eV35`I+3POXo|B@ zJuC?P1hpimQ7^pxRlD92wTZf+I+7eX9W~V}QRl=vr~|0VQp=K_xSV)7YJg3ak^h?7 zk;}YWhucvHM$hGT!zR=VzeBC*uNa5-uduaFMosxy9EDR?diRoIESrf{w)rNY2DS+` zkiCJIR*}Ej3^iY~3%vp}1M_h#*UzKUCvmk+-6B*vzKhy)?bg^CzZaJg$F8+syC<|1eU7`~WSoMj_yeB7q4ENbHMj?I&SR;sdBZ6!&~!duJ@_L_CB^*!)AAnQYXaIfa_Rx(97j zr=U8t^C0=JUEbu7-I$5m{l{?)w))79(i5l|`57bd2DZR^KDLg=qw;z_YNmFfX693z ziY3AIv7dOik$4T(z%GYFmWRU*d-o<4i*PtL|Eqo9m*Ozui>Nj0{;3^IIaraQJdj(R0l7i_R!4`1)WS)kJ(S723VJPFlt1ZsIxsEYk&ZA0Agw2g~DM*$cnNSmM~f+3)@k_9EVh?eRLc!giJ(k@Sd{z@Q!dncsJqR|8wJezg@~r jyd|@EL1A87e^dzp~EyZgyu{R-*gbci$M^edG4!wL(pu zbL)O}t}%Xwo$wk)V8VIlZpX*)ZXAYva6#br7~!3Z`^~u&>O1}JTr}}&j3VB4!MXeJ z0M^7`@Gh)y(YdcHI9LCo3%MFpR4jAuR*b>zJfH<8Rdnv$Wgf(hvDcj2Qpvf*|2cP+ z>%Tkis&f6O3f`q~{mKpArD2!K-krjOk=||M{xwy-i{`#g)xCQL`_%MqD!<VKQ{E`9h=gG2l2Z@4|vy> z`uOJFRVJSCuy>cZe|{TZKCo%+ysOW1vvCm?U@J^)@7=RFJw)MY3a7Ci-XG^(UF;S( z9;*>A!xp$6Bk?4Tz_ZvBlj7+BZor#xOAznDEaDGvA06wSVC_V8@Q!}FPzwrL3I&$-87M!zZv~7w=|bHV(m?pR$fl#K(!}U_bl>6R}CMcfD{d zCSVD6qlNzffrB@y0x2)a44n{7oqN})7|WVLy1S@gLoV@LlIBg)Zc;{ z;6vCPQ&2OMfmyWgR#4DL8l-rIE)Q2>{T|-kiAAWH`3mpG+C9B%h;0G~;h)4Wqeh(3 z%NqCrA0WPj)v^9F-Zj97Q5{XiYHBcxf*M+j_u&2@K8KBoZ|LpI)210}k95b{n1)KT z#TbXL;Y0WhYOmDlV@vfUDs5K=UO?@+q`nNeC53eqG$kicYx5my3L89Y9cqS}!d`)s zP#s%_`rUq1hmN2|{9O=7r+W7f;`{J6?1-9yL8x{zQhlg=1RqnODLR8X2hIm^WIr46 z?Wi?RK;1VgsGo^yU_I)0Mc5Tjp|YV-e;Y_F>OgCUx~~Jq;DG)io7!|LnozMCm2M@d zk)1^S;Cs~6y8+f)#GD9{bkexqJ~)qng;eq)n{W{+#19`qmKG2&)KHE12dKX z4^YqnQ-o@;+Hmg%wX3*u|2bgVzZyC*RTHB&jL8Q6$=Ud%|V zpN^Wbg_xm)FQSl0MaNOzwZ;V)jUS>qbQ0B(3#c`%`n>Iphp;zsB2L9M*xb|53)WE5 zXj{s@_#)RAV-!Y>@$OB$3kzu9l~7PV&KzqE9Yc;rcOJFIEyj8GJWj`X_#>(#W5(Ok z&IU`K2`%kCeGj}q@dHE;=)UGXp4_4L3( zOs4*Nh=O+Ole29K`{2XGQ&8!$9W@h&Fbl6^U0gWFPOvSgr8$HV_%q&(7f~M`HRoE~ z2HO*-qLwrlmCm7^6v|IJ)X2U^rDMW8@A7aNPQX_4y&HgAuqRf1$u8| zh@ zffZjRf3+E!ziJnT2j&Ep;PYI+f=ZvEt8MD?Q0aIO)sd83JL5mc6~sN(*w^mQ_!x2G zTJJ_;CThw4i%qa<=rv2HHaM7yC8&{(czR8>RfPI)meA7nnUcug|H2ecs zSgu}5tGb-QQ5!ijF#eW1Yf7jxc1(N(bQC@Iv@-Dj&v)dZjgS~k0p!fKj5DQUD za0P2);vQSuzNjq7!xMM_b)c;(wAb_w)Diz>;E&jY_=;lX|5JOdp;TN)T!4|-v&eSw zKvbHJ#?BbRJMl2;JKzUYgHikJglvpA6DOly$Nh04F2wQZ_Ivj=Ou&$)vU{;zxQ;`K zM<1}a*l`?3T>E`n`vusQ_yprgWlM$OE(I2j{L z?D}kci#Q+a;IkivEDtAtibi zXZIAWLHsXVf)`OsG&gKZ^95!SH!ZbIx)=3MIf**ZLRTp~NTJm+yI~~uBc6`~@EpE} zoj&(&F8+;L#xjqB zK0G2$+rcsz^}s(d4tsoQ-~B7FJ8>~4VD&ThK2O0!;>?|`8;v~}wWn^lIJ`exTskMb zw=}DC)~dgAe9c-e{CaqMxTthtnCrX31>wDf`^vBFEc-Ptxyl0S_J!XI7wpb$=WnWT iM{((_@cz=-)a2I<=kvhb;e4vLQ?);wzp*gR=l&n_$7&k@ diff --git a/engine/core/locale/ru_RU/LC_MESSAGES/django.po b/engine/core/locale/ru_RU/LC_MESSAGES/django.po index c50cd5bc..06af18ad 100644 --- a/engine/core/locale/ru_RU/LC_MESSAGES/django.po +++ b/engine/core/locale/ru_RU/LC_MESSAGES/django.po @@ -2837,7 +2837,7 @@ msgstr "Ссылки недоступны." #: engine/core/templates/admin/index.html:119 msgid "Most wished product" -msgstr "Самый желанный продукт" +msgstr "Самые желанные товары" #: engine/core/templates/admin/index.html:128 #: engine/core/templates/admin/index.html:144 @@ -2846,7 +2846,7 @@ msgstr "Данных пока нет." #: engine/core/templates/admin/index.html:135 msgid "Most popular product" -msgstr "Самый популярный продукт" +msgstr "Самые популярные товары" #: engine/core/templates/digital_order_created_email.html:7 #: engine/core/templates/digital_order_created_email.html:100