Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added filter columns for UI #103

Merged
merged 2 commits into from Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 17 additions & 7 deletions admin_ui/src/components/RowFormSearch.vue
Expand Up @@ -4,10 +4,15 @@
v-bind:key="property.title"
v-for="(property, keyName) in schema.properties"
>
<template v-if="property.extra.foreign_key">
<template
v-if="property.extra.foreign_key && schema.filter_column_names.includes(keyName)"
>
<label>
{{ property.title }}
<span class="required" v-if="isRequired(keyName)">*</span>
<span
class="required"
v-if="isRequired(keyName)"
>*</span>

<router-link
:to="{
Expand Down Expand Up @@ -39,18 +44,24 @@
<KeySearch
v-bind:fieldName="property.title.toLowerCase()"
v-bind:key="getValue(property.title)"
v-bind:tableName="property.extra.to"
v-bind:rowID="getKeySelectID(property.title)"
v-bind:readable="getValue(property.title)"
v-bind:rowID="getKeySelectID(property.title)"
v-bind:tableName="property.extra.to"
/>
</template>
<template v-else>
<template
v-else-if="schema.filter_column_names.includes(keyName) && property.format !== 'json'"
>
<label>
{{ property.title }}
<span class="required" v-if="isRequired(keyName)">*</span>
<span
class="required"
v-if="isRequired(keyName)"
>*</span>
</label>

<InputField
v-bind:choices="property.extra.choices"
v-bind:format="property.format"
v-bind:isFilter="isFilter"
v-bind:isNullable="property.nullable"
Expand All @@ -59,7 +70,6 @@
v-bind:title="property.title"
v-bind:type="property.type || property.anyOf[0].type"
v-bind:value="getValue(property.title)"
v-bind:choices="property.extra.choices"
/>
</template>
</div>
Expand Down
30 changes: 28 additions & 2 deletions docs/source/table_config/index.rst
Expand Up @@ -13,7 +13,12 @@ Alternatively, you can pass in ``TableConfig`` instances (or mix and match
them).

By passing in a ``TableConfig`` you have extra control over how the UI behaves
for that table. For example, we can set which columns are visible in the list
for that table.

visible_columns
---------------

For example, we can set which columns are visible in the list
view:

.. code-block:: python
Expand All @@ -33,7 +38,28 @@ columns are visible):

.. image:: ./images/with_table_config.jpg

This is really useful when you have a ``Table`` with lots of columns.
filter_columns
--------------

For example, we can set which columns are visible in the filter
sidebar:

.. code-block:: python

from piccolo_admin.endpoints import TableConfig

movie_config = TableConfig(Movie, filter_columns=[
Movie.name, Movie.rating, Movie.director,
])

create_admin(Director, movie_config)

Here is the UI when just passing in a ``Table``:

Here is the UI when just passing in a ``TableConfig`` instance instead (less
columns are visible in filter sidebar):

``TableConfig`` features is really useful when you have a ``Table`` with lots of columns.

In the future, ``TableConfig`` will be extended to allow finer grained control
over the UI.
Expand Down
46 changes: 42 additions & 4 deletions piccolo_admin/endpoints.py
Expand Up @@ -66,12 +66,21 @@ class TableConfig:
You can specify this instead of ``visible_columns``, in which case all
of the ``Table`` columns except the ones specified will be shown in the
list view.
:param filter_columns:
If specified, only these columns will be shown in the filter sidebar
of the UI. This is useful when you have a lot of columns.
:param exclude_filter_columns:
You can specify this instead of ``filter_columns``, in which case all
of the ``Table`` columns except the ones specified will be shown in the
filter sidebar.

"""

table_class: t.Type[Table]
visible_columns: t.Optional[t.List[Column]] = None
exclude_visible_columns: t.Optional[t.List[Column]] = None
filter_columns: t.Optional[t.List[Column]] = None
exclude_filter_columns: t.Optional[t.List[Column]] = None

def __post_init__(self):
if self.visible_columns and self.exclude_visible_columns:
Expand All @@ -80,6 +89,12 @@ def __post_init__(self):
"``exclude_visible_columns``."
)

if self.filter_columns and self.exclude_filter_columns:
raise ValueError(
"Only specify ``filter_columns`` or "
"``exclude_filter_columns``."
)

def get_visible_columns(self) -> t.List[Column]:
if self.visible_columns and not self.exclude_visible_columns:
return self.visible_columns
Expand All @@ -97,6 +112,23 @@ def get_visible_columns(self) -> t.List[Column]:
def get_visible_column_names(self) -> t.Tuple[str, ...]:
return tuple(i._meta.name for i in self.get_visible_columns())

def get_filter_columns(self) -> t.List[Column]:
if self.filter_columns and not self.exclude_filter_columns:
return self.filter_columns

if self.exclude_filter_columns and not self.filter_columns:
column_names = (i._meta.name for i in self.exclude_filter_columns)
return [
i
for i in self.table_class._meta.columns
if i._meta.name not in column_names
]

return self.table_class._meta.columns

def get_filter_column_names(self) -> t.Tuple[str, ...]:
return tuple(i._meta.name for i in self.get_filter_columns())


@dataclass
class FormConfig:
Expand Down Expand Up @@ -148,7 +180,8 @@ def my_endpoint(request: Request, data: MyModel):
name: str
pydantic_model: t.Type[BaseModel]
endpoint: t.Callable[
[Request, pydantic.BaseModel], t.Union[str, None, t.Coroutine],
[Request, pydantic.BaseModel],
t.Union[str, None, t.Coroutine],
]
description: t.Optional[str] = None

Expand Down Expand Up @@ -226,6 +259,7 @@ def __init__(
for table_config in table_configs:
table_class = table_config.table_class
visible_column_names = table_config.get_visible_column_names()
filter_column_names = table_config.get_filter_column_names()
FastAPIWrapper(
root_url=f"/tables/{table_class._meta.tablename}/",
fastapi_app=api_app,
Expand All @@ -234,7 +268,8 @@ def __init__(
read_only=read_only,
page_size=page_size,
schema_extra={
"visible_column_names": visible_column_names
"visible_column_names": visible_column_names,
"filter_column_names": filter_column_names,
},
),
fastapi_kwargs=FastAPIKwargs(
Expand Down Expand Up @@ -368,7 +403,8 @@ async def get_root(self, request: Request) -> HTMLResponse:

def get_user(self, request: Request) -> UserResponseModel:
return UserResponseModel(
username=request.user.display_name, user_id=request.user.user_id,
username=request.user.display_name,
user_id=request.user.user_id,
)

###########################################################################
Expand All @@ -393,7 +429,9 @@ def get_single_form(self, form_slug: str) -> FormConfigResponseModel:
raise HTTPException(status_code=404, detail="No such form found")
else:
return FormConfigResponseModel(
name=form.name, slug=form.slug, description=form.description,
name=form.name,
slug=form.slug,
description=form.description,
dantownsend marked this conversation as resolved.
Show resolved Hide resolved
)

def get_single_form_schema(self, form_slug: str) -> t.Dict[str, t.Any]:
Expand Down
11 changes: 10 additions & 1 deletion piccolo_admin/example.py
Expand Up @@ -185,11 +185,20 @@ async def booking_endpoint(request, data):
Movie.rating,
Movie.director,
],
filter_columns=[
Movie.name,
Movie.rating,
Movie.director,
],
)

director_config = TableConfig(
table_class=Director,
visible_columns=[Director._meta.primary_key, Director.name],
visible_columns=[
Director._meta.primary_key,
Director.name,
Director.gender,
],
)

APP = create_admin(
Expand Down
70 changes: 59 additions & 11 deletions tests/test_endpoints.py
@@ -1,7 +1,13 @@
from unittest import TestCase

from piccolo.apps.user.tables import BaseUser
from piccolo.columns.column_types import ForeignKey, Text, Timestamp, Varchar
from piccolo.columns.column_types import (
ForeignKey,
Integer,
Text,
Timestamp,
Varchar,
)
from piccolo.table import Table
from piccolo_api.session_auth.tables import SessionsBase
from starlette.testclient import TestClient
Expand Down Expand Up @@ -33,6 +39,7 @@ class Post(Table):
name = Varchar(length=100)
content = Text()
created = Timestamp()
rating = Integer()


class TestTableConfig(TestCase):
Expand All @@ -42,7 +49,8 @@ def test_visible_columns(self):
visible_columns=[Post._meta.primary_key, Post.name],
)
self.assertEqual(
post_table.get_visible_column_names(), ("id", "name"),
post_table.get_visible_column_names(),
("id", "name"),
)

def test_exclude_visible_columns(self):
Expand All @@ -52,7 +60,7 @@ def test_exclude_visible_columns(self):
)
self.assertEqual(
tuple(i._meta.name for i in post_table.get_visible_columns()),
("content", "created"),
("content", "created", "rating"),
)

def test_visible_exclude_columns_error(self):
Expand All @@ -64,6 +72,35 @@ def test_visible_exclude_columns_error(self):
)
post_table.get_visible_columns()

def test_filter_columns(self):
post_table = TableConfig(
table_class=Post,
filter_columns=[Post.name, Post.rating],
)
self.assertEqual(
post_table.get_filter_column_names(),
("name", "rating"),
)

def test_exclude_filter_columns(self):
post_table = TableConfig(
table_class=Post,
exclude_filter_columns=[Post._meta.primary_key, Post.name],
)
self.assertEqual(
tuple(i._meta.name for i in post_table.get_filter_columns()),
("content", "created", "rating"),
)

def test_filter_exclude_columns_error(self):
with self.assertRaises(ValueError):
post_table = TableConfig(
table_class=Post,
filter_columns=[Post.name],
exclude_filter_columns=[Post.name, Post.rating],
)
post_table.get_filter_columns()


class TestAdminRouter(TestCase):
def test_get_meta(self):
Expand Down Expand Up @@ -120,7 +157,9 @@ def test_forms(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
"/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
"/auth/login/",
json=payload,
headers={"X-CSRFToken": csrftoken},
)

#######################################################################
Expand Down Expand Up @@ -193,7 +232,9 @@ def test_post_form_success(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
"/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
"/auth/login/",
json=payload,
headers={"X-CSRFToken": csrftoken},
)
#######################################################################
# Post a form
Expand Down Expand Up @@ -241,7 +282,9 @@ def test_post_form_fail(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
"/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
"/auth/login/",
json=payload,
headers={"X-CSRFToken": csrftoken},
)
#######################################################################
# Post a form with errors
Expand Down Expand Up @@ -288,7 +331,9 @@ def test_tables(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
"/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
"/auth/login/",
json=payload,
headers={"X-CSRFToken": csrftoken},
)

#######################################################################
Expand All @@ -297,7 +342,8 @@ def test_tables(self):
response = client.get("/api/tables/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(), ["movie", "director", "studio"],
response.json(),
["movie", "director", "studio"],
)

def test_get_user(self):
Expand All @@ -313,7 +359,9 @@ def test_get_user(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
"/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
"/auth/login/",
json=payload,
headers={"X-CSRFToken": csrftoken},
)

#######################################################################
Expand All @@ -322,6 +370,6 @@ def test_get_user(self):
response = client.get("/api/user/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(), {"username": "Bob", "user_id": "1"},
response.json(),
{"username": "Bob", "user_id": "1"},
)