Complete system.
This commit is contained in:
parent
ba83922660
commit
d20079f3ed
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.history
|
.history
|
||||||
|
.resources
|
||||||
|
.backup*
|
||||||
|
docs
|
||||||
*.db*
|
*.db*
|
||||||
*.png
|
*.png
|
||||||
# ---> Python
|
# ---> Python
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf
|
FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf
|
||||||
FROM python:3.10-alpine
|
FROM python:3.12.8-alpine3.21
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
ENV FLASK_APP=app.py
|
ENV FLASK_APP=app.py
|
||||||
ENV FLASK_RUN_HOST=0.0.0.0
|
ENV FLASK_RUN_HOST=0.0.0.0
|
||||||
@ -37,5 +37,5 @@ RUN pip install --upgrade pip
|
|||||||
RUN pip install -e .
|
RUN pip install -e .
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
python -m snek.app
|
CMD ["python","-m","snek.app"]
|
||||||
#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]
|
||||||
|
@ -20,6 +20,8 @@ dependencies = [
|
|||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"imgkit",
|
"imgkit",
|
||||||
"wkhtmltopdf"
|
"wkhtmltopdf",
|
||||||
|
"jinja-markdown2",
|
||||||
|
"mistune"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3,14 +3,16 @@ import pathlib
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
|
|
||||||
|
from jinja_markdown2 import MarkdownExtension
|
||||||
from snek.system import http
|
from snek.system import http
|
||||||
from snek.system.middleware import cors_middleware
|
from snek.system.middleware import cors_middleware
|
||||||
|
from snek.view.about import AboutHTMLView, AboutMDView
|
||||||
from snek.view.index import IndexView
|
from snek.view.index import IndexView
|
||||||
from snek.view.login import LoginView
|
from snek.view.login import LoginView
|
||||||
from snek.view.login_form import LoginFormView
|
from snek.view.login_form import LoginFormView
|
||||||
from snek.view.register import RegisterView
|
from snek.view.register import RegisterView
|
||||||
from snek.view.register_form import RegisterFormView
|
from snek.view.register_form import RegisterFormView
|
||||||
from snek.view.view import WebView
|
from snek.view.web import WebView
|
||||||
|
|
||||||
|
|
||||||
class Application(BaseApplication):
|
class Application(BaseApplication):
|
||||||
@ -24,6 +26,7 @@ class Application(BaseApplication):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
middlewares=middlewares, template_path=self.template_path, *args, **kwargs
|
middlewares=middlewares, template_path=self.template_path, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
self.jinja2_env.add_extension(MarkdownExtension)
|
||||||
self.setup_router()
|
self.setup_router()
|
||||||
|
|
||||||
def setup_router(self):
|
def setup_router(self):
|
||||||
@ -34,12 +37,14 @@ class Application(BaseApplication):
|
|||||||
name="static",
|
name="static",
|
||||||
show_index=True,
|
show_index=True,
|
||||||
)
|
)
|
||||||
self.router.add_view("/web", WebView)
|
self.router.add_view("/about.html", AboutHTMLView)
|
||||||
self.router.add_view("/login", LoginView)
|
self.router.add_view("/about.md", AboutMDView)
|
||||||
self.router.add_view("/login-form", LoginFormView)
|
self.router.add_view("/web.html", WebView)
|
||||||
self.router.add_view("/register", RegisterView)
|
self.router.add_view("/login.html", LoginView)
|
||||||
|
self.router.add_view("/login-form.json", LoginFormView)
|
||||||
|
self.router.add_view("/register.html", RegisterView)
|
||||||
|
|
||||||
self.router.add_view("/register-form", RegisterFormView)
|
self.router.add_view("/register-form.json", RegisterFormView)
|
||||||
self.router.add_get("/http-get", self.handle_http_get)
|
self.router.add_get("/http-get", self.handle_http_get)
|
||||||
self.router.add_get("/http-photo", self.handle_http_photo)
|
self.router.add_get("/http-photo", self.handle_http_photo)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class RegisterForm(Form):
|
|||||||
)
|
)
|
||||||
email = FormInputElement(
|
email = FormInputElement(
|
||||||
name="email",
|
name="email",
|
||||||
required=True,
|
required=False,
|
||||||
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||||
place_holder="Email address",
|
place_holder="Email address",
|
||||||
type="email"
|
type="email"
|
||||||
|
12
src/snek/mapper/__init__.py
Normal file
12
src/snek/mapper/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import functools
|
||||||
|
from snek.mapper.user import UserMapper
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def get_mappers(app=None):
|
||||||
|
return dict(
|
||||||
|
user=UserMapper(app=app)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_mapper(name, app=None):
|
||||||
|
return get_mappers(app=app)[name]
|
6
src/snek/mapper/user.py
Normal file
6
src/snek/mapper/user.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from snek.system.mapper import BaseMapper
|
||||||
|
from snek.model.user import UserModel
|
||||||
|
|
||||||
|
class UserMapper(BaseMapper):
|
||||||
|
table_name = "user"
|
||||||
|
model: UserModel
|
@ -0,0 +1,12 @@
|
|||||||
|
from snek.model.user import UserModel
|
||||||
|
import functools
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def get_models():
|
||||||
|
return dict(
|
||||||
|
user=UserModel
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_model(name):
|
||||||
|
return get_models()[name]
|
@ -1,6 +1,6 @@
|
|||||||
from snek.system.model import BaseModel,ModelField
|
from snek.system.model import BaseModel,ModelField
|
||||||
|
|
||||||
class User(BaseModel):
|
class UserModel(BaseModel):
|
||||||
|
|
||||||
username = ModelField(
|
username = ModelField(
|
||||||
name="username",
|
name="username",
|
||||||
@ -11,7 +11,7 @@ class User(BaseModel):
|
|||||||
)
|
)
|
||||||
email = ModelField(
|
email = ModelField(
|
||||||
name="email",
|
name="email",
|
||||||
required=True,
|
required=False,
|
||||||
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
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,}")
|
password = ModelField(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}")
|
||||||
|
12
src/snek/service/__init__.py
Normal file
12
src/snek/service/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from snek.service.user import UserService
|
||||||
|
import functools
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def get_services(app):
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
user = UserService(app=app)
|
||||||
|
|
||||||
|
)
|
||||||
|
def get_service(name, app=None):
|
||||||
|
return get_services(app=app)[name]
|
16
src/snek/service/user.py
Normal file
16
src/snek/service/user.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from snek.system.service import BaseService
|
||||||
|
from snek.system import security
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
mapper_name = "user"
|
||||||
|
|
||||||
|
async def create_user(self, username, password):
|
||||||
|
if await self.exists(username=username):
|
||||||
|
raise Exception("User already exists.")
|
||||||
|
model = await self.new()
|
||||||
|
model.username = username
|
||||||
|
model.password = await security.hash(password)
|
||||||
|
if await self.save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create user: {model.errors}.")
|
||||||
|
|
@ -224,7 +224,11 @@ class GenericForm extends HTMLElement {
|
|||||||
|
|
||||||
}
|
}
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
form {
|
form {
|
||||||
|
height:100%;
|
||||||
|
width: 100%;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -26,7 +26,16 @@ class HTMLFrame extends HTMLElement {
|
|||||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
this.container.innerHTML = html;
|
if(url.endsWith(".md")){
|
||||||
|
const parent = this
|
||||||
|
const markdownElement = document.createElement('div')
|
||||||
|
markdownElement.innerHTML = html
|
||||||
|
document.body.appendChild(markdownElement)
|
||||||
|
//parent.parentElement.appendChild(markdownElement)
|
||||||
|
|
||||||
|
}else{
|
||||||
|
this.container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.container.textContent = `Error: ${error.message}`;
|
this.container.textContent = `Error: ${error.message}`;
|
||||||
|
39
src/snek/static/markdown-frame.js
Normal file
39
src/snek/static/markdown-frame.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLFrame extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.shadowRoot.appendChild(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.container.classList.add("html_frame")
|
||||||
|
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.loadAndRender(fullUrl.toString());
|
||||||
|
} else {
|
||||||
|
this.container.textContent = "No source URL!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAndRender(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const html = await response.text();
|
||||||
|
this.container.innerHTML = html;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.container.textContent = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('markdown-frame', HTMLFrame);
|
20
src/snek/static/style.css
Normal file
20
src/snek/static/style.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
color: #f05a28;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: #f05a28;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #000;
|
||||||
|
color: #efefef;
|
||||||
|
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
}
|
0
src/snek/system/api.py
Normal file
0
src/snek/system/api.py
Normal file
@ -1,171 +1,96 @@
|
|||||||
from snek.system import model
|
# Written by retoor@molodetz.nl
|
||||||
|
|
||||||
|
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
|
||||||
|
|
||||||
|
# This code uses the `snek.system.model` library for managing model fields.
|
||||||
|
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from snek.system import model
|
||||||
|
|
||||||
class HTMLElement(model.ModelField):
|
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):
|
def __init__(self, id=None, tag="div", name=None, html=None, class_name=None, text=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.tag = tag
|
||||||
self.text = text
|
self.text = text
|
||||||
self.id = id
|
self.id = id
|
||||||
self.class_name = class_name or name
|
self.class_name = class_name or name
|
||||||
self.html = html
|
self.html = html
|
||||||
super().__init__(name=name,*args, **kwargs)
|
super().__init__(name=name, *args, **kwargs)
|
||||||
|
|
||||||
def to_json(self):
|
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 = super().to_json()
|
||||||
result['text'] = self.text
|
result['text'] = self.text
|
||||||
result['id'] = self.id
|
result['id'] = self.id
|
||||||
result['html'] = self.html
|
result['html'] = self.html
|
||||||
result['class_name'] = self.class_name
|
result['class_name'] = self.class_name
|
||||||
result['tag'] = self.tag
|
result['tag'] = self.tag
|
||||||
return result
|
return result
|
||||||
|
|
||||||
class FormElement(HTMLElement):
|
class FormElement(HTMLElement):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class FormInputElement(FormElement):
|
class FormInputElement(FormElement):
|
||||||
|
def __init__(self, type="text", place_holder=None, *args, **kwargs):
|
||||||
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)
|
super().__init__(tag="input", *args, **kwargs)
|
||||||
self.place_holder = place_holder
|
self.place_holder = place_holder
|
||||||
self.type = type
|
self.type = type
|
||||||
|
|
||||||
|
|
||||||
def to_json(self):
|
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 = super().to_json()
|
||||||
data["place_holder"] = self.place_holder
|
data["place_holder"] = self.place_holder
|
||||||
data["type"] = self.type
|
data["type"] = self.type
|
||||||
return data
|
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").
|
class FormButtonElement(FormElement):
|
||||||
:param args: Additional positional arguments.
|
def __init__(self, tag="button", *args, **kwargs):
|
||||||
:param kwargs: Additional keyword arguments.
|
|
||||||
"""
|
|
||||||
super().__init__(tag=tag, *args, **kwargs)
|
super().__init__(tag=tag, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Form(model.BaseModel):
|
class Form(model.BaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def html_elements(self):
|
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()
|
json_elements = super().to_json()
|
||||||
return [element for element in self.fields if isinstance(element,HTMLElement)]
|
return [element for element in self.fields if isinstance(element, HTMLElement)]
|
||||||
|
|
||||||
def set_user_data(self, data):
|
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'))
|
return super().set_user_data(data.get('fields'))
|
||||||
|
|
||||||
def to_json(self, encode=False):
|
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()
|
elements = super().to_json()
|
||||||
html_elements = {}
|
html_elements = {}
|
||||||
for element in elements.keys():
|
for element in elements.keys():
|
||||||
print("DDD!",element,flush=True)
|
field = getattr(self, element)
|
||||||
field = getattr(self,element)
|
if isinstance(field, HTMLElement):
|
||||||
if isinstance(field,HTMLElement):
|
|
||||||
print("QQQQ!",element,flush=True)
|
|
||||||
try:
|
try:
|
||||||
html_elements[element] = elements[element]
|
html_elements[element] = elements[element]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)
|
||||||
|
|
||||||
return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)
|
|
||||||
@property
|
@property
|
||||||
def errors(self):
|
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 = []
|
result = []
|
||||||
for field in self.html_elements:
|
for field in self.html_elements:
|
||||||
result += field.errors
|
result += field.errors
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
return all(element.is_valid for element in self.html_elements)
|
return all(element.is_valid for element in self.html_elements)
|
@ -1,77 +1,99 @@
|
|||||||
from aiohttp import web
|
# Written by retoor@molodetz.nl
|
||||||
import aiohttp
|
|
||||||
|
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
|
||||||
|
|
||||||
|
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import aiohttp
|
||||||
from app.cache import time_cache_async
|
from app.cache import time_cache_async
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
import pathlib
|
import pathlib
|
||||||
import uuid
|
import uuid
|
||||||
import imgkit
|
import imgkit
|
||||||
import asyncio
|
import asyncio
|
||||||
import zlib
|
import zlib
|
||||||
import io
|
import io
|
||||||
|
|
||||||
async def crc32(data):
|
async def crc32(data):
|
||||||
try:
|
try:
|
||||||
data = data.encode()
|
data = data.encode()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
result = "crc32" + str(zlib.crc32(data))
|
return "crc32" + str(zlib.crc32(data))
|
||||||
return result
|
|
||||||
|
|
||||||
async def get_file(name,suffix=".cache"):
|
async def get_file(name, suffix=".cache"):
|
||||||
name = await crc32(name)
|
name = await crc32(name)
|
||||||
path = pathlib.Path(".").joinpath("cache")
|
path = pathlib.Path(".").joinpath("cache")
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir(parents=True,exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
path = path.joinpath(name + suffix)
|
return path.joinpath(name + suffix)
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def public_touch(name=None):
|
async def public_touch(name=None):
|
||||||
path = pathlib.Path(".").joinpath(str(uuid.uuid4())+name)
|
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
|
||||||
path.open("wb").close()
|
path.open("wb").close()
|
||||||
return path
|
return path
|
||||||
|
|
||||||
async def create_site_photo(url):
|
async def create_site_photo(url):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
if not url.startswith("https"):
|
if not url.startswith("https"):
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
output_path = await get_file("site-screenshot-" + url,".png")
|
output_path = await get_file("site-screenshot-" + url, ".png")
|
||||||
|
|
||||||
if output_path.exists():
|
if output_path.exists():
|
||||||
return output_path
|
return output_path
|
||||||
output_path.touch()
|
output_path.touch()
|
||||||
|
|
||||||
def make_photo():
|
def make_photo():
|
||||||
imgkit.from_url(url, output_path.absolute())
|
imgkit.from_url(url, output_path.absolute())
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
return await loop.run_in_executor(None,make_photo)
|
return await loop.run_in_executor(None, make_photo)
|
||||||
|
|
||||||
async def repair_links(base_url, html_content):
|
async def repair_links(base_url, html_content):
|
||||||
soup = BeautifulSoup(html_content, "html.parser")
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
for tag in soup.find_all(['a', 'img', 'link']):
|
for tag in soup.find_all(['a', 'img', 'link']):
|
||||||
if tag.has_attr('href') and not tag['href'].startswith("http"): # For <a> and <link> tags
|
if tag.has_attr('href') and not tag['href'].startswith("http"):
|
||||||
tag['href'] = urljoin(base_url, tag['href'])
|
tag['href'] = urljoin(base_url, tag['href'])
|
||||||
if tag.has_attr('src') and not tag['src'].startswith("http"): # For <img> tags
|
if tag.has_attr('src') and not tag['src'].startswith("http"):
|
||||||
tag['src'] = urljoin(base_url, tag['src'])
|
tag['src'] = urljoin(base_url, tag['src'])
|
||||||
print("Fixed: ",tag['src'])
|
|
||||||
return soup.prettify()
|
return soup.prettify()
|
||||||
|
|
||||||
async def is_html_content(content: bytes):
|
async def is_html_content(content: bytes):
|
||||||
try:
|
try:
|
||||||
content = content.decode(errors='ignore')
|
content = content.decode(errors='ignore')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
marks = ['<html','<img','<p','<span','<div']
|
marks = ['<html', '<img', '<p', '<span', '<div']
|
||||||
try:
|
try:
|
||||||
content = content.lower()
|
content = content.lower()
|
||||||
for mark in marks:
|
for mark in marks:
|
||||||
if mark in content:
|
if mark in content:
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex)
|
print(ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@time_cache_async(120)
|
@time_cache_async(120)
|
||||||
async def get(url):
|
async def get(url):
|
||||||
@ -79,5 +101,5 @@ async def get(url):
|
|||||||
response = await session.get(url)
|
response = await session.get(url)
|
||||||
content = await response.text()
|
content = await response.text()
|
||||||
if await is_html_content(content):
|
if await is_html_content(content):
|
||||||
content = (await repair_links(url,content)).encode()
|
content = (await repair_links(url, content)).encode()
|
||||||
return content
|
return content
|
64
src/snek/system/mapper.py
Normal file
64
src/snek/system/mapper.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
DEFAULT_LIMIT = 30
|
||||||
|
from snek.system.model import BaseModel
|
||||||
|
from snek.app import Application
|
||||||
|
import types
|
||||||
|
|
||||||
|
class Mapper:
|
||||||
|
|
||||||
|
model_class:BaseModel = None
|
||||||
|
default_limit:int = DEFAULT_LIMIT
|
||||||
|
table_name:str = None
|
||||||
|
|
||||||
|
def __init__(self, app:Application, table_name:str, model_class:BaseModel):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
if not self.model_class:
|
||||||
|
raise ValueError("Mapper configuration error: model_class is not set.")
|
||||||
|
self.model_class = model_class
|
||||||
|
|
||||||
|
self.table_name = table_name
|
||||||
|
if not self.table_name:
|
||||||
|
raise ValueError("Mapper configuration error: table_name is not set.")
|
||||||
|
self.default_limit = self.__class__.default_limit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self.app.db
|
||||||
|
|
||||||
|
async def new(self):
|
||||||
|
return self.model_class(mapper=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def table(self):
|
||||||
|
return self.db[self.table_name]
|
||||||
|
|
||||||
|
async def get(self, uid:str=None, **kwargs) -> types.Optional[BaseModel]
|
||||||
|
if uid:
|
||||||
|
kwargs['uid'] = uid
|
||||||
|
model = self.new()
|
||||||
|
record = self.table.find_one(**kwargs)
|
||||||
|
return self.model_class.from_record(mapper=self,record=record)
|
||||||
|
|
||||||
|
async def exists(self, **kwargs):
|
||||||
|
return self.table.exists(**kwargs)
|
||||||
|
|
||||||
|
async def count(self, **kwargs) -> int:
|
||||||
|
return self.table.count(**kwargs)
|
||||||
|
|
||||||
|
async def save(self, model:BaseModel) -> bool:
|
||||||
|
record = model.record
|
||||||
|
if not record.get('uid'):
|
||||||
|
raise Exception(f"Attempt to save without uid: {record}.")
|
||||||
|
return self.table.upsert(record,['uid'])
|
||||||
|
|
||||||
|
async def find(self, **kwargs) -> types.List[BaseModel]:
|
||||||
|
if not kwargs.get("_limit"):
|
||||||
|
kwargs["_limit"] = self.default_limit
|
||||||
|
for record in self.table.find(**kwargs):
|
||||||
|
yield self.model_class.from_record(mapper=self,record=record)
|
||||||
|
|
||||||
|
async def delete(self, kwargs=None)-> int:
|
||||||
|
if not kwargs or not isinstance(kwargs, dict):
|
||||||
|
raise Exception("Can't execute delete with no filter.")
|
||||||
|
return self.table.delete(**kwargs)
|
43
src/snek/system/markdown.py
Normal file
43
src/snek/system/markdown.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
|
||||||
|
|
||||||
|
from mistune import escape
|
||||||
|
from mistune import Markdown
|
||||||
|
from mistune import HTMLRenderer
|
||||||
|
from pygments import highlight
|
||||||
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
from pygments.formatters import html
|
||||||
|
from pygments.styles import get_style_by_name
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownRenderer(HTMLRenderer):
|
||||||
|
def __init__(self, app, template):
|
||||||
|
self.template = template
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
self.env = self.app.jinja2_env
|
||||||
|
formatter = html.HtmlFormatter()
|
||||||
|
self.env.globals['highlight_styles'] = formatter.get_style_defs()
|
||||||
|
def _escape(self,str):
|
||||||
|
return str ##escape(str)
|
||||||
|
def block_code(self, code, lang=None,info=None):
|
||||||
|
if not lang:
|
||||||
|
lang = info
|
||||||
|
if not lang:
|
||||||
|
return f"<div>{code}</div>"
|
||||||
|
#return '\n<pre><code>%s</code></pre>\n' % escape(code)
|
||||||
|
lexer = get_lexer_by_name(lang, stripall=True)
|
||||||
|
formatter = html.HtmlFormatter(lineseparator="<br>")
|
||||||
|
print(code, lang,info, flush=True)
|
||||||
|
return highlight(code, lexer, formatter)
|
||||||
|
def render(self):
|
||||||
|
markdown_string = self.app.template_path.joinpath(self.template).read_text()
|
||||||
|
renderer = MarkdownRenderer(self.app,self.template)
|
||||||
|
markdown = Markdown(renderer=renderer)
|
||||||
|
return markdown(markdown_string)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_markdown(app, markdown_string):
|
||||||
|
renderer = MarkdownRenderer(app,None)
|
||||||
|
markdown = Markdown(renderer=renderer)
|
||||||
|
return markdown(markdown_string)
|
@ -1,4 +1,12 @@
|
|||||||
from aiohttp import web
|
# Written by retoor@molodetz.nl
|
||||||
|
|
||||||
|
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
|
||||||
|
|
||||||
|
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
|
||||||
|
|
||||||
|
# MIT License: This code is distributed under the MIT License.
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def no_cors_middleware(request, handler):
|
async def no_cors_middleware(request, handler):
|
||||||
@ -7,16 +15,15 @@ async def no_cors_middleware(request, handler):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_allow_middleware(request ,handler):
|
async def cors_allow_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
|
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
|
||||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_middleware(request, handler):
|
async def cors_middleware(request, handler):
|
||||||
# Handle preflight (OPTIONS) requests
|
|
||||||
if request.method == "OPTIONS":
|
if request.method == "OPTIONS":
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
@ -24,7 +31,6 @@ async def cors_middleware(request, handler):
|
|||||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Handle actual requests
|
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
|
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
|
@ -1,36 +1,67 @@
|
|||||||
|
# Written by retoor@molodetz.nl
|
||||||
|
|
||||||
|
# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
|
||||||
|
|
||||||
|
# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
|
||||||
|
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from datetime import datetime , timezone
|
from datetime import datetime, timezone
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
|
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
|
||||||
|
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
return str(datetime.now(timezone.utc))
|
return str(datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
def add_attrs(**kwargs):
|
def add_attrs(**kwargs):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(func, key, value)
|
setattr(func, key, value)
|
||||||
|
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):
|
|
||||||
def decorator(func):
|
def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):
|
||||||
return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)
|
def decorator(func):
|
||||||
|
return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)
|
||||||
|
|
||||||
|
|
||||||
class Validator:
|
class Validator:
|
||||||
_index = 0
|
_index = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self,val):
|
def value(self, val):
|
||||||
self._value = json.loads(json.dumps(val,default=str))
|
self._value = json.loads(json.dumps(val, default=str))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):
|
def initial_value(self):
|
||||||
@ -39,48 +70,49 @@ class Validator:
|
|||||||
def custom_validation(self):
|
def custom_validation(self):
|
||||||
return True
|
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):
|
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
|
self.index = Validator._index
|
||||||
Validator._index += 1
|
Validator._index += 1
|
||||||
self.required = required
|
self.required = required
|
||||||
self.min_num = min_num
|
self.min_num = min_num
|
||||||
self.max_num = max_num
|
self.max_num = max_num
|
||||||
self.min_length = min_length
|
self.min_length = min_length
|
||||||
self.max_length = max_length
|
self.max_length = max_length
|
||||||
self.regex = regex
|
self.regex = regex
|
||||||
self._value = None
|
self._value = None
|
||||||
self.value = value
|
self.value = value
|
||||||
print("xxxx", value,flush=True)
|
print("xxxx", value, flush=True)
|
||||||
|
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
@property
|
|
||||||
|
@property
|
||||||
def errors(self):
|
def errors(self):
|
||||||
error_list = []
|
error_list = []
|
||||||
if self.value is None and self.required:
|
if self.value is None and self.required:
|
||||||
error_list.append("Field is required.")
|
error_list.append("Field is required.")
|
||||||
return error_list
|
return error_list
|
||||||
|
|
||||||
if self.value is None:
|
|
||||||
return error_list
|
|
||||||
|
|
||||||
if self.kind == float or self.kind == int:
|
if self.value is None:
|
||||||
|
return error_list
|
||||||
|
|
||||||
|
if self.kind in [int, float]:
|
||||||
if self.min_num is not None and self.value < self.min_num:
|
if self.min_num is not None and self.value < self.min_num:
|
||||||
error_list.append("Field should be minimal {}.".format(self.min_num))
|
error_list.append(f"Field should be minimal {self.min_num}.")
|
||||||
if self.max_num is not None and self.value > self.max_num:
|
if self.max_num is not None and self.value > self.max_num:
|
||||||
error_list.append("Field should be maximal {}.".format(self.max_num))
|
error_list.append(f"Field should be maximal {self.max_num}.")
|
||||||
if self.min_length is not None and len(self.value) < self.min_length:
|
if self.min_length is not None and len(self.value) < self.min_length:
|
||||||
error_list.append("Field should be minimal {} characters long.".format(self.min_length))
|
error_list.append(f"Field should be minimal {self.min_length} characters long.")
|
||||||
if self.max_length is not None and len(self.value) > self.max_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))
|
error_list.append(f"Field should be maximal {self.max_length} characters long.")
|
||||||
print(self.regex, self.value,flush=True)
|
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):
|
if self.regex and self.value and not re.match(self.regex, self.value):
|
||||||
error_list.append("Invalid value.".format(self.regex))
|
error_list.append("Invalid value.")
|
||||||
if not self.kind is None and type(self.value) != self.kind:
|
if self.kind and not isinstance(self.value, self.kind):
|
||||||
error_list.append("Invalid kind. It is supposed to be {}.".format(self.kind))
|
error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
|
||||||
return error_list
|
return error_list
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.errors:
|
if self.errors:
|
||||||
raise ValueError("\n", self.errors)
|
raise ValueError("\n", self.errors)
|
||||||
@ -94,8 +126,6 @@ class Validator:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
return {
|
return {
|
||||||
"required": self.required,
|
"required": self.required,
|
||||||
@ -109,25 +139,27 @@ class Validator:
|
|||||||
"help_text": self.help_text,
|
"help_text": self.help_text,
|
||||||
"errors": self.errors,
|
"errors": self.errors,
|
||||||
"is_valid": self.is_valid,
|
"is_valid": self.is_valid,
|
||||||
"index":self.index
|
"index": self.index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelField(Validator):
|
class ModelField(Validator):
|
||||||
|
|
||||||
index = 1
|
index = 1
|
||||||
def __init__(self,name=None,save=True, *args, **kwargs):
|
|
||||||
self.name = name
|
def __init__(self, name=None, save=True, *args, **kwargs):
|
||||||
|
self.name = name
|
||||||
self.save = save
|
self.save = save
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
result = super().to_json()
|
result = super().to_json()
|
||||||
result['name'] = self.name
|
result['name'] = self.name
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class CreatedField(ModelField):
|
class CreatedField(ModelField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):
|
def initial_value(self):
|
||||||
return now()
|
return now()
|
||||||
@ -136,67 +168,99 @@ class CreatedField(ModelField):
|
|||||||
if not self.value:
|
if not self.value:
|
||||||
self.value = now()
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class UpdatedField(ModelField):
|
class UpdatedField(ModelField):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.value = now()
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class DeletedField(ModelField):
|
class DeletedField(ModelField):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.value = now()
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class UUIDField(ModelField):
|
class UUIDField(ModelField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):
|
def initial_value(self):
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
|
|
||||||
uid = UUIDField(name="uid",required=True)
|
|
||||||
created_at = CreatedField(name="created_at",required=True, regex=TIMESTAMP_REGEX, place_holder="Created at")
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
uid = UUIDField(name="uid", required=True)
|
||||||
|
created_at = CreatedField(name="created_at", required=True, regex=TIMESTAMP_REGEX, place_holder="Created at")
|
||||||
|
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")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_record(cls, record, mapper):
|
||||||
|
model = cls.__new__()
|
||||||
|
model.mapper = mapper
|
||||||
|
model.record = record
|
||||||
|
return model
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapper(self):
|
||||||
|
return self._mapper
|
||||||
|
|
||||||
|
@mapper.setter
|
||||||
|
def mapper(self, value):
|
||||||
|
self._mapper = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def record(self):
|
||||||
|
return {field.name: field.value for field in self.fields}
|
||||||
|
|
||||||
|
@record.setter
|
||||||
|
def record(self, value):
|
||||||
|
for key, value in self._record.items():
|
||||||
|
field = self.fields.get(key)
|
||||||
|
if not field:
|
||||||
|
continue
|
||||||
|
field.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
print(self.__dict__)
|
print(self.__dict__)
|
||||||
print(dir(self.__class__))
|
print(dir(self.__class__))
|
||||||
|
self._mapper = None
|
||||||
self.fields = {}
|
self.fields = {}
|
||||||
for key in dir(self.__class__):
|
for key in dir(self.__class__):
|
||||||
obj = getattr(self.__class__,key)
|
obj = getattr(self.__class__, key)
|
||||||
|
|
||||||
if isinstance(obj,Validator):
|
if isinstance(obj, Validator):
|
||||||
self.__dict__[key] = copy.deepcopy(obj)
|
self.__dict__[key] = copy.deepcopy(obj)
|
||||||
print("JAAA")
|
print("JAAA")
|
||||||
self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)
|
self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)
|
||||||
self.fields[key] = self.__dict__[key]
|
self.fields[key] = self.__dict__[key]
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
obj = self.__dict__.get(key)
|
obj = self.__dict__.get(key)
|
||||||
if isinstance(obj,Validator):
|
if isinstance(obj, Validator):
|
||||||
obj.value = value
|
obj.value = value
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
obj = self.__dict__.get(key)
|
obj = self.__dict__.get(key)
|
||||||
if isinstance(obj,Validator):
|
if isinstance(obj, Validator):
|
||||||
print("HPAPP")
|
print("HPAPP")
|
||||||
return obj.value
|
return obj.value
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def set_user_data(self, data):
|
def set_user_data(self, data):
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
field = self.fields.get(key)
|
field = self.fields.get(key)
|
||||||
if not field:
|
if not field:
|
||||||
continue
|
continue
|
||||||
if value.get('name'):
|
if value.get('name'):
|
||||||
value = value.get('value')
|
value = value.get('value')
|
||||||
field.value = value
|
field.value = value
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
|
|
||||||
|
@property
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
for field in self.fields.values():
|
for field in self.fields.values():
|
||||||
if not field.is_valid:
|
if not field.is_valid:
|
||||||
@ -205,46 +269,44 @@ class BaseModel:
|
|||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
obj = self.__dict__.get(key)
|
obj = self.__dict__.get(key)
|
||||||
if isinstance(obj,Validator):
|
if isinstance(obj, Validator):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
def __setattr__(self, key, value):
|
||||||
obj = getattr(self,key)
|
obj = getattr(self, key)
|
||||||
if isinstance(obj,Validator):
|
if isinstance(obj, Validator):
|
||||||
obj.value = value
|
obj.value = value
|
||||||
else:
|
else:
|
||||||
self.__dict__[key] = value #setattr(self,key,value)
|
self.__dict__[key] = value
|
||||||
#def __getattr__(self, key):
|
|
||||||
# obj = self.__dict__.get(key)
|
@property
|
||||||
# if isinstance(obj,Validator):
|
|
||||||
# return obj.value
|
|
||||||
@property
|
|
||||||
def record(self):
|
def record(self):
|
||||||
obj = self.to_json()
|
obj = self.to_json()
|
||||||
record = {}
|
record = {}
|
||||||
for key,value in obj.items():
|
for key, value in obj.items():
|
||||||
if getattr(self,key).save:
|
if getattr(self, key).save:
|
||||||
record[key] = value.get('value')
|
record[key] = value.get('value')
|
||||||
return record
|
return record
|
||||||
|
|
||||||
def to_json(self,encode=False):
|
def to_json(self, encode=False):
|
||||||
model_data = OrderedDict({
|
model_data = OrderedDict({
|
||||||
"uid": self.uid.value,
|
"uid": self.uid.value,
|
||||||
"created_at": self.created_at.value,
|
"created_at": self.created_at.value,
|
||||||
"updated_at": self.updated_at.value,
|
"updated_at": self.updated_at.value,
|
||||||
"deleted_at": self.deleted_at.value
|
"deleted_at": self.deleted_at.value
|
||||||
})
|
})
|
||||||
|
|
||||||
for key,value in self.__dict__.items():
|
for key, value in self.__dict__.items():
|
||||||
if key == "record":
|
if key == "record":
|
||||||
continue
|
continue
|
||||||
value = self.__dict__[key]
|
value = self.__dict__[key]
|
||||||
if hasattr(value,"value"):
|
if hasattr(value, "value"):
|
||||||
model_data[key] = value.to_json()
|
model_data[key] = value.to_json()
|
||||||
if encode:
|
if encode:
|
||||||
return json.dumps(model_data,indent=2)
|
return json.dumps(model_data, indent=2)
|
||||||
return model_data
|
return model_data
|
||||||
|
|
||||||
|
|
||||||
class FormElement(ModelField):
|
class FormElement(ModelField):
|
||||||
|
|
||||||
def __init__(self, place_holder=None, *args, **kwargs):
|
def __init__(self, place_holder=None, *args, **kwargs):
|
||||||
@ -252,15 +314,14 @@ class FormElement(ModelField):
|
|||||||
self.place_holder = place_holder
|
self.place_holder = place_holder
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FormElement(ModelField):
|
class FormElement(ModelField):
|
||||||
|
|
||||||
def __init__(self,place_holder=None, *args, **kwargs):
|
def __init__(self, place_holder=None, *args, **kwargs):
|
||||||
self.place_holder = place_holder
|
self.place_holder = place_holder
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
data = super().to_json()
|
data = super().to_json()
|
||||||
data["name"] = self.name
|
data["name"] = self.name
|
||||||
data["place_holder"] = self.place_holder
|
data["place_holder"] = self.place_holder
|
||||||
return data
|
return data
|
20
src/snek/system/security.py
Normal file
20
src/snek/system/security.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import hashlib
|
||||||
|
|
||||||
|
DEFAULT_SALT = b"snekker-de-snek-"
|
||||||
|
|
||||||
|
async def hash(data,salt=DEFAULT_SALT):
|
||||||
|
try:
|
||||||
|
data = data.encode(errors="ignore")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
salt = salt.encode(errors="ignore")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
salted = salt + data
|
||||||
|
|
||||||
|
obj = hashlib.sha256(salted)
|
||||||
|
return obj.hexdigest()
|
||||||
|
|
||||||
|
async def verify(string:str, hashed:str):
|
||||||
|
return await hash(string) == hashed
|
40
src/snek/system/service.py
Normal file
40
src/snek/system/service.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from snek.mapper import get_mapper
|
||||||
|
from snek.system.mapper import BaseMapper
|
||||||
|
from snek.model.user import UserModel
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
|
||||||
|
mapper_name:BaseMapper = None
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
if self.mapper_name:
|
||||||
|
self.mapper = get_mapper(self.mapper_name, app=self.app)
|
||||||
|
else:
|
||||||
|
self.mapper = None
|
||||||
|
|
||||||
|
async def exists(self, **kwargs):
|
||||||
|
return self.mapper.exists(**kwargs)
|
||||||
|
|
||||||
|
async def count(self, **kwargs):
|
||||||
|
return self.mapper.count(**kwargs)
|
||||||
|
|
||||||
|
async def new(self, **kwargs):
|
||||||
|
return await self.mapper.new()
|
||||||
|
|
||||||
|
async def get(self, **kwargs):
|
||||||
|
return await self.mapper.get(**kwargs)
|
||||||
|
|
||||||
|
async def save(self, model:UserModel):
|
||||||
|
if model.is_valid:
|
||||||
|
return self.mapper.save(model) and True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def find(self, **kwargs):
|
||||||
|
return await self.mapper.find(**kwargs)
|
||||||
|
|
||||||
|
async def delete(self, **kwargs):
|
||||||
|
return await self.mapper.delete(**kwargs)
|
38
src/snek/system/view.py
Normal file
38
src/snek/system/view.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from snek.system.markdown import render_markdown
|
||||||
|
|
||||||
|
class BaseView(web.View):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self):
|
||||||
|
return self.request.app
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self.app.db
|
||||||
|
|
||||||
|
async def json_response(self, data):
|
||||||
|
return web.json_response(data)
|
||||||
|
|
||||||
|
async def render_template(self, template_name, context=None):
|
||||||
|
if template_name.endswith(".md"):
|
||||||
|
response = await self.request.app.render_template(template_name,self.request,context)
|
||||||
|
body = await render_markdown(self.app, response.body.decode())
|
||||||
|
return web.Response(body=body,content_type="text/html")
|
||||||
|
return await self.request.app.render_template(template_name, self.request,context)
|
||||||
|
|
||||||
|
class BaseFormView(BaseView):
|
||||||
|
|
||||||
|
form = None
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
form = self.form()
|
||||||
|
return await 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 await self.json_response(form.to_json())
|
||||||
|
|
7
src/snek/templates/about.html
Normal file
7
src/snek/templates/about.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<html-frame url="/about.md"></html-frame>
|
||||||
|
<fancy-button text="Back" url="/web.html"></fancy-button>
|
||||||
|
{% endblock %}
|
15
src/snek/templates/about.md
Normal file
15
src/snek/templates/about.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Snek
|
||||||
|
|
||||||
|
## What is a snek?
|
||||||
|
A snek is a danger noodle.
|
||||||
|
|
||||||
|
## Design choices
|
||||||
|
I made several design choices:
|
||||||
|
- Implemented **the worst 3rd party markdown to html renderer ever**. See this nice *bullet list*.
|
||||||
|
- Only password requirement is thats it requires six characters. Users are responsibly for their own security. Snek is not so arrogant to determine if a password is strong enough. It's up to what user prefers. Snek does not have a forgot-my-password service tho.
|
||||||
|
- Email is not required for registration. Email is (maybe) used in future for resetting password.
|
||||||
|
- Database is SQLite by default. Postgres is also possible. In that case you have to change `db_path` prefix to `postgres:///` and add a postgres docker container to the docker setup.
|
||||||
|
- Homebrew made ORM framework based on dataset.
|
||||||
|
- Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.
|
||||||
|
- !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.
|
||||||
|
- No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.
|
@ -4,9 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
<script src="/fancy-button.js"></script>
|
<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="/html-frame.js"></script>
|
||||||
<script src="/generic-form.js"></script>
|
<script src="/generic-form.js"></script>
|
||||||
<link rel="stylesheet" href="/html-frame.css"></script>
|
<link rel="stylesheet" href="/html-frame.css"></script>
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
<fancy-button url="/login" text="Login"></fancy-button>
|
<fancy-button url="/login" text="Login"></fancy-button>
|
||||||
<span style="padding:10px;">Or</span>
|
<span style="padding:10px;">Or</span>
|
||||||
<fancy-button url="/register" text="Register"></fancy-button>
|
<fancy-button url="/register" text="Register"></fancy-button>
|
||||||
|
<a href="/about.html">Design choices</a>
|
||||||
|
<a href="/web.html">See web Application so far</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<generic-form url="/login-form"></generic-form>
|
<generic-form url="/login-form.json"></generic-form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<generic-form url="/register-form"></generic-form>
|
<generic-form url="/register-form.json"></generic-form>
|
||||||
{% endblock %}
|
{% endblock %}
|
14
src/snek/view/about.py
Normal file
14
src/snek/view/about.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class AboutHTMLView(BaseView):
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("about.html")
|
||||||
|
|
||||||
|
class AboutMDView(BaseView):
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("about.md")
|
@ -1,31 +0,0 @@
|
|||||||
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())
|
|
@ -1,4 +1,4 @@
|
|||||||
from snek.view.base import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
class IndexView(BaseView):
|
class IndexView(BaseView):
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from snek.form.register import RegisterForm
|
from snek.form.register import RegisterForm
|
||||||
from snek.view.base import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
class LoginView(BaseView):
|
class LoginView(BaseView):
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from snek.view.base import BaseFormView
|
from snek.system.view import BaseFormView
|
||||||
from snek.form.login import LoginForm
|
from snek.form.login import LoginForm
|
||||||
|
|
||||||
class LoginFormView(BaseFormView):
|
class LoginFormView(BaseFormView):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from snek.view.base import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
class RegisterView(BaseView):
|
class RegisterView(BaseView):
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from snek.form.register import RegisterForm
|
from snek.form.register import RegisterForm
|
||||||
from snek.view.base import BaseFormView
|
from snek.system.view import BaseFormView
|
||||||
|
|
||||||
class RegisterFormView(BaseFormView):
|
class RegisterFormView(BaseFormView):
|
||||||
form = RegisterForm
|
form = RegisterForm
|
@ -1,4 +1,4 @@
|
|||||||
from snek.view.base import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
class WebView(BaseView):
|
class WebView(BaseView):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user