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

Implements Tethys Reactpy App Scaffold #1081

Merged
merged 45 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
67b2098
Reactpy configured at the baseline-level
shawncrawley May 13, 2024
b16676e
Merge branch 'main' into tethys-reactpy
ckrew May 14, 2024
a585699
Added RESelectInput react component to create a reach dropdown just l…
ckrew May 16, 2024
debe2a4
Integrates reactpy and implements app scaffold
shawncrawley Aug 17, 2024
0c9ecc4
Merge branch 'main' into tethys-reactpy
shawncrawley Aug 17, 2024
f6cc1a9
Handle reactpy-django at app install level
shawncrawley Aug 19, 2024
9324df4
Bugfixes from fresh test
shawncrawley Aug 19, 2024
521eef0
Initial wave of tests and resulting refactors/fixes
shawncrawley Aug 23, 2024
55e31f9
Adds tests and test-based fixes
shawncrawley Aug 29, 2024
645e4de
Merge branch 'main' into tethys-reactpy
shawncrawley Aug 29, 2024
80eea3d
Fix broken tests on Windows, flake8 cleanup
shawncrawley Aug 29, 2024
408b7ab
Applies black formatting
shawncrawley Aug 29, 2024
fdfb0a5
Try fixing async test
shawncrawley Aug 29, 2024
dc93f59
Fix flake8 warning
shawncrawley Aug 29, 2024
5628657
Tweak test for macos
shawncrawley Aug 29, 2024
33e0632
Another tweak for tests on macos
shawncrawley Aug 29, 2024
4640634
Fix broken test from last commit
shawncrawley Aug 29, 2024
af5cb81
black reformatting
shawncrawley Aug 29, 2024
3e59a05
Unpin daphne version
shawncrawley Sep 5, 2024
3c67bcc
Merge branch 'main' into tethys-reactpy
shawncrawley Sep 6, 2024
8d48e47
Bugfix: Default arg must be passed to scaffold_command
shawncrawley Sep 11, 2024
79e44ad
applies suggested changes
shawncrawley Oct 3, 2024
64732c5
Merge branch 'main' into tethys-reactpy
shawncrawley Oct 3, 2024
b5493d2
Revert spot where os.path had been converted to pathlib.Path
shawncrawley Oct 3, 2024
4c9697b
Remove erroneous argument
shawncrawley Oct 3, 2024
ae1d688
Fix bug with pathlib update to static_finders
shawncrawley Oct 7, 2024
46b4f83
Update tests/unit_tests/test_tethys_apps/test_template_loaders.py
shawncrawley Oct 11, 2024
1bdcdcf
Update tethys_cli/cli_helpers.py
shawncrawley Oct 11, 2024
f7af003
Merge branch 'main' into tethys-reactpy
shawncrawley Oct 11, 2024
b6178b8
Merge branch 'main' into tethys-reactpy
swainn Oct 16, 2024
9122f42
Additional tweaks per feedback
shawncrawley Oct 17, 2024
305a910
black and flake8
shawncrawley Oct 17, 2024
0718fd5
remove file that was unintentionally committed
shawncrawley Oct 17, 2024
d898438
Fixes pyproject.toml_tmpl for reacpty scaffold
shawncrawley Oct 19, 2024
f0e00e9
Update tethys_apps/base/url_map.py
shawncrawley Oct 22, 2024
01bdf94
Implements latest feedback from @swainn
shawncrawley Oct 22, 2024
8a810af
Additional tweaks per feedback/tests
shawncrawley Oct 23, 2024
8ab53d4
Fix broken test
shawncrawley Oct 23, 2024
f4bc8b6
Removes reactpy[-django] from dependencies
shawncrawley Nov 25, 2024
f5af044
Replace Path.walk with os.walk
shawncrawley Nov 25, 2024
183cd7f
Merge branch 'main' into tethys-reactpy
shawncrawley Nov 25, 2024
58bd925
Replaces odd Namespace usage with UrlMap
shawncrawley Nov 25, 2024
3ba6119
Merge branch 'tethys-reactpy' of https://github.com/tethysplatform/te…
shawncrawley Nov 25, 2024
7b8a45c
Applies black formatting
shawncrawley Nov 25, 2024
b3bb243
Separates channels and daphne
shawncrawley Nov 25, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/tethys.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ jobs:
run: |
cd ..
. ~/miniconda/etc/profile.d/conda.sh;
conda create -y -c conda-forge -n conda-build conda-build anaconda-client
conda create -y -c conda-forge -n conda-build conda-build anaconda-client
conda activate conda-build
conda config --set anaconda_upload no
mkdir -p ~/conda-bld
Expand Down Expand Up @@ -259,7 +259,7 @@ jobs:
run: |
cd ..
. ~/miniconda/etc/profile.d/conda.sh;
conda create -y -c conda-forge -n conda-build conda-build anaconda-client
conda create -y -c conda-forge -n conda-build conda-build anaconda-client
conda activate conda-build
conda config --set anaconda_upload no
mkdir -p ~/conda-bld
Expand Down
3 changes: 1 addition & 2 deletions environment.yml
shawncrawley marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ dependencies:

# core dependencies
- django>=3.2,<6
- channels
- daphne
- channels["daphne"]
shawncrawley marked this conversation as resolved.
Show resolved Hide resolved
- setuptools_scm
- pip
- requests # required by lots of things
Expand Down
3 changes: 1 addition & 2 deletions micro_environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ dependencies:

# core dependencies
- django>=3.2,<6
- channels
- daphne
- channels["daphne"]
- setuptools_scm
- pip
- requests # required by lots of things
Expand Down
2 changes: 1 addition & 1 deletion tests/apps/tethysapp-test_app/install-with-post.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ requirements:
- geojson
- shapely
post:
- ./test.sh
- ./test.py
1 change: 1 addition & 0 deletions tests/apps/tethysapp-test_app/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("test")
1 change: 0 additions & 1 deletion tests/apps/tethysapp-test_app/test.sh

This file was deleted.

2 changes: 2 additions & 0 deletions tests/coverage.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
[run]
source = $TETHYS_TEST_DIR/../tethys_apps
$TETHYS_TEST_DIR/../tethys_cli
$TETHYS_TEST_DIR/../tethys_components/library.py
$TETHYS_TEST_DIR/../tethys_components/utils.py
shawncrawley marked this conversation as resolved.
Show resolved Hide resolved
$TETHYS_TEST_DIR/../tethys_compute
$TETHYS_TEST_DIR/../tethys_config
$TETHYS_TEST_DIR/../tethys_gizmos
Expand Down
34 changes: 34 additions & 0 deletions tests/unit_tests/test_tethys_apps/test_base/test_app_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.utils import ProgrammingError
from django.test import RequestFactory, override_settings
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from argparse import Namespace

from tethys_apps.exceptions import (
TethysAppSettingDoesNotExist,
Expand Down Expand Up @@ -1543,3 +1544,36 @@ def test_remove_from_db_2(self, mock_ta, mock_log):

# Check tethys log error
mock_log.error.assert_called()

def test_navigation_links_not_auto(self):
app = tethys_app_base.TethysAppBase()
app.nav_links = ["test", "1", "2", "3"]
links = app.navigation_links
self.assertListEqual(links, app.nav_links)

def test_navigation_links_auto_excluded_page(self):
app = tethys_app_base.TethysAppBase()
app.nav_links = "auto"
app.index = "home"
app.root_url = "test-app"

app._registered_url_maps = [
Namespace(name="exclude_page", title="Exclude Page", index=-1),
swainn marked this conversation as resolved.
Show resolved Hide resolved
Namespace(name="last_page", title="Last Page", index=3),
Namespace(name="third_page", title="Third Page", index=2),
Namespace(name="second_page", title="Second Page", index=1),
Namespace(name="home", title="Home", index=0),
]

links = app.navigation_links

self.assertListEqual(
links,
[
{"title": "Home", "href": "/apps/test-app/"},
{"title": "Second Page", "href": "/apps/test-app/second-page/"},
{"title": "Third Page", "href": "/apps/test-app/third-page/"},
{"title": "Last Page", "href": "/apps/test-app/last-page/"},
],
)
self.assertEqual(links, app.nav_links)
257 changes: 257 additions & 0 deletions tests/unit_tests/test_tethys_apps/test_base/test_page_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
from unittest import TestCase, mock
from importlib import reload

from tethys_apps.base import page_handler
import tethys_apps.base.controller as tethys_controller


class TestPageHandler(TestCase):
@mock.patch("tethys_apps.base.page_handler.render")
@mock.patch("tethys_apps.base.page_handler.ComponentLibrary")
@mock.patch("tethys_apps.base.page_handler.get_active_app")
@mock.patch("tethys_apps.base.page_handler.get_layout_component")
def test_global_page_controller(
self, mock_get_layout, mock_get_app, mock_lib, mock_render
):
# FUNCTION ARGS
request = mock.MagicMock()
layout = "test_layout"
component_func = mock.MagicMock()
component_source_code = "test123"
title = "test_title"
custom_css = ["custom.css"]
custom_js = ["custom.js"]

# MOCK INTERNALS
mock_get_app.return_value = "app object"
component_func.__name__ = "my_mock_component_func"
expected_return_value = "Expected return value"
mock_render.return_value = expected_return_value
mock_get_layout.return_value = "my_layout_func"

# EXECUTE FUNCTION
response = page_handler.global_page_controller(
request=request,
layout=layout,
component_func=component_func,
component_source_code=component_source_code,
title=title,
custom_css=custom_css,
custom_js=custom_js,
)

# EVALUATE EXECUTION
mock_get_app.assert_called_once_with(request=request, get_class=True)
mock_get_layout.assert_called_once_with(mock_get_app(), layout)
mock_lib.refresh.assert_called_with(new_identifier="my-mock-component-func")
mock_lib.load_dependencies_from_source_code.assert_called_with(
component_source_code
)
render_called_with_args = mock_render.call_args.args
self.assertEqual(render_called_with_args[0], request)
self.assertEqual(render_called_with_args[1], "tethys_apps/reactpy_base.html")
render_context = render_called_with_args[2]
self.assertListEqual(
list(render_context.keys()),
[
"app",
"layout_func",
"component_func",
"reactjs_version",
"title",
"custom_css",
"custom_js",
],
)
self.assertEqual(render_context["app"], "app object")
self.assertEqual(render_context["layout_func"](), "my_layout_func")
self.assertEqual(render_context["component_func"](), component_func)
self.assertEqual(render_context["reactjs_version"], mock_lib.REACTJS_VERSION)
self.assertEqual(render_context["title"], title)
self.assertEqual(render_context["custom_css"], custom_css)
self.assertEqual(render_context["custom_js"], custom_js)
self.assertEqual(response, expected_return_value)

def test_has_reactpy(self):
mock_has_module = mock.patch("tethys_portal.optional_dependencies.has_module")
mock_has_module.return_value = True
mock_has_module.start()
try:
import reactpy

reload(page_handler)
self.assertEqual(page_handler.component, reactpy.component)
except ModuleNotFoundError as e:
self.assertRaises(ModuleNotFoundError, reload, page_handler)
self.assertTrue("reactpy" in str(e))

mock.patch.stopall()
reload(page_handler)

def test_page_component_wrapper__layout_none(self):
# FUNCTION ARGS
app = mock.MagicMock()
user = mock.MagicMock()
layout = None
component = mock.MagicMock()
component_return_val = "rendered_component"
component.return_value = component_return_val

return_value = page_handler.page_component_wrapper(app, user, layout, component)

self.assertEqual(return_value, component_return_val)

def test_page_component_wrapper__layout_not_none(self):
# FUNCTION ARGS
app = mock.MagicMock()
app.restered_url_maps = []
user = mock.MagicMock()
layout = mock.MagicMock()
layout_return_val = "returned_layout"
layout.return_value = layout_return_val
component = mock.MagicMock()
component_return_val = "rendered_component"
component.return_value = component_return_val

return_value = page_handler.page_component_wrapper(app, user, layout, component)

self.assertEqual(return_value, layout_return_val)
layout.assert_called_once_with(
{"app": app, "user": user, "nav-links": app.navigation_links},
component_return_val,
)

@mock.patch("tethys_apps.base.controller._process_url_kwargs")
@mock.patch("tethys_apps.base.controller.global_page_controller")
@mock.patch("tethys_apps.base.controller.permission_required")
@mock.patch("tethys_apps.base.controller.enforce_quota")
@mock.patch("tethys_apps.base.controller.ensure_oauth2")
@mock.patch("tethys_apps.base.controller.login_required_decorator")
@mock.patch("tethys_apps.base.controller._get_url_map_kwargs_list")
def test_page_with_permissions(
self,
mock_get_url_map_kwargs_list,
mock_login_required_decorator,
mock_ensure_oauth2,
mock_enforce_quota,
mock_permission_required,
mock_global_page_component,
mock_process_kwargs,
):
layout = "MyLayout"
title = "My Cool Page"
index = 0
custom_css = ["custom.css"]
custom_js = ["custom.js"]
my_function = lambda x: x # noqa E731
return_value = tethys_controller.page(
permissions_required=["test_permission"],
enforce_quotas=["test_quota"],
ensure_oauth2_provider=["test_oauth2_provider"],
layout=layout,
title=title,
index=index,
custom_css=custom_css,
custom_js=custom_js,
)(my_function)
self.assertTrue(callable(return_value))
mock_request = mock.MagicMock()
mock_process_kwargs.assert_called_once()
process_kwargs_args = mock_process_kwargs.call_args.args
self.assertTrue(callable(process_kwargs_args[0]))
self.assertEqual(
process_kwargs_args[0](mock_request), mock_login_required_decorator()()()
)
mock_permission_required.assert_called_once()
mock_enforce_quota.assert_called_once()
mock_ensure_oauth2.assert_called_once()
self.assertEqual(mock_login_required_decorator.call_count, 2)
mock_get_url_map_kwargs_list.assert_called_once_with(
function_or_class=my_function,
name=None,
url=None,
protocol="http",
regex=None,
title=title,
index=index,
)

@mock.patch("tethys_apps.base.controller._process_url_kwargs")
@mock.patch("tethys_apps.base.controller.global_page_controller")
@mock.patch("tethys_apps.base.controller._get_url_map_kwargs_list")
def test_page_with_defaults(
self,
mock_get_url_map_kwargs_list,
mock_global_page_component,
mock_process_kwargs,
):
my_function = lambda x: x # noqa: E731
return_value = tethys_controller.page()(my_function)
self.assertTrue(callable(return_value))
mock_request = mock.MagicMock()
mock_process_kwargs.assert_called_once()
process_kwargs_args = mock_process_kwargs.call_args.args
self.assertTrue(callable(process_kwargs_args[0]))
self.assertEqual(
process_kwargs_args[0](mock_request),
mock_global_page_component(
mock_request,
layout="default",
component_func=my_function,
component_source_code="lambda x: x",
title=mock_get_url_map_kwargs_list[0]["title"],
custom_css=[],
custom_js=[],
),
)
mock_get_url_map_kwargs_list.assert_called_once_with(
function_or_class=my_function,
name=None,
url=None,
protocol="http",
regex=None,
title=None,
index=None,
)

@mock.patch("tethys_apps.base.controller._process_url_kwargs")
@mock.patch("tethys_apps.base.controller.global_page_controller")
@mock.patch("tethys_apps.base.controller._get_url_map_kwargs_list")
def test_page_with_handler(
self,
mock_get_url_map_kwargs_list,
mock_global_page_component,
mock_process_kwargs,
):
component_function = lambda x: x # noqa: E731
handler_function = mock.MagicMock()
return_value = tethys_controller.page(handler=handler_function)(
component_function
)
self.assertTrue(callable(return_value))
mock_request = mock.MagicMock()
mock_process_kwargs.assert_called_once()
process_kwargs_args = mock_process_kwargs.call_args.args
self.assertTrue(callable(process_kwargs_args[0]))
mock_global_page_component.assert_not_called()
self.assertEqual(
process_kwargs_args[0](mock_request),
handler_function(
mock_request,
layout="default",
component_func=component_function,
component_source_code="lambda x: x",
title=mock_get_url_map_kwargs_list[0]["title"],
custom_css=[],
custom_js=[],
),
)
mock_get_url_map_kwargs_list.assert_called_once_with(
function_or_class=component_function,
name=None,
url=None,
protocol="http",
regex=None,
title=None,
index=None,
)
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_UrlMap_repr(self):
"<UrlMap: name=foo_name,"
" url=^example/resource/(?P<variable_name>[0-9A-Za-z-_.]+)/$,"
" controller=foo_app.controllers.foo, protocol=http,"
" handler=None, handler_type=None>"
" handler=None, handler_type=None, title=None, index=None>"
)
result = self.bound_UrlMap(
name=self.name, url=url, controller=self.controller
Expand Down
Loading