3
0
Fork 0

feat: add view to change current user password

This commit is contained in:
Lance Edgar 2024-08-05 11:45:00 -05:00
parent 70d13ee1e7
commit a2ba88ca8f
13 changed files with 259 additions and 7 deletions

View file

@ -122,6 +122,7 @@ def make_pyramid_config(settings):
pyramid_config.include('pyramid_beaker')
pyramid_config.include('pyramid_deform')
pyramid_config.include('pyramid_mako')
pyramid_config.include('pyramid_tm')
return pyramid_config

View file

@ -337,6 +337,16 @@ class Form:
with label and containing a widget.
Actual output will depend on the field attributes etc.
Typical output might look like:
.. code-block:: html
<b-field label="Foo"
horizontal
type="is-danger"
message="something went wrong!">
<!-- widget element(s) -->
</b-field>
"""
dform = self.get_deform()
field = dform[fieldname]
@ -354,8 +364,50 @@ class Form:
'label': label,
}
# next we will build array of messages to display..some
# fields always show a "helptext" msg, and some may have
# validation errors..
field_type = None
messages = []
# show errors if present
errors = self.get_field_errors(fieldname)
if errors:
field_type = 'is-danger'
messages.extend(errors)
# ..okay now we can declare the field messages and type
if field_type:
attrs['type'] = field_type
if messages:
if len(messages) == 1:
msg = messages[0]
if msg.startswith('`') and msg.endswith('`'):
attrs[':message'] = msg
else:
attrs['message'] = msg
# TODO
# else:
# # nb. must pass an array as JSON string
# attrs[':message'] = '[{}]'.format(', '.join([
# "'{}'".format(msg.replace("'", r"\'"))
# for msg in messages]))
return HTML.tag('b-field', c=[html], **attrs)
def get_field_errors(self, field):
"""
Return a list of error messages for the given field.
Not useful unless a call to :meth:`validate()` failed.
"""
dform = self.get_deform()
if field in dform:
error = dform[field].errormsg
if error:
return [error]
return []
def get_vue_field_value(self, field):
"""
This method returns a JSON string which will be assigned as
@ -400,7 +452,10 @@ class Form:
the :attr:`validated` attribute.
However if the data is not valid, ``False`` is returned, and
there will be no :attr:`validated` attribute.
there will be no :attr:`validated` attribute. In that case
you should inspect the form errors to learn/display what went
wrong for the user's sake. See also
:meth:`get_field_errors()`.
:returns: Data dict, or ``False``.
"""

View file

@ -0,0 +1,7 @@
## -*- coding: utf-8; -*-
<%inherit file="/form.mako" />
<%def name="title()">Change Password</%def>
${parent.body()}

View file

@ -316,6 +316,7 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${request.user}</a>
<div class="navbar-dropdown">
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div>
</div>

View file

@ -0,0 +1,13 @@
<div tal:define="name name|field.name;
vmodel vmodel|'model_'+name;">
${field.start_mapping()}
<b-input name="${name}"
value="${field.widget.redisplay and cstruct or ''}"
type="password"
placeholder="Password" />
<b-input name="${name}-confirm"
value="${field.widget.redisplay and confirm or ''}"
type="password"
placeholder="Confirm Password" />
${field.end_mapping()}
</div>

View file

@ -1,6 +1,12 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%def name="page_content()">
<div style="margin-top: 2rem; width: 50%;">
${form.render_vue_tag()}
</div>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
${form.render_vue_template()}

View file

@ -9,7 +9,7 @@
% endfor
</section>
<div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: end; width: 100%;">
<div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: ${'end' if form.align_buttons_right else 'start'}; width: 100%; padding-left: 10rem;">
% if form.show_button_reset:
<b-button native-type="reset">

View file

@ -43,7 +43,7 @@ def get_form_data(request):
# 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 request.content_type == 'application/json'):
or getattr(request, 'content_type', None) == 'application/json'):
return request.json_body
return request.POST

View file

@ -25,7 +25,7 @@ Auth Views
"""
import colander
from deform.widget import TextInputWidget, PasswordWidget
from deform.widget import TextInputWidget, PasswordWidget, CheckedPasswordWidget
from wuttaweb.views import View
from wuttaweb.db import Session
@ -45,7 +45,7 @@ class AuthView(View):
Upon successful login, user is redirected to home page.
* route: ``login``
* template: ``/login.mako``
* template: ``/auth/login.mako``
"""
auth = self.app.get_auth_handler()
@ -138,6 +138,66 @@ class AuthView(View):
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.")
@classmethod
def defaults(cls, config):
cls._auth_defaults(config)
@ -149,13 +209,19 @@ class AuthView(View):
config.add_route('login', '/login')
config.add_view(cls, attr='login',
route_name='login',
renderer='/login.mako')
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')
def defaults(config, **kwargs):
base = globals()