Progress.
This commit is contained in:
parent
2e3b85d7f7
commit
ba83922660
@ -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"]
|
||||
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -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.
|
2
Makefile
2
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
|
||||
|
||||
|
@ -6,3 +6,5 @@ services:
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- ./:/code
|
||||
entrypoint: ["python","-m","snek.app"]
|
||||
|
@ -1,3 +1,25 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
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"
|
||||
]
|
||||
|
||||
|
@ -19,6 +19,7 @@ install_requires =
|
||||
gunicorn
|
||||
imgkit
|
||||
wkhtmltopdf
|
||||
shed
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
@ -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,21 +18,36 @@ 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)
|
||||
super().__init__(
|
||||
middlewares=middlewares, template_path=self.template_path, *args, **kwargs
|
||||
)
|
||||
self.setup_router()
|
||||
|
||||
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)
|
||||
|
||||
async def handle_test(self, request):
|
||||
|
||||
return await self.render_template("test.html",request,context={"name":"retoor"})
|
||||
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")
|
||||
@ -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")
|
||||
|
0
src/snek/form/__init__.py
Normal file
0
src/snek/form/__init__.py
Normal file
24
src/snek/form/login.py
Normal file
24
src/snek/form/login.py
Normal file
@ -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"
|
||||
)
|
||||
|
31
src/snek/form/register.py
Normal file
31
src/snek/form/register.py
Normal file
@ -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"
|
||||
)
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
0
src/snek/model/__init__.py
Normal file
0
src/snek/model/__init__.py
Normal file
19
src/snek/model/user.py
Normal file
19
src/snek/model/user.py
Normal file
@ -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,}")
|
||||
|
||||
|
@ -64,6 +64,19 @@ class Room {
|
||||
}
|
||||
|
||||
|
||||
class InlineAppElement extends HTMLElement {
|
||||
|
||||
constructor(){
|
||||
this.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Page {
|
||||
elements = []
|
||||
|
||||
}
|
||||
|
||||
class App {
|
||||
rooms = []
|
||||
constructor() {
|
||||
|
@ -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;
|
54
src/snek/static/fancy-button.js
Normal file
54
src/snek/static/fancy-button.js
Normal file
@ -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)
|
100
src/snek/static/generic-form.css
Normal file
100
src/snek/static/generic-form.css
Normal file
@ -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%;
|
||||
}
|
||||
}
|
321
src/snek/static/generic-form.js
Normal file
321
src/snek/static/generic-form.js
Normal file
@ -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);
|
@ -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;
|
||||
|
||||
}
|
@ -13,27 +13,24 @@ class HTMLFrame extends HTMLElement {
|
||||
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());
|
||||
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);
|
@ -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%;
|
0
src/snek/system/__init__.py
Normal file
0
src/snek/system/__init__.py
Normal file
171
src/snek/system/form.py
Normal file
171
src/snek/system/form.py
Normal file
@ -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)
|
@ -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
|
||||
@ -48,7 +50,9 @@ class Validator:
|
||||
self.regex = regex
|
||||
self._value = None
|
||||
self.value = value
|
||||
self.type = kind
|
||||
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)
|
31
src/snek/templates/base.html
Normal file
31
src/snek/templates/base.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<script src="/fancy-button.js"></script>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="stylesheet" href="/generic-form.css">
|
||||
<script src="/html-frame.js"></script>
|
||||
<script src="/generic-form.js"></script>
|
||||
<link rel="stylesheet" href="/html-frame.css"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
|
||||
</header>
|
||||
<main>
|
||||
<aside class="sidebar">
|
||||
{% block sidebar %}
|
||||
|
||||
{% endblock %}
|
||||
</aside>
|
||||
{% block main %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
31
src/snek/templates/base_chat.html
Normal file
31
src/snek/templates/base_chat.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<script src="/fancy-button.js"></script>
|
||||
<link rel="stylesheet" href="/base.css">
|
||||
<link rel="stylesheet" href="/generic-form.css">
|
||||
<script src="/html-frame.js"></script>
|
||||
<script src="/generic-form.js"></script>
|
||||
<link rel="stylesheet" href="/html-frame.css"></script>
|
||||
<link rel="stylesheet" href="/register__.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
|
||||
</header>
|
||||
<main>
|
||||
<aside class="sidebar">
|
||||
{% block sidebar %}
|
||||
|
||||
{% endblock %}
|
||||
</aside>
|
||||
{% block main %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
20
src/snek/templates/index.html
Normal file
20
src/snek/templates/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Snek chat by Molodetz</title>
|
||||
<link rel="stylesheet" href="generic-form.css">
|
||||
<link rel="stylesheet" href="register__.css">
|
||||
<script src="/fancy-button.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="registration-container">
|
||||
<h1>Snek</h1>
|
||||
<fancy-button url="/login" text="Login"></fancy-button>
|
||||
<span style="padding:10px;">Or</span>
|
||||
<fancy-button url="/register" text="Register"></fancy-button>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,20 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register</title>
|
||||
<link rel="stylesheet" href="register.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="registration-container">
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username or password" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Create Account</button>
|
||||
<a href="/register">Not having an account yet? Register here.</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<generic-form url="/login-form"></generic-form>
|
||||
{% endblock %}
|
||||
|
@ -1,22 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register</title>
|
||||
<link rel="stylesheet" href="register.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="registration-container">
|
||||
<h1>Register</h1>
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="email" name="email" placeholder="Email Address" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<input type="password" name="confirm_password" placeholder="Confirm Password" required>
|
||||
<button type="submit">Create Account</button>
|
||||
<a href="#">Already have an account? Login here.</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<generic-form url="/register-form"></generic-form>
|
||||
{% endblock %}
|
@ -4,9 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dark Themed Chat Application</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="/html_frame.js"></script>
|
||||
<script src="/html_frame.css"></script>
|
||||
<link rel="stylesheet" href="base.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@ -60,4 +58,3 @@
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
31
src/snek/view/base.py
Normal file
31
src/snek/view/base.py
Normal file
@ -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())
|
6
src/snek/view/index.py
Normal file
6
src/snek/view/index.py
Normal file
@ -0,0 +1,6 @@
|
||||
from snek.view.base import BaseView
|
||||
|
||||
class IndexView(BaseView):
|
||||
|
||||
async def get(self):
|
||||
return await self.render_template("index.html")
|
13
src/snek/view/login.py
Normal file
13
src/snek/view/login.py
Normal file
@ -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()})
|
5
src/snek/view/login_form.py
Normal file
5
src/snek/view/login_form.py
Normal file
@ -0,0 +1,5 @@
|
||||
from snek.view.base import BaseFormView
|
||||
from snek.form.login import LoginForm
|
||||
|
||||
class LoginFormView(BaseFormView):
|
||||
form = LoginForm
|
6
src/snek/view/register.py
Normal file
6
src/snek/view/register.py
Normal file
@ -0,0 +1,6 @@
|
||||
from snek.view.base import BaseView
|
||||
|
||||
class RegisterView(BaseView):
|
||||
|
||||
async def get(self):
|
||||
return await self.render_template("register.html")
|
5
src/snek/view/register_form.py
Normal file
5
src/snek/view/register_form.py
Normal file
@ -0,0 +1,5 @@
|
||||
from snek.form.register import RegisterForm
|
||||
from snek.view.base import BaseFormView
|
||||
|
||||
class RegisterFormView(BaseFormView):
|
||||
form = RegisterForm
|
6
src/snek/view/view.py
Normal file
6
src/snek/view/view.py
Normal file
@ -0,0 +1,6 @@
|
||||
from snek.view.base import BaseView
|
||||
|
||||
class WebView(BaseView):
|
||||
|
||||
async def get(self):
|
||||
return await self.render_template("web.html")
|
Loading…
Reference in New Issue
Block a user