diff --git a/docs/api/wuttaweb.views.alembic.rst b/docs/api/wuttaweb.views.alembic.rst
new file mode 100644
index 0000000..ce82a7a
--- /dev/null
+++ b/docs/api/wuttaweb.views.alembic.rst
@@ -0,0 +1,6 @@
+
+``wuttaweb.views.alembic``
+==========================
+
+.. automodule:: wuttaweb.views.alembic
+ :members:
diff --git a/docs/conf.py b/docs/conf.py
index 6bcd169..7465596 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -28,6 +28,7 @@ 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 450476e..f910cc9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -58,6 +58,7 @@ 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
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index 0ef6d87..13331b3 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
+from webhelpers2.html import HTML, tags
from wuttjamaican.conf import parse_list
@@ -537,3 +537,58 @@ 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
new file mode 100644
index 0000000..34ce1a7
--- /dev/null
+++ b/src/wuttaweb/templates/alembic/dashboard.mako
@@ -0,0 +1,220 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/page.mako" />
+
+<%def name="page_content()">
+
+
+
+
+
+
+ {{ script.dir }}
+
+
+
+ {{ script.env_py_location }}
+
+
+
+
+
+
+
+
+
+
+ % if request.has_perm("tables.list"):
+
+ % endif
+
+
+ % if request.has_perm("alembic.migrations.list"):
+
+ % 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">
+
+
+
+
+
+ ${h.form(url("alembic.migrate"), method="POST", ref="migrateForm")}
+ ${h.csrf_token(request)}
+
+
+ You can provide any revspec target. Default will
+ upgrade to all branch heads.
+
+
+
+
+ - alembic upgrade heads
+ - alembic upgrade poser@head
+ - alembic downgrade poser@-1
+ - alembic downgrade poser@base
+ - alembic downgrade fc3a3bcaa069
+
+
+
+
+ don't try that last one ;)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${h.end_form()}
+
+
+
+
+ ${b}-modal>
+ % endif
+
+%def>
+
+<%def name="modify_vue_vars()">
+ ${parent.modify_vue_vars()}
+
+%def>
diff --git a/src/wuttaweb/templates/alembic/migrations/index.mako b/src/wuttaweb/templates/alembic/migrations/index.mako
new file mode 100644
index 0000000..b3cbd79
--- /dev/null
+++ b/src/wuttaweb/templates/alembic/migrations/index.mako
@@ -0,0 +1,25 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/master/index.mako" />
+
+<%def name="page_content()">
+
+
+ % if request.has_perm("alembic.dashboard"):
+
+ % endif
+
+ % if request.has_perm("tables.list"):
+
+ % endif
+
+
+ ${parent.page_content()}
+%def>
diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako
index 45974b6..554be2f 100644
--- a/src/wuttaweb/templates/appinfo/index.mako
+++ b/src/wuttaweb/templates/appinfo/index.mako
@@ -42,6 +42,14 @@
once />
% endif
+ % if request.has_perm("alembic.dashboard"):
+
+ % endif
+