diff --git a/oshipka.sh b/oshipka.sh index f3591e3..a3a3ef5 100755 --- a/oshipka.sh +++ b/oshipka.sh @@ -8,9 +8,10 @@ echo "oshipka is at: $OSHIPKA_PATH" #!/usr/bin/env bash 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 + model [MODEL_NAME] Create or update a model init Install dev env worker Start worker @@ -107,11 +108,20 @@ run_in_prod() { 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() { INITIAL_COMMAND=$1 case "$INITIAL_COMMAND" in bootstrap) bootstrap "$@" ;; + model) model "$@" + ;; init) init "$@" ;; worker) worker "$@" diff --git a/oshipka/persistance/__init__.py b/oshipka/persistance/__init__.py index 7c56a0c..52a8a5c 100644 --- a/oshipka/persistance/__init__.py +++ b/oshipka/persistance/__init__.py @@ -20,6 +20,7 @@ from sqlalchemy_utils import Choice from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago from whooshalchemy import IndexService from oshipka.search import add_to_index, remove_from_index, query_index +from util.strings import camel_case_to_snake_case db = SQLAlchemy() @@ -57,16 +58,6 @@ class LiberalBoolean(TypeDecorator): 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): """ This interface is the parent of all models in our database. diff --git a/oshipka/webapp/views.py b/oshipka/webapp/views.py index 189f18c..249807b 100644 --- a/oshipka/webapp/views.py +++ b/oshipka/webapp/views.py @@ -5,9 +5,23 @@ import inflect from flask import flash, render_template, redirect, request, url_for 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): + template = template if template else "{}/list.html".format(model_view.model_name) + def inner(): instances = model_view.model.query.all() 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): + template = template if template else "{}/get.html".format(model_view.model_name) + def inner(uuid): - model = model_view.model - instance = model.query.filter_by(uuid=uuid).first() + instance = get_instance(model_view, uuid) if not instance: - flash("No {}:{}".format(model_view.model_name, uuid)) return redirect(request.referrer or url_for('home')) template_ctx = template_ctx_func(instance) if template_ctx_func else {} 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): + template = template if template else "{}/edit.html".format(model_view.model_name) + def inner(uuid): - model = model_view.model - instance = model.query.filter_by(uuid=uuid).first() + instance = get_instance(model_view, uuid) if not instance: - flash("No {}:{}".format(model_view.model_name, uuid)) return redirect(request.referrer or url_for('home')) if request.method == "GET": 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 inner(uuid): - model = model_view.model - instance = model.query.filter_by(uuid=uuid).first() + instance = get_instance(model_view, uuid) if not instance: - flash("No {}:{}".format(model_view.model_name, uuid)) return redirect(request.referrer or url_for('home')) if request.method == "GET": return render_template("delete_instance.html", instance=instance, @@ -109,29 +121,29 @@ class ModelView(object): 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) - def register_create(self, list_template, **kwargs): + def register_create(self, create_template=None, **kwargs): url = '/{}/create'.format(self.model_name_pl) self.app.add_url_rule(url, methods=["GET", "POST"], 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) self.app.add_url_rule(url, 'list_{}'.format(self.model_name), list_view(self, list_template, **kwargs)) - def register_get(self, retrieve_template, **kwargs): + def register_get(self, retrieve_template=None, **kwargs): url = '/{}/'.format(self.model_name_pl) self.app.add_url_rule(url, 'get_{}'.format(self.model_name), get_view(self, retrieve_template, **kwargs)) - def register_update(self, update_template, **kwargs): + def register_update(self, update_template=None, **kwargs): url = '/{}//edit'.format(self.model_name_pl) self.app.add_url_rule(url, methods=["GET", "POST"], endpoint='update_{}'.format(self.model_name), diff --git a/requirements.txt b/requirements.txt index 4d7124e..f683eb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ passlib==1.7.2 pathtools==0.1.2 pycparser==2.20 pytz==2019.3 +pyyaml==5.3.1 six==1.14.0 speaklater==1.3 SQLAlchemy==1.3.15 diff --git a/oshipka/util/__init__.py b/util/__init__.py similarity index 100% rename from oshipka/util/__init__.py rename to util/__init__.py diff --git a/oshipka/util/os.py b/util/os.py similarity index 100% rename from oshipka/util/os.py rename to util/os.py diff --git a/oshipka/util/process.py b/util/process.py similarity index 100% rename from oshipka/util/process.py rename to util/process.py diff --git a/oshipka/util/simple_file_cache.py b/util/simple_file_cache.py similarity index 100% rename from oshipka/util/simple_file_cache.py rename to util/simple_file_cache.py diff --git a/util/strings.py b/util/strings.py new file mode 100644 index 0000000..e0a63ad --- /dev/null +++ b/util/strings.py @@ -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('_')) \ No newline at end of file diff --git a/vm_gen/__init__.py b/vm_gen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vm_gen/templates/_model_choice_header_py b/vm_gen/templates/_model_choice_header_py new file mode 100644 index 0000000..a4e3b74 --- /dev/null +++ b/vm_gen/templates/_model_choice_header_py @@ -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 %] \ No newline at end of file diff --git a/vm_gen/templates/_relationship_py b/vm_gen/templates/_relationship_py new file mode 100644 index 0000000..18427d6 --- /dev/null +++ b/vm_gen/templates/_relationship_py @@ -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 ]]")) \ No newline at end of file diff --git a/vm_gen/templates/html/_edit.html b/vm_gen/templates/html/_edit.html new file mode 100644 index 0000000..e69de29 diff --git a/vm_gen/templates/html/_get.html b/vm_gen/templates/html/_get.html new file mode 100644 index 0000000..e69de29 diff --git a/vm_gen/templates/html/_list.html b/vm_gen/templates/html/_list.html new file mode 100644 index 0000000..dcfb8ae --- /dev/null +++ b/vm_gen/templates/html/_list.html @@ -0,0 +1,3 @@ +{% for instance in instances %} +
  • {{ instance.number }}
  • +{% endfor %} \ No newline at end of file diff --git a/vm_gen/templates/html/edit.html b/vm_gen/templates/html/edit.html new file mode 100644 index 0000000..a0dadea --- /dev/null +++ b/vm_gen/templates/html/edit.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} + +{% block content %} + {% include "[[ name|camel_to_snake ]]/_edit.html" %} +{% endblock %} \ No newline at end of file diff --git a/vm_gen/templates/html/get.html b/vm_gen/templates/html/get.html new file mode 100644 index 0000000..f8ee868 --- /dev/null +++ b/vm_gen/templates/html/get.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} + +{% block content %} + {% include "[[ name|camel_to_snake ]]/_get.html" %} +{% endblock %} \ No newline at end of file diff --git a/vm_gen/templates/html/list.html b/vm_gen/templates/html/list.html new file mode 100644 index 0000000..ec231bd --- /dev/null +++ b/vm_gen/templates/html/list.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} + +{% block content %} + {% include "[[ name|camel_to_snake ]]/_list.html" %} +{% endblock %} \ No newline at end of file diff --git a/vm_gen/templates/model_py b/vm_gen/templates/model_py new file mode 100644 index 0000000..9946a14 --- /dev/null +++ b/vm_gen/templates/model_py @@ -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 %] diff --git a/vm_gen/vm_gen.py b/vm_gen/vm_gen.py new file mode 100644 index 0000000..caeaac5 --- /dev/null +++ b/vm_gen/vm_gen.py @@ -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)