From 625728f18f4dfb041ac93d31627e8bf2903a2618 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 18:51:59 -0700 Subject: [PATCH 01/13] comment cleanup --- docs/python/configure-urls.py | 2 +- docs/python/settings.py | 4 ++++ docs/src/get-started/installation.md | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/python/configure-urls.py b/docs/python/configure-urls.py index d619b8a1..49ae5ec1 100644 --- a/docs/python/configure-urls.py +++ b/docs/python/configure-urls.py @@ -2,6 +2,6 @@ urlpatterns = [ - path("reactpy/", include("reactpy_django.http.urls")), ..., + path("reactpy/", include("reactpy_django.http.urls")), ] diff --git a/docs/python/settings.py b/docs/python/settings.py index a08dbc55..965e0909 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -1,7 +1,11 @@ +# Cache used to store ReactPy web modules. # ReactPy requires a multiprocessing-safe and thread-safe cache. +# We recommend redis or python-diskcache. REACTPY_CACHE = "default" +# Database ReactPy uses to store session data. # ReactPy requires a multiprocessing-safe and thread-safe database. +# Do NOT use an in-memory database. REACTPY_DATABASE = "default" # Maximum seconds between reconnection attempts before giving up. diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md index 7aa2ea65..d9763992 100644 --- a/docs/src/get-started/installation.md +++ b/docs/src/get-started/installation.md @@ -76,7 +76,7 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`. 1. Access the `User` that is currently logged in 2. Login or logout the current `User` - 3. Access Django's `Sesssion` object + 3. Access Django's `Session` object In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. From d0b17e4b99f1c7e514ccf3e74f3bdc8290e7dce9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 19:19:46 -0700 Subject: [PATCH 02/13] Use Django database router API --- src/reactpy_django/config.py | 2 +- src/reactpy_django/database.py | 29 ++++++++++++++++++++ src/reactpy_django/templatetags/reactpy.py | 3 +-- src/reactpy_django/utils.py | 8 ++---- src/reactpy_django/websocket/consumer.py | 6 +---- tests/test_app/settings.py | 18 +++++++------ tests/test_app/tests/test_components.py | 8 ++---- tests/test_app/tests/test_database.py | 31 +++++----------------- 8 files changed, 52 insertions(+), 53 deletions(-) create mode 100644 src/reactpy_django/database.py diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 960aa58f..57f4dfb9 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -14,7 +14,7 @@ from reactpy_django.utils import import_dotted_path -# Not user configurable settings +# Non-user configurable settings REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {} REACTPY_VIEW_COMPONENT_IFRAMES: dict[str, ViewComponentIframe] = {} diff --git a/src/reactpy_django/database.py b/src/reactpy_django/database.py new file mode 100644 index 00000000..736ffdc8 --- /dev/null +++ b/src/reactpy_django/database.py @@ -0,0 +1,29 @@ +from reactpy_django.config import REACTPY_DATABASE + + +class Router: + """ + A router to control all database operations on models in the + auth and contenttypes applications. + """ + + route_app_labels = {"reactpy_django"} + + def db_for_read(self, model, **hints): + """Attempts to read go to REACTPY_DATABASE.""" + if model._meta.app_label in self.route_app_labels: + return REACTPY_DATABASE + + def db_for_write(self, model, **hints): + """Attempts to write go to REACTPY_DATABASE.""" + if model._meta.app_label in self.route_app_labels: + return REACTPY_DATABASE + + def allow_relation(self, obj1, obj2, **hints): + """Only relations within the same database are allowed (default behavior).""" + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + """Make sure ReactPy models only appear in REACTPY_DATABASE.""" + if app_label in self.route_app_labels: + return db == REACTPY_DATABASE diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index d1ce87e5..20e9b0de 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -7,7 +7,6 @@ from reactpy_django import models from reactpy_django.config import ( - REACTPY_DATABASE, REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX, REACTPY_WEBSOCKET_URL, @@ -73,7 +72,7 @@ def component(dotted_path: str, *args, **kwargs): params = ComponentParamData(args, kwargs) model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params)) model.full_clean() - model.save(using=REACTPY_DATABASE) + model.save() except Exception as e: if isinstance(e, ComponentParamError): diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 219f5c3e..4a8f9da6 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -24,7 +24,6 @@ from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError - _logger = logging.getLogger(__name__) _component_tag = r"(?Pcomponent)" _component_path = r"(?P\"[^\"'\s]+\"|'[^\"'\s]+')" @@ -334,7 +333,6 @@ def db_cleanup(immediate: bool = False): This function may be expanded in the future to include additional cleanup tasks.""" from .config import ( REACTPY_CACHE, - REACTPY_DATABASE, REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX, ) @@ -351,7 +349,7 @@ def db_cleanup(immediate: bool = False): expires_by: datetime = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX) # Component params exist in the DB, but we don't know when they were last cleaned - if not cleaned_at_str and ComponentSession.objects.using(REACTPY_DATABASE).all(): + if not cleaned_at_str and ComponentSession.objects.all(): _logger.warning( "ReactPy has detected component sessions in the database, " "but no timestamp was found in cache. This may indicate that " @@ -361,9 +359,7 @@ def db_cleanup(immediate: bool = False): # Delete expired component parameters # Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by: - ComponentSession.objects.using(REACTPY_DATABASE).filter( - last_accessed__lte=expires_by - ).delete() + ComponentSession.objects.filter(last_accessed__lte=expires_by).delete() caches[REACTPY_CACHE].set(cache_key, now_str, timeout=None) # Check if cleaning took abnormally long diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index c9f32bce..633e46f7 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -19,7 +19,6 @@ from reactpy_django.types import ComponentParamData, ComponentWebsocket from reactpy_django.utils import db_cleanup, func_has_args - _logger = logging.getLogger(__name__) @@ -78,7 +77,6 @@ async def _run_dispatch_loop(self): """Runs the main loop that performs component rendering tasks.""" from reactpy_django import models from reactpy_django.config import ( - REACTPY_DATABASE, REACTPY_RECONNECT_MAX, REACTPY_REGISTERED_COMPONENTS, ) @@ -117,9 +115,7 @@ async def _run_dispatch_loop(self): await database_sync_to_async(db_cleanup, thread_sensitive=False)() # Get the queries from a DB - params_query = await models.ComponentSession.objects.using( - REACTPY_DATABASE - ).aget( + params_query = await models.ComponentSession.objects.aget( uuid=uuid, last_accessed__gt=now - timedelta(seconds=REACTPY_RECONNECT_MAX), diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 15d6d1a1..285d0244 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -76,10 +76,10 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - # Changing NAME is needed due to a bug related to `manage.py test` migrations - "NAME": os.path.join(BASE_DIR, "test_db.sqlite3") - if "test" in sys.argv - else os.path.join(BASE_DIR, "db.sqlite3"), + # Changing NAME is needed due to a bug related to `manage.py test` + "NAME": os.path.join( + BASE_DIR, "test_db.sqlite3" if "test" in sys.argv else "db.sqlite3" + ), "TEST": { "NAME": os.path.join(BASE_DIR, "test_db.sqlite3"), "OPTIONS": {"timeout": 20}, @@ -91,10 +91,10 @@ if "test" in sys.argv: DATABASES["reactpy"] = { "ENGINE": "django.db.backends.sqlite3", - # Changing NAME is needed due to a bug related to `manage.py test` migrations - "NAME": os.path.join(BASE_DIR, "test_db_2.sqlite3") - if "test" in sys.argv - else os.path.join(BASE_DIR, "db_2.sqlite3"), + # Changing NAME is needed due to a bug related to `manage.py test` + "NAME": os.path.join( + BASE_DIR, "test_db_2.sqlite3" if "test" in sys.argv else "db_2.sqlite3" + ), "TEST": { "NAME": os.path.join(BASE_DIR, "test_db_2.sqlite3"), "OPTIONS": {"timeout": 20}, @@ -103,6 +103,8 @@ "OPTIONS": {"timeout": 20}, } REACTPY_DATABASE = "reactpy" +DATABASE_ROUTERS = ["reactpy_django.database.Router"] + # Cache CACHES = { diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index fc4ef799..0801e28e 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -272,28 +272,24 @@ def test_component_param_error(self): def test_component_session_exists(self): """Session should exist for components with args/kwargs.""" - from reactpy_django.config import REACTPY_DATABASE - component = self.page.locator("#parametrized-component") component.wait_for() parent = component.locator("..") session_id = parent.get_attribute("id") os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - query = ComponentSession.objects.filter(uuid=session_id).using(REACTPY_DATABASE) + query = ComponentSession.objects.filter(uuid=session_id) query_exists = query.exists() os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE") self.assertTrue(query_exists) def test_component_session_missing(self): """No session should exist for components that don't have args/kwargs.""" - from reactpy_django.config import REACTPY_DATABASE - component = self.page.locator("#simple-button") component.wait_for() parent = component.locator("..") session_id = parent.get_attribute("id") os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - query = ComponentSession.objects.filter(uuid=session_id).using(REACTPY_DATABASE) + query = ComponentSession.objects.filter(uuid=session_id) query_exists = query.exists() os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE") self.assertFalse(query_exists) diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index 4b30d58a..decd2316 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -4,7 +4,6 @@ import dill as pickle from django.test import TransactionTestCase - from reactpy_django import utils from reactpy_django.models import ComponentSession from reactpy_django.types import ComponentParamData @@ -14,15 +13,13 @@ class RoutedDatabaseTests(TransactionTestCase): databases = {"reactpy"} def test_component_params(self): - db = list(self.databases)[0] - # Make sure the ComponentParams table is empty - self.assertEqual(ComponentSession.objects.using(db).count(), 0) + self.assertEqual(ComponentSession.objects.count(), 0) params_1 = self._save_params_to_db(1) # Check if a component params are in the database - self.assertEqual(ComponentSession.objects.using(db).count(), 1) - self.assertEqual(pickle.loads(ComponentSession.objects.using(db).first().params), params_1) # type: ignore + self.assertEqual(ComponentSession.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentSession.objects.first().params), params_1) # type: ignore # Force `params_1` to expire from reactpy_django import config @@ -32,14 +29,14 @@ def test_component_params(self): # Create a new, non-expired component params params_2 = self._save_params_to_db(2) - self.assertEqual(ComponentSession.objects.using(db).count(), 2) + self.assertEqual(ComponentSession.objects.count(), 2) # Delete the first component params based on expiration time utils.db_cleanup() # Don't use `immediate` to test cache timestamping logic # Make sure `params_1` has expired - self.assertEqual(ComponentSession.objects.using(db).count(), 1) - self.assertEqual(pickle.loads(ComponentSession.objects.using(db).first().params), params_2) # type: ignore + self.assertEqual(ComponentSession.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentSession.objects.first().params), params_2) # type: ignore def _save_params_to_db(self, value: Any) -> ComponentParamData: db = list(self.databases)[0] @@ -50,19 +47,3 @@ def _save_params_to_db(self, value: Any) -> ComponentParamData: model.save(using=db) return param_data - - -class DefaultDatabaseTests(RoutedDatabaseTests): - databases = {"default"} - - def setUp(self) -> None: - from reactpy_django import config - - config.REACTPY_DATABASE = "default" - return super().setUp() - - def tearDown(self) -> None: - from reactpy_django import config - - config.REACTPY_DATABASE = "reactpy" - return super().tearDown() From 7e6ff78d7fbf307cfcb5456787727695c746729d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 19:20:11 -0700 Subject: [PATCH 03/13] document DATABASE_ROUTERS --- docs/python/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/python/settings.py b/docs/python/settings.py index 965e0909..162d3e0b 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -6,7 +6,9 @@ # Database ReactPy uses to store session data. # ReactPy requires a multiprocessing-safe and thread-safe database. # Do NOT use an in-memory database. +# DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured REACTPY_DATABASE = "default" +DATABASE_ROUTERS = ["reactpy_django.database.Router"] # Maximum seconds between reconnection attempts before giving up. # Use `0` to prevent component reconnection. From 28131f80bf7a5a8e8f331514aa0c1a8d485ea578 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 19:27:17 -0700 Subject: [PATCH 04/13] Add ellipses to database router docs --- docs/python/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python/settings.py b/docs/python/settings.py index 162d3e0b..9c4b9df1 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -8,7 +8,7 @@ # Do NOT use an in-memory database. # DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured REACTPY_DATABASE = "default" -DATABASE_ROUTERS = ["reactpy_django.database.Router"] +DATABASE_ROUTERS = ["reactpy_django.database.Router", ...] # Maximum seconds between reconnection attempts before giving up. # Use `0` to prevent component reconnection. From 33c9196cdc3c0b7b0b39c6340fa63d9a0a94fab2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 20:34:35 -0700 Subject: [PATCH 05/13] format --- src/reactpy_django/utils.py | 7 ++----- src/reactpy_django/websocket/consumer.py | 1 + tests/test_app/tests/test_database.py | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 4a8f9da6..f41c09db 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -24,6 +24,7 @@ from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError + _logger = logging.getLogger(__name__) _component_tag = r"(?Pcomponent)" _component_path = r"(?P\"[^\"'\s]+\"|'[^\"'\s]+')" @@ -331,11 +332,7 @@ def create_cache_key(*args): def db_cleanup(immediate: bool = False): """Deletes expired component sessions from the database. This function may be expanded in the future to include additional cleanup tasks.""" - from .config import ( - REACTPY_CACHE, - REACTPY_DEBUG_MODE, - REACTPY_RECONNECT_MAX, - ) + from .config import REACTPY_CACHE, REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX from .models import ComponentSession clean_started_at = datetime.now() diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 633e46f7..c298b389 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -19,6 +19,7 @@ from reactpy_django.types import ComponentParamData, ComponentWebsocket from reactpy_django.utils import db_cleanup, func_has_args + _logger = logging.getLogger(__name__) diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index decd2316..d6bd9059 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -4,6 +4,7 @@ import dill as pickle from django.test import TransactionTestCase + from reactpy_django import utils from reactpy_django.models import ComponentSession from reactpy_django.types import ComponentParamData From bef0e783d01a9023a8cf6ad9b6497ce9e11e9fbc Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 20:34:50 -0700 Subject: [PATCH 06/13] change docs for default auth backend --- docs/python/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python/settings.py b/docs/python/settings.py index 9c4b9df1..0245ea45 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -25,4 +25,4 @@ # 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_AUTH_BACKEND = None +REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend" From eb8a63369e28061e178897090506861250e4cf9a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 23:00:57 -0700 Subject: [PATCH 07/13] add system checks --- docs/python/settings.py | 9 +- src/reactpy_django/__init__.py | 3 +- src/reactpy_django/checks.py | 153 +++++++++++++++++++++++++++++++++ src/reactpy_django/config.py | 13 +-- tests/test_app/settings.py | 2 +- 5 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 src/reactpy_django/checks.py diff --git a/docs/python/settings.py b/docs/python/settings.py index 0245ea45..5bcdb50e 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -6,7 +6,7 @@ # Database ReactPy uses to store session data. # ReactPy requires a multiprocessing-safe and thread-safe database. # Do NOT use an in-memory database. -# DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured +# DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured. REACTPY_DATABASE = "default" DATABASE_ROUTERS = ["reactpy_django.database.Router", ...] @@ -14,13 +14,14 @@ # Use `0` to prevent component reconnection. REACTPY_RECONNECT_MAX = 259200 -# The URL for ReactPy to serve the component rendering websocket +# The URL for ReactPy to serve the component rendering websocket. REACTPY_WEBSOCKET_URL = "reactpy/" -# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function, or `None` +# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function, +# or `None`. REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor" -# Dotted path to the Django authentication backend to use for ReactPy components +# 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... diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index 8d026baa..fc4c15d4 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -1,4 +1,4 @@ -from reactpy_django import components, decorators, hooks, types, utils +from reactpy_django import checks, components, decorators, hooks, types, utils from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_PATH @@ -10,4 +10,5 @@ "decorators", "types", "utils", + "checks", ] diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py new file mode 100644 index 00000000..d6ce4774 --- /dev/null +++ b/src/reactpy_django/checks.py @@ -0,0 +1,153 @@ +from django.core.checks import Error, Tags, Warning, register + + +@register(Tags.compatibility) +def reactpy_warnings(app_configs, **kwargs): + from django.conf import settings + from django.urls import reverse + + warnings: list[Error | Warning] = [] + + # REACTPY_DATABASE is not an in-memory database. + if ( + getattr(settings, "DATABASES", {}) + .get(getattr(settings, "REACTPY_DATABASE", "default"), {}) + .get("NAME", None) + == ":memory:" + ): + warnings.append( + Warning( + "Using ReactPy with an in-memory database can cause unexpected " + "behaviors.", + hint="Configure settings.py:DATABASES[REACTPY_DATABASE], to use a " + "multiprocessing and thread safe database.", + id="reactpy_django.W001", + ) + ) + + # REACTPY_CACHE is not an in-memory cache. + if getattr(settings, "CACHES", {}).get( + getattr(settings, "REACTPY_CACHE", "default"), {} + ).get("BACKEND", None) in { + "django.core.cache.backends.dummy.DummyCache", + "django.core.cache.backends.locmem.LocMemCache", + }: + warnings.append( + Warning( + "Using ReactPy with an in-memory cache can cause unexpected " + "behaviors.", + hint="Configure settings.py:CACHES[REACTPY_CACHE], to use a " + "multiprocessing and thread safe cache.", + id="reactpy_django.W002", + ) + ) + + # ReactPy URLs exist + try: + reverse("reactpy:web_modules", kwargs={"file": "example"}) + reverse("reactpy:view_to_component", kwargs={"view_path": "example"}) + except Exception: + warnings.append( + Warning( + "ReactPy URLs have not been registered.", + hint="""Add 'path("reactpy/", include("reactpy_django.http.urls"))' """ + "to your application's urlpatterns.", + id="reactpy_django.W003", + ) + ) + + return warnings + + +@register(Tags.compatibility) +def reactpy_errors(app_configs, **kwargs): + from django.conf import settings + + errors: list[Error | Warning] = [] + + # Make sure ASGI is enabled + if not getattr(settings, "ASGI_APPLICATION", None): + errors.append( + Error( + "ASGI_APPLICATION is not defined." + " ReactPy requires ASGI to be enabled.", + hint="Add ASGI_APPLICATION to settings.py.", + id="reactpy_django.E001", + ) + ) + + # DATABASE_ROUTERS is properly configured when REACTPY_DATABASE is defined + if getattr( + settings, "REACTPY_DATABASE", None + ) and "reactpy_django.database.Router" not in getattr( + settings, "DATABASE_ROUTERS", [] + ): + errors.append( + Error( + "ReactPy database has been changed but the database router is " + "not configured.", + hint="Set settings.py:DATABASE_ROUTERS to " + "['reactpy_django.database.Router', ...]", + id="reactpy_django.E002", + ) + ) + + # All settings in reactpy_django.conf are the correct data type + if not isinstance(getattr(settings, "REACTPY_WEBSOCKET_URL", ""), str): + errors.append( + Error( + "Invalid type for REACTPY_WEBSOCKET_URL.", + hint="REACTPY_WEBSOCKET_URL should be a string.", + obj=settings.REACTPY_WEBSOCKET_URL, + id="reactpy_django.E003", + ) + ) + if not isinstance(getattr(settings, "REACTPY_RECONNECT_MAX", 0), int): + errors.append( + Error( + "Invalid type for REACTPY_RECONNECT_MAX.", + hint="REACTPY_RECONNECT_MAX should be an integer.", + obj=settings.REACTPY_RECONNECT_MAX, + id="reactpy_django.E004", + ) + ) + if not isinstance(getattr(settings, "REACTPY_CACHE", ""), str): + errors.append( + Error( + "Invalid type for REACTPY_CACHE.", + hint="REACTPY_CACHE should be a string.", + obj=settings.REACTPY_CACHE, + id="reactpy_django.E005", + ) + ) + if not isinstance(getattr(settings, "REACTPY_DATABASE", ""), str): + errors.append( + Error( + "Invalid type for REACTPY_DATABASE.", + hint="REACTPY_DATABASE should be a string.", + obj=settings.REACTPY_DATABASE, + id="reactpy_django.E006", + ) + ) + if not isinstance( + getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), str + ): + errors.append( + Error( + "Invalid type for REACTPY_DEFAULT_QUERY_POSTPROCESSOR.", + hint="REACTPY_DEFAULT_QUERY_POSTPROCESSOR should be a string.", + obj=settings.REACTPY_DEFAULT_QUERY_POSTPROCESSOR, + id="reactpy_django.E007", + ) + ) + if not isinstance(getattr(settings, "REACTPY_AUTH_BACKEND", ""), str): + errors.append( + Error( + "Invalid type for REACTPY_AUTH_BACKEND.", + hint="REACTPY_AUTH_BACKEND should be a string.", + obj=settings.REACTPY_AUTH_BACKEND, + id="reactpy_django.E008", + ) + ) + + return errors diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 57f4dfb9..c0f444c1 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -41,13 +41,16 @@ "REACTPY_DATABASE", DEFAULT_DB_ALIAS, ) +_default_query_postprocessor = getattr( + settings, + "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", + None, +) REACTPY_DEFAULT_QUERY_POSTPROCESSOR: AsyncPostprocessor | SyncPostprocessor | None = ( import_dotted_path( - getattr( - settings, - "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", - "reactpy_django.utils.django_query_postprocessor", - ) + _default_query_postprocessor + if isinstance(_default_query_postprocessor, str) + else "reactpy_django.utils.django_query_postprocessor", ) ) REACTPY_AUTH_BACKEND: str | None = getattr( diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 285d0244..abcb7d89 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -38,7 +38,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "channels", # Websocket library - "reactpy_django", # Django compatible ReactPy client + "reactpy_django", # Django compatiblity layer for ReactPy "test_app", # This test application ] MIDDLEWARE = [ From 2d5e4dc2b520ff3cd48c23274a4e4adb06074f94 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 23:03:52 -0700 Subject: [PATCH 08/13] fix style --- src/reactpy_django/checks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index d6ce4774..3feb4f18 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -6,7 +6,7 @@ def reactpy_warnings(app_configs, **kwargs): from django.conf import settings from django.urls import reverse - warnings: list[Error | Warning] = [] + warnings = [] # REACTPY_DATABASE is not an in-memory database. if ( @@ -63,7 +63,7 @@ def reactpy_warnings(app_configs, **kwargs): def reactpy_errors(app_configs, **kwargs): from django.conf import settings - errors: list[Error | Warning] = [] + errors = [] # Make sure ASGI is enabled if not getattr(settings, "ASGI_APPLICATION", None): From 537da2e168fbddc881d99c52f083db3bab7bfc7e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 23:07:54 -0700 Subject: [PATCH 09/13] update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10bb1d3f..5d3ae689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,13 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing yet! +### Changed + +- If using `settings.py:REACTPY_DATABASE`, `reactpy_django.database.Router` must now be registered in `settings.py:DATABASE_ROUTERS`. + +### Added + +- Added system checks for a variety of common ReactPy misconfigurations. ## [3.2.1] - 2023-06-29 From 89e9ae7e3450fa495f4ccab73dcd7f185fc1695a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 8 Jul 2023 23:14:47 -0700 Subject: [PATCH 10/13] fix dictionary --- docs/src/dictionary.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index d4f5b4b8..7f57e191 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -35,3 +35,5 @@ backends backend frontend frontends +misconfiguration +misconfigurations From d2927af66bf2ca46f298db762af7d06d34532f0b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 9 Jul 2023 01:56:40 -0700 Subject: [PATCH 11/13] remove unneeded comment due to new checks --- docs/python/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/python/settings.py b/docs/python/settings.py index 5bcdb50e..ffa5ea39 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -5,7 +5,6 @@ # Database ReactPy uses to store session data. # ReactPy requires a multiprocessing-safe and thread-safe database. -# Do NOT use an in-memory database. # DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured. REACTPY_DATABASE = "default" DATABASE_ROUTERS = ["reactpy_django.database.Router", ...] From 4728e6495643837882c538d70c7e6313fd7e9b12 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 9 Jul 2023 21:37:51 -0700 Subject: [PATCH 12/13] update comment --- src/reactpy_django/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index c0f444c1..2b756154 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -14,7 +14,7 @@ from reactpy_django.utils import import_dotted_path -# Non-user configurable settings +# Non-configurable values REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {} REACTPY_VIEW_COMPONENT_IFRAMES: dict[str, ViewComponentIframe] = {} From 599c98df3c5cb341340a2590793cd061e8b8d24a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 4 Aug 2023 00:20:11 -0700 Subject: [PATCH 13/13] Remove chromedriver --- .github/workflows/test-src.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index fda11d5d..f945ea9d 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -18,10 +18,9 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - - uses: nanasess/setup-chromedriver@master - uses: actions/setup-node@v3 with: - node-version: "14" + node-version: "14.x" - name: Use Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: