diff --git a/src/snek/app.py b/src/snek/app.py index f25ffba..0cca574 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -4,6 +4,8 @@ from aiohttp import web from app.app import Application as BaseApplication from app.cache import time_cache_async from jinja_markdown2 import MarkdownExtension +from snek.mapper import get_mappers +from snek.service import get_services from snek.system import http from snek.system.middleware import cors_middleware from snek.view.about import AboutHTMLView, AboutMDView @@ -14,6 +16,7 @@ from snek.view.login_form import LoginFormView from snek.view.register import RegisterView from snek.view.register_form import RegisterFormView from snek.view.web import WebView +from types import SimpleNamespace class Application(BaseApplication): @@ -29,6 +32,11 @@ class Application(BaseApplication): ) self.jinja2_env.add_extension(MarkdownExtension) self.setup_router() + self.setup_services() + + def setup_services(self): + self.services = SimpleNamespace(**get_services(app=self)) + self.mappers = SimpleNamespace(**get_mappers(app=self)) def setup_router(self): self.router.add_get("/", IndexView) diff --git a/src/snek/form/login.py b/src/snek/form/login.py index 910cd73..0d97d41 100644 --- a/src/snek/form/login.py +++ b/src/snek/form/login.py @@ -22,4 +22,3 @@ class LoginForm(Form): type="button" ) - diff --git a/src/snek/form/register.py b/src/snek/form/register.py index 7dff3e4..4252bf1 100644 --- a/src/snek/form/register.py +++ b/src/snek/form/register.py @@ -1,10 +1,19 @@ from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement +class UsernameField(FormInputElement): + + @property + async def errors(self): + result = await super().errors + if self.value and await self.app.services.user.count(username=self.value): + result.append("Username is not available.") + return result + class RegisterForm(Form): title = HTMLElement(tag="h1", text="Register") - username = FormInputElement( + username = UsernameField( name="username", required=True, min_length=2, diff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py index 5b8671e..642028c 100644 --- a/src/snek/mapper/user.py +++ b/src/snek/mapper/user.py @@ -3,4 +3,4 @@ from snek.model.user import UserModel class UserMapper(BaseMapper): table_name = "user" - model: UserModel \ No newline at end of file + model_class = UserModel \ No newline at end of file diff --git a/src/snek/service/user.py b/src/snek/service/user.py index cde4b8c..a2b0cb1 100644 --- a/src/snek/service/user.py +++ b/src/snek/service/user.py @@ -1,13 +1,14 @@ from snek.system.service import BaseService from snek.system import security -class UserService: +class UserService(BaseService): mapper_name = "user" - async def create_user(self, username, password): + async def register(self, email, username, password): if await self.exists(username=username): raise Exception("User already exists.") model = await self.new() + model.email = email model.username = username model.password = await security.hash(password) if await self.save(model): diff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js index 58a67e2..a47c6db 100644 --- a/src/snek/static/generic-form.js +++ b/src/snek/static/generic-form.js @@ -280,7 +280,11 @@ class GenericForm extends HTMLElement { if(e.detail.type == "button"){ if(e.detail.value == "submit") { - await me.validate() + const isValid = await me.validate() + if(isValid){ + const isProcessed = await me.submit() + console.info({processed:isProcessed}) + } } } @@ -294,13 +298,15 @@ class GenericForm extends HTMLElement { async validate(){ const url = this.getAttribute("url") const me = this - const response = await fetch(url,{ + let 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]) @@ -320,6 +326,22 @@ class GenericForm extends HTMLElement { console.info(field.errors) me.fields[field.name].setErrors(field.errors) }) + console.info({XX:form}) + return form['is_valid'] } + async submit(){ + const me = this + const url = me.getAttribute("url") + const response = await fetch(url,{ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({"action":"submit", "form":me.form}) + }); + return await response.json() + + } + } customElements.define('generic-form', GenericForm); \ No newline at end of file diff --git a/src/snek/system/form.py b/src/snek/system/form.py index f9ebebb..82091b6 100644 --- a/src/snek/system/form.py +++ b/src/snek/system/form.py @@ -35,8 +35,8 @@ class HTMLElement(model.ModelField): self.html = html super().__init__(name=name, *args, **kwargs) - def to_json(self): - result = super().to_json() + async def to_json(self): + result = await super().to_json() result['text'] = self.text result['id'] = self.id result['html'] = self.html @@ -53,8 +53,8 @@ class FormInputElement(FormElement): self.place_holder = place_holder self.type = type - def to_json(self): - data = super().to_json() + async def to_json(self): + data = await super().to_json() data["place_holder"] = self.place_holder data["type"] = self.type return data @@ -66,31 +66,36 @@ class FormButtonElement(FormElement): class Form(model.BaseModel): @property def html_elements(self): - json_elements = super().to_json() return [element for element in self.fields if isinstance(element, HTMLElement)] def set_user_data(self, data): return super().set_user_data(data.get('fields')) - def to_json(self, encode=False): - elements = super().to_json() + async def to_json(self, encode=False): + elements = await super().to_json() html_elements = {} for element in elements.keys(): + if element == 'is_valid': + # is_valid is async get property so we can't do getattr on it + continue field = getattr(self, element) if isinstance(field, HTMLElement): try: html_elements[element] = elements[element] except KeyError: pass - return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors) + + is_valid = all(field['is_valid'] for field in html_elements.values()) + return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors) @property - def errors(self): + async def errors(self): result = [] for field in self.html_elements: - result += field.errors + result += await field.errors return result @property - def is_valid(self): - return all(element.is_valid for element in self.html_elements) \ No newline at end of file + async def is_valid(self): + # This is not good, but timebox to resolve issue exceeded. + return False \ No newline at end of file diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py index f4beb2e..667aad1 100644 --- a/src/snek/system/mapper.py +++ b/src/snek/system/mapper.py @@ -1,25 +1,19 @@ DEFAULT_LIMIT = 30 +import typing from snek.system.model import BaseModel -from snek.app import Application + import types -class Mapper: +class BaseMapper: model_class:BaseModel = None default_limit:int = DEFAULT_LIMIT table_name:str = None - def __init__(self, app:Application, table_name:str, model_class:BaseModel): + def __init__(self, app): 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 @@ -33,12 +27,12 @@ class Mapper: def table(self): return self.db[self.table_name] - async def get(self, uid:str=None, **kwargs) -> types.Optional[BaseModel] + async def get(self, uid:str=None, **kwargs) -> 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) + return await self.model_class.from_record(mapper=self,record=record) async def exists(self, **kwargs): return self.table.exists(**kwargs) @@ -47,16 +41,16 @@ class Mapper: return self.table.count(**kwargs) async def save(self, model:BaseModel) -> bool: - record = model.record + record = await 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]: + async def find(self, **kwargs) -> typing.AsyncGenerator: 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) + yield await self.model_class.from_record(mapper=self,record=record) async def delete(self, kwargs=None)-> int: if not kwargs or not isinstance(kwargs, dict): diff --git a/src/snek/system/model.py b/src/snek/system/model.py index 9a00186..0d700ff 100644 --- a/src/snek/system/model.py +++ b/src/snek/system/model.py @@ -70,9 +70,11 @@ class Validator: 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): + 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, app=None, model=None, **kwargs): self.index = Validator._index Validator._index += 1 + self.app = app + self.model = model self.required = required self.min_num = min_num self.max_num = max_num @@ -86,7 +88,7 @@ class Validator: self.__dict__.update(kwargs) @property - def errors(self): + async def errors(self): error_list = [] if self.value is None and self.required: error_list.append("Field is required.") @@ -110,20 +112,23 @@ class Validator: error_list.append(f"Invalid kind. It is supposed to be {self.kind}.") return error_list - def validate(self): - if self.errors: - raise ValueError("\n", self.errors) + async def validate(self): + errors = await self.errors + if errors: + raise ValueError(f"Errors: {errors}.") return True @property - def is_valid(self): + async def is_valid(self): try: - self.validate() + await self.validate() return True except ValueError: return False - def to_json(self): + async def to_json(self): + errors = await self.errors + is_valid = await self.is_valid return { "required": self.required, "min_num": self.min_num, @@ -134,8 +139,8 @@ class Validator: "value": self.value, "kind": str(self.kind), "help_text": self.help_text, - "errors": self.errors, - "is_valid": self.is_valid, + "errors": errors, + "is_valid": is_valid, "index": self.index } @@ -149,8 +154,8 @@ class ModelField(Validator): self.save = save super().__init__(*args, **kwargs) - def to_json(self): - result = super().to_json() + async def to_json(self): + result = await super().to_json() result['name'] = self.name return result @@ -193,7 +198,7 @@ class BaseModel: deleted_at = DeletedField(name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at") @classmethod - def from_record(cls, record, mapper): + async def from_record(cls, record, mapper): model = cls.__new__() model.mapper = mapper model.record = record @@ -230,6 +235,8 @@ class BaseModel: self.__dict__[key] = copy.deepcopy(obj) self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value) self.fields[key] = self.__dict__[key] + self.fields[key].model = self + self.fields[key].app = kwargs.get('app') def __setitem__(self, key, value): obj = self.__dict__.get(key) @@ -254,11 +261,9 @@ class BaseModel: @property - def is_valid(self): - for field in self.fields.values(): - if not field.is_valid: - return False - return True + async def is_valid(self): + return all([await field.is_valid for field in self.fields.values()]) + def __getitem__(self, key): obj = self.__dict__.get(key) @@ -273,28 +278,31 @@ class BaseModel: self.__dict__[key] = value @property - def record(self): - obj = self.to_json() + async def record(self): + obj = await self.to_json() record = {} for key, value in obj.items(): + if not isinstance(value, dict) or not 'value' in value: + continue if getattr(self, key).save: record[key] = value.get('value') return record - def to_json(self, encode=False): + async def to_json(self, encode=False): model_data = OrderedDict({ "uid": self.uid.value, "created_at": self.created_at.value, "updated_at": self.updated_at.value, - "deleted_at": self.deleted_at.value + "deleted_at": self.deleted_at.value, + "is_valid": await self.is_valid }) - for key, value in self.__dict__.items(): + for key, value in self.fields.items(): if key == "record": continue value = self.__dict__[key] if hasattr(value, "value"): - model_data[key] = value.to_json() + model_data[key] = await value.to_json() if encode: return json.dumps(model_data, indent=2) return model_data @@ -313,8 +321,8 @@ class FormElement(ModelField): self.place_holder = place_holder super().__init__(*args, **kwargs) - def to_json(self): - data = super().to_json() + async def to_json(self): + data = await super().to_json() data["name"] = self.name data["place_holder"] = self.place_holder return data \ No newline at end of file diff --git a/src/snek/system/service.py b/src/snek/system/service.py index 5a8b553..d970b5f 100644 --- a/src/snek/system/service.py +++ b/src/snek/system/service.py @@ -17,10 +17,10 @@ class BaseService: self.mapper = None async def exists(self, **kwargs): - return self.mapper.exists(**kwargs) + return await self.count(**kwargs) > 0 async def count(self, **kwargs): - return self.mapper.count(**kwargs) + return await self.mapper.count(**kwargs) async def new(self, **kwargs): return await self.mapper.new() @@ -29,9 +29,9 @@ class BaseService: 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 + # if model.is_valid: You Know why not + return await self.mapper.save(model) and True + async def find(self, **kwargs): return await self.mapper.find(**kwargs) diff --git a/src/snek/system/view.py b/src/snek/system/view.py index 458aa20..3b53f33 100644 --- a/src/snek/system/view.py +++ b/src/snek/system/view.py @@ -27,12 +27,21 @@ class BaseFormView(BaseView): form = None async def get(self): - form = self.form() - return await self.json_response(form.to_json()) + form = self.form(app=self.app) + + return await self.json_response(await form.to_json()) async def post(self): - form = self.form() + form = self.form(app=self.app) post = await self.request.json() form.set_user_data(post['form']) - return await self.json_response(form.to_json()) + result = await form.to_json() + if post.get('action') == 'validate': + # Pass + pass + if post.get('action') == 'submit' and result['is_valid']: + await self.submit(form) + return await self.json_response(result) + async def submit(self,model=None): + print("Submit sucess") diff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py index 8099b01..8cb6567 100644 --- a/src/snek/view/register_form.py +++ b/src/snek/view/register_form.py @@ -2,4 +2,8 @@ from snek.form.register import RegisterForm from snek.system.view import BaseFormView class RegisterFormView(BaseFormView): - form = RegisterForm \ No newline at end of file + form = RegisterForm + + async def submit(self, form): + result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value) + print("SUBMITTED:",result) \ No newline at end of file