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

doc: frontend doc rework #30

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 15 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from pathlib import Path
import sys
from urllib.parse import urlparse

from multiproject.utils import get_project

Expand Down Expand Up @@ -59,8 +60,11 @@
# This is used for linking and such so we link to the thing we're building
is_on_rtd = os.environ.get("READTHEDOCS", None) == "True"
rtd_version = os.environ.get("READTHEDOCS_VERSION", "latest")
if rtd_version not in ["stable", "latest"]:
rtd_version = "stable"
rtd_url = os.environ.get(
"READTHEDOCS_CANONICAL_URL", f"https://alliance-platform.readthedocs.io/en/{rtd_version}/"
)
parts = urlparse(rtd_url)
base_url = f"{parts.scheme}://{parts.hostname}"

dev_port_map = {
"core": 56675,
Expand All @@ -72,8 +76,8 @@
def get_project_mapping(project_name: str):
if is_on_rtd:
if project_name == "core":
return (f"https://alliance-platform.readthedocs.io/en/{rtd_version}/", None)
return (f"https://alliance-platform.readthedocs.io/projects/{project_name}/{rtd_version}/", None)
return (f"{base_url}/en/{rtd_version}/", None)
return (f"{base_url}/projects/{project_name}/{rtd_version}/", None)
port = dev_port_map[project_name]
# In dev load from the local dev server started by pdm build-docs-watch. Load the objects.inv from the filesystem;
# this only works after the first build. We can't load from the dev server because it's not running yet (sphinx
Expand All @@ -89,7 +93,7 @@ def get_project_mapping(project_name: str):
"https://docs.djangoproject.com/en/stable/",
("https://docs.djangoproject.com/en/stable/_objects/"),
),
"python": ("https://docs.python.org/3", "https://docs.python.org/3/objects.inv"),
"python": ("https://docs.python.org/3", None),
}

# Sphinx defaults to automatically resolve *unresolved* labels using all your Intersphinx mappings.
Expand Down Expand Up @@ -129,6 +133,12 @@ def setup(app):
rolename="tfilter",
indextemplate="pair: %s; template filter",
)
# Allows usage of setting role, e.g. :setting:`FORM_RENDERER <django:FORM_RENDERER>`
app.add_crossref_type(
directivename="setting",
rolename="setting",
indextemplate="pair: %s; setting",
)


generate_sidebar(current_dir, globals())
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from allianceutils.auth.permission import reverse_if_probably_allowed
from django import template
from django.db.models import Model
from django.http import HttpRequest
from django.template import Context
from django.urls import reverse

Expand Down Expand Up @@ -77,38 +78,6 @@ def url_with_perm_filter(value, arg1=None):
"""Resolve a URL and check if the current user has permission to access it.

If permission check fails, the component that uses the value will be omitted from rendering.

Usage::

{% component "a" href="my_url_name"|url_with_perm:2 %}

The above example will resolve the URL "my_url_name" with the argument ``2``.

If you need multiple arguments you can use the ``with_arg`` filter::


{% component "a" href="my_url_name"|url_with_perm:2|with_arg:"another arg" %}

To pass kwargs you can use the ``with_kwargs`` filter::

{% component "a" href="my_url_name"|url_with_perm|with_kwargs:my_kwargs %}

Note that as there's no way to define a dictionary in the standard Django template language, you'll need to
pass it in context.

To do object level permission checks use the ``with_perm_obj`` filter to pass through the object::

{% component "a" href="my_url_name"|url_with_perm:obj.pk|with_obj:obj %}

Note that you still have to pass through the pk to resolve the URL with. Passing the object just allows the permission
checks to work without having to manually look up the object. Due to how the ``DetailView.get_object`` method works,
if you are not going to pass the object you must use ``with_kwargs`` to pass through the ID rather than a positional
argument::

{% component "a" href="my_url_name"|url_with_perm|with_kwargs:kwargs %}

Note that the above would do a query to retrieve the object, so it's better to pass the object if you have already
retrieved it.
"""
pfv = NamedUrlDeferredProp(value, True)
if arg1 is not None:
Expand Down Expand Up @@ -159,3 +128,29 @@ def unwrap_list(value):
raise ValueError(f"Expected list of length 1, received length {len(value)}: {value}")
return value[0]
return value


@register.filter
def table_sort_order(request: HttpRequest, ordering_param: str = "ordering"):
"""Extract the current sort ordering from the request GET parameters and return it as a list of dicts

Each ordering entry is a dict with keys "column" and "direction".

For example, the URL /foo?ordering=-bar,email would return::

[
{'column': 'email', 'direction': 'ascending'},
{'column': 'name', 'direction': 'descending'}
]
"""
query = request.GET.copy()
current_sorting_str = query.get(ordering_param)
if current_sorting_str:
return [
{
"column": x[1:] if x.startswith("-") else x,
"direction": "descending" if x.startswith("-") else "ascending",
}
for x in current_sorting_str.split(",")
]
return []
Original file line number Diff line number Diff line change
Expand Up @@ -147,82 +147,6 @@ def bundler_embed(parser: template.base.Parser, token: template.base.Token):
By default, the tags are added to the HTML by the :meth:`~alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets`.
This allows assets to be embedded as needed in templates but all added in one place in the HTML (most likely the ``<head>``).
You can force the tags to be outputted inline with ``inline=True``.

NOTE: This tag only accepts static values as :class:`extract_frontend_assets <alliance_platform.frontend.management.commands.extract_frontend_assets.Command>`
needs to be able to statically generate a list of paths that need to be built for production.

Args:
content_type: (optional) If set to either 'css' or 'js' only assets of the matching type will be embedded. If omitted
both types will be included (if available).
inline: If ``True`` the tags will be embedded inline, otherwise they will be added using the
:meth:`~alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets` tag. Defaults to ``False``.
html_*: any parameter with a ``html_`` prefix will have the ``html_`` stripped and passed through to the embed tag
eg ``html_id="foo"`` would render ``<script id="foo" ...>`` for script tags

Usage with ``bundler_embed_collected_assets``:

.. code-block:: html

// in the base template (e.g. base.html)
<!doctype html>
{% load bundler %}
<html lang="en-AU">
<head>
{% bundler_embed_collected_assets %}
</head>
<body>{% block body %}{% endblock %}</body>
</html>

// in other individual templates, e.g. 'myview.html'

{% extends "base.html" %}
{% block body %}
{% bundler_embed "MyComponent.ts" %}
{% bundler_embed "logo.png" html_alt="My Component Logo" %}
<h1>My View</h1>
{% endblock %}

would output:

.. code-block:: html

<!doctype html>
<html lang="en-AU">
<head>
<script type="module" src="http://localhost:5173/assets/MyComponent.js"></script>
<link rel="stylesheet" href="http://localhost:5173/assets/MyComponent.css" />
</head>
<body>
<img src="http://localhost:5173/assets/logo.png" alt="My Component Logo" />
<h1>My View</h1>
</body>
</html>

Using ``inline=True`` instead:

.. code-block:: html

{% extends "base.html" %}
{% block body %}
{% bundler_embed "MyComponent.ts" inline=True %}
<h1>My View</h1>
{% endblock %}

would output:

.. code-block:: html

<!doctype html>
<html lang="en-AU">
<head></head>
<body>
<script type="module" src="http://localhost:5173/assets/MyComponent.js"></script>
<link rel="stylesheet" href="http://localhost:5173/assets/MyComponent.css" />
<h1>My View</h1>
</body>
</html>

Note that in the example above ``logo.png`` is always embedded inline as it is not a javascript or css file.
"""
tag_name = token.split_contents()[0]
args, kwargs, target_var = parse_tag_arguments(parser, token, supports_as=True)
Expand Down
133 changes: 1 addition & 132 deletions packages/ap-frontend/alliance_platform/frontend/templatetags/react.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,138 +95,7 @@ def resolve_prop(value: Any, node: ComponentNode, context: Context) -> Component
def component(parser: template.base.Parser, token: template.base.Token):
"""Render a React component with the specified props

There are three ways to specify which component to render. The first is for a `"common component" <https://react.dev/reference/react-dom/components/common>`_
which is to say a built-in browser component (e.g. ``div``)::

{% component "h2" %}Heading{% endcomponent %}

The other two are for using a component defined in an external file. These will be loaded via
the specified bundler class (currently :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`). With
a single argument it specifies that the default export from the file is the component to use::

{% component "components/Button" %}Click Me{% endcomponent %}

With two arguments the first is the file path and the second is the named export from that file::

{% component "components/Table" "Column" %}Name{% endcomponent %}

The last option has a variation for using a property of the export. This is useful for components
where related components are added as properties, e.g. ``Table`` and ``Table.Column``::

{% component "components" "Table.Column" %}Name{% endcomponent %}

Note that this is only available when using named exports; default exports don't support it due to
ambiguity around whether the ``.`` indicates file extension or property access.

You can omit the file extension - the above could resolve to ``components/Table.tsx`` (``.js`` and ``.ts`` are also
supported). See :ref:`resolving_paths` for details on how the file path is resolved.

Props are specified as keyword arguments to the tag::

{% component "components/Button" variant="primary" %}Click Me{% endcomponent %}

Additionally, a dict of props can be passed under the ``props`` kwarg::

{% component "components/Button" variant="primary" props=button_props %}Click Me{% endcomponent %}

Children can be passed between the opening ``{% component %}`` and closing ``{% endcomponent %}``. Whitespace
is handled the same as in JSX:

* Whitespace at the beginning and ending of lines is removed
* Blank lines are removed
* New lines adjacent to other components are removed
* New lines in the middle of a string literal is replaced with a single space

So the following are all equivalent::

{% component "div" %}Hello World{% endcomponent %}

{% component %}
Hello world
{% endcomponent %}


{% component %}
Hello
world
{% endcomponent %}


{% component %}

Hello world
{% endcomponent %}

Components can be nested::

{% component "components/Button" type="primary" %}
{% components "icons" "Menu" %}{% endcomponent %}
Open Menu
{% end_component %}

You can use ``as <variable name>`` to store in a variable in context that can then be passed to another tag::

{% component "icons" "Menu" as icon %}{% end_component %}
{% component "components/Button" type="primary" icon=icon %}Open Menu{% end_component %}

All props must be JSON serializable. :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` can be used to define
how to serialize data, with a matching implementation in ``propTransformers.tsx`` to de-serialize it.

For example :class:`~alliance_platform.frontend.prop_handlers.DateProp` handles serializing a python ``datetime`` and
un-serializing it as a native JS ``Date`` on the frontend. See :class:`~alliance_platform.frontend.prop_handlers.ComponentProp`
for documentation about adding your own complex props.

Components are rendered using the ``renderComponent`` function in ``ap_frontend_settings.REACT_RENDER_COMPONENT_FILE``. This can be modified as needed,
for example if a new provider is required.

.. note:: All props passed through are converted to camel case automatically (i.e. ``my_prop`` will become ``myProp``)

Server Side Rendering (SSR)
---------------------------

Components will automatically be rendered on the server. See :ref:`ssr` for details about how this works.

To opt out of SSR pass ``ssr:disabled=True`` to the component after the component name::

{% component 'components/Button.tsx' ssr:disabled=True %}...{% endcomponent %}

Options
-------

Various options can be passed to the component tag. To differentiate from actual props to the component they are
prefixed with `ssr:` for server side rendering options, `component:` for general component options, or `container:`
for options relating to the container the component is rendered into.

- ``ssr:disabled=True`` - if specified, no server side rendering will occur for this component
- ``component:omit_if_empty=True`` - if specified, the component will not be rendered if it has no children. This is
useful for when components may not be rendered based on permission checks
- ``container:tag`` - the HTML tag to use for the container. Defaults to the custom element ``dj-component``.
- ``container:<any other prop>`` - any other props will be passed to the container element. For example, to add
an id to the container you can use ``container:id="my-id"``. Note that while you can pass a style string, it's
likely to be of little use with the default container style ``display: contents``. Most of the time you can just
do the styling on the component itself.

For example::

{% component 'core-ui' 'Button' ssr:disabled=True variant="Outlined"%}
...
{% endcomponent %}

Limitations
-----------

Currently, attempting to render a django form widget that is itself a React component within another component will
not work. This is due to how django widgets have their own templates that are rendered in an isolated context. For
example, this will not work if ``form.field`` also uses the ``{% component %}`` tag:

{% component 'MyComponent' %}
{{ form.field }}
{% endcomponent %}

Alliance UI
-----------
Alliance Core UI components have pre-made tags for more convenient implementation.

See the templatetags.rst docs for more details.
"""
return parse_component_tag(parser, token)

Expand Down
11 changes: 10 additions & 1 deletion packages/ap-frontend/alliance_platform/frontend/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

def get_node_ver(node_path: str) -> Version | None:
"""
Get the node verson from a given path
Get the node version from a given path

if raise_errors is not True then exceptions will be swallowed and None will be returned on error

Args:
node_path: The path to the node executable
"""
try:
proc = subprocess.Popen(
Expand All @@ -36,6 +39,12 @@ def get_node_ver(node_path: str) -> Version | None:


def guess_node_path(nvmrc_path: Path) -> str | None:
"""Attempt to guess the node path based on the .nvmrc file

Args:
nvmrc_path: Path to the .nvmrc file
"""

# nvm's version strings aren't quite the same as those
# used by python but the overlap makes it good enough

Expand Down
Loading