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

refactor: interface query on m-m joins and select specific columns #1398

Merged
merged 28 commits into from
Jul 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
060a96a
refactor: interface query on m-m joins and select specific columns
dpgaspar Jun 15, 2020
dd8b559
mssql hack
dpgaspar Jun 15, 2020
65d1759
add tests with many children
dpgaspar Jun 18, 2020
c3e772e
a bit of refactor on the refactor
dpgaspar Jun 18, 2020
a119c81
remove prints
dpgaspar Jun 18, 2020
cfb5d89
Merge branch 'master' into refactor/interface-query
dpgaspar Jun 18, 2020
ddcd26b
fix and cleanup
dpgaspar Jun 18, 2020
7d93f71
Merge remote-tracking branch 'origin/refactor/interface-query' into r…
dpgaspar Jun 18, 2020
020b8e3
fix and cleanup
dpgaspar Jun 23, 2020
f340c8a
type annotations and code quality
dpgaspar Jun 23, 2020
8c34130
type annotations and code quality
dpgaspar Jun 23, 2020
1c5ab9f
type annotations and code quality
dpgaspar Jun 23, 2020
47424d0
lint
dpgaspar Jun 23, 2020
108146a
more type annotations
dpgaspar Jun 23, 2020
db2127d
only use from_self if necessary
dpgaspar Jun 23, 2020
4f01462
Merge branch 'master' into refactor/interface-query
dpgaspar Jun 23, 2020
b5bba74
fix column model query loading for non inner queries
dpgaspar Jun 23, 2020
46ca15f
Merge remote-tracking branch 'origin/refactor/interface-query' into r…
dpgaspar Jun 23, 2020
f1041fe
Drying and fix get with improved performance
dpgaspar Jun 27, 2020
1085568
MVC starts using select columns, maybe revert
dpgaspar Jun 28, 2020
00f4e5d
revert select_columns from mvc
dpgaspar Jun 28, 2020
7cd065f
feat: list_select_columns to enable wider config
dpgaspar Jun 29, 2020
a570de6
one or none instead of first
dpgaspar Jun 30, 2020
deecae8
doc show_select_columns and list_select_columns
dpgaspar Jun 30, 2020
ecd5bfb
fix, one or none instead of first
dpgaspar Jul 1, 2020
e00451e
Merge branch 'master' into refactor/interface-query
dpgaspar Jul 2, 2020
9036f65
fix, docs
dpgaspar Jul 2, 2020
dd189a9
Merge remote-tracking branch 'origin/refactor/interface-query' into r…
dpgaspar Jul 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions docs/rest_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ so data can be translated back and forth without loss or guesswork::
if 'name' in kwargs['rison']:
return self.response(
200,
message="Hello {}".format(kwargs['rison']['name'])
message=f"Hello {kwargs['rison']['name']}"
)
return self.response_400(message="Please send your name")

Expand Down Expand Up @@ -238,7 +238,7 @@ validate your Rison arguments, this way you can implement a very strict API easi
def greeting4(self, **kwargs):
return self.response(
200,
message="Hello {}".format(kwargs['rison']['name'])
message=f"Hello {kwargs['rison']['name']}"
)

Finally to properly handle all possible exceptions use the ``safe`` decorator,
Expand Down Expand Up @@ -396,7 +396,7 @@ easily reference them::
"""
return self.response(
200,
message="Hello {}".format(kwargs['rison']['name'])
message=f"Hello {kwargs['rison']['name']}"
)


Expand Down Expand Up @@ -1015,6 +1015,33 @@ the ``show_columns`` property. This takes precedence from the *Rison* arguments:
datamodel = SQLAInterface(Contact)
show_columns = ['name']

By default FAB will issue a query containing the exact fields for `show_columns`, but these are also associated with
the response object. Sometimes it's useful to distinguish between the query select columns and the response itself.
Imagine the case you want to use a `@property` to further transform the output, and that transformation implies
two model fields (concat or sum for example)::

class ContactModelApi(ModelRestApi):
resource_name = 'contact'
datamodel = SQLAInterface(Contact)
show_columns = ['name', 'age']
show_select_columns = ['name', 'birthday']


The Model::

class Contact(Model):
id = Column(Integer, primary_key=True)
name = Column(String(150), unique=True, nullable=False)
...
birthday = Column(Date, nullable=True)
...

@property
def age(self):
return date.today().year - self.birthday.year

Note: The same logic is applied on `list_select_columns`

We can add fields that are python functions also, for this on the SQLAlchemy definition,
let's add a new function::

Expand All @@ -1034,7 +1061,7 @@ let's add a new function::
return self.name

def some_function(self):
return "Hello {}".format(self.name)
return f"Hello {self.name}"

And then on the REST API::

Expand Down
95 changes: 55 additions & 40 deletions flask_appbuilder/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import logging
import re
import traceback
from typing import Dict, Optional
from typing import Callable, Dict, List, Optional, Set
import urllib.parse

from apispec import APISpec, yaml_utils
from apispec.exceptions import DuplicateComponentNameError
from flask import Blueprint, current_app, jsonify, make_response, request, Response
from flask_babel import lazy_gettext as _
import jsonschema
from marshmallow import ValidationError
from marshmallow import Schema, ValidationError
from marshmallow_sqlalchemy.fields import Related, RelatedList
import prison
from sqlalchemy.exc import IntegrityError
Expand Down Expand Up @@ -214,39 +214,39 @@ class BaseApi(object):

appbuilder = None
blueprint = None
endpoint = None
endpoint: Optional[str] = None

version = "v1"
version: Optional[str] = "v1"
"""
Define the Api version for this resource/class
"""
route_base = None
route_base: Optional[str] = None
"""
Define the route base where all methods will suffix from
"""
resource_name = None
resource_name: Optional[str] = None
"""
Defines a custom resource name, overrides the inferred from Class name
makes no sense to use it with route base
"""
base_permissions = None
base_permissions: Optional[List[str]] = None
"""
A list of allowed base permissions::

class ExampleApi(BaseApi):
base_permissions = ['can_get']

"""
class_permission_name = None
class_permission_name: Optional[str] = None
"""
Override class permission name default fallback to self.__class__.__name__
"""
previous_class_permission_name = None
previous_class_permission_name: Optional[str] = None
"""
If set security converge will replace all permissions tuples
with this name by the class_permission_name or self.__class__.__name__
"""
method_permission_name = None
method_permission_name: Optional[Dict[str, str]] = None
"""
Override method permission names, example::

Expand All @@ -258,7 +258,7 @@ class ExampleApi(BaseApi):
'delete': 'write'
}
"""
previous_method_permission_name = None
previous_method_permission_name: Optional[Dict[str, str]] = None
"""
Use same structure as method_permission_name. If set security converge
will replace all method permissions by the new ones
Expand All @@ -272,7 +272,7 @@ class ExampleApi(BaseApi):
"""
If using flask-wtf CSRFProtect exempt the API from check
"""
apispec_parameter_schemas = None
apispec_parameter_schemas: Optional[Dict[str, Dict]] = None
"""
Set your custom Rison parameter schemas here so that
they get registered on the OpenApi spec::
Expand Down Expand Up @@ -377,7 +377,7 @@ class ContactModelView(ModelRestApi):

The previous examples will only register the `put`, `post` and `delete` routes
"""
include_route_methods = None
include_route_methods: Set[str] = None
"""
If defined will assume a white list setup, where all endpoints are excluded
except those define on this attribute
Expand Down Expand Up @@ -412,7 +412,7 @@ class GreetingApi(BaseApi):
Use this attribute to override the tag name
"""

def __init__(self):
def __init__(self) -> None:
"""
Initialization of base permissions
based on exposed methods and actions
Expand Down Expand Up @@ -855,72 +855,83 @@ class ModelRestApi(BaseModelApi):
List Title, if not configured the default is
'List ' with pretty model name
"""
show_title = ""
show_title: Optional[str] = ""
"""
Show Title , if not configured the default is
'Show ' with pretty model name
"""
add_title = ""
add_title: Optional[str] = ""
"""
Add Title , if not configured the default is
'Add ' with pretty model name
"""
edit_title = ""
edit_title: Optional[str] = ""
"""
Edit Title , if not configured the default is
'Edit ' with pretty model name
"""

list_columns = None
list_select_columns: Optional[List[str]] = None
"""
A List of column names that will be included on the SQL select.
This is useful for including all necessary columns that are referenced
by properties listed on `list_columns` without generating N+1 queries.
"""
list_columns: Optional[List[str]] = None
"""
A list of columns (or model's methods) to be displayed on the list view.
Use it to control the order of the display
"""
show_columns = None
show_select_columns: Optional[List[str]] = None
"""
A List of column names that will be included on the SQL select.
This is useful for including all necessary columns that are referenced
by properties listed on `show_columns` without generating N+1 queries.
"""
show_columns: Optional[List[str]] = None
"""
A list of columns (or model's methods) for the get item endpoint.
Use it to control the order of the results
"""
add_columns = None
add_columns: Optional[List[str]] = None
"""
A list of columns (or model's methods) to be allowed to post
"""
edit_columns = None
edit_columns: Optional[List[str]] = None
"""
A list of columns (or model's methods) to be allowed to update
"""
list_exclude_columns = None
list_exclude_columns: Optional[List[str]] = None
"""
A list of columns to exclude from the get list endpoint.
By default all columns are included.
"""
show_exclude_columns = None
show_exclude_columns: Optional[List[str]] = None
"""
A list of columns to exclude from the get item endpoint.
By default all columns are included.
"""
add_exclude_columns = None
add_exclude_columns: Optional[List[str]] = None
"""
A list of columns to exclude from the add endpoint.
By default all columns are included.
"""
edit_exclude_columns = None
edit_exclude_columns: Optional[List[str]] = None
"""
A list of columns to exclude from the edit endpoint.
By default all columns are included.
"""
order_columns = None
order_columns: Optional[List[str]] = None
""" Allowed order columns """
page_size = 20
"""
Use this property to change default page size
"""
max_page_size = None
max_page_size: Optional[int] = None
"""
class override for the FAB_API_MAX_SIZE, use special -1 to allow for any page
size
"""
description_columns = None
description_columns: Optional[Dict[str, str]] = None
"""
Dictionary with column descriptions that will be shown on the forms::

Expand All @@ -930,8 +941,8 @@ class MyView(ModelView):
description_columns = {'name':'your models name column',
'address':'the address column'}
"""
validators_columns = None
""" Dictionary to add your own validators for forms """
validators_columns: Optional[Dict[str, Callable]] = None
""" Dictionary to add your own marshmallow validators """

add_query_rel_fields = None
"""
Expand Down Expand Up @@ -973,22 +984,22 @@ class ContactModelView(ModelRestApi):
'gender': ('name', 'asc')
}
"""
list_model_schema = None
list_model_schema: Optional[Schema] = None
"""
Override to provide your own marshmallow Schema
for JSON to SQLA dumps
"""
add_model_schema = None
add_model_schema: Optional[Schema] = None
"""
Override to provide your own marshmallow Schema
for JSON to SQLA dumps
"""
edit_model_schema = None
edit_model_schema: Optional[Schema] = None
"""
Override to provide your own marshmallow Schema
for JSON to SQLA dumps
"""
show_model_schema = None
show_model_schema: Optional[Schema] = None
"""
Override to provide your own marshmallow Schema
for JSON to SQLA dumps
Expand Down Expand Up @@ -1069,7 +1080,7 @@ def _init_titles(self):
self.show_title = "Show " + self._prettify_name(class_name)
self.title = self.list_title

def _init_properties(self):
def _init_properties(self) -> None:
"""
Init Properties
"""
Expand All @@ -1091,6 +1102,7 @@ def _init_properties(self):
for x in self.datamodel.get_user_columns_list()
if x not in self.list_exclude_columns
]
self.list_select_columns = self.list_select_columns or self.list_columns

self.order_columns = (
self.order_columns
Expand All @@ -1101,6 +1113,8 @@ def _init_properties(self):
self.show_columns = [
x for x in list_cols if x not in self.show_exclude_columns
]
self.show_select_columns = self.show_select_columns or self.show_columns

if not self.add_columns:
self.add_columns = [
x for x in list_cols if x not in self.add_exclude_columns
Expand Down Expand Up @@ -1302,7 +1316,7 @@ def get_headless(self, pk, **kwargs) -> Response:
:param kwargs: Query string parameter arguments
:return: HTTP Response
"""
item = self.datamodel.get(pk, self._base_filters)
item = self.datamodel.get(pk, self._base_filters, self.show_select_columns)
if not item:
return self.response_404()

Expand Down Expand Up @@ -1417,13 +1431,15 @@ def get_list_headless(self, **kwargs) -> Response:
# handle select columns
select_cols = _args.get(API_SELECT_COLUMNS_RIS_KEY, [])
_pruned_select_cols = [col for col in select_cols if col in self.list_columns]
# map decorated metadata
self.set_response_key_mappings(
_response,
self.get_list,
_args,
**{API_SELECT_COLUMNS_RIS_KEY: _pruned_select_cols},
)

# Create a response schema with the computed response columns,
# defined or requested
if _pruned_select_cols:
_list_model_schema = self.model2schemaconverter.convert(_pruned_select_cols)
else:
Expand All @@ -1441,14 +1457,13 @@ def get_list_headless(self, **kwargs) -> Response:
# handle pagination
page_index, page_size = self._handle_page_args(_args)
# Make the query
query_select_columns = _pruned_select_cols or self.list_columns
count, lst = self.datamodel.query(
joined_filters,
order_column,
order_direction,
page=page_index,
page_size=page_size,
select_columns=query_select_columns,
select_columns=self.list_select_columns,
)
pks = self.datamodel.get_keys(lst)
_response[API_RESULT_RES_KEY] = _list_model_schema.dump(lst, many=True)
Expand Down
6 changes: 6 additions & 0 deletions flask_appbuilder/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ class InvalidOrderByColumnFABException(FABException):
"""Invalid order by column"""

pass


class InterfaceQueryWithoutSession(FABException):
"""You need to setup a session on the interface to perform queries"""

pass
Loading