files impl

This commit is contained in:
Daniel Tsvetkov 2021-05-08 12:24:13 +02:00
parent 1048122def
commit b680ca8bbd
10 changed files with 118 additions and 21 deletions

View File

@ -1 +1,2 @@
name name
admin
1 name
2 admin

View File

@ -1,2 +1,2 @@
email,password,role_names email,password,role_names
admin@blog.pi2.dev,__SENSITIVE__.ADMIN_PASSWORD,admin admin@app,__SENSITIVE__.ADMIN_PASSWORD,admin
1 email password role_names
2 admin@blog.pi2.dev admin@app __SENSITIVE__.ADMIN_PASSWORD admin

View File

@ -10,15 +10,21 @@ access:
login_required: true login_required: true
roles_required: roles_required:
- admin - admin
- verb: get - verb: get|list|table|search|create|update|delete
login_required: false
- verb: list
login_required: false
- verb: table
login_required: false
- verb: search
login_required: false login_required: false
show_in_nav: yes
columns: columns:
- name: filename - name: body|is_<bool>|<date>_dt
- name: body type: boolean|integer|text|long_text|datetime|filename|
type: long_text default: "{}"
- name: related
type: relationship # type relationship
multiple: true # could be many-to-many
- name: choisy
type: choice
choices:
INT_INFO: int_info # key:value
display:
primary: type
secondary: text
tertiary: alt_names

View File

@ -1,3 +1,4 @@
import os
import importlib import importlib
import json import json
from collections import defaultdict from collections import defaultdict
@ -11,6 +12,7 @@ from sqlalchemy_filters import apply_filters
from oshipka.persistance import db, filter_m_n, update_m_ns from oshipka.persistance import db, filter_m_n, update_m_ns
from oshipka.util.strings import camel_case_to_snake_case from oshipka.util.strings import camel_case_to_snake_case
from config import MEDIA_DIR
webapp_models = importlib.import_module("webapp.models") webapp_models = importlib.import_module("webapp.models")
@ -30,6 +32,14 @@ def default_get_form_func(vc):
bool_values = request.form.getlist(k) bool_values = request.form.getlist(k)
bool_value = True if len(bool_values) == 2 else False bool_value = True if len(bool_values) == 2 else False
vc.serialized_args[k[1:]] = bool_value vc.serialized_args[k[1:]] = bool_value
for k, f in request.files.items():
if f.filename != '':
filedir = os.path.join(MEDIA_DIR, vc.model_view.model_name_pl)
if not os.path.exists(filedir):
os.makedirs(filedir)
vc.serialized_args[k] = os.path.join(vc.model_view.model_name_pl, f.filename)
filepath = os.path.join(filedir, f.filename)
f.save(filepath)
vc.serialized_args.update(dict(filter(lambda k: not k[0].startswith("_"), dict(request.form).items()))) vc.serialized_args.update(dict(filter(lambda k: not k[0].startswith("_"), dict(request.form).items())))
to_delete = [] to_delete = []
for key, value in vc.serialized_args.items(): for key, value in vc.serialized_args.items():
@ -92,7 +102,14 @@ def default_update_func(vc):
del vc.serialized_args[key] del vc.serialized_args[key]
vc.instance = vc.instances[0] vc.instance = vc.instances[0]
for k, v in vc.serialized_args.items(): for k, v in vc.serialized_args.items():
setattr(vc.instance, k, v) if k.startswith("_file_"):
key = k.split('_file_')[1]
current_value = getattr(vc.instance, key)
if current_value and current_value != v:
os.remove(os.path.join(MEDIA_DIR, v))
setattr(vc.instance, key, v)
else:
setattr(vc.instance, k, v)
update_m_ns(vc.instance, m_ns) update_m_ns(vc.instance, m_ns)
db.session.add(vc.instance) db.session.add(vc.instance)
@ -105,6 +122,13 @@ def default_create_func(vc):
def default_delete_func(vc): def default_delete_func(vc):
instance = vc.instances[0] instance = vc.instances[0]
for filecolumn in vc.model_view.model_file_columns:
filename = getattr(instance, filecolumn)
if filename:
try:
os.remove(os.path.join(MEDIA_DIR, filename))
except Exception as e:
flash("Error deleting file: {}".format(e), "error")
db.session.delete(instance) db.session.delete(instance)
@ -226,6 +250,7 @@ class ModelView(object):
_model_name = model.name _model_name = model.name
self.model_name = camel_case_to_snake_case(_model_name) 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)
self.model_file_columns = model._file_columns
MODEL_VIEWS[self.model_name] = self MODEL_VIEWS[self.model_name] = self

View File

@ -6,6 +6,8 @@ class [[ name ]](db.Model, ModelController[% for inherit in interits %], [[ inhe
[%- include "_model_choice_header_py" %] [%- include "_model_choice_header_py" %]
[%- include "_model_searchable_header_py" %] [%- include "_model_searchable_header_py" %]
_file_columns = [ [%- for column in columns %][%- if column.is_file %]"[[ column.name ]]"[%- endif %] [%- endfor %] ]
[%- for column in columns %] [%- for column in columns %]
[%- if column._type == 'relationship' %] [%- if column._type == 'relationship' %]
[%- include "_relationship_py" %] [%- include "_relationship_py" %]

View File

@ -1,4 +1,4 @@
<form action="{{ url_for('create_[[ name|camel_to_snake ]]') }}" method="post"> <form action="{{ url_for('create_[[ name|camel_to_snake ]]') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('list_[[ name|camel_to_snake ]]') }}"/> <input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('list_[[ name|camel_to_snake ]]') }}"/>
<table> <table>
[%- for column in columns %] [%- for column in columns %]
@ -44,6 +44,8 @@
[%- elif column.type in ['long_text', ] %] [%- elif column.type in ['long_text', ] %]
<textarea id="input-[[ name|camel_to_snake ]]-[[ column.name ]]" <textarea id="input-[[ name|camel_to_snake ]]-[[ column.name ]]"
name="[[ column.name ]]"></textarea> name="[[ column.name ]]"></textarea>
[%- elif column.type in ['file', 'audio', 'video', 'image', 'picture', 'img', ] %]
<input type="file" name="_file_[[ column.name ]]" accept="[[ column.accept ]]">
[%- else %] [%- else %]
<input id="input-[[ name|camel_to_snake ]]-[[ column.name ]]" <input id="input-[[ name|camel_to_snake ]]-[[ column.name ]]"
type="text" name="[[ column.name ]]" autocomplete="off" type="text" name="[[ column.name ]]" autocomplete="off"

View File

@ -1,6 +1,9 @@
[%- for column in columns %] [%- for column in columns %]
{% if "[[ column.name ]]" not in skip_list %} {% if "[[ column.name ]]" not in skip_list %}
[%- if column.type in ['video'] %] <li id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"><strong>{{ _("[[ column.name ]]") }}</strong>:
[%- if column.type in ['picture', 'image', 'img'] %]
<img src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" id="display-[[ name|camel_to_snake ]]-[[ column.name ]]" />
[%- elif column.type in ['video'] %]
<video src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" controls class="video-inline" id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"> <video src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" controls class="video-inline" id="display-[[ name|camel_to_snake ]]-[[ column.name ]]">
<source src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" type="video/mp4"> <source src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" type="video/mp4">
<source src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" type="video/webm"> <source src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" type="video/webm">
@ -9,14 +12,15 @@
<audio src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" controls id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"></audio> <audio src="{{ url_for('oshipka_bp.get_media', filepath=instance.[[ column.name ]]) }}" controls id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"></audio>
[%- elif column.type in ['relationship'] %] [%- elif column.type in ['relationship'] %]
[%- if column.multiple %] [%- if column.multiple %]
<li id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"><strong>{{ _("[[ column.name|pluralize ]]") }}</strong>: {{ instance.[[ column.name|pluralize ]] }}</li> {{ instance.[[ column.name|pluralize ]] }}
[%- else %] [%- else %]
<li id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"><strong>{{_("[[ column.name ]]") }}</strong>: {{ instance.[[ column.name ]] }}</li> {{ instance.[[ column.name ]] }}
[%- endif %] [%- endif %]
[%- elif column.type in ['bool', 'boolean', ] %] [%- elif column.type in ['bool', 'boolean', ] %]
<li id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"><strong>{{ _("[[ column.name ]]") }}</strong>: {{ instance.[[ column.name ]]|bool }}</li> {{ instance.[[ column.name ]]|bool }}
[%- else %] [%- else %]
<li id="display-[[ name|camel_to_snake ]]-[[ column.name ]]"><strong>{{ _("[[ column.name ]]") }}</strong>: {{ instance.[[ column.name ]] }}</li> {{ instance.[[ column.name ]] }}
[%- endif %] [%- endif %]
</li>
{% endif %} {% endif %}
[%- endfor %] [%- endfor %]

View File

@ -1,4 +1,4 @@
<form action="{{ url_for('update_[[ name|camel_to_snake ]]', uuid=instance.id) }}" method="post"> <form action="{{ url_for('update_[[ name|camel_to_snake ]]', uuid=instance.id) }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('get_[[ name|camel_to_snake ]]', uuid=instance.id) }}"/> <input type="hidden" name="_next" value="{{ _next or request.args.get('_next') or url_for('get_[[ name|camel_to_snake ]]', uuid=instance.id) }}"/>
<table> <table>
[%- for column in columns %] [%- for column in columns %]
@ -43,6 +43,9 @@
[%- elif column.type in ['long_text', ] %] [%- elif column.type in ['long_text', ] %]
<textarea id="input-[[ name|camel_to_snake ]]-[[ column.name ]]" <textarea id="input-[[ name|camel_to_snake ]]-[[ column.name ]]"
name="[[ column.name ]]">{{ instance.[[ column.name ]] }}</textarea> name="[[ column.name ]]">{{ instance.[[ column.name ]] }}</textarea>
[%- elif column.type in ['file', 'audio', 'video', 'image', 'picture', 'img', ] %]
<p>{{ instance.[[ column.name ]] }}</p>
<input type="file" name="_file_[[ column.name ]]" accept="[[ column.accept ]]">
[%- else %] [%- else %]
<input id="input-[[ name|camel_to_snake ]]-[[ column.name ]]" <input id="input-[[ name|camel_to_snake ]]-[[ column.name ]]"
value="{{ instance.[[ column.name ]] }}" value="{{ instance.[[ column.name ]] }}"

View File

@ -0,0 +1,19 @@
<a href="{{ url_for('home') }}">{{ _("Home") }}</a> |
[%- for nav in navigation_items %]
[%- if nav._verbs.list.is_login_required %]
{% if current_user.is_authenticated %}
[%- endif %]
<a href="{{ url_for('list_[[ nav.name|camel_to_snake ]]') }}">{{ _("[[ nav.name ]]") }}</a>[%- if not loop.last %]|[%- endif %]
[%- if nav._verbs.list.is_login_required %]
{% endif %}
[%- endif %]
[%- endfor %]
<div class="pull-right">
{% if current_user.is_authenticated %}
<a href="#">{{ current_user.email }}</a> |
<a href="{{ url_for('security.logout') }}">{{ _("Logout") }}</a> |
{% else %}
<a href="{{ url_for('security.login') }}">{{ _("Login") }}</a> |
{% endif %}
</div>

View File

@ -37,6 +37,24 @@ def process_secondary(view_model, column_name):
} }
def _process_file(column):
column_type = column.get('type', '')
column_accept = column.get('accept', '')
if column_accept:
accept = column_accept
elif column_type in ['audio']:
accept = 'audio/*'
elif column_type in ['video']:
accept = 'video/*'
elif column_type in ['video']:
accept = 'video/*'
else:
accept = '*'
column["accept"] = accept
column["is_file"] = True
return column
VERBS = { VERBS = {
'get': { 'get': {
'per_item': 'True', 'per_item': 'True',
@ -97,6 +115,9 @@ def enrich_view_model(view_model):
_choices = _process_choice(column) _choices = _process_choice(column)
_column_type = 'ChoiceType({})'.format(_choices.get('name')) _column_type = 'ChoiceType({})'.format(_choices.get('name'))
view_model['_choice_types'].append(_choices) view_model['_choice_types'].append(_choices)
elif column_type in ['file', 'audio', 'video', 'image', 'img', 'picture', ]:
column = _process_file(column)
_column_type = 'db.UnicodeText'.format()
else: else:
_column_type = 'db.UnicodeText' _column_type = 'db.UnicodeText'
column.update({'_type': _column_type}) column.update({'_type': _column_type})
@ -124,6 +145,18 @@ def enrich_view_model(view_model):
return view_model return view_model
def process_navigation(view_models):
navigation_items = []
for view_model in view_models:
if view_model.get('show_in_nav'):
navigation_items.append(view_model)
template = env.get_template(os.path.join('html', 'navigation.html'))
rv = template.render(**{"navigation_items": navigation_items})
filepath = os.path.join(HTML_TEMPLATES_PATH, "navigation.html")
with open(filepath, 'w+') as f:
f.write(rv)
def process_model(view_model): def process_model(view_model):
template = env.get_template('model_py') template = env.get_template('model_py')
model = autopep8.fix_code(template.render(**view_model), options=pep_options) model = autopep8.fix_code(template.render(**view_model), options=pep_options)
@ -171,13 +204,14 @@ def process_html_templates(view_model):
def main(): def main():
all_model_imports = ['from oshipka.persistance import db'] all_model_imports = ['from oshipka.persistance import db']
all_route_imports = [] all_route_imports, all_view_models = [], []
view_model_names = order_from_process_order('yaml', VIEW_MODELS_PATH) view_model_names = order_from_process_order('yaml', VIEW_MODELS_PATH)
for view_model_name in view_model_names: for view_model_name in view_model_names:
with open(os.path.join(VIEW_MODELS_PATH, "{}.yaml".format(view_model_name)), 'r') as stream: with open(os.path.join(VIEW_MODELS_PATH, "{}.yaml".format(view_model_name)), 'r') as stream:
try: try:
view_models = yaml.safe_load_all(stream) view_models = yaml.safe_load_all(stream)
for view_model in view_models: for view_model in view_models:
all_view_models.append(view_model)
view_model = enrich_view_model(view_model) view_model = enrich_view_model(view_model)
process_model(view_model) process_model(view_model)
process_routes(view_model) process_routes(view_model)
@ -191,6 +225,7 @@ def main():
)) ))
except yaml.YAMLError as e: except yaml.YAMLError as e:
breakpoint() breakpoint()
process_navigation(all_view_models)
all_model_imports = autopep8.fix_code('\n'.join(all_model_imports), options=pep_options) all_model_imports = autopep8.fix_code('\n'.join(all_model_imports), options=pep_options)
all_route_imports = autopep8.fix_code('\n'.join(all_route_imports), options=pep_options) all_route_imports = autopep8.fix_code('\n'.join(all_route_imports), options=pep_options)
with open(os.path.join(MODELS_PATH, "__init__.py"), 'w+') as f: with open(os.path.join(MODELS_PATH, "__init__.py"), 'w+') as f: