Add basic "new model view" wizard
This commit is contained in:
parent
f4bc280da7
commit
00548a259b
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -177,6 +177,7 @@ def make_pyramid_config(settings, configure_csrf=True):
|
|||
# and some similar magic for certain master views
|
||||
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
||||
config.add_directive('add_tailbone_config_page', 'tailbone.app.add_config_page')
|
||||
config.add_directive('add_tailbone_model_view', 'tailbone.app.add_model_view')
|
||||
config.add_directive('add_tailbone_view_supplement', 'tailbone.app.add_view_supplement')
|
||||
|
||||
config.add_directive('add_tailbone_websocket', 'tailbone.app.add_websocket')
|
||||
|
@ -240,6 +241,25 @@ def add_config_page(config, route_name, label, permission):
|
|||
config.action(None, action)
|
||||
|
||||
|
||||
def add_model_view(config, model_name, label, route_prefix, permission_prefix):
|
||||
"""
|
||||
Register a model view for the app.
|
||||
"""
|
||||
def action():
|
||||
all_views = config.get_settings().get('tailbone_model_views', {})
|
||||
|
||||
model_views = all_views.setdefault(model_name, [])
|
||||
model_views.append({
|
||||
'label': label,
|
||||
'route_prefix': route_prefix,
|
||||
'permission_prefix': permission_prefix,
|
||||
})
|
||||
|
||||
config.add_settings({'tailbone_model_views': all_views})
|
||||
|
||||
config.action(None, action)
|
||||
|
||||
|
||||
def add_view_supplement(config, route_prefix, cls):
|
||||
"""
|
||||
Register a master view supplement for the app.
|
||||
|
|
|
@ -27,8 +27,10 @@ Tailbone Handler
|
|||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import six
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
from rattail.app import GenericHandler
|
||||
from rattail.files import resource_path
|
||||
|
||||
from tailbone.providers import get_all_providers
|
||||
|
||||
|
@ -38,6 +40,13 @@ class TailboneHandler(GenericHandler):
|
|||
Base class and default implementation for Tailbone handler.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TailboneHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
# TODO: make templates dir configurable?
|
||||
templates = [resource_path('rattail:templates/web')]
|
||||
self.templates = TemplateLookup(directories=templates)
|
||||
|
||||
def get_menu_handler(self, **kwargs):
|
||||
"""
|
||||
Get the configured "menu" handler.
|
||||
|
@ -54,5 +63,18 @@ class TailboneHandler(GenericHandler):
|
|||
return self.menu_handler
|
||||
|
||||
def iter_providers(self):
|
||||
"""
|
||||
Returns an iterator over all registered Tailbone providers.
|
||||
"""
|
||||
providers = get_all_providers(self.config)
|
||||
return six.itervalues(providers)
|
||||
|
||||
def write_model_view(self, data, path, **kwargs):
|
||||
"""
|
||||
Write code for a new model view, based on the given data dict,
|
||||
to the given path.
|
||||
"""
|
||||
template = self.templates.get_template('/new-model-view.mako')
|
||||
content = template.render(**data)
|
||||
with open(path, 'wt') as f:
|
||||
f.write(content)
|
||||
|
|
|
@ -41,6 +41,28 @@
|
|||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<b-field>
|
||||
<b-checkbox name="rattail.running_from_source"
|
||||
v-model="simpleSettings['rattail.running_from_source']"
|
||||
native-value="true"
|
||||
@input="settingsNeedSaved = true">
|
||||
Running from Source
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<b-field label="Top-Level Package" horizontal
|
||||
v-if="simpleSettings['rattail.running_from_source']">
|
||||
<b-input name="rattail.running_from_source.rootpkg"
|
||||
v-model="simpleSettings['rattail.running_from_source.rootpkg']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="block is-size-3">Display</h3>
|
||||
|
|
|
@ -3,6 +3,15 @@
|
|||
|
||||
<%def name="title()">Configure ${config_title}</%def>
|
||||
|
||||
<%def name="extra_styles()">
|
||||
${parent.extra_styles()}
|
||||
<style type="text/css">
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="save_undo_buttons()">
|
||||
<div class="buttons"
|
||||
v-if="settingsNeedSaved">
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
configureFieldsHelp: Boolean,
|
||||
},
|
||||
computed: {},
|
||||
watch: {},
|
||||
methods: {},
|
||||
}
|
||||
|
||||
|
|
339
tailbone/templates/views/model/create.mako
Normal file
339
tailbone/templates/views/model/create.mako
Normal file
|
@ -0,0 +1,339 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/create.mako" />
|
||||
|
||||
<%def name="extra_styles()">
|
||||
${parent.extra_styles()}
|
||||
<style type="text/css">
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="render_this_page()">
|
||||
<b-steps v-model="activeStep"
|
||||
:animated="false"
|
||||
rounded
|
||||
:has-navigation="false"
|
||||
vertical
|
||||
icon-pack="fas">
|
||||
|
||||
<b-step-item step="1"
|
||||
value="enter-details"
|
||||
label="Enter Details"
|
||||
clickable>
|
||||
<h3 class="is-size-3 block">
|
||||
Enter Details
|
||||
</h3>
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="Model Name">
|
||||
<b-select v-model="modelName">
|
||||
<option v-for="name in modelNames"
|
||||
:key="name"
|
||||
:value="name">
|
||||
{{ name }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="View Class Name">
|
||||
<b-input v-model="viewClassName">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="View Route Prefix">
|
||||
<b-input v-model="viewRoutePrefix">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="check"
|
||||
@click="activeStep = 'write-view'">
|
||||
Details are complete
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
</b-step-item>
|
||||
|
||||
<b-step-item step="2"
|
||||
value="write-view"
|
||||
label="Write View">
|
||||
<h3 class="is-size-3 block">
|
||||
Write View
|
||||
</h3>
|
||||
|
||||
<b-field label="Model Name" horizontal>
|
||||
{{ modelName }}
|
||||
</b-field>
|
||||
|
||||
<b-field label="View Class" horizontal>
|
||||
{{ viewClassName }}
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal label="File">
|
||||
<b-input v-model="viewFile"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal>
|
||||
<b-checkbox v-model="viewFileOverwrite">
|
||||
Overwrite file if it exists
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
<div class="form">
|
||||
<div class="buttons">
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="arrow-left"
|
||||
@click="activeStep = 'enter-details'">
|
||||
Back
|
||||
</b-button>
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="save"
|
||||
@click="writeViewFile()"
|
||||
:disabled="writingViewFile">
|
||||
{{ writingViewFile ? "Working, please wait..." : "Write view class to file" }}
|
||||
</b-button>
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="arrow-right"
|
||||
@click="activeStep = 'review-view'">
|
||||
Skip
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-step-item>
|
||||
|
||||
<b-step-item step="3"
|
||||
value="review-view"
|
||||
label="Review View"
|
||||
## clickable
|
||||
>
|
||||
<h3 class="is-size-3 block">
|
||||
Review View
|
||||
</h3>
|
||||
|
||||
<p class="block">
|
||||
View code was generated to file:
|
||||
</p>
|
||||
|
||||
<p class="block is-family-code" style="padding-left: 3rem;">
|
||||
{{ viewFile }}
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
First, review that code and adjust to your liking.
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
Next be sure to include the new view in your config.
|
||||
Typically this is done by editing the file...
|
||||
</p>
|
||||
|
||||
<p class="block is-family-code" style="padding-left: 3rem;">
|
||||
${view_dir}__init__.py
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
...and adding a line to the includeme() block such as:
|
||||
</p>
|
||||
|
||||
<pre class="block">
|
||||
def includeme(config):
|
||||
|
||||
# ...existing config includes here...
|
||||
|
||||
## TODO: stop hard-coding widgets
|
||||
config.include('${pkgroot}.web.views.widgets')
|
||||
</pre>
|
||||
|
||||
<p class="block">
|
||||
Once you've done all that, the web app must be restarted.
|
||||
This may happen automatically depending on your setup.
|
||||
Test the view status below.
|
||||
</p>
|
||||
|
||||
<div class="card block">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
View Status
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
|
||||
<div class="level-item">
|
||||
<span v-if="!viewImportAttempted">
|
||||
check not yet attempted
|
||||
</span>
|
||||
<span v-if="viewImported"
|
||||
class="has-text-success has-text-weight-bold">
|
||||
route found!
|
||||
</span>
|
||||
<span v-if="viewImportAttempted && viewImportProblem"
|
||||
class="has-text-danger">
|
||||
{{ viewImportProblem }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<b-field horizontal label="Route Prefix">
|
||||
<b-input v-model="viewRoutePrefix"></b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="redo"
|
||||
@click="testView()">
|
||||
Test View
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="arrow-left"
|
||||
@click="activeStep = 'write-view'">
|
||||
Back
|
||||
</b-button>
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="check"
|
||||
@click="activeStep = 'commit-code'"
|
||||
:disabled="!viewImported">
|
||||
View class looks good!
|
||||
</b-button>
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="arrow-right"
|
||||
@click="activeStep = 'commit-code'">
|
||||
Skip
|
||||
</b-button>
|
||||
</div>
|
||||
</b-step-item>
|
||||
|
||||
<b-step-item step="4"
|
||||
value="commit-code"
|
||||
label="Commit Code">
|
||||
<h3 class="is-size-3 block">
|
||||
Commit Code
|
||||
</h3>
|
||||
|
||||
<p class="block">
|
||||
Hope you're having a great day.
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
Don't forget to commit code changes to your source repo.
|
||||
</p>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="arrow-left"
|
||||
@click="activeStep = 'review-view'">
|
||||
Back
|
||||
</b-button>
|
||||
<once-button type="is-primary"
|
||||
tag="a" :href="viewURL"
|
||||
icon-left="arrow-right"
|
||||
:disabled="!viewURL"
|
||||
:text="`Show me my new view: ${'$'}{viewClassName}`">
|
||||
</once-button>
|
||||
</div>
|
||||
</b-step-item>
|
||||
|
||||
</b-steps>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
ThisPageData.activeStep = null
|
||||
|
||||
ThisPageData.modelNames = ${json.dumps(model_names)|n}
|
||||
ThisPageData.modelName = null
|
||||
ThisPageData.viewClassName = null
|
||||
ThisPageData.viewRoutePrefix = null
|
||||
|
||||
ThisPage.watch.modelName = function(newName, oldName) {
|
||||
this.viewClassName = `${'$'}{newName}View`
|
||||
this.viewRoutePrefix = newName.toLowerCase()
|
||||
}
|
||||
|
||||
ThisPage.mounted = function() {
|
||||
let params = new URLSearchParams(location.search)
|
||||
if (params.has('model_name')) {
|
||||
this.modelName = params.get('model_name')
|
||||
}
|
||||
}
|
||||
|
||||
ThisPageData.viewFile = '${view_dir}widgets.py'
|
||||
ThisPageData.viewFileOverwrite = false
|
||||
ThisPageData.writingViewFile = false
|
||||
|
||||
ThisPage.methods.writeViewFile = function() {
|
||||
this.writingViewFile = true
|
||||
|
||||
let url = '${url('{}.write_view_file'.format(route_prefix))}'
|
||||
let params = {
|
||||
view_file: this.viewFile,
|
||||
overwrite: this.viewFileOverwrite,
|
||||
view_class_name: this.viewClassName,
|
||||
model_name: this.modelName,
|
||||
route_prefix: this.viewRoutePrefix,
|
||||
}
|
||||
this.submitForm(url, params, response => {
|
||||
this.writingViewFile = false
|
||||
this.activeStep = 'review-view'
|
||||
}, response => {
|
||||
this.writingViewFile = false
|
||||
})
|
||||
}
|
||||
|
||||
ThisPageData.viewImported = false
|
||||
ThisPageData.viewImportAttempted = false
|
||||
ThisPageData.viewImportProblem = null
|
||||
|
||||
ThisPage.methods.testView = function() {
|
||||
|
||||
this.viewImported = false
|
||||
this.viewImportProblem = null
|
||||
|
||||
let url = '${url('{}.check_view'.format(route_prefix))}'
|
||||
|
||||
let params = {
|
||||
route_prefix: this.viewRoutePrefix,
|
||||
}
|
||||
this.submitForm(url, params, response => {
|
||||
this.viewImportAttempted = true
|
||||
if (response.data.problem) {
|
||||
this.viewImportProblem = response.data.problem
|
||||
} else {
|
||||
this.viewImported = true
|
||||
this.viewURL = response.data.url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ThisPageData.viewURL = null
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -2841,9 +2841,12 @@ class MasterView(View):
|
|||
|
||||
def make_grid_action_view(self):
|
||||
use_buefy = self.get_use_buefy()
|
||||
url = self.get_view_index_url if self.use_index_links else None
|
||||
icon = 'eye' if use_buefy else 'zoomin'
|
||||
return self.make_action('view', icon=icon, url=url)
|
||||
return self.make_action('view', icon=icon, url=self.default_view_url())
|
||||
|
||||
def default_view_url(self):
|
||||
if self.use_index_links:
|
||||
return self.get_view_index_url
|
||||
|
||||
def get_view_index_url(self, row, i):
|
||||
route = '{}.view_index'.format(self.get_route_prefix())
|
||||
|
@ -4978,6 +4981,22 @@ class MasterView(View):
|
|||
|
||||
# list/search
|
||||
if cls.listable:
|
||||
|
||||
# master views which represent a typical model class, and
|
||||
# allow for an index view, are registered specially so the
|
||||
# admin may browse the full list of such views
|
||||
modclass = cls.get_model_class(error=False)
|
||||
if modclass:
|
||||
config.add_tailbone_model_view(modclass.__name__,
|
||||
model_title_plural,
|
||||
route_prefix,
|
||||
permission_prefix)
|
||||
|
||||
# but regardless we register the index view, for similar reasons
|
||||
config.add_tailbone_index_page(route_prefix, model_title_plural,
|
||||
'{}.list'.format(permission_prefix))
|
||||
|
||||
# index view
|
||||
config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix),
|
||||
"List / search {}".format(model_title_plural))
|
||||
config.add_route(route_prefix, '{}/'.format(url_prefix))
|
||||
|
@ -4985,8 +5004,6 @@ class MasterView(View):
|
|||
config.add_view(cls, attr='index', route_name=route_prefix,
|
||||
permission='{}.list'.format(permission_prefix),
|
||||
**kwargs)
|
||||
config.add_tailbone_index_page(route_prefix, model_title_plural,
|
||||
'{}.list'.format(permission_prefix))
|
||||
|
||||
# download results
|
||||
# this is the "new" more flexible approach, but we only want to
|
||||
|
|
|
@ -140,6 +140,11 @@ class AppInfoView(MasterView):
|
|||
{'section': 'rattail',
|
||||
'option': 'production',
|
||||
'type': bool},
|
||||
{'section': 'rattail',
|
||||
'option': 'running_from_source',
|
||||
'type': bool},
|
||||
{'section': 'rattail',
|
||||
'option': 'running_from_source.rootpkg'},
|
||||
|
||||
# display
|
||||
{'section': 'tailbone',
|
||||
|
|
|
@ -67,6 +67,7 @@ class TableView(MasterView):
|
|||
]
|
||||
|
||||
has_rows = True
|
||||
rows_title = "Columns"
|
||||
rows_pageable = False
|
||||
rows_filterable = False
|
||||
rows_viewable = False
|
||||
|
@ -170,6 +171,27 @@ class TableView(MasterView):
|
|||
def make_form_schema(self):
|
||||
return TableSchema()
|
||||
|
||||
def get_xref_buttons(self, table):
|
||||
buttons = super(TableView, self).get_xref_buttons(table)
|
||||
|
||||
if table.get('model_name'):
|
||||
all_views = self.request.registry.settings['tailbone_model_views']
|
||||
model_views = all_views.get(table['model_name'], [])
|
||||
for view in model_views:
|
||||
url = self.request.route_url(view['route_prefix'])
|
||||
buttons.append(self.make_xref_button(url=url, text=view['label'],
|
||||
internal=True))
|
||||
|
||||
if self.request.has_perm('model_views.create'):
|
||||
url = self.request.route_url('model_views.create',
|
||||
_query={'model_name': table['model_name']})
|
||||
buttons.append(self.make_buefy_button("New View",
|
||||
is_primary=True,
|
||||
url=url,
|
||||
icon_left='plus'))
|
||||
|
||||
return buttons
|
||||
|
||||
def template_kwargs_create(self, **kwargs):
|
||||
kwargs = super(TableView, self).template_kwargs_create(**kwargs)
|
||||
app = self.get_rattail_app()
|
||||
|
|
219
tailbone/views/views.py
Normal file
219
tailbone/views/views.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Views for views
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from rattail.db.util import get_fieldnames
|
||||
from rattail.util import simple_error
|
||||
|
||||
import colander
|
||||
from deform import widget as dfwidget
|
||||
|
||||
from tailbone.views import MasterView
|
||||
|
||||
|
||||
class ModelViewView(MasterView):
|
||||
"""
|
||||
Master view for views
|
||||
"""
|
||||
normalized_model_name = 'model_view'
|
||||
model_key = 'route_prefix'
|
||||
model_title = "Model View"
|
||||
url_prefix = '/views/model'
|
||||
viewable = True
|
||||
creatable = True
|
||||
editable = False
|
||||
deletable = False
|
||||
filterable = False
|
||||
pageable = False
|
||||
|
||||
grid_columns = [
|
||||
'label',
|
||||
'model_name',
|
||||
'route_prefix',
|
||||
'permission_prefix',
|
||||
]
|
||||
|
||||
def get_data(self, **kwargs):
|
||||
"""
|
||||
Fetch existing model views from app registry
|
||||
"""
|
||||
data = []
|
||||
|
||||
all_views = self.request.registry.settings['tailbone_model_views']
|
||||
for model_name in sorted(all_views):
|
||||
model_views = all_views[model_name]
|
||||
for view in model_views:
|
||||
data.append({
|
||||
'model_name': model_name,
|
||||
'label': view['label'],
|
||||
'route_prefix': view['route_prefix'],
|
||||
'permission_prefix': view['permission_prefix'],
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(ModelViewView, self).configure_grid(g)
|
||||
|
||||
# label
|
||||
g.sorters['label'] = g.make_simple_sorter('label')
|
||||
g.set_sort_defaults('label')
|
||||
g.set_link('label')
|
||||
|
||||
# model_name
|
||||
g.sorters['model_name'] = g.make_simple_sorter('model_name', foldcase=True)
|
||||
g.set_searchable('model_name')
|
||||
|
||||
# route
|
||||
g.sorters['route'] = g.make_simple_sorter('route')
|
||||
|
||||
# permission
|
||||
g.sorters['permission'] = g.make_simple_sorter('permission')
|
||||
|
||||
def default_view_url(self, view, i=None):
|
||||
return self.request.route_url(view['route_prefix'])
|
||||
|
||||
def make_form_schema(self):
|
||||
return ModelViewSchema()
|
||||
|
||||
def template_kwargs_create(self, **kwargs):
|
||||
kwargs = super(ModelViewView, self).template_kwargs_create(**kwargs)
|
||||
app = self.get_rattail_app()
|
||||
db_handler = app.get_db_handler()
|
||||
|
||||
model_classes = db_handler.get_model_classes()
|
||||
kwargs['model_names'] = [cls.__name__ for cls in model_classes]
|
||||
|
||||
pkg = self.rattail_config.get('rattail', 'running_from_source.rootpkg')
|
||||
if pkg:
|
||||
kwargs['pkgroot'] = pkg
|
||||
pkg = sys.modules[pkg]
|
||||
pkgdir = os.path.dirname(pkg.__file__)
|
||||
kwargs['view_dir'] = os.path.join(pkgdir, 'web', 'views') + os.sep
|
||||
else:
|
||||
kwargs['pkgroot'] = 'poser'
|
||||
kwargs['view_dir'] = '??' + os.sep
|
||||
|
||||
return kwargs
|
||||
|
||||
def write_view_file(self):
|
||||
data = self.request.json_body
|
||||
path = data['view_file']
|
||||
|
||||
if os.path.exists(path):
|
||||
if data['overwrite']:
|
||||
os.remove(path)
|
||||
else:
|
||||
return {'error': "File already exists"}
|
||||
|
||||
app = self.get_rattail_app()
|
||||
tb = app.get_tailbone_handler()
|
||||
model_class = getattr(self.model, data['model_name'])
|
||||
|
||||
data['model_module_name'] = self.model.__name__
|
||||
data['model_title_plural'] = getattr(model_class,
|
||||
'model_title_plural',
|
||||
# TODO
|
||||
model_class.__name__)
|
||||
|
||||
data['model_versioned'] = hasattr(model_class, '__versioned__')
|
||||
|
||||
fieldnames = get_fieldnames(self.rattail_config,
|
||||
model_class)
|
||||
fieldnames.remove('uuid')
|
||||
data['model_fieldnames'] = fieldnames
|
||||
|
||||
tb.write_model_view(data, path)
|
||||
|
||||
return {'ok': True}
|
||||
|
||||
def check_view(self):
|
||||
data = self.request.json_body
|
||||
|
||||
try:
|
||||
url = self.request.route_url(data['route_prefix'])
|
||||
except Exception as error:
|
||||
return {'ok': True,
|
||||
'problem': simple_error(error)}
|
||||
|
||||
return {'ok': True, 'url': url}
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
rattail_config = config.registry.settings.get('rattail_config')
|
||||
|
||||
# allow creating views only if *not* production
|
||||
if not rattail_config.production():
|
||||
cls.creatable = True
|
||||
|
||||
cls._model_view_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _model_view_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
|
||||
if cls.creatable:
|
||||
|
||||
# write view class to file
|
||||
config.add_route('{}.write_view_file'.format(route_prefix),
|
||||
'{}/write-view-file'.format(url_prefix),
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='write_view_file',
|
||||
route_name='{}.write_view_file'.format(route_prefix),
|
||||
renderer='json',
|
||||
permission='{}.create'.format(permission_prefix))
|
||||
|
||||
# check view
|
||||
config.add_route('{}.check_view'.format(route_prefix),
|
||||
'{}/check-view'.format(url_prefix),
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='check_view',
|
||||
route_name='{}.check_view'.format(route_prefix),
|
||||
renderer='json',
|
||||
permission='{}.create'.format(permission_prefix))
|
||||
|
||||
|
||||
class ModelViewSchema(colander.Schema):
|
||||
|
||||
model_name = colander.SchemaNode(colander.String())
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
ModelViewView = kwargs.get('ModelViewView', base['ModelViewView'])
|
||||
ModelViewView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
Loading…
Reference in a new issue