generate model, html templates
This commit is contained in:
parent
c665454045
commit
6c45fca97e
12
oshipka.sh
12
oshipka.sh
@ -8,9 +8,10 @@ echo "oshipka is at: $OSHIPKA_PATH"
|
|||||||
|
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
HELP="
|
HELP="
|
||||||
Usage $0 [ bootstrap | worker | web | venv | install | link | cert ]
|
Usage $0 [ bootstrap | model | init | worker | web | venv | install | link | cert ]
|
||||||
|
|
||||||
bootstrap [PROJECT_PATH] Create a new project in PROJECT_PATH
|
bootstrap [PROJECT_PATH] Create a new project in PROJECT_PATH
|
||||||
|
model [MODEL_NAME] Create or update a model
|
||||||
init Install dev env
|
init Install dev env
|
||||||
|
|
||||||
worker Start worker
|
worker Start worker
|
||||||
@ -107,11 +108,20 @@ run_in_prod() {
|
|||||||
gunicorn -w 4 -b 0.0.0.0:${PORT} run:app
|
gunicorn -w 4 -b 0.0.0.0:${PORT} run:app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model() {
|
||||||
|
shift
|
||||||
|
MODEL_NAME=$1
|
||||||
|
source venv/bin/activate
|
||||||
|
python "${OSHIPKA_PATH}/vm_gen/vm_gen.py" "${MODEL_NAME}" "`pwd`"
|
||||||
|
}
|
||||||
|
|
||||||
command_main() {
|
command_main() {
|
||||||
INITIAL_COMMAND=$1
|
INITIAL_COMMAND=$1
|
||||||
case "$INITIAL_COMMAND" in
|
case "$INITIAL_COMMAND" in
|
||||||
bootstrap) bootstrap "$@"
|
bootstrap) bootstrap "$@"
|
||||||
;;
|
;;
|
||||||
|
model) model "$@"
|
||||||
|
;;
|
||||||
init) init "$@"
|
init) init "$@"
|
||||||
;;
|
;;
|
||||||
worker) worker "$@"
|
worker) worker "$@"
|
||||||
|
@ -20,6 +20,7 @@ from sqlalchemy_utils import Choice
|
|||||||
from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago
|
from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago
|
||||||
from whooshalchemy import IndexService
|
from whooshalchemy import IndexService
|
||||||
from oshipka.search import add_to_index, remove_from_index, query_index
|
from oshipka.search import add_to_index, remove_from_index, query_index
|
||||||
|
from util.strings import camel_case_to_snake_case
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@ -57,16 +58,6 @@ class LiberalBoolean(TypeDecorator):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def camel_case_to_snake_case(name):
|
|
||||||
"""
|
|
||||||
Convertes a CamelCase name to snake_case
|
|
||||||
:param name: the name to be converted
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
||||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
||||||
|
|
||||||
|
|
||||||
class ModelController(ModelJsonEncoder):
|
class ModelController(ModelJsonEncoder):
|
||||||
"""
|
"""
|
||||||
This interface is the parent of all models in our database.
|
This interface is the parent of all models in our database.
|
||||||
|
@ -5,9 +5,23 @@ import inflect
|
|||||||
from flask import flash, render_template, redirect, request, url_for
|
from flask import flash, render_template, redirect, request, url_for
|
||||||
|
|
||||||
from oshipka.persistance import db
|
from oshipka.persistance import db
|
||||||
|
from util.strings import camel_case_to_snake_case
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance(model_view, uuid):
|
||||||
|
model = model_view.model
|
||||||
|
if uuid.isdigit():
|
||||||
|
instance = model.query.filter_by(id=uuid).first()
|
||||||
|
else:
|
||||||
|
instance = model.query.filter_by(uuid=uuid).first()
|
||||||
|
if not instance:
|
||||||
|
flash("No {}:{}".format(model_view.model_name, uuid))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
def list_view(model_view, template, template_ctx_func=None):
|
def list_view(model_view, template, template_ctx_func=None):
|
||||||
|
template = template if template else "{}/list.html".format(model_view.model_name)
|
||||||
|
|
||||||
def inner():
|
def inner():
|
||||||
instances = model_view.model.query.all()
|
instances = model_view.model.query.all()
|
||||||
template_ctx = template_ctx_func(instances) if template_ctx_func else {}
|
template_ctx = template_ctx_func(instances) if template_ctx_func else {}
|
||||||
@ -17,11 +31,11 @@ def list_view(model_view, template, template_ctx_func=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_view(model_view, template, template_ctx_func=None):
|
def get_view(model_view, template, template_ctx_func=None):
|
||||||
|
template = template if template else "{}/get.html".format(model_view.model_name)
|
||||||
|
|
||||||
def inner(uuid):
|
def inner(uuid):
|
||||||
model = model_view.model
|
instance = get_instance(model_view, uuid)
|
||||||
instance = model.query.filter_by(uuid=uuid).first()
|
|
||||||
if not instance:
|
if not instance:
|
||||||
flash("No {}:{}".format(model_view.model_name, uuid))
|
|
||||||
return redirect(request.referrer or url_for('home'))
|
return redirect(request.referrer or url_for('home'))
|
||||||
template_ctx = template_ctx_func(instance) if template_ctx_func else {}
|
template_ctx = template_ctx_func(instance) if template_ctx_func else {}
|
||||||
return render_template(template, instance=instance, **template_ctx)
|
return render_template(template, instance=instance, **template_ctx)
|
||||||
@ -34,11 +48,11 @@ def serialize_form():
|
|||||||
|
|
||||||
|
|
||||||
def update_view(model_view, template, pre_process_func=None, post_process_func=None, **kwargs):
|
def update_view(model_view, template, pre_process_func=None, post_process_func=None, **kwargs):
|
||||||
|
template = template if template else "{}/edit.html".format(model_view.model_name)
|
||||||
|
|
||||||
def inner(uuid):
|
def inner(uuid):
|
||||||
model = model_view.model
|
instance = get_instance(model_view, uuid)
|
||||||
instance = model.query.filter_by(uuid=uuid).first()
|
|
||||||
if not instance:
|
if not instance:
|
||||||
flash("No {}:{}".format(model_view.model_name, uuid))
|
|
||||||
return redirect(request.referrer or url_for('home'))
|
return redirect(request.referrer or url_for('home'))
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template(template, instance=instance)
|
return render_template(template, instance=instance)
|
||||||
@ -83,10 +97,8 @@ def create_view(model_view, template, template_ctx_func=None, post_add=None, pos
|
|||||||
|
|
||||||
def delete_view(model_view):
|
def delete_view(model_view):
|
||||||
def inner(uuid):
|
def inner(uuid):
|
||||||
model = model_view.model
|
instance = get_instance(model_view, uuid)
|
||||||
instance = model.query.filter_by(uuid=uuid).first()
|
|
||||||
if not instance:
|
if not instance:
|
||||||
flash("No {}:{}".format(model_view.model_name, uuid))
|
|
||||||
return redirect(request.referrer or url_for('home'))
|
return redirect(request.referrer or url_for('home'))
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template("delete_instance.html", instance=instance,
|
return render_template("delete_instance.html", instance=instance,
|
||||||
@ -109,29 +121,29 @@ class ModelView(object):
|
|||||||
|
|
||||||
p = inflect.engine()
|
p = inflect.engine()
|
||||||
|
|
||||||
self.model_name = model.__name__.lower()
|
self.model_name = camel_case_to_snake_case(model.__name__)
|
||||||
self.model_name_pl = p.plural(self.model_name)
|
self.model_name_pl = p.plural(self.model_name)
|
||||||
|
|
||||||
def register_create(self, list_template, **kwargs):
|
def register_create(self, create_template=None, **kwargs):
|
||||||
url = '/{}/create'.format(self.model_name_pl)
|
url = '/{}/create'.format(self.model_name_pl)
|
||||||
self.app.add_url_rule(url, methods=["GET", "POST"],
|
self.app.add_url_rule(url, methods=["GET", "POST"],
|
||||||
endpoint='create_{}'.format(self.model_name),
|
endpoint='create_{}'.format(self.model_name),
|
||||||
view_func=create_view(self, list_template, **kwargs))
|
view_func=create_view(self, create_template, **kwargs))
|
||||||
|
|
||||||
def register_list(self, list_template, **kwargs):
|
def register_list(self, list_template=None, **kwargs):
|
||||||
url = '/{}'.format(self.model_name_pl)
|
url = '/{}'.format(self.model_name_pl)
|
||||||
self.app.add_url_rule(url,
|
self.app.add_url_rule(url,
|
||||||
'list_{}'.format(self.model_name),
|
'list_{}'.format(self.model_name),
|
||||||
list_view(self, list_template, **kwargs))
|
list_view(self, list_template, **kwargs))
|
||||||
|
|
||||||
def register_get(self, retrieve_template, **kwargs):
|
def register_get(self, retrieve_template=None, **kwargs):
|
||||||
url = '/{}/<uuid>'.format(self.model_name_pl)
|
url = '/{}/<uuid>'.format(self.model_name_pl)
|
||||||
|
|
||||||
self.app.add_url_rule(url,
|
self.app.add_url_rule(url,
|
||||||
'get_{}'.format(self.model_name),
|
'get_{}'.format(self.model_name),
|
||||||
get_view(self, retrieve_template, **kwargs))
|
get_view(self, retrieve_template, **kwargs))
|
||||||
|
|
||||||
def register_update(self, update_template, **kwargs):
|
def register_update(self, update_template=None, **kwargs):
|
||||||
url = '/{}/<uuid>/edit'.format(self.model_name_pl)
|
url = '/{}/<uuid>/edit'.format(self.model_name_pl)
|
||||||
self.app.add_url_rule(url, methods=["GET", "POST"],
|
self.app.add_url_rule(url, methods=["GET", "POST"],
|
||||||
endpoint='update_{}'.format(self.model_name),
|
endpoint='update_{}'.format(self.model_name),
|
||||||
|
@ -24,6 +24,7 @@ passlib==1.7.2
|
|||||||
pathtools==0.1.2
|
pathtools==0.1.2
|
||||||
pycparser==2.20
|
pycparser==2.20
|
||||||
pytz==2019.3
|
pytz==2019.3
|
||||||
|
pyyaml==5.3.1
|
||||||
six==1.14.0
|
six==1.14.0
|
||||||
speaklater==1.3
|
speaklater==1.3
|
||||||
SQLAlchemy==1.3.15
|
SQLAlchemy==1.3.15
|
||||||
|
20
util/strings.py
Normal file
20
util/strings.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def camel_case_to_snake_case(name):
|
||||||
|
"""
|
||||||
|
Convertes a CamelCase name to snake_case
|
||||||
|
:param name: the name to be converted
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||||
|
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def snake_case_to_camel_case(name):
|
||||||
|
"""
|
||||||
|
Convertes a snake_case name to CamelCase
|
||||||
|
:param name: the name to be converted
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return ''.join(x.title() for x in name.split('_'))
|
0
vm_gen/__init__.py
Normal file
0
vm_gen/__init__.py
Normal file
13
vm_gen/templates/_model_choice_header_py
Normal file
13
vm_gen/templates/_model_choice_header_py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[%- if _choice_types %]
|
||||||
|
[%- for choice_type in _choice_types %]
|
||||||
|
[%- for key in choice_type.choices.keys() %]
|
||||||
|
[[ choice_type.name|upper ]]_TYPE_[[ key ]] = "[[ key ]]"
|
||||||
|
[%- endfor %]
|
||||||
|
|
||||||
|
[[ choice_type.name ]] = [
|
||||||
|
[%- for key, value in choice_type.choices.items() %]
|
||||||
|
([[ choice_type.name|upper ]]_TYPE_[[ key ]], u'[[ value ]]'),
|
||||||
|
[%- endfor %]
|
||||||
|
]
|
||||||
|
[%- endfor %]
|
||||||
|
[%- endif %]
|
4
vm_gen/templates/_relationship_py
Normal file
4
vm_gen/templates/_relationship_py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
[[ column.name ]]_id = db.Column(db.Integer, db.ForeignKey('[[ column.name ]].id'))
|
||||||
|
[[ column.name ]] = db.relationship('[[ column.name|snake_to_camel ]]', backref=db.backref("[[ name|camel_to_snake|pluralize ]]"))
|
0
vm_gen/templates/html/_edit.html
Normal file
0
vm_gen/templates/html/_edit.html
Normal file
0
vm_gen/templates/html/_get.html
Normal file
0
vm_gen/templates/html/_get.html
Normal file
3
vm_gen/templates/html/_list.html
Normal file
3
vm_gen/templates/html/_list.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{% for instance in instances %}
|
||||||
|
<li><a href="{{ url_for('get_[[ name|camel_to_snake ]]', uuid=instance.uuid) }}">{{ instance.number }}</a></li>
|
||||||
|
{% endfor %}
|
5
vm_gen/templates/html/edit.html
Normal file
5
vm_gen/templates/html/edit.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "[[ name|camel_to_snake ]]/_edit.html" %}
|
||||||
|
{% endblock %}
|
5
vm_gen/templates/html/get.html
Normal file
5
vm_gen/templates/html/get.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "[[ name|camel_to_snake ]]/_get.html" %}
|
||||||
|
{% endblock %}
|
5
vm_gen/templates/html/list.html
Normal file
5
vm_gen/templates/html/list.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "[[ name|camel_to_snake ]]/_list.html" %}
|
||||||
|
{% endblock %}
|
17
vm_gen/templates/model_py
Normal file
17
vm_gen/templates/model_py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[%- if _choice_types %]
|
||||||
|
from sqlalchemy_utils import ChoiceType
|
||||||
|
[%- endif %]
|
||||||
|
|
||||||
|
from oshipka.persistance import db, ModelController
|
||||||
|
|
||||||
|
|
||||||
|
class [[ name ]](db.Model, ModelController):
|
||||||
|
[%- include "_model_choice_header_py" %]
|
||||||
|
|
||||||
|
[%- for column in columns %]
|
||||||
|
[%- if column._type == 'relationship' %]
|
||||||
|
[%- include "_relationship_py" %]
|
||||||
|
[%- else %]
|
||||||
|
[[ column.name ]] = db.Column([[ column._type ]][%- if column.default %], default="[[ column.default ]]"[%- endif %])
|
||||||
|
[%- endif %]
|
||||||
|
[%- endfor %]
|
108
vm_gen/vm_gen.py
Normal file
108
vm_gen/vm_gen.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import inflect
|
||||||
|
import yaml
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
|
from util.strings import snake_case_to_camel_case, camel_case_to_snake_case
|
||||||
|
|
||||||
|
|
||||||
|
def _process_choice(column):
|
||||||
|
column_name = column.get('name', '')
|
||||||
|
column_upper = column_name.upper()
|
||||||
|
types_name = "{}_TYPES".format(column_upper)
|
||||||
|
choices = column.get('choices', {})
|
||||||
|
return {
|
||||||
|
'name': types_name,
|
||||||
|
'choices': choices,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_view_model(view_model):
|
||||||
|
columns = []
|
||||||
|
for column in view_model.get('columns', {}):
|
||||||
|
column_type = column.get('type')
|
||||||
|
if column_type in ['text', 'long_text', ]:
|
||||||
|
_column_type = 'db.UnicodeText'
|
||||||
|
elif column_type in ['bool', ]:
|
||||||
|
_column_type = 'db.Boolean'
|
||||||
|
elif column_type in ['relationship', ]:
|
||||||
|
_column_type = 'relationship'
|
||||||
|
elif column_type in ['choice', ]:
|
||||||
|
if '_choice_types' not in view_model:
|
||||||
|
view_model['_choice_types'] = []
|
||||||
|
_choices = _process_choice(column)
|
||||||
|
_column_type = 'ChoiceType({})'.format(_choices.get('name'))
|
||||||
|
view_model['_choice_types'].append(_choices)
|
||||||
|
else:
|
||||||
|
_column_type = 'db.UnicodeText'
|
||||||
|
column.update({'_type': _column_type})
|
||||||
|
columns.append(column)
|
||||||
|
view_model['columns'] = columns
|
||||||
|
return view_model
|
||||||
|
|
||||||
|
|
||||||
|
def process_model(view_model):
|
||||||
|
template = env.get_template('model_py')
|
||||||
|
rv = template.render(**view_model)
|
||||||
|
_model_name = view_model.get('name')
|
||||||
|
filename = "{}.py".format(camel_case_to_snake_case(_model_name.split('.yaml')[0]))
|
||||||
|
with open(os.path.join(MODELS_PATH, filename), 'w+') as f:
|
||||||
|
f.write(rv)
|
||||||
|
|
||||||
|
|
||||||
|
def process_html_templates(view_model):
|
||||||
|
_model_name_snake = camel_case_to_snake_case(view_model.get('name'))
|
||||||
|
model_dir = os.path.join(HTML_TEMPLATES_PATH, _model_name_snake)
|
||||||
|
if not os.path.exists(model_dir):
|
||||||
|
os.makedirs(model_dir)
|
||||||
|
|
||||||
|
for filename in os.listdir(os.path.join(VM_TEMPLATES_PATH, "html")):
|
||||||
|
template = env.get_template(os.path.join('html', filename))
|
||||||
|
rv = template.render(**view_model)
|
||||||
|
with open(os.path.join(model_dir, filename), 'w+') as f:
|
||||||
|
f.write(rv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(view_model_name):
|
||||||
|
view_model_names = os.listdir(VIEW_MODELS_PATH) if not view_model_name else ["{}.yaml".format(view_model_name)]
|
||||||
|
for view_model_name in view_model_names:
|
||||||
|
with open(os.path.join(VIEW_MODELS_PATH, view_model_name), 'r') as stream:
|
||||||
|
try:
|
||||||
|
view_models = yaml.safe_load_all(stream)
|
||||||
|
for view_model in view_models:
|
||||||
|
view_model = enrich_view_model(view_model)
|
||||||
|
process_model(view_model)
|
||||||
|
process_html_templates(view_model)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
model_name = sys.argv[1]
|
||||||
|
basepath = sys.argv[2]
|
||||||
|
oshipka_path = os.environ.get('OSHIPKA_PATH')
|
||||||
|
|
||||||
|
VM_TEMPLATES_PATH = os.path.join(oshipka_path, "vm_gen", "templates")
|
||||||
|
|
||||||
|
WEBAPP_PATH = os.path.join(basepath, "webapp")
|
||||||
|
VIEW_MODELS_PATH = os.path.join(WEBAPP_PATH, "view_models")
|
||||||
|
MODELS_PATH = os.path.join(WEBAPP_PATH, "models_gen")
|
||||||
|
HTML_TEMPLATES_PATH = os.path.join(WEBAPP_PATH, "templates_gen")
|
||||||
|
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(searchpath=VM_TEMPLATES_PATH),
|
||||||
|
block_start_string='[%',
|
||||||
|
block_end_string='%]',
|
||||||
|
variable_start_string='[[',
|
||||||
|
variable_end_string=']]'
|
||||||
|
)
|
||||||
|
env.filters['snake_to_camel'] = snake_case_to_camel_case
|
||||||
|
env.filters['camel_to_snake'] = camel_case_to_snake_case
|
||||||
|
|
||||||
|
p = inflect.engine()
|
||||||
|
env.filters['pluralize'] = p.plural
|
||||||
|
|
||||||
|
main(model_name)
|
Loading…
Reference in New Issue
Block a user