diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f29365..577f67f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Using the following categories, list your changes in this order: ### Changed - Bumped the minimum ReactPy version to `1.0.2`. -- Prettier websocket URLs for components that do not have sessions. +- Prettier WebSocket URLs for components that do not have sessions. - Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled. - Bumped the minimum `@reactpy/client` version to `0.3.1` - Bumped the minimum Django version to `4.2`. @@ -79,7 +79,7 @@ Using the following categories, list your changes in this order: ### Changed -- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your websockets. +- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your WebSockets. - Cleaner logging output for auto-detected ReactPy root components. ### Deprecated @@ -91,14 +91,14 @@ Using the following categories, list your changes in this order: - Warning W007 (`REACTPY_WEBSOCKET_URL doesn't end with a slash`) has been removed. ReactPy now automatically handles slashes. - Warning W008 (`REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character`) has been removed. ReactPy now automatically handles this scenario. -- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI webserver. +- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI web server. ## [3.3.2] - 2023-08-13 ### Added -- ReactPy Websocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. -- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your webserver with `uvloop`. +- ReactPy WebSocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. +- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your web server with `uvloop`. ### Fixed diff --git a/README.md b/README.md index 8e05e247..8aebcd8b 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,6 @@ def hello_world(recipient: str): In your **Django app**'s HTML template, you can now embed your ReactPy component using the `component` template tag. Within this tag, you will need to type in the dotted path to the component. - - Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` (_from the previous example_) accepts a `recipient` argument. diff --git a/docs/includes/orm.md b/docs/includes/orm.md index 22151a74..fafb6226 100644 --- a/docs/includes/orm.md +++ b/docs/includes/orm.md @@ -1,13 +1,13 @@ -Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception. +Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `#!python SynchronousOnlyOperation` exception. -These `SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. +These `#!python SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. -By default, automatic recursive fetching of `ManyToMany` or `ForeignKey` fields is enabled within the default `QueryOptions.postprocessor`. This is needed to prevent `SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. +By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the default `#!python QueryOptions.postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/home-code-examples/add-interactivity-demo.html new file mode 100644 index 00000000..d9e99579 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity-demo.html @@ -0,0 +1,165 @@ +
+
+ +
+
+ + + + example.com/videos.html +
+
+ +
+
+

Searchable Videos

+

Type a search query below.

+ +
+ +

5 Videos

+ +
+
+ + + +
+
+

ReactPy: The Documentary

+

From web library to taco delivery service

+
+ +
+ +
+
+ + + +
+
+

Code using Worst Practices

+

Harriet Potter (2013)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Foriegn

+

Tim Cooker (2015)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Cooks

+

Soap Boat and Dinosaur Dan (2018)

+
+ +
+ +
+
+ + + +
+
+

Introducing Quantum Components

+

Isaac Asimov and Lauren-kun (2020)

+
+ +
+
+ + +
+
diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py new file mode 100644 index 00000000..90976446 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity.py @@ -0,0 +1,30 @@ +from reactpy import component, html, use_state + + +def filter_videos(videos, search_text): + return None + + +def search_input(dictionary, value): + return None + + +def video_list(videos, empty_heading): + return None + + +@component +def searchable_video_list(videos): + search_text, set_search_text = use_state("") + found_videos = filter_videos(videos, search_text) + + return html._( + search_input( + {"on_change": lambda new_text: set_search_text(new_text)}, + value=search_text, + ), + video_list( + videos=found_videos, + empty_heading=f"No matches for “{search_text}”", + ), + ) diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/home-code-examples/code-block.html new file mode 100644 index 00000000..c1f14e5d --- /dev/null +++ b/docs/overrides/home-code-examples/code-block.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/home-code-examples/create-user-interfaces-demo.html new file mode 100644 index 00000000..9a684d3c --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces-demo.html @@ -0,0 +1,24 @@ +
+
+
+
+ + + +
+
+

My video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py new file mode 100644 index 00000000..37776ab1 --- /dev/null +++ b/docs/overrides/home-code-examples/create-user-interfaces.py @@ -0,0 +1,22 @@ +from reactpy import component, html + + +def thumbnail(video): + return None + + +def like_button(video): + return None + + +@component +def video(video): + return html.div( + thumbnail(video), + html.a( + {"href": video.url}, + html.h3(video.title), + html.p(video.description), + ), + like_button(video), + ) diff --git a/docs/overrides/home-code-examples/write-components-with-python-demo.html b/docs/overrides/home-code-examples/write-components-with-python-demo.html new file mode 100644 index 00000000..203287c6 --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python-demo.html @@ -0,0 +1,65 @@ +
+
+

3 Videos

+
+
+ + + +
+
+

First video

+

Video description

+
+ +
+
+
+ + + +
+
+

Second video

+

Video description

+
+ +
+
+
+ + + +
+
+

Third video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py new file mode 100644 index 00000000..6af43baa --- /dev/null +++ b/docs/overrides/home-code-examples/write-components-with-python.py @@ -0,0 +1,15 @@ +from reactpy import component, html + + +@component +def video_list(videos, empty_heading): + count = len(videos) + heading = empty_heading + if count > 0: + noun = "Videos" if count > 1 else "Video" + heading = f"{count} {noun}" + + return html.section( + html.h2(heading), + [video(video) for video in videos], + ) diff --git a/docs/overrides/home.html b/docs/overrides/home.html new file mode 100644 index 00000000..3664c44f --- /dev/null +++ b/docs/overrides/home.html @@ -0,0 +1,132 @@ + +{% extends "main.html" %} + + +{% block content %}{% endblock %} + + +{% block tabs %} + +
+
+ +

{{ config.site_name }}

+

{{ config.site_description }}

+ +
+ +
+

Create user interfaces from components

+

+ ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy + components like thumbnail, like_button, and video. Then combine + them into entire screens, pages, and apps. +

+
+ {% with image="create-user-interfaces.png", class="pop-left" %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} + {% include "home-code-examples/create-user-interfaces-demo.html" %} +
+

+ Whether you work on your own or with thousands of other developers, using React feels the same. It is + designed to let you seamlessly combine components written by independent people, teams, and + organizations. +

+
+ +
+

Write components with pure Python code

+

+ ReactPy components are Python functions. Want to show some content conditionally? Use an + if statement. Displaying a list? Try using + list comprehension. + Learning ReactPy is learning programming. +

+
+ {% with image="write-components-with-python.png", class="pop-left" %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} + {% include "home-code-examples/write-components-with-python-demo.html" %} + +
+
+ +
+

Add interactivity wherever you need it

+

+ ReactPy components receive data and return what should appear on the screen. You can pass them new data in + response to an interaction, like when the user types into an input. ReactPy will then update the screen to + match the new data. +

+
+ {% with image="add-interactivity.png" %} + {% include "home-code-examples/code-block.html" %} + {% endwith %} + {% include "home-code-examples/add-interactivity-demo.html" %} +
+

+ You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render + interactive ReactPy components anywhere on it. +

+
+ +
+

Go full-stack with the Django framework

+

+ ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and + data fetching. To build an entire app with ReactPy, we recommend a backend framework like + Django. +

+ + Get Started + +
+
+{% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0b173292..bc3074e6 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -13,7 +13,7 @@ {% endblock %} {% block outdated %} -You're not viewing the latest version. +You're not viewing the latest official release. Click here to go to latest. diff --git a/docs/python/configure-channels.py b/docs/python/configure-channels-asgi-app.py similarity index 55% rename from docs/python/configure-channels.py rename to docs/python/configure-channels-asgi-app.py index 337a922f..a2834e03 100644 --- a/docs/python/configure-channels.py +++ b/docs/python/configure-channels-asgi-app.py @@ -1,5 +1 @@ -INSTALLED_APPS = [ - "daphne", - ..., -] ASGI_APPLICATION = "example_project.asgi.application" diff --git a/docs/python/configure-channels-installed-app.py b/docs/python/configure-channels-installed-app.py new file mode 100644 index 00000000..fc9e4017 --- /dev/null +++ b/docs/python/configure-channels-installed-app.py @@ -0,0 +1,4 @@ +INSTALLED_APPS = [ + "daphne", + ..., +] diff --git a/docs/src/changelog/index.md b/docs/src/about/changelog.md similarity index 100% rename from docs/src/changelog/index.md rename to docs/src/about/changelog.md diff --git a/docs/src/contribute/code.md b/docs/src/about/code.md similarity index 56% rename from docs/src/contribute/code.md rename to docs/src/about/code.md index a5b52955..b163d01c 100644 --- a/docs/src/contribute/code.md +++ b/docs/src/about/code.md @@ -14,7 +14,7 @@ --- -## Modifying Code +## Creating an environment If you plan to make code changes to this repository, you will need to install the following dependencies first: @@ -38,7 +38,13 @@ Then, by running the command below you can: pip install -e . -r requirements.txt ``` -Finally, to verify that everything is working properly, you can manually run the test webserver. +!!! warning "Pitfall" + + Some of our development dependencies require a C++ compiler, which is not installed by default on Windows. + + If you receive errors related to this during installation, follow the instructions in your console errors. + +Finally, to verify that everything is working properly, you can manually run the test web server. ```bash linenums="0" cd tests @@ -47,6 +53,42 @@ python manage.py runserver Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to see if the tests are rendering correctly. -## GitHub Pull Request +## Creating a pull request {% include-markdown "../../includes/pr.md" %} + +## Running the full test suite + +!!! note + + This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. + +By running the command below you can run the full test suite: + +```bash linenums="0" +nox -s test +``` + +Or, if you want to run the tests in the background: + +```bash linenums="0" +nox -s test -- --headless +``` + +## Running Django tests + +If you want to only run our Django tests in your current environment, you can use the following command: + +```bash linenums="0" +cd tests +python manage.py test +``` + +## Running Django test web server + +If you want to manually run the Django test application, you can use the following command: + +```bash linenums="0" +cd tests +python manage.py runserver +``` diff --git a/docs/src/contribute/docs.md b/docs/src/about/docs.md similarity index 96% rename from docs/src/contribute/docs.md rename to docs/src/about/docs.md index 913bc0a6..6c2f413b 100644 --- a/docs/src/contribute/docs.md +++ b/docs/src/about/docs.md @@ -31,7 +31,7 @@ Then, by running the command below you can: pip install -e . -r requirements.txt --upgrade ``` -Finally, to verify that everything is working properly, you can manually run the docs preview webserver. +Finally, to verify that everything is working properly, you can manually run the docs preview web server. ```bash linenums="0" mkdocs serve diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css new file mode 100644 index 00000000..f71fa55a --- /dev/null +++ b/docs/src/assets/css/admonition.css @@ -0,0 +1,166 @@ +[data-md-color-scheme="slate"] { + --admonition-border-color: transparent; + --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); + --note-bg-color: rgb(43 110 98/ 0.2); + --terminal-bg-color: #0c0c0c; + --terminal-title-bg-color: #000; + --deep-dive-bg-color: rgb(43 52 145 / 0.2); + --you-will-learn-bg-color: #353a45; + --pitfall-bg-color: rgb(182 87 0 / 0.2); +} +[data-md-color-scheme="default"] { + --admonition-border-color: rgba(0, 0, 0, 0.08); + --admonition-expanded-border-color: var(--admonition-border-color); + --note-bg-color: rgb(244 251 249); + --terminal-bg-color: rgb(64 71 86); + --terminal-title-bg-color: rgb(35 39 47); + --deep-dive-bg-color: rgb(243 244 253); + --you-will-learn-bg-color: rgb(246, 247, 249); + --pitfall-bg-color: rgb(254, 245, 231); +} + +.md-typeset details, +.md-typeset .admonition { + border-color: var(--admonition-border-color) !important; + box-shadow: none; +} + +.md-typeset :is(.admonition, details) { + margin: 0.55em 0; +} + +.md-typeset .admonition { + font-size: 0.7rem; +} + +.md-typeset .admonition:focus-within, +.md-typeset details:focus-within { + box-shadow: none !important; +} + +.md-typeset details[open] { + border-color: var(--admonition-expanded-border-color) !important; +} + +/* +Admonition: "summary" +React Name: "You will learn" +*/ +.md-typeset .admonition.summary { + background: var(--you-will-learn-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .summary .admonition-title { + font-size: 1rem; + background: transparent; + padding-left: 0.6rem; + padding-bottom: 0; +} + +.md-typeset .summary .admonition-title:before { + display: none; +} + +.md-typeset .admonition.summary { + border-color: #ffffff17 !important; +} + +/* +Admonition: "note" +React Name: "Note" +*/ +.md-typeset .admonition.note { + background: var(--note-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .note .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(68 172 153); +} + +.md-typeset .note .admonition-title:before { + font-size: 1.1rem; + background: rgb(68 172 153); +} + +.md-typeset .note > .admonition-title:before, +.md-typeset .note > summary:before { + -webkit-mask-image: var(--md-admonition-icon--abstract); + mask-image: var(--md-admonition-icon--abstract); +} + +/* +Admonition: "warning" +React Name: "Pitfall" +*/ +.md-typeset .admonition.warning { + background: var(--pitfall-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .warning .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(219 125 39); +} + +.md-typeset .warning .admonition-title:before { + font-size: 1.1rem; + background: rgb(219 125 39); +} + +/* +Admonition: "info" +React Name: "Deep Dive" +*/ +.md-typeset .admonition.info { + background: var(--deep-dive-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; +} + +.md-typeset .info .admonition-title { + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(136 145 236); +} + +.md-typeset .info .admonition-title:before { + font-size: 1.1rem; + background: rgb(136 145 236); +} + +/* +Admonition: "example" +React Name: "Terminal" +*/ +.md-typeset .admonition.example { + background: var(--terminal-bg-color); + border-radius: 0.4rem; + overflow: hidden; + border: none; +} + +.md-typeset .example .admonition-title { + background: var(--terminal-title-bg-color); + color: rgb(246 247 249); +} + +.md-typeset .example .admonition-title:before { + background: rgb(246 247 249); +} + +.md-typeset .admonition.example code { + background: transparent; + color: #fff; + box-shadow: none; +} diff --git a/docs/src/assets/css/button.css b/docs/src/assets/css/button.css new file mode 100644 index 00000000..8f71391a --- /dev/null +++ b/docs/src/assets/css/button.css @@ -0,0 +1,41 @@ +[data-md-color-scheme="slate"] { + --md-button-font-color: #fff; + --md-button-border-color: #404756; +} + +[data-md-color-scheme="default"] { + --md-button-font-color: #000; + --md-button-border-color: #8d8d8d; +} + +.md-typeset .md-button { + border-width: 1px; + border-color: var(--md-button-border-color); + border-radius: 9999px; + color: var(--md-button-font-color); + transition: color 125ms, background 125ms, border-color 125ms, + transform 125ms; +} + +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + border-color: var(--md-button-border-color); + color: var(--md-button-font-color); + background: rgba(78, 87, 105, 0.05); +} + +.md-typeset .md-button.md-button--primary { + color: #fff; + border-color: transparent; + background: var(--reactpy-color-dark); +} + +.md-typeset .md-button.md-button--primary:focus, +.md-typeset .md-button.md-button--primary:hover { + border-color: transparent; + background: var(--reactpy-color-darker); +} + +.md-typeset .md-button:focus { + transform: scale(0.98); +} diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css new file mode 100644 index 00000000..d1556dc0 --- /dev/null +++ b/docs/src/assets/css/code.css @@ -0,0 +1,111 @@ +:root { + --code-max-height: 17.25rem; + --md-code-backdrop: rgba(0, 0, 0, 0) 0px 0px 0px 0px, + rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.03) 0px 0.8px 2px 0px, + rgba(0, 0, 0, 0.047) 0px 2.7px 6.7px 0px, + rgba(0, 0, 0, 0.08) 0px 12px 30px 0px; +} +[data-md-color-scheme="slate"] { + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: #16181d; + --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); + --code-tab-color: rgb(52 58 70); + --md-code-hl-name-color: #aadafc; + --md-code-hl-string-color: hsl(21 49% 63% / 1); + --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); + --md-code-hl-constant-color: hsl(213.91deg 68% 61%); + --md-code-hl-number-color: #bfd9ab; + --func-and-decorator-color: #dcdcae; + --module-import-color: #60c4ac; +} +[data-md-color-scheme="default"] { + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: rgba(208, 211, 220, 0.4); + --md-code-fg-color: rgb(64, 71, 86); + --code-tab-color: #fff; + --func-and-decorator-color: var(--md-code-hl-function-color); + --module-import-color: #e153e5; +} +[data-md-color-scheme="default"] .md-typeset .highlight > pre > code, +[data-md-color-scheme="default"] .md-typeset .highlight > table.highlighttable { + --md-code-bg-color: #fff; +} + +/* All code blocks */ +.md-typeset pre > code { + max-height: var(--code-max-height); +} + +/* Code blocks with no line number */ +.md-typeset .highlight > pre > code { + border-radius: 16px; + max-height: var(--code-max-height); + box-shadow: var(--md-code-backdrop); +} + +/* Code blocks with line numbers */ +.md-typeset .highlighttable .linenos { + max-height: var(--code-max-height); + overflow: hidden; +} +.md-typeset .highlighttable { + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; +} + +/* Tabbed code blocks */ +.md-typeset .tabbed-set { + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--md-default-fg-color--lightest); +} +.md-typeset .tabbed-set .tabbed-block { + overflow: hidden; +} +.js .md-typeset .tabbed-set .tabbed-labels { + background: var(--code-tab-color); + margin: 0; + padding-left: 0.8rem; +} +.md-typeset .tabbed-set .tabbed-labels > label { + font-weight: 400; + font-size: 0.7rem; + padding-top: 0.55em; + padding-bottom: 0.35em; +} +.md-typeset .tabbed-set .highlighttable { + border-radius: 0; +} + +/* Code hightlighting colors */ + +/* Module imports */ +.highlight .nc, +.highlight .ne, +.highlight .nn, +.highlight .nv { + color: var(--module-import-color); +} + +/* Function def name and decorator */ +.highlight .nd, +.highlight .nf { + color: var(--func-and-decorator-color); +} + +/* None type */ +.highlight .kc { + color: var(--md-code-hl-constant-color); +} + +/* Keywords such as def and return */ +.highlight .k { + color: var(--md-code-hl-constant-color); +} + +/* HTML tags */ +.highlight .nt { + color: var(--md-code-hl-constant-color); +} diff --git a/docs/src/assets/css/footer.css b/docs/src/assets/css/footer.css new file mode 100644 index 00000000..9dcaca2d --- /dev/null +++ b/docs/src/assets/css/footer.css @@ -0,0 +1,29 @@ +[data-md-color-scheme="slate"] { + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +[data-md-color-scheme="default"] { + --md-footer-fg-color: var(--md-typeset-color); + --md-footer-fg-color--light: var(--md-typeset-color); + --md-footer-bg-color: var(--md-default-bg-color); + --md-footer-bg-color--dark: var(--md-default-bg-color); + --md-footer-border-color: var(--md-header-border-color); +} + +.md-footer { + border-top: 1px solid var(--md-footer-border-color); +} + +.md-copyright { + width: 100%; +} + +.md-copyright__highlight { + width: 100%; +} + +.legal-footer-right { + float: right; +} diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css new file mode 100644 index 00000000..c72e7093 --- /dev/null +++ b/docs/src/assets/css/home.css @@ -0,0 +1,335 @@ +img.home-logo { + height: 120px; +} + +.home .row { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 6rem 0.8rem; +} + +.home .row:not(.first, .stripe) { + background: var(--row-bg-color); +} + +.home .row.stripe { + background: var(--row-stripe-bg-color); + border: 0 solid var(--stripe-border-color); + border-top-width: 1px; + border-bottom-width: 1px; +} + +.home .row.first { + text-align: center; +} + +.home .row h1 { + max-width: 28rem; + line-height: 1.15; + font-weight: 500; + margin-bottom: 0.55rem; + margin-top: -1rem; +} + +.home .row.first h1 { + margin-top: 0.55rem; + margin-bottom: -0.75rem; +} + +.home .row > p { + max-width: 35rem; + line-height: 1.5; + font-weight: 400; +} + +.home .row.first > p { + font-size: 32px; + font-weight: 500; +} + +/* Code blocks */ +.home .row .tabbed-set { + background: var(--home-tabbed-set-bg-color); + margin: 0; +} + +.home .row .tabbed-content { + padding: 20px 18px; + overflow-x: auto; +} + +.home .row .tabbed-content img { + user-select: none; + -moz-user-select: none; + -webkit-user-drag: none; + -webkit-user-select: none; + -ms-user-select: none; + max-width: 580px; +} + +.home .row .tabbed-content { + -webkit-filter: var(--code-block-filter); + filter: var(--code-block-filter); +} + +/* Code examples */ +.home .example-container { + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 11%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 87 45 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + border-radius: 16px; + margin: 30px 0; + max-width: 100%; + grid-column-gap: 20px; + padding-left: 20px; + padding-right: 20px; +} + +.home .demo .white-bg { + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + max-width: 590px; + min-width: -webkit-min-content; + min-width: -moz-min-content; + min-width: min-content; + row-gap: 1rem; + padding: 1rem; +} + +.home .demo .vid-row { + display: flex; + flex-direction: row; + -moz-column-gap: 12px; + column-gap: 12px; +} + +.home .demo { + color: #000; +} + +.home .demo .vid-thumbnail { + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 55%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 63 87 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + width: 9rem; + aspect-ratio: 16 / 9; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; +} + +.home .demo .vid-text { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 100%; +} + +.home .demo h2 { + font-size: 18px; + line-height: 1.375; + margin: 0; + text-align: left; + font-weight: 700; +} + +.home .demo h3 { + font-size: 16px; + line-height: 1.25; + margin: 0; +} + +.home .demo p { + font-size: 14px; + line-height: 1.375; + margin: 0; +} + +.home .demo .browser-nav-url { + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + font-size: 14px; + color: grey; + display: flex; + align-items: center; + justify-content: center; + -moz-column-gap: 5px; + column-gap: 5px; +} + +.home .demo .browser-navbar { + margin: -1rem; + margin-bottom: 0; + padding: 0.75rem 1rem; + border-bottom: 1px solid darkgrey; +} + +.home .demo .browser-viewport { + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + row-gap: 1rem; + height: 400px; + overflow-y: scroll; + margin: -1rem; + padding: 1rem; +} + +.home .demo .browser-viewport .search-header > h1 { + color: #000; + text-align: left; + font-size: 24px; + margin: 0; +} + +.home .demo .browser-viewport .search-header > p { + text-align: left; + font-size: 16px; + margin: 10px 0; +} + +.home .demo .search-bar input { + width: 100%; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + padding-left: 40px; + padding-right: 40px; + height: 40px; + color: #000; +} + +.home .demo .search-bar svg { + height: 40px; + position: absolute; + transform: translateX(75%); +} + +.home .demo .search-bar { + position: relative; +} + +/* Desktop Styling */ +@media screen and (min-width: 60em) { + .home .row { + text-align: center; + } + .home .row > p { + font-size: 21px; + } + .home .row > h1 { + font-size: 52px; + } + .home .row .pop-left { + margin-left: -20px; + margin-right: 0; + margin-top: -20px; + margin-bottom: -20px; + } + .home .row .pop-right { + margin-left: 0px; + margin-right: 0px; + margin-top: -20px; + margin-bottom: -20px; + } +} + +/* Mobile Styling */ +@media screen and (max-width: 60em) { + .home .row { + padding: 4rem 0.8rem; + } + .home .row > h1, + .home .row > p { + padding-left: 1rem; + padding-right: 1rem; + } + .home .row.first { + padding-top: 2rem; + } + .home-btns { + width: 100%; + display: grid; + grid-gap: 0.5rem; + gap: 0.5rem; + } + .home .example-container { + display: flex; + flex-direction: column; + row-gap: 20px; + width: 100%; + justify-content: center; + border-radius: 0; + padding: 1rem 0; + } + .home .row { + padding-left: 0; + padding-right: 0; + } + .home .tabbed-set { + width: 100%; + border-radius: 0; + } + .home .demo { + width: 100%; + display: flex; + justify-content: center; + } + .home .demo > .white-bg { + width: 80%; + max-width: 80%; + } +} diff --git a/docs/src/assets/css/main.css b/docs/src/assets/css/main.css new file mode 100644 index 00000000..500ae4be --- /dev/null +++ b/docs/src/assets/css/main.css @@ -0,0 +1,84 @@ +/* Variable overrides */ +:root { + --reactpy-color: #58b962; + --reactpy-color-dark: #42914a; + --reactpy-color-darker: #34743b; + --reactpy-color-opacity-10: rgb(88 185 98 / 10%); +} + +[data-md-color-accent="red"] { + --md-primary-fg-color--light: var(--reactpy-color); + --md-primary-fg-color--dark: var(--reactpy-color-dark); +} + +[data-md-color-scheme="slate"] { + --md-default-bg-color: rgb(35 39 47); + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); + --md-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color--light: #fff; + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +[data-md-color-scheme="default"] { + --md-primary-fg-color: var(--md-default-bg-color); + --md-default-fg-color--light: #000; + --md-default-fg-color--lighter: #0000007e; + --md-default-fg-color--lightest: #00000029; + --md-typeset-color: rgb(35, 39, 47); + --md-typeset-a-color: var(--reactpy-color); + --md-accent-fg-color: var(--reactpy-color-dark); +} + +/* Font changes */ +.md-typeset { + font-weight: 300; +} + +.md-typeset h1 { + font-weight: 500; + margin: 0; + font-size: 2.5em; +} + +.md-typeset h2 { + font-weight: 500; +} + +.md-typeset h3 { + font-weight: 600; +} + +/* Intro section styling */ +p.intro { + font-size: 0.9rem; + font-weight: 500; +} + +/* Hide "Overview" jump selector */ +h2#overview { + visibility: hidden; + height: 0; + margin: 0; + padding: 0; +} + +/* Reduce size of the outdated banner */ +.md-banner__inner { + margin: 0.45rem auto; +} + +/* Desktop Styles */ +@media screen and (min-width: 60em) { + /* Remove max width on desktop */ + .md-grid { + max-width: none; + } +} + +/* Max size of page content */ +.md-content { + max-width: 56rem; +} diff --git a/docs/src/assets/css/navbar.css b/docs/src/assets/css/navbar.css new file mode 100644 index 00000000..32347ccb --- /dev/null +++ b/docs/src/assets/css/navbar.css @@ -0,0 +1,163 @@ +[data-md-color-scheme="slate"] { + --md-header-border-color: rgb(255 255 255 / 5%); +} + +[data-md-color-scheme="default"] { + --md-header-border-color: rgb(0 0 0 / 7%); +} + +.md-header { + border: 0 solid transparent; + border-bottom-width: 1px; +} + +.md-header--shadow { + box-shadow: none; + border-color: var(--md-header-border-color); + transition: border-color 0.35s cubic-bezier(0.1, 0.7, 0.1, 1); +} + +/* Version selector */ +.md-header__topic .md-ellipsis, +.md-header__title [data-md-component="header-topic"] { + display: none; +} + +[dir="ltr"] .md-version__current { + margin: 0; +} + +.md-version__list { + margin: 0.2rem -0.8rem; +} + +/* Mobile Styling */ +@media screen and (max-width: 60em) { + label.md-header__button.md-icon[for="__drawer"] { + order: 1; + } + .md-header__button.md-logo { + display: initial; + order: 2; + margin-right: auto; + } + .md-header__title { + order: 3; + } + .md-header__button[for="__search"] { + order: 4; + } + .md-header__option[data-md-component="palette"] { + order: 5; + } + .md-header__source { + display: initial; + order: 6; + } + .md-header__source .md-source__repository { + display: none; + } +} + +/* Desktop Styling */ +@media screen and (min-width: 60em) { + /* Nav container */ + nav.md-header__inner { + display: contents; + } + header.md-header { + display: flex; + align-items: center; + } + + /* Logo */ + .md-header__button.md-logo { + order: 1; + padding-right: 0.4rem; + padding-top: 0; + padding-bottom: 0; + } + .md-header__button.md-logo img { + height: 2rem; + } + + /* Version selector */ + [dir="ltr"] .md-header__title { + order: 2; + margin: 0; + margin-right: 0.8rem; + margin-left: 0.2rem; + } + .md-header__topic { + position: relative; + } + + /* Search */ + .md-search { + order: 3; + width: 100%; + margin-right: 0.6rem; + } + .md-search__inner { + width: 100%; + float: unset !important; + } + .md-search__form { + border-radius: 9999px; + } + [data-md-toggle="search"]:checked ~ .md-header .md-header__option { + max-width: unset; + opacity: unset; + transition: unset; + } + + /* Tabs */ + .md-tabs { + order: 4; + min-width: -webkit-fit-content; + min-width: -moz-fit-content; + min-width: fit-content; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + z-index: -1; + overflow: visible; + border: none !important; + } + li.md-tabs__item.md-tabs__item--active { + background: var(--reactpy-color-opacity-10); + border-radius: 9999px; + color: var(--md-typeset-a-color); + } + .md-tabs__link { + margin: 0; + } + .md-tabs__item { + height: 1.8rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + + /* Dark/Light Selector */ + .md-header__option[data-md-component="palette"] { + order: 5; + } + + /* GitHub info */ + .md-header__source { + order: 6; + margin-left: 0 !important; + } +} + +/* Ultrawide Desktop Styles */ +@media screen and (min-width: 1919px) { + .md-search { + order: 2; + width: 100%; + max-width: 34.4rem; + margin: 0 auto; + } +} diff --git a/docs/src/assets/css/sidebar.css b/docs/src/assets/css/sidebar.css new file mode 100644 index 00000000..aeadf3b5 --- /dev/null +++ b/docs/src/assets/css/sidebar.css @@ -0,0 +1,100 @@ +:root { + --sizebar-font-size: 0.62rem; +} + +/* Desktop Styling */ +@media screen and (min-width: 76.1875em) { + /* Move the sidebar and TOC to the edge of the page */ + .md-main__inner.md-grid { + margin-left: 0; + margin-right: 0; + max-width: unset; + display: grid; + grid-template-columns: auto 1fr auto; + } + + .md-content { + justify-self: center; + width: 100%; + } + /* Made the sidebar buttons look React-like */ + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + text-transform: uppercase; + } + + .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + } + + .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { + color: rgb(133 142 159); + margin: 0.5rem; + } + + .md-nav__item .md-nav__link { + position: relative; + } + + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { + color: unset; + } + + .md-nav__item + .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.2; + z-index: -1; + background: grey; + } + + .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: var(--reactpy-color-opacity-10); + } + + .md-nav__link { + padding: 0.5rem 0.5rem 0.5rem 1rem; + margin: 0; + border-radius: 0 10px 10px 0; + font-weight: 500; + overflow: hidden; + font-size: var(--sizebar-font-size); + } + + .md-sidebar__scrollwrap { + margin: 0; + } + + [dir="ltr"] + .md-nav--lifted + .md-nav[data-md-level="1"] + > .md-nav__list + > .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item { + padding: 0; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 300; + } + + .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { + font-weight: 400; + padding-left: 1.25rem; + } +} diff --git a/docs/src/assets/css/table-of-contents.css b/docs/src/assets/css/table-of-contents.css new file mode 100644 index 00000000..aa9a61ae --- /dev/null +++ b/docs/src/assets/css/table-of-contents.css @@ -0,0 +1,39 @@ +/* Table of Contents styling */ +@media screen and (min-width: 60em) { + [data-md-component="sidebar"] .md-nav__title[for="__toc"] { + text-transform: uppercase; + margin: 0.5rem; + margin-left: 0; + font-size: var(--sizebar-font-size); + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active { + position: relative; + } + + [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.15; + z-index: -1; + background: var(--md-typeset-a-color); + } + + [data-md-component="toc"] .md-nav__link { + padding: 0.5rem 0.5rem; + margin: 0; + border-radius: 10px 0 0 10px; + font-weight: 400; + } + [dir="ltr"] .md-sidebar__inner { + padding: 0; + } + + .md-nav__item { + padding: 0; + } +} diff --git a/docs/src/assets/img/add-interactivity.png b/docs/src/assets/img/add-interactivity.png new file mode 100644 index 00000000..e5e24d29 Binary files /dev/null and b/docs/src/assets/img/add-interactivity.png differ diff --git a/docs/src/assets/img/create-user-interfaces.png b/docs/src/assets/img/create-user-interfaces.png new file mode 100644 index 00000000..13abd064 Binary files /dev/null and b/docs/src/assets/img/create-user-interfaces.png differ diff --git a/docs/src/assets/img/write-components-with-python.png b/docs/src/assets/img/write-components-with-python.png new file mode 100644 index 00000000..ba34cdf9 Binary files /dev/null and b/docs/src/assets/img/write-components-with-python.png differ diff --git a/docs/src/static/js/extra.js b/docs/src/assets/js/main.js similarity index 100% rename from docs/src/static/js/extra.js rename to docs/src/assets/js/main.js diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md deleted file mode 100644 index fe01be26..00000000 --- a/docs/src/contribute/running-tests.md +++ /dev/null @@ -1,58 +0,0 @@ -## Overview - -

- -You will need to set up a Python environment to run the ReactPy-Django test suite. - -

- ---- - -## Running Tests - -This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. - -If you plan to run tests, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -pip install -e . -r requirements.txt --upgrade -``` - -## Full Test Suite - -By running the command below you can run the full test suite: - -```bash linenums="0" -nox -s test -``` - -Or, if you want to run the tests in the background: - -```bash linenums="0" -nox -s test -- --headless -``` - -## Django Tests - -If you want to only run our Django tests in your current environment, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py test -``` - -## Django Test Webserver - -If you want to manually run the Django test application, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py runserver -``` diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 2dbbc4e6..d3d2eb25 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -2,8 +2,8 @@ django sanic plotly nox -websocket -websockets +WebSocket +WebSockets changelog async pre @@ -16,7 +16,6 @@ refetched refetching html jupyter -webserver iframe keyworded stylesheet diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md deleted file mode 100644 index 6c57fda3..00000000 --- a/docs/src/features/hooks.md +++ /dev/null @@ -1,328 +0,0 @@ -## Overview - -

- -Prefabricated hooks can be used within your `components.py` to help simplify development. - -

- -!!! note - - Looking for standard React hooks? - - This package only contains Django specific hooks. Standard hooks can be found within [`reactive-python/reactpy`](https://reactpy.dev/docs/reference/hooks-api.html#basic-hooks). - ---- - -## Use Query - -The `use_query` hook is used fetch Django ORM queries. - -The function you provide into this hook must return either a `Model` or `QuerySet`. - -=== "components.py" - - ```python - {% include "../../python/use-query.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - | Name | Type | Description | Default | - | --- | --- | --- | --- | - | `options` | `QueryOptions | None` | An optional `QueryOptions` object that can modify how the query is executed. | None | - | `query` | `Callable[_Params, _Result | None]` | A callable that returns a Django `Model` or `QuerySet`. | N/A | - | `*args` | `_Params.args` | Positional arguments to pass into `query`. | N/A | - | `**kwargs` | `_Params.kwargs` | Keyword arguments to pass into `query`. | N/A | - - **Returns** - - | Type | Description | - | --- | --- | - | `Query[_Result | None]` | An object containing `loading`/`error` states, your `data` (if the query has successfully executed), and a `refetch` callable that can be used to re-run the query. | - -??? question "How can I provide arguments to my query function?" - - `*args` and `**kwargs` can be provided to your query function via `use_query` parameters. - - === "components.py" - - ```python - {% include "../../python/use-query-args.py" %} - ``` - -??? question "Why does `get_items` in the example return `TodoItem.objects.all()`?" - - This was a technical design decision to based on [Apollo's `useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions. - - The `use_query` hook ensures the provided `Model` or `QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components. - -??? question "How can I use `QueryOptions` to customize fetching behavior?" - - **`thread_sensitive`** - - Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. - - This setting only applies to sync query functions, and will be ignored for async functions. - - === "components.py" - - ```python - {% include "../../python/use-query-thread-sensitive.py" %} - ``` - - --- - - **`postprocessor`** - - {% include-markdown "../../includes/orm.md" start="" end="" %} - - However, if you... - - 1. Want to use this hook to defer IO intensive tasks to be computed in the background - 2. Want to to utilize `use_query` with a different ORM - - ... then you can either set a custom `postprocessor`, or disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-disable.py" %} - ``` - - If you wish to create a custom `postprocessor`, you will need to create a callable. - - The first argument of `postprocessor` must be the query `data`. All proceeding arguments - are optional `postprocessor_kwargs` (see below). This `postprocessor` must return - the modified `data`. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-change.py" %} - ``` - - --- - - **`postprocessor_kwargs`** - - {% include-markdown "../../includes/orm.md" start="" end="" %} - - However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `use_query` hook. - - You can disable the prefetching behavior of the default `postprocessor` (located at `reactpy_django.utils.django_query_postprocessor`) via the `QueryOptions.postprocessor_kwargs` parameter. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-kwargs.py" %} - ``` - - _Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._ - -??? question "Can I define async query functions?" - - Async functions are supported by `use_query`. You can use them in the same way as a sync query function. - - However, be mindful of Django async ORM restrictions. - - === "components.py" - - ```python - {% include "../../python/use-query-async.py" %} - ``` - -??? question "Can I make ORM calls without hooks?" - - {% include-markdown "../../includes/orm.md" start="" end="" %} - -## Use Mutation - -The `use_mutation` hook is used to create, update, or delete Django ORM objects. - -The function you provide into this hook will have no return value. - -=== "components.py" - - ```python - {% include "../../python/use-mutation.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - | Name | Type | Description | Default | - | --- | --- | --- | --- | - | `mutate` | `Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `False`, then your `refetch` function will not be used. | N/A | - | `refetch` | `Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `query` function (used by the `use_query` hook) or a sequence of `query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `None` | - - **Returns** - - | Type | Description | - | --- | --- | - | `Mutation[_Params]` | An object containing `loading`/`error` states, a `reset` callable that will set `loading`/`error` states to defaults, and a `execute` callable that will run the query. | - -??? question "How can I provide arguments to my mutation function?" - - `*args` and `**kwargs` can be provided to your mutation function via `mutation.execute` parameters. - - === "components.py" - - ```python - {% include "../../python/use-mutation-args-kwargs.py" %} - ``` - -??? question "Can `use_mutation` trigger a refetch of `use_query`?" - - Yes, `use_mutation` can queue a refetch of a `use_query` via the `refetch=...` argument. - - The example below is a merge of the `use_query` and `use_mutation` examples above with the addition of a `refetch` argument on `use_mutation`. - - Please note that any `use_query` hooks that use `get_items` will be refetched upon a successful mutation. - - === "components.py" - - ```python - {% include "../../python/use-mutation-query-refetch.py" %} - ``` - - === "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? question "Can I make a failed `use_mutation` try again?" - - Yes, a `use_mutation` can be re-performed by calling `reset()` on your `use_mutation` instance. - - For example, take a look at `reset_event` below. - - === "components.py" - - ```python - {% include "../../python/use-mutation-reset.py" %} - ``` - - === "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? question "Can I make ORM calls without hooks?" - - {% include-markdown "../../includes/orm.md" start="" end="" %} - -## Use Connection - -You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. - -=== "components.py" - - ```python - {% include "../../python/use-connection.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `Connection` | The component's websocket. | - -## Use Scope - -This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). - -=== "components.py" - - ```python - {% include "../../python/use-scope.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `MutableMapping[str, Any]` | The websocket's `scope`. | - -## Use Location - -This is a shortcut that returns the Websocket's `path`. - -You can expect this hook to provide strings such as `/reactpy/my_path`. - -=== "components.py" - - ```python - {% include "../../python/use-location.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `Location` | A object containing the current URL's `pathname` and `search` query. | - -??? info "This hook's behavior will be changed in a future update" - - This hook will be updated to return the browser's currently active path. This change will come in alongside ReactPy URL routing support. - - Check out [reactive-python/reactpy-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. - -## Use Origin - -This is a shortcut that returns the Websocket's `origin`. - -You can expect this hook to provide strings such as `http://example.com`. - -=== "components.py" - - ```python - {% include "../../python/use-origin.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). | diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md deleted file mode 100644 index 3917d766..00000000 --- a/docs/src/features/settings.md +++ /dev/null @@ -1,38 +0,0 @@ -## Overview - -

- -Your **Django project's** `settings.py` can modify the behavior of ReactPy. - -

- -!!! note - - The default configuration of ReactPy is suitable for the vast majority of use cases. - - You should only consider changing settings when the necessity arises. - ---- - -## Primary Configuration - - - -These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. - -| Setting | Default Value | Example Value(s) | Description | -| --- | --- | --- | --- | -| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.
We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | -| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database.
If configuring `REACTPY_DATABASE`, it is mandatory to use our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | -| `REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `args` and `kwargs` passed into your component template tag.
Use `#!python 0` to not store any session data. | -| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. | -| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | -| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. | -| `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.
Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. | -| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.
You can use the `host` argument in your [template tag](../features/template-tag.md#component) as a manual override. | -| `REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | -| `REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | -| `REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | -| `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | - - diff --git a/docs/src/features/utils.md b/docs/src/features/utils.md deleted file mode 100644 index 9ba8e312..00000000 --- a/docs/src/features/utils.md +++ /dev/null @@ -1,65 +0,0 @@ -## Overview - -

- -Utility functions provide various miscellaneous functionality. These are typically not used, but are available for advanced use cases. - -

- ---- - -## Django Query Postprocessor - -This is the default postprocessor for the `use_query` hook. - -This postprocessor is designed to avoid Django's `SynchronousOnlyException` by recursively fetching all fields within a `Model` or `QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). - -=== "components.py" - - ```python - {% include "../../python/django-query-postprocessor.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - | Name | Type | Description | Default | - | --- | --- | --- | --- | - | `data` | `QuerySet | Model` | The `Model` or `QuerySet` to recursively fetch fields from. | N/A | - | `many_to_many` | `bool` | Whether or not to recursively fetch `ManyToManyField` relationships. | `True` | - | `many_to_one` | `bool` | Whether or not to recursively fetch `ForeignKey` relationships. | `True` | - - **Returns** - - | Type | Description | - | --- | --- | - | `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. | - -## Register Component - -The `register_component` function is used manually register a root component with ReactPy. - -You should always call `register_component` within a Django [`AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready) to retain compatibility with ASGI webserver workers. - -=== "apps.py" - - ```python - {% include "../../python/register-component.py" %} - ``` - -??? question "Do I need to register my components?" - - You typically will not need to use this function. - - For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. - - You only need to use this function if your host application does not contain any HTML templates that [reference](../features/template-tag.md#component) your components. - - A common scenario where this is needed is when you are modifying the [template tag `host = ...` argument](../features/template-tag.md#component) in order to configure a dedicated Django application as a rendering server for ReactPy. On this dedicated rendering server, you would need to manually register your components. diff --git a/docs/src/get-started/choose-django-app.md b/docs/src/get-started/choose-django-app.md deleted file mode 100644 index 1594baa5..00000000 --- a/docs/src/get-started/choose-django-app.md +++ /dev/null @@ -1,26 +0,0 @@ -## Overview - -

- -Set up a **Django Project** with at least one app. - -

- -!!! note - - If you have reached this point, you should have already [installed ReactPy-Django](../get-started/installation.md) through the previous steps. - ---- - -## Deciding which Django App to use - -You will now need to pick at least one **Django app** to start using ReactPy-Django on. - -For the following examples, we will assume the following: - -1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). -2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. - -??? question "How do I organize my Django project for ReactPy?" - - ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. diff --git a/docs/src/get-started/create-component.md b/docs/src/get-started/create-component.md deleted file mode 100644 index 1f94c308..00000000 --- a/docs/src/get-started/create-component.md +++ /dev/null @@ -1,40 +0,0 @@ -## Overview - -

- -You can let ReactPy know what functions are components by using the `#!python @component` decorator. - -

- ---- - -## Declaring a function as a root component - -You will need a file to start creating ReactPy components. - -We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. - -Within this file, you can define your component functions and then add ReactPy's `#!python @component` decorator. - -=== "components.py" - - {% include-markdown "../../../README.md" start="" end="" %} - -??? question "What should I name my ReactPy files and functions?" - - You have full freedom in naming/placement of your files and functions. - - We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - - Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](./use-template-tag.md)). So, at minimum your component path needs to be valid to Python's `importlib`. - -??? question "What does the decorator actually do?" - - While not all components need to be decorated, there are a few features this decorator adds to your components. - - 1. The ability to be used as a root component. - - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](./use-template-tag.md)). - 2. The ability to use [hooks](../features/hooks.md). - - The decorator is required on any component where hooks are defined. - 3. Scoped failures. - - If a decorated component generates an exception, then only that one component will fail to render. diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md deleted file mode 100644 index be727947..00000000 --- a/docs/src/get-started/installation.md +++ /dev/null @@ -1,125 +0,0 @@ -## Overview - -

- -[ReactPy-Django](https://github.com/reactive-python/reactpy-django) can be used to add used to add [ReactPy](https://github.com/reactive-python/reactpy) support to an existing **Django project**. Minimal configuration is required to get started. - -

- -!!! note - - These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. - - If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. - ---- - -## Step 1: Install from PyPI - -```bash linenums="0" -pip install reactpy-django -``` - -## Step 2: Configure [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) - -In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS). - -=== "settings.py" - - ```python - {% include "../../python/configure-installed-apps.py" %} - ``` - -??? warning "Enable Django Channels ASGI (Required)" - - ReactPy-Django requires ASGI Websockets from [Django Channels](https://github.com/django/channels). - - If you have not enabled ASGI on your **Django project** yet, you will need to - - 1. Install `channels[daphne]` - 2. Add `daphne` to `INSTALLED_APPS` - 3. Set your `ASGI_APPLICATION` variable. - - === "settings.py" - - ```python - {% include "../../python/configure-channels.py" %} - ``` - - Consider reading the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info. - -??? note "Configure ReactPy settings (Optional)" - - {% include "../features/settings.md" start="" end="" %} - -## Step 3: Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) - -Add ReactPy HTTP paths to your `urlpatterns`. - -=== "urls.py" - - ```python - {% include "../../python/configure-urls.py" %} - ``` - -## Step 4: Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) - -Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`. - -=== "asgi.py" - - ```python - {% include "../../python/configure-asgi.py" %} - ``` - -??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" - - There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to: - - 1. Access the `User` that is currently logged in - 2. Login or logout the current `User` - 3. Access Django's `Session` object - - In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. - - ```python linenums="0" - {% include "../../python/configure-asgi-middleware.py" start="# start" %} - ``` - -??? question "Where is my `asgi.py`?" - - If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). - -## Step 5: Run database migrations - -Run Django's database migrations to initialize ReactPy-Django's database table. - -```bash linenums="0" -python manage.py migrate -``` - -## Step 6: Check your configuration - -Run Django's check command to verify if ReactPy was set up correctly. - -```bash linenums="0" -python manage.py check -``` - -## Step 7: Create your first component! - -The [following steps](./choose-django-app.md) will show you how to create your first ReactPy component. - -Prefer a quick summary? Read the **At a Glance** section below. - -!!! info "At a Glance" - - **`my_app/components.py`** - - {% include-markdown "../../../README.md" start="" end="" %} - - --- - - **`my_app/templates/my-template.html`** - - {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/get-started/learn-more.md b/docs/src/get-started/learn-more.md deleted file mode 100644 index 3ee45968..00000000 --- a/docs/src/get-started/learn-more.md +++ /dev/null @@ -1,17 +0,0 @@ -# :confetti_ball: Congratulations :confetti_ball: - -

- -If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! - -

- -!!! info "Deep Dive" - - The docs you are reading only covers our Django integration. To learn more, check out one of the following links: - - - [ReactPy-Django Feature Reference](../features/components.md) - - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) - - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) - - Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/get-started/register-view.md b/docs/src/get-started/register-view.md deleted file mode 100644 index 472d722b..00000000 --- a/docs/src/get-started/register-view.md +++ /dev/null @@ -1,41 +0,0 @@ -## Overview - -

- -Render your template containing your ReactPy component using a Django view. - -

- -!!! Note - - We assume you have [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but we have included a simple example below. - ---- - -## Creating a Django view and URL path - -Within your **Django app**'s `views.py` file, you will need to create a function to render the HTML template containing your ReactPy components. - -In this example, we will create a view that renders `my-template.html` ([_from the previous step_](./use-template-tag.md)). - -=== "views.py" - - ```python - {% include "../../python/example/views.py" %} - ``` - -We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view). - -=== "urls.py" - - ```python - {% include "../../python/example/urls.py" %} - ``` - -??? question "Which urls.py do I add my views to?" - - For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. - - Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. - - Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. diff --git a/docs/src/get-started/run-webserver.md b/docs/src/get-started/run-webserver.md deleted file mode 100644 index cb4f87f1..00000000 --- a/docs/src/get-started/run-webserver.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -

- -Run a webserver to display your Django view. - -

- ---- - -## Viewing your component using a webserver - -To test your new Django view, run the following command to start up a development webserver. - -```bash linenums="0" -python manage.py runserver -``` - -Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](./register-view.md)). - -If you copy-pasted our example component, you will now see your component display "Hello World". - -!!! warning "Pitfall" - - Do not use `manage.py runserver` for production. - - This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). diff --git a/docs/src/get-started/use-template-tag.md b/docs/src/get-started/use-template-tag.md deleted file mode 100644 index 8ef210fd..00000000 --- a/docs/src/get-started/use-template-tag.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -

- -Decide where the component will be displayed by using our template tag. - -

- ---- - -## Embedding a component in a template - -{% include-markdown "../../../README.md" start="" end="" %} - -Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` ([_from the previous step_](./create-component.md)) accepts a `recipient` argument. - -=== "my-template.html" - - {% include-markdown "../../../README.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -??? question "Where is my templates folder?" - - If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). diff --git a/docs/src/index.md b/docs/src/index.md index 6b8b3aaa..384ec5b6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,17 +1,6 @@ --- +template: home.html hide: - navigation - toc --- - -![ReactPy Django](https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg){ align=left style=height:40px } - -# ReactPy Django - -{% include-markdown "../../README.md" start="" end="" %} - -{% include-markdown "../../README.md" start="" end="" %} - -## Resources - -{% include-markdown "../../README.md" start="" end="" %} diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md new file mode 100644 index 00000000..7d7c949f --- /dev/null +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -0,0 +1,128 @@ +## Overview + +

+ +If you want to add some interactivity to your existing **Django project**, you don't have to rewrite it in ReactPy. Use [ReactPy-Django](https://github.com/reactive-python/reactpy-django) to add [ReactPy](https://github.com/reactive-python/reactpy) to your existing stack, and render interactive components anywhere. + +

+ +!!! note + + These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. + + If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. + +--- + +## Step 1: Install from PyPI + +Run the following command to install [`reactpy-django`](https://pypi.org/project/reactpy-django/) in your Python environment. + +```bash linenums="0" +pip install reactpy-django +``` + +## Step 2: Configure `settings.py` + +Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. + +=== "settings.py" + + ```python + {% include "../../python/configure-installed-apps.py" %} + ``` + +??? warning "Enable ASGI and Django Channels (Required)" + + ReactPy-Django requires Django ASGI and [Django Channels](https://github.com/django/channels) WebSockets. + + If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: + + 1. Install `channels[daphne]` + 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. + + ```python linenums="0" + {% include "../../python/configure-channels-installed-app.py" %} + ``` + + 3. Set your `#!python ASGI_APPLICATION` variable. + + ```python linenums="0" + {% include "../../python/configure-channels-asgi-app.py" %} + ``` + +??? note "Configure ReactPy settings (Optional)" + + {% include "../reference/settings.md" start="" end="" %} + +## Step 3: Configure `urls.py` + +Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. + +=== "urls.py" + + ```python + {% include "../../python/configure-urls.py" %} + ``` + +## Step 4: Configure `asgi.py` + +Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. + +=== "asgi.py" + + ```python + {% include "../../python/configure-asgi.py" %} + ``` + +??? note "Add `#!python AuthMiddlewareStack` and `#!python SessionMiddlewareStack` (Optional)" + + There are many situations where you need to access the Django `#!python User` or `#!python Session` objects within ReactPy components. For example, if you want to: + + 1. Access the `#!python User` that is currently logged in + 2. Login or logout the current `#!python User` + 3. Access Django's `#!python Session` object + + In these situations will need to ensure you are using `#!python AuthMiddlewareStack` and/or `#!python SessionMiddlewareStack`. + + ```python linenums="0" + {% include "../../python/configure-asgi-middleware.py" start="# start" %} + ``` + +??? question "Where is my `asgi.py`?" + + If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). + +## Step 5: Run database migrations + +Run Django's [`migrate` command](https://docs.djangoproject.com/en/dev/topics/migrations/) to initialize ReactPy-Django's database table. + +```bash linenums="0" +python manage.py migrate +``` + +## Step 6: Check your configuration + +Run Django's [`check` command](https://docs.djangoproject.com/en/dev/ref/django-admin/#check) to verify if ReactPy was set up correctly. + +```bash linenums="0" +python manage.py check +``` + +## Step 7: Create your first component + +The [next step](./your-first-component.md) will show you how to create your first ReactPy component. + +Prefer a quick summary? Read the **At a Glance** section below. + +!!! info "At a Glance: Your First Component" + + **`my_app/components.py`** + + {% include-markdown "../../../README.md" start="" end="" %} + + --- + + **`my_app/templates/my-template.html`** + + {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md new file mode 100644 index 00000000..e7ddcd65 --- /dev/null +++ b/docs/src/learn/your-first-component.md @@ -0,0 +1,131 @@ +## Overview + +

+ +Components are one of the core concepts of ReactPy. They are the foundation upon which you build user interfaces (UI), which makes them the perfect place to start your journey! + +

+ +!!! note + + If you have reached this point, you should have already [installed ReactPy-Django](../learn/add-reactpy-to-a-django-project.md) through the previous steps. + +--- + +## Selecting a Django App + +You will now need to pick at least one **Django app** to start using ReactPy-Django on. + +For the following examples, we will assume the following: + +1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). +2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. + +??? question "How do I organize my Django project for ReactPy?" + + ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. + +## Defining a component + +You will need a file to start creating ReactPy components. + +We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. + +Within this file, you can define your component functions using ReactPy's `#!python @component` decorator. + +=== "components.py" + + {% include-markdown "../../../README.md" start="" end="" %} + +??? question "What should I name my ReactPy files and functions?" + + You have full freedom in naming/placement of your files and functions. + + We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. + + Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. + +??? question "What does the decorator actually do?" + + While not all components need to be decorated, there are a few features this decorator adds to your components. + + 1. The ability to be used as a root component. + - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](#embedding-in-a-template)). + 2. The ability to use [hooks](../reference/hooks.md). + - The decorator is required on any component where hooks are defined. + 3. Scoped failures. + - If a decorated component generates an exception, then only that one component will fail to render. + +## Embedding in a template + +In your **Django app**'s HTML template, you can now embed your ReactPy component using the `#!jinja {% component %}` template tag. Within this tag, you will need to type in the dotted path to the component. + +Additionally, you can pass in `#!python args` and `#!python kwargs` into your component function. After reading the code below, pay attention to how the function definition for `#!python hello_world` ([_from the previous step_](#defining-a-component)) accepts a `#!python recipient` argument. + +=== "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +??? question "Where is my templates folder?" + + If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + +## Setting up a Django view + +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my-template.html` ([_from the previous step_](#embedding-in-a-template)). + +=== "views.py" + + ```python + {% include "../../python/example/views.py" %} + ``` + +We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. + +=== "urls.py" + + ```python + {% include "../../python/example/urls.py" %} + ``` + +??? question "Which urls.py do I add my views to?" + + For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. + + Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. + + Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. + +## Viewing your component + +To test your new Django view, run the following command to start up a development web server. + +```bash linenums="0" +python manage.py runserver +``` + +Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](#setting-up-a-django-view)). + +If you copy-pasted our example component, you will now see your component display "Hello World". + +??? warning "Do not use `manage.py runserver` for production" + + This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). + +## Learn more + +**Congratulations!** If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! + +!!! info "Deep Dive" + + The docs you are reading only covers our Django integration. To learn more, check out one of the following links: + + - [ReactPy-Django Feature Reference](../reference/components.md) + - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) + - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) + + Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/features/components.md b/docs/src/reference/components.md similarity index 51% rename from docs/src/features/components.md rename to docs/src/reference/components.md index f4dae25a..d3f235da 100644 --- a/docs/src/features/components.md +++ b/docs/src/reference/components.md @@ -24,24 +24,24 @@ Convert any Django view into a ReactPy component by using this decorator. Compat | Name | Type | Description | Default | | --- | --- | --- | --- | - | `view` | `Callable | View` | The view function or class to convert. | N/A | - | `compatibility` | `bool` | If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments will be ignored. | `False` | - | `transforms` | `Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | - | `strict_parsing` | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | + | `#!python view` | `#!python Callable | View` | The view function or class to convert. | N/A | + | `#!python compatibility` | `#!python bool` | If `#!python True`, the component will be rendered in an iframe. When using compatibility mode `#!python tranforms`, `#!python strict_parsing`, `#!python request`, `#!python args`, and `#!python kwargs` arguments will be ignored. | `#!python False` | + | `#!python transforms` | `#!python Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `#!python tuple` | + | `#!python strict_parsing` | `#!python bool` | If `#!python True`, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `#!python True` | **Returns** | Type | Description | | --- | --- | - | `_ViewComponentConstructor` | A function that takes `request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `key` which is used by ReactPy. | + | `#!python _ViewComponentConstructor` | A function that takes `#!python request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `#!python key` which is used by ReactPy. | -??? Warning "Potential information exposure when using `compatibility = True`" +??? Warning "Potential information exposure when using `#!python compatibility = True`" - When using `compatibility` mode, ReactPy automatically exposes a URL to your view. + When using `#!python compatibility` mode, ReactPy automatically exposes a URL to your view. It is your responsibility to ensure privileged information is not leaked via this method. - You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... + You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`#!python user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... === "Function Based View" @@ -57,17 +57,17 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? info "Existing limitations" - There are currently several limitations of using `view_to_component` that may be resolved in a future version of `reactpy_django`. + There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. - Requires manual intervention to change request methods beyond `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. - Has no option to automatically intercept local anchor link (such as `#!html `) click events. - _Please note these limitations do not exist when using `compatibility` mode._ + _Please note these limitations do not exist when using `#!python compatibility` mode._ ??? question "How do I use this for Class Based Views?" - You can simply pass your Class Based View directly into `view_to_component`. + You can simply pass your Class Based View directly into `#!python view_to_component`. === "components.py" @@ -77,7 +77,7 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? question "How do I transform views from external libraries?" - In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator. + In order to convert external views, you can utilize `#!python view_to_component` as a function, rather than a decorator. === "components.py" @@ -85,11 +85,11 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-func.py" %} ``` -??? question "How do I provide `request`, `args`, and `kwargs` to a view?" +??? question "How do I provide `#!python request`, `#!python args`, and `#!python kwargs` to a view?" - **`Request`** + **`#!python Request`** - You can use the `request` parameter to provide the view a custom request object. + You can use the `#!python request` parameter to provide the view a custom request object. === "components.py" @@ -99,9 +99,9 @@ Convert any Django view into a ReactPy component by using this decorator. Compat --- - **`args` and `kwargs`** + **`#!python args` and `#!python kwargs`** - You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view. + You can use the `#!python args` and `#!python kwargs` parameters to provide positional and keyworded arguments to a view. === "components.py" @@ -109,15 +109,15 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-args-kwargs.py" %} ``` -??? question "How do I use `strict_parsing`, `compatibility`, and `transforms`?" +??? question "How do I use `#!python strict_parsing`, `#!python compatibility`, and `#!python transforms`?" - **`strict_parsing`** + **`#!python strict_parsing`** By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `. - In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`. + In these scenarios, you may want to rely on best-fit parsing by setting the `#!python strict_parsing` parameter to `#!python False`. === "components.py" @@ -129,11 +129,11 @@ Convert any Django view into a ReactPy component by using this decorator. Compat --- - **`compatibility`** + **`#!python compatibility`** For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. - Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode. + Any view can be rendered within compatibility mode. However, the `#!python transforms`, `#!python strict_parsing`, `#!python request`, `#!python args`, and `#!python kwargs` arguments do not apply to compatibility mode. @@ -143,17 +143,17 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-compatibility.py" %} ``` - _Note: By default the `compatibility` iframe is unstyled, and thus won't look pretty until you add some CSS._ + _Note: By default the `#!python compatibility` iframe is unstyled, and thus won't look pretty until you add some CSS._ --- - **`transforms`** + **`#!python transforms`** - After your view has been turned into [VDOM](https://reactpy.dev/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node. + After your view has been turned into [VDOM](https://reactpy.dev/docs/reference/specifications.html#vdom) (python dictionaries), `#!python view_to_component` will call your `#!python transforms` functions on every VDOM node. This allows you to modify your view prior to rendering. - For example, if you are trying to modify the text of a node with a certain `id`, you can create a transform like such: + For example, if you are trying to modify the text of a node with a certain `#!python id`, you can create a transform like such: === "components.py" @@ -177,22 +177,22 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. | Name | Type | Description | Default | | --- | --- | --- | --- | - | `static_path` | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | - | `key` | `Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `None` | + | `#!python static_path` | `#!python str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | -??? question "Should I put `django_css` at the top of my HTML?" +??? question "Should I put `#!python django_css` at the top of my HTML?" Yes, if the stylesheet contains styling for your component. -??? question "Can I load static CSS using `html.link` instead?" +??? question "Can I load static CSS using `#!python html.link` instead?" - While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause unintended visual behavior, so use this at your own discretion. + While you can load stylesheets with `#!python html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause unintended visual behavior, so use this at your own discretion. Here's an example on what you should avoid doing for Django static files: @@ -202,9 +202,9 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. ??? question "How do I load external CSS?" - `django_css` can only be used with local static files. + `#!python django_css` can only be used with local static files. - For external CSS, substitute `django_css` with `html.link`. + For external CSS, substitute `#!python django_css` with `#!python html.link`. ```python {% include "../../python/django-css-external-link.py" %} @@ -214,7 +214,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed. + To help improve webpage load times, you can use the `#!python django_css` component to defer loading your stylesheet until it is needed. ## Django JS @@ -232,22 +232,22 @@ Allows you to defer loading JavaScript until a component begins rendering. This | Name | Type | Description | Default | | --- | --- | --- | --- | - | `static_path` | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | - | `key` | `Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `None` | + | `#!python static_path` | `#!python str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | -??? question "Should I put `django_js` at the bottom of my HTML?" +??? question "Should I put `#!python django_js` at the bottom of my HTML?" Yes, if your scripts are reliant on the contents of the component. -??? question "Can I load static JavaScript using `html.script` instead?" +??? question "Can I load static JavaScript using `#!python html.script` instead?" - While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. + While you can load JavaScript with `#!python html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. Here's an example on what you should avoid doing for Django static files: @@ -257,9 +257,9 @@ Allows you to defer loading JavaScript until a component begins rendering. This ??? question "How do I load external JS?" - `django_js` can only be used with local static files. + `#!python django_js` can only be used with local static files. - For external JavaScript, substitute `django_js` with `html.script`. + For external JavaScript, substitute `#!python django_js` with `#!python html.script`. ```python {% include "../../python/django-js-remote-script.py" %} @@ -269,4 +269,4 @@ Allows you to defer loading JavaScript until a component begins rendering. This Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed. + To help improve webpage load times, you can use the `#!python django_js` component to defer loading your JavaScript until it is needed. diff --git a/docs/src/features/decorators.md b/docs/src/reference/decorators.md similarity index 51% rename from docs/src/features/decorators.md rename to docs/src/reference/decorators.md index ee79a4e8..59440366 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/reference/decorators.md @@ -10,11 +10,11 @@ Decorator functions can be used within your `components.py` to help simplify dev ## Auth Required -You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses). +You can limit access to a component to users with a specific `#!python auth_attribute` by using this decorator (with or without parentheses). -By default, this decorator checks if the user is logged in and not deactivated (`is_active`). +By default, this decorator checks if the user is logged in and not deactivated (`#!python is_active`). -This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). +This decorator is commonly used to selectively render a component only if a user [`#!python is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`#!python is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). === "components.py" @@ -28,20 +28,20 @@ This decorator is commonly used to selectively render a component only if a user | Name | Type | Description | Default | | --- | --- | --- | --- | - | `auth_attribute` | `str` | The value to check within the user object. This is checked in the form of `UserModel.`. | `#!python "is_active"` | - | `fallback` | `ComponentType`, `VdomDict`, `None` | The `component` or `reactpy.html` snippet to render if the user is not authenticated. | `None` | + | `#!python auth_attribute` | `#!python str` | The value to check within the user object. This is checked via `#!python getattr(scope["user"], auth_attribute)`. | `#!python "is_active"` | + | `#!python fallback` | `#!python ComponentType | VdomDict | None` | The `#!python component` or `#!python reactpy.html` snippet to render if the user is not authenticated. | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | - | `VdomDict` | An `reactpy.html` snippet. | - | `None` | No component render. | + | #!python Component` | A ReactPy component. | + | #!python VdomDict` | A `#!python reactpy.html` snippet. | + | #!python None` | No component render. | ??? question "How do I render a different component if authentication fails?" - You can use a component with the `fallback` argument, as seen below. + You can use a component with the `#!python fallback` argument, as seen below. === "components.py" @@ -49,9 +49,9 @@ This decorator is commonly used to selectively render a component only if a user {% include "../../python/auth-required-component-fallback.py" %} ``` -??? question "How do I render a simple `reactpy.html` snippet if authentication fails?" +??? question "How do I render a simple `#!python reactpy.html` snippet if authentication fails?" - You can use a `reactpy.html` snippet with the `fallback` argument, as seen below. + You can use a `#!python reactpy.html` snippet with the `#!python fallback` argument, as seen below. === "components.py" @@ -59,9 +59,9 @@ This decorator is commonly used to selectively render a component only if a user {% include "../../python/auth-required-vdom-fallback.py" %} ``` -??? question "How can I check if a user `is_staff`?" +??? question "How can I check if a user `#!python is_staff`?" - You can set the `auth_attribute` to `is_staff`, as seen blow. + You can do this by setting `#!python auth_attribute="is_staff"`, as seen blow. === "components.py" @@ -73,7 +73,7 @@ This decorator is commonly used to selectively render a component only if a user You will need to be using a [custom user model](https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model) within your Django instance. - For example, if your user model has the field `is_really_cool` ... + For example, if your user model has the field `#!python is_really_cool` ... === "models.py" diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md new file mode 100644 index 00000000..62986930 --- /dev/null +++ b/docs/src/reference/hooks.md @@ -0,0 +1,328 @@ +## Overview + +

+ +Prefabricated hooks can be used within your `components.py` to help simplify development. + +

+ +!!! note + + Looking for standard React hooks? + + This package only contains Django specific hooks. Standard hooks can be found within [`reactive-python/reactpy`](https://reactpy.dev/docs/reference/hooks-api.html#basic-hooks). + +--- + +## Use Query + +This hook is used [read](https://www.sumologic.com/glossary/crud/) data from the Django ORM. + +The query function you provide must return either a `#!python Model` or `#!python QuerySet`. + +=== "components.py" + + ```python + {% include "../../python/use-query.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python options` | `#!python QueryOptions | None` | An optional `#!python QueryOptions` object that can modify how the query is executed. | `#!python None` | + | `#!python query` | `#!python Callable[_Params, _Result | None]` | A callable that returns a Django `#!python Model` or `#!python QuerySet`. | N/A | + | `#!python *args` | `#!python _Params.args` | Positional arguments to pass into `#!python query`. | N/A | + | `#!python **kwargs` | `#!python _Params.kwargs` | Keyword arguments to pass into `#!python query`. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Query[_Result | None]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. | + +??? question "How can I provide arguments to my query function?" + + `#!python *args` and `#!python **kwargs` can be provided to your query function via `#!python use_query` parameters. + + === "components.py" + + ```python + {% include "../../python/use-query-args.py" %} + ``` + +??? question "Why does `#!python get_items` in the example return `#!python TodoItem.objects.all()`?" + + This was a technical design decision to based on [Apollo's `#!javascript useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `#!python SynchronousOnlyOperation` exceptions. + + The `#!python use_query` hook ensures the provided `#!python Model` or `#!python QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components. + +??? question "How can I use `#!python QueryOptions` to customize fetching behavior?" + + **`#!python thread_sensitive`** + + Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`#!python sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + + This setting only applies to sync query functions, and will be ignored for async functions. + + === "components.py" + + ```python + {% include "../../python/use-query-thread-sensitive.py" %} + ``` + + --- + + **`#!python postprocessor`** + + {% include-markdown "../../includes/orm.md" start="" end="" %} + + However, if you... + + 1. Want to use this hook to defer IO intensive tasks to be computed in the background + 2. Want to to utilize `#!python use_query` with a different ORM + + ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python QueryOptions.postprocessor` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-disable.py" %} + ``` + + If you wish to create a custom `#!python postprocessor`, you will need to create a callable. + + The first argument of `#!python postprocessor` must be the query `#!python data`. All proceeding arguments + are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` must return + the modified `#!python data`. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-change.py" %} + ``` + + --- + + **`#!python postprocessor_kwargs`** + + {% include-markdown "../../includes/orm.md" start="" end="" %} + + However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `#!python use_query` hook. + + You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python QueryOptions.postprocessor_kwargs` parameter. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-kwargs.py" %} + ``` + + _Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._ + +??? question "Can I define async query functions?" + + Async functions are supported by `#!python use_query`. You can use them in the same way as a sync query function. + + However, be mindful of Django async ORM restrictions. + + === "components.py" + + ```python + {% include "../../python/use-query-async.py" %} + ``` + +??? question "Can I make ORM calls without hooks?" + + {% include-markdown "../../includes/orm.md" start="" end="" %} + +## Use Mutation + +This hook is used to [create, update, or delete](https://www.sumologic.com/glossary/crud/) Django ORM objects. + +The mutation function you provide should have no return value. + +=== "components.py" + + ```python + {% include "../../python/use-mutation.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python mutate` | `#!python Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | + | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `#!python query` function (used by the `#!python use_query` hook) or a sequence of `#!python query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `#!python None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Mutation[_Params]` | An object containing `#!python loading`/`#!python error` states, a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults, and a `#!python execute` callable that will run the query. | + +??? question "How can I provide arguments to my mutation function?" + + `#!python *args` and `#!python **kwargs` can be provided to your mutation function via #!python mutation.execute` parameters. + + === "components.py" + + ```python + {% include "../../python/use-mutation-args-kwargs.py" %} + ``` + +??? question "Can `#!python use_mutation` trigger a refetch of `#!python use_query`?" + + Yes, `#!python use_mutation` can queue a refetch of a `#!python use_query` via the `#!python refetch=...` argument. + + The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument. + + Please note that any `#!python use_query` hooks that use `#!python get_items` will be refetched upon a successful mutation. + + === "components.py" + + ```python + {% include "../../python/use-mutation-query-refetch.py" %} + ``` + + === "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? question "Can I make a failed `#!python use_mutation` try again?" + + Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance. + + For example, take a look at `#!python reset_event` below. + + === "components.py" + + ```python + {% include "../../python/use-mutation-reset.py" %} + ``` + + === "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? question "Can I make ORM calls without hooks?" + + {% include-markdown "../../includes/orm.md" start="" end="" %} + +## Use Connection + +This hook is used to fetch the Django Channels [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer). + +=== "components.py" + + ```python + {% include "../../python/use-connection.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Connection` | The component's WebSocket. | + +## Use Scope + +This is a shortcut that returns the WebSocket's [`#!python scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). + +=== "components.py" + + ```python + {% include "../../python/use-scope.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python MutableMapping[str, Any]` | The WebSocket's `#!python scope`. | + +## Use Location + +This is a shortcut that returns the WebSocket's `#!python path`. + +You can expect this hook to provide strings such as `/reactpy/my_path`. + +=== "components.py" + + ```python + {% include "../../python/use-location.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Location` | An object containing the current URL's `#!python pathname` and `#!python search` query. | + +??? info "This hook's behavior will be changed in a future update" + + This hook will be updated to return the browser's currently active HTTP path. This change will come in alongside ReactPy URL routing support. + + Check out [reactive-python/reactpy-django#147](https://github.com/reactive-python/reactpy-django/issues/147) for more information. + +## Use Origin + +This is a shortcut that returns the WebSocket's `#!python origin`. + +You can expect this hook to provide strings such as `http://example.com`. + +=== "components.py" + + ```python + {% include "../../python/use-origin.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python str | None` | A string containing the browser's current origin, obtained from WebSocket headers (if available). | diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md new file mode 100644 index 00000000..9f108a0d --- /dev/null +++ b/docs/src/reference/settings.md @@ -0,0 +1,38 @@ +## Overview + +

+ +Your **Django project's** `settings.py` can modify the behavior of ReactPy. + +

+ +!!! note + + The default configuration of ReactPy is suitable for the vast majority of use cases. + + You should only consider changing settings when the necessity arises. + +--- + +## Primary Configuration + + + +These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. + +| Setting | Default Value | Example Value(s) | Description | +| --- | --- | --- | --- | +| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | +| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` | +| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `#!python args` and `#!python kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. | +| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy WebSocket and HTTP URLs. | +| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. | +| `#!python REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `#!python AuthMiddlewareStack` and...
2. You are using Django's `#!python AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `#!python backend` attribute. | +| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). | +| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. | +| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. | +| `#!python REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. | +| `#!python REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. | +| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. | + + diff --git a/docs/src/features/template-tag.md b/docs/src/reference/template-tag.md similarity index 69% rename from docs/src/features/template-tag.md rename to docs/src/reference/template-tag.md index e04c5bb9..6f20be36 100644 --- a/docs/src/features/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -10,7 +10,7 @@ Django template tags can be used within your HTML templates to provide ReactPy f ## Component -The `component` template tag can be used to insert any number of ReactPy components onto your page. +This template tag can be used to insert any number of ReactPy components onto your page. === "my-template.html" @@ -22,18 +22,18 @@ The `component` template tag can be used to insert any number of ReactPy compone | Name | Type | Description | Default | | --- | --- | --- | --- | - | `dotted_path` | `str` | The dotted path to the component to render. | N/A | - | `*args` | `Any` | The positional arguments to provide to the component. | N/A | - | `class` | `str | None` | The HTML class to apply to the top-level component div. | `None` | - | `key` | `str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `key` within a template tag is effectively useless. | `None` | - | `host` | `str | None` | The host to use for the ReactPy connections. If set to `None`, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `None` | - | `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A | + | `#!python dotted_path` | `#!python str` | The dotted path to the component to render. | N/A | + | `#!python *args` | `#!python Any` | The positional arguments to provide to the component. | N/A | + | `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!python None` | + | `#!python key` | `#!python str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` | + | `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If set to `#!python None`, the host will be automatically configured.
Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` | + | `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A | **Returns** | Type | Description | | --- | --- | - | `Component` | A ReactPy component. | + | `#!python Component` | A ReactPy component. | @@ -65,7 +65,7 @@ The `component` template tag can be used to insert any number of ReactPy compone ??? question "Can I render components on a different server (distributed computing)?" - Yes! By using the `host` keyword argument, you can render components from a completely separate ASGI server. + Yes! By using the `#!python host` keyword argument, you can render components from a completely separate ASGI server. === "my-template.html" @@ -80,8 +80,8 @@ The `component` template tag can be used to insert any number of ReactPy compone Here's a couple of things to keep in mind: 1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment. - 2. You will not need to register ReactPy HTTP or websocket paths on any applications that do not perform any component rendering. - 3. Your component will only be able to access `*args`/`**kwargs` you provide to the template tag if your applications share a common database. + 2. You will not need to register ReactPy HTTP or WebSocket paths on any applications that do not perform any component rendering. + 3. Your component will only be able to access `#!python *args`/`#!python **kwargs` you provide to the template tag if your applications share a common database. @@ -112,7 +112,7 @@ The `component` template tag can be used to insert any number of ReactPy compone ??? question "Can I use positional arguments instead of keyword arguments?" - You can use any combination of `*args`/`**kwargs` in your template tag. + You can use any combination of `#!python *args`/`#!python **kwargs` in your template tag. === "my-template.html" diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md new file mode 100644 index 00000000..d71facc2 --- /dev/null +++ b/docs/src/reference/utils.md @@ -0,0 +1,65 @@ +## Overview + +

+ +Utility functions provide various miscellaneous functionality. These are typically not used, but are available for advanced use cases. + +

+ +--- + +## Django Query Postprocessor + +This is the default postprocessor for the `#!python use_query` hook. + +This postprocessor is designed to avoid Django's `#!python SynchronousOnlyException` by recursively fetching all fields within a `#!python Model` or `#!python QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). + +=== "components.py" + + ```python + {% include "../../python/django-query-postprocessor.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python data` | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` to recursively fetch fields from. | N/A | + | `#!python many_to_many` | `#!python bool` | Whether or not to recursively fetch `#!python ManyToManyField` relationships. | `#!python True` | + | `#!python many_to_one` | `#!python bool` | Whether or not to recursively fetch `#!python ForeignKey` relationships. | `#!python True` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` with all fields fetched. | + +## Register Component + +This function is used manually register a root component with ReactPy. + +=== "apps.py" + + ```python + {% include "../../python/register-component.py" %} + ``` + +??? warning "Only use this within `#!python AppConfig.ready()`" + + You should always call `#!python register_component` within a Django [`#!python AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + +??? question "Do I need to use this?" + + You typically will not need to use this function. + + For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. + + This is typically only needed when you have a dedicated Django application as a rendering server that doesn't have templates, such as when modifying the [template tag `#!python host` argument](../reference/template-tag.md#component). On this dedicated rendering server, you would need to manually register your components. diff --git a/docs/src/static/css/extra.css b/docs/src/static/css/extra.css deleted file mode 100644 index d3967666..00000000 --- a/docs/src/static/css/extra.css +++ /dev/null @@ -1,376 +0,0 @@ -/* Variable overrides */ -:root { - --code-max-height: 17.25rem; -} - -[data-md-color-scheme="slate"] { - --md-code-hl-color: #ffffcf1c; - --md-hue: 225; - --md-default-bg-color: hsla(var(--md-hue), 15%, 16%, 1); - --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); - --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); - --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); - --md-code-bg-color: #16181d; - --md-primary-fg-color: #2b3540; - --md-default-fg-color--light: #fff; - --md-typeset-a-color: #00b0f0; - --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); - --tabbed-labels-color: rgb(52 58 70); -} - -[data-md-color-scheme="default"] { - --tabbed-labels-color: #7d829e26; -} - -/* General admonition styling */ -/* TODO: Write this in a way that supports the light theme */ -[data-md-color-scheme="slate"] .md-typeset details, -[data-md-color-scheme="slate"] .md-typeset .admonition { - border-color: transparent !important; -} - -.md-typeset :is(.admonition, details) { - margin: 0.55em 0; -} - -.md-typeset .admonition { - font-size: 0.7rem; -} - -.md-typeset .admonition:focus-within, -.md-typeset details:focus-within { - box-shadow: var(--md-shadow-z1) !important; -} - -/* Colors for "summary" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - background: #353a45; - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title { - font-size: 1rem; - background: transparent; - padding-left: 0.6rem; - padding-bottom: 0; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title:before { - display: none; -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - border-color: #ffffff17 !important; -} - -/* Colors for "note" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.note { - background: rgb(43 110 98/ 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(68 172 153); -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(68 172 153); -} - -.md-typeset .note > .admonition-title:before, -.md-typeset .note > summary:before { - -webkit-mask-image: var(--md-admonition-icon--abstract); - mask-image: var(--md-admonition-icon--abstract); -} - -/* Colors for "warning" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.warning { - background: rgb(182 87 0 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(219 125 39); -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(219 125 39); -} - -/* Colors for "info" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.info { - background: rgb(43 52 145 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(136 145 236); -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(136 145 236); -} - -/* Colors for "example" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.example { - background: rgb(94 104 126); - border-radius: 0.4rem; -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title { - background: rgb(78 87 105); - color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title:before { - background-color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.example code { - background: transparent; - color: #fff; -} - -/* Move the sidebars to the edges of the page */ -.md-main__inner.md-grid { - margin-left: 0; - margin-right: 0; - max-width: unset; - display: flex; - justify-content: center; -} - -.md-sidebar--primary { - margin-right: auto; -} - -.md-sidebar.md-sidebar--secondary { - margin-left: auto; -} - -.md-content { - max-width: 56rem; -} - -/* Maintain content positioning even if sidebars are disabled */ -@media screen and (min-width: 76.1875em) { - .md-sidebar { - display: block; - } - - .md-sidebar[hidden] { - visibility: hidden; - } -} - -/* Sidebar styling */ -@media screen and (min-width: 76.1875em) { - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - text-transform: uppercase; - } - - .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - } - - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - color: rgb(133 142 159); - margin: 0.5rem; - } - - .md-nav__item .md-nav__link { - position: relative; - } - - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { - color: unset; - } - - .md-nav__item - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.2; - z-index: -1; - background-color: grey; - } - - .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - .md-nav__link { - padding: 0.5rem 0.5rem 0.5rem 1rem; - margin: 0; - border-radius: 0 10px 10px 0; - font-weight: 600; - overflow: hidden; - } - - .md-sidebar__scrollwrap { - margin: 0; - } - - [dir="ltr"] - .md-nav--lifted - .md-nav[data-md-level="1"] - > .md-nav__list - > .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 300; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 400; - padding-left: 1.25rem; - } -} - -/* Table of Contents styling */ -@media screen and (min-width: 60em) { - [data-md-component="sidebar"] .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - margin-left: 0; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active { - position: relative; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - [data-md-component="toc"] .md-nav__link { - padding: 0.5rem 0.5rem; - margin: 0; - border-radius: 10px 0 0 10px; - } - [dir="ltr"] .md-sidebar__inner { - padding: 0; - } - - .md-nav__item { - padding: 0; - } -} - -/* Font changes */ -.md-typeset { - font-weight: 300; -} - -.md-typeset h1 { - font-weight: 500; - margin: 0; - font-size: 2.5em; -} - -.md-typeset h2 { - font-weight: 500; -} - -.md-typeset h3 { - font-weight: 600; -} - -/* Intro section styling */ -p.intro { - font-size: 0.9rem; - font-weight: 500; -} - -/* Hide invisible jump selectors */ -h2#overview { - visibility: hidden; - height: 0; - margin: 0; - padding: 0; -} - -/* Code blocks */ -.md-typeset pre > code { - border-radius: 16px; -} - -.md-typeset .highlighttable .linenos { - max-height: var(--code-max-height); - overflow: hidden; -} - -.md-typeset .tabbed-block .highlighttable code { - border-radius: 0; -} - -.md-typeset .tabbed-block { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - overflow: hidden; -} - -.js .md-typeset .tabbed-labels { - background: var(--tabbed-labels-color); - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - -.md-typeset .tabbed-labels > label { - font-weight: 400; - font-size: 0.7rem; - padding-top: 0.55em; - padding-bottom: 0.35em; -} - -.md-typeset pre > code { - max-height: var(--code-max-height); -} - -/* Reduce height of outdated banner */ -.md-banner__inner { - margin: 0.45rem auto; -} diff --git a/mkdocs.yml b/mkdocs.yml index ae5129bc..e4e19c65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,28 +2,22 @@ nav: - Home: index.md - Get Started: - - Install ReactPy-Django: get-started/installation.md - - Choose a Django App: get-started/choose-django-app.md - - Create a Component: get-started/create-component.md - - Use the Template Tag: get-started/use-template-tag.md - - Register a View: get-started/register-view.md - - Run the Webserver: get-started/run-webserver.md - - Learn More: get-started/learn-more.md + - Add ReactPy to a Django Project: learn/add-reactpy-to-a-django-project.md + - Your First Component: learn/your-first-component.md - Reference: - - Components: features/components.md - - Hooks: features/hooks.md - - Decorators: features/decorators.md - - Utilities: features/utils.md - - Template Tag: features/template-tag.md - - Settings: features/settings.md + - Components: reference/components.md + - Hooks: reference/hooks.md + - Decorators: reference/decorators.md + - Utilities: reference/utils.md + - Template Tag: reference/template-tag.md + - Settings: reference/settings.md - About: - Contribute: - - Code: contribute/code.md - - Docs: contribute/docs.md - - Running Tests: contribute/running-tests.md + - Code: about/code.md + - Docs: about/docs.md - GitHub Discussions: https://github.com/reactive-python/reactpy-django/discussions - Discord: https://discord.gg/uNb5P4hA9X - - Changelog: changelog/index.md + - Changelog: about/changelog.md theme: name: material @@ -34,19 +28,22 @@ theme: toggle: icon: material/white-balance-sunny name: Switch to light mode - primary: light blue - accent: light blue + primary: red # We use red to indicate that something is unthemed + accent: red - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/weather-night name: Switch to dark mode - primary: black + primary: white + accent: red features: - navigation.instant - navigation.tabs + - navigation.tabs.sticky - navigation.top - content.code.copy + - search.highlight icon: repo: fontawesome/brands/github logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg @@ -93,10 +90,18 @@ extra: provider: mike extra_javascript: - - static/js/extra.js + - assets/js/main.js extra_css: - - static/css/extra.css + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css watch: - docs @@ -107,8 +112,8 @@ watch: site_name: ReactPy-Django site_author: Archmonger -site_description: React for Django developers. -copyright: Copyright © 2023 Reactive Python +site_description: It's React, but in Python. Now for Django developers. +copyright: Copyright © 2023 Reactive Python. repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django repo_name: reactive-python/reactpy-django diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index adc437d0..0025008b 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -60,7 +60,7 @@ def reactpy_warnings(app_configs, **kwargs): Warning( "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled " "and you running with Daphne.", - hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different webserver.", + hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.", id="reactpy_django.W003", ) ) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index a99da927..df552e42 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -24,7 +24,7 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" -# Run in production mode when using a real webserver +# Run in production mode when using a real web server DEBUG = all( not sys.argv[0].endswith(substring) for substring in {"hypercorn", "uvicorn", "daphne"}