diff --git a/CHANGELOG.md b/CHANGELOG.md index 57295c5..7e13a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,6 @@ All notable changes to wuttaweb will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.26.0 (2025-12-28) - -### Feat - -- add "wizard" for creating new table/model/revision -- add support for Create Alembic Migration -- add CopyableTextWidget and `` component -- overhaul how form vue template is rendered -- add basic views for Alembic Migrations, Dashboard -- add basic Table views - -### Fix - -- let checkbox widget show static text instead of Yes/No -- rename form-saving methods etc. for consistency in MasterView -- temporarily avoid make_uuid() -- remove password filter option for Users grid -- use smarter default for `grid.sort_multiple` based on model class - ## v0.25.1 (2025-12-20) ### Fix diff --git a/docs/api/wuttaweb.views.alembic.rst b/docs/api/wuttaweb.views.alembic.rst deleted file mode 100644 index ce82a7a..0000000 --- a/docs/api/wuttaweb.views.alembic.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.views.alembic`` -========================== - -.. automodule:: wuttaweb.views.alembic - :members: diff --git a/docs/api/wuttaweb.views.tables.rst b/docs/api/wuttaweb.views.tables.rst deleted file mode 100644 index 4594aaa..0000000 --- a/docs/api/wuttaweb.views.tables.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.views.tables`` -========================= - -.. automodule:: wuttaweb.views.tables - :members: diff --git a/docs/conf.py b/docs/conf.py index 7465596..6bcd169 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,6 @@ templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = { - "alembic": ("https://alembic.sqlalchemy.org/en/latest/", None), "colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None), "deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None), "fanstatic": ("https://www.fanstatic.org/en/latest/", None), diff --git a/docs/index.rst b/docs/index.rst index f910cc9..bd5c25a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,7 +58,6 @@ the narrative docs are pretty scant. That will eventually change. api/wuttaweb.subscribers api/wuttaweb.util api/wuttaweb.views - api/wuttaweb.views.alembic api/wuttaweb.views.auth api/wuttaweb.views.base api/wuttaweb.views.batch @@ -71,7 +70,6 @@ the narrative docs are pretty scant. That will eventually change. api/wuttaweb.views.reports api/wuttaweb.views.roles api/wuttaweb.views.settings - api/wuttaweb.views.tables api/wuttaweb.views.upgrades api/wuttaweb.views.users diff --git a/pyproject.toml b/pyproject.toml index 263fe66..17e3c94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.26.0" +version = "0.25.1" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -42,10 +42,9 @@ dependencies = [ "pyramid_fanstatic", "pyramid_mako", "pyramid_tm", - "SQLAlchemy-Utils", "waitress", "WebHelpers2", - "WuttJamaican[db]>=0.28.0", + "WuttJamaican[db]>=0.27.0", "zope.sqlalchemy>=1.5", ] diff --git a/src/wuttaweb/code-templates/new-table.mako b/src/wuttaweb/code-templates/new-table.mako deleted file mode 100644 index 046862f..0000000 --- a/src/wuttaweb/code-templates/new-table.mako +++ /dev/null @@ -1,68 +0,0 @@ -## -*- coding: utf-8; mode: python; -*- -# -*- coding: utf-8; -*- -""" -Model definition for ${model_title_plural} -""" - -import sqlalchemy as sa -from sqlalchemy import orm - -from wuttjamaican.db import model - - -class ${model_name}(model.Base): - """ - ${description} - """ - __tablename__ = "${table_name}" - % if any([c["data_type"]["type"] == "_fk_uuid_" for c in columns]): - __table_args__ = ( - % for column in columns: - % if column["data_type"]["type"] == "_fk_uuid_": - sa.ForeignKeyConstraint(["${column['name']}"], ["${column['data_type']['reference']}.uuid"], - name="${table_name}_fk_${column['data_type']['reference']}"), - % endif - % endfor - ) - % endif - % if versioned: - % if all([c["versioned"] for c in columns]): - __versioned__ = {} - % else: - __versioned__ = { - "exclude": [ - % for column in columns: - % if not column["versioned"]: - "${column['name']}", - % endif - % endfor - ], - } - % endif - % endif - __wutta_hint__ = { - "model_title": "${model_title}", - "model_title_plural": "${model_title_plural}", - } - % for column in columns: - - % if column["name"] == "uuid": - uuid = model.uuid_column() - % else: - ${column["name"]} = sa.Column(${column["formatted_data_type"]}, nullable=${column["nullable"]}, doc=""" - ${column["description"] or ""} - """) - % if column["data_type"]["type"] == "_fk_uuid_" and column["relationship"]: - ${column["relationship"]["name"]} = orm.relationship( - "${column['relationship']['reference_model']}", - doc=""" - ${column["description"] or ""} - """) - % endif - % endif - % endfor - - # TODO: you usually should define the __str__() method - - # def __str__(self): - # return self.name or "" diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index 0d2a42d..6365aa8 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -275,10 +275,6 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth deform_form = None validated = None - vue_template = "/forms/vue_template.mako" - fields_template = "/forms/vue_fields.mako" - buttons_template = "/forms/vue_buttons.mako" - def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals self, request, @@ -335,7 +331,6 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth self.show_button_cancel = show_button_cancel self.button_label_cancel = button_label_cancel self.auto_disable_cancel = auto_disable_cancel - self.form_attrs = {} self.config = self.request.wutta_config self.app = self.config.get_app() @@ -811,10 +806,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth # get fields fields = self.get_fields() if not fields: - raise ValueError( - "could not determine fields list; " - "please set model_class or fields explicitly" - ) + raise NotImplementedError if self.model_class: @@ -947,7 +939,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth """ return HTML.tag(self.vue_tagname, **kwargs) - def render_vue_template(self, template=None, **context): + def render_vue_template(self, template="/forms/vue_template.mako", **context): """ Render the Vue template block for the form. @@ -962,8 +954,8 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth @@ -977,121 +969,36 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth Actual output will of course depend on form attributes, i.e. :attr:`vue_tagname` and :attr:`fields` list etc. - Default logic will also invoke (indirectly): - - * :meth:`render_vue_fields()` - * :meth:`render_vue_buttons()` - - :param template: Optional template path to override the class - default. - - :returns: HTML literal + :param template: Path to Mako template which is used to render + the output. """ - context = self.get_vue_context(**context) - html = render(template or self.vue_template, context) - return HTML.literal(html) - - def get_vue_context(self, **context): # pylint: disable=missing-function-docstring context["form"] = self context["dform"] = self.get_deform() context.setdefault("request", self.request) context["model_data"] = self.get_vue_model_data() # set form method, enctype - form_attrs = context.setdefault("form_attrs", dict(self.form_attrs)) - form_attrs.setdefault("method", self.action_method) + context.setdefault("form_attrs", {}) + context["form_attrs"].setdefault("method", self.action_method) if self.action_method == "post": - form_attrs.setdefault("enctype", "multipart/form-data") + context["form_attrs"].setdefault("enctype", "multipart/form-data") # auto disable button on submit if self.auto_disable_submit: - form_attrs["@submit"] = "formSubmitting = true" + context["form_attrs"]["@submit"] = "formSubmitting = true" - # duplicate entire context for sake of fields/buttons template - context["form_context"] = context - - return context - - def render_vue_fields(self, context, template=None, **kwargs): - """ - Render the fields section within the form template. - - This is normally invoked from within the form's - ``vue_template`` like this: - - .. code-block:: none - - ${form.render_vue_fields(form_context)} - - There is a default ``fields_template`` but that is only the - last resort. Logic will first look for a - ``form_vue_fields()`` def within the *main template* being - rendered for the page. - - An example will surely help: - - .. code-block:: mako - - <%inherit file="/master/edit.mako" /> - - <%def name="form_vue_fields()"> - -

this is my custom fields section:

- - ${form.render_vue_field("myfield")} - - - - This keeps the custom fields section within the main page - template as opposed to yet another file. But if your page - template has no ``form_vue_fields()`` def, then the class - default template is used. (Unless the ``template`` param - is specified.) - - See also :meth:`render_vue_template()` and - :meth:`render_vue_buttons()`. - - :param context: This must be the original context as provided - to the form's ``vue_template``. See example above. - - :param template: Optional template path to use instead of the - defaults described above. - - :returns: HTML literal - """ - context.update(kwargs) - html = False - - if not template: - - if main_template := context.get("main_template"): - try: - vue_fields = main_template.get_def("form_vue_fields") - except AttributeError: - pass - else: - html = vue_fields.render(**context) - - if html is False: - template = self.fields_template - - if html is False: - html = render(template, context) - - return HTML.literal(html) + output = render(template, context) + return HTML.literal(output) def render_vue_field( # pylint: disable=unused-argument,too-many-locals self, fieldname, readonly=None, - label=True, - horizontal=True, **kwargs, ): """ Render the given field completely, i.e. ```` wrapper - with label and a widget, with validation errors flagged as - needed. + with label and containing a widget. Actual output will depend on the field attributes etc. Typical output might look like: @@ -1102,23 +1009,14 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth horizontal type="is-danger" message="something went wrong!"> - + - :param fieldname: Name of field to render. + .. warning:: - :param readonly: Optional override for readonly flag. - - :param label: Whether to include/set the field label. - - :param horizontal: Boolean value for the ``horizontal`` flag - on the field. - - :param \\**kwargs: Remaining kwargs are passed to widget's - ``serialize()`` method. - - :returns: HTML literal + Any ``**kwargs`` received from caller are ignored by this + method. For now they are allowed, for sake of backwawrd + compatibility. This may change in the future. """ # readonly comes from: caller, field flag, or form flag if readonly is None: @@ -1136,9 +1034,10 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth # render proper widget if field is in deform/schema field = dform[fieldname] + kw = {} if readonly: - kwargs["readonly"] = True - html = field.serialize(**kwargs) + kw["readonly"] = True + html = field.serialize(**kw) else: # render static text if field not in deform/schema @@ -1153,13 +1052,12 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth html = HTML.literal(html or " ") # render field label - if label: - label = self.get_label(fieldname) + label = self.get_label(fieldname) # b-field attrs attrs = { - ":horizontal": "true" if horizontal else "false", - "label": label or "", + ":horizontal": "true", + "label": label, } # next we will build array of messages to display..some @@ -1187,36 +1085,6 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth return HTML.tag("b-field", c=[html], **attrs) - def render_vue_buttons(self, context, template=None, **kwargs): - """ - Render the buttons section within the form template. - - This is normally invoked from within the form's - ``vue_template`` like this: - - .. code-block:: none - - ${form.render_vue_buttons(form_context)} - - .. note:: - - This method does not yet inspect the main page template, - unlike :meth:`render_vue_fields()`. - - See also :meth:`render_vue_template()`. - - :param context: This must be the original context as provided - to the form's ``vue_template``. See example above. - - :param template: Optional template path to override the class - default. - - :returns: HTML literal - """ - context.update(kwargs) - html = render(template or self.buttons_template, context) - return HTML.literal(html) - def render_vue_finalize(self): """ Render the Vue "finalize" script for the form. @@ -1235,25 +1103,6 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth """ return render_vue_finalize(self.vue_tagname, self.vue_component) - def get_field_vmodel(self, field): - """ - Convenience to return the ``v-model`` data reference for the - given field. For instance: - - .. code-block:: none - - - -
- easter egg! -
- - :returns: JS-valid string referencing the field value - """ - dform = self.get_deform() - return f"modelData.{dform[field].oid}" - def get_vue_model_data(self): """ Returns a dict with form model data. Values may be nested diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index 72c4d79..0ef6d87 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -60,7 +60,7 @@ from deform.widget import ( # pylint: disable=unused-import DateTimeInputWidget, MoneyInputWidget, ) -from webhelpers2.html import HTML, tags +from webhelpers2.html import HTML from wuttjamaican.conf import parse_list @@ -147,24 +147,6 @@ class NotesWidget(TextAreaWidget): readonly_template = "readonly/notes" -class CopyableTextWidget(Widget): # pylint: disable=abstract-method - """ - A readonly text widget which adds a "copy" icon/link just after - the text. - """ - - def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring - """ """ - if not cstruct: - return colander.null - - return HTML.tag("wutta-copyable-text", **{"text": cstruct}) - - def deserialize(self, field, pstruct): # pylint: disable=empty-docstring - """ """ - raise NotImplementedError - - class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): """ Custom widget for :class:`python:set` fields. @@ -555,58 +537,3 @@ class BatchIdWidget(Widget): # pylint: disable=abstract-method batch_id = int(cstruct) return f"{batch_id:08d}" - - -class AlembicRevisionWidget(Widget): # pylint: disable=missing-class-docstring - """ - Widget to show an Alembic revision identifier, with link to view - the revision. - """ - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - - def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring - """ """ - if not cstruct: - return colander.null - - return tags.link_to( - cstruct, self.request.route_url("alembic.migrations.view", revision=cstruct) - ) - - def deserialize(self, field, pstruct): # pylint: disable=empty-docstring - """ """ - raise NotImplementedError - - -class AlembicRevisionsWidget(Widget): - """ - Widget to show list of Alembic revision identifiers, with links to - view each revision. - """ - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.config = self.request.wutta_config - - def serialize(self, field, cstruct, **kw): # pylint: disable=empty-docstring - """ """ - if not cstruct: - return colander.null - - revisions = [] - for rev in self.config.parse_list(cstruct): - revisions.append( - tags.link_to( - rev, self.request.route_url("alembic.migrations.view", revision=rev) - ) - ) - - return ", ".join(revisions) - - def deserialize(self, field, pstruct): # pylint: disable=empty-docstring - """ """ - raise NotImplementedError diff --git a/src/wuttaweb/templates/alembic/dashboard.mako b/src/wuttaweb/templates/alembic/dashboard.mako deleted file mode 100644 index badacc7..0000000 --- a/src/wuttaweb/templates/alembic/dashboard.mako +++ /dev/null @@ -1,234 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/page.mako" /> - -<%def name="page_content()"> -
- -
-
- - - {{ script.dir }} - - - - {{ script.env_py_location }} - - - -
    -
  • - {{ path }} -
  • -
-
- -
- -
-
- % if request.has_perm("app_tables.list"): - - % endif - % if request.has_perm("app_tables.create"): - - % endif -
-
- % if request.has_perm("alembic.migrations.list"): - - % endif - % if request.has_perm("alembic.migrations.create"): - - % endif -
- % if request.has_perm("alembic.migrate"): -
- - Migrate Database - -
- % endif -
-
- -
-

Script Heads

- - - - {{ props.row.branch_labels }} - - - {{ props.row.doc }} - - - {{ props.row.is_current ? "Yes" : "No" }} - - - - - - - - - {{ props.row.path }} - - - -
-

Database Heads

- - - - {{ props.row.branch_labels }} - - - {{ props.row.doc }} - - - - - - - - - {{ props.row.path }} - - - - % if request.has_perm("alembic.migrate"): - <${b}-modal has-modal-card - :active.sync="migrateShowDialog"> - - - % endif - - - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - - diff --git a/src/wuttaweb/templates/alembic/migrations/configure.mako b/src/wuttaweb/templates/alembic/migrations/configure.mako deleted file mode 100644 index 903a85d..0000000 --- a/src/wuttaweb/templates/alembic/migrations/configure.mako +++ /dev/null @@ -1,31 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/configure.mako" /> - -<%def name="form_content()"> - -

Basics

-
- - - - - - - - -
- - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - - diff --git a/src/wuttaweb/templates/alembic/migrations/create.mako b/src/wuttaweb/templates/alembic/migrations/create.mako deleted file mode 100644 index af3803e..0000000 --- a/src/wuttaweb/templates/alembic/migrations/create.mako +++ /dev/null @@ -1,70 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/master/create.mako" /> - -<%def name="page_content()"> -
-
- % if request.has_perm("alembic.dashboard"): - - % endif -
- - ${parent.page_content()} - - -<%def name="form_vue_fields()"> - - ${form.render_vue_field("description", horizontal=False)} - - ${form.render_vue_field("autogenerate", horizontal=False, label=False, static_text="Auto-generate migration logic based on current app model")} - -
- - -
- -
- - Revise existing branch - -
- -
- - ${form.render_vue_field("revise_branch", horizontal=True)} - -
- -
- - Start new branch - -
- -
- - ${form.render_vue_field("new_branch", horizontal=True)} - ${form.render_vue_field("version_location", horizontal=True)} - -

- NOTE: New version locations must be added to the - [alembic] section of - your config file (and app restarted) before they will appear as - options here. -

- -
-
- -
- diff --git a/src/wuttaweb/templates/alembic/migrations/index.mako b/src/wuttaweb/templates/alembic/migrations/index.mako deleted file mode 100644 index bcda890..0000000 --- a/src/wuttaweb/templates/alembic/migrations/index.mako +++ /dev/null @@ -1,25 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/master/index.mako" /> - -<%def name="page_content()"> -
- - % if request.has_perm("alembic.dashboard"): - - % endif - - % if request.has_perm("app_tables.list"): - - % endif - -
- ${parent.page_content()} - diff --git a/src/wuttaweb/templates/alembic/migrations/view.mako b/src/wuttaweb/templates/alembic/migrations/view.mako deleted file mode 100644 index 6bd47e8..0000000 --- a/src/wuttaweb/templates/alembic/migrations/view.mako +++ /dev/null @@ -1,17 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/master/view.mako" /> - -<%def name="page_content()"> -
-
- % if request.has_perm("alembic.dashboard"): - - % endif -
- - ${parent.page_content()} - diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako index 137d8b1..7f1abb5 100644 --- a/src/wuttaweb/templates/appinfo/index.mako +++ b/src/wuttaweb/templates/appinfo/index.mako @@ -32,26 +32,6 @@ -
- - % if request.has_perm("app_tables.list"): - - % endif - - % if request.has_perm("alembic.dashboard"): - - % endif - -
-