Skip to content

Commit

Permalink
Additional tweaks per feedback
Browse files Browse the repository at this point in the history
Adds tests for remaining tethys_component reactpy files
Adds reactpy-django to standard install
Fixes broken support for variable in url for pages
Adds react-loading-overlay and react-map-gl to built-in ComponentLibrary support
Fixes buggy use_workspace
  • Loading branch information
shawncrawley committed Oct 17, 2024
1 parent b6178b8 commit 9122f42
Show file tree
Hide file tree
Showing 16 changed files with 203 additions and 42 deletions.
4 changes: 4 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,7 @@ dependencies:
- factory_boy
- flake8
- flake8-bugbear

# reactpy dependencies
- pip:
- reactpy-django
3 changes: 1 addition & 2 deletions tests/coverage.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
[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
$TETHYS_TEST_DIR/../tethys_components
$TETHYS_TEST_DIR/../tethys_compute
$TETHYS_TEST_DIR/../tethys_config
$TETHYS_TEST_DIR/../tethys_gizmos
Expand Down
45 changes: 41 additions & 4 deletions tests/unit_tests/test_tethys_apps/test_base/test_page_handler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from unittest import TestCase, mock
from importlib import reload

Expand Down Expand Up @@ -61,6 +62,7 @@ def test_global_page_controller(
"title",
"custom_css",
"custom_js",
"extras"
],
)
self.assertEqual(render_context["app"], "app object")
Expand All @@ -80,9 +82,6 @@ def setUpClass(cls):
mock_has_module = mock.patch("tethys_portal.optional_dependencies.has_module")
mock_has_module.return_value = True
mock_has_module.start()
# mock.patch("builtins.__import__").start()
import sys

mock_reactpy = mock.MagicMock()
sys.modules["reactpy"] = mock_reactpy
mock_reactpy.component = lambda x: x
Expand All @@ -91,6 +90,7 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
mock.patch.stopall()
del sys.modules["reactpy"]
reload(page_handler)

def test_page_component_wrapper__layout_none(self):
Expand All @@ -105,11 +105,26 @@ def test_page_component_wrapper__layout_none(self):
return_value = page_handler.page_component_wrapper(app, user, layout, component)

self.assertEqual(return_value, component_return_val)

def test_page_component_wrapper__layout_none_with_extras(self):
# FUNCTION ARGS
app = mock.MagicMock()
user = mock.MagicMock()
layout = None
extras = {"extra1": "val1", "extra2": 2}
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, extras)

self.assertEqual(return_value, component_return_val)
component.assert_called_once_with(extra1="val1", extra2=2)

def test_page_component_wrapper__layout_not_none(self):
# FUNCTION ARGS
app = mock.MagicMock()
app.restered_url_maps = []
app.registered_url_maps = []
user = mock.MagicMock()
layout = mock.MagicMock()
layout_return_val = "returned_layout"
Expand All @@ -125,6 +140,28 @@ def test_page_component_wrapper__layout_not_none(self):
{"app": app, "user": user, "nav-links": app.navigation_links},
component_return_val,
)

def test_page_component_wrapper__layout_not_none_with_extras(self):
# FUNCTION ARGS
app = mock.MagicMock()
app.registered_url_maps = []
user = mock.MagicMock()
layout = mock.MagicMock()
layout_return_val = "returned_layout"
layout.return_value = layout_return_val
extras = {"extra1": "val1", "extra2": 2}
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, extras)

self.assertEqual(return_value, layout_return_val)
layout.assert_called_once_with(
{"app": app, "user": user, "nav-links": app.navigation_links},
component_return_val,
)
component.assert_called_once_with(extra1="val1", extra2=2)


class TestPage(TestCase):
Expand Down
84 changes: 84 additions & 0 deletions tests/unit_tests/test_tethys_components/test_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from tethys_components import custom
from tethys_components.library import Library as lib
from unittest import TestCase, mock, IsolatedAsyncioTestCase
from importlib import reload
import asyncio


class TestCustomComponents(IsolatedAsyncioTestCase):
@classmethod
def setUpClass(cls):
mock.patch('reactpy.component', new_callable=lambda: lambda x: x).start()
reload(custom)

@classmethod
def tearDownClass(cls):
mock.patch.stopall()
reload(custom)
lib.refresh()

def test_Panel_defaults(self):
test_component = custom.Panel({})
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('attributes', test_component)
self.assertIn('children', test_component)

async def test_Panel_all_props_provided(self):
test_set_show = mock.MagicMock()
props = {
"show": True,
"set-show": test_set_show,
"position": "right",
"extent": "30vw",
"name": "Test Panel 123"
}
test_component = custom.Panel(props)
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('attributes', test_component)
self.assertIn('children', test_component)
test_set_show.assert_not_called()
event_handler = test_component['children'][0]['children'][1]['eventHandlers']['on_click']
self.assertTrue(callable(event_handler.function))
await event_handler.function([None])
test_set_show.assert_called_once_with(False)

def test_HeaderButton(self):
test_component = custom.HeaderButton({})
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('attributes', test_component)

def test_NavIcon(self):
test_component = custom.NavIcon('test_src', 'test_color')
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('attributes', test_component)

def test_NavMenu(self):
test_component = custom.NavMenu({})
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('children', test_component)

def test_HeaderWithNavBar(self):
custom.lib.hooks = mock.MagicMock()
custom.lib.hooks.use_query().data.id = 10
test_app = mock.MagicMock(icon="icon.png", color="test_color")
test_user = mock.MagicMock()
test_nav_links = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock()]
test_component = custom.HeaderWithNavBar(test_app, test_user, test_nav_links)
self.assertIsInstance(test_component, dict)
self.assertIn('tagName', test_component)
self.assertIn('attributes', test_component)
self.assertIn('children', test_component)
del custom.lib.hooks

def test_get_db_object(self):
test_app = mock.MagicMock()
return_val = custom.get_db_object(test_app)
self.assertEqual(return_val, test_app.db_object)

def test_hooks(self):
custom.lib.hooks # should not fail
16 changes: 16 additions & 0 deletions tests/unit_tests/test_tethys_components/test_layouts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from tethys_components import layouts
from unittest import TestCase, mock
from reactpy.core.component import Component


class TestComponentLayouts(TestCase):

@mock.patch("tethys_components.layouts.HeaderWithNavBar", return_value={})
def test_NavHeader(self, _):
test_layout = layouts.NavHeader({
'app': mock.MagicMock(),
'user': mock.MagicMock(),
'nav-links': mock.MagicMock()
})
self.assertIsInstance(test_layout, Component)
self.assertIsInstance(test_layout.render(), dict)
2 changes: 1 addition & 1 deletion tests/unit_tests/test_tethys_components/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_standard_library_workflow(self):
)
self.assertIn("does_not_exist", lib.STYLE_DEPS)
self.assertListEqual(lib.STYLE_DEPS["does_not_exist"], ["my_style.css"])
self.assertListEqual(lib.DEFAULTS, ["rp", "does_not_exist"])
self.assertListEqual(lib.DEFAULTS, ["rp", "mapgl", "does_not_exist"])

# REGISTER AGAIN EXACTLY
lib.register(
Expand Down
17 changes: 9 additions & 8 deletions tests/unit_tests/test_tethys_components/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,21 @@ def test_get_workspace_for_user(self):
def test_use_workspace(self, mock_inspect):
mock_import = mock.patch("builtins.__import__").start()
try:
mock_inspect.stack().__getitem__().__getitem__().f_code.co_filename = str(
mock_stack_item_1 = mock.MagicMock()
mock_stack_item_1.__getitem__().f_code.co_filename = "throws_exception"
mock_stack_item_2 = mock.MagicMock()
mock_stack_item_2.__getitem__().f_code.co_filename = str(
TEST_APP_DIR
)
mock_inspect.stack.return_value = [mock_stack_item_1, mock_stack_item_2]
workspace = utils.use_workspace("john")
self.assertEqual(
mock_import.call_args_list[-1][0][0], "reactpy_django.hooks"
)
self.assertEqual(mock_import.call_args_list[-1][0][3][0], "use_query")
mock_import().use_query.assert_called_once_with(
utils.get_workspace,
{"app_package": "test_app", "user": "john"},
postprocessor=None,
)
self.assertEqual(workspace, mock_import().use_query().data)
self.assertEqual(mock_import.call_args_list[-1][0][3][0], "use_memo")
mock_import().use_memo.assert_called_once()
self.assertIn('<function use_workspace.<locals>.<lambda> at', str(mock_import().use_memo.call_args_list[0]))
self.assertEqual(workspace, mock_import().use_memo())
finally:
mock.patch.stopall()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import sys
import unittest
from unittest import mock

from django.test import override_settings

# Fixes the Cache-Control error in tests. Must appear before view imports.
mock.patch("django.views.decorators.cache.never_cache", lambda x: x).start()
if 'tethys_portal.views.accounts' in sys.modules:
del sys.modules['tethys_portal.views.accounts']

from tethys_portal.views.accounts import login_view, register, logout_view # noqa: E402

Expand Down
4 changes: 4 additions & 0 deletions tests/unit_tests/test_tethys_portal/test_views/test_psa.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import sys
from unittest import mock

from django.http import HttpResponseBadRequest
Expand All @@ -16,6 +17,9 @@
mock.patch("django.views.decorators.cache.never_cache", lambda x: x).start()
mock.patch("social_django.utils.psa", side_effect=mock_decorator).start()

if 'tethys_portal.views.psa' in sys.modules:
del sys.modules['tethys_portal.views.psa']

from tethys_portal.views.psa import tenant, auth, complete # noqa: E402


Expand Down
3 changes: 3 additions & 0 deletions tests/unit_tests/test_tethys_portal/test_views/test_user.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import sys
import unittest
from unittest import mock
from django.test import override_settings

# Fixes the Cache-Control error in tests. Must appear before view imports.
mock.patch("django.views.decorators.cache.never_cache", lambda x: x).start()
if 'tethys_portal.views.user' in sys.modules:
del sys.modules['tethys_portal.views.user']

from tethys_portal.views.user import ( # noqa: E402
profile,
Expand Down
3 changes: 2 additions & 1 deletion tethys_apps/base/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ def wrapped(component_function):
index=index,
)

def controller_wrapper(request):
def controller_wrapper(request, **kwargs):
controller = handler or global_page_controller
if permissions_required:
controller = permission_required(
Expand Down Expand Up @@ -541,6 +541,7 @@ def controller_wrapper(request):
title=url_map_kwargs_list[0]["title"],
custom_css=custom_css,
custom_js=custom_js,
**kwargs,
)

_process_url_kwargs(controller_wrapper, url_map_kwargs_list)
Expand Down
8 changes: 5 additions & 3 deletions tethys_apps/base/page_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def global_page_controller(
title=None,
custom_css=None,
custom_js=None,
**kwargs
):
app = get_active_app(request=request, get_class=True)
layout_func = get_layout_component(app, layout)
Expand All @@ -27,6 +28,7 @@ def global_page_controller(
"title": title,
"custom_css": custom_css or [],
"custom_js": custom_js or [],
"extras": kwargs,
}

return render(request, "tethys_apps/reactpy_base.html", context)
Expand All @@ -36,7 +38,7 @@ def global_page_controller(
from reactpy import component

@component
def page_component_wrapper(app, user, layout, component):
def page_component_wrapper(app, user, layout, component, extras=None):
"""
ReactPy Component that wraps every custom user page
Expand All @@ -52,7 +54,7 @@ def page_component_wrapper(app, user, layout, component):
if layout is not None:
return layout(
{"app": app, "user": user, "nav-links": app.navigation_links},
component(),
component(**extras) if extras else component(),
)
else:
return component()
return component(**extras) if extras else component()
2 changes: 1 addition & 1 deletion tethys_apps/templates/tethys_apps/reactpy_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
{% include "analytical_body_top.html" %}
{% endif %}

{% component "tethys_apps.base.page_handler.page_component_wrapper" app=app user=request.user layout=layout_func component=component_func %}
{% component "tethys_apps.base.page_handler.page_component_wrapper" app=app user=request.user layout=layout_func component=component_func extras=extras %}

{% if has_terms %}
{% include "terms.html" %}
Expand Down
7 changes: 3 additions & 4 deletions tethys_components/custom.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from reactpy import component
from reactpy_django.hooks import use_location, use_query
from tethys_portal.settings import STATIC_URL
from .utils import Props
from .library import Library as lib
Expand Down Expand Up @@ -89,7 +88,7 @@ def NavIcon(src, background_color):

@component
def NavMenu(props, *children):
nav_title = props.pop("nav-title")
nav_title = props.pop("nav-title", "Navigation")

return lib.html.div(
lib.bs.Offcanvas(
Expand All @@ -108,9 +107,9 @@ def get_db_object(app):

@component
def HeaderWithNavBar(app, user, nav_links):
app_db_query = use_query(get_db_object, {"app": app})
app_db_query = lib.hooks.use_query(get_db_object, {"app": app})
app_id = app_db_query.data.id if app_db_query.data else 999
location = use_location()
location = lib.hooks.use_location()

return lib.bs.Navbar(
Props(
Expand Down
Loading

0 comments on commit 9122f42

Please sign in to comment.