diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de4999..d8c5635 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.7.0 (2024-08-15) - -### Feat - -- add sane views for 403 Forbidden and 404 Not Found -- add permission checks for menus, view routes -- add first-time setup page to create admin user -- expose User password for editing in master views -- expose Role permissions for editing -- expose User "roles" for editing -- improve widget, rendering for Role notes - -### Fix - -- add stub for `PersonView.make_user()` -- allow arbitrary kwargs for `Form.render_vue_field()` -- make some tweaks for better tailbone compatibility -- prevent delete for built-in roles - ## v0.6.0 (2024-08-13) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 35d94ff..c3ce9d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.7.0" +version = "0.6.0" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -39,7 +39,7 @@ dependencies = [ "pyramid_tm", "waitress", "WebHelpers2", - "WuttJamaican[db]>=0.11.1", + "WuttJamaican[db]>=0.11.0", "zope.sqlalchemy>=1.5", ] diff --git a/src/wuttaweb/app.py b/src/wuttaweb/app.py index 845b41f..bafc921 100644 --- a/src/wuttaweb/app.py +++ b/src/wuttaweb/app.py @@ -135,12 +135,6 @@ def make_pyramid_config(settings): pyramid_config.include('pyramid_mako') pyramid_config.include('pyramid_tm') - # add some permissions magic - pyramid_config.add_directive('add_wutta_permission_group', - 'wuttaweb.auth.add_permission_group') - pyramid_config.add_directive('add_wutta_permission', - 'wuttaweb.auth.add_permission') - return pyramid_config diff --git a/src/wuttaweb/auth.py b/src/wuttaweb/auth.py index 88b1fea..de9b868 100644 --- a/src/wuttaweb/auth.py +++ b/src/wuttaweb/auth.py @@ -148,93 +148,3 @@ class WuttaSecurityPolicy: auth = app.get_auth_handler() user = self.identity(request) return auth.has_permission(self.db_session, user, permission) - - -def add_permission_group(pyramid_config, key, label=None, overwrite=True): - """ - Pyramid directive to add a "permission group" to the app's - awareness. - - The app must be made aware of all permissions, so they are exposed - when editing a - :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic - for discovering permissions is in - :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`. - - This is usually called from within a master view's - :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish - the permission group which applies to the view model. - - A simple example of usage:: - - pyramid_config.add_permission_group('widgets', label="Widgets") - - :param key: Unique key for the permission group. In the context - of a master view, this will be the same as - :attr:`~wuttaweb.views.master.MasterView.permission_prefix`. - - :param label: Optional label for the permission group. If not - specified, it is derived from ``key``. - - :param overwrite: If the permission group was already established, - this flag controls whether the group's label should be - overwritten (with ``label``). - - See also :func:`add_permission()`. - """ - config = pyramid_config.get_settings()['wutta_config'] - app = config.get_app() - def action(): - perms = pyramid_config.get_settings().get('wutta_permissions', {}) - if overwrite or key not in perms: - group = perms.setdefault(key, {'key': key}) - group['label'] = label or app.make_title(key) - pyramid_config.add_settings({'wutta_permissions': perms}) - pyramid_config.action(None, action) - - -def add_permission(pyramid_config, groupkey, key, label=None): - """ - Pyramid directive to add a single "permission" to the app's - awareness. - - The app must be made aware of all permissions, so they are exposed - when editing a - :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic - for discovering permissions is in - :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`. - - This is usually called from within a master view's - :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish - "known" permissions based on master view feature flags - (:attr:`~wuttaweb.views.master.MasterView.viewable`, - :attr:`~wuttaweb.views.master.MasterView.editable`, etc.). - - A simple example of usage:: - - pyramid_config.add_permission('widgets', 'widgets.polish', - label="Polish all the widgets") - - :param key: Unique key for the permission group. In the context - of a master view, this will be the same as - :attr:`~wuttaweb.views.master.MasterView.permission_prefix`. - - :param key: Unique key for the permission. This should be the - "complete" permission name which includes the permission - prefix. - - :param label: Optional label for the permission. If not - specified, it is derived from ``key``. - - See also :func:`add_permission_group()`. - """ - def action(): - config = pyramid_config.get_settings()['wutta_config'] - app = config.get_app() - perms = pyramid_config.get_settings().get('wutta_permissions', {}) - group = perms.setdefault(groupkey, {'key': groupkey}) - group.setdefault('label', app.make_title(groupkey)) - perm = group.setdefault('perms', {}).setdefault(key, {'key': key}) - perm['label'] = label or app.make_title(key) - pyramid_config.add_settings({'wutta_permissions': perms}) - pyramid_config.action(None, action) diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index 8ed17f8..7ee9b01 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -120,13 +120,6 @@ class Form: See also :meth:`set_validator()`. - .. attribute:: defaults - - Dict of default field values, used to construct the form in - :meth:`get_schema()`. - - See also :meth:`set_default()`. - .. attribute:: readonly Boolean indicating the form does not allow submit. In practice @@ -255,7 +248,6 @@ class Form: nodes={}, widgets={}, validators={}, - defaults={}, readonly=False, readonly_fields=[], required_fields={}, @@ -279,7 +271,6 @@ class Form: self.nodes = nodes or {} self.widgets = widgets or {} self.validators = validators or {} - self.defaults = defaults or {} self.readonly = readonly self.readonly_fields = set(readonly_fields or []) self.required_fields = required_fields or {} @@ -384,23 +375,6 @@ class Form: """ self.fields = FieldList(fields) - def append(self, *keys): - """ - Add some fields(s) to the form. - - This is a convenience to allow adding multiple fields at - once:: - - form.append('first_field', - 'second_field', - 'third_field') - - It will add each field to :attr:`fields`. - """ - for key in keys: - if key not in self.fields: - self.fields.append(key) - def remove(self, *keys): """ Remove some fields(s) from the form. @@ -497,18 +471,6 @@ class Form: if self.schema and key in self.schema: self.schema[key].validator = validator - def set_default(self, key, value): - """ - Set/override the default value for a field. - - :param key: Name of field. - - :param validator: Default value for the field. - - Default value overrides are tracked via :attr:`defaults`. - """ - self.defaults[key] = value - def set_readonly(self, key, readonly=True): """ Enable or disable the "readonly" flag for a given field. @@ -662,22 +624,29 @@ class Form: if self.model_class: - # collect list of field names and/or nodes - includes = [] - for key in fields: + # first define full list of 'includes' - final schema + # should contain all of these fields + includes = list(fields) + + # determine which we want ColanderAlchemy to handle + auto_includes = [] + for key in includes: + + # skip if we already have a node defined if key in self.nodes: - includes.append(self.nodes[key]) - else: - includes.append(key) + continue + + # we want the magic for this field + auto_includes.append(key) # make initial schema with ColanderAlchemy magic schema = SQLAlchemySchemaNode(self.model_class, - includes=includes) + includes=auto_includes) - # fill in the blanks if anything got missed - for key in fields: - if key not in schema: - node = colander.SchemaNode(colander.String(), name=key) + # now fill in the blanks for non-magic fields + for key in includes: + if key not in auto_includes: + node = self.nodes[key] schema.add(node) else: @@ -716,11 +685,6 @@ class Form: elif key in schema: # field-level schema[key].validator = validator - # apply default value overrides - for key, value in self.defaults.items(): - if key in schema: - schema[key].default = value - # apply required flags for key, required in self.required_fields.items(): if key in schema: @@ -811,12 +775,7 @@ class Form: output = render(template, context) return HTML.literal(output) - def render_vue_field( - self, - fieldname, - readonly=None, - **kwargs, - ): + def render_vue_field(self, fieldname, readonly=None): """ Render the given field completely, i.e. ```` wrapper with label and containing a widget. @@ -832,12 +791,6 @@ class Form: message="something went wrong!"> - - .. warning:: - - 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: @@ -950,20 +903,6 @@ class Form: return model_data - # TODO: for tailbone compat, should document? - # (ideally should remove this and find a better way) - def get_vue_field_value(self, key): - """ """ - if key not in self.fields: - return - - dform = self.get_deform() - if key not in dform: - return - - field = dform[key] - return make_json_safe(field.cstruct) - def validate(self): """ Try to validate the form, using data from the :attr:`request`. diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 98ce0f1..ccb357f 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -257,101 +257,3 @@ class PersonRef(ObjectRef): def sort_query(self, query): """ """ return query.order_by(self.model_class.full_name) - - -class WuttaSet(colander.Set): - """ - Custom schema type for :class:`python:set` fields. - - This is a subclass of :class:`colander.Set`, but adds - Wutta-related params to the constructor. - - :param request: Current :term:`request` object. - - :param session: Optional :term:`db session` to use instead of - :class:`wuttaweb.db.Session`. - """ - - def __init__(self, request, session=None): - super().__init__() - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - self.session = session or Session() - - -class RoleRefs(WuttaSet): - """ - Form schema type for the User - :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` - association proxy field. - - This is a subclass of :class:`WuttaSet`. It uses a ``set`` of - :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role` ``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.RoleRefsWidget`. - """ - kwargs.setdefault('session', self.session) - - if 'values' not in kwargs: - model = self.app.model - auth = self.app.get_auth_handler() - avoid = { - auth.get_role_authenticated(self.session), - auth.get_role_anonymous(self.session), - } - avoid = set([role.uuid for role in avoid]) - roles = self.session.query(model.Role)\ - .filter(~model.Role.uuid.in_(avoid))\ - .order_by(model.Role.name)\ - .all() - values = [(role.uuid, role.name) for role in roles] - kwargs['values'] = values - - return widgets.RoleRefsWidget(self.request, **kwargs) - - -class Permissions(WuttaSet): - """ - Form schema type for the Role - :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions` - association proxy field. - - This is a subclass of :class:`WuttaSet`. It uses a ``set`` of - :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Permission.permission` - values for underlying data format. - - :param permissions: Dict with all possible permissions. Should be - in the same format as returned by - :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`. - """ - - def __init__(self, request, permissions, *args, **kwargs): - super().__init__(request, *args, **kwargs) - self.permissions = permissions - - def widget_maker(self, **kwargs): - """ - Constructs a default widget for the field. - - :returns: Instance of - :class:`~wuttaweb.forms.widgets.PermissionsWidget`. - """ - kwargs.setdefault('session', self.session) - kwargs.setdefault('permissions', self.permissions) - - if 'values' not in kwargs: - values = [] - for gkey, group in self.permissions.items(): - for pkey, perm in group['perms'].items(): - values.append((pkey, perm['label'])) - kwargs['values'] = values - - return widgets.PermissionsWidget(self.request, **kwargs) diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index b4d8254..6627375 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -30,21 +30,12 @@ in the namespace: * :class:`deform:deform.widget.Widget` (base class) * :class:`deform:deform.widget.TextInputWidget` -* :class:`deform:deform.widget.TextAreaWidget` -* :class:`deform:deform.widget.PasswordWidget` -* :class:`deform:deform.widget.CheckedPasswordWidget` * :class:`deform:deform.widget.SelectWidget` -* :class:`deform:deform.widget.CheckboxChoiceWidget` """ -import colander -from deform.widget import (Widget, TextInputWidget, TextAreaWidget, - PasswordWidget, CheckedPasswordWidget, - SelectWidget, CheckboxChoiceWidget) +from deform.widget import Widget, TextInputWidget, SelectWidget from webhelpers2.html import HTML -from wuttaweb.db import Session - class ObjectRefWidget(SelectWidget): """ @@ -57,18 +48,6 @@ class ObjectRefWidget(SelectWidget): the form schema; via :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`. - In readonly mode, this renders a ```` tag around the - :attr:`model_instance` (converted to string). - - Otherwise it renders a select (dropdown) element allowing user to - choose from available records. - - This is a subclass of :class:`deform:deform.widget.SelectWidget` - and uses these Deform templates: - - * ``select`` - * ``readonly/objectref`` - .. attribute:: model_instance Reference to the model record instance, i.e. the "far side" of @@ -81,112 +60,23 @@ class ObjectRefWidget(SelectWidget): when the :class:`~wuttaweb.forms.schema.ObjectRef` type instance (associated with the node) is serialized. """ - readonly_template = 'readonly/objectref' def __init__(self, request, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request - -class NotesWidget(TextAreaWidget): - """ - Widget for use with "notes" fields. - - In readonly mode, this shows the notes with a background to make - them stand out a bit more. - - Otherwise it effectively shows a ``