-
${base_meta.full_logo()}
-
Welcome to ${app.get_title()}
+
+
+ ## ${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
+
Welcome to ${base_meta.app_title()}
+
%def>
diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py
index a8d059f..3275301 100644
--- a/src/wuttaweb/util.py
+++ b/src/wuttaweb/util.py
@@ -21,33 +21,12 @@
#
################################################################################
"""
-Web Utilities
+Utilities
"""
import importlib
-def get_form_data(request):
- """
- Returns the effective form data for the given request.
-
- Mostly this is a convenience, which simply returns one of the
- following, depending on various attributes of the request.
-
- * :attr:`pyramid:pyramid.request.Request.POST`
- * :attr:`pyramid:pyramid.request.Request.json_body`
- """
- # nb. we prefer JSON only if no POST is present
- # TODO: this seems to work for our use case at least, but perhaps
- # there is a better way? see also
- # https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr
- if not request.POST and (
- getattr(request, 'is_xhr', False)
- or getattr(request, 'content_type', None) == 'application/json'):
- return request.json_body
- return request.POST
-
-
def get_libver(
request,
key,
diff --git a/src/wuttaweb/views/__init__.py b/src/wuttaweb/views/__init__.py
index 68fdd77..0b62a83 100644
--- a/src/wuttaweb/views/__init__.py
+++ b/src/wuttaweb/views/__init__.py
@@ -33,4 +33,4 @@ from .base import View
def includeme(config):
- config.include('wuttaweb.views.essential')
+ config.include('wuttaweb.views.common')
diff --git a/src/wuttaweb/views/auth.py b/src/wuttaweb/views/auth.py
deleted file mode 100644
index 389271b..0000000
--- a/src/wuttaweb/views/auth.py
+++ /dev/null
@@ -1,288 +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 Views
-"""
-
-import colander
-from deform.widget import TextInputWidget, PasswordWidget, CheckedPasswordWidget
-
-from wuttaweb.views import View
-from wuttaweb.db import Session
-from wuttaweb.auth import login_user, logout_user
-
-
-class AuthView(View):
- """
- Auth views shared by all apps.
- """
-
- def login(self, session=None):
- """
- View for user login.
-
- This view shows the login form, and handles its submission.
- Upon successful login, user is redirected to home page.
-
- * route: ``login``
- * template: ``/auth/login.mako``
- """
- auth = self.app.get_auth_handler()
-
- # TODO: should call request.get_referrer()
- referrer = self.request.route_url('home')
-
- # redirect if already logged in
- if self.request.user:
- self.request.session.flash(f"{self.request.user} is already logged in", 'error')
- return self.redirect(referrer)
-
- form = self.make_form(schema=self.login_make_schema(),
- align_buttons_right=True,
- show_button_reset=True,
- button_label_submit="Login",
- button_icon_submit='user')
-
- # TODO
- # form.show_cancel = False
-
- # validate basic form data (sanity check)
- data = form.validate()
- if data:
-
- # truly validate user credentials
- session = session or Session()
- user = auth.authenticate_user(session, data['username'], data['password'])
- if user:
-
- # okay now they're truly logged in
- headers = login_user(self.request, user)
- return self.redirect(referrer, headers=headers)
-
- else:
- self.request.session.flash("Invalid user credentials", 'error')
-
- return {
- 'index_title': self.app.get_title(),
- 'form': form,
- # TODO
- # 'referrer': referrer,
- }
-
- def login_make_schema(self):
- schema = colander.Schema()
-
- # nb. we must explicitly declare the widgets in order to also
- # specify the ref attribute. this is needed for autofocus and
- # keydown behavior for login form.
-
- schema.add(colander.SchemaNode(
- colander.String(),
- name='username',
- widget=TextInputWidget(attributes={
- 'ref': 'username',
- })))
-
- schema.add(colander.SchemaNode(
- colander.String(),
- name='password',
- widget=PasswordWidget(attributes={
- 'ref': 'password',
- })))
-
- return schema
-
- def logout(self):
- """
- View for user logout.
-
- This deletes/invalidates the current user session and then
- redirects to the login page.
-
- Note that a simple GET is sufficient; POST is not required.
-
- * route: ``logout``
- * template: n/a
- """
- # truly logout the user
- headers = logout_user(self.request)
-
- # TODO
- # # redirect to home page after logout, if so configured
- # if self.config.get_bool('wuttaweb.home_after_logout', default=False):
- # return self.redirect(self.request.route_url('home'), headers=headers)
-
- # otherwise redirect to referrer, with 'login' page as fallback
- # TODO: should call request.get_referrer()
- # referrer = self.request.get_referrer(default=self.request.route_url('login'))
- referrer = self.request.route_url('login')
- return self.redirect(referrer, headers=headers)
-
- def change_password(self):
- """
- View allowing a user to change their own password.
-
- This view shows a change-password form, and handles its
- submission. If successful, user is redirected to home page.
-
- If current user is not authenticated, no form is shown and
- user is redirected to home page.
-
- * route: ``change_password``
- * template: ``/auth/change_password.mako``
- """
- if not self.request.user:
- return self.redirect(self.request.route_url('home'))
-
- form = self.make_form(schema=self.change_password_make_schema(),
- show_button_reset=True)
-
- data = form.validate()
- if data:
- auth = self.app.get_auth_handler()
- auth.set_user_password(self.request.user, data['new_password'])
- self.request.session.flash("Your password has been changed.")
- # TODO: should use request.get_referrer() instead
- referrer = self.request.route_url('home')
- return self.redirect(referrer)
-
- return {'index_title': str(self.request.user),
- 'form': form}
-
- def change_password_make_schema(self):
- schema = colander.Schema()
-
- schema.add(colander.SchemaNode(
- colander.String(),
- name='current_password',
- widget=PasswordWidget(),
- validator=self.change_password_validate_current_password))
-
- schema.add(colander.SchemaNode(
- colander.String(),
- name='new_password',
- widget=CheckedPasswordWidget(),
- validator=self.change_password_validate_new_password))
-
- return schema
-
- def change_password_validate_current_password(self, node, value):
- auth = self.app.get_auth_handler()
- user = self.request.user
- if not auth.check_user_password(user, value):
- node.raise_invalid("Current password is incorrect.")
-
- def change_password_validate_new_password(self, node, value):
- auth = self.app.get_auth_handler()
- user = self.request.user
- if auth.check_user_password(user, value):
- node.raise_invalid("New password must be different from old password.")
-
- def become_root(self):
- """
- Elevate the current request to 'root' for full system access.
-
- This is only allowed if current (authenticated) user is a
- member of the Administrator role. Also note that GET is not
- allowed for this view, only POST.
-
- See also :meth:`stop_root()`.
- """
- if self.request.method != 'POST':
- raise self.forbidden()
-
- if not self.request.is_admin:
- raise self.forbidden()
-
- self.request.session['is_root'] = True
- self.request.session.flash("You have been elevated to 'root' and now have full system access")
-
- url = self.request.get_referrer()
- return self.redirect(url)
-
- def stop_root(self):
- """
- Lower the current request from 'root' back to normal access.
-
- Also note that GET is not allowed for this view, only POST.
-
- See also :meth:`become_root()`.
- """
- if self.request.method != 'POST':
- raise self.forbidden()
-
- if not self.request.is_admin:
- raise self.forbidden()
-
- self.request.session['is_root'] = False
- self.request.session.flash("Your normal system access has been restored")
-
- url = self.request.get_referrer()
- return self.redirect(url)
-
- @classmethod
- def defaults(cls, config):
- cls._auth_defaults(config)
-
- @classmethod
- def _auth_defaults(cls, config):
-
- # login
- config.add_route('login', '/login')
- config.add_view(cls, attr='login',
- route_name='login',
- renderer='/auth/login.mako')
-
- # logout
- config.add_route('logout', '/logout')
- config.add_view(cls, attr='logout',
- route_name='logout')
-
- # change password
- config.add_route('change_password', '/change-password')
- config.add_view(cls, attr='change_password',
- route_name='change_password',
- renderer='/auth/change_password.mako')
-
- # become root
- config.add_route('become_root', '/root/yes',
- request_method='POST')
- config.add_view(cls, attr='become_root',
- route_name='become_root')
-
- # stop root
- config.add_route('stop_root', '/root/no',
- request_method='POST')
- config.add_view(cls, attr='stop_root',
- route_name='stop_root')
-
-
-def defaults(config, **kwargs):
- base = globals()
-
- AuthView = kwargs.get('AuthView', base['AuthView'])
- AuthView.defaults(config)
-
-
-def includeme(config):
- defaults(config)
diff --git a/src/wuttaweb/views/base.py b/src/wuttaweb/views/base.py
index e412ed2..3906c0b 100644
--- a/src/wuttaweb/views/base.py
+++ b/src/wuttaweb/views/base.py
@@ -24,10 +24,6 @@
Base Logic for Views
"""
-from pyramid import httpexceptions
-
-from wuttaweb import forms
-
class View:
"""
@@ -39,7 +35,8 @@ class View:
.. attribute:: request
- Reference to the current :term:`request` object.
+ Reference to the current
+ :class:`pyramid:pyramid.request.Request` object.
.. attribute:: app
@@ -54,38 +51,3 @@ class View:
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
-
- def forbidden(self):
- """
- Convenience method, to raise a HTTP 403 Forbidden exception::
-
- raise self.forbidden()
- """
- return httpexceptions.HTTPForbidden()
-
- def make_form(self, **kwargs):
- """
- Make and return a new :class:`~wuttaweb.forms.base.Form`
- instance, per the given ``kwargs``.
-
- This is the "default" form factory which merely invokes
- the constructor.
- """
- return forms.Form(self.request, **kwargs)
-
- def redirect(self, url, **kwargs):
- """
- Convenience method to return a HTTP 302 response.
-
- Note that this technically returns an "exception" - so in
- your code, you can either return that error, or raise it::
-
- return self.redirect('/')
- # ..or
- raise self.redirect('/')
-
- Which you should do will depend on context, but raising the
- error is always "safe" since Pyramid will handle that
- correctly no matter what.
- """
- return httpexceptions.HTTPFound(location=url, **kwargs)
diff --git a/src/wuttaweb/views/essential.py b/src/wuttaweb/views/essential.py
deleted file mode 100644
index 93c8149..0000000
--- a/src/wuttaweb/views/essential.py
+++ /dev/null
@@ -1,45 +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
.
-#
-################################################################################
-"""
-Essential views for convenient includes
-
-Most apps should include this module::
-
- pyramid_config.include('wuttaweb.views.essential')
-
-That will in turn include the following modules:
-
-* :mod:`wuttaweb.views.auth`
-* :mod:`wuttaweb.views.common`
-"""
-
-
-def defaults(config, **kwargs):
- mod = lambda spec: kwargs.get(spec, spec)
-
- config.include(mod('wuttaweb.views.auth'))
- config.include(mod('wuttaweb.views.common'))
-
-
-def includeme(config):
- defaults(config)
diff --git a/tests/forms/__init__.py b/tests/forms/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py
deleted file mode 100644
index 27e2109..0000000
--- a/tests/forms/test_base.py
+++ /dev/null
@@ -1,271 +0,0 @@
-# -*- coding: utf-8; -*-
-
-from unittest import TestCase
-from unittest.mock import MagicMock
-
-import colander
-import deform
-from pyramid import testing
-
-from wuttjamaican.conf import WuttaConfig
-from wuttaweb.forms import base
-from wuttaweb import helpers
-
-
-class TestFieldList(TestCase):
-
- def test_insert_before(self):
- fields = base.FieldList(['f1', 'f2'])
- self.assertEqual(fields, ['f1', 'f2'])
-
- # typical
- fields.insert_before('f1', 'XXX')
- self.assertEqual(fields, ['XXX', 'f1', 'f2'])
- fields.insert_before('f2', 'YYY')
- self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2'])
-
- # appends new field if reference field is invalid
- fields.insert_before('f3', 'ZZZ')
- self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2', 'ZZZ'])
-
- def test_insert_after(self):
- fields = base.FieldList(['f1', 'f2'])
- self.assertEqual(fields, ['f1', 'f2'])
-
- # typical
- fields.insert_after('f1', 'XXX')
- self.assertEqual(fields, ['f1', 'XXX', 'f2'])
- fields.insert_after('XXX', 'YYY')
- self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2'])
-
- # appends new field if reference field is invalid
- fields.insert_after('f3', 'ZZZ')
- self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2', 'ZZZ'])
-
-
-class TestForm(TestCase):
-
- def setUp(self):
- self.config = WuttaConfig()
- self.request = testing.DummyRequest(wutta_config=self.config)
-
- self.pyramid_config = testing.setUp(request=self.request, settings={
- 'mako.directories': ['wuttaweb:templates'],
- 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
- })
-
- def tearDown(self):
- testing.tearDown()
-
- def make_form(self, request=None, **kwargs):
- return base.Form(request or self.request, **kwargs)
-
- def make_schema(self):
- schema = colander.Schema(children=[
- colander.SchemaNode(colander.String(),
- name='foo'),
- colander.SchemaNode(colander.String(),
- name='bar'),
- ])
- return schema
-
- def test_init_with_none(self):
- form = self.make_form()
- self.assertIsNone(form.fields)
-
- def test_init_with_fields(self):
- form = self.make_form(fields=['foo', 'bar'])
- self.assertEqual(form.fields, ['foo', 'bar'])
-
- def test_init_with_schema(self):
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- self.assertEqual(form.fields, ['foo', 'bar'])
-
- def test_vue_tagname(self):
- form = self.make_form()
- self.assertEqual(form.vue_tagname, 'wutta-form')
-
- def test_vue_component(self):
- form = self.make_form()
- self.assertEqual(form.vue_component, 'WuttaForm')
-
- def test_contains(self):
- form = self.make_form(fields=['foo', 'bar'])
- self.assertIn('foo', form)
- self.assertNotIn('baz', form)
-
- def test_iter(self):
- form = self.make_form(fields=['foo', 'bar'])
-
- fields = list(iter(form))
- self.assertEqual(fields, ['foo', 'bar'])
-
- fields = []
- for field in form:
- fields.append(field)
- self.assertEqual(fields, ['foo', 'bar'])
-
- def test_set_fields(self):
- form = self.make_form(fields=['foo', 'bar'])
- self.assertEqual(form.fields, ['foo', 'bar'])
- form.set_fields(['baz'])
- self.assertEqual(form.fields, ['baz'])
-
- def test_get_schema(self):
- form = self.make_form()
- self.assertIsNone(form.schema)
-
- # provided schema is returned
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- self.assertIs(form.schema, schema)
- self.assertIs(form.get_schema(), schema)
-
- # auto-generating schema not yet supported
- form = self.make_form(fields=['foo', 'bar'])
- self.assertIsNone(form.schema)
- self.assertRaises(NotImplementedError, form.get_schema)
-
- def test_get_deform(self):
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- self.assertFalse(hasattr(form, 'deform_form'))
- dform = form.get_deform()
- self.assertIsInstance(dform, deform.Form)
- self.assertIs(form.deform_form, dform)
-
- def test_get_label(self):
- form = self.make_form(fields=['foo', 'bar'])
- self.assertEqual(form.get_label('foo'), "Foo")
- form.set_label('foo', "Baz")
- self.assertEqual(form.get_label('foo'), "Baz")
-
- def test_set_label(self):
- form = self.make_form(fields=['foo', 'bar'])
- self.assertEqual(form.get_label('foo'), "Foo")
- form.set_label('foo', "Baz")
- self.assertEqual(form.get_label('foo'), "Baz")
-
- # schema should be updated when setting label
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- form.set_label('foo', "Woohoo")
- self.assertEqual(form.get_label('foo'), "Woohoo")
- self.assertEqual(schema['foo'].title, "Woohoo")
-
- def test_render_vue_tag(self):
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- html = form.render_vue_tag()
- self.assertEqual(html, '
')
-
- def test_render_vue_template(self):
- self.pyramid_config.include('pyramid_mako')
- self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
- 'pyramid.events.BeforeRender')
-
- # form button is disabled on @submit by default
- schema = self.make_schema()
- form = self.make_form(schema=schema)
- html = form.render_vue_template()
- self.assertIn('