Skip to content

Commit

Permalink
make chart data dynamic
Browse files Browse the repository at this point in the history
  • Loading branch information
dantownsend committed Jul 1, 2023
1 parent da9fe50 commit 976f55b
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 51 deletions.
80 changes: 44 additions & 36 deletions piccolo_admin/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import annotations

import decimal
import inspect
import itertools
import json
Expand Down Expand Up @@ -349,6 +350,10 @@ class FormConfigResponseModel(BaseModel):
description: t.Optional[str] = None


Number = t.Union[int, float, decimal.Decimal]
ChartData = t.Sequence[t.Tuple[str, Number]]


class ChartConfig:
"""
Used to specify charts, which are passed into ``create_admin``.
Expand All @@ -360,42 +365,40 @@ class ChartConfig:
: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 (e.g. ``[["Male", 7], ["Female", 3]]``).
:param data_source:
A function (async or sync) which returns the data to be displayed in
the chart. It must returns a sequence of tuples. The first element is
the label, and the second is the value::
>>> [("Male", 7), ("Female", 3)]
Here's a full example:
.. code-block:: python
import asyncio
import typing as t
from piccolo.query.methods.select import Count
from piccolo_admin.endpoints import (
create_admin,
ChartConfig,
ChartConfigResponseModel,
)
from movies.tables import Director, Movie
async def director_movie_count() -> t.List[ChartConfigResponseModel]:
from movies.tables import Movie
async def get_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]
chart_data = asyncio.run(director_movie_count())
# 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=chart_data,
chart_type="Pie", # or Bar or Line etc.
data_source=get_director_movie_count
)
create_admin(charts=[director_chart])
Expand All @@ -405,20 +408,23 @@ async def director_movie_count() -> t.List[ChartConfigResponseModel]:
def __init__(
self,
title: str,
chart_type: str,
data: t.List[t.List[t.Any]],
data_source: t.Callable[[], t.Awaitable[ChartData]],
chart_type: t.Literal["Pie", "Line", "Column", "Bar", "Area"] = "Bar",
):
self.title = title
self.chart_slug = self.title.replace(" ", "-").lower()
self.chart_type = chart_type
self.data = data
self.data_source = data_source


class ChartConfigResponseModel(BaseModel):
class ChartResponseModel(BaseModel):
title: str
chart_slug: str
chart_type: str
data: t.List[t.List[t.Any]]


class ChartDataResponseModel(ChartResponseModel):
data: ChartData


def handle_auth_exception(request: Request, exc: Exception):
Expand Down Expand Up @@ -458,7 +464,7 @@ class AdminRouter(FastAPI):
def __init__(
self,
*tables: t.Union[t.Type[Table], TableConfig],
forms: t.List[FormConfig] = [],
forms: t.Optional[t.List[FormConfig]] = None,
auth_table: t.Type[BaseUser] = BaseUser,
session_table: t.Type[SessionsBase] = SessionsBase,
session_expiry: timedelta = timedelta(hours=1),
Expand All @@ -470,11 +476,11 @@ def __init__(
production: bool = False,
site_name: str = "Piccolo Admin",
default_language_code: str = "auto",
translations: t.List[Translation] = None,
translations: t.Optional[t.List[Translation]] = None,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
charts: t.List[ChartConfig] = [],
sidebar_links: t.Dict[str, str] = {},
charts: t.Optional[t.List[ChartConfig]] = None,
sidebar_links: t.Optional[t.Dict[str, str]] = None,
) -> None:
super().__init__(
title=site_name,
Expand Down Expand Up @@ -556,13 +562,13 @@ def __init__(

self.auth_table = auth_table
self.site_name = site_name
self.forms = forms
self.forms = forms or []
self.read_only = read_only
self.charts = charts
self.charts = charts or []
self.chart_config_map = {
chart.chart_slug: chart for chart in self.charts
}
self.sidebar_links = sidebar_links
self.sidebar_links = sidebar_links or {}
self.form_config_map = {form.slug: form for form in self.forms}

with open(os.path.join(ASSET_PATH, "index.html")) as f:
Expand Down Expand Up @@ -675,15 +681,15 @@ def __init__(
endpoint=self.get_charts, # type: ignore
methods=["GET"],
tags=["Charts"],
response_model=t.List[ChartConfigResponseModel],
response_model=t.List[ChartResponseModel],
)

private_app.add_api_route(
path="/charts/{chart_slug:str}/",
endpoint=self.get_single_chart, # type: ignore
methods=["GET"],
tags=["Charts"],
response_model=ChartConfigResponseModel,
response_model=ChartDataResponseModel,
)

private_app.add_api_route(
Expand Down Expand Up @@ -941,33 +947,35 @@ def get_user(self, request: Request) -> UserResponseModel:
###########################################################################
# Custom charts

def get_charts(self) -> t.List[ChartConfigResponseModel]:
def get_charts(self) -> t.List[ChartResponseModel]:
"""
Returns all charts registered with the admin.
"""
return [
ChartConfigResponseModel(
ChartResponseModel(
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:
async def get_single_chart(
self, chart_slug: str
) -> ChartDataResponseModel:
"""
Returns a 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(
data = await chart.data_source()
return ChartDataResponseModel(
title=chart.title,
chart_slug=chart.chart_slug,
chart_type=chart.chart_type,
data=chart.data,
data=data,
)

###########################################################################
Expand Down Expand Up @@ -1174,7 +1182,7 @@ def create_admin(
production: bool = False,
site_name: str = "Piccolo Admin",
default_language_code: str = "auto",
translations: t.List[Translation] = None,
translations: t.Optional[t.List[Translation]] = None,
auto_include_related: bool = True,
allowed_hosts: t.Sequence[str] = [],
debug: bool = False,
Expand Down
44 changes: 35 additions & 9 deletions piccolo_admin/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from piccolo.columns.readable import Readable
from piccolo.engine.postgres import PostgresEngine
from piccolo.engine.sqlite import SQLiteEngine
from piccolo.query.methods.select import Count as CountAgg
from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
from piccolo_api.media.local import LocalMediaStorage
from piccolo_api.media.s3 import S3MediaStorage
Expand Down Expand Up @@ -421,16 +422,36 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
)


async def fetch_chart_data() -> t.List[t.List[t.Any]]:
async def get_director_movie_count():
movies = (
await Movie.select(
Movie.director.name.as_alias("director"), CountAgg(Movie.id)
)
.group_by(Movie.director)
.order_by(Movie.director.name)
)

return [(i["director"], i["count"]) for i in movies]


async def get_movie_genre_count():
movies = (
await Movie.select(Movie.genre, CountAgg(Movie.id))
.group_by(Movie.genre)
.order_by(Movie.genre)
)

return [(i["genre"], i["count"]) for i in movies]


async def placeholder_data():
return [
["George Lucas", 4],
["Peter Jackson", 6],
["Ron Howard", 1],
]


chart_data = asyncio.run(fetch_chart_data())

APP = create_admin(
[
movie_config,
Expand Down Expand Up @@ -458,29 +479,34 @@ async def fetch_chart_data() -> t.List[t.List[t.Any]]:
session_table=Sessions,
charts=[
ChartConfig(
title="Movie count Pie",
title="Movies per director",
chart_type="Pie",
data=chart_data,
data_source=get_director_movie_count,
),
ChartConfig(
title="Movies per genre",
chart_type="Column",
data_source=get_movie_genre_count,
),
ChartConfig(
title="Movie count Line",
chart_type="Line",
data=chart_data,
data=placeholder_data,
),

Check failure

Code scanning / CodeQL

Wrong name for an argument in a class instantiation Error

Keyword argument 'data' is not a supported parameter name of
ChartConfig.__init__
.
ChartConfig(
title="Movie count Column",
chart_type="Column",
data=chart_data,
data=placeholder_data,
),

Check failure

Code scanning / CodeQL

Wrong name for an argument in a class instantiation Error

Keyword argument 'data' is not a supported parameter name of
ChartConfig.__init__
.
ChartConfig(
title="Movie count Bar",
chart_type="Bar",
data=chart_data,
data=placeholder_data,
),

Check failure

Code scanning / CodeQL

Wrong name for an argument in a class instantiation Error

Keyword argument 'data' is not a supported parameter name of
ChartConfig.__init__
.
ChartConfig(
title="Movie count Area",
chart_type="Area",
data=chart_data,
data=placeholder_data,
),

Check failure

Code scanning / CodeQL

Wrong name for an argument in a class instantiation Error

Keyword argument 'data' is not a supported parameter name of
ChartConfig.__init__
.
],
sidebar_links={
Expand Down
12 changes: 6 additions & 6 deletions requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
black==21.12b0
isort==5.10.1
twine==4.0.0
mypy==0.942
black==23.1a1
isort==5.12.0
twine==4.0.2
mypy==1.4.1
pip-upgrader==1.4.15
wheel==0.37.1
python-dotenv==0.20.0
wheel==0.40.0
python-dotenv==1.0.0

0 comments on commit 976f55b

Please sign in to comment.