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 1 commit
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
Prev Previous commit
Next Next commit
Integrates reactpy and implements app scaffold
  • Loading branch information
shawncrawley committed Aug 17, 2024
commit debe2a4d1dbe21cdd5c02f28b228b52af8092a5e
4 changes: 1 addition & 3 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 Expand Up @@ -65,7 +64,6 @@ dependencies:
- django-analytical # track usage analytics
- django-json-widget # enable json widget for app settings
- djangorestframework # enable REST API framework
- reactpy-django

# Map Layout
- PyShp
Expand Down
4 changes: 1 addition & 3 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 All @@ -36,4 +35,3 @@ dependencies:
- django-bootstrap5
- django-model-utils
- django-guardian
- reactpy-django
8 changes: 7 additions & 1 deletion tethys_apps/base/app_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

tethys_log = logging.getLogger("tethys.app_base")

DEFAULT_CONTROLLER_MODULES = ["controllers", "consumers", "handlers"]
DEFAULT_CONTROLLER_MODULES = ["controllers", "consumers", "handlers", "pages"]


class TethysBase(TethysBaseMixin):
Expand All @@ -51,6 +51,8 @@ class TethysBase(TethysBaseMixin):
root_url = ""
index = None
controller_modules = []
default_layout = None
custom_css = []

def __init__(self):
self._url_patterns = None
Expand All @@ -76,6 +78,10 @@ def id(cls):
"""Returns ID of Django database object."""
return cls.db_object.id

@classproperty
def layout(cls):
return cls.default_layout

@classmethod
def _resolve_ref_function(cls, ref, ref_type):
"""
Expand Down
205 changes: 205 additions & 0 deletions tethys_apps/base/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
from django.views.generic import View
from django.http import HttpRequest
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.conf import settings
from django.shortcuts import render

from tethys_components.library import Library as ComponentLibrary
from tethys_cli.cli_colors import write_warning
from tethys_quotas.decorators import enforce_quota
from tethys_services.utilities import ensure_oauth2
from . import url_map_maker
from .app_base import DEFAULT_CONTROLLER_MODULES


from .bokeh_handler import (
_get_bokeh_controller,
with_workspaces as with_workspaces_decorator,
Expand All @@ -37,6 +41,7 @@
from typing import Union, Any
from collections.abc import Callable

from reactpy import component

app_controllers_list = list()

Expand Down Expand Up @@ -398,6 +403,127 @@ def wrapped(function_or_class):

return wrapped if function_or_class is None else wrapped(function_or_class)

def page(
function_or_class: Union[
Callable[[HttpRequest, ...], Any], TethysController
] = None,
/,
*,
# UrlMap Overrides
name: str = None,
url: Union[str, list, tuple, dict, None] = None,
protocol: str = "http",
regex: Union[str, list, tuple] = None,
_handler: Union[str, Callable] = None,
_handler_type: str = None,
# login_required kwargs
login_required: bool = True,
redirect_field_name: str = REDIRECT_FIELD_NAME,
login_url: str = None,
# workspace decorators
app_workspace: bool = False,
user_workspace: bool = False,
# ensure_oauth2 kwarg
ensure_oauth2_provider: str = None,
# enforce_quota kwargs
enforce_quotas: Union[str, list, tuple, None] = None,
# permission_required kwargs
permissions_required: Union[str, list, tuple] = None,
permissions_use_or: bool = False,
permissions_message: str = None,
permissions_raise_exception: bool = False,
# additional kwargs to pass to TethysController.as_controller
layout="default",
title=None,
index=None,
custom_css=[],
custom_js=[]
) -> Callable:
"""
Decorator to register a function or TethysController class as a controller
(by automatically registering a UrlMap for it).

Args:
name: Name of the url map. Letters and underscores only (_). Must be unique within the app. The default is the name of the function being decorated.
url: URL pattern to map the endpoint for the controller or consumer. If a `list` then a separate UrlMap is generated for each URL in the list. The first URL is given `name` and subsequent URLS are named `name` _1, `name` _2 ... `name` _n. Can also be passed as dict mapping names to URL patterns. In this case the `name` argument is ignored.
protocol: 'http' for controllers or 'websocket' for consumers. Default is http.
regex: Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order.
login_required: If user is required to be logged in to access the controller. Default is `True`.
redirect_field_name: URL query string parameter for the redirect path. Default is "next".
login_url: URL to send users to in order to authenticate.
app_workspace: Whether to pass the app workspace as an argument to the controller.
user_workspace: Whether to pass the user workspace as an argument to the controller.
ensure_oauth2_provider: An OAuth2 provider name to ensure is authenticated to access the controller.
enforce_quotas: The name(s) of quotas to enforce on the controller.
permissions_required: The name(s) of permissions that a user is required to have to access the controller.
permissions_use_or: When multiple permissions are provided and this is True, use OR comparison rather than AND comparison, which is default.
permissions_message: Override default message that is displayed to user when permission is denied. Default message is "We're sorry, but you are not allowed to perform this operation.".
permissions_raise_exception: Raise 403 error if True. Defaults to False.
layout: Layout within which the page content will be wrapped
title: Title of page as used in both the built-in Navigation component and the browser tab
index: Index of the page as used to determine the display order in the built-in Navigation component. Defaults to top-to-bottom as written in code. Pass -1 to remove from built-in Navigation component.
custom_css: A list of URLs to additional css files that should be rendered with the page. These will be rendered in the order provided.
custom_js: A list of URLs to additional js files that should be rendered with the page. These will be rendered in the order provided.

**NOTE:** The :ref:`handler-decorator` should be used in favor of using the following arguments directly.

Args:
_handler: Dot-notation path a handler function. A handler is associated to a specific controller and contains the main logic for creating and establishing a communication between the client and the server.
_handler_type: Tethys supported handler type. 'bokeh' is the only handler type currently supported.
""" # noqa: E501

permissions_required = _listify(permissions_required)
enforce_quota_codenames = _listify(enforce_quotas)
layout = f'{layout.__module__}.{layout.__name__}' if callable(layout) else layout

def wrapped(function_or_class):
page_module_path = f'{function_or_class.__module__}.{function_or_class.__name__}'
url_map_kwargs_list = _get_url_map_kwargs_list(
function_or_class=function_or_class,
name=name,
url=url,
protocol=protocol,
regex=regex,
handler=_handler,
handler_type=_handler_type,
app_workspace=app_workspace,
user_workspace=user_workspace,
title=title,
index=index
)

def controller_wrapper(request):
controller = _global_page_component_controller
if permissions_required:
controller = permission_required(
*permissions_required,
use_or=permissions_use_or,
message=permissions_message,
raise_exception=permissions_raise_exception,
)(controller)

for codename in enforce_quota_codenames:
controller = enforce_quota(codename)(controller)

if ensure_oauth2_provider:
# this needs to come before login_required
controller = ensure_oauth2(ensure_oauth2_provider)(controller)

if login_required:
# this should be at the end, so it's the first to be evaluated
controller = login_required_decorator(
redirect_field_name=redirect_field_name, login_url=login_url
)(controller)

return controller(request, inspect.getsource(function_or_class), layout, page_module_path, url_map_kwargs_list[0]['title'], custom_css, custom_js)

# UNCOMMENT IF WE DECIDE TO GO WITH USING THE COMPONENT FUNCITON DIRECTLY, AS OPPOSED TO WRAPPING
# IT WITH THE GLOBAL_COMPONENT FUNCTION
# register_component(component_module_path)
_process_url_kwargs(controller_wrapper, url_map_kwargs_list)
return function_or_class

return wrapped if function_or_class is None else wrapped(function_or_class)

controller_decorator = controller

Expand Down Expand Up @@ -568,6 +694,20 @@ def wrapped(function):
return wrapped if function is None else wrapped(function)


def _global_page_component_controller(request, component_source_code, layout, page_module_path, title=None, custom_css=[], custom_js=[]):
ComponentLibrary.refresh(new_identifier=page_module_path.split('.')[-1].replace('_', '-'))
ComponentLibrary.load_dependencies_from_source_code(component_source_code)
context = {
'page_module_path_context_arg': page_module_path,
'reactjs_version': ComponentLibrary.REACTJS_VERSION,
'layout_context_arg': layout,
'title': title,
'custom_css': custom_css,
'custom_js': custom_js
}

return render(request, 'tethys_apps/reactpy_base.html', context)

def _get_url_map_kwargs_list(
function_or_class: Union[
Callable[[HttpRequest, ...], Any], TethysController
Expand All @@ -580,6 +720,8 @@ def _get_url_map_kwargs_list(
handler_type: str = None,
app_workspace=False,
user_workspace=False,
title=None,
index=None
):
final_urls = []
if url is not None:
Expand Down Expand Up @@ -636,6 +778,9 @@ def _get_url_map_kwargs_list(
f"{url_name}_{i}" if i else url_name: final_url
for i, final_url in enumerate(final_urls)
}

if not title:
title = url_name.replace('_', ' ').title()

return [
dict(
Expand All @@ -646,6 +791,8 @@ def _get_url_map_kwargs_list(
regex=regex,
handler=handler,
handler_type=handler_type,
title=title,
index=index
)
for url_name, final_url in final_urls.items()
]
Expand Down Expand Up @@ -772,3 +919,61 @@ def register_controllers(
)

return url_maps

@component
def page_component_wrapper(layout, page_module_path):
from reactpy_django.hooks import use_user # Avoid Django configuration error
path_parts = page_module_path.split('.')

app_name = path_parts[1]
app_module_name = f'tethysapp.{app_name}.app'
app_module = __import__(app_module_name, fromlist=['App'])
if hasattr(settings, "DEBUG") and settings.DEBUG:
importlib.reload(app_module)
App = app_module.App()

component_module_name = '.'.join(path_parts[:-1])
component_name = path_parts[-1]
component_module = __import__(component_module_name, fromlist=[component_name])
if hasattr(settings, "DEBUG") and settings.DEBUG:
importlib.reload(component_module)
Component = getattr(component_module, component_name)

if layout is not None:
Layout = None
if layout == 'default':
if callable(App.layout):
Layout = App.layout
else:
layout_module_name = 'tethys_components.layouts'
layout_name = App.layout
else:
layout_module_path_parts = layout.split('.')
layout_module_name = '.'.join(layout_module_path_parts[:-1])
layout_name = layout_module_path_parts[-1]

if not Layout:
layout_module = __import__(layout_module_name, fromlist=[layout_name])
Layout = getattr(layout_module, layout_name)

user = use_user()
nav_links = []
for url_map in sorted(App.registered_url_maps, key=lambda x: x.index if x.index is not None else 999):
if url_map.index == -1: continue # Do not render
nav_links.append(
{
'title': url_map.title,
'href': f'/apps/{App.root_url}/{url_map.name.replace('_', '-') + '/' if url_map.name != App.index else ""}'
}
)

return Layout(
{
'app': App,
'user': user,
'nav-links': nav_links
},
Component()
)
else:
return Component()
8 changes: 7 additions & 1 deletion tethys_apps/base/url_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(
regex=None,
handler=None,
handler_type=None,
title=None,
index=None
):
"""
Constructor
Expand All @@ -39,6 +41,8 @@ def __init__(
regex (str or iterable, optional): Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order.
handler (str): Dot-notation path a handler function. A handler is associated to a specific controller and contains the main logic for creating and establishing a communication between the client and the server.
handler_type (str): Tethys supported handler type. 'bokeh' is the only handler type currently supported.
title (str): The title to be used both in built-in Navigation components and in the browser tab
index (int): Used to determine the render order of nav items in built-in Navigation components. Defaults to the unpredictable processing order of the @page decorated functions. Set to -1 to remove from built-in Navigation components.
shawncrawley marked this conversation as resolved.
Show resolved Hide resolved
""" # noqa: E501
# Validate
if regex and (
Expand All @@ -57,14 +61,16 @@ def __init__(
self.custom_match_regex = regex
self.handler = handler
self.handler_type = handler_type
self.title = title
self.index = index

def __repr__(self):
"""
String representation
"""
return (
f"<UrlMap: name={self.name}, url={self.url}, controller={self.controller}, protocol={self.protocol}, "
f"handler={self.handler}, handler_type={self.handler_type}>"
f"handler={self.handler}, handler_type={self.handler_type}, title={self.title}, index={self.index}>"
shawncrawley marked this conversation as resolved.
Show resolved Hide resolved
)

@staticmethod
Expand Down
Loading