From 07240ea035e52a4543e88069821023d9a31a1b95 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 17 May 2021 15:51:41 -0700 Subject: [PATCH 01/21] django startproject --- .gitignore | 128 ++++++++++++++++++++++++++++++++++++++++++++ dj_idom/__init__.py | 0 dj_idom/asgi.py | 16 ++++++ dj_idom/settings.py | 125 ++++++++++++++++++++++++++++++++++++++++++ dj_idom/urls.py | 21 ++++++++ manage.py | 22 ++++++++ requirements.txt | 4 ++ 7 files changed, 316 insertions(+) create mode 100644 .gitignore create mode 100644 dj_idom/__init__.py create mode 100644 dj_idom/asgi.py create mode 100644 dj_idom/settings.py create mode 100644 dj_idom/urls.py create mode 100644 manage.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..03576fff --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ + +# Django # +logs +*.log +*.pot +*.pyc +.dccachea +__pycache__ +db.sqlite3 +media +cache +static-deploy +data +settings.json + +# Backup files # +*.bak + +# If you are using PyCharm # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/gradle.xml +.idea/**/libraries +*.iws /out/ + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode +.vscode/* +.vscode/settings.json +.vscode/tasks.json +.vscode/launch.json +.vscode/extensions.json +.history +%SystemDrive% + +# Mac file system +.DS_Store \ No newline at end of file diff --git a/dj_idom/__init__.py b/dj_idom/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dj_idom/asgi.py b/dj_idom/asgi.py new file mode 100644 index 00000000..0890d130 --- /dev/null +++ b/dj_idom/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for dj_idom project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj_idom.settings') + +application = get_asgi_application() diff --git a/dj_idom/settings.py b/dj_idom/settings.py new file mode 100644 index 00000000..088fc2d4 --- /dev/null +++ b/dj_idom/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for dj_idom project. + +Generated by 'django-admin startproject' using Django 3.2.3. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'dj_idom.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +ASGI_APPLICATION = 'dj_idom.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/dj_idom/urls.py b/dj_idom/urls.py new file mode 100644 index 00000000..05e85330 --- /dev/null +++ b/dj_idom/urls.py @@ -0,0 +1,21 @@ +"""dj_idom URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/manage.py b/manage.py new file mode 100644 index 00000000..f23117b6 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj_idom.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..23525b52 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +django<4.0.0 +daphne<4.0.0 +channels<4.0.0 +idom<1.0.0 \ No newline at end of file From 5a03f824fe365ca359397d2fcd0c9d8cfee02788 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 17 May 2021 22:33:32 -0700 Subject: [PATCH 02/21] barebones websocket consumer --- dj_idom/asgi.py | 23 +++++++++++++++++++++-- dj_idom/consumers.py | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 dj_idom/consumers.py diff --git a/dj_idom/asgi.py b/dj_idom/asgi.py index 0890d130..73b32f00 100644 --- a/dj_idom/asgi.py +++ b/dj_idom/asgi.py @@ -9,8 +9,27 @@ import os +from django.conf.urls import url from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj_idom.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dj_idom.settings") -application = get_asgi_application() +# Fetch ASGI application before importing dependencies that require ORM models. +http_asgi_app = get_asgi_application() + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator + +from .consumers import CommandConsumer + +application = ProtocolTypeRouter( + { + # ASGI app has concurrency problems, see + # See https://github.com/django/channels/issues/1587 + "http": http_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter([url("", CommandConsumer().as_asgi())])) + ), + } +) diff --git a/dj_idom/consumers.py b/dj_idom/consumers.py new file mode 100644 index 00000000..e2fab96e --- /dev/null +++ b/dj_idom/consumers.py @@ -0,0 +1,18 @@ +"""Anything used to construct a websocket endpoint""" +from channels.generic.websocket import AsyncJsonWebsocketConsumer + + +class CommandConsumer(AsyncJsonWebsocketConsumer): + """Websocket communication.""" + + # INITIAL CONNECTION + async def connect(self): + """When the browser attempts to connect to the server.""" + # Accept the connection + await self.accept() + pass + + # RECEIVING COMMANDS + async def receive_json(self, content, **kwargs): + """When the browser attempts to send a message to the server.""" + pass From 1591eb1a8b01711346d184dcea1605f91615aa8d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 17 May 2021 22:39:47 -0700 Subject: [PATCH 03/21] create barebones http page --- dj_idom/settings.py | 74 ++++++++++++++++++------------------- dj_idom/templates/base.html | 15 ++++++++ dj_idom/urls.py | 6 +-- dj_idom/views.py | 8 ++++ 4 files changed, 62 insertions(+), 41 deletions(-) create mode 100644 dj_idom/templates/base.html create mode 100644 dj_idom/views.py diff --git a/dj_idom/settings.py b/dj_idom/settings.py index 088fc2d4..d2753ceb 100644 --- a/dj_idom/settings.py +++ b/dj_idom/settings.py @@ -9,7 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ - +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c' +SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,52 +31,52 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'dj_idom.urls' +ROOT_URLCONF = "dj_idom.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "dj_idom", "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -ASGI_APPLICATION = 'dj_idom.asgi.application' +ASGI_APPLICATION = "dj_idom.asgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -86,16 +86,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -103,9 +103,9 @@ # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -117,9 +117,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/dj_idom/templates/base.html b/dj_idom/templates/base.html new file mode 100644 index 00000000..d50b7c5f --- /dev/null +++ b/dj_idom/templates/base.html @@ -0,0 +1,15 @@ + + + + + + + + IDOM + + + +

IDOM Test Page

+ + + \ No newline at end of file diff --git a/dj_idom/urls.py b/dj_idom/urls.py index 05e85330..8fdc66a8 100644 --- a/dj_idom/urls.py +++ b/dj_idom/urls.py @@ -13,9 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin from django.urls import path +from .views import base_template -urlpatterns = [ - path('admin/', admin.site.urls), -] +urlpatterns = [path("", base_template)] diff --git a/dj_idom/views.py b/dj_idom/views.py new file mode 100644 index 00000000..33775317 --- /dev/null +++ b/dj_idom/views.py @@ -0,0 +1,8 @@ +from django.template import loader +from django.http import HttpResponse + + +def base_template(request): + template = loader.get_template("base.html") + context = {} + return HttpResponse(template.render(context, request)) From 923276e7d43121708f7762ef8ac810f4e03d9400 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 17 May 2021 22:59:37 -0700 Subject: [PATCH 04/21] barebones unauthenticated websocket --- dj_idom/asgi.py | 8 +------- dj_idom/settings.py | 6 ++++++ dj_idom/static/scripts.js | 40 +++++++++++++++++++++++++++++++++++++ dj_idom/templates/base.html | 2 ++ requirements.txt | 8 ++++---- 5 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 dj_idom/static/scripts.js diff --git a/dj_idom/asgi.py b/dj_idom/asgi.py index 73b32f00..b3538e6e 100644 --- a/dj_idom/asgi.py +++ b/dj_idom/asgi.py @@ -17,19 +17,13 @@ # Fetch ASGI application before importing dependencies that require ORM models. http_asgi_app = get_asgi_application() -from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter -from channels.security.websocket import AllowedHostsOriginValidator from .consumers import CommandConsumer application = ProtocolTypeRouter( { - # ASGI app has concurrency problems, see - # See https://github.com/django/channels/issues/1587 "http": http_asgi_app, - "websocket": AllowedHostsOriginValidator( - AuthMiddlewareStack(URLRouter([url("", CommandConsumer().as_asgi())])) - ), + "websocket": URLRouter([url("", CommandConsumer().as_asgi())]), } ) diff --git a/dj_idom/settings.py b/dj_idom/settings.py index d2753ceb..456172e7 100644 --- a/dj_idom/settings.py +++ b/dj_idom/settings.py @@ -37,6 +37,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "channels", # Websocket library ] MIDDLEWARE = [ @@ -123,3 +124,8 @@ # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Static Files (CSS, JavaScript, Images) +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "dj_idom", "static"), +] diff --git a/dj_idom/static/scripts.js b/dj_idom/static/scripts.js new file mode 100644 index 00000000..e1b97b3c --- /dev/null +++ b/dj_idom/static/scripts.js @@ -0,0 +1,40 @@ +// Set up a websocket at the base endpoint +let LOCATION = window.location; +let WS_PROTOCOL = ""; +if (LOCATION.protocol == "https:") { + WS_PROTOCOL = "wss://"; +} else { + WS_PROTOCOL = "ws://"; +} +let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host; +let COMMAND_SOCKET = new WebSocket(WS_ENDPOINT_URL); + +// Receivable commands +COMMAND_SOCKET.onmessage = function (response) { + // Websocket message received, parse for JSON + console.info(response); + json_response = JSON.parse(response.data); + + // Check for valid commands + console.info("Websocket has recieved a message", json_response); +}; + +// Websocket open event +COMMAND_SOCKET.onopen = function () { + console.info("Websocket has opened."); +}; + +// Websocket close event +COMMAND_SOCKET.onclose = function () { + console.info("Websocket has closed."); +}; + +// Websocket error event +COMMAND_SOCKET.onerror = function (error) { + console.error( + "Websocket encountered a crtical error: ", + error.message, + "Closing socket..." + ); + COMMAND_SOCKET.close(); +}; diff --git a/dj_idom/templates/base.html b/dj_idom/templates/base.html index d50b7c5f..35157fac 100644 --- a/dj_idom/templates/base.html +++ b/dj_idom/templates/base.html @@ -1,3 +1,4 @@ +{% load static %} @@ -5,6 +6,7 @@ + IDOM diff --git a/requirements.txt b/requirements.txt index 23525b52..a9a1490d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django<4.0.0 -daphne<4.0.0 -channels<4.0.0 -idom<1.0.0 \ No newline at end of file +django<4.0.0 # Django Library +daphne<4.0.0 # Production ASGI webserver +channels<4.0.0 # Django websocket features +idom<1.0.0 # Python React \ No newline at end of file From 182cbd6eeaf46fa4309470e0e3f4b97e31cbd53d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 18 May 2021 00:21:45 -0700 Subject: [PATCH 05/21] remove unneeded pass --- dj_idom/consumers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dj_idom/consumers.py b/dj_idom/consumers.py index e2fab96e..2f429284 100644 --- a/dj_idom/consumers.py +++ b/dj_idom/consumers.py @@ -10,7 +10,6 @@ async def connect(self): """When the browser attempts to connect to the server.""" # Accept the connection await self.accept() - pass # RECEIVING COMMANDS async def receive_json(self, content, **kwargs): From 78369ea8799618d4cb93fc4f268af5ddfe9c541f Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 12 Jun 2021 13:59:00 -0700 Subject: [PATCH 06/21] organize for dist as pypi package --- dj_idom/consumers.py | 17 ---- noxfile.py | 60 ++++++++++++ pyproject.toml | 12 +++ requirements.txt | 8 +- requirements/check-style.txt | 3 + requirements/pkg-deps.txt | 2 + requirements/test-env.txt | 2 + requirements/test-run.txt | 1 + setup.cfg | 11 +++ setup.py | 96 ++++++++++++++++++++ src/django_idom/__init__.py | 5 + src/django_idom/websocket_consumer.py | 34 +++++++ manage.py => tests/manage.py | 4 +- {dj_idom => tests/tests}/__init__.py | 0 {dj_idom => tests/tests}/asgi.py | 12 ++- tests/tests/migrations/__init__.py | 0 {dj_idom => tests/tests}/settings.py | 10 +- {dj_idom => tests/tests}/static/scripts.js | 0 {dj_idom => tests/tests}/templates/base.html | 18 ++-- tests/tests/tests.py | 3 + {dj_idom => tests/tests}/urls.py | 6 +- {dj_idom => tests/tests}/views.py | 6 ++ 22 files changed, 267 insertions(+), 43 deletions(-) delete mode 100644 dj_idom/consumers.py create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements/check-style.txt create mode 100644 requirements/pkg-deps.txt create mode 100644 requirements/test-env.txt create mode 100644 requirements/test-run.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/django_idom/__init__.py create mode 100644 src/django_idom/websocket_consumer.py rename manage.py => tests/manage.py (85%) rename {dj_idom => tests/tests}/__init__.py (100%) rename {dj_idom => tests/tests}/asgi.py (64%) create mode 100644 tests/tests/migrations/__init__.py rename {dj_idom => tests/tests}/settings.py (93%) rename {dj_idom => tests/tests}/static/scripts.js (100%) rename {dj_idom => tests/tests}/templates/base.html (62%) create mode 100644 tests/tests/tests.py rename {dj_idom => tests/tests}/urls.py (95%) rename {dj_idom => tests/tests}/views.py (67%) diff --git a/dj_idom/consumers.py b/dj_idom/consumers.py deleted file mode 100644 index 2f429284..00000000 --- a/dj_idom/consumers.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Anything used to construct a websocket endpoint""" -from channels.generic.websocket import AsyncJsonWebsocketConsumer - - -class CommandConsumer(AsyncJsonWebsocketConsumer): - """Websocket communication.""" - - # INITIAL CONNECTION - async def connect(self): - """When the browser attempts to connect to the server.""" - # Accept the connection - await self.accept() - - # RECEIVING COMMANDS - async def receive_json(self, content, **kwargs): - """When the browser attempts to send a message to the server.""" - pass diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..d939f348 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path +from typing import List, Tuple + +import nox +from nox.sessions import Session + + +HERE = Path(__file__).parent +POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$") + + +@nox.session(reuse_venv=True) +def format(session: Session) -> None: + install_requirements_file(session, "check-style") + session.run("black", ".") + session.run("isort", ".") + + +@nox.session +def test(session: Session) -> None: + """Run the complete test suite""" + session.install("--upgrade", "pip", "setuptools", "wheel") + test_suite(session) + test_style(session) + + +@nox.session +def test_suite(session: Session) -> None: + """Run the Python-based test suite""" + session.env["IDOM_DEBUG_MODE"] = "1" + install_requirements_file(session, "test-env") + session.install(".[all]") + session.run("figure-it-out") + + +@nox.session +def test_style(session: Session) -> None: + """Check that style guidelines are being followed""" + install_requirements_file(session, "check-style") + session.run("flake8", "src/django_idom", "tests") + black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist" + session.run( + "black", + ".", + "--check", + "--exclude", + rf"/({black_default_exclude}|venv|node_modules)/", + ) + session.run("isort", ".", "--check-only") + + +def install_requirements_file(session: Session, name: str) -> None: + file_path = HERE / "requirements" / (name + ".txt") + assert file_path.exists(), f"requirements file {file_path} does not exist" + session.install("-r", str(file_path)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..18a77f5a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.isort] +multi_line_output = 3 +force_grid_wrap = 0 +use_parentheses = "True" +ensure_newline_before_comments = "True" +include_trailing_comma = "True" +line_length = 88 +lines_after_imports = 2 diff --git a/requirements.txt b/requirements.txt index a9a1490d..22728910 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django<4.0.0 # Django Library -daphne<4.0.0 # Production ASGI webserver -channels<4.0.0 # Django websocket features -idom<1.0.0 # Python React \ No newline at end of file +-r requirements/pkg-deps.txt +-r requirements/check-style.txt +-r requirements/test-env.txt +-r requirements/test-run.txt diff --git a/requirements/check-style.txt b/requirements/check-style.txt new file mode 100644 index 00000000..5e5647ac --- /dev/null +++ b/requirements/check-style.txt @@ -0,0 +1,3 @@ +black +flake8 +isort diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt new file mode 100644 index 00000000..b823d656 --- /dev/null +++ b/requirements/pkg-deps.txt @@ -0,0 +1,2 @@ +channels<4.0.0 # Django websocket features +idom<1.0.0 # Python React diff --git a/requirements/test-env.txt b/requirements/test-env.txt new file mode 100644 index 00000000..d32ec43a --- /dev/null +++ b/requirements/test-env.txt @@ -0,0 +1,2 @@ +django<4.0.0 # Django Library +daphne<4.0.0 # Production ASGI webserver diff --git a/requirements/test-run.txt b/requirements/test-run.txt new file mode 100644 index 00000000..816817c6 --- /dev/null +++ b/requirements/test-run.txt @@ -0,0 +1 @@ +nox diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..998619dd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +universal=1 + +[flake8] +ignore = E203, E266, E501, W503, F811, N802 +max-line-length = 88 +max-complexity = 18 +select = B,C,E,F,W,T4,B9,N,ROH +exclude = + .eggs/* + .nox/* diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..dbdbbf67 --- /dev/null +++ b/setup.py @@ -0,0 +1,96 @@ +import os +import sys +from pathlib import Path + +from setuptools import find_packages, setup + + +# the name of the project +name = "django_idom" + +# basic paths used to gather files +root_dir = Path(__file__).parent +src_dir = root_dir / "src" +package_dir = src_dir / name + + +# ----------------------------------------------------------------------------- +# Package Definition +# ----------------------------------------------------------------------------- + + +package = { + "name": name, + "python_requires": ">=3.7", + "packages": find_packages(str(src_dir)), + "package_dir": {"": "src"}, + "description": "Control the web with Python", + "author": "Ryan Morshead", + "author_email": "ryan.morshead@gmail.com", + "url": "https://github.com/idom-team/django-idom", + "license": "MIT", + "platforms": "Linux, Mac OS X, Windows", + "keywords": ["interactive", "widgets", "DOM", "React"], + "zip_safe": False, + "classifiers": [ + "Framework :: Django", + "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Multimedia :: Graphics", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Environment :: Web Environment", + ], +} + + +# ----------------------------------------------------------------------------- +# Library Version +# ----------------------------------------------------------------------------- + +with open(os.path.join(package_dir, "__init__.py")) as f: + for line in f.read().split("\n"): + if line.startswith("__version__ = "): + package["version"] = eval(line.split("=", 1)[1]) + break + else: + print("No version found in %s/__init__.py" % package_dir) + sys.exit(1) + + +# ----------------------------------------------------------------------------- +# Requirements +# ----------------------------------------------------------------------------- + + +requirements = [] +with (root_dir / "requirements" / "pkg-deps.txt").open() as f: + for line in map(str.strip, f): + if not line.startswith("#"): + requirements.append(line) +package["install_requires"] = requirements + + +# ----------------------------------------------------------------------------- +# Library Description +# ----------------------------------------------------------------------------- + + +with (root_dir / "README.md").open() as f: + long_description = f.read() + +package["long_description"] = long_description +package["long_description_content_type"] = "text/markdown" + + +# ----------------------------------------------------------------------------- +# Install It +# ----------------------------------------------------------------------------- + + +if __name__ == "__main__": + setup(**package) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py new file mode 100644 index 00000000..b62571ae --- /dev/null +++ b/src/django_idom/__init__.py @@ -0,0 +1,5 @@ +__version__ = "0.0.1" + +from .websocket_consumer import IdomAsyncWebSocketConsumer + +__all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py new file mode 100644 index 00000000..e9a9e598 --- /dev/null +++ b/src/django_idom/websocket_consumer.py @@ -0,0 +1,34 @@ +"""Anything used to construct a websocket endpoint""" +import asyncio +from typing import Any + +from channels.generic.websocket import AsyncJsonWebsocketConsumer + +from idom.core.dispatcher import dispatch_single_view +from idom.core.component import ComponentConstructor + + +class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): + """Communicates with the browser to perform actions on-demand.""" + + def __init__( + self, component: ComponentConstructor, *args: Any, **kwargs: Any + ) -> None: + self._idom_component_constructor = component + super().__init__(*args, **kwargs) + + async def connect(self) -> None: + self._idom_recv_queue = recv_queue = asyncio.Queue() + self._idom_dispatcher_future = dispatch_single_view( + self._idom_component_constructor, + self.send_json, + recv_queue.get, + ) + + async def close(self, *args: Any, **kwargs: Any) -> None: + self._idom_dispatcher_future.cancel() + await asyncio.wait([self._idom_dispatcher_future]) + super().close(*args, **kwargs) + + async def receive_json(self, content: Any, **kwargs: Any) -> None: + await self._idom_recv_queue.put(content) diff --git a/manage.py b/tests/manage.py similarity index 85% rename from manage.py rename to tests/manage.py index f23117b6..b5361c64 100644 --- a/manage.py +++ b/tests/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj_idom.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/dj_idom/__init__.py b/tests/tests/__init__.py similarity index 100% rename from dj_idom/__init__.py rename to tests/tests/__init__.py diff --git a/dj_idom/asgi.py b/tests/tests/asgi.py similarity index 64% rename from dj_idom/asgi.py rename to tests/tests/asgi.py index b3538e6e..c007a784 100644 --- a/dj_idom/asgi.py +++ b/tests/tests/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for dj_idom project. +ASGI config for tests project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -12,18 +12,22 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dj_idom.settings") +from .views import HelloWorld + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") # Fetch ASGI application before importing dependencies that require ORM models. http_asgi_app = get_asgi_application() from channels.routing import ProtocolTypeRouter, URLRouter -from .consumers import CommandConsumer +from django_idom import IdomAsyncWebSocketConsumer application = ProtocolTypeRouter( { "http": http_asgi_app, - "websocket": URLRouter([url("", CommandConsumer().as_asgi())]), + "websocket": URLRouter( + [url("", IdomAsyncWebSocketConsumer.as_asgi(component=HelloWorld))] + ), } ) diff --git a/tests/tests/migrations/__init__.py b/tests/tests/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dj_idom/settings.py b/tests/tests/settings.py similarity index 93% rename from dj_idom/settings.py rename to tests/tests/settings.py index 456172e7..6620d1a4 100644 --- a/dj_idom/settings.py +++ b/tests/tests/settings.py @@ -1,5 +1,5 @@ """ -Django settings for dj_idom project. +Django settings for tests project. Generated by 'django-admin startproject' using Django 3.2.3. @@ -50,12 +50,12 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "dj_idom.urls" +ROOT_URLCONF = "tests.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "dj_idom", "templates")], + "DIRS": [os.path.join(BASE_DIR, "tests", "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -68,7 +68,7 @@ }, ] -ASGI_APPLICATION = "dj_idom.asgi.application" +ASGI_APPLICATION = "tests.asgi.application" # Database @@ -127,5 +127,5 @@ # Static Files (CSS, JavaScript, Images) STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "dj_idom", "static"), + os.path.join(BASE_DIR, "tests", "static"), ] diff --git a/dj_idom/static/scripts.js b/tests/tests/static/scripts.js similarity index 100% rename from dj_idom/static/scripts.js rename to tests/tests/static/scripts.js diff --git a/dj_idom/templates/base.html b/tests/tests/templates/base.html similarity index 62% rename from dj_idom/templates/base.html rename to tests/tests/templates/base.html index 35157fac..9e54666a 100644 --- a/dj_idom/templates/base.html +++ b/tests/tests/templates/base.html @@ -1,17 +1,15 @@ {% load static %} - - - - - + + + + IDOM - + - +

IDOM Test Page

- - - \ No newline at end of file + + diff --git a/tests/tests/tests.py b/tests/tests/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/tests/tests/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dj_idom/urls.py b/tests/tests/urls.py similarity index 95% rename from dj_idom/urls.py rename to tests/tests/urls.py index 8fdc66a8..23e1acf0 100644 --- a/dj_idom/urls.py +++ b/tests/tests/urls.py @@ -1,14 +1,18 @@ -"""dj_idom URL Configuration +"""tests URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.2/topics/http/urls/ + Examples: + Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') + Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') + Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) diff --git a/dj_idom/views.py b/tests/tests/views.py similarity index 67% rename from dj_idom/views.py rename to tests/tests/views.py index 33775317..50a4385b 100644 --- a/dj_idom/views.py +++ b/tests/tests/views.py @@ -1,3 +1,4 @@ +import idom from django.template import loader from django.http import HttpResponse @@ -6,3 +7,8 @@ def base_template(request): template = loader.get_template("base.html") context = {} return HttpResponse(template.render(context, request)) + + +@idom.component +def HelloWorld(): + return idom.html.h1({"id": "hello-world"}, "Hello World!") From 3cda81d230633b4d7e7d9e8440f9b61f368718dc Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 12 Jun 2021 15:17:09 -0700 Subject: [PATCH 07/21] convert to building standard js app --- .gitignore | 3 +- noxfile.py | 15 + requirements/test-env.txt | 3 +- src/django_idom/websocket_consumer.py | 19 +- tests/.gitignore | 1 + tests/js/.gitignore | 2 + tests/js/package-lock.json | 603 ++++++++++++++++++ tests/js/package.json | 23 + tests/js/rollup.config.js | 22 + tests/js/src/index.js | 13 + tests/manage.py | 2 +- tests/{tests => test_app}/__init__.py | 0 tests/{tests => test_app}/asgi.py | 4 +- .../management}/__init__.py | 0 .../test_app/management/commands/__init__.py | 0 .../test_app/management/commands/build_js.py | 14 + tests/test_app/migrations/__init__.py | 0 tests/{tests => test_app}/settings.py | 19 +- tests/test_app/static/favicon.ico | Bin 0 -> 4286 bytes tests/{tests => test_app}/templates/base.html | 8 +- tests/{tests => test_app}/tests.py | 0 tests/{tests => test_app}/urls.py | 2 +- tests/{tests => test_app}/views.py | 0 tests/tests/static/scripts.js | 40 -- 24 files changed, 733 insertions(+), 60 deletions(-) create mode 100644 tests/.gitignore create mode 100644 tests/js/.gitignore create mode 100644 tests/js/package-lock.json create mode 100644 tests/js/package.json create mode 100644 tests/js/rollup.config.js create mode 100644 tests/js/src/index.js rename tests/{tests => test_app}/__init__.py (100%) rename tests/{tests => test_app}/asgi.py (87%) rename tests/{tests/migrations => test_app/management}/__init__.py (100%) create mode 100644 tests/test_app/management/commands/__init__.py create mode 100644 tests/test_app/management/commands/build_js.py create mode 100644 tests/test_app/migrations/__init__.py rename tests/{tests => test_app}/settings.py (86%) create mode 100644 tests/test_app/static/favicon.ico rename tests/{tests => test_app}/templates/base.html (60%) rename tests/{tests => test_app}/tests.py (100%) rename tests/{tests => test_app}/urls.py (95%) rename tests/{tests => test_app}/views.py (100%) delete mode 100644 tests/tests/static/scripts.js diff --git a/.gitignore b/.gitignore index 03576fff..c46fef78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Django # logs *.log @@ -125,4 +124,4 @@ GitHub.sublime-settings %SystemDrive% # Mac file system -.DS_Store \ No newline at end of file +.DS_Store diff --git a/noxfile.py b/noxfile.py index d939f348..994975e1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,6 +14,20 @@ POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$") +@nox.session(reuse_venv=True) +def manage(session: Session) -> None: + session.install("-r", "requirements.txt") + session.install("idom[stable]") + session.install("-e", ".") + session.chdir("tests") + + build_js_on_commands = ["runserver"] + if set(session.posargs).intersection(build_js_on_commands): + session.run("python", "manage.py", "build_js") + + session.run("python", "manage.py", *session.posargs) + + @nox.session(reuse_venv=True) def format(session: Session) -> None: install_requirements_file(session, "check-style") @@ -35,6 +49,7 @@ def test_suite(session: Session) -> None: session.env["IDOM_DEBUG_MODE"] = "1" install_requirements_file(session, "test-env") session.install(".[all]") + session.chdir("tests") session.run("figure-it-out") diff --git a/requirements/test-env.txt b/requirements/test-env.txt index d32ec43a..d3e4ba56 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1,2 +1 @@ -django<4.0.0 # Django Library -daphne<4.0.0 # Production ASGI webserver +django diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py index e9a9e598..c29330b5 100644 --- a/src/django_idom/websocket_consumer.py +++ b/src/django_idom/websocket_consumer.py @@ -4,6 +4,7 @@ from channels.generic.websocket import AsyncJsonWebsocketConsumer +from idom.core.layout import Layout from idom.core.dispatcher import dispatch_single_view from idom.core.component import ComponentConstructor @@ -18,17 +19,23 @@ def __init__( super().__init__(*args, **kwargs) async def connect(self) -> None: + await super().connect() self._idom_recv_queue = recv_queue = asyncio.Queue() - self._idom_dispatcher_future = dispatch_single_view( - self._idom_component_constructor, - self.send_json, - recv_queue.get, + self._idom_dispatcher_future = asyncio.ensure_future( + dispatch_single_view( + Layout(self._idom_component_constructor()), + self.send_json, + recv_queue.get, + ) ) async def close(self, *args: Any, **kwargs: Any) -> None: - self._idom_dispatcher_future.cancel() + if self._idom_dispatcher_future.done(): + await self._idom_dispatcher_future + else: + self._idom_dispatcher_future.cancel() await asyncio.wait([self._idom_dispatcher_future]) - super().close(*args, **kwargs) + await super().close(*args, **kwargs) async def receive_json(self, content: Any, **kwargs: Any) -> None: await self._idom_recv_queue.put(content) diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..bf4a6d58 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +test_app/static/build.js diff --git a/tests/js/.gitignore b/tests/js/.gitignore new file mode 100644 index 00000000..5980a333 --- /dev/null +++ b/tests/js/.gitignore @@ -0,0 +1,2 @@ +# Javascript +node_modules diff --git a/tests/js/package-lock.json b/tests/js/package-lock.json new file mode 100644 index 00000000..6639bf96 --- /dev/null +++ b/tests/js/package-lock.json @@ -0,0 +1,603 @@ +{ + "name": "tests", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "dependencies": { + "idom-client-react": "^0.8.2" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } + }, + "node_modules/@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "node_modules/fast-json-patch": { + "version": "3.0.0-1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz", + "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/htm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz", + "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ==" + }, + "node_modules/idom-client-react": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz", + "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==", + "dependencies": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.51.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz", + "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.12.0" + } + }, + "node_modules/rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", + "dev": true, + "dependencies": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.11.0" + } + }, + "node_modules/rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", + "dev": true, + "dependencies": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + }, + "dependencies": { + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "fast-json-patch": { + "version": "3.0.0-1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz", + "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "htm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz", + "integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ==" + }, + "idom-client-react": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz", + "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==", + "requires": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "peer": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "peer": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.51.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz", + "integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==", + "dev": true, + "requires": { + "fsevents": "~2.3.1" + } + }, + "rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "dev": true, + "requires": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + } +} diff --git a/tests/js/package.json b/tests/js/package.json new file mode 100644 index 00000000..3844cf32 --- /dev/null +++ b/tests/js/package.json @@ -0,0 +1,23 @@ +{ + "name": "tests", + "version": "1.0.0", + "description": "test app for idom_django websocket server", + "main": "src/index.js", + "files": [ + "src/**/*.js" + ], + "scripts": { + "build": "rollup --config", + "format": "prettier --ignore-path .gitignore --write ." + }, + "dependencies": { + "idom-client-react": "^0.8.2" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } +} diff --git a/tests/js/rollup.config.js b/tests/js/rollup.config.js new file mode 100644 index 00000000..da897f27 --- /dev/null +++ b/tests/js/rollup.config.js @@ -0,0 +1,22 @@ +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; +import replace from "rollup-plugin-replace"; + +const { PRODUCTION } = process.env; + +export default { + input: "src/index.js", + output: { + file: "../test_app/static/build.js", + format: "esm", + }, + plugins: [ + resolve(), + commonjs(), + replace({ + "process.env.NODE_ENV": JSON.stringify( + PRODUCTION ? "production" : "development" + ), + }), + ], +}; diff --git a/tests/js/src/index.js b/tests/js/src/index.js new file mode 100644 index 00000000..613515dc --- /dev/null +++ b/tests/js/src/index.js @@ -0,0 +1,13 @@ +import { mountLayoutWithWebSocket } from "idom-client-react"; + +// Set up a websocket at the base endpoint +let LOCATION = window.location; +let WS_PROTOCOL = ""; +if (LOCATION.protocol == "https:") { + WS_PROTOCOL = "wss://"; +} else { + WS_PROTOCOL = "ws://"; +} +let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host; + +mountLayoutWithWebSocket(document.getElementById("mount"), WS_ENDPOINT_URL); diff --git a/tests/manage.py b/tests/manage.py index b5361c64..234cb618 100644 --- a/tests/manage.py +++ b/tests/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/tests/tests/__init__.py b/tests/test_app/__init__.py similarity index 100% rename from tests/tests/__init__.py rename to tests/test_app/__init__.py diff --git a/tests/tests/asgi.py b/tests/test_app/asgi.py similarity index 87% rename from tests/tests/asgi.py rename to tests/test_app/asgi.py index c007a784..0db87cf4 100644 --- a/tests/tests/asgi.py +++ b/tests/test_app/asgi.py @@ -1,5 +1,5 @@ """ -ASGI config for tests project. +ASGI config for test_app project. It exposes the ASGI callable as a module-level variable named ``application``. @@ -14,7 +14,7 @@ from .views import HelloWorld -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") # Fetch ASGI application before importing dependencies that require ORM models. http_asgi_app = get_asgi_application() diff --git a/tests/tests/migrations/__init__.py b/tests/test_app/management/__init__.py similarity index 100% rename from tests/tests/migrations/__init__.py rename to tests/test_app/management/__init__.py diff --git a/tests/test_app/management/commands/__init__.py b/tests/test_app/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py new file mode 100644 index 00000000..cb4fc433 --- /dev/null +++ b/tests/test_app/management/commands/build_js.py @@ -0,0 +1,14 @@ +import subprocess +from pathlib import Path +from django.core.management.base import BaseCommand + +HERE = Path(__file__).parent +JS_DIR = HERE.parent.parent.parent / "js" + + +class Command(BaseCommand): + help = "Build javascript source for test app" + + def handle(self, *args, **options): + subprocess.run(["npm", "install"], cwd=JS_DIR, check=True) + subprocess.run(["npm", "run", "build"], cwd=JS_DIR, check=True) diff --git a/tests/test_app/migrations/__init__.py b/tests/test_app/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tests/settings.py b/tests/test_app/settings.py similarity index 86% rename from tests/tests/settings.py rename to tests/test_app/settings.py index 6620d1a4..cb5c9a6e 100644 --- a/tests/tests/settings.py +++ b/tests/test_app/settings.py @@ -1,5 +1,5 @@ """ -Django settings for tests project. +Django settings for test_app project. Generated by 'django-admin startproject' using Django 3.2.3. @@ -38,6 +38,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "channels", # Websocket library + "test_app", # This test application ] MIDDLEWARE = [ @@ -50,12 +51,12 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "tests.urls" +ROOT_URLCONF = "test_app.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "tests", "templates")], + "DIRS": [os.path.join(BASE_DIR, "test_app", "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -68,7 +69,7 @@ }, ] -ASGI_APPLICATION = "tests.asgi.application" +ASGI_APPLICATION = "test_app.asgi.application" # Database @@ -125,7 +126,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +STATIC_ROOT = os.path.join(BASE_DIR, "static-deploy") + # Static Files (CSS, JavaScript, Images) STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "tests", "static"), + os.path.join(BASE_DIR, "test_app", "static"), +] + +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "npm.finders.NpmFinder", ] diff --git a/tests/test_app/static/favicon.ico b/tests/test_app/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7005d293835c93d500859a20ce3215bf6c9b39f2 GIT binary patch literal 4286 zcmeH~K~BRk5Jkr<2rYy&tl>DcCVMtT{-8jgnt#am@VNy7`bUdv~>l$Ed%0 z-B#y5l|C^b7Q`fVAGy`D&!G?fxX0IKvH9aG0;&7xxt_lf4*~A+wOLHRtxv3dYJFlo zRQzM}YxC9Pn7eON^Eo!PzU?Dj-@o - + IDOM

IDOM Test Page

+
+ diff --git a/tests/tests/tests.py b/tests/test_app/tests.py similarity index 100% rename from tests/tests/tests.py rename to tests/test_app/tests.py diff --git a/tests/tests/urls.py b/tests/test_app/urls.py similarity index 95% rename from tests/tests/urls.py rename to tests/test_app/urls.py index 23e1acf0..91dab28d 100644 --- a/tests/tests/urls.py +++ b/tests/test_app/urls.py @@ -1,4 +1,4 @@ -"""tests URL Configuration +"""test_app URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.2/topics/http/urls/ diff --git a/tests/tests/views.py b/tests/test_app/views.py similarity index 100% rename from tests/tests/views.py rename to tests/test_app/views.py diff --git a/tests/tests/static/scripts.js b/tests/tests/static/scripts.js deleted file mode 100644 index e1b97b3c..00000000 --- a/tests/tests/static/scripts.js +++ /dev/null @@ -1,40 +0,0 @@ -// Set up a websocket at the base endpoint -let LOCATION = window.location; -let WS_PROTOCOL = ""; -if (LOCATION.protocol == "https:") { - WS_PROTOCOL = "wss://"; -} else { - WS_PROTOCOL = "ws://"; -} -let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host; -let COMMAND_SOCKET = new WebSocket(WS_ENDPOINT_URL); - -// Receivable commands -COMMAND_SOCKET.onmessage = function (response) { - // Websocket message received, parse for JSON - console.info(response); - json_response = JSON.parse(response.data); - - // Check for valid commands - console.info("Websocket has recieved a message", json_response); -}; - -// Websocket open event -COMMAND_SOCKET.onopen = function () { - console.info("Websocket has opened."); -}; - -// Websocket close event -COMMAND_SOCKET.onclose = function () { - console.info("Websocket has closed."); -}; - -// Websocket error event -COMMAND_SOCKET.onerror = function (error) { - console.error( - "Websocket encountered a crtical error: ", - error.message, - "Closing socket..." - ); - COMMAND_SOCKET.close(); -}; From 7c76da0b21e63b65062a643fe83456dc900079e0 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 12 Jun 2021 17:08:08 -0700 Subject: [PATCH 08/21] setup CI --- .github/FUNDING.yml | 12 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++++++ .github/workflows/test.yml | 35 +++++++++++++++++++++++++++++++ .gitignore | 1 + noxfile.py | 4 ++-- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e01b3e62 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [rmorshea] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..037e3c2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://idom-docs.herokuapp.com/ + about: Refer to the documentation before starting a discussion + - name: Community Support + url: https://github.com/idom-team/idom/discussions + about: Report issues, request features, and ask questions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..aa9d2cc7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + test-python-versions: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v2 + - uses: nanasess/setup-chromedriver@master + - uses: actions/setup-node@v2-beta + with: + node-version: "14" + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python Dependencies + run: pip install -r requirements/test-run.txt + - name: Run Tests + run: | + npm install -g npm@v7.13.0 + nox -s test diff --git a/.gitignore b/.gitignore index c46fef78..434278f2 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache diff --git a/noxfile.py b/noxfile.py index 994975e1..3a7b9d81 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,8 +39,8 @@ def format(session: Session) -> None: def test(session: Session) -> None: """Run the complete test suite""" session.install("--upgrade", "pip", "setuptools", "wheel") - test_suite(session) - test_style(session) + session.notify("test_suite") + session.notify("test_style") @nox.session From edeadbe887c588a38b56fc1701836525e68b5d91 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 12 Jun 2021 20:15:13 -0700 Subject: [PATCH 09/21] minor fixes --- .github/workflows/test.yml | 3 +-- tests/test_app/settings.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa9d2cc7..2754574b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,10 @@ on: jobs: test-python-versions: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8, 3.9] - os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v2 - uses: nanasess/setup-chromedriver@master diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index cb5c9a6e..3eb5e546 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -136,5 +136,4 @@ STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", - "npm.finders.NpmFinder", ] From e994b71c878c84d8266a265e6d1b062e5add9165 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 12 Jun 2021 23:10:14 -0700 Subject: [PATCH 10/21] Update README.md --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2444be03..1170612a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # Django IDOM -Support for IDOM in Django + + Tests + + + Version Info + + + License: MIT + + +A package for building highly interactive user interfaces in pure Python inspred by +[ReactJS](https://reactjs.org/). + +**Be sure to [read the IDOM Documentation](https://idom-docs.herokuapp.com)!** + +If you have ideas or find a bug, be sure to post an +[issue](https://github.com/idom-team/django-idom/issues) +or create a +[pull request](https://github.com/idom-team/django-idom/pulls). Thanks in advance! + +

+ + Try it Now + Binder + +

+ +Click the badge above to get started! It will take you to a [Jupyter Notebooks](https://jupyter.org/) +hosted by [Binder](https://mybinder.org/) with some great examples. + +### Or Install it Now + +```bash +pip install django-idom +``` + +# Django Integration + +This version of IDOM can be directly integrated into Django. For example + +```python +# Example code goes here +``` + +For examples on how to use IDOM, [read the IDOM Documentation](https://idom-docs.herokuapp.com). From aa3670211e3c5f84343bdb36ce996518a8a65a70 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 13 Jun 2021 14:57:44 -0700 Subject: [PATCH 11/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1170612a..943a2aed 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ License: MIT -A package for building highly interactive user interfaces in pure Python inspred by +A package for building highly interactive user interfaces in pure Python inspired by [ReactJS](https://reactjs.org/). **Be sure to [read the IDOM Documentation](https://idom-docs.herokuapp.com)!** From b3e81d9ebe251736ff04ebc9c4528fca6126b488 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:31:28 -0700 Subject: [PATCH 12/21] use npm latest --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2754574b..5edd2881 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,5 +30,6 @@ jobs: run: pip install -r requirements/test-run.txt - name: Run Tests run: | - npm install -g npm@v7.13.0 + npm install -g npm@latest + npm --version nox -s test From 673cc07b08bfe4c4e13032e9a88170989423b1c9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:42:34 -0700 Subject: [PATCH 13/21] settings clean up --- tests/test_app/settings.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 3eb5e546..4b6aeeb4 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -15,7 +15,6 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ @@ -24,12 +23,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True - ALLOWED_HOSTS = [] - # Application definition - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -40,7 +36,6 @@ "channels", # Websocket library "test_app", # This test application ] - MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -50,9 +45,7 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] - ROOT_URLCONF = "test_app.urls" - TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -68,13 +61,10 @@ }, }, ] - ASGI_APPLICATION = "test_app.asgi.application" - # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases - DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -85,7 +75,6 @@ # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", @@ -101,38 +90,26 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ - LANGUAGE_CODE = "en-us" - TIME_ZONE = "UTC" - USE_I18N = True - USE_L10N = True - USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ - -STATIC_URL = "/static/" - # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - STATIC_ROOT = os.path.join(BASE_DIR, "static-deploy") # Static Files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ +STATIC_URL = "/static/" STATICFILES_DIRS = [ os.path.join(BASE_DIR, "test_app", "static"), ] - STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", From 3e454ec0a1fcc82db19fc10079f25732b3d829f8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:54:22 -0700 Subject: [PATCH 14/21] add src dir to system path --- tests/test_app/settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 4b6aeeb4..d6d3414b 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -10,10 +10,13 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os +import sys from pathlib import Path + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +SRC_DIR = BASE_DIR.parent / "src" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ @@ -62,6 +65,7 @@ }, ] ASGI_APPLICATION = "test_app.asgi.application" +sys.path.append(str(SRC_DIR)) # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases @@ -72,7 +76,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ From c31f4a0120fe94c08bf044f423627b057df8b989 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 14:21:41 -0700 Subject: [PATCH 15/21] fix flake8 errors --- tests/test_app/asgi.py | 6 ++++-- tests/test_app/tests.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index 0db87cf4..3f868302 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -12,16 +12,18 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application +from django_idom import IdomAsyncWebSocketConsumer # noqa: E402 + from .views import HelloWorld + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") # Fetch ASGI application before importing dependencies that require ORM models. http_asgi_app = get_asgi_application() -from channels.routing import ProtocolTypeRouter, URLRouter +from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 -from django_idom import IdomAsyncWebSocketConsumer application = ProtocolTypeRouter( { diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 7ce503c2..d11b46d0 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,3 +1,5 @@ from django.test import TestCase # Create your tests here. +class Temp(TestCase): + pass \ No newline at end of file From 1d05e90ff54c188219160f85ffea2552f36382b6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 14:23:38 -0700 Subject: [PATCH 16/21] more style cleanup --- tests/test_app/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index d11b46d0..1a839f8d 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -# Create your tests here. + class Temp(TestCase): - pass \ No newline at end of file + pass From 6b64aa234b528518f4fb09fe2ab5e8d38587b531 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 20 Jun 2021 14:29:04 -0700 Subject: [PATCH 17/21] style cleanup 3 --- src/django_idom/__init__.py | 4 ++-- src/django_idom/websocket_consumer.py | 5 ++--- tests/test_app/management/commands/build_js.py | 2 ++ tests/test_app/urls.py | 2 ++ tests/test_app/views.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index b62571ae..60932c7a 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,5 +1,5 @@ -__version__ = "0.0.1" - from .websocket_consumer import IdomAsyncWebSocketConsumer + +__version__ = "0.0.1" __all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py index c29330b5..a9fb13d4 100644 --- a/src/django_idom/websocket_consumer.py +++ b/src/django_idom/websocket_consumer.py @@ -3,10 +3,9 @@ from typing import Any from channels.generic.websocket import AsyncJsonWebsocketConsumer - -from idom.core.layout import Layout -from idom.core.dispatcher import dispatch_single_view from idom.core.component import ComponentConstructor +from idom.core.dispatcher import dispatch_single_view +from idom.core.layout import Layout class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py index cb4fc433..61af5ed9 100644 --- a/tests/test_app/management/commands/build_js.py +++ b/tests/test_app/management/commands/build_js.py @@ -1,7 +1,9 @@ import subprocess from pathlib import Path + from django.core.management.base import BaseCommand + HERE = Path(__file__).parent JS_DIR = HERE.parent.parent.parent / "js" diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index 91dab28d..4449e447 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -18,6 +18,8 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.urls import path + from .views import base_template + urlpatterns = [path("", base_template)] diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 50a4385b..ccc6560e 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,6 +1,6 @@ import idom -from django.template import loader from django.http import HttpResponse +from django.template import loader def base_template(request): From 54af7c5bbe890d9a66aefe121bdb5428f3d50c03 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Mon, 19 Jul 2021 22:36:54 -0700 Subject: [PATCH 18/21] add basic test of IDOM --- noxfile.py | 8 +++-- requirements/test-env.txt | 5 +++ src/django_idom/__init__.py | 1 + src/django_idom/websocket_consumer.py | 33 ++++++++++-------- tests/test_app/asgi.py | 6 ++-- .../test_app/management/commands/build_js.py | 2 ++ tests/test_app/settings.py | 8 +++-- tests/test_app/tests.py | 34 +++++++++++++++++-- tests/test_app/urls.py | 2 ++ tests/test_app/views.py | 22 +++++++++++- 10 files changed, 96 insertions(+), 25 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3a7b9d81..ced51466 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,11 +46,13 @@ def test(session: Session) -> None: @nox.session def test_suite(session: Session) -> None: """Run the Python-based test suite""" - session.env["IDOM_DEBUG_MODE"] = "1" install_requirements_file(session, "test-env") session.install(".[all]") - session.chdir("tests") - session.run("figure-it-out") + + session.chdir(HERE / "tests") + session.env["IDOM_DEBUG_MODE"] = "1" + session.run("python", "manage.py", "build_js") + session.run("python", "manage.py", "test") @nox.session diff --git a/requirements/test-env.txt b/requirements/test-env.txt index d3e4ba56..c100c316 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1 +1,6 @@ django +selenium + +# required due issue with channels: +# https://github.com/django/channels/issues/1639#issuecomment-817994671 +twisted<21 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index b62571ae..ccb3667f 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -2,4 +2,5 @@ from .websocket_consumer import IdomAsyncWebSocketConsumer + __all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py index c29330b5..2fae2869 100644 --- a/src/django_idom/websocket_consumer.py +++ b/src/django_idom/websocket_consumer.py @@ -3,10 +3,9 @@ from typing import Any from channels.generic.websocket import AsyncJsonWebsocketConsumer - -from idom.core.layout import Layout from idom.core.dispatcher import dispatch_single_view -from idom.core.component import ComponentConstructor +from idom.core.layout import Layout, LayoutEvent +from idom.core.proto import ComponentConstructor class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): @@ -20,22 +19,26 @@ def __init__( async def connect(self) -> None: await super().connect() - self._idom_recv_queue = recv_queue = asyncio.Queue() - self._idom_dispatcher_future = asyncio.ensure_future( - dispatch_single_view( - Layout(self._idom_component_constructor()), - self.send_json, - recv_queue.get, - ) - ) + self._idom_dispatcher_future = asyncio.ensure_future(self._run_dispatch_loop()) - async def close(self, *args: Any, **kwargs: Any) -> None: + async def disconnect(self, code: int) -> None: if self._idom_dispatcher_future.done(): await self._idom_dispatcher_future else: self._idom_dispatcher_future.cancel() - await asyncio.wait([self._idom_dispatcher_future]) - await super().close(*args, **kwargs) + await super().disconnect(code) async def receive_json(self, content: Any, **kwargs: Any) -> None: - await self._idom_recv_queue.put(content) + await self._idom_recv_queue.put(LayoutEvent(**content)) + + async def _run_dispatch_loop(self): + self._idom_recv_queue = recv_queue = asyncio.Queue() + try: + await dispatch_single_view( + Layout(self._idom_component_constructor()), + self.send_json, + recv_queue.get, + ) + except Exception: + await self.close() + raise diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index 0db87cf4..56726f27 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -12,7 +12,8 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application -from .views import HelloWorld +from .views import Root + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") @@ -23,11 +24,12 @@ from django_idom import IdomAsyncWebSocketConsumer + application = ProtocolTypeRouter( { "http": http_asgi_app, "websocket": URLRouter( - [url("", IdomAsyncWebSocketConsumer.as_asgi(component=HelloWorld))] + [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))] ), } ) diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py index cb4fc433..61af5ed9 100644 --- a/tests/test_app/management/commands/build_js.py +++ b/tests/test_app/management/commands/build_js.py @@ -1,7 +1,9 @@ import subprocess from pathlib import Path + from django.core.management.base import BaseCommand + HERE = Path(__file__).parent JS_DIR = HERE.parent.parent.parent / "js" diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 3eb5e546..67f93f40 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -12,6 +12,7 @@ import os from pathlib import Path + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -78,8 +79,11 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "TEST": { + "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"), + }, + }, } diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 7ce503c2..485349f0 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,3 +1,33 @@ -from django.test import TestCase +from channels.testing import ChannelsLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait -# Create your tests here. + +class TestIdomCapabilities(ChannelsLiveServerTestCase): + def setUp(self): + self.driver = make_driver(5, 5) + self.driver.get(self.live_server_url) + + def tearDown(self) -> None: + self.driver.quit() + + def wait_until(self, condition, timeout=5): + WebDriverWait(self.driver, timeout).until(lambda driver: condition()) + + def test_hello_world(self): + self.driver.find_element_by_id("hello-world") + + def test_counter(self): + button = self.driver.find_element_by_id("counter-inc") + count = self.driver.find_element_by_id("counter-num") + + for i in range(5): + self.wait_until(lambda: count.get_attribute("data-count") == str(i)) + button.click() + + +def make_driver(page_load_timeout, implicit_wait_timeout): + driver = webdriver.Chrome() + driver.set_page_load_timeout(page_load_timeout) + driver.implicitly_wait(implicit_wait_timeout) + return driver diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index 91dab28d..4449e447 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -18,6 +18,8 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.urls import path + from .views import base_template + urlpatterns = [path("", base_template)] diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 50a4385b..a996eb1e 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,6 +1,6 @@ import idom -from django.template import loader from django.http import HttpResponse +from django.template import loader def base_template(request): @@ -9,6 +9,26 @@ def base_template(request): return HttpResponse(template.render(context, request)) +@idom.component +def Root(): + return idom.html.div(HelloWorld(), Counter()) + + @idom.component def HelloWorld(): return idom.html.h1({"id": "hello-world"}, "Hello World!") + + +@idom.component +def Counter(): + count, set_count = idom.hooks.use_state(0) + return idom.html.div( + idom.html.button( + {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, + "Click me!", + ), + idom.html.p( + {"id": "counter-num", "data-count": count}, + f"Current count is: {count}", + ), + ) From 2091408a3740c1ea697653fe1b061fcc50c232c8 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:09:22 -0700 Subject: [PATCH 19/21] fix rollup warning for this --- tests/js/rollup.config.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/js/rollup.config.js b/tests/js/rollup.config.js index da897f27..ad597a61 100644 --- a/tests/js/rollup.config.js +++ b/tests/js/rollup.config.js @@ -19,4 +19,15 @@ export default { ), }), ], + onwarn: function (warning) { + // Skip certain warnings + + // should intercept ... but doesn't in some rollup versions + if (warning.code === "THIS_IS_UNDEFINED") { + return; + } + + // console.warn everything else + console.warn(warning.message); + }, }; From c377f3d63aa8060311935ff3fac87ebfce47ca78 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:13:37 -0700 Subject: [PATCH 20/21] fix style issues --- src/django_idom/__init__.py | 1 + tests/test_app/asgi.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 79adf834..60932c7a 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,4 +1,5 @@ from .websocket_consumer import IdomAsyncWebSocketConsumer + __version__ = "0.0.1" __all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index 906b0542..113b147f 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -12,10 +12,10 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application -from .views import Root - from django_idom import IdomAsyncWebSocketConsumer # noqa: E402 +from .views import Root + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") @@ -25,7 +25,6 @@ from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 - application = ProtocolTypeRouter( { "http": http_asgi_app, From 4e1a970281c0c6b54fc0288948fd7ee7c87444fa Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:27:29 -0700 Subject: [PATCH 21/21] allow tests to run in headless mode in CI --- .github/workflows/test.yml | 2 +- noxfile.py | 3 ++- tests/test_app/tests.py | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5edd2881..d3097b33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,4 +32,4 @@ jobs: run: | npm install -g npm@latest npm --version - nox -s test + nox -s test -- --headless diff --git a/noxfile.py b/noxfile.py index ced51466..4a943345 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,7 +39,7 @@ def format(session: Session) -> None: def test(session: Session) -> None: """Run the complete test suite""" session.install("--upgrade", "pip", "setuptools", "wheel") - session.notify("test_suite") + session.notify("test_suite", posargs=session.posargs) session.notify("test_style") @@ -51,6 +51,7 @@ def test_suite(session: Session) -> None: session.chdir(HERE / "tests") session.env["IDOM_DEBUG_MODE"] = "1" + session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs)) session.run("python", "manage.py", "build_js") session.run("python", "manage.py", "test") diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 485349f0..1a80ee5a 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,3 +1,5 @@ +import os + from channels.testing import ChannelsLiveServerTestCase from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait @@ -27,7 +29,9 @@ def test_counter(self): def make_driver(page_load_timeout, implicit_wait_timeout): - driver = webdriver.Chrome() + options = webdriver.ChromeOptions() + options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0))) + driver = webdriver.Chrome(options=options) driver.set_page_load_timeout(page_load_timeout) driver.implicitly_wait(implicit_wait_timeout) return driver