Browse Source

added settings page

master
Holger Frey 7 years ago
parent
commit
e53788559f
  1. 1
      .gitignore
  2. 68
      ordr2/schemas/account.py
  3. 12
      ordr2/static/style.css
  4. 20
      ordr2/templates/account/email_confirmation.jinja2
  5. 13
      ordr2/templates/account/settings.jinja2
  6. 25
      ordr2/templates/emails/mail_change.jinja2
  7. 129
      ordr2/views/account.py

1
.gitignore vendored

@ -1,5 +1,6 @@
# ignore sqlite database # ignore sqlite database
ordr2.sqlite ordr2.sqlite
ordr2_development.sqlite
# ignore test mails # ignore test mails
mail/ mail/

68
ordr2/schemas/account.py

@ -18,9 +18,7 @@ class RegistrationSchema(CSRFSchema):
username = colander.SchemaNode( username = colander.SchemaNode(
colander.String(), colander.String(),
widget=deform.widget.TextInputWidget( widget=deform.widget.TextInputWidget(readonly=True),
readonly=True
),
description='automagically generated for you', description='automagically generated for you',
validator=deferred_unique_username_validator, validator=deferred_unique_username_validator,
) )
@ -60,8 +58,70 @@ class ResetPasswordSchema(CSRFSchema):
@classmethod @classmethod
def as_form(cls, request, **override): def as_form(cls, request, **override):
settings = { settings = {
'buttons': ('Create Account', 'Cancel'), 'buttons': ('Reset Password', 'Cancel'),
'css_class': 'form reset-password' 'css_class': 'form reset-password'
} }
settings.update(override) settings.update(override)
return super().as_form(request, **settings) return super().as_form(request, **settings)
class SettingsSectionGeneralSchema(colander.Schema):
''' Section 'General' for account settings schema '''
username = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(readonly=True),
)
first_name = colander.SchemaNode(
colander.String()
)
last_name = colander.SchemaNode(
colander.String()
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
role = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(readonly=True),
)
class SettingsSectionChangePasswordSchema(colander.Schema):
''' Section 'Change Password' for account settings schema '''
new_password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget(),
missing=''
)
class SettingsSectionConfirmChangesSchema(colander.Schema):
''' Section 'ConfirmChanges' for account settings schema '''
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.PasswordWidget(),
validator=deferred_password_validator,
description='Enter your current password to confirm changes'
)
class AccountSettingsSchema(CSRFSchema):
''' account settings schema '''
general = SettingsSectionGeneralSchema()
change_password = SettingsSectionChangePasswordSchema()
confirm_changes = SettingsSectionConfirmChangesSchema()
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Save Settings', 'Cancel'),
'css_class': 'form account-settings'
}
settings.update(override)
return super().as_form(request, **settings)

12
ordr2/static/style.css

@ -27,7 +27,7 @@ main h1 {
margin-top:2rem; margin-top:2rem;
} }
.item-password > div { .item-password > div, .item-new_password > div {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -47,3 +47,13 @@ main h1 {
.form-group.has-error .form-text:nth-of-type(2) { .form-group.has-error .form-text:nth-of-type(2) {
margin-top: -20px; margin-top: -20px;
} }
.form .panel {
margin-bottom:3rem;
}
.form .panel-heading {
font-size:150%;
margin-bottom:1.5rem;
border-bottom:1px solid #ccc;
}

20
ordr2/templates/account/email_confirmation.jinja2

@ -0,0 +1,20 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block title %} Ordr | Account Settings {% endblock title %}
{% block content %}
<h1>Verify Your Email Address</h1>
{{ macros.show_flash_messages() }}
<p>
An email has been sent to your new email address.
</p>
<p>
Please follow the link in the email to verify your address.
</p>
{{ macros.white_space_column('col-6') }}
{% endblock content %}

13
ordr2/templates/account/settings.jinja2

@ -0,0 +1,13 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block title %} Ordr | Account Settings {% endblock title %}
{% block content %}
<h1>Account Settings</h1>
{{ form.render()|safe }}
{{ macros.white_space_column('col-6') }}
{% endblock content %}

25
ordr2/templates/emails/mail_change.jinja2

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>[ordr] verify your email address</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
</head>
<body>
<h1>Hi there!</h1>
<p>
Please verify your email address for the account "{{ user.username }}" by following this link
<a href="{{ request.resource_url(context, data.token.hash) }}">{{ request.resource_url(context, data.token.hash) }}</a>
</p>
<p> The link will expire on {{ data.token.expires.strftime('%d.%m.%y at %H:%M') }}.
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

129
ordr2/views/account.py

@ -7,9 +7,13 @@ from pyramid.security import remember, forget
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy import or_ from sqlalchemy import or_
from ordr2.events import CompleteRegistration, PasswordReset from ordr2.events import ChangedEmail, CompleteRegistration, PasswordReset
from ordr2.models.account import User, Role, TokenSubject from ordr2.models.account import User, Role, TokenSubject
from ordr2.schemas.account import RegistrationSchema, ResetPasswordSchema from ordr2.schemas.account import (
RegistrationSchema,
ResetPasswordSchema,
AccountSettingsSchema
)
PROPOSED_PASSWORD_LENGTH = 12 PROPOSED_PASSWORD_LENGTH = 12
@ -20,6 +24,18 @@ PROPOSED_PASSWORD_LENGTH = 12
@view_config( @view_config(
context='ordr2:resources.account.AccountResource', context='ordr2:resources.account.AccountResource',
name='login', name='login',
request_method='GET',
permission='login',
renderer='ordr2:templates/account/login.jinja2'
)
def login_form(context, request):
return {}
@view_config(
context='ordr2:resources.account.AccountResource',
name='login',
request_method='POST',
permission='login', permission='login',
renderer='ordr2:templates/account/login.jinja2' renderer='ordr2:templates/account/login.jinja2'
) )
@ -38,6 +54,11 @@ def login(context, request):
request.resource_url(request.root, 'orders'), request.resource_url(request.root, 'orders'),
headers=headers headers=headers
) )
request.session.flash(
'You entered the wrong username or password',
'error'
)
return {} return {}
@ -241,3 +262,107 @@ def reset_password_form_processing(context, request):
request.session.flash('Your password was changed', 'ok') request.session.flash('Your password was changed', 'ok')
return HTTPFound(request.resource_url(context.__parent__, 'login')) return HTTPFound(request.resource_url(context.__parent__, 'login'))
# account settings
@view_config(
context='ordr2:resources.account.AccountResource',
request_method='GET',
name='settings',
permission='settings',
renderer='ordr2:templates/account/settings.jinja2'
)
def settings_form(context, request):
prefill = {
'general': {
'username': context.model.username,
'first_name': context.model.first_name,
'last_name': context.model.last_name,
'email': context.model.email,
'role': str(context.model.role)
}
}
form = AccountSettingsSchema.as_form(request)
form.set_appstruct(prefill)
return {'form': form}
@view_config(
context='ordr2:resources.account.AccountResource',
request_method='POST',
name='settings',
permission='settings',
renderer='ordr2:templates/account/settings.jinja2'
)
def settings_form_processing(context, request):
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
# validate the form data
form = AccountSettingsSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
context.model.first_name = appstruct['general']['first_name']
context.model.last_name = appstruct['general']['last_name']
new_password = appstruct['change_password']['new_password']
if new_password:
context.model.set_password(new_password)
request.session.flash(
'Password updated sucessfully',
'success'
)
# issue a warning on a short password
if len(new_password) < PROPOSED_PASSWORD_LENGTH:
request.session.flash(
'You should really consider a longer password',
'warning'
)
# email address changed
# the email address is not updated directly,
# but a email sent to confirm the new address
new_email = appstruct['general']['email']
if new_email != context.model.email:
# create a verify-new-email token and send email
token = context.model.issue_token(
request,
TokenSubject.CHANGE_EMAIL,
{'new_email': new_email}
)
notification = ChangedEmail(
request,
context.model,
{'token': token},
send_to=new_email
)
request.registry.notify(notification)
return HTTPFound(request.resource_url(context, 'verify-new-email'))
return HTTPFound(request.resource_url(request.root))
@view_config(
context='ordr2:resources.account.AccountResource',
name='verify-new-email',
permission='settings',
renderer='ordr2:templates/account/email_confirmation.jinja2'
)
def email_confirmation(context, request):
''' email sent to new address '''
return {}
@view_config(
context='ordr2:resources.account.EmailVerificationToken',
permission='settings'
)
def email_change_confirmed(context, request):
''' changed email address is confirmed '''
context.model.owner.email = context.model.payload['new_email']
request.dbsession.delete(context.model)
request.session.flash('Email change sucessful', 'success')
return HTTPFound(request.resource_url(request.root, 'login'))