Skip to content

Commit

Permalink
Merge branch 'master' into fix-ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
dpgaspar authored Feb 4, 2020
2 parents 7700aee + 196497e commit 1c50615
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 47 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Flask-AppBuilder ChangeLog
==========================

Improvements and Bug fixes on 2.2.2
-----------------------------------

- Fix, [mvc] List page's pagination start with 1 (#1216)
- Fix, AttributeError in manager.py when a permission is null (#1217)
- Fix, [api] using default method name when unspecified in method_permission_name (#1235)
- New, [api] New, http 403 forbidden on default responses (#1237)
- New, [mvc] [api] exclude and include route methods (#1234)
- New, [mvc] [security] make userstatschartview optional (#1239)
- New, [mvc] Disable old API flag and tests (#1244)
- Fix, [mvc] jinja2 crashes with defined actions and removed action routes (#1245)

Improvements and Bug fixes on 2.2.1
-----------------------------------

Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__author__ = "Daniel Vaz Gaspar"
__version__ = "2.2.2rc1"
__version__ = "2.2.2"

from .actions import action # noqa: F401
from .api import ModelRestApi # noqa: F401
Expand Down
5 changes: 4 additions & 1 deletion flask_appbuilder/baseviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ def _register_urls(self):
):
continue
if attr_name in self.exclude_route_methods:
log.info(f"Not registering route for method {attr_name}")
log.info(
f"Not registering route for method "
"{self.__class__.__name__}.{attr_name}"
)
continue
attr = getattr(self, attr_name)
if hasattr(attr, "_urls"):
Expand Down
63 changes: 34 additions & 29 deletions flask_appbuilder/templates/appbuilder/general/lib.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,31 @@
{% macro render_action_links(actions, pk, modelview_name) %}
{% set actions = actions | get_actions_on_show(modelview_name) %}
{% for key in actions %}
{% set action = actions.get(key) %}

{% set url = url_for(modelview_name + '.action', name = action.name, pk = pk) %}
<a href="javascript:void(0)" class="btn btn-sm btn-primary"
onclick="var a = new AdminActions(); return a.execute_single('{{url}}','{{action.confirmation}}');">
<i class="fa {{action.icon}}"></i>
{{_(action.text)}}
</a>
{% set action = actions.get(key) %}
{% set endpoint = modelview_name + '.action' %}
{% set path = endpoint | safe_url_for(name = action.name, pk = pk) %}
{% if path %}
<a href="javascript:void(0)" class="btn btn-sm btn-primary"
onclick="var a = new AdminActions(); return a.execute_single('{{path}}','{{action.confirmation}}');">
<i class="fa {{action.icon}}"></i>
{{_(action.text)}}
</a>
{% endif %}
{% endfor %}
{% endmacro %}

{% macro action_form(actions, modelview_name) %}
{% if actions %}
{% set url = url_for(modelview_name + '.action_post') %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% set endpoint = modelview_name + '.action_post' %}
{% set path = endpoint | safe_url_for %}
{% if path %}
<form id="action_form" action="{{ path }}" method="POST" style="display: none">
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<input type="hidden" id="action" name="action" />
</form>
{% endif %}
<input type="hidden" id="action" name="action" />
</form>
{% endif %}
{% endmacro %}

Expand Down Expand Up @@ -169,7 +174,7 @@
</li>
{% else %}
<li>
<a href="{{ p | link_page(modelview_name) }}">{{ (p +1) }}</a>
<a href="{{ p | link_page(modelview_name) }}">{{ (p + 1) }}</a>
</li>
{% endif %}
{% endfor %}
Expand Down Expand Up @@ -279,21 +284,21 @@ <h4 class="panel-title">
{% endmacro %}

{% macro render_list_header(can_add, page, page_size, count, filters, actions, modelview_name) %}
{{ render_pagination(page, page_size, count, modelview_name) }}
{{ render_set_page_size(page, page_size, count, modelview_name) }}
{% if can_add %}
{% set endpoint = modelview_name + '.add' %}
{% set path = endpoint | safe_url_for %}
{% if path %}
{% set path = path | set_link_filters(filters) %}
{{ lnk_add(path) }}
{% endif %}
{{ render_pagination(page, page_size, count, modelview_name) }}
{{ render_set_page_size(page, page_size, count, modelview_name) }}
{% if can_add %}
{% set endpoint = modelview_name + '.add' %}
{% set path = endpoint | safe_url_for %}
{% if path %}
{% set path = path | set_link_filters(filters) %}
{{ lnk_add(path) }}
{% endif %}
{{ render_actions(actions, modelview_name) }}
{{ lnk_back() }}
<div class="pull-right">
<strong>{{ _('Record Count') }}:</strong> {{ count }}
</div>
{% endif %}
{{ render_actions(actions, modelview_name) }}
{{ lnk_back() }}
<div class="pull-right">
<strong>{{ _('Record Count') }}:</strong> {{ count }}
</div>
{% endmacro %}

{% macro btn_crud(can_show, can_edit, can_delete, pk, modelview_name, filters) %}
Expand Down
192 changes: 177 additions & 15 deletions flask_appbuilder/tests/test_mvc.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import datetime
import json
import logging
from typing import Set

from flask import redirect, request, session
from flask_appbuilder import SQLA
from flask import Flask, redirect, request, session
from flask_appbuilder import AppBuilder, SQLA
from flask_appbuilder.charts.views import (
ChartView,
DirectByChartView,
Expand All @@ -16,7 +17,8 @@
from flask_appbuilder.models.generic.interface import GenericInterface
from flask_appbuilder.models.group import aggregate_avg, aggregate_count, aggregate_sum
from flask_appbuilder.models.sqla.filters import FilterEqual, FilterStartsWith
from flask_appbuilder.views import CompactCRUDMixin, MasterDetailView
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.views import CompactCRUDMixin, MasterDetailView, ModelView
import jinja2

from .base import FABTestCase
Expand Down Expand Up @@ -61,9 +63,6 @@ def test_babel_empty_languages(self):
"""
MVC: Test babel empty languages
"""
from flask import Flask
from flask_appbuilder import AppBuilder

app = Flask(__name__)
app.config.from_object("flask_appbuilder.tests.config_api")
app.config["LANGUAGES"] = {}
Expand All @@ -82,9 +81,6 @@ def test_babel_languages(self):
"""
MVC: Test babel languages
"""
from flask import Flask
from flask_appbuilder import AppBuilder

app = Flask(__name__)
app.config.from_object("flask_appbuilder.tests.config_api")
app.config["LANGUAGES"] = {
Expand All @@ -106,13 +102,8 @@ def test_babel_languages(self):
self.assertEqual(rv.status_code, 302)


class FlaskTestCase(FABTestCase):
class BaseMVCTestCase(FABTestCase):
def setUp(self):
from flask import Flask
from flask_appbuilder import AppBuilder
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.views import ModelView

self.app = Flask(__name__)
self.app.jinja_env.undefined = jinja2.StrictUndefined
self.app.config.from_object("flask_appbuilder.tests.config_api")
Expand All @@ -121,6 +112,177 @@ def setUp(self):
self.db = SQLA(self.app)
self.appbuilder = AppBuilder(self.app, self.db.session)

@property
def registered_endpoints(self) -> Set:
return {item.endpoint for item in self.app.url_map.iter_rules()}

def get_registered_view_endpoints(self, view_name) -> Set:
return {
item.endpoint
for item in self.app.url_map.iter_rules()
if item.endpoint.split(".")[0] == view_name
}


class MVCSwitchRouteMethodsTestCase(BaseMVCTestCase):
"""
Specific to test ModelView's:
- include_route_methods
- exclude_route_methods
- disable_api_route_methods
"""

def setUp(self):
super().setUp()

class Model2IncludeView(ModelView):
datamodel = SQLAInterface(Model2)
include_route_methods = {"list", "show"}

self.appbuilder.add_view(Model2IncludeView, "Model2IncludeView")

class Model2ExcludeView(ModelView):
datamodel = SQLAInterface(Model2)
exclude_route_methods: Set = {
"api",
"api_read",
"api_get",
"api_create",
"api_update",
"api_delete",
"api_column_add",
"api_column_edit",
"api_readvalues",
}

self.appbuilder.add_view(Model2ExcludeView, "Model2ExcludeView")

class Model2IncludeExcludeView(ModelView):
datamodel = SQLAInterface(Model2)
include_route_methods: Set = {
"api",
"api_read",
"api_get",
"api_create",
"api_update",
"api_delete",
"api_column_add",
"api_column_edit",
"api_readvalues",
}
exclude_route_methods: Set = {
"api_create",
"api_update",
"api_delete",
"api_column_add",
"api_column_edit",
"api_readvalues",
}

self.appbuilder.add_view_no_menu(
Model2IncludeExcludeView, "Model2IncludeExcludeView"
)

class Model2DisableMVCApiView(ModelView):
datamodel = SQLAInterface(Model2)
disable_api_route_methods = True

self.appbuilder.add_view(Model2DisableMVCApiView, "Model2DisableMVCApiView")

def test_include_route_methods(self):
"""
MVC: Include route methods
"""
expected_endpoints = {"Model2IncludeView.list", "Model2IncludeView.show"}
self.assertEqual(
expected_endpoints, self.get_registered_view_endpoints("Model2IncludeView")
)
# Check that permissions do not exist
unexpected_permissions = [
("can_add", "Model2IncludeView"),
("can_edit", "Model2IncludeView"),
("can_delete", "Model2IncludeView"),
("can_download", "Model2IncludeView"),
]
for unexpected_permission in unexpected_permissions:
pvm = self.appbuilder.sm.find_permission_view_menu(*unexpected_permission)
self.assertIsNone(pvm)
# Login and list with admin, check that mutation links are not rendered
client = self.app.test_client()
self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
rv = client.get("/model2includeview/list/")
self.assertEqual(rv.status_code, 200)
data = rv.data.decode("utf-8")
self.assertNotIn("/model2includeview/add", data)
self.assertNotIn("/model2includeview/edit", data)
self.assertNotIn("/model2includeview/delete", data)

def test_exclude_route_methods(self):
"""
MVC: Exclude route methods
"""
expected_endpoints: Set = {
"Model2ExcludeView.list",
"Model2ExcludeView.show",
"Model2ExcludeView.edit",
"Model2ExcludeView.download",
"Model2ExcludeView.action",
"Model2ExcludeView.delete",
"Model2ExcludeView.add",
"Model2ExcludeView.action_post",
}
self.assertEqual(
expected_endpoints, self.get_registered_view_endpoints("Model2ExcludeView")
)

def test_include_exclude_route_methods(self):
"""
MVC: Include and Exclude route methods
"""

expected_endpoints: Set = {
"Model2IncludeExcludeView.api",
"Model2IncludeExcludeView.api_read",
"Model2IncludeExcludeView.api_get",
}
self.assertEqual(
expected_endpoints,
self.get_registered_view_endpoints("Model2IncludeExcludeView"),
)
# Check that permissions do not exist
unexpected_permissions = [
("can_add", "Model2IncludeExcludeView"),
("can_edit", "Model2IncludeExcludeView"),
("can_delete", "Model2IncludeExcludeView"),
("can_download", "Model2IncludeExcludeView"),
]
for unexpected_permission in unexpected_permissions:
pvm = self.appbuilder.sm.find_permission_view_menu(*unexpected_permission)
self.assertIsNone(pvm)

def test_disable_mvc_api_methods(self):
"""
MVC: Disable MVC API
"""
expected_endpoints: Set = {
"Model2DisableMVCApiView.list",
"Model2DisableMVCApiView.show",
"Model2DisableMVCApiView.add",
"Model2DisableMVCApiView.edit",
"Model2DisableMVCApiView.delete",
"Model2DisableMVCApiView.action",
"Model2DisableMVCApiView.download",
"Model2DisableMVCApiView.action_post",
}
self.assertEqual(
expected_endpoints,
self.get_registered_view_endpoints("Model2DisableMVCApiView"),
)


class MVCTestCase(BaseMVCTestCase):
def setUp(self):
super().setUp()
sess = PSSession()

class PSView(ModelView):
Expand Down
Loading

0 comments on commit 1c50615

Please sign in to comment.