Progress.

This commit is contained in:
retoor 2025-01-24 03:28:43 +01:00
parent 2e3b85d7f7
commit ba83922660
42 changed files with 1050 additions and 210 deletions

View File

@ -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
View 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.

View File

@ -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

View File

@ -6,3 +6,5 @@ services:
- "8081:8081"
volumes:
- ./:/code
entrypoint: ["python","-m","snek.app"]

View File

@ -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"
]

View File

@ -19,6 +19,7 @@ install_requires =
gunicorn
imgkit
wkhtmltopdf
shed
[options.packages.find]
where = src

View File

@ -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")

View File

24
src/snek/form/login.py Normal file
View 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
View 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"
)

View File

@ -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")

View File

19
src/snek/model/user.py Normal file
View 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,}")

View File

@ -64,6 +64,19 @@ class Room {
}
class InlineAppElement extends HTMLElement {
constructor(){
this.
}
}
class Page {
elements = []
}
class App {
rooms = []
constructor() {

View File

@ -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;

View 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)

View 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%;
}
}

View 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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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%;

View File

171
src/snek/system/form.py Normal file
View 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)

View File

@ -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)

View 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>

View 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>

View 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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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
View 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
View 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
View 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()})

View File

@ -0,0 +1,5 @@
from snek.view.base import BaseFormView
from snek.form.login import LoginForm
class LoginFormView(BaseFormView):
form = LoginForm

View 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")

View 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
View 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")