From a061e362c377fe1d89389d697c4121f9e83a3467 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 2 Jan 2023 09:44:05 -0600 Subject: [PATCH] Add beginnings of "New Table" feature nowhere near complete yet, but skeleton is more or less in place --- tailbone/forms/core.py | 2 + tailbone/templates/tables/create.mako | 214 ++++++++++++++++++++++++++ tailbone/views/master.py | 2 +- tailbone/views/tables.py | 183 ++++++++++++++++++++-- 4 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 tailbone/templates/tables/create.mako diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index ea8808e3..a791f4cb 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -1135,6 +1135,8 @@ class Form(object): if record: try: return record[field_name] + except KeyError: + return None except TypeError: return getattr(record, field_name, None) diff --git a/tailbone/templates/tables/create.mako b/tailbone/templates/tables/create.mako new file mode 100644 index 00000000..90d9d26f --- /dev/null +++ b/tailbone/templates/tables/create.mako @@ -0,0 +1,214 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/create.mako" /> + +<%def name="extra_styles()"> + ${parent.extra_styles()} + + + +<%def name="render_buefy_form()"> + + + +

+ Enter Details +

+ ${parent.render_buefy_form()} +
+ + +

+ Write Model +

+
+ + TODO: poser_widget + + + TODO: PoserWidget + + + TODO: ~/src/poser/poser/db/model/widgets.py + +
+ + Back + + + Write model class to file + +
+
+
+ + +

+ Review Model +

+

TODO: review model class here

+
+ + Back + + + Model class looks good! + +
+
+ + +

+ Write Revision +

+

+ You said the model class looked good, so next we will generate + a revision script, used to modify DB schema. +

+

+ TODO: write revision script here +

+
+ + Back + + + Write revision script to file + +
+
+ + +

+ Review Revision +

+

TODO: review revision script here

+
+ + Back + + + Revision script looks good! + +
+
+ + +

+ Upgrade DB +

+

TODO: upgrade DB here

+
+ + Back + + + Upgrade database + +
+
+ + +

+ Review DB +

+

TODO: review DB here

+
+ + Back + + + DB looks good! + +
+
+ + +

+ Commit Code +

+

TODO: commit changes here

+
+ + Back + + + Code changes are committed! + +
+
+
+ + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index d382918c..98355ec3 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -676,7 +676,7 @@ class MasterView(View): return self.render_to_response(template, context) def make_create_form(self): - return self.make_form(self.get_model_class()) + return self.make_form() def save_create_form(self, form): uploads = self.normalize_uploads(form) diff --git a/tailbone/views/tables.py b/tailbone/views/tables.py index 5d4f7d95..49d9e7a5 100644 --- a/tailbone/views/tables.py +++ b/tailbone/views/tables.py @@ -26,6 +26,12 @@ Views with info about the underlying Rattail tables from __future__ import unicode_literals, absolute_import +import sys +import warnings + +import colander +from deform import widget as dfwidget + from tailbone.views import MasterView @@ -34,20 +40,31 @@ class TableView(MasterView): Master view for tables """ normalized_model_name = 'table' - model_key = 'name' + model_key = 'table_name' model_title = "Table" creatable = False editable = False deletable = False - viewable = False filterable = False pageable = False + labels = { + 'branch_name': "Schema Branch", + 'model_name': "Model Class", + 'module_name': "Module", + 'module_file': "File", + } + grid_columns = [ - 'name', + 'table_name', '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): """ Fetch existing table names and estimate row counts via PG SQL @@ -62,18 +79,166 @@ class TableView(MasterView): order by n_live_tup desc; """ 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] 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.set_sort_defaults('name') - g.set_searchable('name') + def get_instance(self): + from sqlalchemy_utils import get_mapper -# TODO: deprecate / remove this -TablesView = TableView + model = self.model + 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):