Browse Source

added user registration form

master
Holger Frey 7 years ago
parent
commit
b8a0cd008b
  1. 5
      ordr2/__init__.py
  2. 8
      ordr2/templates/account/register.jinja2
  3. 77
      ordr2/views/account.py
  4. 23
      tests/__init__.py
  5. 55
      tests/_functional/registration.py
  6. 107
      tests/views/account.py

5
ordr2/__init__.py

@ -14,11 +14,14 @@ def main(global_config, **settings):
config.include('.models') config.include('.models')
config.include('.resources') config.include('.resources')
config.include('.schemas')
config.include('.security') config.include('.security')
config.include('.session') config.include('.session')
config.include('.schemas')
config.include('.views') config.include('.views')
# explicit include for jinja2 to enable rendering in events module
config.include('pyramid_jinja2')
config.scan() config.scan()
return config.make_wsgi_app() return config.make_wsgi_app()

8
ordr2/templates/account/register.jinja2

@ -3,12 +3,12 @@
{% block title %} Ordr | Account Registration {% endblock title %} {% block title %} Ordr | Account Registration {% endblock title %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row">
<div class="col-8"> <div class="col-2"></div>
<div class="col-5">
<div class="jumbotron">
<h1>Account Registration</h1> <h1>Account Registration</h1>
</div> {{ form.render()|safe }}
</div> </div>
</div> </div>

77
ordr2/views/account.py

@ -1,28 +1,24 @@
''' account registration, login, logout and settings ''' ''' account registration, login, logout and settings '''
import deform
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget from pyramid.security import remember, forget
from pyramid.view import view_config from pyramid.view import view_config
from ordr2.models import User from ordr2.events import CompleteRegistration
from ordr2.models.account import User, Role, TokenSubject
from ordr2.schemas.account import RegistrationSchema
@view_config( PROPOSED_PASSWORD_LENGTH = 12
context='ordr2:resources.account.AccountResource',
name='register',
permission='register',
renderer='ordr2:templates/account/register.jinja2'
)
def register(context, request):
''' the new user registraion page '''
return {}
@view_config( @view_config(
context='ordr2:resources.account.AccountResource', context='ordr2:resources.account.AccountResource',
name='login', name='login',
permission='login',
request_method='POST', request_method='POST',
permission='login',
renderer='ordr2:templates/account/login.jinja2' renderer='ordr2:templates/account/login.jinja2'
) )
def login(context, request): def login(context, request):
@ -52,3 +48,62 @@ def logout(context, request):
''' log out an user ''' ''' log out an user '''
headers = forget(request) headers = forget(request)
return HTTPFound(request.resource_url(request.root), headers=headers) return HTTPFound(request.resource_url(request.root), headers=headers)
@view_config(
context='ordr2:resources.account.AccountResource',
name='register',
request_method='GET',
permission='register',
renderer='ordr2:templates/account/register.jinja2'
)
def registration_form(context, request):
''' the new user registraion page '''
form = RegistrationSchema.as_form(request)
return {'form':form}
@view_config(
context='ordr2:resources.account.AccountResource',
name='register',
request_method='POST',
permission='register',
renderer='ordr2:templates/account/register.jinja2'
)
def registration_form_processing(context, request):
''' registration form processing '''
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
# validate the form data
form = RegistrationSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation successfull, create user
account = User(
username=appstruct['username'],
first_name=appstruct['first_name'],
last_name=appstruct['last_name'],
email=appstruct['email'],
role=Role.UNVALIDATED
)
account.set_password(appstruct['password'])
request.dbsession.add(account)
# create a verify-new-account token and send email
hash = account.issue_token(request, TokenSubject.USER_REGISTRATION)
notification = CompleteRegistration(request, account, {'hash': hash})
request.registry.notify(notification)
# issue a warning on a short password
if len(appstruct['password']) < PROPOSED_PASSWORD_LENGTH:
request.session.flash(
'warning',
'You should really consider a longer password'
)
return HTTPFound(request.resource_url(context, 'registered'))

23
tests/__init__.py

@ -5,6 +5,8 @@ import pytest
import transaction import transaction
from pyramid import testing from pyramid import testing
from pyramid.csrf import get_csrf_token
from webob.multidict import MultiDict
# some path mangling to get the path to passlib.ini # some path mangling to get the path to passlib.ini
@ -22,7 +24,8 @@ APP_SETTINGS = {
'passlib.config': passlib_config_path, 'passlib.config': passlib_config_path,
'pyramid.includes': [ 'pyramid.includes': [
'pyramid_jinja2', 'pyramid_jinja2',
] ],
'mail.default_sender': 'ordr@example.com'
} }
EXAMPLE_USER_DATA = { EXAMPLE_USER_DATA = {
@ -41,7 +44,8 @@ EXAMPLE_USER_DATA = {
def app_config(): def app_config():
''' fixture for tests requiring a pyramid.testing setup ''' ''' fixture for tests requiring a pyramid.testing setup '''
with testing.testConfig(settings=APP_SETTINGS) as config: with testing.testConfig(settings=APP_SETTINGS) as config:
#config.include('pyramid_mailer.testing') config.include('pyramid_mailer.testing')
config.include('pyramid_jinja2')
from ordr2.models.account import passlib_context from ordr2.models.account import passlib_context
passlib_context.update(schemes=['argon2']) passlib_context.update(schemes=['argon2'])
@ -71,7 +75,6 @@ def dbsession(app_config):
Base.metadata.drop_all(engine) Base.metadata.drop_all(engine)
# helpers # helpers
def get_user(role_name): def get_user(role_name):
@ -98,4 +101,18 @@ def create_users(db):
db.add(user) db.add(user)
def set_deform_data(request, form_data, extra_data=None, **kwargs):
''' augments the request to include post data as provided by deform '''
post_dict = MultiDict()
post_dict['__formid__'] = 'deform'
post_dict['_charset_'] = 'UTF-8'
post_dict['csrf_token'] = get_csrf_token(request)
post_dict.update(form_data)
if extra_data:
post_dict.update(extra_data)
post_dict.update(kwargs)
request.POST = post_dict

55
tests/_functional/registration.py

@ -21,7 +21,6 @@ def test_account_register_unauthenticated(testapp):
testapp.reset() testapp.reset()
response = testapp.get('/account/register') response = testapp.get('/account/register')
# basic content test # basic content test
assert 'Ordr | Account Registration' in response assert 'Ordr | Account Registration' in response
# test the main nav section links and highlighting # test the main nav section links and highlighting
@ -30,4 +29,58 @@ def test_account_register_unauthenticated(testapp):
assert li_one.find('a').text == 'FAQs' assert li_one.find('a').text == 'FAQs'
assert 'active' in li_two['class'] assert 'active' in li_two['class']
assert li_two.find('a').text == 'Register' assert li_two.find('a').text == 'Register'
# check for the registration form
form = response.html.find('form', class_='registration')
assert form is not None
@pytest.mark.xfail
def test_account_registeration_flow(testapp):
''' test the complete registration process '''
# submit the registration form
form = resonse.forms[1]
form['username'] = 'AmyMcDonald'
form['first_name'] = 'Amy'
form['last_name'] = 'McDonald'
form['email'] = 'amy@example.com'
form['password'] = 'Amy'
form['password_confirm'] = 'Amy'
response = form.submitt()
assert response.location == '/account/verify'
response = response.follow()
assert 'email sent' in response
# click the email verification token
email = ''
token = email
response = testapp.get('/account/' + token)
assert 'consider a longer password' in response
assert 'activated by an administrator' in response
# logging in should not work
form = response.forms[0]
form['username'] = 'AmyMcDonald'
form['password'] = 'Amy'
response = form.submit()
assert '<!-- user is logged in -->' not in response
# activate the new user
testapp.login('admin')
response = testapp.get('/admin/users?role=new')
response = response.click('edit user')
form = response.forms[1]
form['role'] = 'USER'
form.submit()
testapp.logout()
# login should now work
response = testapp.get('/')
form = response.forms[0]
form['username'] = 'AmyMcDonald'
form['password'] = 'Amy'
response = form.submit()
assert '<!-- user is logged in -->' in response

107
tests/views/account.py

@ -1,12 +1,26 @@
''' Tests for ordr2.views.account ''' ''' Tests for ordr2.views.account '''
import deform
import pytest import pytest
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from pyramid.testing import DummyRequest, DummyResource from pyramid.testing import DummyRequest, DummyResource
from pyramid_mailer import get_mailer
from webob.multidict import MultiDict
from .. import app_config, dbsession, get_user, create_users, set_deform_data
from .. import app_config, dbsession, get_user, create_users
REGISTRATION_FORM_DATA = MultiDict([
('username', 'AmyMcDonald'),
('first_name', 'Amy'),
('last_name', 'McDonald'),
('email', 'mcdonald@example.com'),
('__start__', 'password:mapping'),
('password', 'Amy'),
('password-confirm', 'Amy'),
('__end__', 'password:mapping'),
])
@pytest.mark.parametrize('rolename', ['user', 'purchaser', 'admin']) @pytest.mark.parametrize('rolename', ['user', 'purchaser', 'admin'])
@ -83,3 +97,94 @@ def test_logout(app_config):
assert isinstance(result, HTTPFound) assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//' assert result.location == 'http://example.com//'
def test_registration_form(app_config):
''' registration form '''
from ordr2.views.account import registration_form
request = DummyRequest()
context = DummyResource()
result = registration_form(context, request)
assert isinstance(result['form'], deform.Form)
def test_registration_form_processing_ok(dbsession):
''' registration form processing with valid data'''
from ordr2.models.account import User, Role, TokenSubject
from ordr2.views.account import registration_form_processing
user = get_user('user') # intentionally not added to database
context = DummyResource(model=user)
request = DummyRequest(dbsession=dbsession, context=context)
set_deform_data(request, REGISTRATION_FORM_DATA)
result = registration_form_processing(context, request)
# return value of function call
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com/registered'
# user should be added to database
user = dbsession.query(User).first()
assert user.username == REGISTRATION_FORM_DATA['username']
assert user.first_name == REGISTRATION_FORM_DATA['first_name']
assert user.last_name == REGISTRATION_FORM_DATA['last_name']
assert user.email == REGISTRATION_FORM_DATA['email']
assert user.check_password(REGISTRATION_FORM_DATA['password'])
assert user.role == Role.UNVALIDATED
# a token should be created
token = user.tokens[0]
assert token.subject == TokenSubject.USER_REGISTRATION
# and a verification email should be sent
# mailer = get_mailer(request.registry)
# last_mail = mailer.outbox[-1]
# assert 'Please verify your email address ' in last_mail.html
# assert 'http://example.com/' + token.hash in last_mail.html
def test_registration_form_processing_cancel(app_config):
''' canceling registration form processing '''
from ordr2.models.account import User, Role, TokenSubject
from ordr2.views.account import registration_form_processing
user = get_user('user') # intentionally not added to database
context = DummyResource(model=user)
request = DummyRequest(dbsession=dbsession, context=context)
set_deform_data(request, REGISTRATION_FORM_DATA, {'Cancel': 'Cancel'})
result = registration_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
@pytest.mark.parametrize(
'key,value', [
('username', ''),
('username', 'TerryGilliam'),
('first_name', ''),
('last_name', ''),
('email', ''),
('email', 'no email'),
('email', 'gilliam@example.com'),
('password', ''),
('password-confirm', ''),
('password-confirm', 'no match')
]
)
def test_registration_form_processing_validation_error(dbsession, key, value):
''' registration form processing with valid data'''
from ordr2.models.account import User, Role, TokenSubject
from ordr2.views.account import registration_form_processing
admin = get_user('user')
dbsession.add(admin)
context = DummyResource(model=get_user('admin'))
request = DummyRequest(dbsession=dbsession, context=context)
set_deform_data(request, REGISTRATION_FORM_DATA, {key: value})
result = registration_form_processing(context, request)
# return value of function call
assert isinstance(result['form'], deform.Form)