Browse Source

added schemas, starting on registration

rework
Holger Frey 7 years ago
parent
commit
f63e337dde
  1. 1
      ordr/__init__.py
  2. 2
      ordr/resources/__init__.py
  3. 26
      ordr/resources/account.py
  4. 28
      ordr/resources/helpers.py
  5. 53
      ordr/schemas/__init__.py
  6. 36
      ordr/schemas/account.py
  7. 63
      ordr/schemas/helpers.py
  8. 5
      ordr/templates/account/registration_form.jinja2
  9. 2
      ordr/templates/layout.jinja2
  10. 14
      ordr/views/registration.py
  11. 1
      setup.py
  12. 9
      tests/_functional/registration.py
  13. 26
      tests/resources/account.py
  14. 65
      tests/resources/base_child_resource.py
  15. 4
      tests/resources/root.py
  16. 28
      tests/schemas/__init__.py
  17. 165
      tests/schemas/helpers.py
  18. 12
      tests/views/registration.py

1
ordr/__init__.py

@ -17,6 +17,7 @@ def main(global_config, **settings):
config.include('pyramid_jinja2') config.include('pyramid_jinja2')
config.include('.models') config.include('.models')
config.include('.resources') config.include('.resources')
config.include('.schemas')
config.include('.security') config.include('.security')
config.add_static_view('static', 'static', cache_max_age=3600) config.add_static_view('static', 'static', cache_max_age=3600)
config.scan() config.scan()

2
ordr/resources/__init__.py

@ -34,7 +34,7 @@ class RootResource:
:raises: KeyError if child resource is not found :raises: KeyError if child resource is not found
''' '''
map = { map = {
'registration': RegistrationResource 'register': RegistrationResource
} }
child_class = map[key] child_class = map[key]
return child_class(request=self.request, name=key, parent=self) return child_class(request=self.request, name=key, parent=self)

26
ordr/resources/account.py

@ -1,9 +1,12 @@
''' Resources (sub) package, used to connect URLs to views ''' ''' Resources (sub) package, used to connect URLs to views '''
from pyramid.security import Allow, Everyone, DENY_ALL from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.schemas.account import RegistrationSchema
from .helpers import BaseChildResource
class RegistrationResource:
class RegistrationResource(BaseChildResource):
''' The resource for new user registration ''' The resource for new user registration
:param pyramid.request.Request request: the current request object :param pyramid.request.Request request: the current request object
@ -11,19 +14,16 @@ class RegistrationResource:
:param parent: the parent resouce :param parent: the parent resouce
''' '''
nav_active = 'welcome' nav_active = 'registration'
def __init__(self, request, name, parent):
''' Create registration resource
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
'''
self.request = request
self.__name__ = name
self.__parent__ = parent
def __acl__(self): def __acl__(self):
''' access controll list for the resource ''' ''' access controll list for the resource '''
return [(Allow, Everyone, 'view'), DENY_ALL] return [(Allow, Everyone, 'view'), DENY_ALL]
def get_registration_form(self, **override):
''' returns the registration form'''
settings = {
'buttons': ('Create account', 'Cancel'),
}
settings.update(override)
return self._prepare_form(RegistrationSchema, **settings)

28
ordr/resources/helpers.py

@ -0,0 +1,28 @@
''' Resources (sub) package, used to connect URLs to views '''
from pyramid.security import Allow, Everyone, DENY_ALL
class BaseChildResource:
def __init__(self, request, name, parent):
''' Create a child resource
:param pyramid.request.Request request: the current request object
:param str name: the name of the resource
:param parent: the parent resouce
'''
self.request = request
self.__name__ = name
self.__parent__ = parent
def __acl__(self):
''' access controll list for the resource '''
raise NotImplementedError()
def _prepare_form(self, schema, prefill=None, **settings):
''' prepares a deform form for the resource'''
form = schema.as_form(self.request, **settings)
if prefill is not None:
form.set_appstruct(prefill)
return form

53
ordr/schemas/__init__.py

@ -0,0 +1,53 @@
''' Schemas (sub) package, for form rendering and validation '''
import colander
import deform
from deform.renderer import configure_zpt_renderer
from .helpers import (
deferred_csrf_default,
deferred_csrf_validator
)
# Base Schema
class CSRFSchema(colander.Schema):
''' base class for schemas with csrf validation '''
csrf_token = colander.SchemaNode(
colander.String(),
default=deferred_csrf_default,
validator=deferred_csrf_validator,
widget=deform.widget.HiddenWidget(),
)
@classmethod
def as_form(cls, request, url=None, **kwargs):
''' returns the schema as a form
:param pyramid.request.Request request: the current request
:param str url:
form action url,
url is not set, the current context and view name will be used to
constuct a url for the form
:param kwargs:
additional parameters for the form rendering.
'''
if url is None:
url = request.resource_url(request.context, request.view_name)
schema = cls().bind(request=request)
form = deform.Form(schema, action=url, **kwargs)
return form
def includeme(config):
'''
Initialize the form schemas
Activate this setup using ``config.include('ordr.schemas')``.
'''
# Make Deform widgets aware of our widget template paths
configure_zpt_renderer(['ordr:templates/deform'])

36
ordr/schemas/account.py

@ -0,0 +1,36 @@
import colander
import deform
from . import CSRFSchema
from .helpers import (
deferred_unique_email_validator,
deferred_unique_username_validator,
)
# schema for user registration
class RegistrationSchema(CSRFSchema):
''' new user registration '''
username = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(readonly=True),
description='automagically generated for you',
validator=deferred_unique_username_validator,
)
first_name = colander.SchemaNode(
colander.String()
)
last_name = colander.SchemaNode(
colander.String()
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget()
)

63
ordr/schemas/helpers.py

@ -0,0 +1,63 @@
''' helper functions for schemas '''
import colander
from pyramid.csrf import get_csrf_token, check_csrf_token
from ordr.models import User
@colander.deferred
def deferred_csrf_default(node, kw):
''' sets the current csrf token '''
request = kw.get('request')
return get_csrf_token(request)
@colander.deferred
def deferred_csrf_validator(node, kw):
''' validates a submitted csrf token '''
def validate_csrf(node, value):
request = kw.get('request')
if not check_csrf_token(request, raises=False):
raise colander.Invalid(node, 'Bad CSRF token')
return validate_csrf
@colander.deferred
def deferred_unique_username_validator(node, kw):
''' checks if an username is not registered already '''
def validate_unique_username(node, value):
request = kw.get('request')
user = request.dbsession.query(User).filter_by(username=value).first()
if user is not None:
raise colander.Invalid(node, 'User name already registered')
return validate_unique_username
@colander.deferred
def deferred_unique_email_validator(node, kw):
''' checks if an email is not registered already '''
email_validator = colander.Email()
def validate_unique_email(node, value):
email_validator(node, value) # raises exception on invalid address
request = kw.get('request')
user = request.dbsession.query(User).filter_by(email=value).first()
if user not in (None, request.context.model):
# allow existing email addresses if
# it belongs to the user that is currently edited
raise colander.Invalid(node, 'Email address in use')
return validate_unique_email
@colander.deferred
def deferred_password_validator(node, kw):
''' checks password confirmation for settings '''
def validate_password_confirmation(node, value):
request = kw.get('request')
if request.user is None or not request.user.check_password(value):
raise colander.Invalid(node, 'Wrong password')
return validate_password_confirmation

5
ordr/templates/account/registration_form.jinja2

@ -0,0 +1,5 @@
{% extends "ordr:templates/layout.jinja2" %}
{% block content %}
{{ context.get_registration_form().render()|safe }}
{% endblock content %}

2
ordr/templates/layout.jinja2

@ -30,7 +30,7 @@
<li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='faq' %}active{% endif %}"> <li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='faq' %}active{% endif %}">
<a href="/faq" class="nav-link">FAQs</a> <a href="/faq" class="nav-link">FAQs</a>
</li> </li>
<li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='register' %}active{% endif %}"> <li class="nav-item {% if context.nav_active=='registration' %}active{% endif %}">
<a href="/register" class="nav-link">Register</a> <a href="/register" class="nav-link">Register</a>
</li> </li>
</ul> </ul>

14
ordr/views/registration.py

@ -0,0 +1,14 @@
# from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
# from ordr.models import User
@view_config(
context='ordr.resources.account.RegistrationResource',
permission='view',
request_method='GET',
renderer='ordr:templates/account/registration_form.jinja2'
)
def registration_form(context, request):
return {}

1
setup.py

@ -11,6 +11,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
requires = [ requires = [
'argon2_cffi', 'argon2_cffi',
'bcrypt', 'bcrypt',
'deform',
'passlib', 'passlib',
'plaster_pastedeploy', 'plaster_pastedeploy',
'pyramid >= 1.9a', 'pyramid >= 1.9a',

9
tests/_functional/registration.py

@ -0,0 +1,9 @@
''' functional tests for ordr2.views.registration '''
from . import testappsetup, testapp # noqa: F401
def test_registration_form(testapp): # noqa: F811
result = testapp.get('/register')
active = result.html.find('li', class_='active')
assert active.a['href'] == '/register'

26
tests/resources/account.py

@ -1,20 +1,22 @@
''' Tests for the account resources ''' ''' Tests for the account resources '''
from pyramid.testing import DummyRequest
def test_registration_init():
from ordr.resources.account import RegistrationResource
resource = RegistrationResource(
request='some request',
name='a name',
parent='the parent'
)
assert resource.__name__ == 'a name'
assert resource.__parent__ == 'the parent'
assert resource.request == 'some request'
def test_registration_acl(): def test_registration_acl():
from pyramid.security import Allow, Everyone, DENY_ALL from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationResource from ordr.resources.account import RegistrationResource
resource = RegistrationResource('some request', 'a name', 'the parent') resource = RegistrationResource('some request', 'a name', 'the parent')
assert resource.__acl__() == [(Allow, Everyone, 'view'), DENY_ALL] assert resource.__acl__() == [(Allow, Everyone, 'view'), DENY_ALL]
def test_registration_get_registration_form():
from pyramid.security import Allow, Everyone, DENY_ALL
from ordr.resources.account import RegistrationResource
import deform
request = DummyRequest()
resource = RegistrationResource(request, 'a name', 'the parent')
form = resource.get_registration_form()
assert isinstance(form, deform.Form)
assert len(form.buttons) == 2
assert form.buttons[0].title == 'Create account'
assert form.buttons[1].title == 'Cancel'

65
tests/resources/base_child_resource.py

@ -0,0 +1,65 @@
''' Tests for the root resource '''
import pytest
from pyramid.testing import DummyRequest, DummyResource
def test_base_child_init():
from ordr.resources.helpers import BaseChildResource
resource = BaseChildResource(
request='some request',
name='a name',
parent='the parent'
)
assert resource.__name__ == 'a name'
assert resource.__parent__ == 'the parent'
assert resource.request == 'some request'
def test_base_child_acl():
from ordr.resources.helpers import BaseChildResource
resource = BaseChildResource(
request='some request',
name='a name',
parent='the parent'
)
with pytest.raises(NotImplementedError):
resource.__acl__()
def test_base_child_prepare_form():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
import deform
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
form = resource._prepare_form(RegistrationSchema)
assert isinstance(form, deform.Form)
assert form.action == 'http://example.com//'
assert len(form.buttons) == 0
def test_base_child_prepare_form_url():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
form = resource._prepare_form(RegistrationSchema, url='/foo')
assert form.action == '/foo'
def test_base_child_prepare_form_settings():
from ordr.resources.helpers import BaseChildResource
from ordr.schemas.account import RegistrationSchema
import deform
parent = DummyResource()
request = DummyRequest()
resource = BaseChildResource(request, 'a name', parent)
settings = {'buttons': ('ok', 'cancel')}
form = resource._prepare_form(RegistrationSchema, **settings)
assert len(form.buttons) == 2
assert isinstance(form.buttons[0], deform.Button)
assert isinstance(form.buttons[1], deform.Button)

4
tests/resources/root.py

@ -22,9 +22,9 @@ def test_root_getitem():
from ordr.resources import RootResource from ordr.resources import RootResource
from ordr.resources.account import RegistrationResource from ordr.resources.account import RegistrationResource
root = RootResource(None) root = RootResource(None)
child = root['registration'] child = root['register']
assert isinstance(child, RegistrationResource) assert isinstance(child, RegistrationResource)
assert child.__name__ == 'registration' assert child.__name__ == 'register'
assert child.__parent__ == root assert child.__parent__ == root
assert child.request == root.request assert child.request == root.request

28
tests/schemas/__init__.py

@ -0,0 +1,28 @@
''' Test package for ordr.schemas '''
from pyramid.testing import DummyRequest, DummyResource
def test_csrf_schema_form_with_custom_url():
''' test for creation with custom url '''
from ordr.schemas import CSRFSchema
request = DummyRequest()
form = CSRFSchema.as_form(request, url='/Nudge/Nudge')
assert form.action == '/Nudge/Nudge'
assert form.buttons == []
def test_csrf_schema_form_with_automatic_url():
''' test for creation with custom url '''
from ordr.schemas import CSRFSchema
root = DummyResource()
context = DummyResource('Crunchy', root)
request = DummyRequest(context=context, view_name='Frog')
form = CSRFSchema.as_form(request, buttons=['submit'])
assert 'http://example.com/Crunchy/Frog' == form.action
assert len(form.buttons) == 1
assert form.buttons[0].type == 'submit'

165
tests/schemas/helpers.py

@ -0,0 +1,165 @@
''' Tests for ordr.schemas.helpers '''
import pytest
from pyramid.testing import DummyRequest, DummyResource
from .. import app_config, dbsession, get_example_user # noqa: F401
def test_deferred_csrf_default():
''' deferred_csrf_default should return a csrf token '''
from ordr.schemas.helpers import deferred_csrf_default
from pyramid.csrf import get_csrf_token
request = DummyRequest()
token = deferred_csrf_default(None, {'request': request})
assert token == get_csrf_token(request)
def test_deferred_csrf_validator_ok():
''' test deferred_csrf_validator with valid csrf token '''
from ordr.schemas.helpers import deferred_csrf_validator
from pyramid.csrf import get_csrf_token
request = DummyRequest()
token = get_csrf_token(request)
request.POST = {'csrf_token': token}
validation_func = deferred_csrf_validator(None, {'request': request})
assert validation_func(None, None) is None
@pytest.mark.parametrize('post', [{}, {'csrf_token': 'Albatross!'}])
def test_deferred_csrf_validator_fails_on_no_csrf_token(post):
''' test deferred_csrf_validator with invalid or missing csrf token '''
from ordr.schemas.helpers import deferred_csrf_validator
from colander import Invalid
request = DummyRequest()
request.POST = post
validation_func = deferred_csrf_validator(None, {'request': request})
with pytest.raises(Invalid):
assert validation_func(None, None) is None
def test_deferred_unique_username_validator_ok(dbsession): # noqa: F811
''' unknown usernames should not raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_username_validator
from ordr.models.account import Role
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.USER)
dbsession.add(user)
validation_func = deferred_unique_username_validator(
None,
{'request': request}
)
assert validation_func(None, 'AnneElk') is None
def test_deferred_unique_username_validator_fails(dbsession): # noqa: F811
''' known username should raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_username_validator
from ordr.models.account import Role
from colander import Invalid
request = DummyRequest(dbsession=dbsession)
user = get_example_user(Role.USER)
dbsession.add(user)
validation_func = deferred_unique_username_validator(
None,
{'request': request}
)
with pytest.raises(Invalid):
assert validation_func(None, 'TerryGilliam') is None
def test_deferred_unique_email_validator_ok(dbsession): # noqa: F811
''' unknown emails should not raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.models.account import Role
context = DummyResource(model=None)
request = DummyRequest(dbsession=dbsession, context=context)
user = get_example_user(Role.USER)
dbsession.add(user)
validation_func = deferred_unique_email_validator(
None,
{'request': request}
)
assert validation_func(None, 'elk@example.com') is None
def test_deferred_unique_email_validator_ok_same_user(dbsession): # noqa: F811
''' known emails of a user might not raise an error
if a user is edited and the mail address is not change, no invalidation
error should be raised
'''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.models.account import Role
user = get_example_user(Role.USER)
context = DummyResource(model=user)
request = DummyRequest(dbsession=dbsession, context=context)
dbsession.add(user)
validation_func = deferred_unique_email_validator(
None,
{'request': request}
)
assert validation_func(None, user.email) is None
@pytest.mark.parametrize( # noqa: F811
'email', ['', 'gilliam@example.com', 'malformed']
)
def test_deferred_unique_email_validator_fails(dbsession, email):
''' known, empty or malformed emails should raise an invalidation error '''
from ordr.schemas.helpers import deferred_unique_email_validator
from ordr.models.account import Role
from colander import Invalid
context = DummyResource(model=None)
request = DummyRequest(dbsession=dbsession, context=context)
user = get_example_user(Role.USER)
dbsession.add(user)
validation_func = deferred_unique_email_validator(
None,
{'request': request}
)
with pytest.raises(Invalid):
assert validation_func(None, email) is None
def test_deferred_password_validator_ok():
''' correct password should not raise invalidation error '''
from ordr.schemas.helpers import deferred_password_validator
from ordr.models.account import Role
user = get_example_user(Role.USER)
request = DummyRequest(user=user)
validation_func = deferred_password_validator(None, {'request': request})
assert validation_func(None, 'Terry') is None
def test_deferred_password_validator_fails():
''' incorrect password should raise invalidation error '''
from ordr.schemas.helpers import deferred_password_validator
from ordr.models.account import Role
from colander import Invalid
user = get_example_user(Role.USER)
request = DummyRequest(user=user)
validation_func = deferred_password_validator(None, {'request': request})
with pytest.raises(Invalid):
assert validation_func(None, 'Wrong Password') is None

12
tests/views/registration.py

@ -0,0 +1,12 @@
import pytest
from pyramid.httpexceptions import HTTPFound
from pyramid.testing import DummyRequest
from .. import app_config, dbsession # noqa: F401
def test_faq():
from ordr.views.registration import registration_form
result = registration_form(None, None)
assert result == {}