From ba83922660dade77dcb96e8ba9c73cfcba8c2b81 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 24 Jan 2025 03:28:43 +0100 Subject: [PATCH] Progress. --- .../prachtig-gitter_like.html | 0 .../prachtig_gitter_like.html | 0 Dockerfile | 3 +- LICENSE.txt | 21 ++ Makefile | 2 + compose.yml | 2 + pyproject.toml | 24 +- setup.cfg | 1 + src/snek/app.py | 79 +++-- src/snek/form/__init__.py | 0 src/snek/form/login.py | 24 ++ src/snek/form/register.py | 31 ++ src/snek/forms.py | 40 --- src/snek/gunicorn.py | 2 +- src/snek/model/__init__.py | 0 src/snek/model/user.py | 19 ++ src/snek/static/app.js | 15 +- src/snek/static/{styles.css => base.css} | 9 - src/snek/static/fancy-button.js | 54 +++ src/snek/static/generic-form.css | 100 ++++++ src/snek/static/generic-form.js | 321 ++++++++++++++++++ .../static/{html_frame.css => html-frame.css} | 3 - .../static/{html_frame.js => html-frame.js} | 15 +- .../static/{register.css => register__.css} | 24 +- src/snek/system/__init__.py | 0 src/snek/system/form.py | 171 ++++++++++ src/snek/{ => system}/http.py | 0 src/snek/{ => system}/middleware.py | 0 src/snek/{models.py => system/model.py} | 87 ++--- src/snek/templates/base.html | 31 ++ src/snek/templates/base_chat.html | 31 ++ src/snek/templates/index.html | 20 ++ src/snek/templates/login.html | 25 +- src/snek/templates/register.html | 27 +- src/snek/templates/{test.html => web.html} | 7 +- src/snek/view/base.py | 31 ++ src/snek/view/index.py | 6 + src/snek/view/login.py | 13 + src/snek/view/login_form.py | 5 + src/snek/view/register.py | 6 + src/snek/view/register_form.py | 5 + src/snek/view/view.py | 6 + 42 files changed, 1050 insertions(+), 210 deletions(-) rename {src/snek/static => .resources}/prachtig-gitter_like.html (100%) rename {src/snek/templates => .resources}/prachtig_gitter_like.html (100%) create mode 100644 LICENSE.txt create mode 100644 src/snek/form/__init__.py create mode 100644 src/snek/form/login.py create mode 100644 src/snek/form/register.py delete mode 100644 src/snek/forms.py create mode 100644 src/snek/model/__init__.py create mode 100644 src/snek/model/user.py rename src/snek/static/{styles.css => base.css} (94%) create mode 100644 src/snek/static/fancy-button.js create mode 100644 src/snek/static/generic-form.css create mode 100644 src/snek/static/generic-form.js rename src/snek/static/{html_frame.css => html-frame.css} (54%) rename src/snek/static/{html_frame.js => html-frame.js} (65%) rename src/snek/static/{register.css => register__.css} (74%) create mode 100644 src/snek/system/__init__.py create mode 100644 src/snek/system/form.py rename src/snek/{ => system}/http.py (100%) rename src/snek/{ => system}/middleware.py (100%) rename src/snek/{models.py => system/model.py} (78%) create mode 100644 src/snek/templates/base.html create mode 100644 src/snek/templates/base_chat.html create mode 100644 src/snek/templates/index.html rename src/snek/templates/{test.html => web.html} (92%) create mode 100644 src/snek/view/base.py create mode 100644 src/snek/view/index.py create mode 100644 src/snek/view/login.py create mode 100644 src/snek/view/login_form.py create mode 100644 src/snek/view/register.py create mode 100644 src/snek/view/register_form.py create mode 100644 src/snek/view/view.py diff --git a/src/snek/static/prachtig-gitter_like.html b/.resources/prachtig-gitter_like.html similarity index 100% rename from src/snek/static/prachtig-gitter_like.html rename to .resources/prachtig-gitter_like.html diff --git a/src/snek/templates/prachtig_gitter_like.html b/.resources/prachtig_gitter_like.html similarity index 100% rename from src/snek/templates/prachtig_gitter_like.html rename to .resources/prachtig_gitter_like.html diff --git a/Dockerfile b/Dockerfile index 76436ba..47c0ece 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,4 +37,5 @@ RUN pip install --upgrade pip RUN pip install -e . EXPOSE 8081 -CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"] +python -m snek.app +#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6b0da88 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 retoor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 65c58fa..f153fc1 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ install: python3 -m venv .venv $(PIP) install -e . + + run: $(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload diff --git a/compose.yml b/compose.yml index 776e60d..3b1f650 100644 --- a/compose.yml +++ b/compose.yml @@ -6,3 +6,5 @@ services: - "8081:8081" volumes: - ./:/code + entrypoint: ["python","-m","snek.app"] + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 07de284..d98557e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,25 @@ [build-system] requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[project] +name = "Snek" +version = "1.0.0" +readme = "README.md" +license = { file = "LICENSE", content-type="text/markdown" } +description = "Snek Chat Application by Molodetz" +authors = [ + { name = "retoor", email = "retoor@molodetz.nl" } +] +keywords = ["chat", "snek", "molodetz"] +requires-python = ">=3.12" +dependencies = [ + "mkdocs>=1.4.0", + "shed", + "app @ git+https://retoor.molodetz.nl/retoor/app", + "beautifulsoup4", + "gunicorn", + "imgkit", + "wkhtmltopdf" +] + diff --git a/setup.cfg b/setup.cfg index ca8353d..045fc92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ install_requires = gunicorn imgkit wkhtmltopdf + shed [options.packages.find] where = src diff --git a/src/snek/app.py b/src/snek/app.py index 97fbb96..0e2ed63 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -1,10 +1,16 @@ -from app.app import Application as BaseApplication -from snek.forms import RegisterForm -from aiohttp import web -import aiohttp import pathlib -from snek import http -from snek.middleware import cors_allow_middleware,cors_middleware + +from aiohttp import web +from app.app import Application as BaseApplication + +from snek.system import http +from snek.system.middleware import cors_middleware +from snek.view.index import IndexView +from snek.view.login import LoginView +from snek.view.login_form import LoginFormView +from snek.view.register import RegisterView +from snek.view.register_form import RegisterFormView +from snek.view.view import WebView class Application(BaseApplication): @@ -12,23 +18,38 @@ class Application(BaseApplication): def __init__(self, *args, **kwargs): middlewares = [ cors_middleware, - web.normalize_path_middleware(merge_slashes=True) + web.normalize_path_middleware(merge_slashes=True), ] self.template_path = pathlib.Path(__file__).parent.joinpath("templates") - super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs) - self.router.add_static("/",pathlib.Path(__file__).parent.joinpath("static"),name="static",show_index=True) - self.router.add_get("/register", self.handle_register) - self.router.add_get("/login", self.handle_login) - self.router.add_get("/test", self.handle_test) - self.router.add_post("/register", self.handle_register) - self.router.add_get("/http-get",self.handle_http_get) - self.router.add_get("/http-photo",self.handle_http_photo) + super().__init__( + middlewares=middlewares, template_path=self.template_path, *args, **kwargs + ) + self.setup_router() - async def handle_test(self,request): + def setup_router(self): + self.router.add_get("/", IndexView) + self.router.add_static( + "/", + pathlib.Path(__file__).parent.joinpath("static"), + name="static", + show_index=True, + ) + self.router.add_view("/web", WebView) + self.router.add_view("/login", LoginView) + self.router.add_view("/login-form", LoginFormView) + self.router.add_view("/register", RegisterView) + + self.router.add_view("/register-form", RegisterFormView) + self.router.add_get("/http-get", self.handle_http_get) + self.router.add_get("/http-photo", self.handle_http_photo) - return await self.render_template("test.html",request,context={"name":"retoor"}) + async def handle_test(self, request): - async def handle_http_get(self, request:web.Request): + return await self.render_template( + "test.html", request, context={"name": "retoor"} + ) + + async def handle_http_get(self, request: web.Request): url = request.query.get("url") content = await http.get(url) return web.Response(body=content) @@ -36,25 +57,13 @@ class Application(BaseApplication): async def handle_http_photo(self, request): url = request.query.get("url") path = await http.create_site_photo(url) - return web.Response(body=path.read_bytes(),headers={ - "Content-Type": "image/png" - }) + return web.Response( + body=path.read_bytes(), headers={"Content-Type": "image/png"} + ) - async def handle_login(self, request): - if request.method == "GET": - return await self.render_template("login.html", request) #web.json_response({"form": RegisterForm().to_json()}) - elif request.method == "POST": - return await self.render_template("login.html", request) #web.json_response({"form": RegisterForm().to_json()}) - - - async def handle_register(self, request): - if request.method == "GET": - return await self.render_template("register.html", request) #web.json_response({"form": RegisterForm().to_json()}) - elif request.method == "POST": - return self.render("register.html") app = Application() -if __name__ == '__main__': +if __name__ == "__main__": - web.run_app(app,port=8081,host="0.0.0.0") + web.run_app(app, port=8081, host="0.0.0.0") diff --git a/src/snek/form/__init__.py b/src/snek/form/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/snek/form/login.py b/src/snek/form/login.py new file mode 100644 index 0000000..a87f7d3 --- /dev/null +++ b/src/snek/form/login.py @@ -0,0 +1,24 @@ +from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement + +class LoginForm(Form): + + title = HTMLElement(tag="h1", text="Login") + + username = FormInputElement( + name="username", + required=True, + min_length=2, + max_length=20, + regex=r"^[a-zA-Z0-9_]+$", + place_holder="Username", + type="text" + ) + password = FormInputElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}",type="password",place_holder="Password") + + action = FormButtonElement( + name="action", + value="submit", + text="Login", + type="button" + ) + diff --git a/src/snek/form/register.py b/src/snek/form/register.py new file mode 100644 index 0000000..60399fb --- /dev/null +++ b/src/snek/form/register.py @@ -0,0 +1,31 @@ +from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement + +class RegisterForm(Form): + + title = HTMLElement(tag="h1", text="Register") + + username = FormInputElement( + name="username", + required=True, + min_length=2, + max_length=20, + regex=r"^[a-zA-Z0-9_]+$", + place_holder="Username", + type="text" + ) + email = FormInputElement( + name="email", + required=True, + regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", + place_holder="Email address", + type="email" + ) + password = FormInputElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}",type="password",place_holder="Password") + + action = FormButtonElement( + name="action", + value="submit", + text="Register", + type="button" + ) + diff --git a/src/snek/forms.py b/src/snek/forms.py deleted file mode 100644 index f4a7b24..0000000 --- a/src/snek/forms.py +++ /dev/null @@ -1,40 +0,0 @@ -from snek import models - -class FormElement(models.ModelField): - - def __init__(self,html_type, place_holder=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.place_holder = place_holder - self.html_type = html_type - - def to_json(self): - data = super().to_json() - data["html_type"] = self.html_type - data["place_holder"] = self.place_holder - return data - -class Form(models.BaseModel): - pass - -class RegisterForm(Form): - - username = FormElement( - name="username", - required=True, - min_length=2, - max_length=20, - regex=r"^[a-zA-Z0-9_]+$", - place_holder="Username", - html_type="text" - ) - email = FormElement( - name="email", - required=True, - regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", - place_holder="Email address", - html_type="email" - ) - password = FormElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",html_type="password") - - - diff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py index 4055bbd..8583142 100644 --- a/src/snek/gunicorn.py +++ b/src/snek/gunicorn.py @@ -1,3 +1,3 @@ -from snek.app import app +from snek.app import app application = app diff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/snek/model/user.py b/src/snek/model/user.py new file mode 100644 index 0000000..44553f8 --- /dev/null +++ b/src/snek/model/user.py @@ -0,0 +1,19 @@ +from snek.system.model import BaseModel,ModelField + +class User(BaseModel): + + username = ModelField( + name="username", + required=True, + min_length=2, + max_length=20, + regex=r"^[a-zA-Z0-9_]+$", + ) + email = ModelField( + name="email", + required=True, + regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" + ) + password = ModelField(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}") + + diff --git a/src/snek/static/app.js b/src/snek/static/app.js index 701beda..f06ee24 100644 --- a/src/snek/static/app.js +++ b/src/snek/static/app.js @@ -57,13 +57,26 @@ class Room { this.name = name } setMessages(list){ - + } } +class InlineAppElement extends HTMLElement { + + constructor(){ + this. + } + +} + +class Page { + elements = [] + +} + class App { rooms = [] constructor() { diff --git a/src/snek/static/styles.css b/src/snek/static/base.css similarity index 94% rename from src/snek/static/styles.css rename to src/snek/static/base.css index 83c1fb1..363e4f1 100644 --- a/src/snek/static/styles.css +++ b/src/snek/static/base.css @@ -1,11 +1,9 @@ -/* General Reset */ * { margin: 0; padding: 0; box-sizing: border-box; } -/* Body Styling */ body { font-family: Arial, sans-serif; background-color: #1a1a1a; @@ -16,7 +14,6 @@ body { height: 100vh; } -/* Header Navigation */ header { background-color: #0f0f0f; padding: 10px 20px; @@ -43,14 +40,12 @@ header nav a:hover { color: #fff; } -/* Main Layout */ main { display: flex; flex: 1; overflow: hidden; } -/* Sidebar */ .sidebar { width: 250px; background-color: #121212; @@ -84,7 +79,6 @@ main { color: #fff; } -/* Chat Area */ .chat-area { flex: 1; display: flex; @@ -103,7 +97,6 @@ main { color: #fff; } -/* Chat Messages */ .chat-messages { flex: 1; padding: 20px; @@ -155,7 +148,6 @@ main { color: #aaa; } -/* Input Area */ .chat-input { padding: 15px; background-color: #121212; @@ -190,7 +182,6 @@ main { background-color: #e04924; } -/* Responsive Adjustments */ @media (max-width: 768px) { .sidebar { display: none; diff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js new file mode 100644 index 0000000..5407a3b --- /dev/null +++ b/src/snek/static/fancy-button.js @@ -0,0 +1,54 @@ + + +class FancyButton extends HTMLElement { + url = null + type="button" + value = null + constructor(){ + super() + this.attachShadow({mode:'open'}) + this.container = document.createElement('span') + this.styleElement = document.createElement("style") + this.styleElement.innerHTML = ` + :root { + width:100%; + --width: 100%; + } + button { + width: var(--width); + min-width: 33%; + padding: 10px; + background-color: #f05a28; + border: none; + border-radius: 5px; + color: white; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; + } + button:hover { + color: #EFEFEF; + background-color: #e04924; + } + ` + this.container.appendChild(this.styleElement) + this.buttonElement = document.createElement('button') + this.container.appendChild(this.buttonElement) + this.shadowRoot.appendChild(this.container) + } + + connectedCallback() { + this.url = this.getAttribute('url'); + this.value = this.getAttribute('value') + const me = this + this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text"))) + this.buttonElement.addEventListener("click",()=>{ + if(me.url){ + window.location = me.url + } + }) + } +} + +customElements.define("fancy-button",FancyButton) diff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css new file mode 100644 index 0000000..d593816 --- /dev/null +++ b/src/snek/static/generic-form.css @@ -0,0 +1,100 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: Arial, sans-serif; + background-color: #1a1a1a; + color: #e6e6e6; + line-height: 1.5; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + } + +generic-form { + margin: 0; + padding: 0; + box-sizing: border-box; + background-color: #000000; + +} + +.generic-form-container { + + background-color: #0f0f0f; + border-radius: 10px; + padding: 30px; + width: 400px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + text-align: center; + +} + +.generic-form-container h1 { + font-size: 2em; + color: #f05a28; + margin-bottom: 20px; +} +input { + + border: 10px solid #000000; +} +.generic-form-container generic-field { + width: 100%; + padding: 10px; + margin: 10px 0; + border: 1px solid #333; + border-radius: 5px; + background-color: #1a1a1a; + color: #e6e6e6; + font-size: 1em; +} + +.generic-form-container button { + width: 100%; + padding: 10px; + background-color: #f05a28; + border: none; + border-radius: 5px; + color: white; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; +} + +.generic-form-container button:hover { + background-color: #e04924; +} + +.generic-form-container a { + color: #f05a28; + text-decoration: none; + display: block; + margin-top: 15px; + font-size: 0.9em; + transition: color 0.3s; +} + +.generic-form-container a:hover { + color: #e04924; +} + + +.error { + color: #d8000c; + font-size: 0.9em; + margin-top: 5px; +} + + +@media (max-width: 500px) { + .generic-form-container { + width: 90%; + } +} \ No newline at end of file diff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js new file mode 100644 index 0000000..11fea47 --- /dev/null +++ b/src/snek/static/generic-form.js @@ -0,0 +1,321 @@ + +class GenericField extends HTMLElement { + form = null + field = null + inputElement = null + footerElement = null + action = null + container = null + styleElement = null + name = null + get value() { + return this.inputElement.value + } + get type() { + + return this.field.tag + } + set value(val) { + val = val == null ? '' : val + this.inputElement.value = val + this.inputElement.setAttribute("value", val) + } + setInvalid(){ + this.inputElement.classList.add("error") + this.inputElement.classList.remove("valid") + } + setErrors(errors){ + if(errors.length) + this.inputElement.setAttribute("title", errors[0]) + else + this.inputElement.setAttribute("title","") + } + setValid(){ + this.inputElement.classList.remove("error") + this.inputElement.classList.add("valid") + } + constructor() { + super() + this.attachShadow({mode:'open'}) + this.container = document.createElement('div') + this.styleElement = document.createElement('style') + this.styleElement.innerHTML = ` + + h1 { + font-size: 2em; + color: #f05a28; + margin-bottom: 20px; + margin-top: 0px; + } + + input { + width: 90%; + padding: 10px; + margin: 10px 0; + border: 1px solid #333; + border-radius: 5px; + background-color: #1a1a1a; + color: #e6e6e6; + font-size: 1em; + } + + button { + width: 50%; + padding: 10px; + background-color: #f05a28; + border: none; + float: right; + margin-top: 10px; + margin-left: 10px; + margin-right: 10px; + border-radius: 5px; + color: white; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; + clear: both; + } + + button:hover { + background-color: #e04924; + } + + a { + color: #f05a28; + text-decoration: none; + display: block; + margin-top: 15px; + font-size: 0.9em; + transition: color 0.3s; + } + + a:hover { + color: #e04924; + } + .valid { + border: 1px solid green; + color:green; + font-size: 0.9em; + margin-top: 5px; + } + .error { + border: 3px solid red; + color: #d8000c; + font-size: 0.9em; + margin-top: 5px; + } + @media (max-width: 500px) { + input { + width: 90%; + } + } + + ` + this.container.appendChild(this.styleElement) + + this.shadowRoot.appendChild(this.container) + } + connectedCallback(){ + + this.updateAttributes() + + } + setAttribute(name,value){ + this[name] = value + } + updateAttributes(){ + if(this.inputElement == null && this.field){ + this.inputElement = document.createElement(this.field.tag) + if(this.field.tag == 'button'){ + if(this.field.value == "submit"){ + + + } + this.action = this.field.value + } + this.inputElement.name = this.field.name + this.name = this.inputElement.name + const me = this + this.inputElement.addEventListener("keyup",(e)=>{ + if(e.key == 'Enter'){ + me.dispatchEvent(new Event("submit")) + }else if(me.field.value != e.target.value) + { + const event = new CustomEvent("change", {detail:me,bubbles:true}) + me.dispatchEvent(event) + } + }) + this.inputElement.addEventListener("click",(e)=>{ + const event = new CustomEvent("click",{detail:me,bubbles:true}) + me.dispatchEvent(event) + }) + this.container.appendChild(this.inputElement) + +} + if(!this.field){ + return + } + this.inputElement.setAttribute("type",this.field.type == null ? 'input' : this.field.type) + this.inputElement.setAttribute("name",this.field.name == null ? '' : this.field.name) + + if(this.field.text != null){ + this.inputElement.innerText = this.field.text + } + if(this.field.html != null){ + this.inputElement.innerHTML = this.field.html + } + if(this.field.class_name){ + this.inputElement.classList.add(this.field.class_name) + } + this.inputElement.setAttribute("tabindex", this.field.index) + this.inputElement.classList.add(this.field.name) + this.value = this.field.value + let place_holder = null + if(this.field.place_holder) + place_holder = this.field.place_holder + if(this.field.required && place_holder){ + place_holder = place_holder + } + if(place_holder) + this.field.place_holder = "* " + place_holder + this.inputElement.setAttribute("placeholder",place_holder) + if(this.field.required) + this.inputElement.setAttribute("required","required") + else + this.inputElement.removeAttribute("required") + if(!this.footerElement){ + this.footerElement = document.createElement('div') + this.footerElement.style.clear = 'both' + this.container.appendChild(this.footerElement) + } + } +} + +customElements.define('generic-field', GenericField); + +class GenericForm extends HTMLElement { + fields = {} + form = {} + constructor() { + + + super(); + this.attachShadow({ mode: 'open' }); + this.styleElement = document.createElement("style") + this.styleElement.innerHTML = ` + + * { + margin: 0; + padding: 0; + box-sizing: border-box; + width:90% + + } + + div { + + background-color: #0f0f0f; + border-radius: 10px; + padding: 30px; + width: 400px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + text-align: center; + + } + @media (max-width: 500px) { + form { + width: 80%; + } + }` + + this.container = document.createElement('div'); + this.container.appendChild(this.styleElement) + this.container.classList.add("generic-form-container") + this.shadowRoot.appendChild(this.container); + } + + connectedCallback() { + const url = this.getAttribute('url'); + if (url) { + const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get") + if(!url.startsWith("/")) + fullUrl.searchParams.set('url', url) + this.loadForm(fullUrl.toString()); + } else { + this.container.textContent = "No URL provided!"; + } + } + + async loadForm(url) { + const me = this + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); + } + me.form = await response.json(); + + let fields = Object.values(me.form.fields) + + fields = fields.sort((a,b)=>{ + console.info(a.index,b.index) + return a.index - b.index + }) + fields.forEach(field=>{ + const fieldElement = document.createElement('generic-field') + me.fields[field.name] = fieldElement + fieldElement.setAttribute("form", me) + fieldElement.setAttribute("field", field) + me.container.appendChild(fieldElement) + fieldElement.updateAttributes() + fieldElement.addEventListener("change",(e)=>{ + me.form.fields[e.detail.name].value = e.detail.value + }) + fieldElement.addEventListener("click",async (e)=>{ + if(e.detail.type == "button"){ + if(e.detail.value == "submit") + { + await me.validate() + } + } + + }) + }) + + } catch (error) { + this.container.textContent = `Error: ${error.message}`; + } + } + async validate(){ + const url = this.getAttribute("url") + const me = this + const response = await fetch(url,{ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({"action":"validate", "form":me.form}) + }); + const form = await response.json() + Object.values(form.fields).forEach(field=>{ + if(!me.form.fields[field.name]) + return + me.form.fields[field.name].is_valid = field.is_valid + if(!field.is_valid){ + me.fields[field.name].setInvalid() + me.fields[field.name].setErrors(field.errors) + console.info(field.name,"is invalid") + }else{ + me.fields[field.name].setValid() + } + me.fields[field.name].setAttribute("field",field) + me.fields[field.name].updateAttributes() + }) + Object.values(form.fields).forEach(field=>{ + console.info(field.errors) + me.fields[field.name].setErrors(field.errors) + }) + } + } + customElements.define('generic-form', GenericForm); \ No newline at end of file diff --git a/src/snek/static/html_frame.css b/src/snek/static/html-frame.css similarity index 54% rename from src/snek/static/html_frame.css rename to src/snek/static/html-frame.css index 6b64c76..92a1f97 100644 --- a/src/snek/static/html_frame.css +++ b/src/snek/static/html-frame.css @@ -1,9 +1,6 @@ .html-frame { width: 100px; height: 50px; - position: relative; overflow: hidden; - clip-path: inset(0px 0px 50px 100px); /* Crop content */ border: 1px solid black; - } \ No newline at end of file diff --git a/src/snek/static/html_frame.js b/src/snek/static/html-frame.js similarity index 65% rename from src/snek/static/html_frame.js rename to src/snek/static/html-frame.js index 19a0c34..22581ce 100644 --- a/src/snek/static/html_frame.js +++ b/src/snek/static/html-frame.js @@ -12,28 +12,25 @@ class HTMLFrame extends HTMLElement { if (url) { const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get") if(!url.startsWith("/")) - fullUrl.searchParams.set('url', url) - console.info(fullUrl) - this.fetchAndDisplayHtml(fullUrl.toString()); + fullUrl.searchParams.set('url', url) + this.loadAndRender(fullUrl.toString()); } else { - this.container.textContent = "No URL provided!"; + this.container.textContent = "No source URL!"; } } - async fetchAndDisplayHtml(url) { + async loadAndRender(url) { try { const response = await fetch(url); if (!response.ok) { - throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); + throw new Error(`Error: ${response.status} ${response.statusText}`); } const html = await response.text(); - this.container.innerHTML = html; // Insert the fetched HTML into the container + this.container.innerHTML = html; } catch (error) { this.container.textContent = `Error: ${error.message}`; } } } - - // Define the custom element customElements.define('html-frame', HTMLFrame); \ No newline at end of file diff --git a/src/snek/static/register.css b/src/snek/static/register__.css similarity index 74% rename from src/snek/static/register.css rename to src/snek/static/register__.css index fc4ca0f..57186b8 100644 --- a/src/snek/static/register.css +++ b/src/snek/static/register__.css @@ -1,24 +1,12 @@ -/* General Reset */ + * { margin: 0; padding: 0; box-sizing: border-box; } - /* Body Styling */ - body { - font-family: Arial, sans-serif; - background-color: #1a1a1a; - color: #e6e6e6; - line-height: 1.5; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100vh; - } - /* Registration Form Container */ + .registration-container { background-color: #0f0f0f; border-radius: 10px; @@ -26,16 +14,15 @@ width: 400px; box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); text-align: center; + left: calc(50%-200); } - /* Form Heading */ .registration-container h1 { font-size: 2em; color: #f05a28; margin-bottom: 20px; } - /* Input Fields */ .registration-container input { width: 100%; padding: 10px; @@ -47,7 +34,6 @@ font-size: 1em; } - /* Submit Button */ .registration-container button { width: 100%; padding: 10px; @@ -65,7 +51,6 @@ background-color: #e04924; } - /* Links */ .registration-container a { color: #f05a28; text-decoration: none; @@ -79,14 +64,11 @@ color: #e04924; } - /* Error Message Styling */ .error { color: #d8000c; font-size: 0.9em; margin-top: 5px; } - - /* Responsive Design */ @media (max-width: 500px) { .registration-container { width: 90%; diff --git a/src/snek/system/__init__.py b/src/snek/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/snek/system/form.py b/src/snek/system/form.py new file mode 100644 index 0000000..68f7c0f --- /dev/null +++ b/src/snek/system/form.py @@ -0,0 +1,171 @@ +from snek.system import model + +class HTMLElement(model.ModelField): + def __init__(self,id:str=None, tag:str="div", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs): + """ + Create a new HTMLElement. + + :param id: The id of the element + :param tag: The tag of the element + :param name: The name of the element, used to generate a class name if not provided + :param html: The inner html of the element + :param class_name: The class name of the element + :param text: The text of the element + """ + self.tag = tag + self.text = text + self.id = id + self.class_name = class_name or name + self.html = html + super().__init__(name=name,*args, **kwargs) + + def to_json(self): + """ + Return a json representation of the element. + + This will return a dict with the following keys: + + - text: The text of the element + - id: The id of the element + - html: The inner html of the element + - class_name: The class name of the element + - tag: The tag of the element + + :return: A json representation of the element + :rtype: dict + """ + result = super().to_json() + result['text'] = self.text + result['id'] = self.id + result['html'] = self.html + result['class_name'] = self.class_name + result['tag'] = self.tag + return result + +class FormElement(HTMLElement): + pass + +class FormInputElement(FormElement): + + def __init__(self,type="text",place_holder=None, *args, **kwargs): + """ + Initialize a FormInputElement with specified attributes. + + :param type: The type of the input element (default is "text"). + :param place_holder: The placeholder text for the input element. + :param args: Additional positional arguments. + :param kwargs: Additional keyword arguments. + """ + + super().__init__(tag="input", *args, **kwargs) + self.place_holder = place_holder + self.type = type + + + def to_json(self): + """ + Return a json representation of the element. + + This will return a dict with the following keys: + + - place_holder: The placeholder text for the input element + - type: The type of the input element + + :return: A json representation of the element + :rtype: dict + """ + data = super().to_json() + data["place_holder"] = self.place_holder + data["type"] = self.type + return data + +class FormButtonElement(FormElement): + # Just use the label text property to assign a button label. + def __init__(self, tag="button", *args, **kwargs): + """ + Initialize a FormButtonElement with specified attributes. + + :param tag: The tag of the button element (default is "button"). + :param args: Additional positional arguments. + :param kwargs: Additional keyword arguments. + """ + super().__init__(tag=tag, *args, **kwargs) + + +class Form(model.BaseModel): + + @property + def html_elements(self): + """ + Return a list of all :class:`HTMLElement` objects in the form. + + This is a convenience property that filters the :attr:`fields` list to only + include elements that are instances of :class:`HTMLElement`. + + :return: A list of :class:`HTMLElement` objects + :rtype: list + """ + json_elements = super().to_json() + return [element for element in self.fields if isinstance(element,HTMLElement)] + def set_user_data(self, data): + """ + Set user data for the form by updating the fields with the provided data. + + This method extracts the 'fields' key from the provided data dictionary + and passes it to the parent class's `set_user_data` method to update the + form fields accordingly. + + :param data: A dictionary containing the form data, expected to have a + 'fields' key with the data to update the form fields. + """ + + return super().set_user_data(data.get('fields')) + + def to_json(self, encode=False): + """ + Return a JSON representation of the form, including field values and metadata. + + This method returns a dictionary with the following keys: + + - ``fields``: A dictionary of field names to their current values. + - ``is_valid``: A boolean indicating whether the form is valid. + - ``errors``: A dictionary of field names to lists of error strings. + + If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded + before being returned. Otherwise, the dictionary is returned directly. + + :param encode: If ``True``, JSON-encode the returned dictionary. + :type encode: bool + :return: A JSON representation of the form. + :rtype: dict + """ + elements = super().to_json() + html_elements = {} + for element in elements.keys(): + print("DDD!",element,flush=True) + field = getattr(self,element) + if isinstance(field,HTMLElement): + print("QQQQ!",element,flush=True) + try: + html_elements[element] = elements[element] + except KeyError: + pass + + return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors) + @property + def errors(self): + """ + Return a list of all error strings from all fields in the form. + + The list will be empty if all fields are valid. + + :return: A list of error strings. + :rtype: list + """ + result = [] + for field in self.html_elements: + result += field.errors + return result + @property + def is_valid(self): + return all(element.is_valid for element in self.html_elements) diff --git a/src/snek/http.py b/src/snek/system/http.py similarity index 100% rename from src/snek/http.py rename to src/snek/system/http.py diff --git a/src/snek/middleware.py b/src/snek/system/middleware.py similarity index 100% rename from src/snek/middleware.py rename to src/snek/system/middleware.py diff --git a/src/snek/models.py b/src/snek/system/model.py similarity index 78% rename from src/snek/models.py rename to src/snek/system/model.py index ec5d9ce..a699a9e 100644 --- a/src/snek/models.py +++ b/src/snek/system/model.py @@ -23,7 +23,7 @@ def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**k return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func) class Validator: - + _index = 0 @property def value(self): return self._value @@ -34,12 +34,14 @@ class Validator: @property def initial_value(self): - return None + return self.value def custom_validation(self): return True def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs): + self.index = Validator._index + Validator._index += 1 self.required = required self.min_num = min_num self.max_num = max_num @@ -47,8 +49,10 @@ class Validator: self.max_length = max_length self.regex = regex self._value = None - self.value = value - self.type = kind + self.value = value + print("xxxx", value,flush=True) + + self.kind = kind self.help_text = help_text self.__dict__.update(kwargs) @property @@ -61,7 +65,7 @@ class Validator: if self.value is None: return error_list - if self.type == float or self.type == int: + if self.kind == float or self.kind == int: if self.min_num is not None and self.value < self.min_num: error_list.append("Field should be minimal {}.".format(self.min_num)) if self.max_num is not None and self.value > self.max_num: @@ -70,10 +74,11 @@ class Validator: error_list.append("Field should be minimal {} characters long.".format(self.min_length)) if self.max_length is not None and len(self.value) > self.max_length: error_list.append("Field should be maximal {} characters long.".format(self.max_length)) + print(self.regex, self.value,flush=True) if not self.regex is None and not self.value is None and not re.match(self.regex, self.value): error_list.append("Invalid value.".format(self.regex)) - if not self.type is None and type(self.value) != self.type: - error_list.append("Invalid type. It is supposed to be {}.".format(self.type)) + if not self.kind is None and type(self.value) != self.kind: + error_list.append("Invalid kind. It is supposed to be {}.".format(self.kind)) return error_list def validate(self): @@ -89,6 +94,8 @@ class Validator: except ValueError: return False + + def to_json(self): return { "required": self.required, @@ -98,19 +105,26 @@ class Validator: "max_length": self.max_length, "regex": self.regex, "value": self.value, - "type": self.type, + "kind": str(self.kind), "help_text": self.help_text, "errors": self.errors, - "is_valid": self.is_valid + "is_valid": self.is_valid, + "index":self.index } class ModelField(Validator): + + index = 1 def __init__(self,name=None,save=True, *args, **kwargs): self.name = name - self.save = save super().__init__(*args, **kwargs) + def to_json(self): + result = super().to_json() + result['name'] = self.name + return result + class CreatedField(ModelField): @@ -146,9 +160,11 @@ class BaseModel: updated_at = UpdatedField(name="updated_at",regex=TIMESTAMP_REGEX,place_holder="Updated at") deleted_at = DeletedField(name="deleted_at",regex=TIMESTAMP_REGEX, place_holder="Deleted at") + def __init__(self, *args, **kwargs): print(self.__dict__) print(dir(self.__class__)) + self.fields = {} for key in dir(self.__class__): obj = getattr(self.__class__,key) @@ -156,6 +172,7 @@ class BaseModel: self.__dict__[key] = copy.deepcopy(obj) print("JAAA") self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value) + self.fields[key] = self.__dict__[key] def __setitem__(self, key, value): obj = self.__dict__.get(key) @@ -169,6 +186,22 @@ class BaseModel: return obj.value return obj + def set_user_data(self, data): + for key, value in data.items(): + field = self.fields.get(key) + if not field: + continue + if value.get('name'): + value = value.get('value') + field.value = value + + + @property + def is_valid(self): + for field in self.fields.values(): + if not field.is_valid: + return False + return True def __getitem__(self, key): obj = self.__dict__.get(key) @@ -180,7 +213,7 @@ class BaseModel: if isinstance(obj,Validator): obj.value = value else: - setattr(self,key,value) + self.__dict__[key] = value #setattr(self,key,value) #def __getattr__(self, key): # obj = self.__dict__.get(key) # if isinstance(obj,Validator): @@ -201,6 +234,7 @@ class BaseModel: "updated_at": self.updated_at.value, "deleted_at": self.deleted_at.value }) + for key,value in self.__dict__.items(): if key == "record": continue @@ -225,39 +259,8 @@ class FormElement(ModelField): self.place_holder = place_holder super().__init__(*args, **kwargs) - def to_json(self): data = super().to_json() data["name"] = self.name data["place_holder"] = self.place_holder return data - - - - -class TestModel(BaseModel): - - first_name = FormElement(name="first_name",required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="First name") - last_name = FormElement(name="last_name",required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="Last name") - email = FormElement(name="email",required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",place_holder="Email address") - password = FormElement(name="password",required=True,regex=r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$",place_holder="Password") - -class Form: - username = FormElement(required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="Username") - email = FormElement(required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",place_holder="Email address") - def __init__(self, *args, **kwargs): - self.place_holder = kwargs.pop("place_holder",None) - - -if __name__ == "__main__": - model = TestModel(first_name="John",last_name="Doe",email="n9K9p@example.com",password="Password123") - model2 = TestModel(first_name="John",last_name="Doe",email="ddd",password="zzz") - model.first_name = "AAA" - print(model.first_name) - print(model.first_name.value) - - print(model.first_name) - print(model.first_name.value) - print(model.to_json(True)) - print(model2.to_json(True)) - print(model2.record) diff --git a/src/snek/templates/base.html b/src/snek/templates/base.html new file mode 100644 index 0000000..5b1bdf2 --- /dev/null +++ b/src/snek/templates/base.html @@ -0,0 +1,31 @@ + + + + + + {% block title %}{% endblock %} + + + + + + + + + +
+ {% block header %} + {% endblock %} + +
+
+ + {% block main %} + {% endblock %} +
+ + \ No newline at end of file diff --git a/src/snek/templates/base_chat.html b/src/snek/templates/base_chat.html new file mode 100644 index 0000000..09c025b --- /dev/null +++ b/src/snek/templates/base_chat.html @@ -0,0 +1,31 @@ + + + + + + {% block title %}{% endblock %} + + + + + + + + + +
+ {% block header %} + {% endblock %} + +
+
+ + {% block main %} + {% endblock %} +
+ + \ No newline at end of file diff --git a/src/snek/templates/index.html b/src/snek/templates/index.html new file mode 100644 index 0000000..1ee1f77 --- /dev/null +++ b/src/snek/templates/index.html @@ -0,0 +1,20 @@ + + + + + + Snek chat by Molodetz + + + + + +
+

Snek

+ + Or + + +
+ + diff --git a/src/snek/templates/login.html b/src/snek/templates/login.html index d37f3fa..c09ec70 100644 --- a/src/snek/templates/login.html +++ b/src/snek/templates/login.html @@ -1,20 +1,5 @@ - - - - - - Register - - - -
-

Login

-
- - - - Not having an account yet? Register here. -
-
- - +{% extends "base.html" %} + +{% block main %} + +{% endblock %} diff --git a/src/snek/templates/register.html b/src/snek/templates/register.html index da41629..61da961 100644 --- a/src/snek/templates/register.html +++ b/src/snek/templates/register.html @@ -1,22 +1,5 @@ - - - - - - Register - - - -
-

Register

-
- - - - - - Already have an account? Login here. -
-
- - +{% extends "base.html" %} + +{% block main %} + +{% endblock %} \ No newline at end of file diff --git a/src/snek/templates/test.html b/src/snek/templates/web.html similarity index 92% rename from src/snek/templates/test.html rename to src/snek/templates/web.html index c23103a..0403e1b 100644 --- a/src/snek/templates/test.html +++ b/src/snek/templates/web.html @@ -4,9 +4,7 @@ Dark Themed Chat Application - - - +
@@ -59,5 +57,4 @@ - - + \ No newline at end of file diff --git a/src/snek/view/base.py b/src/snek/view/base.py new file mode 100644 index 0000000..d962ee5 --- /dev/null +++ b/src/snek/view/base.py @@ -0,0 +1,31 @@ +from aiohttp import web + +class BaseView(web.View): + + @property + def app(self): + return self.request.app + + @property + def db(self): + return self.app.db + + def json_response(self, data): + return web.json_response(data) + + def render_template(self, template_name, context=None): + return self.request.app.render_template(template_name, self.request,context) + +class BaseFormView(BaseView): + + form = None + + async def get(self): + form = self.form() + return self.json_response(form.to_json()) + + async def post(self): + form = self.form() + post = await self.request.json() + form.set_user_data(post['form']) + return self.json_response(form.to_json()) \ No newline at end of file diff --git a/src/snek/view/index.py b/src/snek/view/index.py new file mode 100644 index 0000000..a5d8b92 --- /dev/null +++ b/src/snek/view/index.py @@ -0,0 +1,6 @@ +from snek.view.base import BaseView + +class IndexView(BaseView): + + async def get(self): + return await self.render_template("index.html") diff --git a/src/snek/view/login.py b/src/snek/view/login.py new file mode 100644 index 0000000..3a3beaf --- /dev/null +++ b/src/snek/view/login.py @@ -0,0 +1,13 @@ +from snek.form.register import RegisterForm +from snek.view.base import BaseView + +class LoginView(BaseView): + + async def get(self): + return await self.render_template("login.html") #web.json_response({"form": RegisterForm().to_json()}) + + async def post(self): + form = RegisterForm() + form.set_user_data(await self.request.post()) + print(form.is_valid()) + return await self.render_template("login.html", self.request) #web.json_response({"form": RegisterForm().to_json()}) diff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py new file mode 100644 index 0000000..26527da --- /dev/null +++ b/src/snek/view/login_form.py @@ -0,0 +1,5 @@ +from snek.view.base import BaseFormView +from snek.form.login import LoginForm + +class LoginFormView(BaseFormView): + form = LoginForm \ No newline at end of file diff --git a/src/snek/view/register.py b/src/snek/view/register.py new file mode 100644 index 0000000..095b7a3 --- /dev/null +++ b/src/snek/view/register.py @@ -0,0 +1,6 @@ +from snek.view.base import BaseView + +class RegisterView(BaseView): + + async def get(self): + return await self.render_template("register.html") \ No newline at end of file diff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py new file mode 100644 index 0000000..0ae7630 --- /dev/null +++ b/src/snek/view/register_form.py @@ -0,0 +1,5 @@ +from snek.form.register import RegisterForm +from snek.view.base import BaseFormView + +class RegisterFormView(BaseFormView): + form = RegisterForm \ No newline at end of file diff --git a/src/snek/view/view.py b/src/snek/view/view.py new file mode 100644 index 0000000..ea642a3 --- /dev/null +++ b/src/snek/view/view.py @@ -0,0 +1,6 @@ +from snek.view.base import BaseView + +class WebView(BaseView): + + async def get(self): + return await self.render_template("web.html") \ No newline at end of file