diff --git a/ordr2/templates/account/forgot_password_email.jinja2 b/ordr2/templates/account/forgot_password_email.jinja2
new file mode 100644
index 0000000..82e2a59
--- /dev/null
+++ b/ordr2/templates/account/forgot_password_email.jinja2
@@ -0,0 +1,16 @@
+{% extends "ordr2:templates/layout.jinja2" %}
+
+{% block title %} Ordr | Forgotten Password {% endblock title %}
+
+{% block content %}
+
+
+
+
+
Password Reset Link
+
To reset your password, an email has been sent to you.
+
Please follow the link in the email to set a new password.
+
+
+
+{% endblock content %}
diff --git a/ordr2/templates/account/forgot_password_form.jinja2 b/ordr2/templates/account/forgot_password_form.jinja2
new file mode 100644
index 0000000..e6e4303
--- /dev/null
+++ b/ordr2/templates/account/forgot_password_form.jinja2
@@ -0,0 +1,23 @@
+{% extends "ordr2:templates/layout.jinja2" %}
+
+{% block title %} Ordr | Forgotten Password {% endblock title %}
+
+{% block content %}
+
+
+
+
Forgotten password?
+
+
+
+{% endblock content %}
diff --git a/ordr2/views/account.py b/ordr2/views/account.py
index 554a91a..71f6aff 100644
--- a/ordr2/views/account.py
+++ b/ordr2/views/account.py
@@ -5,8 +5,9 @@ import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from pyramid.view import view_config
+from sqlalchemy import or_
-from ordr2.events import CompleteRegistration
+from ordr2.events import CompleteRegistration, PasswordReset
from ordr2.models.account import User, Role, TokenSubject
from ordr2.schemas.account import RegistrationSchema
@@ -14,6 +15,8 @@ from ordr2.schemas.account import RegistrationSchema
PROPOSED_PASSWORD_LENGTH = 12
+# login and logout
+
@view_config(
context='ordr2:resources.account.AccountResource',
name='login',
@@ -33,7 +36,7 @@ def login(context, request):
if user.is_active and user.check_password(password):
headers = remember(request, user.id)
return HTTPFound(
- request.resource_path(request.root, 'orders'),
+ request.resource_url(request.root, 'orders'),
headers=headers
)
return {}
@@ -50,6 +53,8 @@ def logout(context, request):
return HTTPFound(request.resource_url(request.root), headers=headers)
+# account registration
+
@view_config(
context='ordr2:resources.account.AccountResource',
name='register',
@@ -116,6 +121,7 @@ def registration_form_processing(context, request):
renderer='ordr2:templates/account/registration_confirmation.jinja2'
)
def registration_confirmation(context, request):
+ ''' email to verify registration was sent '''
return {}
@@ -125,6 +131,67 @@ def registration_confirmation(context, request):
renderer='ordr2:templates/account/registration_completed.jinja2'
)
def registration_completed(context, request):
+ ''' registration was verified by mail link '''
context.model.owner.role = Role.NEW
request.dbsession.delete(context.model)
return {}
+
+
+# password recovery
+
+@view_config(
+ context='ordr2:resources.account.AccountResource',
+ name='forgot-password',
+ request_method='GET',
+ permission='reset password',
+ renderer='ordr2:templates/account/forgot_password_form.jinja2'
+ )
+def forgot_password_form(context, request):
+ ''' forgot password form '''
+ return {}
+
+
+@view_config(
+ context='ordr2:resources.account.AccountResource',
+ name='forgot-password',
+ request_method='POST',
+ permission='reset password',
+ renderer='ordr2:templates/account/forgot_password_form.jinja2'
+ )
+def forgot_password_form_processing(context, request):
+ ''' forgot password form processing '''
+ if 'cancel' in request.POST:
+ return HTTPFound(request.resource_url(request.root))
+
+ identifier = request.POST.get('username_or_email')
+ account = (
+ request.dbsession
+ .query(User)
+ .filter(or_(User.username == identifier, User.email == identifier))
+ .first()
+ )
+ if not account:
+ request.session.flash(
+ 'warning',
+ 'Username or email address unknown'
+ )
+ return {}
+
+ # create a forgot-my-password token and send email
+ token = account.issue_token(request, TokenSubject.RESET_PASSWORD)
+ notification = PasswordReset(request, account, {'token': token})
+ request.registry.notify(notification)
+
+ return HTTPFound(request.resource_url(context, 'forgot-password-email'))
+
+
+@view_config(
+ context='ordr2:resources.account.AccountResource',
+ name='forgot-password-email',
+ request_method='GET',
+ permission='reset password',
+ renderer='ordr2:templates/account/forgot_password_email.jinja2'
+ )
+def forgot_password_email_sent(context, request):
+ ''' password reset link was sent '''
+ return {}
diff --git a/tests/views/account.py b/tests/views/account.py
index 6cd0273..17b4b6e 100644
--- a/tests/views/account.py
+++ b/tests/views/account.py
@@ -40,7 +40,7 @@ def test_account_login_active_users(dbsession, rolename):
result = login(None, request)
assert isinstance(result, HTTPFound)
- assert result.location == '//orders'
+ assert result.location == 'http://example.com//orders'
@pytest.mark.parametrize('rolename', ['unvalidated', 'new', 'inactive'])
@@ -99,6 +99,8 @@ def test_logout(app_config):
assert result.location == 'http://example.com//'
+# tests for the registration form
+
def test_registration_form(app_config):
''' registration form '''
from ordr2.views.account import registration_form
@@ -214,3 +216,72 @@ def test_registration_completed(dbsession):
assert user.role == Role.NEW
assert dbsession.query(Token).count() == 0
assert dbsession.query(User).count() == 1
+
+
+# tests for password reset
+
+def test_forgot_password_form():
+ ''' display the forgot password form '''
+ from ordr2.views.account import forgot_password_form
+
+ result = forgot_password_form(None, None)
+
+ assert result == {}
+
+
+@pytest.mark.parametrize('who', ['TerryGilliam', 'gilliam@example.com'])
+def test_forgot_password_form_processing_ok(dbsession, who):
+ ''' process the forgot password form '''
+ from ordr2.views.account import forgot_password_form_processing
+
+ dbsession.add(get_user('user'))
+ request = DummyRequest(
+ dbsession=dbsession,
+ POST={'username_or_email': who}
+ )
+ result = forgot_password_form_processing(None, request)
+
+ assert isinstance(result, HTTPFound)
+ assert result.location == 'http://example.com//forgot-password-email'
+
+
+def test_forgot_password_form_processing_cancel(dbsession):
+ ''' forgot password form was canceled '''
+ from ordr2.views.account import forgot_password_form_processing
+
+ dbsession.add(get_user('user'))
+ request = DummyRequest(
+ dbsession=dbsession,
+ POST={'username_or_email': 'gilliam@example.com', 'cancel': 'Cancel'}
+ )
+ result = forgot_password_form_processing(None, request)
+
+ assert isinstance(result, HTTPFound)
+ assert result.location == 'http://example.com//'
+
+
+@pytest.mark.parametrize(
+ 'who',
+ ['unknown user', 'unknown@example.com', '']
+ )
+def test_forgot_password_form_processing_error(dbsession, who):
+ ''' process the forgot password form '''
+ from ordr2.views.account import forgot_password_form_processing
+
+ dbsession.add(get_user('user'))
+ request = DummyRequest(
+ dbsession=dbsession,
+ POST = {'username_or_email': who}
+ )
+ result = forgot_password_form_processing(None, request)
+
+ assert result == {}
+
+
+def test_forgot_password_email_sent():
+ ''' message that a password reset link was sent '''
+ from ordr2.views.account import forgot_password_email_sent
+
+ result = forgot_password_email_sent(None, None)
+
+ assert result == {}