diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fc273..ba15691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,24 +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.3.0 (2024-08-05) - -### Feat - -- add support for admin user to become / stop being root -- add view to change current user password -- add basic logo, favicon images -- add auth views, for login/logout -- add custom security policy, login/logout for pyramid -- add `wuttaweb.views.essential` module -- add initial/basic forms support -- add `wuttaweb.db` module, with `Session` -- add `util.get_form_data()` convenience function - -### Fix - -- allow custom user getter for `new_request_set_user()` hook - ## v0.2.0 (2024-07-14) ### Feat diff --git a/docs/api/wuttaweb/auth.rst b/docs/api/wuttaweb/auth.rst deleted file mode 100644 index d645c67..0000000 --- a/docs/api/wuttaweb/auth.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.auth`` -================= - -.. automodule:: wuttaweb.auth - :members: diff --git a/docs/api/wuttaweb/db.rst b/docs/api/wuttaweb/db.rst deleted file mode 100644 index b90e227..0000000 --- a/docs/api/wuttaweb/db.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.db`` -=============== - -.. automodule:: wuttaweb.db - :members: diff --git a/docs/api/wuttaweb/forms.base.rst b/docs/api/wuttaweb/forms.base.rst deleted file mode 100644 index 8569309..0000000 --- a/docs/api/wuttaweb/forms.base.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.forms.base`` -======================= - -.. automodule:: wuttaweb.forms.base - :members: diff --git a/docs/api/wuttaweb/forms.rst b/docs/api/wuttaweb/forms.rst deleted file mode 100644 index 1d83240..0000000 --- a/docs/api/wuttaweb/forms.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.forms`` -================== - -.. automodule:: wuttaweb.forms - :members: diff --git a/docs/api/wuttaweb/index.rst b/docs/api/wuttaweb/index.rst index 204864e..2e49d4b 100644 --- a/docs/api/wuttaweb/index.rst +++ b/docs/api/wuttaweb/index.rst @@ -8,10 +8,6 @@ :maxdepth: 1 app - auth - db - forms - forms.base handler helpers menus @@ -19,7 +15,5 @@ subscribers util views - views.auth views.base views.common - views.essential diff --git a/docs/api/wuttaweb/views.auth.rst b/docs/api/wuttaweb/views.auth.rst deleted file mode 100644 index 9a03e3e..0000000 --- a/docs/api/wuttaweb/views.auth.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.views.auth`` -======================= - -.. automodule:: wuttaweb.views.auth - :members: diff --git a/docs/api/wuttaweb/views.essential.rst b/docs/api/wuttaweb/views.essential.rst deleted file mode 100644 index 79c0b57..0000000 --- a/docs/api/wuttaweb/views.essential.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.views.essential`` -============================ - -.. automodule:: wuttaweb.views.essential - :members: diff --git a/docs/conf.py b/docs/conf.py index 3d568ef..3955a96 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,15 +20,12 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', - 'sphinx.ext.todo', ] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] intersphinx_mapping = { - 'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None), - 'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None), 'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None), 'python': ('https://docs.python.org/3/', None), 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), diff --git a/pyproject.toml b/pyproject.toml index ce0044d..985bef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.3.0" +version = "0.2.0" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -32,13 +32,10 @@ requires-python = ">= 3.8" dependencies = [ "pyramid>=2", "pyramid_beaker", - "pyramid_deform", "pyramid_mako", - "pyramid_tm", "waitress", "WebHelpers2", "WuttJamaican[db]>=0.7.0", - "zope.sqlalchemy>=1.5", ] diff --git a/src/wuttaweb/app.py b/src/wuttaweb/app.py index 6aadc0c..18b07fb 100644 --- a/src/wuttaweb/app.py +++ b/src/wuttaweb/app.py @@ -31,9 +31,6 @@ from wuttjamaican.conf import make_config from pyramid.config import Configurator -import wuttaweb.db -from wuttaweb.auth import WuttaSecurityPolicy - class WebAppProvider(AppProvider): """ @@ -86,21 +83,17 @@ def make_wutta_config(settings): If this config file path cannot be discovered, an error is raised. """ - # validate config file path + # initialize config and embed in settings dict, to make + # available for web requests later path = settings.get('wutta.config') if not path or not os.path.exists(path): raise ValueError("Please set 'wutta.config' in [app:main] " "section of config to the path of your " "config file. Lame, but necessary.") - # make config per usual, add to settings wutta_config = make_config(path) + settings['wutta_config'] = wutta_config - - # configure database sessions - if hasattr(wutta_config, 'appdb_engine'): - wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine) - return wutta_config @@ -111,18 +104,10 @@ def make_pyramid_config(settings): The config is initialized with certain features deemed useful for all apps. """ - settings.setdefault('pyramid_deform.template_search_path', - 'wuttaweb:templates/deform') - pyramid_config = Configurator(settings=settings) - # configure user authorization / authentication - pyramid_config.set_security_policy(WuttaSecurityPolicy()) - pyramid_config.include('pyramid_beaker') - pyramid_config.include('pyramid_deform') pyramid_config.include('pyramid_mako') - pyramid_config.include('pyramid_tm') return pyramid_config diff --git a/src/wuttaweb/auth.py b/src/wuttaweb/auth.py deleted file mode 100644 index de9b868..0000000 --- a/src/wuttaweb/auth.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Auth Utility Logic -""" - -import re - -from pyramid.authentication import SessionAuthenticationHelper -from pyramid.request import RequestLocalCache -from pyramid.security import remember, forget - -from wuttaweb.db import Session - - -def login_user(request, user): - """ - Perform the steps necessary to "login" the given user. This - returns a ``headers`` dict which you should pass to the final - redirect, like so:: - - from pyramid.httpexceptions import HTTPFound - - headers = login_user(request, user) - return HTTPFound(location='/', headers=headers) - - .. warning:: - - This logic does not "authenticate" the user! It assumes caller - has already authenticated the user and they are safe to login. - - See also :func:`logout_user()`. - """ - headers = remember(request, user.uuid) - return headers - - -def logout_user(request): - """ - Perform the logout action for the given request. This returns a - ``headers`` dict which you should pass to the final redirect, like - so:: - - from pyramid.httpexceptions import HTTPFound - - headers = logout_user(request) - return HTTPFound(location='/', headers=headers) - - See also :func:`login_user()`. - """ - request.session.delete() - request.session.invalidate() - headers = forget(request) - return headers - - -class WuttaSecurityPolicy: - """ - Pyramid :term:`security policy` for WuttaWeb. - - For more on the Pyramid details, see :doc:`pyramid:narr/security`. - - But the idea here is that you should be able to just use this, - without thinking too hard:: - - from pyramid.config import Configurator - from wuttaweb.auth import WuttaSecurityPolicy - - pyramid_config = Configurator() - pyramid_config.set_security_policy(WuttaSecurityPolicy()) - - This security policy will then do the following: - - * use the request "web session" for auth storage (e.g. current - ``user.uuid``) - * check permissions as needed, by calling - :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()` - for current user - - :param db_session: Optional :term:`db session` to use, instead of - :class:`wuttaweb.db.Session`. Probably only useful for tests. - """ - - def __init__(self, db_session=None): - self.session_helper = SessionAuthenticationHelper() - self.identity_cache = RequestLocalCache(self.load_identity) - self.db_session = db_session or Session() - - def load_identity(self, request): - config = request.registry.settings['wutta_config'] - app = config.get_app() - model = app.model - - # fetch user uuid from current session - uuid = self.session_helper.authenticated_userid(request) - if not uuid: - return - - # fetch user object from db - user = self.db_session.get(model.User, uuid) - if not user: - return - - return user - - def identity(self, request): - return self.identity_cache.get_or_create(request) - - def authenticated_userid(self, request): - user = self.identity(request) - if user is not None: - return user.uuid - - def remember(self, request, userid, **kw): - return self.session_helper.remember(request, userid, **kw) - - def forget(self, request, **kw): - return self.session_helper.forget(request, **kw) - - def permits(self, request, context, permission): - - # nb. root user can do anything - if getattr(request, 'is_root', False): - return True - - config = request.registry.settings['wutta_config'] - app = config.get_app() - auth = app.get_auth_handler() - user = self.identity(request) - return auth.has_permission(self.db_session, user, permission) diff --git a/src/wuttaweb/db.py b/src/wuttaweb/db.py deleted file mode 100644 index 32d2418..0000000 --- a/src/wuttaweb/db.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Database sessions for web app - -The web app uses a different database session than other -(e.g. console) apps. The web session is "registered" to the HTTP -request/response life cycle (aka. transaction) such that the session -is automatically rolled back on error, and automatically committed if -the response is finalized without error. - -.. class:: Session - - Primary database session class for the web app. - - Note that you often do not need to "instantiate" this session, and - can instead call methods directly on the class:: - - from wuttaweb.db import Session - - users = Session.query(model.User).all() - - However in certain cases you may still want/need to instantiate it, - e.g. when passing a "true/normal" session to other logic. But you - can always call instance methods as well:: - - from wuttaweb.db import Session - from some_place import some_func - - session = Session() - - # nb. assuming func does not expect a "web" session per se, pass instance - some_func(session) - - # nb. these behave the same (instance vs. class method) - users = session.query(model.User).all() - users = Session.query(model.User).all() -""" - -from sqlalchemy import orm -from zope.sqlalchemy.datamanager import register - - -Session = orm.scoped_session(orm.sessionmaker()) - -register(Session) diff --git a/src/wuttaweb/forms/__init__.py b/src/wuttaweb/forms/__init__.py deleted file mode 100644 index 35102be..0000000 --- a/src/wuttaweb/forms/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Forms Library - -The ``wuttaweb.forms`` namespace contains the following: - -* :class:`~wuttaweb.forms.base.Form` -""" - -from .base import Form diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py deleted file mode 100644 index 0974a50..0000000 --- a/src/wuttaweb/forms/base.py +++ /dev/null @@ -1,476 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Base form classes -""" - -import json -import logging - -import colander -import deform -from pyramid.renderers import render -from webhelpers2.html import HTML - -from wuttaweb.util import get_form_data - - -log = logging.getLogger(__name__) - - -class FieldList(list): - """ - Convenience wrapper for a form's field list. This is a subclass - of :class:`python:list`. - - You normally would not need to instantiate this yourself, but it - is used under the hood for e.g. :attr:`Form.fields`. - """ - - def insert_before(self, field, newfield): - """ - Insert a new field, before an existing field. - - :param field: String name for the existing field. - - :param newfield: String name for the new field, to be inserted - just before the existing ``field``. - """ - if field in self: - i = self.index(field) - self.insert(i, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - def insert_after(self, field, newfield): - """ - Insert a new field, after an existing field. - - :param field: String name for the existing field. - - :param newfield: String name for the new field, to be inserted - just after the existing ``field``. - """ - if field in self: - i = self.index(field) - self.insert(i + 1, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - -class Form: - """ - Base class for all forms. - - :param request: Reference to current :term:`request` object. - - :param fields: List of field names for the form. This is - optional; if not specified an attempt will be made to deduce - the list automatically. See also :attr:`fields`. - - :param schema: Colander-based schema object for the form. This is - optional; if not specified an attempt will be made to construct - one automatically. See also :meth:`get_schema()`. - - :param labels: Optional dict of default field labels. - - .. note:: - - Some parameters are not explicitly described above. However - their corresponding attributes are described below. - - Form instances contain the following attributes: - - .. attribute:: fields - - :class:`FieldList` instance containing string field names for - the form. By default, fields will appear in the same order as - they are in this list. - - .. attribute:: request - - Reference to current :term:`request` object. - - .. attribute:: action_url - - String URL to which the form should be submitted, if applicable. - - .. attribute:: vue_tagname - - String name for Vue component tag. By default this is - ``'wutta-form'``. See also :meth:`render_vue_tag()`. - - .. attribute:: align_buttons_right - - Flag indicating whether the buttons (submit, cancel etc.) - should be aligned to the right of the area below the form. If - not set, the buttons are left-aligned. - - .. attribute:: auto_disable_submit - - Flag indicating whether the submit button should be - auto-disabled, whenever the form is submitted. - - .. attribute:: button_label_submit - - String label for the form submit button. Default is ``"Save"``. - - .. attribute:: button_icon_submit - - String icon name for the form submit button. Default is ``'save'``. - - .. attribute:: show_button_reset - - Flag indicating whether a Reset button should be shown. - - .. attribute:: validated - - If the :meth:`validate()` method was called, and it succeeded, - this will be set to the validated data dict. - - Note that in all other cases, this attribute may not exist. - """ - - def __init__( - self, - request, - fields=None, - schema=None, - labels={}, - action_url=None, - vue_tagname='wutta-form', - align_buttons_right=False, - auto_disable_submit=True, - button_label_submit="Save", - button_icon_submit='save', - show_button_reset=False, - ): - self.request = request - self.schema = schema - self.labels = labels or {} - self.action_url = action_url - self.vue_tagname = vue_tagname - self.align_buttons_right = align_buttons_right - self.auto_disable_submit = auto_disable_submit - self.button_label_submit = button_label_submit - self.button_icon_submit = button_icon_submit - self.show_button_reset = show_button_reset - - self.config = self.request.wutta_config - self.app = self.config.get_app() - - if fields is not None: - self.set_fields(fields) - elif self.schema: - self.set_fields([f.name for f in self.schema]) - else: - self.fields = None - - def __contains__(self, name): - """ - Custom logic for the ``in`` operator, to allow easily checking - if the form contains a given field:: - - myform = Form() - if 'somefield' in myform: - print("my form has some field") - """ - return bool(self.fields and name in self.fields) - - def __iter__(self): - """ - Custom logic to allow iterating over form field names:: - - myform = Form(fields=['foo', 'bar']) - for fieldname in myform: - print(fieldname) - """ - return iter(self.fields) - - @property - def vue_component(self): - """ - String name for the Vue component, e.g. ``'WuttaForm'``. - - This is a generated value based on :attr:`vue_tagname`. - """ - words = self.vue_tagname.split('-') - return ''.join([word.capitalize() for word in words]) - - def set_fields(self, fields): - """ - Explicitly set the list of form fields. - - This will overwrite :attr:`fields` with a new - :class:`FieldList` instance. - - :param fields: List of string field names. - """ - self.fields = FieldList(fields) - - def set_label(self, key, label): - """ - Set the label for given field name. - - See also :meth:`get_label()`. - """ - self.labels[key] = label - - # update schema if necessary - if self.schema and key in self.schema: - self.schema[key].title = label - - def get_label(self, key): - """ - Get the label for given field name. - - Note that this will always return a string, auto-generating - the label if needed. - - See also :meth:`set_label()`. - """ - return self.labels.get(key, self.app.make_title(key)) - - def get_schema(self): - """ - Return the :class:`colander:colander.Schema` object for the - form, generating it automatically if necessary. - """ - if not self.schema: - raise NotImplementedError - - return self.schema - - def get_deform(self): - """ - Return the :class:`deform:deform.Form` instance for the form, - generating it automatically if necessary. - """ - if not hasattr(self, 'deform_form'): - schema = self.get_schema() - form = deform.Form(schema) - self.deform_form = form - - return self.deform_form - - def render_vue_tag(self, **kwargs): - """ - Render the Vue component tag for the form. - - By default this simply returns: - - .. code-block:: html - - - - The actual output will depend on various form attributes, in - particular :attr:`vue_tagname`. - """ - return HTML.tag(self.vue_tagname, **kwargs) - - def render_vue_template( - self, - template='/forms/vue_template.mako', - **context): - """ - Render the Vue template block for the form. - - This returns something like: - - .. code-block:: none - - - - .. todo:: - - Why can't Sphinx render the above code block as 'html' ? - - It acts like it can't handle a `` - - - -${parent.body()} diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako index b04c980..5511195 100644 --- a/src/wuttaweb/templates/base.mako +++ b/src/wuttaweb/templates/base.mako @@ -151,11 +151,9 @@