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 @@ @@ -1,5 +1,6 @@
# ignore sqlite database
ordr2.sqlite
ordr2_development.sqlite
# ignore test mails
mail/

68
ordr2/schemas/account.py

@ -18,9 +18,7 @@ class RegistrationSchema(CSRFSchema): @@ -18,9 +18,7 @@ class RegistrationSchema(CSRFSchema):
username = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(
readonly=True
),
widget=deform.widget.TextInputWidget(readonly=True),
description='automagically generated for you',
validator=deferred_unique_username_validator,
)
@ -60,8 +58,70 @@ class ResetPasswordSchema(CSRFSchema): @@ -60,8 +58,70 @@ class ResetPasswordSchema(CSRFSchema):
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Create Account', 'Cancel'),
'buttons': ('Reset Password', 'Cancel'),
'css_class': 'form reset-password'
}
settings.update(override)
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 { @@ -27,7 +27,7 @@ main h1 {
margin-top:2rem;
}
.item-password > div {
.item-password > div, .item-new_password > div {
margin-bottom: 1rem;
}
@ -47,3 +47,13 @@ main h1 { @@ -47,3 +47,13 @@ main h1 {
.form-group.has-error .form-text:nth-of-type(2) {
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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ -7,9 +7,13 @@ from pyramid.security import remember, forget
from pyramid.view import view_config
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.schemas.account import RegistrationSchema, ResetPasswordSchema
from ordr2.schemas.account import (
RegistrationSchema,
ResetPasswordSchema,
AccountSettingsSchema
)
PROPOSED_PASSWORD_LENGTH = 12
@ -20,6 +24,18 @@ PROPOSED_PASSWORD_LENGTH = 12 @@ -20,6 +24,18 @@ PROPOSED_PASSWORD_LENGTH = 12
@view_config(
context='ordr2:resources.account.AccountResource',
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',
renderer='ordr2:templates/account/login.jinja2'
)
@ -38,6 +54,11 @@ def login(context, request): @@ -38,6 +54,11 @@ def login(context, request):
request.resource_url(request.root, 'orders'),
headers=headers
)
request.session.flash(
'You entered the wrong username or password',
'error'
)
return {}
@ -241,3 +262,107 @@ def reset_password_form_processing(context, request): @@ -241,3 +262,107 @@ def reset_password_form_processing(context, request):
request.session.flash('Your password was changed', 'ok')
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'))