Holger Frey
7 years ago
18 changed files with 511 additions and 29 deletions
@ -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 |
@ -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']) |
@ -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() |
||||||
|
) |
@ -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 |
@ -0,0 +1,5 @@ |
|||||||
|
{% extends "ordr:templates/layout.jinja2" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
{{ context.get_registration_form().render()|safe }} |
||||||
|
{% endblock content %} |
@ -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 {} |
@ -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' |
@ -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) |
@ -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' |
@ -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 |
@ -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 == {} |
Reference in new issue