diff --git a/CHANGELOG.md b/CHANGELOG.md
index b17ddc7..0935339 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,30 +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.12.0 (2024-08-22)
-
-### Feat
-
-- add "copy link" button for sharing a grid view
-- add initial support for proper grid filters
-- add initial filtering logic to grid class
-- add "searchable" column support for grids
-- improve page linkage between role/user/person
-- add basic autocomplete support, for Person
-
-### Fix
-
-- cleanup templates for home, login pages
-- cleanup logic for appinfo/configure
-- expose settings for app node title, type
-- show installed python packages on appinfo page
-- tweak login form to stop extending size of background card
-- add setting to auto-redirect anon users to login, from home page
-- add form padding, validators for /configure pages
-- add padding around main form, via wrapper css
-- show CRUD buttons in header only if relevant and user has access
-- tweak style config for home link app title in main menu
-
## v0.11.0 (2024-08-20)
### Feat
diff --git a/pyproject.toml b/pyproject.toml
index 9fc1d83..fe91ac9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.12.0"
+version = "0.11.0"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@@ -41,7 +41,7 @@ dependencies = [
"pyramid_tm",
"waitress",
"WebHelpers2",
- "WuttJamaican[db]>=0.12.1",
+ "WuttJamaican[db]>=0.12.0",
"zope.sqlalchemy>=1.5",
]
diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py
index d5b893a..5acca59 100644
--- a/src/wuttaweb/forms/base.py
+++ b/src/wuttaweb/forms/base.py
@@ -25,7 +25,6 @@ Base form classes
"""
import logging
-from collections import OrderedDict
import colander
import deform
@@ -312,9 +311,6 @@ class Form:
self.set_fields(fields or self.get_fields())
- # nb. this tracks grid JSON data for inclusion in page template
- self.grid_vue_data = OrderedDict()
-
def __contains__(self, name):
"""
Custom logic for the ``in`` operator, to allow easily checking
@@ -754,10 +750,6 @@ class Form:
kwargs['appstruct'] = self.model_instance
form = deform.Form(schema, **kwargs)
- # nb. must give a reference back to wutta form; this is
- # for sake of field schema nodes and widgets, e.g. to
- # access the main model instance
- form.wutta_form = self
self.deform_form = form
return self.deform_form
@@ -826,17 +818,6 @@ class Form:
output = render(template, context)
return HTML.literal(output)
- def add_grid_vue_data(self, grid):
- """ """
- if not grid.key:
- raise ValueError("grid must have a key!")
-
- if grid.key in self.grid_vue_data:
- log.warning("grid data with key '%s' already registered, "
- "but will be replaced", grid.key)
-
- self.grid_vue_data[grid.key] = grid.get_vue_data()
-
def render_vue_field(
self,
fieldname,
diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py
index a3a464b..8c245f6 100644
--- a/src/wuttaweb/forms/schema.py
+++ b/src/wuttaweb/forms/schema.py
@@ -246,9 +246,6 @@ class ObjectRef(colander.SchemaType):
values.insert(0, self.empty_option)
kwargs['values'] = values
- if 'url' not in kwargs:
- kwargs['url'] = lambda person: self.request.route_url('people.view', uuid=person.uuid)
-
return widgets.ObjectRefWidget(self.request, **kwargs)
@@ -324,28 +321,6 @@ class RoleRefs(WuttaSet):
return widgets.RoleRefsWidget(self.request, **kwargs)
-class UserRefs(WuttaSet):
- """
- Form schema type for the Role
- :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users`
- association proxy field.
-
- This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
- :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` ``uuid``
- values for underlying data format.
- """
-
- def widget_maker(self, **kwargs):
- """
- Constructs a default widget for the field.
-
- :returns: Instance of
- :class:`~wuttaweb.forms.widgets.UserRefsWidget`.
- """
- kwargs.setdefault('session', self.session)
- return widgets.UserRefsWidget(self.request, **kwargs)
-
-
class Permissions(WuttaSet):
"""
Form schema type for the Role
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index ee58a1a..b4d8254 100644
--- a/src/wuttaweb/forms/widgets.py
+++ b/src/wuttaweb/forms/widgets.py
@@ -44,7 +44,6 @@ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
from webhelpers2.html import HTML
from wuttaweb.db import Session
-from wuttaweb.grids import Grid
class ObjectRefWidget(SelectWidget):
@@ -84,19 +83,9 @@ class ObjectRefWidget(SelectWidget):
"""
readonly_template = 'readonly/objectref'
- def __init__(self, request, url=None, *args, **kwargs):
+ def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
- self.url = url
-
- def get_template_values(self, field, cstruct, kw):
- """ """
- values = super().get_template_values(field, cstruct, kw)
-
- if 'url' not in values and self.url and field.schema.model_instance:
- values['url'] = self.url(field.schema.model_instance)
-
- return values
class NotesWidget(TextAreaWidget):
@@ -148,17 +137,12 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for use with User
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
- This is the default widget for the
- :class:`~wuttaweb.forms.schema.RoleRefs` type.
This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
"""
- readonly_template = 'readonly/rolerefs'
def serialize(self, field, cstruct, **kw):
""" """
- model = self.app.model
-
# special logic when field is editable
readonly = kw.get('readonly', self.readonly)
if not readonly:
@@ -175,78 +159,10 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
if val[0] != admin.uuid]
kw['values'] = values
- else: # readonly
-
- # roles
- roles = []
- if cstruct:
- for uuid in cstruct:
- role = self.session.query(model.Role).get(uuid)
- if role:
- roles.append(role)
- kw['roles'] = roles
-
- # url
- url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
- kw['url'] = url
-
# default logic from here
return super().serialize(field, cstruct, **kw)
-class UserRefsWidget(WuttaCheckboxChoiceWidget):
- """
- Widget for use with Role
- :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field.
- This is the default widget for the
- :class:`~wuttaweb.forms.schema.UserRefs` type.
-
- This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however
- it only supports readonly mode and does not use a template.
- Rather, it generates and renders a
- :class:`~wuttaweb.grids.base.Grid` showing the users list.
- """
-
- def serialize(self, field, cstruct, **kw):
- """ """
- readonly = kw.get('readonly', self.readonly)
- if not readonly:
- raise NotImplementedError("edit not allowed for this widget")
-
- model = self.app.model
- columns = ['person', 'username', 'active']
-
- # generate data set for users
- users = []
- if cstruct:
- for uuid in cstruct:
- user = self.session.query(model.User).get(uuid)
- if user:
- users.append(dict([(key, getattr(user, key))
- for key in columns + ['uuid']]))
-
- # grid
- grid = Grid(self.request, key='roles.view.users',
- columns=columns, data=users)
-
- # view action
- if self.request.has_perm('users.view'):
- url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid'])
- grid.add_action('view', icon='eye', url=url)
- grid.set_link('person')
- grid.set_link('username')
-
- # edit action
- if self.request.has_perm('users.edit'):
- url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid'])
- grid.add_action('edit', url=url)
-
- # render as simple
- # nb. must indicate we are a part of this form
- form = getattr(field.parent, 'wutta_form', None)
- return grid.render_table_element(form)
-
-
class PermissionsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for use with Role
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index 3e7695c..3f22aad 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -39,7 +39,6 @@ from webhelpers2.html import HTML
from wuttaweb.db import Session
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
-from wuttjamaican.util import UNSPECIFIED
log = logging.getLogger(__name__)
@@ -283,40 +282,6 @@ class Grid:
Only relevant if :attr:`paginated` is true. If not specified,
constructor will assume ``1`` (first page).
-
- .. attribute:: searchable_columns
-
- Set of columns declared as searchable for the Vue component.
-
- See also :meth:`set_searchable()` and :meth:`is_searchable()`.
-
- .. attribute:: filterable
-
- Boolean indicating whether the grid should show a "filters"
- section where user can filter data in various ways. Default is
- ``False``.
-
- .. attribute:: filters
-
- Dict of :class:`~wuttaweb.grids.filters.GridFilter` instances
- available for use with backend filtering.
-
- Only relevant if :attr:`filterable` is true.
-
- See also :meth:`set_filter()`.
-
- .. attribute:: filter_defaults
-
- Dict containing default state preferences for the filters.
-
- See also :meth:`set_filter_defaults()`.
-
- .. attribute:: joiners
-
- Dict of "joiner" functions for use with backend filtering and
- sorting.
-
- See :meth:`set_joiner()` for more info.
"""
def __init__(
@@ -341,11 +306,6 @@ class Grid:
pagesize_options=None,
pagesize=None,
page=1,
- searchable_columns=None,
- filterable=False,
- filters=None,
- filter_defaults=None,
- joiners=None,
):
self.request = request
self.vue_tagname = vue_tagname
@@ -356,7 +316,6 @@ class Grid:
self.renderers = renderers or {}
self.actions = actions or []
self.linked_columns = linked_columns or []
- self.joiners = joiners or {}
self.config = self.request.wutta_config
self.app = self.config.get_app()
@@ -385,19 +344,6 @@ class Grid:
self.pagesize = pagesize or self.get_pagesize()
self.page = page
- # searching
- self.searchable_columns = set(searchable_columns or [])
-
- # filtering
- self.filterable = filterable
- if filters is not None:
- self.filters = filters
- elif self.filterable:
- self.filters = self.make_backend_filters()
- else:
- self.filters = {}
- self.set_filter_defaults(**(filter_defaults or {}))
-
def get_columns(self):
"""
Returns the official list of column names for the grid, or
@@ -486,7 +432,7 @@ class Grid:
if key in self.columns:
self.columns.remove(key)
- def set_label(self, key, label, column_only=False):
+ def set_label(self, key, label):
"""
Set/override the label for a column.
@@ -494,18 +440,11 @@ class Grid:
:param label: New label for the column header.
- :param column_only: Boolean indicating whether the label
- should be applied *only* to the column header (if
- ``True``), vs. applying also to the filter (if ``False``).
-
See also :meth:`get_label()`. Label overrides are tracked via
:attr:`labels`.
"""
self.labels[key] = label
- if not column_only and key in self.filters:
- self.filters[key].label = label
-
def get_label(self, key):
"""
Returns the label text for a given column.
@@ -604,92 +543,6 @@ class Grid:
return True
return False
- def set_searchable(self, key, searchable=True):
- """
- (Un)set the given column's searchable flag for the Vue
- component.
-
- See also :meth:`is_searchable()`. Flags are tracked via
- :attr:`searchable_columns`.
- """
- if searchable:
- self.searchable_columns.add(key)
- elif key in self.searchable_columns:
- self.searchable_columns.remove(key)
-
- def is_searchable(self, key):
- """
- Check if the given column is marked as searchable for the Vue
- component.
-
- See also :meth:`set_searchable()`.
- """
- return key in self.searchable_columns
-
- def add_action(self, key, **kwargs):
- """
- Convenience to add a new :class:`GridAction` instance to the
- grid's :attr:`actions` list.
- """
- self.actions.append(GridAction(self.request, key, **kwargs))
-
- ##############################
- # joining methods
- ##############################
-
- def set_joiner(self, key, joiner):
- """
- Set/override the backend joiner for a column.
-
- A "joiner" is sometimes needed when a column with "related but
- not primary" data is involved in a sort or filter operation.
-
- A sorter or filter may need to "join" other table(s) to get at
- the appropriate data. But if a given column has both a sorter
- and filter defined, and both are used at the same time, we
- don't want the join to happen twice.
-
- Hence we track joiners separately, also keyed by column name
- (as are sorters and filters). When a column's sorter **and/or**
- filter is needed, the joiner will be invoked.
-
- :param key: Name of column.
-
- :param joiner: A joiner callable, as described below.
-
- A joiner callable must accept just one ``(data)`` arg and
- return the "joined" data/query, for example::
-
- model = app.model
- grid = Grid(request, model_class=model.Person)
-
- def join_external_profile_value(query):
- return query.join(model.ExternalProfile)
-
- def sort_external_profile(query, direction):
- sortspec = getattr(model.ExternalProfile.description, direction)
- return query.order_by(sortspec())
-
- grid.set_joiner('external_profile', join_external_profile)
- grid.set_sorter('external_profile', sort_external_profile)
-
- See also :meth:`remove_joiner()`. Backend joiners are tracked
- via :attr:`joiners`.
- """
- self.joiners[key] = joiner
-
- def remove_joiner(self, key):
- """
- Remove the backend joiner for a column.
-
- Note that this removes the joiner *function*, so there is no
- way to apply joins for this column unless another joiner is
- later defined for it.
-
- See also :meth:`set_joiner()`.
- """
- self.joiners.pop(key, None)
-
##############################
# sorting methods
##############################
@@ -991,147 +844,6 @@ class Grid:
return key in self.sorters
return True
- ##############################
- # filtering methods
- ##############################
-
- def make_backend_filters(self, filters=None):
- """
- Make backend filters for all columns in the grid.
-
- This is called by the constructor, if :attr:`filterable` is
- true.
-
- For each column in the grid, this checks the provided
- ``filters`` and if the column is not yet in there, will call
- :meth:`make_filter()` to add it.
-
- .. note::
-
- This only works if grid has a :attr:`model_class`. If not,
- this method just returns the initial filters (or empty
- dict).
-
- :param filters: Optional dict of initial filters. Any
- existing filters will be left intact, not replaced.
-
- :returns: Final dict of all filters. Includes any from the
- initial ``filters`` param as well as any which were
- created.
- """
- filters = filters or {}
-
- if self.model_class:
- for key in self.columns:
- if key in filters:
- continue
- prop = getattr(self.model_class, key, None)
- if (prop and hasattr(prop, 'property')
- and isinstance(prop.property, orm.ColumnProperty)):
- filters[prop.key] = self.make_filter(prop)
-
- return filters
-
- def make_filter(self, columninfo, **kwargs):
- """
- Create and return a :class:`GridFilter` instance suitable for
- use on the given column.
-
- Code usually does not need to call this directly. See also
- :meth:`set_filter()`, which calls this method automatically.
-
- :param columninfo: Can be either a model property (see below),
- or a column name.
-
- :returns: A :class:`GridFilter` instance.
- """
- model_property = None
- if isinstance(columninfo, str):
- key = columninfo
- if self.model_class:
- try:
- mapper = sa.inspect(self.model_class)
- except sa.exc.NoInspectionAvailable:
- pass
- else:
- model_property = mapper.get_property(key)
- if not model_property:
- raise ValueError(f"cannot locate model property for key: {key}")
- else:
- model_property = columninfo
-
- return GridFilter(self.request, model_property, **kwargs)
-
- def set_filter(self, key, filterinfo=None, **kwargs):
- """
- Set/override the backend filter for a column.
-
- Only relevant if :attr:`filterable` is true.
-
- :param key: Name of column.
-
- :param filterinfo: Can be either a
- :class:`~wuttweb.grids.filters.GridFilter` instance, or
- else a model property (see below).
-
- If ``filterinfo`` is a ``GridFilter`` instance, it will be
- used as-is for the backend filter.
-
- Otherwise :meth:`make_filter()` will be called to obtain the
- backend filter. The ``filterinfo`` will be passed along to
- that call; if it is empty then ``key`` will be used instead.
-
- See also :meth:`remove_filter()`. Backend filters are tracked
- via :attr:`filters`.
- """
- filtr = None
-
- if filterinfo and callable(filterinfo):
- # filtr = filterinfo
- raise NotImplementedError
- else:
- kwargs.setdefault('label', self.get_label(key))
- filtr = self.make_filter(filterinfo or key, **kwargs)
-
- self.filters[key] = filtr
-
- def remove_filter(self, key):
- """
- Remove the backend filter for a column.
-
- This removes the filter *instance*, so there is no way to
- filter by this column unless another filter is later defined
- for it.
-
- See also :meth:`set_filter()`.
- """
- self.filters.pop(key, None)
-
- def set_filter_defaults(self, **defaults):
- """
- Set default state preferences for the grid filters.
-
- These preferences will affect the initial grid display, until
- user requests a different filtering method.
-
- Each kwarg should be named by filter key, and the value should
- be a dict of preferences for that filter. For instance::
-
- grid.set_filter_defaults(name={'active': True,
- 'verb': 'contains',
- 'value': 'foo'},
- value={'active': True})
-
- Filter defaults are tracked via :attr:`filter_defaults`.
- """
- filter_defaults = dict(getattr(self, 'filter_defaults', {}))
-
- for key, values in defaults.items():
- filtr = filter_defaults.setdefault(key, {})
- filtr.update(values)
-
- self.filter_defaults = filter_defaults
-
##############################
# paging methods
##############################
@@ -1222,15 +934,6 @@ class Grid:
# initial default settings
settings = {}
- if self.filterable:
- for filtr in self.filters.values():
- defaults = self.filter_defaults.get(filtr.key, {})
- settings[f'filter.{filtr.key}.active'] = defaults.get('active',
- filtr.default_active)
- settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
- filtr.default_verb)
- settings[f'filter.{filtr.key}.value'] = defaults.get('value',
- filtr.default_value)
if self.sortable:
if self.sort_defaults:
# nb. as of writing neither Buefy nor Oruga support a
@@ -1248,27 +951,11 @@ class Grid:
# update settings dict based on what we find in the request
# and/or user session. always prioritize the former.
- # nb. do not read settings if user wants a reset
- if self.request.GET.get('reset-view'):
- # at this point we only have default settings, and we want
- # to keep those *and* persist them for next time, below
- pass
-
- elif self.request_has_settings('filter'):
- self.update_filter_settings(settings, src='request')
- if self.request_has_settings('sort'):
- self.update_sort_settings(settings, src='request')
- else:
- self.update_sort_settings(settings, src='session')
- self.update_page_settings(settings)
-
- elif self.request_has_settings('sort'):
- self.update_filter_settings(settings, src='session')
+ if self.request_has_settings('sort'):
self.update_sort_settings(settings, src='request')
self.update_page_settings(settings)
elif self.request_has_settings('page'):
- self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
self.update_page_settings(settings)
@@ -1277,7 +964,6 @@ class Grid:
persist = False
# but still should load whatever is in user session
- self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
self.update_page_settings(settings)
@@ -1287,13 +973,6 @@ class Grid:
# update ourself to reflect settings dict..
- # filtering
- if self.filterable:
- for filtr in self.filters.values():
- filtr.active = settings[f'filter.{filtr.key}.active']
- filtr.verb = settings[f'filter.{filtr.key}.verb'] or filtr.default_verb
- filtr.value = settings[f'filter.{filtr.key}.value']
-
# sorting
if self.sortable:
# nb. doing this for frontend sorting also
@@ -1318,18 +997,11 @@ class Grid:
def request_has_settings(self, typ):
""" """
- if typ == 'filter' and self.filterable:
- for filtr in self.filters.values():
- if filtr.key in self.request.GET:
- return True
- if 'filter' in self.request.GET: # user may be applying empty filters
- return True
-
- elif typ == 'sort' and self.sortable and self.sort_on_backend:
+ if typ == 'sort':
if 'sort1key' in self.request.GET:
return True
- elif typ == 'page' and self.paginated and self.paginate_on_backend:
+ elif typ == 'page':
for key in ['pagesize', 'page']:
if key in self.request.GET:
return True
@@ -1361,31 +1033,6 @@ class Grid:
# okay then, default it is
return default
- def update_filter_settings(self, settings, src=None):
- """ """
- if not self.filterable:
- return
-
- for filtr in self.filters.values():
- prefix = f'filter.{filtr.key}'
-
- if src == 'request':
- # consider filter active if query string contains a value for it
- settings[f'{prefix}.active'] = filtr.key in self.request.GET
- settings[f'{prefix}.verb'] = self.get_setting(
- settings, f'{filtr.key}.verb', src='request', default='')
- settings[f'{prefix}.value'] = self.get_setting(
- settings, filtr.key, src='request', default='')
-
- elif src == 'session':
- settings[f'{prefix}.active'] = self.get_setting(
- settings, f'{prefix}.active', src='session',
- normalize=lambda v: str(v).lower() == 'true', default=False)
- settings[f'{prefix}.verb'] = self.get_setting(
- settings, f'{prefix}.verb', src='session', default='')
- settings[f'{prefix}.value'] = self.get_setting(
- settings, f'{prefix}.value', src='session', default='')
-
def update_sort_settings(self, settings, src=None):
""" """
if not (self.sortable and self.sort_on_backend):
@@ -1452,18 +1099,8 @@ class Grid:
skey = f'grid.{self.key}.{key}'
self.request.session[skey] = value(key)
- # filter settings
- if self.filterable:
-
- # always save all filters, with status
- for filtr in self.filters.values():
- persist(f'filter.{filtr.key}.active',
- value=lambda k: 'true' if settings.get(k) else 'false')
- persist(f'filter.{filtr.key}.verb')
- persist(f'filter.{filtr.key}.value')
-
# sort settings
- if self.sortable and self.sort_on_backend:
+ if self.sortable:
# first must clear all sort settings from dest. this is
# because number of sort settings will vary, so we delete
@@ -1507,15 +1144,10 @@ class Grid:
See also these methods which may be called by this one:
- * :meth:`filter_data()`
* :meth:`sort_data()`
* :meth:`paginate_data()`
"""
data = self.data or []
- self.joined = set()
-
- if self.filterable:
- data = self.filter_data(data)
if self.sortable and self.sort_on_backend:
data = self.sort_data(data)
@@ -1526,46 +1158,6 @@ class Grid:
return data
- @property
- def active_filters(self):
- """
- Returns the list of currently active filters.
-
- This inspects each :class:`GridFilter` in :attr:`filters` and
- only returns the ones marked active.
- """
- return [filtr for filtr in self.filters.values()
- if filtr.active]
-
- def filter_data(self, data, filters=None):
- """
- Filter the given data and return the result. This is called
- by :meth:`get_visible_data()`.
-
- :param filters: Optional list of filters to use. If not
- specified, the grid's :attr:`active_filters` are used.
- """
- if filters is None:
- filters = self.active_filters
- if not filters:
- return data
-
- for filtr in filters:
- key = filtr.key
-
- if key in self.joiners and key not in self.joined:
- data = self.joiners[key](data)
- self.joined.add(key)
-
- try:
- data = filtr.apply_filter(data)
- except VerbNotSupported as error:
- log.warning("verb not supported for '%s' filter: %s", key, error.verb)
- except:
- log.exception("filtering data by '%s' failed!", key)
-
- return data
-
def sort_data(self, data, sorters=None):
"""
Sort the given data and return the result. This is called by
@@ -1596,11 +1188,6 @@ class Grid:
if not sortfunc:
return data
- # join appropriate model if needed
- if sortkey in self.joiners and sortkey not in self.joined:
- data = self.joiners[sortkey](data)
- self.joined.add(sortkey)
-
# invoke the sorter
data = sortfunc(data, sortdir)
@@ -1642,58 +1229,6 @@ class Grid:
# rendering methods
##############################
- def render_table_element(
- self,
- form=None,
- template='/grids/table_element.mako',
- **context):
- """
- Render a simple Vue table element for the grid.
-
- This is what you want for a "simple" grid which does require a
- unique Vue component, but can instead use the standard table
- component.
-
- This returns something like:
-
- .. code-block:: html
-
-
-
-
-
- See :meth:`render_vue_template()` for a more complete variant.
-
- Actual output will of course depend on grid attributes,
- :attr:`key`, :attr:`columns` etc.
-
- :param form: Reference to the
- :class:`~wuttaweb.forms.base.Form` instance which
- "contains" this grid. This is needed in order to ensure
- the grid data is available to the form Vue component.
-
- :param template: Path to Mako template which is used to render
- the output.
-
- .. note::
-
- The above example shows ``gridData['mykey']`` as the Vue
- data reference. This should "just work" if you provide the
- correct ``form`` arg and the grid is contained directly by
- that form's Vue component.
-
- However, this may not account for all use cases. For now
- we wait and see what comes up, but know the dust may not
- yet be settled here.
- """
-
- # nb. must register data for inclusion on page template
- if form:
- form.add_grid_vue_data(self)
-
- # otherwise logic is the same, just different template
- return self.render_vue_template(template=template, **context)
-
def render_vue_tag(self, **kwargs):
"""
Render the Vue component tag for the grid.
@@ -1716,9 +1251,6 @@ class Grid:
"""
Render the Vue template block for the grid.
- This is what you want for a "full-featured" grid which will
- exist as its own unique Vue component on the frontend.
-
This returns something like:
.. code-block:: none
@@ -1729,21 +1261,12 @@ class Grid:
-
-
.. todo::
Why can't Sphinx render the above code block as 'html' ?
It acts like it can't handle a ``
%def>
diff --git a/src/wuttaweb/templates/auth/login.mako b/src/wuttaweb/templates/auth/login.mako
index 278992b..e6b77c6 100644
--- a/src/wuttaweb/templates/auth/login.mako
+++ b/src/wuttaweb/templates/auth/login.mako
@@ -4,9 +4,13 @@
<%def name="title()">Login%def>
+<%def name="render_this_page()">
+ ${self.page_content()}
+%def>
+
<%def name="page_content()">
-
${base_meta.full_logo(image_url or None)}
+
${base_meta.full_logo()}
${form.render_vue_tag()}
@@ -40,3 +44,6 @@
%def>
+
+
+${parent.body()}
diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako
index 6c57c46..f58f7ec 100644
--- a/src/wuttaweb/templates/base.mako
+++ b/src/wuttaweb/templates/base.mako
@@ -151,33 +151,6 @@
white-space: nowrap;
}
- ##############################
- ## grids
- ##############################
-
- .wutta-filter {
- display: flex;
- gap: 0.5rem;
- }
-
- .wutta-filter .button.filter-toggle {
- justify-content: left;
- }
-
- .wutta-filter .button.filter-toggle,
- .wutta-filter .filter-verb {
- min-width: 15rem;
- }
-
- .wutta-filter .filter-verb .select,
- .wutta-filter .filter-verb .select select {
- width: 100%;
- }
-
- ##############################
- ## forms
- ##############################
-
.wutta-form-wrapper {
margin-left: 5rem;
margin-top: 2rem;
@@ -528,7 +501,7 @@
label="Delete This" />
% endif
% elif master.editing:
- % if master.has_perm('view'):
+ % if instance_viewable and master.has_perm('view'):
% endif
% elif master.deleting:
- % if master.has_perm('view'):
+ % if instance_viewable and master.has_perm('view'):
${app.get_node_title()}%def>
+<%def name="global_title()">${app.get_title()}%def>
<%def name="extra_styles()">%def>
@@ -12,8 +12,8 @@
${h.image(config.get('wuttaweb.header_logo_url', default=request.static_url('wuttaweb:static/img/favicon.ico')), "Header Logo", style="height: 49px;")}
%def>
-<%def name="full_logo(image_url=None)">
- ${h.image(image_url or config.get('wuttaweb.logo_url', default=request.static_url('wuttaweb:static/img/logo.png')), f"{app.get_title()} logo")}
+<%def name="full_logo()">
+ ${h.image(config.get('wuttaweb.logo_url', default=request.static_url('wuttaweb:static/img/logo.png')), f"{app.get_title()} logo")}
%def>
<%def name="footer()">
diff --git a/src/wuttaweb/templates/configure.mako b/src/wuttaweb/templates/configure.mako
index f0363e0..6b7a766 100644
--- a/src/wuttaweb/templates/configure.mako
+++ b/src/wuttaweb/templates/configure.mako
@@ -3,17 +3,6 @@
<%def name="title()">Configure ${config_title}%def>
-<%def name="extra_styles()">
- ${parent.extra_styles()}
-
-%def>
-
<%def name="page_content()">
${self.buttons_content()}
@@ -53,14 +42,15 @@
Cancel
- ${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})}
+ ${h.form(request.current_route_url())}
${h.csrf_token(request)}
${h.hidden('remove_settings', 'true')}
+ icon-left="trash"
+ @click="purgingSettings = true">
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings for ${config_title}" }}
${h.end_form()}
diff --git a/src/wuttaweb/templates/deform/readonly/objectref.pt b/src/wuttaweb/templates/deform/readonly/objectref.pt
index 3ab9e0e..0b941ab 100644
--- a/src/wuttaweb/templates/deform/readonly/objectref.pt
+++ b/src/wuttaweb/templates/deform/readonly/objectref.pt
@@ -1,9 +1 @@
-
-
- ${str(field.schema.model_instance or '')}
-
-
- ${str(field.schema.model_instance or '')}
-
-
+${str(field.schema.model_instance or '')}
diff --git a/src/wuttaweb/templates/deform/readonly/rolerefs.pt b/src/wuttaweb/templates/deform/readonly/rolerefs.pt
deleted file mode 100644
index ba27041..0000000
--- a/src/wuttaweb/templates/deform/readonly/rolerefs.pt
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/src/wuttaweb/templates/forms/vue_template.mako b/src/wuttaweb/templates/forms/vue_template.mako
index e7d3f2b..70540d0 100644
--- a/src/wuttaweb/templates/forms/vue_template.mako
+++ b/src/wuttaweb/templates/forms/vue_template.mako
@@ -11,12 +11,7 @@
% if not form.readonly:
-
-