Add beginnings of "New Table" feature

nowhere near complete yet, but skeleton is more or less in place
This commit is contained in:
Lance Edgar 2023-01-02 09:44:05 -06:00
parent 7e852c1836
commit a061e362c3
4 changed files with 391 additions and 10 deletions

View file

@ -1135,6 +1135,8 @@ class Form(object):
if record: if record:
try: try:
return record[field_name] return record[field_name]
except KeyError:
return None
except TypeError: except TypeError:
return getattr(record, field_name, None) return getattr(record, field_name, None)

View file

@ -0,0 +1,214 @@
## -*- 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_buefy_form()">
<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>
${parent.render_buefy_form()}
</b-step-item>
<b-step-item step="2"
value="write-model"
label="Write Model"
clickable>
<h3 class="is-size-3 block">
Write Model
</h3>
<div class="form">
<b-field horizontal label="Table Name">
<span>TODO: poser_widget</span>
</b-field>
<b-field horizontal label="Model Class">
<span>TODO: PoserWidget</span>
</b-field>
<b-field horizontal label="File">
<span>TODO: ~/src/poser/poser/db/model/widgets.py</span>
</b-field>
<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="activeStep = 'review-model'">
Write model class to file
</b-button>
</div>
</div>
</b-step-item>
<b-step-item step="3"
value="review-model"
label="Review Model">
<h3 class="is-size-3 block">
Review Model
</h3>
<p class="block">TODO: review model class here</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'write-model'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="check"
@click="activeStep = 'write-revision'">
Model class looks good!
</b-button>
</div>
</b-step-item>
<b-step-item step="4"
value="write-revision"
label="Write Revision">
<h3 class="is-size-3 block">
Write Revision
</h3>
<p class="block">
You said the model class looked good, so next we will generate
a revision script, used to modify DB schema.
</p>
<p class="block">
TODO: write revision script here
</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'review-model'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="save"
@click="activeStep = 'review-revision'">
Write revision script to file
</b-button>
</div>
</b-step-item>
<b-step-item step="5"
value="review-revision"
label="Review Revision">
<h3 class="is-size-3 block">
Review Revision
</h3>
<p class="block">TODO: review revision script here</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'write-revision'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="check"
@click="activeStep = 'upgrade-db'">
Revision script looks good!
</b-button>
</div>
</b-step-item>
<b-step-item step="6"
value="upgrade-db"
label="Upgrade DB">
<h3 class="is-size-3 block">
Upgrade DB
</h3>
<p class="block">TODO: upgrade DB here</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'review-revision'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="check"
@click="activeStep = 'review-db'">
Upgrade database
</b-button>
</div>
</b-step-item>
<b-step-item step="7"
value="review-db"
label="Review DB">
<h3 class="is-size-3 block">
Review DB
</h3>
<p class="block">TODO: review DB here</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'upgrade-db'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="check"
@click="activeStep = 'commit-code'">
DB looks good!
</b-button>
</div>
</b-step-item>
<b-step-item step="8"
value="commit-code"
label="Commit Code">
<h3 class="is-size-3 block">
Commit Code
</h3>
<p class="block">TODO: commit changes here</p>
<div class="buttons">
<b-button icon-pack="fas"
icon-left="arrow-left"
@click="activeStep = 'review-db'">
Back
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="check"
@click="alert('TODO: redirect to table view')">
Code changes are committed!
</b-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
</script>
</%def>
${parent.body()}

View file

@ -676,7 +676,7 @@ class MasterView(View):
return self.render_to_response(template, context) return self.render_to_response(template, context)
def make_create_form(self): def make_create_form(self):
return self.make_form(self.get_model_class()) return self.make_form()
def save_create_form(self, form): def save_create_form(self, form):
uploads = self.normalize_uploads(form) uploads = self.normalize_uploads(form)

View file

@ -26,6 +26,12 @@ Views with info about the underlying Rattail tables
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import sys
import warnings
import colander
from deform import widget as dfwidget
from tailbone.views import MasterView from tailbone.views import MasterView
@ -34,20 +40,31 @@ class TableView(MasterView):
Master view for tables Master view for tables
""" """
normalized_model_name = 'table' normalized_model_name = 'table'
model_key = 'name' model_key = 'table_name'
model_title = "Table" model_title = "Table"
creatable = False creatable = False
editable = False editable = False
deletable = False deletable = False
viewable = False
filterable = False filterable = False
pageable = False pageable = False
labels = {
'branch_name': "Schema Branch",
'model_name': "Model Class",
'module_name': "Module",
'module_file': "File",
}
grid_columns = [ grid_columns = [
'name', 'table_name',
'row_count', 'row_count',
] ]
def __init__(self, request):
super(TableView, self).__init__(request)
app = self.get_rattail_app()
self.db_handler = app.get_db_handler()
def get_data(self, **kwargs): def get_data(self, **kwargs):
""" """
Fetch existing table names and estimate row counts via PG SQL Fetch existing table names and estimate row counts via PG SQL
@ -62,18 +79,166 @@ class TableView(MasterView):
order by n_live_tup desc; order by n_live_tup desc;
""" """
result = self.Session.execute(sql) result = self.Session.execute(sql)
return [dict(name=row['relname'], row_count=row['n_live_tup']) return [dict(table_name=row['relname'], row_count=row['n_live_tup'])
for row in result] for row in result]
def configure_grid(self, g): def configure_grid(self, g):
g.sorters['name'] = g.make_simple_sorter('name', foldcase=True) super(TableView, self).configure_grid(g)
# table_name
g.sorters['table_name'] = g.make_simple_sorter('table_name', foldcase=True)
g.set_sort_defaults('table_name')
g.set_searchable('table_name')
g.set_link('table_name')
# row_count
g.sorters['row_count'] = g.make_simple_sorter('row_count') g.sorters['row_count'] = g.make_simple_sorter('row_count')
g.set_sort_defaults('name')
g.set_searchable('name') def get_instance(self):
from sqlalchemy_utils import get_mapper
# TODO: deprecate / remove this model = self.model
TablesView = TableView table_name = self.request.matchdict['table_name']
sql = """
select n_live_tup
from pg_stat_user_tables
where schemaname = 'public' and relname = :table_name
order by n_live_tup desc;
"""
result = self.Session.execute(sql, {'table_name': table_name})
row = result.fetchone()
if not row:
raise self.notfound()
data = {
'table_name': table_name,
'row_count': row['n_live_tup'],
}
table = model.Base.metadata.tables.get(table_name)
if table is not None:
try:
mapper = get_mapper(table)
except ValueError:
pass
else:
data['model_name'] = mapper.class_.__name__
data['model_title'] = mapper.class_.get_model_title()
data['model_title_plural'] = mapper.class_.get_model_title_plural()
data['description'] = mapper.class_.__doc__
# TODO: how to reliably get branch? must walk all revisions?
module_parts = mapper.class_.__module__.split('.')
data['branch_name'] = module_parts[0]
data['module_name'] = mapper.class_.__module__
data['module_file'] = sys.modules[mapper.class_.__module__].__file__
return data
def get_instance_title(self, table):
return table['table_name']
def make_form_schema(self):
return TableSchema()
def configure_form(self, f):
super(TableView, self).configure_form(f)
app = self.get_rattail_app()
# exclude some fields when creating
if self.creating:
f.remove('row_count',
'module_name',
'module_file')
# branch_name
if self.creating:
# move this field to top of form, as it's more fundamental
# when creating new table
f.remove('branch_name')
f.insert(0, 'branch_name')
# define options for dropdown
branches = self.db_handler.get_alembic_branch_names()
values = [(branch, branch) for branch in branches]
f.set_widget('branch_name', dfwidget.SelectWidget(values=values))
# default to custom app branch, if applicable
table_prefix = app.get_table_prefix()
if table_prefix in branches:
f.set_default('branch_name', table_prefix)
f.set_helptext('branch_name', "Leave this set to your custom app branch, unless you know what you're doing.")
# table_name
if self.creating:
f.set_default('table_name', '{}_widget'.format(app.get_table_prefix()))
f.set_helptext('table_name', "Should be singular in nature, i.e. 'widget' not 'widgets'")
# model_name
if self.creating:
f.set_default('model_name', '{}Widget'.format(app.get_class_prefix()))
f.set_helptext('model_name', "Should be singular in nature, i.e. 'Widget' not 'Widgets'")
# model_title*
if self.creating:
f.set_default('model_title', 'Widget')
f.set_helptext('model_title', "Human-friendly singular model title.")
f.set_default('model_title_plural', 'Widgets')
f.set_helptext('model_title_plural', "Human-friendly plural model title.")
# description
if self.creating:
f.set_default('description', "Represents a cool widget.")
f.set_helptext('description', "Brief description of what a record in this table represents.")
# TODO: not sure yet how to handle "save" action
# def save_create_form(self, form):
# return form.validated
@classmethod
def defaults(cls, config):
rattail_config = config.registry.settings.get('rattail_config')
# allow creating tables only if *not* production
if not rattail_config.production():
cls.creatable = True
cls._defaults(config)
class TablesView(TableView):
def __init__(self, request):
warnings.warn("TablesView is deprecated; please use TableView instead",
DeprecationWarning, stacklevel=2)
super(TablesView, self).__init__(request)
class TableSchema(colander.Schema):
table_name = colander.SchemaNode(colander.String())
row_count = colander.SchemaNode(colander.Integer(),
missing=colander.null)
model_name = colander.SchemaNode(colander.String())
model_title = colander.SchemaNode(colander.String())
model_title_plural = colander.SchemaNode(colander.String())
description = colander.SchemaNode(colander.String())
branch_name = colander.SchemaNode(colander.String())
module_name = colander.SchemaNode(colander.String(),
missing=colander.null)
module_file = colander.SchemaNode(colander.String(),
missing=colander.null)
def defaults(config, **kwargs): def defaults(config, **kwargs):