^@&UFB!+X(msJDMjg>t&bWd+VT4
z6$R3tG%)4V
zSoPX3_uHjb*JO5kRC2ZUSCwqCEP3{s8kia;VqnQr4e8!TwkgYCZrxLX9e}uY$@xgF-nEj378xmgb%igX;Z`8Vc(sRT5dXW{Xyw<
z&(V9ZQ1@?x{O;3APlkOEgukhaedg}c@#6MurTG`BI`O5oshpGP2q^S2z38XH{nX_2
zDN%jc4s)qehDqMF8`mBbsZhcZ{vNy5CyV}}2}!pld@#-Ct%1`(x)CL1CSo#bu8?lz
zkh+A~ELc*uANh<`@7Vu?c2E$}giPPS*f8D}Y3_*7OPxx>3U1UviN~w#3}3wOKrKxX
zShZ3_1H@w^K}lKBEYYXw&q=#;3b)A6VKr}_wR?&XJTx?I=U-`qd%Ckhg$o(FKXQ{r
zG7wU*>PGJ*AXwLb{>w2jKd&_3(8+D1tcz)UY2war=(%r3gGg3cN~UG{1wsAB<|FXm
z1H9n%Z!ADq-|}uW4+}JRUy6HI$Gf_geefa)Ts*ZgRAx>pNEHM8m^R^u@la1mY)6px
zc=$3t>`J@tb^(o;n+1h(LB*C?!tv|Z9&fMeIG!jBbU)?mpReGSG
z!7Z&uZBB>$w;{`vE(IRzs1^J_vM>QRnC+~r1LyP0H)mz7B$U;T(4}I{_J~v)ORHKA
z>#h@V$%yYNiJ@ZU+ieHoPN+W9WvY3H!Aw~JkBq_?XYe`Vx)bAy$LQ-R)w2bMouQE?
zrE5CJv$yF@Cn7RWV4DJVHAeA4I*89mTs}&+gB=yp1MM_t5&)8>
zdN2k%?Xx8*_wK%cr$_|RE$jT2vdxNS7c(hizqP5({`ZnR3)S5?v6?;H)~C3Rug~Wh
z-0=Wgy}lUQRePxuwe1k0z^#vG2Z+d94-
z*;uaF+#6+zXnvB>K34yyIiNmXoP%f^>
zd2p)=tsk`3-C51d`XssGZt=4!5C09u@c8TWFgfj@07-tO<$>gyjy`kOOh)ZXcS>4Y
zvvF0i7vfB!_V?--`Q=6Q9efOo&j*ThA}X!Mq)>zyk@84MTD^oA^7|Z|-%7fC{8-o5
z_@@K%N|D~4zF8#1!mzAuaF~S&jELP|LAR%GZ)JblpQKGIBYL`CF-^S7(xCkn2Gwn@lq@VI*MObfiBAOO((CX&E$q|D
zWaz*c;_)uQi8Zf6LzdYRy+hwt)z~2h<1_5_qtXp7Y78X5@thu#xhf*JE^hE^-
zUi-+%AFFroyY!dArcMKz?LJ(jb2^S}`O?+`9VoMI$Hk6AkW|a^qiQN-Tc}I4_OR_U
za5bUZ>GBJAKmy0ol4PGvpb-R;@S3pHyVmS(%l<%@XQ(J
z<>wCyp8>e^s@t4`74q&CMfgQqlYMf1lZJVBt`DH+Cn7gEUw<7x)rg(-36dhGl>lY{
z9k@3VA_6R~gdu=ji8YIOsnu-VM^ru4g26a03s3Lhe+EJgIc$Jl^TDT*CE%E1OLq7>
z^u5%ELf6J{%^3-+)OmJ%_hjG*2gY1tQTS;@7%wf4z!>(gZ49IsMb%4U`R2JD4c!RB9ISvE=&Al`rIrDljK
zKJ`!d{Ag(owSI5b4&Vlz0AoJdO07jva2;w(Ra4Mr1J$1vV%D-hSx&LI%4NiOUj0N-Dnr}4?yUs`!
z-(uD+%@v24fGTbRD|1;=lFcc?r0+GCa;S5k2+7m1LX0Hn{re4hwScWgIp{?T$zoIU
zR`fRVfkuz3(ZOaFu5lyD-R?Azg!wF5?BkZpnP{c{HIYB2WoAbCw3o#kPY>|=I-$fi
z{-;YS#PNGMcT>|!kz{#JhMPiW7Uek3iTiAd*zgY`$+E+MYGfDkt4mV(DxZM1nT!-Y
znfB1Ttgx?r$7Oms(rkDek$yGs!LxbWUQmn@p!`#3znEGJdUhj|a?jC_xH0?Z)#7qg
z*Bjc^pC7T)vH?>?ZwXhJxxvR)@7|cR_B-5|Y;MgpV<9l8)DjrvY31+=|{?=sY&>x9|6i1DLGqpO*`9uS1IdE9>?>
zCv5&K)CkrlCu)zUXH#UB9sq=WdG_Aal%zJV8ld*~Uw;72@3F#+!#u;Qx+O5G<~i~}
z=wF@quE-hvQ3Dv_7ZUN!IG3Au@
z{2xC>jF3{k3Mg2?Zfml$xWuR?B1eDHveYKS$ML{SCqoPHTA5qG#)BAiO90m59_yu^
ztX+$wDM5n;hiWNcGFQku84Ck|jAJqqw|W_5
z5Ce=*QOd!+FR3Q55E|NG+4FlmKoZhwNC$fUytL?#q(hs5g_OW8M`LhBwnAh)nDY*?
zwG2ZN0uX-&fEKBIMbQO#T(F~HwV2#hgWq{kTO`LZ3Rk}ai24#ZS;7~ICA%M+%=6R5
zJ5zIMY4am%-+7L4C5A#kqH*a45IoUK(hS$^v8%p&?Z&0Ui)VFJk9a*|
zh+vW&olOni|9M_U+1st<`r~hCX+jWBV;K*o&cWe@G2;_>#QoJ>UKrqzX_8prpEx1!J!tvc+V})Dz#@Uwv-nmgA!hCGC==+if@h
z%ekd)HyZ%&`NmZ21HXfCiXb^K8
z7cVA9M&iUsqmC>jC1_f$$Jk~OqT)B#=Dc|nCjjEOBDv}AatwLVhj@UJ?J`njuFSNX
zDOG#V(y}DY3Vljk&qr(Uep~yHEXJRGn<|DS(uzEi;m%+W;=c9XkP_x$Sou}1PxY(r
z+4?fBPR?9(luu6UFP{p@y#sV_e17y;zn}$TJK%j5^g&zi5{(y1_&)#A@zYG@_H*JqwY);yi0(>-hcbf`B=s^~3
z0p_)QmSg)5P#NarFG|!22}xv&e34d5U>#jhY&&)^hu!(r^Bk6F04fv#kiu}TLGtZs
zzp%XSW%gmuaf+R-@mX8_Y{J;8gp8fZ!7DP=+dxW={;sa$>af53jU2NF&VLv1&~1D8
z-mRNgUW%o5k`h}0!0`d*-zf@Ah>@}?hy+(=87{B8ykryBhJ9cX7SZqxNi-lMO~?U~
z3vzSzzkHWehWCG{S>yVaOqXdJD8<1F@u|`ONp6K_A^HhPpf_6+-eYQjbx&TV&vo=6
z1&74sj2Cdt&EKsab4UOr8A#^PMv5oK6#RSS&7n8$V9+W1>`}G%x}$$__X;-Hn+Fsl
zK+0iaGm_WPGv3wbQV+}pGuTGn%$A&pNCL93u+q_Mpj=92{#T&S)3fe9R3R)b=WXF<
zCq%^-be;C_fs~y7Ldy4jPYy@Yd@out>o1i4s_v?>w>R2f1Ut80<@vO~gZxx+*Ojs1
zW1mNY0ohM=Qu_U*$CGDx?Fno9X|cKq`&_ZXs7QqP#P`{7^3~rAo42T9#7T+}=G&*CqL@Nn^
zwWhyBau^PFUA2lwW0~@|cfo!d+5asKT&-ze&f&uQc
z4elE$DZc}f@`uh}#$aBN-UXapcx3rpgiFD;wv?ND+^8GVJA<0kyraVZ*0kqRniV^c
z86JxAmAuA-wF19Dx9>+h7-(PLQC+~x+{OL7dE{SuNyTIz>y#XaH1q4ZFt#R
z*x*rY7+Y@hA_I@4gYj)z>^}yI%d~%0jqNArq3gdWqy>kzM|#}sxNFD8?=`ofn{0V$
zf0Nd45;?mb64Ev*M(Z57V(yNOV20RPz@<$Tk1YjZciv|57`ONDf>fDFa3Nk2Y;HT)
zW`vs`tz}a5zu@sYYtR$5+aA5anrJ|$bli|aBTw2GQ&KG_w%3-9-lFZ!j|j~nRfzW@
z={z>i{j>j!R42xsJ%07@OT8DzmtxohT)i7qK+qN_9iOb~pC;=h%Me@T(}1o?;u&Tf
zIC~TpejMlg;RJ4uxd@+y|d2%7xUWa=P!2s=(7HSJjKkeVEfKR#k!lSoU~CQkJkv
z>B`ZtG_^kx!MvnsgK9hpg$|(jqn@sYfOY$1X&C^EJ{qb25~a8)C$Tn{IMs@=`5PK0
z@$f4k>wZlvuKPI&ERm{ouW)0&kN;%}4giKK?gl5FA0FL5+Y+zVuz%{$xj^(0gnT}~
zNy%K9MDd#I==ZXQrphIWoh5~I7T;CT#=PnSeN6T-YBVRAxAj
zNkfIpqs+N`Pr-E>4VEVXTiowS*U8u40IH5QuVmw!bsqtX^lGYpw}MCcvt>lc%w%5*s_l8o
zcjZr;RxWBx*dspMn~0>J`__!QkuR{s@7V38B>E{b&=N>n@T5n*!f>7d=3As0nh_rIVEP)!m5*tpk&`1FhYOZ`@u!(2d~fg=te;hH$+|d_ip02&e_vI1
z*(u}d?dId7Z2+n-8EKXMyk2KFKBc7in%0O^P1~O48viPAc0$z{S}!3tI(ZM!t0p`q
z`ZE9ouHU}Jf1~}~RyM6osb+5IAHado#0opDmmWr+=Du&NtmvQyNNowwN<=>v%Fw!T
z(xEQt#RN2J5~?I07tCNCxihQSf{7n>Ti+RqXClm)6^i|SaQ$vVbJ4@T4*O}CGy{XH
zs&pF9A!R}O-xcY!$nL;al?ecMU#+!z5U?=F>R9ulP_>k-Hql9K<3c;MY`*Hr!}tA3f6U|7FOkSn1THgc>0H&iCn2C
z`fIjCEN-wv*526oml)OCcl<6M%{=oR+8KN9n9&|sm2iTAX(NTmk{`JQ9pIFgWmvq=
ze1nTe#YTS6Gfqna5hVA~XG~XdcAfe8rTq?>VQF?cmCmRCbNlk60$a5=Q?o`m=Pj!3
zQ#IbRHXjegW<~H0{oOo`S`BS>8VPtJew7P!`ZfjNv}9P=&JL22G+I|;V<%jda
zxCgVP%G}I(8ZEGR?cX{IBZWZTqi(5AjrEn*uQde$0XgD&sYQUE&o@y@u*|MoZoPOf&}0AY5AO)@rw%J7bA4o(T00N2XRgw5Pm~4dJ-gur%
z4|k5gcCO>{p=jY_!0ql2OmTrRRj%+k^9G<5G%6>0B(J6byhkcu`MF=Z`8AgiO&D@o
zC&-pd@|U>6$j_J41{~L$Z0yd?lZdsGHX52_>o^43@=z%_rE-@+e})dOUrJQg2u}d8
z-aJay^43j>@bH0G51)!_Co9YiuHOA}jaB5OD;ASe@1xnAzq5^vZXham|MTXNOfHHh
zPrSk&z;Rt&BOBL%dJ;6tT3GrcnjPrM2E#eck>;$}echRsqJ_1N!0$&`;)?bl>=MIG
zl@9>gdiKpTcI3-rFMndRA8>&x7w2&?h2q}X9J|CA77(-tFPJZFmRJ(MbMlHj@wy*y
zpt!IbEV(PnT;EvOx61cL`>f60WA^Vj_5S=B&+G*;HVP-y)K^j^%tej-^-6=}zys>)
zpZqUMssINzSc^jHNCfmT5WTjs-;m+d$&s7Gj5$y}vMWs7fJeTwYKR-f+yr>D>p!6l
z-Xi0R6jJ={*s3e$q5dyt4x^+h-$*R09$dP@BnaG-e}*XQhDo2>7T%nn2N=keJ8%
zzPEVIFsJIoMWP;bYz^o{qiMf>V&LK+&
z7)091{;>fudC4z-8a^8W=zPrW9?B#r*KQSI0UQi7#5jKGojofTSq~!0#>G9aqKsxI
zg;^y~*cgFV;;1sWb8y6Zs#P(;40arKUC`BstS@{Ww%1GUk2j>m_V$6+zo2h%sFMh`
z)kR@_0dzIlg-M?N_=dA7Wia;E>s6T>mz~TABksA6xtZCWn2cxW)>{OCp
z$|+8@G~0}^PcuRP7CmeERrc3^L4O4xKU6*eYkPGv&mJy2yoKwU&5jDX`46ZIrEe&<
zT6U_LKk>$e=7!
zS>5wNDOQcn=e*H%B)s7XoOEM|-FAs*evGzQ)Z5XQ?SH?9y|wzY&IWVfh_S_Au$(>f
zba|p#*Ci2M*AtZ5?b#(05GU2^h={OQz{BV74xSJq;}{tP|8|8~nfM;u+H<)HMIW3J
zZpHiacr^TVDeH4*MTA8DxZ}zmWl?fey|~e?ZL5X={(M?;oO`vI5$qq?!$YDO#j;9r
zqWqMu&_&zt&X$bU@ooJ|Ml1L=XLKl8@^}4}SSGmj1?DYs%QL#~MfgE2!w*~xn|`@D
zIV)A)N_^_GL0Y1|B?{uVnSv|+)MbyHq%`@8@*sv6XW}>5vs_91scviINSfi{2zt$n
zzYug9x5H=jKR4{R-d)QMj!3P4WNqn`8YqHoy3Z)6MEK9Oe(R49PPS#4g4fhh@P8Kv
z+sabw$4LwSHX`c}G3OtV;QR(fX%L8-iS&N~R?~}Z$V!dA1k#Rc_~$>obpD}eoqW*Z
zbAtYquw@081FJL5eYK~4u$EPUs87WP)|s^H;o}nl`svoU6Q5
zEx6raX7(zMfllC~)V~j?j69?%%a4!aIYl#9=%)hqPfdC@AkB-N!
zw`-B&DFMH85YZwB}8pzVYv|
zX2qJ1UpLLt2n%AYl+SyG^xLP9P&)HsP4b7`v_&7EH_)YRn%u{qHADJ($Mozi@W_5s
zq_1S&wpo8?BpwAfN^G?WFce95qIX>=`Wz_4^t{z1b*Z7mRVy*Dl*z6NyxUc8&Q+4_
z(z#ddW)$X;mr)P#*ebg^NremzfH%C2hm5h9x$3_=s1zM<7*`61M>JWL3c5R?kmg%`
z(VLmlh`oDmNByXEC2(~u|1(K8_cTtTfV~;qXxZxRw`Xq&SLmjU5POWAsn)g6QB!^+
z<%kIVshTG18#xV#b7~P{!O6tylh
z3mQBNkWU(&dVkSss*qieAKg{ve3ybcroZP~DRa?N$%%?+(RmX1rh%=NJ{AIh;fq%(
z=>z|L2KW49pE#XbOhFHr&7JDF1a}++)9bbVc!}LJ&Gq5mcZ5?FWK2z${Kzt!9Q~k5
z;dRAKWE|^eiG34>JO24a_gffuS9bl&Ct6-+S?B+rrU+?g+f^udWFj(!jr(6erx&d<
zUCOh>wtrOh%6L65Wb!zJ8zlsJ5rI)X$u9Xj|Ndv~tCiD6y1r021a(V$L3F$NpdXl6Gym49>1q7zC}Ljdd_U-3XzTT(@Pe5+
zB_{CMP^hFPH{3c^3IJE7J4;$wTk$hGl-gVY*urymkJEl`dBOsvWQP9^%g&m_84V
ze_&XDT>;u*R-42aNpFPd;7hny
zP~^=DVCM%+u*H<&!$TAV6TWkWA{=1ZXO{I{p!o;c2m6hu^N=up~
z95hKyMrsC?{$0?@#3XCkN_%GPSn)(>aQ
zmxnV(h3TA~o$}oiW{8X^-fs4(wGrvfOvzn+39>2sUgf9d~cr4L{C>WHM)ojvm)Bqzh)fFNQA~0uDyA!uCd*g?96ls)-3m>Z8G}VLX
zAgtMLx;!Jhdyh2G#>c0_F8sX24iP3l3bP?lQ9Y_(4l_y5$meRz@lzs85K`xhe)eqo
zrDcY0RFrV>%Kx+SxmPBdTMOA;VtM6akzqT;^-g7bk
zc!z?AEO&QhELyi$FLv%+!==lLmRdJlZ0BH|5U_Xe-$_dj|F>Ap{M-L^e*3MfcV+!Q
zgeafidVOj3!Rtn)k9!|KEwP$1=~NL=M=uAfK~hQSv6RZ#s>fgXOA`J|?yY)JJGb|X
zYSo&S83qRz?}-f0_$D(y?Y!*rDvl|Kms$DHEQ{pl1(sx8cI|+v7j}u&mUXZe^WZ^*m&IpG@bgv-RaK&b*W>EjxDQ
zs%Ut4x{s~x-|2ro+sZ9hd%8qJ{rKwaWlOZmqgHL&ym!~9RVg>iVt_`ds#-=yu{V4M
zdMP9*$d+ltdqFPt~8PEU@hq1&Z}iGksO
zMcH-G@g*{Xpo2Od1Fd9WkZ^~{D1kP89s{N-VB+fI0G=|`PzX#l3=BQWptFWXwTy-s
d;RN%awZ7ueMX8O+>A-};;OXk;vd$@?2>?ptVygfE
literal 0
HcmV?d00001
diff --git a/docs/source/charts/images/charts_sidebar.png b/docs/source/charts/images/charts_sidebar.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1202fdbd16933f4fa9fed6bda65dd97f09a89f9
GIT binary patch
literal 26551
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Μ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
k|DXPUN$mT7^F>0(Z-HdGPyhe`
literal 0
HcmV?d00001
diff --git a/docs/source/charts/index.rst b/docs/source/charts/index.rst
new file mode 100644
index 00000000..5ea98881
--- /dev/null
+++ b/docs/source/charts/index.rst
@@ -0,0 +1,32 @@
+.. _Charts:
+
+Charts
+======
+
+Piccolo Admin can display different types of charts based on yours
+data. Five chart types are supported: ``Pie``, ``Line``, ``Column``,
+``Bar`` and ``Area``.
+
+Here's an example of charts usage, using FastAPI:
+
+.. literalinclude:: ./examples/app.py
+
+Piccolo Admin will then show a charts in the UI.
+
+.. image:: ./images/charts_sidebar.png
+
+.. image:: ./images/chart.png
+
+.. warning::
+
+ The data format must be a ``list of lists``
+ (eg. ``[["Male", 7], ["Female", 3]]``).
+
+-------------------------------------------------------------------------------
+
+Source
+------
+
+.. currentmodule:: piccolo_admin.endpoints
+
+.. autoclass:: ChartConfig
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8ea42457..74def468 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -46,6 +46,7 @@ Table of Contents
../help_text/index
../table_config/index
../custom_forms/index
+ ../charts/index
../actions/index
../media_storage/index
../internationalization/index
diff --git a/piccolo_admin/endpoints.py b/piccolo_admin/endpoints.py
index ba02de98..0c951fc0 100644
--- a/piccolo_admin/endpoints.py
+++ b/piccolo_admin/endpoints.py
@@ -349,6 +349,60 @@ class FormConfigResponseModel(BaseModel):
description: t.Optional[str] = None
+@dataclass
+class ChartConfig:
+ """
+ Used to specify charts, which are passed into ``create_admin``.
+
+ :param title:
+ This will be displayed in the UI in the sidebar.
+ :param chart_slug:
+ This determines which chart will be displayed.
+ :param chart_type:
+ Available chart types. There are five types: ``Pie``, ``Line``,
+ ``Column``, ``Bar`` and ``Area``.
+ :param data:
+ The data to be passed to the admin ui. The data format must be
+ a ``list of lists`` (eg. ``[["Male", 7], ["Female", 3]]``).
+
+ Here's a full example:
+
+ .. code-block:: python
+
+ async def director_movie_count():
+ movies = await Movie.select(
+ Movie.director.name.as_alias("director"),
+ Count(Movie.id)
+ ).group_by(
+ Movie.director
+ )
+ # Flatten the response so it's a list of lists
+ # like [['George Lucas', 3], ...]
+ return [[i['director'], i['count']] for i in movies]
+
+ director_chart = ChartConfig(
+ title='Movie count',
+ chart_type="Pie", # or Bar or Line etc.
+ data=director_movie_count,
+
+ create_admin(charts=[director_chart])
+
+ """
+
+ def __init__(self, title: str, chart_type: str, data: t.List[t.Any]):
+ self.title = title
+ self.chart_slug = self.title.replace(" ", "-").lower()
+ self.chart_type = chart_type
+ self.data = data
+
+
+class ChartConfigResponseModel(BaseModel):
+ title: str
+ chart_slug: str
+ chart_type: str
+ data: t.List[t.Any]
+
+
def handle_auth_exception(request: Request, exc: Exception):
return JSONResponse({"error": "Auth failed"}, status_code=401)
@@ -401,6 +455,7 @@ def __init__(
translations: t.List[Translation] = None,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
+ charts: t.List[ChartConfig] = [],
) -> None:
super().__init__(
title=site_name,
@@ -484,6 +539,10 @@ def __init__(
self.site_name = site_name
self.forms = forms
self.read_only = read_only
+ self.charts = charts
+ self.chart_config_map = {
+ chart.chart_slug: chart for chart in self.charts
+ }
self.form_config_map = {form.slug: form for form in self.forms}
with open(os.path.join(ASSET_PATH, "index.html")) as f:
@@ -584,6 +643,22 @@ def __init__(
tags=["Forms"],
)
+ private_app.add_api_route(
+ path="/charts/",
+ endpoint=self.get_charts, # type: ignore
+ methods=["GET"],
+ tags=["Charts"],
+ response_model=t.List[ChartConfigResponseModel],
+ )
+
+ private_app.add_api_route(
+ path="/charts/{chart_slug:str}/",
+ endpoint=self.get_single_chart, # type: ignore
+ methods=["GET"],
+ tags=["Charts"],
+ response_model=ChartConfigResponseModel,
+ )
+
private_app.add_api_route(
path="/user/",
endpoint=self.get_user, # type: ignore
@@ -836,6 +911,38 @@ def get_user(self, request: Request) -> UserResponseModel:
user_id=str(request.user.user_id),
)
+ ###########################################################################
+ # Custom charts
+
+ def get_charts(self) -> t.List[ChartConfigResponseModel]:
+ """
+ Returns all charts registered with the admin.
+ """
+ return [
+ ChartConfigResponseModel(
+ title=chart.title,
+ chart_slug=chart.chart_slug,
+ chart_type=chart.chart_type,
+ data=chart.data,
+ )
+ for chart in self.charts
+ ]
+
+ def get_single_chart(self, chart_slug: str) -> ChartConfigResponseModel:
+ """
+ Returns single chart.
+ """
+ chart = self.chart_config_map.get(chart_slug, None)
+ if chart is None:
+ raise HTTPException(status_code=404, detail="No such chart found")
+ else:
+ return ChartConfigResponseModel(
+ title=chart.title,
+ chart_slug=chart.chart_slug,
+ chart_type=chart.chart_type,
+ data=chart.data,
+ )
+
###########################################################################
# Custom forms
@@ -845,7 +952,9 @@ def get_forms(self) -> t.List[FormConfigResponseModel]:
"""
return [
FormConfigResponseModel(
- name=form.name, slug=form.slug, description=form.description
+ name=form.name,
+ slug=form.slug,
+ description=form.description,
)
for form in self.forms
]
@@ -1034,6 +1143,7 @@ def create_admin(
auto_include_related: bool = True,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
+ charts: t.List[ChartConfig] = [],
):
"""
:param tables:
@@ -1136,6 +1246,10 @@ def create_admin(
If ``True``, debug mode is enabled. Any unhandled exceptions will
return a stack trace, rather than a generic 500 error. Don't use this
in production!
+ :param charts:
+ For each :class:`ChartConfig `
+ specified, a chart will automatically be rendered in the user interface,
+ accessible via the sidebar.
""" # noqa: E501
auth_table = auth_table or BaseUser
@@ -1181,4 +1295,5 @@ def create_admin(
translations=translations,
allowed_hosts=allowed_hosts,
debug=debug,
+ charts=charts,
)
diff --git a/piccolo_admin/example.py b/piccolo_admin/example.py
index 8f49d8f6..6e1ff36e 100644
--- a/piccolo_admin/example.py
+++ b/piccolo_admin/example.py
@@ -47,6 +47,7 @@
from starlette.requests import Request
from piccolo_admin.endpoints import (
+ ChartConfig,
FormConfig,
OrderBy,
TableConfig,
@@ -440,6 +441,22 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
],
auth_table=User,
session_table=Sessions,
+ charts=[
+ ChartConfig(
+ title="Movie count",
+ chart_type="Pie",
+ data=[
+ ["George Lucas", 4],
+ ["Peter Jackson", 6],
+ ["Ron Howard", 1],
+ ],
+ ),
+ ChartConfig(
+ title="Director gender",
+ chart_type="Column",
+ data=[["Male", 7], ["Female", 3]],
+ ),
+ ],
)
diff --git a/piccolo_admin/translations/data.py b/piccolo_admin/translations/data.py
index c8f41606..4f0c0f19 100644
--- a/piccolo_admin/translations/data.py
+++ b/piccolo_admin/translations/data.py
@@ -35,6 +35,7 @@
"Back to home page": "Back to home page",
"Back": "Back",
"Change password": "Change password",
+ "Charts": "Charts",
"Clear filters": "Clear filters",
"Close": "Close",
"Create": "Create",
@@ -104,6 +105,7 @@
"Back to home page": "Yn ôl i'r dudalen gartref",
"Back": "Ol",
"Change password": "Newid cyfrinair",
+ "Charts": "Siartiau",
"Clear filters": "Clirio hidlwyr",
"Close": "Cau",
"Create": "Creu",
@@ -172,6 +174,7 @@
"Back to home page": "Vrati se na početnu stranicu",
"Back": "Natrag",
"Change password": "Promijeni lozinku",
+ "Charts": "Grafikoni",
"Clear filters": "Obriši filtere",
"Close": "Zatvori",
"Create": "Kreiraj",
@@ -241,6 +244,7 @@
"Back to home page": "Voltar à página inicial",
"Back": "Voltar atrás",
"Change password": "Mudar senha",
+ "Charts": "Gráficos",
"Clear filters": "Limpar Filtros",
"Close": "Fechar",
"Create": "Criar",
@@ -310,6 +314,7 @@
"Back to home page": "Zurück zur Startseite",
"Back": "Zurück",
"Change password": "Passwort ändern",
+ "Charts": "Diagramme",
"Clear filters": "Filter löschen",
"Close": "Schließen",
"Create": "Anlegen",
@@ -379,6 +384,7 @@
"Back to home page": "Retour à la page d'accueil",
"Back": "Retour",
"Change password": "Changer le mot de passe",
+ "Charts": "Graphiques",
"Clear filters": "Supprimer les filtres",
"Close": "Fermer",
"Create": "Créer",
@@ -448,6 +454,7 @@
"Back to home page": "Volver a la página de inicio",
"Back": "atrás",
"Change password": "Cambia la contraseña",
+ "Charts": "Gráficos",
"Clear filters": "Eliminar filtros",
"Close": "Cerca",
"Create": "Crear",
@@ -516,6 +523,7 @@
"Back to home page": "Takaisin pääsivulle",
"Back": "Takaisin",
"Change password": "Vaihda salasana",
+ "Charts": "Kaaviot",
"Clear filters": "Nollaa suodattimet",
"Close": "Sulje",
"Create": "Luo",
@@ -584,6 +592,7 @@
"Back to home page": "Вернуться на главную страницу",
"Back": "Назад",
"Change password": "Сменить пароль",
+ "Charts": "Графики",
"Clear filters": "Сбросить фильтры",
"Close": "Закрыть",
"Create": "Создать",
@@ -652,6 +661,7 @@
"Back to home page": "Повернутися на головну сторінку",
"Back": "Назад",
"Change password": "Змінити пароль",
+ "Charts": "Діаграми",
"Clear filters": "Очистити фільтри",
"Close": "Закрити",
"Create": "Створити",
@@ -720,6 +730,7 @@
"Back to home page": "返回主页",
"Back": "返回",
"Change password": "修改密码",
+ "Charts": "图表",
"Clear filters": "清除过滤器",
"Close": "关闭",
"Create": "创建",
@@ -789,6 +800,7 @@
"Back to home page": "Anasayfaya geri dön",
"Back": "Geri dön",
"Change password": "Şifreyi değiştir",
+ "Charts": "Grafikler",
"Clear filters": "Filtreleri temizle",
"Close": "Kapat",
"Create": "Oluştur",
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index 2d4ee44b..ee002812 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -801,7 +801,6 @@ def test_get_language_failed(self):
class TestHooks(TestCase):
-
credentials = {"username": "Bob", "password": "bob123"}
def setUp(self):
@@ -874,7 +873,6 @@ def hook(row, mock: MagicMock = mock):
class TestValidators(TestCase):
-
credentials = {"username": "Bob", "password": "bob123"}
def setUp(self):
@@ -930,3 +928,84 @@ def post_single_validator(piccolo_crud, request):
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.content, b'{"detail":"Not allowed!"}')
+
+
+class TestCharts(TestCase):
+ credentials = {"username": "Bob", "password": "bob123"}
+
+ def setUp(self):
+ create_db_tables_sync(SessionsBase, BaseUser, if_not_exists=True)
+ BaseUser.create_user_sync(
+ **self.credentials, active=True, admin=True, superuser=True
+ )
+
+ def tearDown(self):
+ SessionsBase.alter().drop_table().run_sync()
+ BaseUser.alter().drop_table().run_sync()
+
+ def test_charts(self):
+ """
+ Make sure the charts listing can be retrieved.
+ """
+ client = TestClient(APP)
+
+ # To get a CSRF cookie
+ response = client.get("/")
+ csrftoken = response.cookies["csrftoken"]
+
+ # Login
+ payload = dict(csrftoken=csrftoken, **self.credentials)
+ client.post(
+ "/public/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
+ )
+
+ #######################################################################
+ # List all forms
+
+ response = client.get("/api/charts/")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.json(),
+ [
+ {
+ "title": "Movie count",
+ "chart_slug": "movie-count",
+ "chart_type": "Pie",
+ "data": [
+ ["George Lucas", 4],
+ ["Peter Jackson", 6],
+ ["Ron Howard", 1],
+ ],
+ },
+ {
+ "title": "Director gender",
+ "chart_slug": "director-gender",
+ "chart_type": "Column",
+ "data": [["Male", 7], ["Female", 3]],
+ },
+ ],
+ )
+
+ #######################################################################
+ # Now get the ChartConfig for a single chart
+
+ response = client.get("/api/charts/movie-count/")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.json(),
+ {
+ "title": "Movie count",
+ "chart_slug": "movie-count",
+ "chart_type": "Pie",
+ "data": [
+ ["George Lucas", 4],
+ ["Peter Jackson", 6],
+ ["Ron Howard", 1],
+ ],
+ },
+ )
+ response = client.get("/api/charts/no-such-chart/")
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.content, b'{"detail":"No such chart found"}')
From cafb5c4f89d84ed783de87a0141b40508de8aa00 Mon Sep 17 00:00:00 2001
From: sinisaos
Date: Fri, 26 May 2023 15:41:19 +0200
Subject: [PATCH 02/14] fix Playwright tests
---
e2e/test_codegen.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/e2e/test_codegen.py b/e2e/test_codegen.py
index 6e2de28d..ca9ce269 100644
--- a/e2e/test_codegen.py
+++ b/e2e/test_codegen.py
@@ -37,8 +37,8 @@ def test_row_listing_filter(playwright: Playwright, dev_server) -> None:
page.locator('input[name="username"]').press("Tab")
page.locator('input[name="password"]').fill("piccolo123")
page.locator('input[name="password"]').press("Enter")
- page.get_by_role("link", name="director").click()
- page.get_by_role("link", name="Show filters").click()
+ page.get_by_role("link", name="director", exact=True).click()
+ page.get_by_role("link", name="Show filters", exact=True).click()
page.locator('input[name="name"]').click()
page.locator('input[name="name"]').fill("Howard")
page.locator('input[name="name"]').press("Enter")
@@ -108,8 +108,8 @@ def test_file_upload(playwright: Playwright, dev_server) -> None:
page.locator('input[name="username"]').press("Tab")
page.locator('input[name="password"]').fill("piccolo123")
page.locator('input[name="password"]').press("Enter")
- page.get_by_role("link", name="director").click()
- page.get_by_role("link", name="8").click()
+ page.get_by_role("link", name="director", exact=True).click()
+ page.get_by_role("link", name="8", exact=True).click()
page.locator('input[type="file"]').click()
page.locator('input[type="file"]').set_input_files(
"./e2e/upload/piccolo.jpg"
@@ -143,7 +143,7 @@ def test_bulk_update(playwright: Playwright, dev_server) -> None:
page.locator('input[name="username"]').press("Tab")
page.locator('input[name="password"]').fill("piccolo123")
page.locator('input[name="password"]').press("Enter")
- page.get_by_role("link", name="director").click()
+ page.get_by_role("link", name="director", exact=True).click()
page.locator("th").first.click()
page.get_by_role("row", name="id Name Gender Photo").get_by_role(
"checkbox"
@@ -171,9 +171,9 @@ def test_table_crud(playwright: Playwright, dev_server) -> None:
page.locator('input[name="username"]').press("Tab")
page.locator('input[name="password"]').fill("piccolo123")
page.locator('input[name="password"]').press("Enter")
- page.get_by_role("link", name="director").click()
- page.get_by_role("cell", name="8").click()
- page.get_by_role("link", name="8").click()
+ page.get_by_role("link", name="director", exact=True).click()
+ page.get_by_role("cell", name="8", exact=True).click()
+ page.get_by_role("link", name="8", exact=True).click()
page.locator('input[name="name"]').click()
page.locator('input[name="name"]').fill("Ronald William Howard")
page.locator('input[name="name"]').press("Enter")
From 9ff72829871c7a12444730202468916e36551e99 Mon Sep 17 00:00:00 2001
From: sinisaos
Date: Sun, 28 May 2023 10:37:57 +0200
Subject: [PATCH 03/14] fix charts in dark mode
---
admin_ui/src/components/TableNav.vue | 2 +-
admin_ui/src/main.less | 23 +++++++++++++++++++
docs/source/charts/images/chart.png | Bin 19323 -> 11486 bytes
docs/source/charts/images/charts_sidebar.png | Bin 26551 -> 27819 bytes
4 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/admin_ui/src/components/TableNav.vue b/admin_ui/src/components/TableNav.vue
index 6133e410..3ed2b110 100644
--- a/admin_ui/src/components/TableNav.vue
+++ b/admin_ui/src/components/TableNav.vue
@@ -1,5 +1,5 @@
-
+