Add basic "new model view" wizard
This commit is contained in:
parent
f4bc280da7
commit
00548a259b
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# 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
|
# 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_index_page', 'tailbone.app.add_index_page')
|
||||||
config.add_directive('add_tailbone_config_page', 'tailbone.app.add_config_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_view_supplement', 'tailbone.app.add_view_supplement')
|
||||||
|
|
||||||
config.add_directive('add_tailbone_websocket', 'tailbone.app.add_websocket')
|
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)
|
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):
|
def add_view_supplement(config, route_prefix, cls):
|
||||||
"""
|
"""
|
||||||
Register a master view supplement for the app.
|
Register a master view supplement for the app.
|
||||||
|
|
|
@ -27,8 +27,10 @@ Tailbone Handler
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from mako.lookup import TemplateLookup
|
||||||
|
|
||||||
from rattail.app import GenericHandler
|
from rattail.app import GenericHandler
|
||||||
|
from rattail.files import resource_path
|
||||||
|
|
||||||
from tailbone.providers import get_all_providers
|
from tailbone.providers import get_all_providers
|
||||||
|
|
||||||
|
@ -38,6 +40,13 @@ class TailboneHandler(GenericHandler):
|
||||||
Base class and default implementation for Tailbone handler.
|
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):
|
def get_menu_handler(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get the configured "menu" handler.
|
Get the configured "menu" handler.
|
||||||
|
@ -54,5 +63,18 @@ class TailboneHandler(GenericHandler):
|
||||||
return self.menu_handler
|
return self.menu_handler
|
||||||
|
|
||||||
def iter_providers(self):
|
def iter_providers(self):
|
||||||
|
"""
|
||||||
|
Returns an iterator over all registered Tailbone providers.
|
||||||
|
"""
|
||||||
providers = get_all_providers(self.config)
|
providers = get_all_providers(self.config)
|
||||||
return six.itervalues(providers)
|
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-checkbox>
|
||||||
</b-field>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Display</h3>
|
<h3 class="block is-size-3">Display</h3>
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
|
|
||||||
<%def name="title()">Configure ${config_title}</%def>
|
<%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()">
|
<%def name="save_undo_buttons()">
|
||||||
<div class="buttons"
|
<div class="buttons"
|
||||||
v-if="settingsNeedSaved">
|
v-if="settingsNeedSaved">
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
configureFieldsHelp: Boolean,
|
configureFieldsHelp: Boolean,
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
|
watch: {},
|
||||||
methods: {},
|
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):
|
def make_grid_action_view(self):
|
||||||
use_buefy = self.get_use_buefy()
|
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'
|
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):
|
def get_view_index_url(self, row, i):
|
||||||
route = '{}.view_index'.format(self.get_route_prefix())
|
route = '{}.view_index'.format(self.get_route_prefix())
|
||||||
|
@ -4978,6 +4981,22 @@ class MasterView(View):
|
||||||
|
|
||||||
# list/search
|
# list/search
|
||||||
if cls.listable:
|
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),
|
config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix),
|
||||||
"List / search {}".format(model_title_plural))
|
"List / search {}".format(model_title_plural))
|
||||||
config.add_route(route_prefix, '{}/'.format(url_prefix))
|
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,
|
config.add_view(cls, attr='index', route_name=route_prefix,
|
||||||
permission='{}.list'.format(permission_prefix),
|
permission='{}.list'.format(permission_prefix),
|
||||||
**kwargs)
|
**kwargs)
|
||||||
config.add_tailbone_index_page(route_prefix, model_title_plural,
|
|
||||||
'{}.list'.format(permission_prefix))
|
|
||||||
|
|
||||||
# download results
|
# download results
|
||||||
# this is the "new" more flexible approach, but we only want to
|
# this is the "new" more flexible approach, but we only want to
|
||||||
|
|
|
@ -140,6 +140,11 @@ class AppInfoView(MasterView):
|
||||||
{'section': 'rattail',
|
{'section': 'rattail',
|
||||||
'option': 'production',
|
'option': 'production',
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
{'section': 'rattail',
|
||||||
|
'option': 'running_from_source',
|
||||||
|
'type': bool},
|
||||||
|
{'section': 'rattail',
|
||||||
|
'option': 'running_from_source.rootpkg'},
|
||||||
|
|
||||||
# display
|
# display
|
||||||
{'section': 'tailbone',
|
{'section': 'tailbone',
|
||||||
|
|
|
@ -67,6 +67,7 @@ class TableView(MasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
rows_title = "Columns"
|
||||||
rows_pageable = False
|
rows_pageable = False
|
||||||
rows_filterable = False
|
rows_filterable = False
|
||||||
rows_viewable = False
|
rows_viewable = False
|
||||||
|
@ -170,6 +171,27 @@ class TableView(MasterView):
|
||||||
def make_form_schema(self):
|
def make_form_schema(self):
|
||||||
return TableSchema()
|
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):
|
def template_kwargs_create(self, **kwargs):
|
||||||
kwargs = super(TableView, self).template_kwargs_create(**kwargs)
|
kwargs = super(TableView, self).template_kwargs_create(**kwargs)
|
||||||
app = self.get_rattail_app()
|
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