Skip to content

Commit d6f425a

Browse files
committed
make chart data dynamic
1 parent bc19e7c commit d6f425a

File tree

3 files changed

+82
-44
lines changed

3 files changed

+82
-44
lines changed

piccolo_admin/endpoints.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from __future__ import annotations
55

6+
import decimal
67
import inspect
78
import itertools
89
import json
@@ -349,7 +350,10 @@ class FormConfigResponseModel(BaseModel):
349350
description: t.Optional[str] = None
350351

351352

352-
@dataclass
353+
Number = t.Union[int, float, decimal.Decimal]
354+
ChartData = t.Sequence[t.Tuple[str, Number]]
355+
356+
353357
class ChartConfig:
354358
"""
355359
Used to specify charts, which are passed into ``create_admin``.
@@ -361,46 +365,58 @@ class ChartConfig:
361365
:param chart_type:
362366
Available chart types. There are five types: ``Pie``, ``Line``,
363367
``Column``, ``Bar`` and ``Area``.
364-
:param data:
365-
The data to be passed to the admin ui. The data format must be
366-
a ``list of lists`` (eg. ``[["Male", 7], ["Female", 3]]``).
368+
:param data_source:
369+
A function (async or sync) which returns the data to be displayed in
370+
the chart. It must returns a sequence of tuples. The first element is
371+
the label, and the second is the value::
372+
373+
>>> [("Male", 7), ("Female", 3)]
367374
368375
Here's a full example:
369376
370377
.. code-block:: python
371378
372-
async def director_movie_count():
379+
async def get_director_movie_count():
373380
movies = await Movie.select(
374381
Movie.director.name.as_alias("director"),
375382
Count(Movie.id)
376383
).group_by(
377384
Movie.director
378385
)
379386
# Flatten the response so it's a list of lists
380-
# like [['George Lucas', 3], ...]
381-
return [[i['director'], i['count']] for i in movies]
387+
# like [('George Lucas', 3), ...]
388+
return [(i['director'], i['count']) for i in movies]
382389
383390
director_chart = ChartConfig(
384391
title='Movie count',
385-
chart_type="Pie", # or Bar or Line etc.
386-
data=director_movie_count,
392+
chart_type="Pie", # or Bar or Line etc.
393+
data_source=get_director_movie_count
394+
)
387395
388396
create_admin(charts=[director_chart])
389397
390398
"""
391399

392-
def __init__(self, title: str, chart_type: str, data: t.List[t.Any]):
400+
def __init__(
401+
self,
402+
title: str,
403+
data_source: t.Callable[[], t.Awaitable[ChartData]],
404+
chart_type: t.Literal["Pie", "Line", "Column", "Bar", "Area"] = "Bar",
405+
):
393406
self.title = title
394407
self.chart_slug = self.title.replace(" ", "-").lower()
395408
self.chart_type = chart_type
396-
self.data = data
409+
self.data_source = data_source
397410

398411

399-
class ChartConfigResponseModel(BaseModel):
412+
class ChartResponseModel(BaseModel):
400413
title: str
401414
chart_slug: str
402415
chart_type: str
403-
data: t.List[t.Any]
416+
417+
418+
class ChartDataResponseModel(ChartResponseModel):
419+
data: ChartData
404420

405421

406422
def handle_auth_exception(request: Request, exc: Exception):
@@ -440,7 +456,7 @@ class AdminRouter(FastAPI):
440456
def __init__(
441457
self,
442458
*tables: t.Union[t.Type[Table], TableConfig],
443-
forms: t.List[FormConfig] = [],
459+
forms: t.Optional[t.List[FormConfig]] = None,
444460
auth_table: t.Type[BaseUser] = BaseUser,
445461
session_table: t.Type[SessionsBase] = SessionsBase,
446462
session_expiry: timedelta = timedelta(hours=1),
@@ -452,11 +468,11 @@ def __init__(
452468
production: bool = False,
453469
site_name: str = "Piccolo Admin",
454470
default_language_code: str = "auto",
455-
translations: t.List[Translation] = None,
471+
translations: t.Optional[t.List[Translation]] = None,
456472
allowed_hosts: t.Sequence[str] = [],
457473
debug: bool = False,
458-
charts: t.List[ChartConfig] = [],
459-
sidebar_links: t.Dict[str, str] = {},
474+
charts: t.Optional[t.List[ChartConfig]] = None,
475+
sidebar_links: t.Optional[t.Dict[str, str]] = None,
460476
) -> None:
461477
super().__init__(
462478
title=site_name,
@@ -538,13 +554,13 @@ def __init__(
538554

539555
self.auth_table = auth_table
540556
self.site_name = site_name
541-
self.forms = forms
557+
self.forms = forms or []
542558
self.read_only = read_only
543-
self.charts = charts
559+
self.charts = charts or []
544560
self.chart_config_map = {
545561
chart.chart_slug: chart for chart in self.charts
546562
}
547-
self.sidebar_links = sidebar_links
563+
self.sidebar_links = sidebar_links or {}
548564
self.form_config_map = {form.slug: form for form in self.forms}
549565

550566
with open(os.path.join(ASSET_PATH, "index.html")) as f:
@@ -657,15 +673,15 @@ def __init__(
657673
endpoint=self.get_charts, # type: ignore
658674
methods=["GET"],
659675
tags=["Charts"],
660-
response_model=t.List[ChartConfigResponseModel],
676+
response_model=t.List[ChartResponseModel],
661677
)
662678

663679
private_app.add_api_route(
664680
path="/charts/{chart_slug:str}/",
665681
endpoint=self.get_single_chart, # type: ignore
666682
methods=["GET"],
667683
tags=["Charts"],
668-
response_model=ChartConfigResponseModel,
684+
response_model=ChartDataResponseModel,
669685
)
670686

671687
private_app.add_api_route(
@@ -923,33 +939,35 @@ def get_user(self, request: Request) -> UserResponseModel:
923939
###########################################################################
924940
# Custom charts
925941

926-
def get_charts(self) -> t.List[ChartConfigResponseModel]:
942+
def get_charts(self) -> t.List[ChartResponseModel]:
927943
"""
928944
Returns all charts registered with the admin.
929945
"""
930946
return [
931-
ChartConfigResponseModel(
947+
ChartResponseModel(
932948
title=chart.title,
933949
chart_slug=chart.chart_slug,
934950
chart_type=chart.chart_type,
935-
data=chart.data,
936951
)
937952
for chart in self.charts
938953
]
939954

940-
def get_single_chart(self, chart_slug: str) -> ChartConfigResponseModel:
955+
async def get_single_chart(
956+
self, chart_slug: str
957+
) -> ChartDataResponseModel:
941958
"""
942-
Returns single chart.
959+
Returns a single chart.
943960
"""
944961
chart = self.chart_config_map.get(chart_slug, None)
945962
if chart is None:
946963
raise HTTPException(status_code=404, detail="No such chart found")
947964
else:
948-
return ChartConfigResponseModel(
965+
data = await chart.data_source()
966+
return ChartDataResponseModel(
949967
title=chart.title,
950968
chart_slug=chart.chart_slug,
951969
chart_type=chart.chart_type,
952-
data=chart.data,
970+
data=data,
953971
)
954972

955973
###########################################################################
@@ -1156,7 +1174,7 @@ def create_admin(
11561174
production: bool = False,
11571175
site_name: str = "Piccolo Admin",
11581176
default_language_code: str = "auto",
1159-
translations: t.List[Translation] = None,
1177+
translations: t.Optional[t.List[Translation]] = None,
11601178
auto_include_related: bool = True,
11611179
allowed_hosts: t.Sequence[str] = [],
11621180
debug: bool = False,

piccolo_admin/example.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from piccolo.columns.readable import Readable
4141
from piccolo.engine.postgres import PostgresEngine
4242
from piccolo.engine.sqlite import SQLiteEngine
43+
from piccolo.query.methods.select import Count as CountAgg
4344
from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
4445
from piccolo_api.media.local import LocalMediaStorage
4546
from piccolo_api.media.s3 import S3MediaStorage
@@ -418,6 +419,29 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
418419
menu_group="Testing",
419420
)
420421

422+
423+
async def get_director_movie_count():
424+
movies = (
425+
await Movie.select(
426+
Movie.director.name.as_alias("director"), CountAgg(Movie.id)
427+
)
428+
.group_by(Movie.director)
429+
.order_by(Movie.director.name)
430+
)
431+
432+
return [(i["director"], i["count"]) for i in movies]
433+
434+
435+
async def get_movie_genre_count():
436+
movies = (
437+
await Movie.select(Movie.genre, CountAgg(Movie.id))
438+
.group_by(Movie.genre)
439+
.order_by(Movie.genre)
440+
)
441+
442+
return [(i["genre"], i["count"]) for i in movies]
443+
444+
421445
APP = create_admin(
422446
[
423447
movie_config,
@@ -445,18 +469,14 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
445469
session_table=Sessions,
446470
charts=[
447471
ChartConfig(
448-
title="Movie count",
472+
title="Movies per director",
449473
chart_type="Pie",
450-
data=[
451-
["George Lucas", 4],
452-
["Peter Jackson", 6],
453-
["Ron Howard", 1],
454-
],
474+
data_source=get_director_movie_count,
455475
),
456476
ChartConfig(
457-
title="Director gender",
477+
title="Movies per genre",
458478
chart_type="Column",
459-
data=[["Male", 7], ["Female", 3]],
479+
data_source=get_movie_genre_count,
460480
),
461481
],
462482
sidebar_links={

requirements/dev-requirements.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
black==21.12b0
2-
isort==5.10.1
3-
twine==4.0.0
4-
mypy==0.942
1+
black==23.1a1
2+
isort==5.12.0
3+
twine==4.0.2
4+
mypy==1.4.1
55
pip-upgrader==1.4.15
6-
wheel==0.37.1
7-
python-dotenv==0.20.0
6+
wheel==0.40.0
7+
python-dotenv==1.0.0

0 commit comments

Comments
 (0)