mirror of
https://github.com/Textualize/textual-web.git
synced 2025-10-17 02:36:40 +03:00
signup form
This commit is contained in:
131
poetry.lock
generated
131
poetry.lock
generated
@@ -158,6 +158,29 @@ files = [
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.7.1"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
|
||||
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.3"
|
||||
@@ -207,6 +230,18 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
|
||||
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
||||
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2023.7.22"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
|
||||
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.2.0"
|
||||
@@ -320,6 +355,21 @@ files = [
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.3"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
|
||||
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.3.3"
|
||||
@@ -404,6 +454,67 @@ files = [
|
||||
{file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "0.17.3"
|
||||
description = "A minimal low-level HTTP client."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
|
||||
{file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0,<5.0"
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
sniffio = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.24.1"
|
||||
description = "The next generation HTTP client."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
|
||||
{file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
httpcore = ">=0.15.0,<0.18.0"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
@@ -922,16 +1033,28 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
|
||||
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "0.32.0"
|
||||
version = "0.33.0"
|
||||
description = "Modern Text User Interface framework"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "textual-0.32.0-py3-none-any.whl", hash = "sha256:81fc68406c8806bc864e2f035874a868b4ff0cf466289dce5f7b31869949383b"},
|
||||
{file = "textual-0.32.0.tar.gz", hash = "sha256:f7b6683bc18faee6fd3c47cfbad43fbf8273c5fecc12230d52ce5ee089021327"},
|
||||
{file = "textual-0.33.0-py3-none-any.whl", hash = "sha256:698a093add0fd21c786232bcacde9ff427a9d5dc9ea5deca93437d9453e4ead1"},
|
||||
{file = "textual-0.33.0.tar.gz", hash = "sha256:e0a98b1d9c4458c5bb4269c65d0a7f3e926f197411242d2f8faf80183d47a728"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1132,4 +1255,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "770b4f563889262e7e305670bcd95cd81f574cdcccf9663b982dbee509c5b1b9"
|
||||
content-hash = "82e8c50344f547993eff43d20c1d7431707725ae6a564672e5aa252a5c6272ae"
|
||||
|
||||
@@ -18,6 +18,7 @@ pydantic = "^2.1.1"
|
||||
xdg = "^6.0.0"
|
||||
msgpack = "^1.0.5"
|
||||
importlib-metadata = ">=4.11.3"
|
||||
httpx = ">=0.24.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
import httpx
|
||||
from rich.console import RenderableType
|
||||
|
||||
|
||||
from textual import on
|
||||
from textual import work
|
||||
from textual.app import App, ComposeResult
|
||||
from textual import events
|
||||
from textual.containers import Vertical, Container
|
||||
@@ -21,9 +23,13 @@ class Form(Container):
|
||||
color: $text;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: $boost;
|
||||
|
||||
padding: 1 2;
|
||||
background: $boost;
|
||||
padding: 1 2;
|
||||
layout: grid;
|
||||
grid-size: 2;
|
||||
grid-columns: auto 50;
|
||||
grid_rows: auto;
|
||||
grid-gutter: 1;
|
||||
}
|
||||
|
||||
Form .title {
|
||||
@@ -31,12 +37,14 @@ class Form(Container):
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
margin-bottom: 2;
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
column-span: 2;
|
||||
}
|
||||
|
||||
Form Button {
|
||||
width: 100%;
|
||||
margin: 2 1 0 1;
|
||||
margin: 1 1 0 0;
|
||||
column-span: 2;
|
||||
}
|
||||
LoadingIndicator {
|
||||
width: 100%;
|
||||
@@ -46,67 +54,43 @@ class Form(Container):
|
||||
}
|
||||
Form:disabled Button {
|
||||
display: none;
|
||||
|
||||
}
|
||||
Form:disabled LoadingIndicator {
|
||||
display: block;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class FormField(Container):
|
||||
DEFAULT_CSS = """
|
||||
FormField {
|
||||
layout: horizontal;
|
||||
height: auto;
|
||||
width: 80;
|
||||
margin: 1 0;
|
||||
|
||||
}
|
||||
FormField Input {
|
||||
border: tall transparent;
|
||||
|
||||
}
|
||||
Form:disabled FormField Label{
|
||||
opacity:0.5;
|
||||
}
|
||||
FormField:focus-within > Label {
|
||||
color: $text;
|
||||
column-span: 2;
|
||||
}
|
||||
|
||||
FormField > Label {
|
||||
width: 1fr;
|
||||
margin: 1 2;
|
||||
text-align: right;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
FormField .group PasswordStrength {
|
||||
margin: 0 1 0 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
FormField > Input, FormField > Select {
|
||||
width: 2fr;
|
||||
}
|
||||
|
||||
|
||||
FormField Vertical.group {
|
||||
height: auto;
|
||||
width: 2fr;
|
||||
}
|
||||
|
||||
FormField Vertical.group Input {
|
||||
Form Label {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 1 0 0 1;
|
||||
}
|
||||
|
||||
Form .group {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Form .group > * {
|
||||
padding: 0 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
Form Input {
|
||||
border: tall transparent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PasswordStrength(Widget):
|
||||
DEFAULT_CSS = """
|
||||
PasswordStrength {
|
||||
margin: 1;
|
||||
PasswordStrength {
|
||||
height: 1;
|
||||
padding-left: 0;
|
||||
}
|
||||
PasswordStrength > .password-strength--highlight {
|
||||
color: $error;
|
||||
@@ -147,7 +131,7 @@ class PasswordStrength(Widget):
|
||||
highlight_style=highlight_style,
|
||||
)
|
||||
else:
|
||||
return "Minimum 8 character, no common words"
|
||||
return "Minimum 8 characters, no common words"
|
||||
|
||||
|
||||
def slugify(value: str, allow_unicode=False) -> str:
|
||||
@@ -170,39 +154,115 @@ def slugify(value: str, allow_unicode=False) -> str:
|
||||
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
||||
|
||||
|
||||
class SignupInput(Vertical):
|
||||
DEFAULT_CSS = """
|
||||
|
||||
SignupInput {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class ErrorLabel(Label):
|
||||
DEFAULT_CSS = """
|
||||
SignupScreen ErrorLabel {
|
||||
color: $error;
|
||||
text-align: left !important;
|
||||
padding-left: 1;
|
||||
padding-top: 0;
|
||||
display: none;
|
||||
}
|
||||
SignupScreen ErrorLabel.-show-error {
|
||||
display: block;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class SignupScreen(Screen):
|
||||
def compose(self) -> ComposeResult:
|
||||
with Form():
|
||||
yield Label("Textual-web Signup", classes="title")
|
||||
with FormField():
|
||||
yield Label("Your name*")
|
||||
yield Input(id="name")
|
||||
with FormField():
|
||||
yield Label("Account slug*")
|
||||
yield Input(id="org-name", placeholder="Identifier used in URLs")
|
||||
with FormField():
|
||||
yield Label("Email*")
|
||||
yield Input(id="email")
|
||||
with FormField():
|
||||
yield Label("Password*")
|
||||
|
||||
yield Label("Your name*")
|
||||
with SignupInput(id="name"):
|
||||
yield Input()
|
||||
yield ErrorLabel()
|
||||
|
||||
yield Label("Account slug*")
|
||||
with SignupInput(id="account_slug"):
|
||||
yield Input(placeholder="Identifier used in URLs")
|
||||
yield ErrorLabel()
|
||||
|
||||
yield Label("Email*")
|
||||
with SignupInput(id="email"):
|
||||
yield Input()
|
||||
yield ErrorLabel()
|
||||
|
||||
yield Label("Password*")
|
||||
with SignupInput(id="password"):
|
||||
with Vertical(classes="group"):
|
||||
yield Input(password=True, id="password")
|
||||
yield Input(password=True)
|
||||
yield PasswordStrength()
|
||||
with FormField():
|
||||
yield Label("Password (again)")
|
||||
yield Input(password=True, id="password-check")
|
||||
yield ErrorLabel()
|
||||
|
||||
yield Label("Password (again)")
|
||||
with SignupInput(id="password_check"):
|
||||
yield Input(password=True)
|
||||
yield ErrorLabel()
|
||||
|
||||
yield Button("Signup", variant="primary", id="signup")
|
||||
yield LoadingIndicator()
|
||||
|
||||
@on(Button.Pressed, "#signup")
|
||||
def signup(self):
|
||||
self.disabled = True
|
||||
data = {
|
||||
input.id: input.query_one(Input).value for input in self.query(SignupInput)
|
||||
}
|
||||
self.send_signup(data)
|
||||
|
||||
@on(Input.Changed, "#password")
|
||||
@work
|
||||
async def send_signup(self, data: dict[str, str]) -> None:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
"http://127.0.0.1:8080/api/signup/", data=data
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
except Exception as error:
|
||||
self.notify("Unable to reach server", severity="error")
|
||||
self.log(error)
|
||||
return
|
||||
finally:
|
||||
self.disabled = False
|
||||
|
||||
self.notify("There are errors in the form. Please try again.")
|
||||
self.query_one(Form).add_class("-show-errors")
|
||||
|
||||
for error_label in self.query(ErrorLabel):
|
||||
error_label.update("")
|
||||
error_label.remove_class("-show-error")
|
||||
|
||||
for error in result:
|
||||
if error["loc"]:
|
||||
error_label = self.query_one(
|
||||
f"#{error['loc'][0]} ErrorLabel", ErrorLabel
|
||||
)
|
||||
error_label.add_class("-show-error")
|
||||
error_label.update(error["ctx"].get("error", error["msg"]))
|
||||
else:
|
||||
self.notify(
|
||||
error["ctx"].get("error", error["msg"]), severity="error", timeout=5
|
||||
)
|
||||
self.app.log(result)
|
||||
|
||||
@on(Input.Changed, "#password Input")
|
||||
def input_changed(self, event: Input.Changed):
|
||||
self.query_one(PasswordStrength).password = event.input.value
|
||||
|
||||
@on(events.DescendantBlur, "#password-check")
|
||||
@on(events.DescendantBlur, "#password_check")
|
||||
def password_check(self, event: Input.Changed) -> None:
|
||||
password = self.query_one("#password", Input).value
|
||||
if password:
|
||||
@@ -210,12 +270,14 @@ class SignupScreen(Screen):
|
||||
if password != password_check:
|
||||
self.notify("Passwords do not match", severity="error")
|
||||
|
||||
@on(events.DescendantFocus, "#org-name")
|
||||
def update_org_name(self) -> None:
|
||||
org_name = self.query_one("#org-name", Input).value
|
||||
@on(events.DescendantFocus, "#account_slug Input")
|
||||
def update_account_slug(self) -> None:
|
||||
org_name = self.query_one("#account_slug Input", Input).value
|
||||
if not org_name:
|
||||
name = self.query_one("#name", Input).value
|
||||
self.query_one("#org-name", Input).insert_text_at_cursor(slugify(name))
|
||||
name = self.query_one("#name Input", Input).value
|
||||
self.query_one("#account_slug Input", Input).insert_text_at_cursor(
|
||||
slugify(name)
|
||||
)
|
||||
|
||||
|
||||
class SignUpApp(App):
|
||||
|
||||
@@ -8,6 +8,7 @@ class Environment:
|
||||
"""Data structure to describe the environment (dev, prod, local)."""
|
||||
|
||||
name: str
|
||||
api_url: str
|
||||
url: str
|
||||
|
||||
|
||||
@@ -18,10 +19,12 @@ ENVIRONMENTS = {
|
||||
# ),
|
||||
"local": Environment(
|
||||
name="local",
|
||||
api_url="ws://127.0.0.1:8080/api",
|
||||
url="ws://127.0.0.1:8080/app-service/",
|
||||
),
|
||||
"dev": Environment(
|
||||
name="dev",
|
||||
api_url="https://textualize-dev.io/api",
|
||||
url="wss://textualize-dev.io/app-service/",
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user