Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
Holger Frey | f8d6d475d1 | 7 years ago |
Holger Frey | 5cfd68e85e | 7 years ago |
Holger Frey | 0a2f7a5832 | 7 years ago |
Holger Frey | 5d79bd34d6 | 7 years ago |
Holger Frey | 8dbf43ea99 | 7 years ago |
Holger Frey | 7668ecfc88 | 7 years ago |
Holger Frey | 8503f27c66 | 7 years ago |
Holger Frey | 1c96f9b970 | 7 years ago |
Holger Frey | b19368bc2f | 7 years ago |
Holger Frey | de2dbd352a | 7 years ago |
Holger Frey | 494957aeba | 7 years ago |
Holger Frey | 329c268e37 | 7 years ago |
Holger Frey | f63e337dde | 7 years ago |
Holger Frey | bbf89ad3a5 | 7 years ago |
Holger Frey | 20032eede1 | 7 years ago |
Holger Frey | c8c0c4678a | 7 years ago |
Holger Frey | 551cc260e2 | 7 years ago |
Holger Frey | 65ad7738ff | 7 years ago |
Holger Frey | 53f36e8566 | 7 years ago |
Holger Frey | 0d83eac6c1 | 7 years ago |
Holger Frey | a21612cf0e | 7 years ago |
Holger Frey | 8f80a7ce37 | 7 years ago |
Holger Frey | 5f7f26b3b6 | 7 years ago |
Holger Frey | 2be8692625 | 7 years ago |
Holger Frey | a0b50a81d6 | 7 years ago |
Holger Frey | 8b2c6a1e24 | 7 years ago |
Holger Frey | 3b9246633a | 7 years ago |
Holger Frey | 9cc02f5e4e | 7 years ago |
Holger Frey | 04fea4e7cd | 7 years ago |
Holger Frey | 93dc403dd6 | 7 years ago |
Holger Frey | bd8e3557aa | 7 years ago |
Holger Frey | d154d74c4a | 7 years ago |
Holger Frey | 039e1d111d | 7 years ago |
Holger Frey | 8b1441dd34 | 7 years ago |
Holger Frey | 5e7702cf04 | 7 years ago |
Holger Frey | fb0dab46a4 | 7 years ago |
Holger Frey | a93b5239e5 | 7 years ago |
Holger Frey | fd712e4e5a | 7 years ago |
87 changed files with 5417 additions and 1 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
[run] |
||||
source = ordr |
||||
omit = ordr/test* |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
include *.txt *.ini *.cfg *.rst |
||||
recursive-include ordr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
.PHONY: clean clean-test clean-pyc clean-build help |
||||
.DEFAULT_GOAL := help |
||||
|
||||
define BROWSER_PYSCRIPT |
||||
import os, webbrowser, sys |
||||
|
||||
try: |
||||
from urllib import pathname2url |
||||
except: |
||||
from urllib.request import pathname2url |
||||
|
||||
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) |
||||
endef |
||||
export BROWSER_PYSCRIPT |
||||
|
||||
define PRINT_HELP_PYSCRIPT |
||||
import re, sys |
||||
|
||||
for line in sys.stdin: |
||||
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) |
||||
if match: |
||||
target, help = match.groups() |
||||
print("%-20s %s" % (target, help)) |
||||
endef |
||||
export PRINT_HELP_PYSCRIPT |
||||
|
||||
BROWSER := python -c "$$BROWSER_PYSCRIPT" |
||||
|
||||
help: |
||||
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) |
||||
|
||||
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||
|
||||
clean-build: ## remove build artifacts
|
||||
rm -fr build/ |
||||
rm -fr dist/ |
||||
rm -fr .eggs/ |
||||
find . -name '*.egg-info' -exec rm -fr {} + |
||||
find . -name '*.egg' -exec rm -f {} + |
||||
|
||||
clean-pyc: ## remove Python file artifacts
|
||||
find . -name '*.pyc' -exec rm -f {} + |
||||
find . -name '*.pyo' -exec rm -f {} + |
||||
find . -name '*~' -exec rm -f {} + |
||||
find . -name '__pycache__' -exec rm -fr {} + |
||||
|
||||
clean-test: ## remove test and coverage artifacts
|
||||
rm -fr .tox/ |
||||
rm -f .coverage |
||||
rm -fr htmlcov/ |
||||
|
||||
lint: ## check style with flake8
|
||||
flake8 ordr tests |
||||
|
||||
test: ## run tests quickly with the default Python, ignoring functional tests
|
||||
py.test -x |
||||
|
||||
coverage: ## check code coverage quickly with the default Python
|
||||
coverage run --source ordr -m pytest --ignore tests/_functional/ |
||||
coverage report -m --omit=ordr/scripts/* |
||||
coverage html --omit=ordr/scripts/* |
||||
$(BROWSER) htmlcov/index.html |
||||
|
||||
fcoverage: ## check code coverage with the default Python and functional tests
|
||||
coverage run --source ordr -m pytest |
||||
coverage report -m |
||||
coverage html |
||||
$(BROWSER) htmlcov/index.html |
||||
|
||||
release: clean ## package and upload a release
|
||||
python setup.py sdist upload |
||||
python setup.py bdist_wheel upload |
||||
|
||||
dist: clean ## builds source and wheel package
|
||||
python setup.py sdist |
||||
python setup.py bdist_wheel |
||||
ls -l dist |
||||
|
||||
install: clean ## install the package to the active Python's site-packages
|
||||
python setup.py install |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
[[source]] |
||||
|
||||
url = "https://pypi.python.org/simple" |
||||
verify_ssl = true |
||||
name = "pypi" |
||||
|
||||
|
||||
[packages] |
||||
|
||||
|
||||
|
||||
[dev-packages] |
||||
|
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
Ordr |
||||
==== |
||||
|
||||
This is a complete redo of the Ordr project. |
||||
|
||||
|
||||
Getting Started |
||||
--------------- |
||||
|
||||
- Change directory into your newly created project. |
||||
|
||||
cd ordr |
||||
|
||||
- Create a Python virtual environment. |
||||
|
||||
python3 -m venv env |
||||
|
||||
- Upgrade packaging tools. |
||||
|
||||
env/bin/pip install --upgrade pip setuptools |
||||
|
||||
- Install the project in editable mode with its testing requirements. |
||||
|
||||
env/bin/pip install -e ".[testing]" |
||||
|
||||
- Configure the database. |
||||
|
||||
env/bin/initialize_ordr_db development.ini |
||||
|
||||
- Run your project's tests. |
||||
|
||||
env/bin/pytest |
||||
|
||||
- Run your project. |
||||
|
||||
env/bin/pserve development.ini |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
### |
||||
# app configuration |
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html |
||||
### |
||||
|
||||
[app:main] |
||||
use = egg:ordr |
||||
|
||||
pyramid.reload_templates = true |
||||
pyramid.debug_authorization = false |
||||
pyramid.debug_notfound = false |
||||
pyramid.debug_routematch = false |
||||
pyramid.default_locale_name = en |
||||
pyramid.includes = |
||||
pyramid_mailer.debug |
||||
pyramid_debugtoolbar |
||||
pyramid_listing |
||||
|
||||
sqlalchemy.url = sqlite:///%(here)s/ordr.sqlite |
||||
|
||||
retry.attempts = 3 |
||||
|
||||
auth.secret = 'Change Me 1' |
||||
session.secret = 'Change Me 2' |
||||
session.auto_csrf = true |
||||
static_views.cache_max_age = 0 |
||||
|
||||
# passlib settings |
||||
# setup the context to support only argon2 for the moment |
||||
passlib.schemes = argon2 bcrypt |
||||
# default encryption scheme is argon2 |
||||
passlib.default = argon2 |
||||
# flag every encryption method as deprecated except the first one |
||||
passlib.deprecated = auto |
||||
|
||||
# time a user token is valid (in minutes) |
||||
token_expiry.change_email = 120 |
||||
token_expiry.registration = 120 |
||||
token_expiry.reset_password = 120 |
||||
|
||||
|
||||
# email delivery |
||||
mail.host = localhost |
||||
mail.port = 2525 |
||||
mail.default_sender = ordr@example.com |
||||
|
||||
# By default, the toolbar only appears for clients from IP addresses |
||||
# '127.0.0.1' and '::1'. |
||||
# debugtoolbar.hosts = 127.0.0.1 ::1 |
||||
|
||||
### |
||||
# wsgi server configuration |
||||
### |
||||
|
||||
[server:main] |
||||
use = egg:waitress#main |
||||
listen = localhost:6543 |
||||
|
||||
### |
||||
# logging configuration |
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html |
||||
### |
||||
|
||||
[loggers] |
||||
keys = root, ordr, sqlalchemy |
||||
|
||||
[handlers] |
||||
keys = console |
||||
|
||||
[formatters] |
||||
keys = generic |
||||
|
||||
[logger_root] |
||||
level = INFO |
||||
handlers = console |
||||
|
||||
[logger_ordr] |
||||
level = DEBUG |
||||
handlers = |
||||
qualname = ordr |
||||
|
||||
[logger_sqlalchemy] |
||||
level = INFO |
||||
handlers = |
||||
qualname = sqlalchemy.engine |
||||
# "level = INFO" logs SQL queries. |
||||
# "level = DEBUG" logs SQL queries and results. |
||||
# "level = WARN" logs neither. (Recommended for production systems.) |
||||
|
||||
[handler_console] |
||||
class = StreamHandler |
||||
args = (sys.stderr,) |
||||
level = NOTSET |
||||
formatter = generic |
||||
|
||||
[formatter_generic] |
||||
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
from pyramid.config import Configurator |
||||
from pyramid.session import SignedCookieSessionFactory |
||||
|
||||
|
||||
__version__ = '0.0.1' |
||||
|
||||
|
||||
def main(global_config, **settings): # pragma: no cover |
||||
''' This function returns a Pyramid WSGI application. ''' |
||||
config = Configurator(settings=settings) |
||||
|
||||
session_factory = SignedCookieSessionFactory(settings['session.secret']) |
||||
config.set_session_factory(session_factory) |
||||
config.set_default_csrf_options(require_csrf=settings['session.auto_csrf']) |
||||
|
||||
config.include('pyramid_jinja2') |
||||
config.include('.models') |
||||
config.include('.resources') |
||||
config.include('.schemas') |
||||
config.include('.security') |
||||
config.include('.views') |
||||
|
||||
config.scan() |
||||
|
||||
return config.make_wsgi_app() |
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
''' custom events and event subsribers ''' |
||||
|
||||
from pyramid.events import subscriber |
||||
from pyramid.renderers import render |
||||
from pyramid_mailer import get_mailer |
||||
from pyramid_mailer.message import Message |
||||
|
||||
|
||||
# custom events |
||||
|
||||
class UserNotification(object): |
||||
''' base class for user notification emails |
||||
|
||||
:param pyramid.request.Request request: current request object |
||||
:param ordr.models.account.Users account: send notification to this user |
||||
:param dict data: additional data to pass to the mail template |
||||
:param str send_to: optional email address overriding user's email address |
||||
''' |
||||
|
||||
#: subject of the notification |
||||
subject = None |
||||
|
||||
#: template to render |
||||
template = None |
||||
|
||||
def __init__(self, request, account, data=None, send_to=None): |
||||
self.request = request |
||||
self.account = account |
||||
self.data = data |
||||
self.send_to = send_to or account.email |
||||
|
||||
|
||||
class ActivationNotification(UserNotification): |
||||
''' user notification for account activation ''' |
||||
|
||||
subject = '[ordr] Your account was activated' |
||||
template = 'ordr:templates/emails/activation.jinja2' |
||||
|
||||
|
||||
class ChangeEmailNotification(UserNotification): |
||||
''' user notification for verifying a change of the mail address ''' |
||||
|
||||
subject = '[ordr] Verify New Email Address' |
||||
template = 'ordr:templates/emails/email_change.jinja2' |
||||
|
||||
|
||||
class OrderStatusNotification(UserNotification): |
||||
''' user notification for order status change ''' |
||||
|
||||
subject = '[ordr] Order Status Change' |
||||
template = 'ordr:templates/emails/order.jinja2' |
||||
|
||||
|
||||
class PasswordResetNotification(UserNotification): |
||||
''' user notification for password reset link ''' |
||||
|
||||
subject = '[ordr] Password Reset' |
||||
template = 'ordr:templates/emails/password_reset.jinja2' |
||||
|
||||
|
||||
class RegistrationNotification(UserNotification): |
||||
''' user notification for account activation ''' |
||||
|
||||
subject = '[ordr] Please verify your email address' |
||||
template = 'ordr:templates/emails/registration.jinja2' |
||||
|
||||
|
||||
# subsribers for events |
||||
|
||||
@subscriber(UserNotification) |
||||
def notify_user(event): |
||||
''' notify a user about an event ''' |
||||
body = render( |
||||
event.template, |
||||
{'user': event.account, 'data': event.data}, |
||||
event.request |
||||
) |
||||
message = Message( |
||||
subject=event.subject, |
||||
sender=event.request.registry.settings['mail.default_sender'], |
||||
recipients=[event.send_to], |
||||
html=body |
||||
) |
||||
mailer = get_mailer(event.request.registry) |
||||
mailer.send(message) |
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
from sqlalchemy import engine_from_config |
||||
from sqlalchemy.orm import sessionmaker |
||||
from sqlalchemy.orm import configure_mappers |
||||
import zope.sqlalchemy |
||||
|
||||
# import or define all models here to ensure they are attached to the |
||||
# Base.metadata prior to any initialization routines |
||||
from .account import Role, Token, TokenSubject, User # flake8: noqa |
||||
|
||||
# run configure_mappers after defining all of the models to ensure |
||||
# all relationships can be setup |
||||
configure_mappers() |
||||
|
||||
|
||||
def get_engine(settings, prefix='sqlalchemy.'): |
||||
return engine_from_config(settings, prefix) |
||||
|
||||
|
||||
def get_session_factory(engine): |
||||
factory = sessionmaker() |
||||
factory.configure(bind=engine) |
||||
return factory |
||||
|
||||
|
||||
def get_tm_session(session_factory, transaction_manager): |
||||
''' |
||||
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. |
||||
|
||||
This function will hook the session to the transaction manager which |
||||
will take care of committing any changes. |
||||
|
||||
- When using pyramid_tm it will automatically be committed or aborted |
||||
depending on whether an exception is raised. |
||||
|
||||
- When using scripts you should wrap the session in a manager yourself. |
||||
For example:: |
||||
|
||||
import transaction |
||||
|
||||
engine = get_engine(settings) |
||||
session_factory = get_session_factory(engine) |
||||
with transaction.manager: |
||||
dbsession = get_tm_session(session_factory, transaction.manager) |
||||
''' |
||||
dbsession = session_factory() |
||||
zope.sqlalchemy.register( |
||||
dbsession, transaction_manager=transaction_manager) |
||||
return dbsession |
||||
|
||||
|
||||
def includeme(config): # pragma: no cover |
||||
''' |
||||
Initialize the model for a Pyramid app. |
||||
|
||||
Activate this setup using ``config.include('ordr.models')``. |
||||
''' |
||||
settings = config.get_settings() |
||||
settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager' |
||||
|
||||
# use pyramid_tm to hook the transaction lifecycle to the request |
||||
config.include('pyramid_tm') |
||||
|
||||
# use pyramid_retry to retry a request when transient exceptions occur |
||||
config.include('pyramid_retry') |
||||
|
||||
session_factory = get_session_factory(get_engine(settings)) |
||||
config.registry['dbsession_factory'] = session_factory |
||||
|
||||
# make request.dbsession available for use in Pyramid |
||||
config.add_request_method( |
||||
# r.tm is the transaction manager used by pyramid_tm |
||||
lambda r: get_tm_session(session_factory, r.tm), |
||||
'dbsession', |
||||
reify=True |
||||
) |
@ -0,0 +1,234 @@
@@ -0,0 +1,234 @@
|
||||
''' Models for User Accounts and Roles ''' |
||||
|
||||
import enum |
||||
import uuid |
||||
|
||||
from datetime import datetime, timedelta |
||||
from pyramid import httpexceptions |
||||
from sqlalchemy import ( |
||||
Column, |
||||
Date, |
||||
DateTime, |
||||
Enum, |
||||
ForeignKey, |
||||
Integer, |
||||
Text, |
||||
Unicode |
||||
) |
||||
from sqlalchemy.orm import relationship |
||||
|
||||
from .meta import Base, JsonEncoder |
||||
|
||||
|
||||
# custom exceptions |
||||
|
||||
class TokenExpired(httpexceptions.HTTPGone): |
||||
pass |
||||
|
||||
|
||||
# enumerations |
||||
|
||||
class Role(enum.Enum): |
||||
''' roles of user accounts ''' |
||||
|
||||
UNVALIDATED = 'unvalidated' #: new user, email not validated |
||||
NEW = 'new' #: new user, email validated, not active |
||||
USER = 'user' #: standard user, may place and view orders |
||||
PURCHASER = 'purchaser' #: privileged user, may edit orders |
||||
ADMIN = 'admin' #: fully privileged user |
||||
INACTIVE = 'inactive' #: user that is no longer active ("deleted") |
||||
|
||||
@property |
||||
def principal(self): |
||||
''' returns the principal identifier of the role ''' |
||||
return 'role:' + self.name.lower() |
||||
|
||||
def __str__(self): |
||||
''' string representation ''' |
||||
return self.name.capitalize() |
||||
|
||||
|
||||
class TokenSubject(enum.Enum): |
||||
''' Email Token Subjects for user accounts ''' |
||||
|
||||
REGISTRATION = 'registration' #: validate email for new user |
||||
RESET_PASSWORD = 'reset_password' #: reset a forgotten password |
||||
CHANGE_EMAIL = 'change_email' #: validate an email change |
||||
|
||||
|
||||
# database driven models |
||||
|
||||
class User(Base): |
||||
''' A user of the application ''' |
||||
|
||||
__tablename__ = 'users' |
||||
|
||||
#: primary key |
||||
id = Column(Integer, primary_key=True) |
||||
|
||||
#: unique user name |
||||
username = Column(Text, nullable=False, unique=True) |
||||
|
||||
#: hashed password, see :mod:`ordr.security` |
||||
password_hash = Column(Text, nullable=False) |
||||
|
||||
#: role of the user, see :class:`ordr.models.account.Role` |
||||
role = Column(Enum(Role), nullable=False) |
||||
|
||||
first_name = Column(Text, nullable=False) |
||||
last_name = Column(Text, nullable=False) |
||||
email = Column(Text, nullable=False, unique=True) |
||||
date_created = Column(Date, nullable=False, default=datetime.utcnow) |
||||
|
||||
#: tokens for new user registration and forgotten passwords |
||||
tokens = relationship( |
||||
'Token', |
||||
back_populates='owner', |
||||
cascade="all, delete-orphan" |
||||
) |
||||
|
||||
@property |
||||
def principal(self): |
||||
''' returns the principal identifier for the user ''' |
||||
return 'user:{}'.format(self.id) |
||||
|
||||
@property |
||||
def principals(self): |
||||
''' returns all principal identifiers for the user including roles ''' |
||||
principals = [self.principal, self.role.principal] |
||||
if self.role is Role.PURCHASER: |
||||
# a purchaser is also a user |
||||
principals.append(Role.USER.principal) |
||||
elif self.role is Role.ADMIN: |
||||
# an admin is also a purchaser and a user |
||||
principals.append(Role.PURCHASER.principal) |
||||
principals.append(Role.USER.principal) |
||||
return principals |
||||
|
||||
@property |
||||
def is_active(self): |
||||
''' is true if the user has an active role ''' |
||||
return self.role in {Role.USER, Role.PURCHASER, Role.ADMIN} |
||||
|
||||
def set_password(self, password): |
||||
''' hashes a password using :mod:`ordr.security.passlib_context` ''' |
||||
from ordr.security import password_context |
||||
self.password_hash = password_context.hash(password) |
||||
|
||||
def check_password(self, password): |
||||
''' checks a password against a stored password hash |
||||
|
||||
if the password algorithm is considered deprecated, the stored hash |
||||
will be updated using the current algorithm |
||||
''' |
||||
from ordr.security import password_context |
||||
ok, new_hash = password_context.verify_and_update( |
||||
password, |
||||
self.password_hash |
||||
) |
||||
|
||||
if not ok: |
||||
# password does not match, return False |
||||
return False |
||||
elif new_hash: |
||||
# algorithm is deprecated, update hash with new algorithm |
||||
self.password_hash = new_hash |
||||
|
||||
# password match, return True |
||||
return True |
||||
|
||||
def issue_token(self, request, subject, payload=None): |
||||
''' issues a token for mail change, password reset or user verification |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param ordr.models.account.TokenSubject subject: kind of token |
||||
:param payload: extra data to store with the token, JSON serializable |
||||
:rtype: (str) unique hash to access the token |
||||
''' |
||||
return Token.issue(request, self, subject, payload) |
||||
|
||||
def __str__(self): |
||||
''' string representation ''' |
||||
return str(self.username) |
||||
|
||||
|
||||
class Token(Base): |
||||
''' Tokens for mail change, account verification and password reset ''' |
||||
|
||||
__tablename__ = 'tokens' |
||||
|
||||
#: hash identifyer of the token |
||||
hash = Column(Unicode, primary_key=True) |
||||
|
||||
#: :class:`ordr.models.account.TokenSubject` |
||||
subject = Column(Enum(TokenSubject), nullable=False) |
||||
|
||||
#: token expires at this date and time |
||||
expires = Column(DateTime, nullable=False) |
||||
|
||||
#: additional data to attach to a token |
||||
payload = Column(JsonEncoder, nullable=True) |
||||
|
||||
#: the user_id the token belongs to |
||||
owner_id = Column(Integer, ForeignKey('users.id')) |
||||
|
||||
#: the user the token belongs to |
||||
owner = relationship('User', back_populates='tokens') |
||||
|
||||
@classmethod |
||||
def issue(cls, request, owner, subject, payload=None): |
||||
''' issues a token for password reset or user verification |
||||
|
||||
if the expiry keys for the token is not set in the app configuration, |
||||
the token will expire in five minutes. |
||||
|
||||
to set the expiry time in the conig use `token_expiry.` prefix followed |
||||
by the lowercase name of the token subject and a time in minutes. For |
||||
example, to give an active user two hours time to verify a registration |
||||
use `token_expiry.registration = 120` |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param ordr2.models.account.TokenSubject subject: kind of token |
||||
:param ordr2.models.account.User owner: account the token is issued for |
||||
:param payload: extra data to store with the token, JSON serializable |
||||
:rtype: ordr2.models.account.Token |
||||
''' |
||||
settings_key = 'token_expiry.' + subject.name.lower() |
||||
minutes = request.registry.settings.get(settings_key, 5) |
||||
expires = datetime.utcnow() + timedelta(minutes=int(minutes)) |
||||
return cls( |
||||
hash=uuid.uuid4().hex, |
||||
subject=subject, |
||||
payload=payload, |
||||
owner=owner, |
||||
expires=expires |
||||
) |
||||
|
||||
@classmethod |
||||
def retrieve(cls, request, hash, subject=None): |
||||
''' returns a token from the database |
||||
|
||||
The database is queried for a token with the given hash. If an |
||||
optional subject is given, the query will search only for tokens of |
||||
this kind. |
||||
|
||||
The method will return None if a token could not be found or the |
||||
token has already expired. If the token has expired, it will be deleted |
||||
from the database |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str hash: token hash |
||||
:param ordr2.models.account.TokenSubject subject: kind of token |
||||
:rtype: ordr2.models.account.Token or None |
||||
''' |
||||
query = request.dbsession.query(cls).filter_by(hash=hash) |
||||
if subject: |
||||
query = query.filter_by(subject=subject) |
||||
token = query.first() |
||||
|
||||
if token is None: |
||||
return None |
||||
elif token.expires < datetime.utcnow(): |
||||
request.dbsession.delete(token) |
||||
raise TokenExpired('Token has expired') |
||||
return token |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
''' SQL conventions and custom Encoders ''' |
||||
|
||||
import json |
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base |
||||
from sqlalchemy.schema import MetaData |
||||
from sqlalchemy.types import TypeDecorator, Unicode |
||||
|
||||
# Recommended naming convention used by Alembic, as various different database |
||||
# providers will autogenerate vastly different names making migrations more |
||||
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html |
||||
NAMING_CONVENTION = { |
||||
"ix": "ix_%(column_0_label)s", |
||||
"uq": "uq_%(table_name)s_%(column_0_name)s", |
||||
"ck": "ck_%(table_name)s_%(constraint_name)s", |
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", |
||||
"pk": "pk_%(table_name)s" |
||||
} |
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION) |
||||
Base = declarative_base(metadata=metadata) |
||||
|
||||
|
||||
class JsonEncoder(TypeDecorator): |
||||
''' Custom type for storing data structures as json-encoded string. ''' |
||||
|
||||
impl = Unicode |
||||
|
||||
def process_bind_param(self, value, dialect): |
||||
''' inbound (to database) ''' |
||||
if value is not None: |
||||
value = json.dumps(value) |
||||
return value |
||||
|
||||
def process_result_value(self, value, dialect): |
||||
''' outbound (from database) ''' |
||||
if value is not None: |
||||
value = json.loads(value) |
||||
return value |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
''' Resources (sub) package, used to connect URLs to views ''' |
||||
|
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
|
||||
from .account import AccountResource |
||||
|
||||
|
||||
class RootResource: |
||||
''' The root resource for the application |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
''' |
||||
|
||||
nav_active = 'welcome' |
||||
|
||||
def __init__(self, request): |
||||
''' Create the root resource |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
''' |
||||
self.__name__ = None |
||||
self.__parent__ = None |
||||
self.request = request |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, Everyone, 'view'), DENY_ALL] |
||||
|
||||
def __getitem__(self, key): |
||||
''' retruns a child resource |
||||
|
||||
:param str key: name of the child resource |
||||
:returns: child resource |
||||
:raises: KeyError if child resource is not found |
||||
''' |
||||
map = { |
||||
'account': AccountResource, |
||||
} |
||||
child_class = map[key] |
||||
return child_class(name=key, parent=self) |
||||
|
||||
|
||||
def includeme(config): # pragma: no cover |
||||
''' |
||||
Initialize the resources for traversal in a Pyramid app. |
||||
|
||||
Activate this setup using ``config.include('ordr2.resources')``. |
||||
''' |
||||
config.set_root_factory(RootResource) |
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
''' Resources (sub) package, used to connect URLs to views ''' |
||||
|
||||
import deform |
||||
|
||||
from pyramid.security import Allow, Authenticated, Everyone, DENY_ALL |
||||
|
||||
from ordr.models.account import Token, TokenSubject |
||||
from ordr.schemas.account import ( |
||||
ChangePasswordSchema, |
||||
RegistrationSchema, |
||||
ResetPasswordSchema, |
||||
SettingsSchema |
||||
) |
||||
|
||||
|
||||
from .helpers import BaseChildResource |
||||
|
||||
|
||||
class RegistrationTokenResource(BaseChildResource): |
||||
''' Resource for vaildating a new registered user's email |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = 'registration' |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, Everyone, 'register'), DENY_ALL] |
||||
|
||||
|
||||
class RegistrationResource(BaseChildResource): |
||||
''' The resource for new user registration |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = 'registration' |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, Everyone, 'register'), DENY_ALL] |
||||
|
||||
def __getitem__(self, key): |
||||
''' returns a resource for a valid registration token ''' |
||||
token = Token.retrieve(self.request, key, TokenSubject.REGISTRATION) |
||||
if token is None: |
||||
raise KeyError(f'Token {key} not found') |
||||
return RegistrationTokenResource(name=key, parent=self, model=token) |
||||
|
||||
def get_registration_form(self, **kwargs): |
||||
''' returns the registration form''' |
||||
settings = { |
||||
'buttons': ( |
||||
deform.Button(name='create', title='Create Account'), |
||||
deform.Button( |
||||
title='Cancel', |
||||
type='link', |
||||
value=self.request.resource_url(self.request.root), |
||||
css_class='btn btn-outline-secondary' |
||||
) |
||||
), |
||||
} |
||||
settings.update(kwargs) |
||||
return self._prepare_form(RegistrationSchema, **settings) |
||||
|
||||
|
||||
class PasswordResetTokenResource(BaseChildResource): |
||||
''' Resource for the reset password link |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = None |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, Everyone, 'reset'), DENY_ALL] |
||||
|
||||
def get_reset_form(self, **kwargs): |
||||
''' returns password reset form ''' |
||||
settings = { |
||||
'buttons': ( |
||||
deform.Button(name='change', title='Set New Password'), |
||||
deform.Button(name='cancel', title='Cancel'), |
||||
) |
||||
} |
||||
settings.update(kwargs) |
||||
return self._prepare_form(ResetPasswordSchema, **settings) |
||||
|
||||
|
||||
class PasswordResetResource(BaseChildResource): |
||||
''' The resource for resetting a forgotten password |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = None |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, Everyone, 'reset'), DENY_ALL] |
||||
|
||||
def __getitem__(self, key): |
||||
''' returns a resource for a valid reset password token ''' |
||||
token = Token.retrieve(self.request, key, TokenSubject.RESET_PASSWORD) |
||||
if token is None: |
||||
raise KeyError(f'Token {key} not found') |
||||
return PasswordResetTokenResource(name=key, parent=self, model=token) |
||||
|
||||
|
||||
class ChangeEmailTokenResource(BaseChildResource): |
||||
''' Resource for changing the email address |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = None |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [(Allow, self.model.owner.principal, 'edit'), DENY_ALL] |
||||
|
||||
|
||||
class AccountResource(BaseChildResource): |
||||
''' The resource for changing account settings and passwords |
||||
|
||||
:param pyramid.request.Request request: the current request object |
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
''' |
||||
|
||||
nav_active = None |
||||
|
||||
def __init__(self, name, parent, model=None): |
||||
''' Create the resource |
||||
|
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
:param model: optional data model for the resource |
||||
|
||||
If model is not set, the current user will be used |
||||
''' |
||||
super().__init__(name, parent, model) |
||||
self.model = model or getattr(self.request, 'user', None) |
||||
|
||||
def __acl__(self): |
||||
''' access controll list for the resource ''' |
||||
return [ |
||||
(Allow, Everyone, 'view'), |
||||
(Allow, Everyone, 'login'), |
||||
(Allow, Everyone, 'logout'), |
||||
(Allow, Everyone, 'register'), |
||||
(Allow, Everyone, 'reset'), |
||||
(Allow, Authenticated, 'edit'), |
||||
DENY_ALL |
||||
] |
||||
|
||||
def __getitem__(self, key): |
||||
''' returns a resource for child resource ''' |
||||
# static child resources |
||||
map = { |
||||
'register': RegistrationResource, |
||||
'forgot': PasswordResetResource, |
||||
} |
||||
if key in map: |
||||
child_class = map[key] |
||||
return child_class(name=key, parent=self) |
||||
|
||||
# change email verification |
||||
token = Token.retrieve(self.request, key, TokenSubject.CHANGE_EMAIL) |
||||
if token is None: |
||||
raise KeyError(f'Token {key} not found') |
||||
return ChangeEmailTokenResource(name=key, parent=self, model=token) |
||||
|
||||
def get_settings_form(self, **kwargs): |
||||
''' returns the account settings form ''' |
||||
settings = { |
||||
'buttons': ( |
||||
deform.Button(name='change', title='Change Settings'), |
||||
deform.Button(name='cancel', title='Cancel'), |
||||
) |
||||
} |
||||
settings.update(kwargs) |
||||
return self._prepare_form(SettingsSchema, **settings) |
||||
|
||||
def get_password_form(self, **kwargs): |
||||
''' returns the change password form ''' |
||||
settings = { |
||||
'buttons': ( |
||||
deform.Button(name='change', title='Change Password'), |
||||
deform.Button(name='cancel', title='Cancel'), |
||||
) |
||||
} |
||||
settings.update(kwargs) |
||||
return self._prepare_form(ChangePasswordSchema, **settings) |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
''' Resources (sub) package, used to connect URLs to views ''' |
||||
|
||||
|
||||
class BaseChildResource: |
||||
|
||||
def __init__(self, name, parent, model=None): |
||||
''' Create a child resource |
||||
|
||||
:param str name: the name of the resource |
||||
:param parent: the parent resouce |
||||
:param model: optional data model for the resource |
||||
''' |
||||
self.__name__ = name |
||||
self.__parent__ = parent |
||||
self.request = parent.request |
||||
self.model = model |
||||
|
||||
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 @@
@@ -0,0 +1,53 @@
|
||||
''' Schemas (sub) package, for form rendering and validation ''' |
||||
|
||||
import colander |
||||
import deform |
||||
|
||||
from deform.renderer import configure_zpt_renderer |
||||
|
||||
from .validators 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, action=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. |
||||
''' |
||||
schema = cls().bind(request=request) |
||||
if action is None: |
||||
action = request.resource_url(request.context, request.view_name) |
||||
settings = {'col_label': 3, 'col_input': 9, 'action': action} |
||||
settings.update(kwargs) |
||||
return deform.Form(schema, **settings) |
||||
|
||||
|
||||
def includeme(config): # pragma: no cover |
||||
''' |
||||
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,101 @@
@@ -0,0 +1,101 @@
|
||||
import colander |
||||
import deform |
||||
|
||||
from . import CSRFSchema |
||||
|
||||
from .validators import ( |
||||
deferred_unique_email_validator, |
||||
deferred_unique_username_validator, |
||||
deferred_password_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, |
||||
oid='registration_username' |
||||
) |
||||
|
||||
first_name = colander.SchemaNode( |
||||
colander.String(), |
||||
oid='registration_first_name' |
||||
) |
||||
|
||||
last_name = colander.SchemaNode( |
||||
colander.String(), |
||||
oid='registration_last_name' |
||||
) |
||||
|
||||
email = colander.SchemaNode( |
||||
colander.String(), |
||||
validator=deferred_unique_email_validator |
||||
) |
||||
|
||||
password = colander.SchemaNode( |
||||
colander.String(), |
||||
widget=deform.widget.CheckedPasswordWidget(), |
||||
validator=colander.Length(min=8) |
||||
) |
||||
|
||||
|
||||
class ResetPasswordSchema(CSRFSchema): |
||||
''' reset a forgotten password ''' |
||||
|
||||
password = colander.SchemaNode( |
||||
colander.String(), |
||||
widget=deform.widget.CheckedPasswordWidget(), |
||||
validator=colander.Length(min=8) |
||||
) |
||||
|
||||
|
||||
class SettingsSchema(CSRFSchema): |
||||
''' new user registration ''' |
||||
|
||||
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 |
||||
) |
||||
|
||||
confirmation = colander.SchemaNode( |
||||
colander.String(), |
||||
widget=deform.widget.PasswordWidget(), |
||||
validator=deferred_password_validator |
||||
) |
||||
|
||||
|
||||
class ChangePasswordSchema(CSRFSchema): |
||||
''' change the password ''' |
||||
|
||||
password = colander.SchemaNode( |
||||
colander.String(), |
||||
widget=deform.widget.CheckedPasswordWidget(), |
||||
validator=colander.Length(min=8) |
||||
) |
||||
|
||||
confirmation = colander.SchemaNode( |
||||
colander.String(), |
||||
widget=deform.widget.PasswordWidget(), |
||||
validator=deferred_password_validator |
||||
) |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
''' 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 is not None: |
||||
if user != getattr(request.context, 'model', None): |
||||
# 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,60 @@
@@ -0,0 +1,60 @@
|
||||
import os |
||||
import sys |
||||
import transaction |
||||
|
||||
from pyramid.paster import ( |
||||
get_appsettings, |
||||
setup_logging, |
||||
) |
||||
|
||||
from pyramid.scripts.common import parse_vars |
||||
|
||||
from urllib.parse import urlparse |
||||
|
||||
from ..models.meta import Base |
||||
from ..models import ( |
||||
get_engine, |
||||
get_session_factory, |
||||
get_tm_session, |
||||
) |
||||
from ..models import Role, User |
||||
|
||||
|
||||
def usage(argv): |
||||
cmd = os.path.basename(argv[0]) |
||||
print('usage: %s <config_uri> [var=value]\n' |
||||
'(example: "%s development.ini")' % (cmd, cmd)) |
||||
sys.exit(1) |
||||
|
||||
|
||||
def main(argv=sys.argv): |
||||
if len(argv) < 2: |
||||
usage(argv) |
||||
config_uri = argv[1] |
||||
options = parse_vars(argv[2:]) |
||||
setup_logging(config_uri) |
||||
settings = get_appsettings(config_uri, options=options) |
||||
|
||||
# remove an existing database |
||||
sqlalchemy_url = urlparse(settings['sqlalchemy.url']) |
||||
path = os.path.abspath(sqlalchemy_url.path) |
||||
if os.path.exists(path): |
||||
os.remove(path) |
||||
|
||||
engine = get_engine(settings) |
||||
Base.metadata.create_all(engine) |
||||
|
||||
session_factory = get_session_factory(engine) |
||||
|
||||
with transaction.manager: |
||||
dbsession = get_tm_session(session_factory, transaction.manager) |
||||
|
||||
account = User( |
||||
username='Holgi', |
||||
first_name='Holger', |
||||
last_name='Frey', |
||||
email='frey@imtek.de', |
||||
role=Role.ADMIN |
||||
) |
||||
account.set_password('test') |
||||
dbsession.add(account) |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
''' User Authentication and Authorization ''' |
||||
|
||||
from passlib.context import CryptContext |
||||
from pyramid.authentication import AuthTktAuthenticationPolicy |
||||
from pyramid.authorization import ACLAuthorizationPolicy |
||||
from pyramid.security import Authenticated, Everyone |
||||
from pyramid.settings import aslist |
||||
|
||||
from ordr.models.account import User |
||||
|
||||
#: passlib context for hashing passwords |
||||
# at least one scheme must be set in advance, will be overridden by the |
||||
# settings in the .ini file. |
||||
password_context = CryptContext(schemes=['argon2']) |
||||
|
||||
|
||||
class AuthenticationPolicy(AuthTktAuthenticationPolicy): |
||||
''' How to authenticate users ''' |
||||
|
||||
def authenticated_userid(self, request): |
||||
''' returns the id of an authenticated user |
||||
|
||||
heavy lifting done in get_user() attached to request |
||||
''' |
||||
user = request.user |
||||
if user is not None: |
||||
return user.id |
||||
|
||||
def effective_principals(self, request): |
||||
''' returns a list of principals for the user ''' |
||||
principals = [Everyone] |
||||
user = request.user |
||||
if user is not None: |
||||
principals.append(Authenticated) |
||||
principals.extend(user.principals) |
||||
return principals |
||||
|
||||
|
||||
def get_user(request): |
||||
''' retrieves the user object by the unauthenticated user id |
||||
|
||||
:param pyramid.request.Request request: |
||||
the current request object |
||||
:rtype: :class:`ordr.models.account.User` or None |
||||
''' |
||||
user_id = request.unauthenticated_userid |
||||
if user_id is not None: |
||||
user = request.dbsession.query(User).filter_by(id=user_id).first() |
||||
if user and user.is_active: |
||||
return user |
||||
return None |
||||
|
||||
|
||||
def crypt_context_settings_to_string(settings, prefix='passlib.'): |
||||
''' returns a passlib context setting as a INI-formatted content |
||||
|
||||
:param dict settings: settings for the crypt context |
||||
:param str prefix: prefix of the settings keys |
||||
:rtype: (str) config string in INI format for CryptContext.load() |
||||
|
||||
This looks at first like a dump hack, but the parsing of all possible |
||||
context settings is quite a task. Since passlib has a context parser |
||||
included, this seems the most reliable way to do it. |
||||
''' |
||||
config_lines = ['[passlib]'] |
||||
for ini_key, value in settings.items(): |
||||
if ini_key.startswith(prefix): |
||||
context_key = ini_key.replace(prefix, '') |
||||
# the pyramid .ini format is different on lists |
||||
# than the .ini format used by passlib. |
||||
if context_key in {'schemes', 'deprecated'} and ',' not in value: |
||||
value = ','.join(aslist(value)) |
||||
config_lines.append(f'{context_key} = {value}') |
||||
return '\n'.join(config_lines) |
||||
|
||||
|
||||
def includeme(config): # pragma: no cover |
||||
''' initializing authentication, authorization and password hash settings |
||||
|
||||
Activate this setup using ``config.include('ordr.security')``. |
||||
''' |
||||
settings = config.get_settings() |
||||
|
||||
# configure the passlib context manager for hashing user passwords |
||||
config_str = crypt_context_settings_to_string(settings, prefix='passlib.') |
||||
password_context.load(config_str) |
||||
|
||||
# config for authentication and authorization |
||||
authn_policy = AuthenticationPolicy( |
||||
settings.get('auth.secret', ''), |
||||
hashalg='sha512', |
||||
) |
||||
config.set_authentication_policy(authn_policy) |
||||
config.set_authorization_policy(ACLAuthorizationPolicy()) |
||||
|
||||
# attach the get_user function returned by get_user_closure() |
||||
config.add_request_method(get_user, 'user', reify=True) |
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
$(function() { |
||||
|
||||
function capitalize(s){ |
||||
return s.replace( /\b./g, function(a){ return a.toUpperCase(); } ); |
||||
}; |
||||
|
||||
function generate_user_name() { |
||||
var first_name = $('#registration_first_name').val(); |
||||
var last_name = $('#registration_last_name').val(); |
||||
var user_name = capitalize(first_name) + capitalize(last_name); |
||||
return user_name.replace( /[\s-]/g, '') |
||||
}; |
||||
|
||||
// autocomplete of the username (registration form)
|
||||
$('#registration_first_name').keyup(function () { |
||||
$('#registration_username').val( generate_user_name() ); |
||||
}); |
||||
$('#registration_last_name').keyup(function() { |
||||
$('#registration_username').val( generate_user_name() ); |
||||
}); |
||||
}); |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Forgot Your Password?</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 1: Validate Account |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 2: Change Password |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h3>Password Reset Succesfull</h3> |
||||
<p class="mt-3">Your password has been changed.</p> |
||||
<p>You can now <a href="{{ request.resource_url(request.root) }}">log in</a> again.</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Forgot Your Password?</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 1: Validate Account |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 2: Change Password |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6 mt-3"> |
||||
<p>Please enter your mail address or your username to reset your password.</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center"> |
||||
<div class="col-6"> |
||||
<form action="{{request.resource_url(context)}}" method="POST"> |
||||
<div class="form-group form-row mt-3"> |
||||
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}"> |
||||
<input type="text" class="form-control {% if formerror %}is-invalid{% endif %}" id="input-username" placeholder="Mail Address or Username" name="identifier" autofocus="autofocus"> |
||||
{% if formerror %} |
||||
<div class="invalid-feedback"> |
||||
Username or email address unknown, or account is not activated. |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
<div class="form-group form-row mt-5"> |
||||
<button type="submit" name="send_mail" class="btn btn-primary mr-1">Send Reset Link</button> |
||||
<button type="submit" name="cancel" class="btn btn-outline-secondary">Cancel</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Forgot Your Password?</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 1: Validate Account |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 2: Change Password |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h3>Change your password</h3> |
||||
<p class="mt-3">{{ form.render()|safe }}</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Forgot Your Password?</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 1: Validate Account |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 2: Change Password |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h3>Verify Your Email Address</h3> |
||||
<p class="mt-3">To continue the process, an email has been sent to you.</p> |
||||
<p>Please follow the link in the email to verify your account.</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row mt-5"> |
||||
<div class="col-8 offset-2"> |
||||
<div class="jumbotron"> |
||||
<h1 class="display-4">Welcome to <span class="text-primary">ordr</span>!</h1> |
||||
<p class="lead">An order management system to simplify your shopping for laborartory supplies.</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col-4 offset-2"> |
||||
<h4 class="mb-4">Login</h4> |
||||
<form action="{{ request.resource_url(context, 'login') }}" method="POST"> |
||||
<div class="form-group"> |
||||
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}"> |
||||
<input type="text" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-username" placeholder="Username" name="username" autofocus="autofocus"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<input type="password" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-password" placeholder="Password" name="password"> |
||||
{% if loginerror %} |
||||
<div class="invalid-feedback"> |
||||
Username and password do not match, or account is not activated. |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Login</button> |
||||
<small class="float-right mt-2"><a href="/forgot">Forgot your password?</a></small> |
||||
</form> |
||||
</div> |
||||
<div class="col-4"> |
||||
<h4 class="mb-4">Register</h4> |
||||
<p> |
||||
Registration is easy as 1-2-3. |
||||
Just fill out the <a href="/register">form</a> and as soon as your |
||||
account has been activated you can start shopping. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Change Your Password</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center"> |
||||
<div class="col-6"> |
||||
<h3>Your password was changed successfully</h3> |
||||
<p class="mt-3">You can now log in with your new password.</p> |
||||
<p>Happy <a href="{{ request.resource_url(request.root) }}">ordering</a> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Change Your Password</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center"> |
||||
<div class="col-6 mt-3"> |
||||
{{ form.render()|safe }} |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block title %} Ordr | Registration {% endblock title %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Registration</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 1: Registration |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 2: Validate Email |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h3>Registration Completed</h3> |
||||
<p class="mt-3">Thank you for verifying your email address.</p> |
||||
<p>Before you can start ordering, an administrator must activate your account</p> |
||||
<p>You'll receive an email when your account is activated</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block title %} Ordr | Registration {% endblock title %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Registration</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 1: Registration |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 2: Validate Email |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
{{ form.render()|safe }} |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block title %} Ordr | Registration {% endblock title %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Registration</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 1: Registration |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-primary"> |
||||
Step 2: Validate Email |
||||
</p> |
||||
</div> |
||||
<div class="col-2"> |
||||
<p class="text-secondary"> |
||||
Step 3: Finished |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h3>Verify Your Email Address</h3> |
||||
<p class="mt-3">To complete the registration process an email has been sent to you.</p> |
||||
<p>Please follow the link in the email to verify your address and complete the registration process.</p> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Change Settings</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center"> |
||||
<div class="col-6 mt-3"> |
||||
{{ form.render()|safe }} |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-6"> |
||||
<h1>Change Settings</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row justify-content-md-center"> |
||||
<div class="col-6"> |
||||
<h3>Your email was changed successfully</h3> |
||||
<p class="mt-3">New notifications will be sent to {{request.user.email}}.</p> |
||||
<p>Happy <a href="{{ request.resource_url(request.root) }}">ordering</a> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
<div i18n:domain="deform" tal:omit-tag="" |
||||
tal:define="oid oid|field.oid; |
||||
name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
style style|field.widget.style; |
||||
required required|'required' if field.required else None; |
||||
was_validated True if field.get_root().error else False; |
||||
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'; |
||||
is_valid was_validated and not is_invalid; |
||||
"> |
||||
${field.start_mapping()} |
||||
<div> |
||||
<input type="password" |
||||
name="${name}" |
||||
value="${field.widget.redisplay and cstruct or ''}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''}; |
||||
style style; |
||||
required required;" |
||||
id="${oid}" |
||||
i18n:attributes="placeholder" |
||||
placeholder="Password"/> |
||||
</div> |
||||
<div class="mt-2"> |
||||
<input type="password" |
||||
name="${name}-confirm" |
||||
value="${field.widget.redisplay and confirm or ''}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''}; |
||||
style style; |
||||
required required;" |
||||
id="${oid}-confirm" |
||||
i18n:attributes="placeholder" |
||||
placeholder="Confirm Password"/> |
||||
|
||||
<!--! error message must directly follow input field for bootstrap 4 --> |
||||
<div class="invalid-feedback" |
||||
tal:define="errstr 'error-%s' % field.oid" |
||||
tal:repeat="msg field.error.messages()" |
||||
i18n:translate="" |
||||
tal:attributes="id repeat.msg.index==0 and errstr or |
||||
('%s-%s' % (errstr, repeat.msg.index))" |
||||
tal:condition="is_invalid"> |
||||
${msg} |
||||
</div> |
||||
</div> |
||||
${field.end_mapping()} |
||||
</div> |
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
<form |
||||
tal:define="style style|field.widget.style; |
||||
css_class css_class|string:${field.widget.css_class or field.css_class or ''}; |
||||
item_template item_template|field.widget.item_template; |
||||
autocomplete autocomplete|field.autocomplete; |
||||
title title|field.title; |
||||
errormsg errormsg|field.errormsg; |
||||
description description|field.description; |
||||
buttons buttons|field.buttons; |
||||
use_ajax use_ajax|field.use_ajax; |
||||
ajax_options ajax_options|field.ajax_options; |
||||
formid formid|field.formid; |
||||
action action|field.action or None; |
||||
method method|field.method; |
||||
col_label col_label|field.col_label; |
||||
col_input col_input|field.col_input; |
||||
was_validated True if field.get_root().error else False;" |
||||
|
||||
tal:attributes="autocomplete autocomplete; |
||||
style style; |
||||
class css_class; |
||||
action action;" |
||||
id="${formid}" |
||||
method="${method}" |
||||
enctype="multipart/form-data" |
||||
accept-charset="utf-8" |
||||
i18n:domain="deform" |
||||
> |
||||
|
||||
<fieldset class="deform-form-fieldset"> |
||||
|
||||
<legend tal:condition="title">${title}}</legend> |
||||
|
||||
<input type="hidden" name="_charset_" /> |
||||
<input type="hidden" name="__formid__" value="${formid}"/> |
||||
|
||||
<p class="section first" tal:condition="description"> |
||||
${description} |
||||
</p> |
||||
|
||||
<div tal:repeat="child field" |
||||
tal:replace="structure child.render_template(item_template)"/> |
||||
|
||||
<div class="form-row deform-form-buttons"> |
||||
<div class="col-${col_label}"></div> |
||||
<div class="form-group col-{$col_input} mt-4"> |
||||
<tal:loop tal:repeat="button buttons"> |
||||
<button |
||||
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-default';" |
||||
tal:attributes="disabled button.disabled if button.disabled else None" |
||||
id="${formid+button.name}" |
||||
name="${button.name}" |
||||
type="${button.type}" |
||||
class="btn ${button.css_class or btn_disposition}" |
||||
value="${button.value}" |
||||
tal:condition="button.type != 'link'"> |
||||
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span> |
||||
${button.title} |
||||
</button> |
||||
<a |
||||
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-default'; |
||||
btn_href button.value|''" |
||||
class="btn ${button.css_class or btn_disposition}" |
||||
id="${field.formid + button.name}" |
||||
href="${btn_href}" |
||||
tal:condition="button.type == 'link'"> |
||||
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span> |
||||
${button.title} |
||||
</a> |
||||
</tal:loop> |
||||
</div> |
||||
</div> |
||||
|
||||
</fieldset> |
||||
|
||||
<script type="text/javascript" tal:condition="use_ajax"> |
||||
$(function() { |
||||
// jquery handler for .ready() called |
||||
|
||||
deform.addCallback( |
||||
'${formid}', |
||||
function(oid) { |
||||
var target = '#' + oid; |
||||
var options = { |
||||
target: target, |
||||
replaceTarget: true, |
||||
success: function() { |
||||
deform.processCallbacks(); |
||||
deform.focusFirstInput(target); |
||||
}, |
||||
beforeSerialize: function() { |
||||
// See http://bit.ly/1agBs9Z (hack to fix tinymce-related ajax bug) |
||||
if ('tinymce' in window) { |
||||
$(tinymce.get()).each( |
||||
function(i, el) { |
||||
var content = el.getContent(); |
||||
var editor_input = document.getElementById(el.id); |
||||
editor_input.value = content; |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
var extra_options = ${ajax_options} || {}; |
||||
$('#' + oid).ajaxForm($.extend(options, extra_options)); |
||||
} |
||||
); |
||||
}); |
||||
</script> |
||||
|
||||
</form> |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
<div tal:define="error_class error_class|field.widget.error_class; |
||||
description description|field.description; |
||||
title title|field.title; |
||||
oid oid|field.oid; |
||||
hidden hidden|field.widget.hidden; |
||||
category category|field.widget.category; |
||||
structural hidden or category == 'structural'; |
||||
required required|'required' if field.required else None; |
||||
was_validated True if field.get_root().error else False; |
||||
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'; |
||||
col_label col_label|field.col_label; |
||||
col_input col_input|field.col_input;" |
||||
class="form-group form-row ${field.error and 'has-error' or ''} ${field.widget.item_css_class or ''} ${field.default_item_css_class()}" |
||||
title="${description}" |
||||
id="item-${oid}" |
||||
tal:omit-tag="structural" |
||||
i18n:domain="deform"> |
||||
|
||||
<label for="${oid}" |
||||
class="control-label col-${col_label} col-form-label ${required and 'required' or ''}" |
||||
tal:condition="not structural" |
||||
id="req-${oid}" |
||||
> |
||||
${title} |
||||
</label> |
||||
<div class="col-${col_input}"> |
||||
<div tal:define="input_prepend field.widget.input_prepend | None; |
||||
input_append field.widget.input_append | None" |
||||
tal:omit-tag="not (input_prepend or input_append)" |
||||
class="input-group"> |
||||
<div class="input-group-prepend" tal:condition="input_prepend"> |
||||
<div class="input-group-text">${input_prepend}</div> |
||||
</div> |
||||
<span tal:replace="structure field.serialize(cstruct).strip()"></span> |
||||
<div class="input-group-append" tal:condition="input_append"> |
||||
<div class="input-group-text">${input_append}</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="invalid-feedback" |
||||
tal:define="errstr 'error-%s' % field.oid" |
||||
tal:repeat="msg field.error.messages()" |
||||
i18n:translate="" |
||||
tal:attributes="id repeat.msg.index==0 and errstr or |
||||
('%s-%s' % (errstr, repeat.msg.index))" |
||||
tal:condition="is_invalid"> |
||||
${msg} |
||||
</div> |
||||
|
||||
<small tal:condition="field.description and not field.widget.hidden" |
||||
class="form-text text-muted" > |
||||
${field.description} |
||||
</small> |
||||
</div> |
||||
</div> |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
required required|'required' if field.required else None; |
||||
mask mask|field.widget.mask; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder; |
||||
style style|field.widget.style; |
||||
was_validated True if field.get_root().error else False; |
||||
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'; |
||||
is_valid was_validated and not is_invalid; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="password" name="${name}" value="${cstruct}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''}; |
||||
style style; |
||||
required required" |
||||
id="${oid}"/> |
||||
</span> |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
mask mask|field.widget.mask; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder; |
||||
style style|field.widget.style; |
||||
was_validated True if field.get_root().error else False; |
||||
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'; |
||||
is_valid was_validated and not is_invalid; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="text" name="${name}" value="${cstruct}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''}; |
||||
style style" |
||||
id="${oid}" |
||||
readonly="readonly"/> |
||||
</span> |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
required required|'required' if field.required else None; |
||||
mask mask|field.widget.mask; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder; |
||||
style style|field.widget.style; |
||||
was_validated True if field.get_root().error else False; |
||||
is_invalid is_invalid|field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'; |
||||
is_valid was_validated and not is_invalid; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="text" name="${name}" value="${cstruct}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if is_invalid else ''} ${'is-valid' if is_valid else ''}; |
||||
style style; |
||||
required required" |
||||
id="${oid}"/> |
||||
</span> |
@ -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 new 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 new 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> |
@ -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] reset your password</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> |
||||
To set a new password for the account "{{ user.username }}" follow 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> |
@ -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> |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block title %} Ordr | Error {% endblock title %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-8"> |
||||
<h1 class="mt-3">An Error has occured</h1> |
||||
<p class="mt-4">The page you are looking for could not be found</p> |
||||
<small class="text-secondary">404 - Page not found</small> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
||||
|
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block title %} Ordr | Error {% endblock title %} |
||||
|
||||
{% block content %} |
||||
<div class="row justify-content-md-center mt-3"> |
||||
<div class="col-8"> |
||||
<h1 class="mt-3">An Error has occured</h1> |
||||
<p class="mt-4">The link you've clicked has expired.</p> |
||||
<small class="text-secondary">410 - Gone</small> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
||||
|
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="{{request.locale_name}}"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> |
||||
<meta name="description" content="ordr"> |
||||
<meta name="author" content="IMTEk / CPI / Holger Frey"> |
||||
<link rel="shortcut icon" href="{{request.static_url('ordr:static/pyramid-16x16.png')}}"> |
||||
|
||||
<title>{% block title %} Ordr {% endblock title %}</title> |
||||
|
||||
<!-- Bootstrap core CSS --> |
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous"> |
||||
|
||||
<!-- Deform form renderin gcss --> |
||||
{# <link rel="stylesheet" href="{{request.static_url('deform:static/css/form.css')}}" type="text/css" media="screen" /> #} |
||||
|
||||
<!-- Custom styles for this scaffold --> |
||||
<link href="{{request.static_url('ordr:static/style.css')}}" rel="stylesheet"> |
||||
</head> |
||||
|
||||
<body> |
||||
<nav class="navbar navbar-dark bg-dark navbar-expand-sm"> |
||||
<a class="navbar-brand text-primary" href="{{ request.resource_url(request.root) }}"><strong>ordr</strong></a> |
||||
{% if not request.user %} |
||||
<ul class="navbar-nav mr-auto"> |
||||
<li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='login' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root) }}" class="nav-link">Welcome</a> |
||||
</li> |
||||
<li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='faq' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root, 'faq') }}" class="nav-link">FAQs</a> |
||||
</li> |
||||
<li class="nav-item {% if context.nav_active=='registration' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root, 'account', 'register') }}" class="nav-link">Register</a> |
||||
</li> |
||||
</ul> |
||||
{% else %} |
||||
<ul class="navbar-nav mr-auto"> |
||||
<li class="nav-item {% if context.nav_active=='orders' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root, 'orders') }}" class="nav-link">Orders</a> |
||||
</li> |
||||
<li class="nav-item {% if context.nav_active=='welcome' and request.view_name=='faq' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root, 'faq') }}" class="nav-link">FAQs</a> |
||||
</li> |
||||
{% if 'role:admin' in request.user.principals %} |
||||
<li class="nav-item {% if context.nav_active=='admin' %}active{% endif %}"> |
||||
<a href="{{ request.resource_url(request.root, 'admin') }}" class="nav-link">Admin</a> |
||||
</li> |
||||
{% endif %} |
||||
</ul> |
||||
<ul class="navbar-nav"> |
||||
<li class="nav-item dropdown"> |
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" > |
||||
{{ request.user }} |
||||
</a> |
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userDropdown"> |
||||
<a class="dropdown-item" href="{{ request.resource_url(request.root, 'account', 'logout') }}">Logout</a> |
||||
<div class="dropdown-divider"></div> |
||||
<a class="dropdown-item small" href="{{ request.resource_url(request.root, 'account', 'settings') }}">Settings</a> |
||||
<a class="dropdown-item small" href="{{ request.resource_url(request.root, 'account', 'password') }}">Change Password</a> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
{% endif %} |
||||
</nav> |
||||
<div class="container-fluid content"> |
||||
{% block content %} |
||||
<p>No content</p> |
||||
{% endblock content %} |
||||
</div> |
||||
|
||||
<!-- Bootstrap core JavaScript |
||||
================================================== --> |
||||
<!-- Placed at the end of the document so the pages load faster --> |
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> |
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> |
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> |
||||
<script src="{{request.static_url('ordr:static/scripts.js')}}"></script> |
||||
</body> |
||||
|
||||
</html> |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
{% extends "ordr:templates/layout.jinja2" %} |
||||
|
||||
{% block content %} |
||||
<div class="content"> |
||||
<h1>FAQ</h1> |
||||
<p class="lead">Welcome to <span class="font-normal">Ordr</span>, a Pyramid application generated by<br><span class="font-normal">Cookiecutter</span>.</p> |
||||
</div> |
||||
{% endblock content %} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
''' views (sub) package for ordr ''' |
||||
|
||||
|
||||
def includeme(config): # pragma: no cover |
||||
''' |
||||
Initialize the views in a Pyramid app. |
||||
|
||||
Activate this setup using ``config.include('ordr2.views')``. |
||||
''' |
||||
settings = config.get_settings() |
||||
age = int(settings.get('static_views.cache_max_age', 3600)) |
||||
|
||||
config.add_static_view('static', 'ordr:static', cache_max_age=age) |
||||
config.add_static_view('deform', 'deform:static', cache_max_age=age) |
@ -0,0 +1,402 @@
@@ -0,0 +1,402 @@
|
||||
''' views for user accounts |
||||
|
||||
This includes login, logout, registration, forgotten passwords, changing |
||||
settings and passwords |
||||
''' |
||||
|
||||
import deform |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.security import remember, forget |
||||
from pyramid.view import view_config |
||||
from sqlalchemy import func, or_ |
||||
|
||||
|
||||
from ordr.events import ( |
||||
ChangeEmailNotification, |
||||
PasswordResetNotification, |
||||
RegistrationNotification |
||||
) |
||||
from ordr.models.account import Role, TokenSubject, User |
||||
|
||||
|
||||
# account resource root |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='view' |
||||
) |
||||
def account(context, request): |
||||
''' redirect if '/account' was requested directly ''' |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
|
||||
# login and logout |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
name='login', |
||||
request_method='GET', |
||||
permission='login', |
||||
renderer='ordr:templates/account/login.jinja2', |
||||
) |
||||
def login(context, request): |
||||
''' shows the login page ''' |
||||
context.nav_active = 'welcome' |
||||
return {'loginerror': False} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
name='login', |
||||
request_method='POST', |
||||
permission='login', |
||||
renderer='ordr:templates/account/login.jinja2', |
||||
) |
||||
def check_login(context, request): |
||||
''' check user credentials ''' |
||||
username = request.POST.get('username') |
||||
password = request.POST.get('password') |
||||
user = ( |
||||
request.dbsession |
||||
.query(User) |
||||
.filter_by(username=username) |
||||
.first() |
||||
) |
||||
|
||||
if user and user.is_active and user.check_password(password): |
||||
headers = remember(request, user.id) |
||||
return HTTPFound(request.resource_url(request.root), headers=headers) |
||||
|
||||
context.nav_active = 'welcome' |
||||
return {'loginerror': True} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
name='logout', |
||||
permission='logout' |
||||
) |
||||
def logout(context, request): |
||||
''' log out of an user ''' |
||||
headers = forget(request) |
||||
return HTTPFound(request.resource_url(request.root), headers=headers) |
||||
|
||||
|
||||
# registration process |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.RegistrationResource', |
||||
permission='register', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/registration_form.jinja2' |
||||
) |
||||
def registration_form(context, request): |
||||
''' show registration form ''' |
||||
form = context.get_registration_form() |
||||
return {'form': form} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.RegistrationResource', |
||||
permission='register', |
||||
request_method='POST', |
||||
renderer='ordr:templates/account/registration_form.jinja2' |
||||
) |
||||
def registration_form_processing(context, request): |
||||
''' process registration form ''' |
||||
if 'create' not in request.POST: |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
form = context.get_registration_form() |
||||
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 |
||||
token = account.issue_token(request, TokenSubject.REGISTRATION) |
||||
notification = RegistrationNotification(request, account, {'token': token}) |
||||
request.registry.notify(notification) |
||||
|
||||
return HTTPFound(request.resource_url(context, 'verify')) |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.RegistrationResource', |
||||
name='verify', |
||||
permission='register', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/registration_verify.jinja2' |
||||
) |
||||
def registration_verify_email(context, request): |
||||
''' show email verification text ''' |
||||
return {} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.RegistrationTokenResource', |
||||
permission='register', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/registration_completed.jinja2' |
||||
) |
||||
def registration_completed(context, request): |
||||
''' registration is completed, awaiting activation by admin ''' |
||||
token = context.model |
||||
account = token.owner |
||||
account.role = Role.NEW |
||||
request.dbsession.delete(token) |
||||
return {} |
||||
|
||||
|
||||
# forgotten password process |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetResource', |
||||
permission='reset', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/forgotten_password_form.jinja2' |
||||
) |
||||
def forgotten_password_form(context, request): |
||||
''' show forgotten password form ''' |
||||
return {'formerror': False} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetResource', |
||||
permission='reset', |
||||
request_method='POST', |
||||
renderer='ordr:templates/account/forgotten_password_form.jinja2' |
||||
) |
||||
def forgotten_password_form_processing(context, request): |
||||
''' process forgotten password form ''' |
||||
if 'cancel' in request.POST: |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
identifier = request.POST.get('identifier', '') |
||||
account = ( |
||||
request.dbsession |
||||
.query(User) |
||||
.filter(or_( |
||||
func.lower(User.username) == identifier.lower(), |
||||
func.lower(User.email) == identifier.lower() |
||||
)) |
||||
.first() |
||||
) |
||||
if account is None or not account.is_active: |
||||
return {'formerror': True} |
||||
|
||||
# create a verify-new-account token and send email |
||||
token = account.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
notification = PasswordResetNotification( |
||||
request, |
||||
account, |
||||
{'token': token} |
||||
) |
||||
request.registry.notify(notification) |
||||
|
||||
return HTTPFound(request.resource_url(context, 'verify')) |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetResource', |
||||
name='verify', |
||||
permission='reset', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/forgotten_password_verify.jinja2' |
||||
) |
||||
def forgotten_password_verify_email(context, request): |
||||
''' show email verification text ''' |
||||
return {} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetResource', |
||||
name='completed', |
||||
permission='reset', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/forgotten_password_completed.jinja2' |
||||
) |
||||
def forgotten_password_completed(context, request): |
||||
''' user is verified, process reset password form ''' |
||||
return {} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetTokenResource', |
||||
permission='reset', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/forgotten_password_reset.jinja2' |
||||
) |
||||
def reset_password_form(context, request): |
||||
''' user is verified, show reset password form ''' |
||||
form = context.get_reset_form() |
||||
return {'form': form} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.PasswordResetTokenResource', |
||||
permission='reset', |
||||
request_method='POST', |
||||
renderer='ordr:templates/account/forgotten_password_reset.jinja2' |
||||
) |
||||
def reset_password_form_processing(context, request): |
||||
''' process the password reset form ''' |
||||
if 'change' not in request.POST: |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
form = context.get_reset_form() |
||||
data = request.POST.items() |
||||
try: |
||||
appstruct = form.validate(data) |
||||
except deform.ValidationFailure as e: |
||||
return {'form': form} |
||||
|
||||
# set new password |
||||
token = context.model |
||||
account = token.owner |
||||
account.set_password(appstruct['password']) |
||||
request.dbsession.delete(token) |
||||
|
||||
return HTTPFound(request.resource_url(context.__parent__, 'completed')) |
||||
|
||||
|
||||
# account settings |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='edit', |
||||
name='settings', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/settings_form.jinja2' |
||||
) |
||||
def settings_form(context, request): |
||||
''' show the settings form ''' |
||||
prefill = { |
||||
'username': request.user.username, |
||||
'first_name': request.user.first_name, |
||||
'last_name': request.user.last_name, |
||||
'email': request.user.email, |
||||
} |
||||
form = context.get_settings_form(prefill=prefill) |
||||
return {'form': form} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='edit', |
||||
name='settings', |
||||
request_method='POST', |
||||
renderer='ordr:templates/account/settings_form.jinja2' |
||||
) |
||||
def settings_form_processing(context, request): |
||||
''' process the settings form ''' |
||||
if 'change' not in request.POST: |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
form = context.get_settings_form() |
||||
data = request.POST.items() |
||||
try: |
||||
appstruct = form.validate(data) |
||||
except deform.ValidationFailure as e: |
||||
return {'form': form} |
||||
|
||||
# form validation successfull, change user |
||||
request.user.first_name = appstruct['first_name'] |
||||
request.user.last_name = appstruct['last_name'] |
||||
|
||||
if appstruct['email'] == request.user.email: |
||||
# email was not changed |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
# create a verify-new-email token and send email |
||||
token = request.user.issue_token( |
||||
request, |
||||
TokenSubject.CHANGE_EMAIL, |
||||
payload={'email': appstruct['email']} |
||||
) |
||||
notification = ChangeEmailNotification( |
||||
request, |
||||
account, |
||||
{'token': token}, |
||||
send_to=appstruct['email'] |
||||
) |
||||
request.registry.notify(notification) |
||||
|
||||
return HTTPFound(request.resource_url(context, 'verify')) |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.ChangeEmailTokenResource', |
||||
permission='edit', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/settings_mail_changed.jinja2' |
||||
) |
||||
def verify_email_change(context, request): |
||||
''' show email verification text ''' |
||||
payload = context.model.payload |
||||
request.user.email = payload['email'] |
||||
request.dbsession.delete(context.model) |
||||
return {} |
||||
|
||||
|
||||
# change password |
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='edit', |
||||
name='password', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/password_form.jinja2' |
||||
) |
||||
def password_form(context, request): |
||||
''' show the change password form ''' |
||||
form = context.get_password_form() |
||||
return {'form': form} |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='edit', |
||||
name='password', |
||||
request_method='POST', |
||||
renderer='ordr:templates/account/password_form.jinja2' |
||||
) |
||||
def password_form_processing(context, request): |
||||
''' process the change password form ''' |
||||
if 'change' not in request.POST: |
||||
return HTTPFound(request.resource_url(request.root)) |
||||
|
||||
form = context.get_password_form() |
||||
data = request.POST.items() |
||||
try: |
||||
appstruct = form.validate(data) |
||||
except deform.ValidationFailure as e: |
||||
return {'form': form} |
||||
|
||||
# form validation successfull, change the password |
||||
request.user.set_password(appstruct['password']) |
||||
return HTTPFound(request.resource_url(context, 'changed')) |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.account.AccountResource', |
||||
permission='edit', |
||||
name='changed', |
||||
request_method='GET', |
||||
renderer='ordr:templates/account/password_changed.jinja2' |
||||
) |
||||
def password_changed(context, request): |
||||
''' the password changed message ''' |
||||
return {} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
from pyramid.view import notfound_view_config, view_config |
||||
|
||||
from ordr.models.account import TokenExpired |
||||
|
||||
|
||||
@notfound_view_config( |
||||
renderer='ordr:templates/errors/404_file_not_found.jinja2' |
||||
) |
||||
def notfound_view(context, request): |
||||
''' display a file not found page ''' |
||||
request.response.status = 404 |
||||
return {} |
||||
|
||||
|
||||
@view_config( |
||||
context=TokenExpired, |
||||
renderer='ordr:templates/errors/410_token_expiry.jinja2' |
||||
) |
||||
def token_expired(context, request): |
||||
''' display page describing expired token ''' |
||||
request.response.status = 410 |
||||
return {} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.view import view_config |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.RootResource', |
||||
permission='view', |
||||
) |
||||
def welcome(context, request): |
||||
''' web root redirects ''' |
||||
if request.user: |
||||
redirect_to = request.resource_url(context, 'orders') |
||||
else: |
||||
redirect_to = request.resource_url(context, 'account', 'login') |
||||
return HTTPFound(redirect_to) |
||||
|
||||
|
||||
@view_config( |
||||
context='ordr.resources.RootResource', |
||||
name='faq', |
||||
permission='view', |
||||
renderer='ordr:templates/pages/faq.jinja2' |
||||
) |
||||
def faq(context, request): |
||||
''' displays the FAQ page ''' |
||||
return {} |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
### |
||||
# app configuration |
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html |
||||
### |
||||
|
||||
[app:main] |
||||
use = egg:ordr |
||||
|
||||
pyramid.reload_templates = false |
||||
pyramid.debug_authorization = false |
||||
pyramid.debug_notfound = false |
||||
pyramid.debug_routematch = false |
||||
pyramid.default_locale_name = en |
||||
|
||||
sqlalchemy.url = sqlite:///%(here)s/ordr.sqlite |
||||
|
||||
retry.attempts = 3 |
||||
|
||||
### |
||||
# wsgi server configuration |
||||
### |
||||
|
||||
[server:main] |
||||
use = egg:waitress#main |
||||
listen = *:6543 |
||||
|
||||
### |
||||
# logging configuration |
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html |
||||
### |
||||
|
||||
[loggers] |
||||
keys = root, ordr, sqlalchemy |
||||
|
||||
[handlers] |
||||
keys = console |
||||
|
||||
[formatters] |
||||
keys = generic |
||||
|
||||
[logger_root] |
||||
level = WARN |
||||
handlers = console |
||||
|
||||
[logger_ordr] |
||||
level = WARN |
||||
handlers = |
||||
qualname = ordr |
||||
|
||||
[logger_sqlalchemy] |
||||
level = WARN |
||||
handlers = |
||||
qualname = sqlalchemy.engine |
||||
# "level = INFO" logs SQL queries. |
||||
# "level = DEBUG" logs SQL queries and results. |
||||
# "level = WARN" logs neither. (Recommended for production systems.) |
||||
|
||||
[handler_console] |
||||
class = StreamHandler |
||||
args = (sys.stderr,) |
||||
level = NOTSET |
||||
formatter = generic |
||||
|
||||
[formatter_generic] |
||||
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
pip>=9.0.1 |
||||
bumpversion>=0.5.3 |
||||
wheel>=0.30.0 |
||||
flake8>=3.5.0 |
||||
coverage>=4.5.1 |
||||
|
||||
pytest>=3.4.1 |
||||
pytest-runner>=2.11.1 |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
[bumpversion] |
||||
current_version = 0.0.1 |
||||
commit = True |
||||
tag = True |
||||
|
||||
[bumpversion:file:setup.py] |
||||
search = version='{current_version}' |
||||
replace = version='{new_version}' |
||||
|
||||
[bumpversion:file:ordr/__init__.py] |
||||
search = __version__ = '{current_version}' |
||||
replace = __version__ = '{new_version}' |
||||
|
||||
[bdist_wheel] |
||||
universal = 1 |
||||
|
||||
[flake8] |
||||
exclude = docs |
||||
ignore = W293 |
||||
hang_closing = True |
||||
|
||||
[aliases] |
||||
test = pytest |
||||
|
||||
[tool:pytest] |
||||
testpaths = tests |
||||
python_files = *.py |
||||
collect_ignore = ['setup.py'] |
||||
xfail_strict = true |
||||
|
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
import os |
||||
|
||||
from setuptools import setup, find_packages |
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__)) |
||||
with open(os.path.join(here, 'README.txt')) as f: |
||||
README = f.read() |
||||
with open(os.path.join(here, 'CHANGES.txt')) as f: |
||||
CHANGES = f.read() |
||||
|
||||
requires = [ |
||||
'argon2_cffi', |
||||
'bcrypt', |
||||
'deform', |
||||
'passlib', |
||||
'plaster_pastedeploy', |
||||
'pyramid >= 1.9a', |
||||
'pyramid_debugtoolbar', |
||||
'pyramid_jinja2', |
||||
'pyramid_listing', |
||||
'pyramid_mailer', |
||||
'pyramid_retry', |
||||
'pyramid_tm', |
||||
'SQLAlchemy', |
||||
'transaction', |
||||
'zope.sqlalchemy', |
||||
'waitress', |
||||
] |
||||
|
||||
tests_require = [ |
||||
'WebTest >= 1.3.1', # py3 compat |
||||
'pytest', |
||||
'pytest-cov', |
||||
] |
||||
|
||||
setup( |
||||
name='ordr', |
||||
version='0.0.1', |
||||
description='Ordr', |
||||
long_description=README + '\n\n' + CHANGES, |
||||
classifiers=[ |
||||
'Programming Language :: Python', |
||||
'Framework :: Pyramid', |
||||
'Topic :: Internet :: WWW/HTTP', |
||||
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', |
||||
], |
||||
author='', |
||||
author_email='', |
||||
url='', |
||||
keywords='web pyramid pylons', |
||||
packages=find_packages(), |
||||
include_package_data=True, |
||||
zip_safe=False, |
||||
extras_require={ |
||||
'testing': tests_require, |
||||
}, |
||||
install_requires=requires, |
||||
entry_points={ |
||||
'paste.app_factory': [ |
||||
'main = ordr:main', |
||||
], |
||||
'console_scripts': [ |
||||
'initialize_ordr_db = ordr.scripts.initializedb:main', |
||||
], |
||||
}, |
||||
) |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
import pytest |
||||
import transaction |
||||
|
||||
from pyramid import testing |
||||
from pyramid.csrf import get_csrf_token |
||||
|
||||
|
||||
APP_SETTINGS = { |
||||
'sqlalchemy.url': 'sqlite:///:memory:', |
||||
'session.secret': 'something', |
||||
'session.auto_csrf': True, |
||||
'passlib.schemes': 'argon2 bcrypt', |
||||
'passlib.default': 'argon2', |
||||
'passlib.deprecated': 'auto', |
||||
'mail.default_sender': 'ordr@example.com' |
||||
} |
||||
|
||||
EXAMPLE_USER_DATA = { |
||||
'UNVALIDATED': (1, 'Graham', 'Chapman'), |
||||
'NEW': (2, 'John', 'Cleese'), |
||||
'USER': (3, 'Terry', 'Gilliam'), |
||||
'PURCHASER': (4, 'Eric', 'Idle'), |
||||
'ADMIN': (5, 'Terry', 'Jones'), |
||||
'INACTIVE': (6, 'Michael', 'Palin'), |
||||
} |
||||
|
||||
|
||||
# fixtures |
||||
|
||||
@pytest.fixture(scope='session') |
||||
def app_config(): |
||||
''' fixture for tests requiring a pyramid.testing setup ''' |
||||
with testing.testConfig(settings=APP_SETTINGS) as config: |
||||
config.include('pyramid_jinja2') |
||||
config.include('pyramid_listing') |
||||
config.include('pyramid_mailer.testing') |
||||
yield config |
||||
|
||||
|
||||
@pytest.fixture(scope='function') |
||||
def dbsession(app_config): |
||||
''' fixture for testing with database connection ''' |
||||
from ordr.models.meta import Base |
||||
from ordr.models import ( |
||||
get_engine, |
||||
get_session_factory, |
||||
get_tm_session |
||||
) |
||||
|
||||
settings = app_config.get_settings() |
||||
engine = get_engine(settings) |
||||
session_factory = get_session_factory(engine) |
||||
session = get_tm_session(session_factory, transaction.manager) |
||||
Base.metadata.create_all(engine) |
||||
|
||||
yield session |
||||
|
||||
transaction.abort() |
||||
Base.metadata.drop_all(engine) |
||||
|
||||
|
||||
# helpers |
||||
|
||||
def get_example_user(role): |
||||
''' get the user model for one well known user ''' |
||||
from ordr.models import User |
||||
|
||||
id_, first_name, last_name = EXAMPLE_USER_DATA[role.name] |
||||
user = User( |
||||
id=id_, |
||||
username=first_name + last_name, |
||||
first_name=first_name, |
||||
last_name=last_name, |
||||
email=last_name.lower() + '@example.com', |
||||
role=role |
||||
) |
||||
user.set_password(first_name) |
||||
|
||||
return user |
||||
|
||||
|
||||
def get_post_request(data, **kwargs): |
||||
''' returns a dummy request with csrf_token for validating deform forms ''' |
||||
request = testing.DummyRequest() |
||||
|
||||
post_data = {'csrf_token': get_csrf_token(request)} |
||||
post_data.update(data) |
||||
|
||||
return testing.DummyRequest(POST=post_data, **kwargs) |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
''' functional tests for ordr2 ''' |
||||
|
||||
import pytest |
||||
import re |
||||
import transaction |
||||
import webtest |
||||
|
||||
from bs4 import BeautifulSoup |
||||
|
||||
from .. import APP_SETTINGS, get_example_user |
||||
|
||||
|
||||
WEBTEST_SETTINGS = APP_SETTINGS.copy() |
||||
WEBTEST_SETTINGS['pyramid.includes'] = [ |
||||
'pyramid_mailer.testing' |
||||
] |
||||
|
||||
|
||||
class CustomTestApp(webtest.TestApp): |
||||
''' might add custom functionality to webtest.TestApp ''' |
||||
pass |
||||
|
||||
def login(self, username, password): |
||||
''' login ''' |
||||
self.logout() |
||||
result = self.get('/account/login') |
||||
login_form = result.forms[0] |
||||
login_form['username'] = username |
||||
login_form['password'] = password |
||||
login_form.submit() |
||||
response = self.get('/faq') |
||||
return username in response |
||||
|
||||
def logout(self): |
||||
''' logout ''' |
||||
self.get('/account/logout') |
||||
|
||||
def reset(self): |
||||
''' reset the webapp ''' |
||||
self.logout() |
||||
super().reset() |
||||
|
||||
|
||||
def create_users(dbsession): |
||||
''' create example users ''' |
||||
from ordr.models.account import Role |
||||
for role in Role: |
||||
user = get_example_user(role) |
||||
dbsession.add(user) |
||||
|
||||
|
||||
def get_token_url(email, prefix='/'): |
||||
''' extracts an account token url from an email ''' |
||||
soup = BeautifulSoup(email.html, 'html.parser') |
||||
for link in soup.find_all('a'): |
||||
if re.search(prefix + '[a-f0-9]{32}', link['href']): |
||||
return link['href'] |
||||
|
||||
|
||||
@pytest.fixture(scope='module') |
||||
def testappsetup(): |
||||
''' setup of fixture for using webtest |
||||
|
||||
this fixture just sets up the testapp. please use the testapp() fixture |
||||
below for real tests. |
||||
''' |
||||
from ordr.models.meta import Base |
||||
from ordr.models import get_tm_session |
||||
from ordr import main |
||||
|
||||
app = main({}, **WEBTEST_SETTINGS) |
||||
testapp = CustomTestApp(app) |
||||
|
||||
session_factory = app.registry['dbsession_factory'] |
||||
engine = session_factory.kw['bind'] |
||||
Base.metadata.create_all(engine) |
||||
|
||||
with transaction.manager: |
||||
# set up test data here |
||||
dbsession = get_tm_session(session_factory, transaction.manager) |
||||
create_users(dbsession) |
||||
|
||||
yield testapp |
||||
|
||||
Base.metadata.drop_all(engine) |
||||
|
||||
|
||||
@pytest.fixture(scope='function') |
||||
def testapp(testappsetup): |
||||
''' fixture using webtests, resets the logged every time ''' |
||||
testappsetup.reset() |
||||
yield testappsetup |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
''' functional tests for ordr accounts ''' |
||||
|
||||
from .. import testappsetup, testapp, get_token_url # noqa: F401 |
||||
|
||||
|
||||
def test_account_root(testapp): # noqa: F811 |
||||
''' check the redirect if '/account' is requested ''' |
||||
testapp.login('TerryGilliam', 'Terry') |
||||
response = testapp.get('/account') |
||||
assert response.location == 'http://localhost/' |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
''' functional tests for ordr2.views.forgotten_password ''' |
||||
|
||||
from pyramid_mailer import get_mailer |
||||
|
||||
from . import testappsetup, testapp, get_token_url # noqa: F401 |
||||
|
||||
|
||||
def test_forgot_password_process(testapp): # noqa: F811 |
||||
''' test the forgot password form ''' |
||||
response = testapp.get('/account/forgot') |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
assert active_nav is None |
||||
assert 'Step 1: Validate Account' in active_step.text |
||||
assert 'Forgot Your Password?' in response |
||||
assert 'unknown username or email' not in response |
||||
|
||||
# fill out this form with invalid data |
||||
form = response.form |
||||
form['identifier'] = 'unknown identifier' |
||||
response = form.submit(name='send_mail') |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
assert active_nav is None |
||||
assert 'Step 1: Validate Account' in active_step.text |
||||
assert 'Forgot Your Password?' in response |
||||
assert 'Username or email address unknown' in response |
||||
|
||||
# fill out this form with valid data |
||||
response = testapp.get('/account/forgot') |
||||
form = response.form |
||||
form['identifier'] = 'TerryGilliam' |
||||
response = form.submit(name='send_mail') |
||||
assert response.location == 'http://localhost/account/forgot/verify' |
||||
|
||||
response = response.follow() |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
assert active_nav is None |
||||
assert 'Step 1: Validate Account' in active_step.text |
||||
assert 'Verify Your Email Address' in response |
||||
|
||||
# click the email verification token |
||||
mailer = get_mailer(testapp.app.registry) |
||||
email = mailer.outbox[-1] |
||||
assert email.subject == '[ordr] Password Reset' |
||||
|
||||
token_link = get_token_url(email, prefix='/forgot/') |
||||
response = testapp.get(token_link) |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
assert active_nav is None |
||||
assert 'Step 2: Change Password' in active_step.text |
||||
assert 'Forgot Your Password?' in response |
||||
assert 'do not match' not in response |
||||
|
||||
# fill out the change password form with invalid data |
||||
form = response.form |
||||
form['password'] = 'some passwords' |
||||
form['password-confirm'] = 'that do not match' |
||||
response = form.submit(name='change') |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
assert active_nav is None |
||||
assert 'Step 2: Change Password' in active_step.text |
||||
assert 'Forgot Your Password?' in response |
||||
assert 'Password did not match confirm' in response |
||||
|
||||
# fill out the change password form with valid data |
||||
form = response.form |
||||
form['password'] = 'Lost in La Mancha' |
||||
form['password-confirm'] = 'Lost in La Mancha' |
||||
response = form.submit(name='change') |
||||
assert response.location == 'http://localhost/account/forgot/completed' |
||||
|
||||
response = response.follow() |
||||
active_nav = response.html.find('li', class_='active') |
||||
active_step = response.html.find('p', class_='text-primary') |
||||
content = response.html.find('div', class_='content') |
||||
assert active_nav is None |
||||
assert 'Step 3: Finished' in active_step.text |
||||
assert 'Forgot Your Password?' in response |
||||
assert 'Password Reset Succesfull' in response |
||||
assert content.a['href'] == 'http://localhost/' |
||||
assert content.a.text == 'log in' |
||||
|
||||
# old password should not work but the new one |
||||
assert not testapp.login('TerryGilliam', 'Terry') |
||||
assert testapp.login('TerryGilliam', 'Lost in La Mancha') |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
''' functional tests for ordr2.views.pages ''' |
||||
|
||||
import pytest |
||||
|
||||
from . import testappsetup, testapp # noqa: F401 |
||||
|
||||
|
||||
def test_login_get(testapp): # noqa: F811 |
||||
''' test the login form ''' |
||||
response = testapp.get('/account/login') |
||||
active = response.html.find('li', class_='active') |
||||
form = response.form |
||||
|
||||
assert active.a['href'] == 'http://localhost/' |
||||
assert form.action == 'http://localhost/account/login' |
||||
|
||||
|
||||
def test_login_ok(testapp): # noqa: F811 |
||||
''' test login form with valid credentials ''' |
||||
response = testapp.get('/account/login') |
||||
|
||||
login_form = response.forms[0] |
||||
login_form['username'] = 'TerryGilliam' |
||||
login_form['password'] = 'Terry' |
||||
response = login_form.submit() |
||||
|
||||
assert response.location == 'http://localhost/' |
||||
|
||||
response = testapp.get('/faq') |
||||
assert 'TerryGilliam' in response |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'username,password', |
||||
[('John', 'Cleese'), ('unknown user', 'wrong password')] |
||||
) |
||||
def test_login_denied(testapp, username, password): |
||||
''' test login form with invalid credentials ''' |
||||
response = testapp.get('/account/login') |
||||
|
||||
login_form = response.forms[0] |
||||
login_form['username'] = 'John' |
||||
login_form['password'] = 'Cleese' |
||||
response = login_form.submit() |
||||
|
||||
assert 'account is not activated' in response |
||||
|
||||
|
||||
def test_logout(testapp): # noqa: F811 |
||||
''' test login form with valid credentials ''' |
||||
response = testapp.get('/account/login') |
||||
|
||||
login_form = response.forms[0] |
||||
login_form['username'] = 'TerryGilliam' |
||||
login_form['password'] = 'Terry' |
||||
login_form.submit() |
||||
|
||||
response = testapp.get('/faq') |
||||
assert 'TerryGilliam' in response |
||||
|
||||
response = testapp.get('/account/logout') |
||||
assert response.location == 'http://localhost/' |
||||
|
||||
response = testapp.get('/faq') |
||||
assert 'TerryGilliam' not in response |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
''' functional tests for ordr2.views.registration ''' |
||||
|
||||
from pyramid_mailer import get_mailer |
||||
|
||||
from . import testappsetup, testapp, get_token_url # noqa: F401 |
||||
|
||||
|
||||
def test_registration_form(testapp): # noqa: F811 |
||||
''' test the registration form ''' |
||||
response = testapp.get('/account/register') |
||||
active = response.html.find('li', class_='active') |
||||
assert active.a['href'] == 'http://localhost/account/register' |
||||
assert 'Registration' in response.html.title.text |
||||
|
||||
|
||||
def test_registration_form_invalid(testapp): # noqa: F811 |
||||
''' test the registration form with invalid data ''' |
||||
response = testapp.get('/account/register') |
||||
|
||||
form = response.form |
||||
form['email'] = 'not an email address' |
||||
response = form.submit(name='create') |
||||
|
||||
assert 'Invalid email address' in response |
||||
assert 'Registration' in response.html.title.text |
||||
|
||||
|
||||
def test_registration_process(testapp): # noqa: F811 |
||||
''' test the registration process with valid data ''' |
||||
response = testapp.get('/account/register') |
||||
|
||||
form = response.form |
||||
form['username'] = 'AmyMcDonald', |
||||
form['first_name'] = 'Amy', |
||||
form['last_name'] = 'McDonald', |
||||
form['email'] = 'amy.mcdonald@example.com', |
||||
form['password'] = 'Make Amy McDonald A Rich Girl Fund', |
||||
form['password-confirm'] = 'Make Amy McDonald A Rich Girl Fund', |
||||
response = form.submit(name='create') |
||||
assert response.location == 'http://localhost/account/register/verify' |
||||
|
||||
response = response.follow() |
||||
active = response.html.find('li', class_='active') |
||||
assert active.a['href'] == 'http://localhost/account/register' |
||||
assert 'Please follow the link in the email' in response |
||||
assert 'Registration' in response.html.title.text |
||||
|
||||
# click the email verification token |
||||
mailer = get_mailer(testapp.app.registry) |
||||
email = mailer.outbox[-1] |
||||
assert email.subject == '[ordr] Please verify your email address' |
||||
|
||||
token_link = get_token_url(email, prefix='/account/register/') |
||||
response = testapp.get(token_link) |
||||
active = response.html.find('li', class_='active') |
||||
assert active.a['href'] == 'http://localhost/account/register' |
||||
assert 'Registration Completed' in response |
||||
assert 'Registration' in response.html.title.text |
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
''' functional tests for ordr2.views.account.py ''' |
||||
|
||||
from pyramid_mailer import get_mailer |
||||
|
||||
from .. import testappsetup, testapp, get_token_url # noqa: F401 |
||||
|
||||
|
||||
def test_account_change_settings(testapp): # noqa: F811 |
||||
testapp.login('TerryGilliam', 'Terry') |
||||
|
||||
response = testapp.get('/account/settings') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Settings' in response |
||||
assert 'value="gilliam@example.com"' in response |
||||
assert 'Wrong Password' not in response |
||||
|
||||
# fill out the form without confirmation password |
||||
form = response.form |
||||
form['first_name'] = 'Amy' |
||||
form['last_name'] = 'McDonald' |
||||
response = form.submit(name='change') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Settings' in response |
||||
assert 'required' in response |
||||
|
||||
# fill out the form with invalid data but correct password |
||||
response = testapp.get('/account/settings') |
||||
form = response.form |
||||
form['first_name'] = 'Amy' |
||||
form['last_name'] = 'McDonald' |
||||
form['email'] = 'this is not an email address' |
||||
form['confirmation'] = 'Terry' |
||||
response = form.submit(name='change') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Settings' in response |
||||
assert 'Invalid email address' in response |
||||
|
||||
# fill out the form with valid data and correct password |
||||
response = testapp.get('/account/settings') |
||||
form = response.form |
||||
form['first_name'] = 'Amy' |
||||
form['last_name'] = 'McDonald' |
||||
form['confirmation'] = 'Terry' |
||||
response = form.submit(name='change') |
||||
assert response.location == 'http://localhost/' |
||||
|
||||
response = testapp.get('/account/settings') |
||||
assert 'value="Amy"' in response |
||||
|
||||
|
||||
def test_account_change_email(testapp): # noqa: F811 |
||||
testapp.login('TerryGilliam', 'Terry') |
||||
response = testapp.get('/account/settings') |
||||
|
||||
# fill out the form with valid data and correct password |
||||
form = response.form |
||||
form['email'] = 'amy@example.com' |
||||
form['confirmation'] = 'Terry' |
||||
response = form.submit(name='change') |
||||
assert response.location == 'http://localhost/account/verify' |
||||
|
||||
# click the email verification token |
||||
mailer = get_mailer(testapp.app.registry) |
||||
email = mailer.outbox[-1] |
||||
assert email.subject == '[ordr] Verify New Email Address' |
||||
assert email.recipients == ['amy@example.com'] |
||||
|
||||
token_link = get_token_url(email, prefix='/account/') |
||||
response = testapp.get(token_link) |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Settings' in response |
||||
assert 'changed sucessfully' not in response |
||||
|
||||
|
||||
def test_account_change_password(testapp): # noqa: F811 |
||||
testapp.login('TerryGilliam', 'Terry') |
||||
|
||||
response = testapp.get('/account/password') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Password' in response |
||||
assert 'Wrong Password' not in response |
||||
|
||||
# fill out the form with incorrect confirmation password |
||||
form = response.form |
||||
form['password'] = 'Lost in La Mancha' |
||||
form['password-confirm'] = 'Lost in La Mancha' |
||||
form['confirmation'] = 'Unknown Password' |
||||
response = form.submit(name='change') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Password' in response |
||||
assert 'Wrong password' in response |
||||
|
||||
# fill out the form with invalid data but correct password |
||||
response = testapp.get('/account/password') |
||||
form = response.form |
||||
form['password'] = 'Lost in La Mancha' |
||||
form['password-confirm'] = 'confirmation does not match' |
||||
form['confirmation'] = 'Terry' |
||||
response = form.submit(name='change') |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Change Password' in response |
||||
assert 'Password did not match confirm' in response |
||||
|
||||
# fill out the form with valid data and correct password |
||||
response = testapp.get('/account/password') |
||||
form = response.form |
||||
form['password'] = 'Lost in La Mancha' |
||||
form['password-confirm'] = 'Lost in La Mancha' |
||||
form['confirmation'] = 'Terry' |
||||
response = form.submit(name='change') |
||||
assert response.location == 'http://localhost/account/changed' |
||||
|
||||
response = response.follow() |
||||
active_nav = response.html.find('li', class_='active') |
||||
assert active_nav is None |
||||
assert 'Your password was changed successfully' in response |
||||
|
||||
assert testapp.login('TerryGilliam', 'Lost in La Mancha') |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
''' functional tests for ordr2.views.errors ''' |
||||
|
||||
from . import testappsetup, testapp # noqa: F401 |
||||
|
||||
|
||||
def test_404(testapp): # noqa: F811 |
||||
''' test the 404 page ''' |
||||
response = testapp.get('/unknown', status=404) |
||||
assert '404' in response |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
''' functional tests for ordr2.templates.layout |
||||
|
||||
The tests for the layout are performed on '/faqs' or '/orders', since these |
||||
two urls are accessible by either everyone or all active users |
||||
''' |
||||
|
||||
import pytest |
||||
|
||||
from . import testappsetup, testapp # noqa: F401 |
||||
|
||||
|
||||
def test_navbar_no_user(testapp): # noqa: F811 |
||||
''' test the navigation on top of the page for an unauthenticated user ''' |
||||
response = testapp.get('/faq') |
||||
navbar = response.html.find('nav', class_='navbar-dark') |
||||
expected = [ |
||||
'http://localhost/', |
||||
'http://localhost/', |
||||
'http://localhost/faq', |
||||
'http://localhost/account/register' |
||||
] |
||||
hrefs = [a['href'] for a in navbar.find_all('a')] |
||||
|
||||
assert expected == hrefs |
||||
assert '/orders' not in response |
||||
assert 'nav-item dropdown' not in response |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'username,password,extras', [ |
||||
('TerryGilliam', 'Terry', []), |
||||
('EricIdle', 'Eric', []), |
||||
('TerryJones', 'Terry', ['http://localhost/admin']), |
||||
] |
||||
) |
||||
def test_navbar_with_user(testapp, username, password, extras): |
||||
''' test the navigation on top of the page for an authenticated user ''' |
||||
testapp.login(username, password) |
||||
response = testapp.get('/faq') |
||||
navbar = response.html.find('nav', class_='navbar-dark') |
||||
hrefs = [a['href'] for a in navbar.find_all('a')] |
||||
expected = [ |
||||
'http://localhost/', |
||||
'http://localhost/orders', |
||||
'http://localhost/faq' |
||||
] |
||||
expected.extend(extras) |
||||
expected.extend([ |
||||
'#', |
||||
'http://localhost/account/logout', |
||||
'http://localhost/account/settings', |
||||
'http://localhost/account/password' |
||||
]) |
||||
|
||||
assert expected == hrefs |
||||
assert 'nav-item dropdown' in response |
||||
assert username in response |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
''' functional tests for ordr2.views.pages ''' |
||||
|
||||
from . import testappsetup, testapp # noqa: F401 |
||||
|
||||
|
||||
def test_welcome(testapp): # noqa: F811 |
||||
''' test the redirects on web root ''' |
||||
response = testapp.get('/') |
||||
assert response.location == 'http://localhost/account/login' |
||||
|
||||
testapp.login('TerryGilliam', 'Terry') |
||||
|
||||
response = testapp.get('/') |
||||
assert response.location == 'http://localhost/orders' |
||||
|
||||
|
||||
def test_faq(testapp): # noqa: F811 |
||||
''' test the faq page ''' |
||||
response = testapp.get('/faq') |
||||
active = response.html.find('li', class_='active') |
||||
assert active.a['href'] == 'http://localhost/faq' |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
''' Tests for ordr.events ''' |
||||
|
||||
from datetime import datetime |
||||
from pyramid.testing import DummyRequest |
||||
from pyramid_mailer import get_mailer |
||||
|
||||
from . import app_config, get_example_user # noqa: F401 |
||||
|
||||
|
||||
def test_user_notification_init(app_config): # noqa: F811 |
||||
''' test creation of user notification events ''' |
||||
from ordr.events import UserNotification |
||||
from ordr.models.account import Role |
||||
|
||||
user = get_example_user(Role.USER) |
||||
notification = UserNotification('request', user, 'data') |
||||
|
||||
assert notification.request == 'request' |
||||
assert notification.account == user |
||||
assert notification.data == 'data' |
||||
assert notification.send_to == user.email |
||||
|
||||
|
||||
def test_user_notification_init_send_to_override(app_config): # noqa: F811 |
||||
''' test creation of user notification events ''' |
||||
from ordr.events import UserNotification |
||||
from ordr.models.account import Role |
||||
|
||||
user = get_example_user(Role.USER) |
||||
notification = UserNotification('request', user, 'data', 'amy@example.com') |
||||
|
||||
assert notification.request == 'request' |
||||
assert notification.account == user |
||||
assert notification.data == 'data' |
||||
assert notification.send_to == 'amy@example.com' |
||||
|
||||
|
||||
def test_notify_user(app_config): # noqa: F811 |
||||
''' test the user notification ''' |
||||
from ordr.events import RegistrationNotification, notify_user |
||||
from ordr.models.account import Token, Role |
||||
|
||||
request = DummyRequest() |
||||
user = get_example_user(Role.USER) |
||||
token = Token(expires=datetime.utcnow(), hash='some_hash') |
||||
notification = RegistrationNotification(request, user, {'token': token}) |
||||
notify_user(notification) |
||||
mailer = get_mailer(request.registry) |
||||
last_mail = mailer.outbox[-1] |
||||
|
||||
assert 'Please verify your email address ' in last_mail.html |
||||
assert 'http://example.com//some_hash' in last_mail.html |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
''' test (sub) package for views ''' |
@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
|
||||
import pytest |
||||
|
||||
from datetime import datetime, timedelta |
||||
from pyramid.testing import DummyRequest |
||||
|
||||
from .. import app_config, dbsession, get_example_user # noqa: F401 |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'key,result', [('NEW', 'role:new'), ('USER', 'role:user')] |
||||
) |
||||
def test_role_principal(key, result): |
||||
''' test the principal representation of a role ''' |
||||
from ordr.models.account import Role |
||||
subject = Role[key] |
||||
assert subject.principal == result |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'key,result', [('NEW', 'New'), ('USER', 'User')] |
||||
) |
||||
def test_role__str__(key, result): |
||||
''' test the string representation of a role ''' |
||||
from ordr.models.account import Role |
||||
subject = Role[key] |
||||
assert str(subject) == result |
||||
|
||||
|
||||
@pytest.mark.parametrize('id_', [1, 2, 5, 123]) |
||||
def test_user_principal(id_): |
||||
''' test the principal representation of a user ''' |
||||
from ordr.models.account import User |
||||
user = User(id=id_) |
||||
assert user.principal == f'user:{id_}' |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'name, principals', [ |
||||
('UNVALIDATED', ['role:unvalidated']), |
||||
('NEW', ['role:new']), |
||||
('USER', ['role:user']), |
||||
('PURCHASER', ['role:purchaser', 'role:user']), |
||||
('ADMIN', ['role:admin', 'role:purchaser', 'role:user']), |
||||
('INACTIVE', ['role:inactive']), |
||||
] |
||||
) |
||||
def test_user_principals(name, principals): |
||||
''' test all principals of a user ''' |
||||
from ordr.models.account import User, Role |
||||
|
||||
user = User(id=1, role=Role[name]) |
||||
expected = ['user:1'] |
||||
expected.extend(principals) |
||||
|
||||
assert expected == user.principals |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'name, expected', [ |
||||
('UNVALIDATED', False), |
||||
('NEW', False), |
||||
('USER', True), |
||||
('PURCHASER', True), |
||||
('ADMIN', True), |
||||
('INACTIVE', False), |
||||
] |
||||
) |
||||
def test_user_is_active(name, expected): |
||||
''' test the calculated property 'active' of a user ''' |
||||
from ordr.models.account import User, Role |
||||
user = User(id=1, role=Role[name]) |
||||
assert expected == user.is_active |
||||
|
||||
|
||||
def test_user_set_password(): |
||||
''' test 'set_password()' method of a user ''' |
||||
from ordr.models.account import User |
||||
from ordr.security import password_context |
||||
|
||||
password_context.update(schemes=['argon2']) |
||||
user = User() |
||||
assert user.password_hash is None |
||||
|
||||
user.set_password('password') |
||||
assert user.password_hash.startswith('$argon2') |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'password,expected', [ |
||||
('', False), |
||||
('wrong', False), |
||||
('password', True), |
||||
] |
||||
) |
||||
def test_user_check_password(password, expected): |
||||
''' test the 'check_password()' method of a user ''' |
||||
from ordr.models.account import User |
||||
from ordr.security import password_context |
||||
|
||||
password_context.update(schemes=['argon2']) |
||||
hash = ('$argon2i$v=19$m=512,t=2,p=2$' |
||||
'YcyZMyak9D7nvFfKmVOq1Q$fnzNh58HWfvxHvRDGjhTqA' |
||||
) |
||||
user = User(password_hash=hash) |
||||
|
||||
assert user.check_password(password) == expected |
||||
|
||||
|
||||
def test_user_check_password_updates_old_sheme(): |
||||
''' test that 'check_password()' updates the hash off an old scheme ''' |
||||
from ordr.models.account import User |
||||
from ordr.security import password_context |
||||
|
||||
password_context.update( |
||||
schemes=['argon2', 'bcrypt'], |
||||
default='argon2', |
||||
deprecated='auto' |
||||
) |
||||
old_hash = '$2b$12$6ljSfpLaXBeEVOeaP1scUe6IAa0cztM.UBbjc1PdrI4j0vwgoYgpi' |
||||
user = User(password_hash=old_hash) |
||||
|
||||
assert user.check_password('password') |
||||
assert user.password_hash.startswith('$argon2') |
||||
assert user.check_password('password') |
||||
|
||||
|
||||
def test_user__str__(): |
||||
''' test the string representation of a user ''' |
||||
from ordr.models.account import User |
||||
user = User(username='Eric Idle') |
||||
assert str(user) == 'Eric Idle' |
||||
|
||||
|
||||
def test_user_issue_token(app_config): # noqa: F811 |
||||
''' test the 'issue_token()' method of a user ''' |
||||
from ordr.models.account import User, Token, TokenSubject |
||||
|
||||
request = DummyRequest() |
||||
user = User() |
||||
token = user.issue_token(request, TokenSubject.REGISTRATION, {'foo': 1}) |
||||
|
||||
assert isinstance(token, Token) |
||||
assert token.hash is not None |
||||
assert token.subject == TokenSubject.REGISTRATION |
||||
assert token.payload == {'foo': 1} |
||||
assert token.owner == user |
||||
|
||||
|
||||
def test_token_issue_token(app_config): # noqa: F811 |
||||
''' test the 'issue()' class method of the token class ''' |
||||
from ordr.models.account import User, Token, TokenSubject |
||||
|
||||
request = DummyRequest() |
||||
user = User() |
||||
token = Token.issue(request, user, TokenSubject.REGISTRATION, {'foo': 1}) |
||||
expected_expires = datetime.utcnow() + timedelta(minutes=5) |
||||
|
||||
assert isinstance(token, Token) |
||||
assert token.hash is not None |
||||
assert token.subject == TokenSubject.REGISTRATION |
||||
assert token.payload == {'foo': 1} |
||||
assert token.owner == user |
||||
assert token.expires.timestamp() == pytest.approx( |
||||
expected_expires.timestamp(), |
||||
abs=1 |
||||
) |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'subject,delta', [('REGISTRATION', 5), ('RESET_PASSWORD', 10)] |
||||
) |
||||
def test_token_issue_token_time_from_settings(app_config, subject, delta): |
||||
''' test that 'issue()' uses the exiration time from setting ''' |
||||
from ordr.models.account import User, Token, TokenSubject |
||||
|
||||
request = DummyRequest() |
||||
request.registry.settings['token_expiry.reset_password'] = 10 |
||||
user = User() |
||||
token_subject = TokenSubject[subject] |
||||
token = Token.issue(request, user, token_subject, None) |
||||
expected_expires = datetime.utcnow() + timedelta(minutes=delta) |
||||
|
||||
assert token.expires.timestamp() == pytest.approx( |
||||
expected_expires.timestamp(), |
||||
abs=1 |
||||
) |
||||
|
||||
|
||||
@pytest.mark.parametrize('use_subject', [True, False]) # noqa: F811 |
||||
def test_registration_token_retrieve_ok(dbsession, use_subject): |
||||
''' test 'retrieve()' class method returns token instance ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
subject = TokenSubject.REGISTRATION if use_subject else None |
||||
result = Token.retrieve(request, token.hash, subject=subject) |
||||
|
||||
assert result == token |
||||
|
||||
|
||||
def test_registration_token_retrieve_not_found(dbsession): # noqa: F811 |
||||
''' test 'retrieve()' class method returns None if token not found ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
user = get_example_user(Role.NEW) |
||||
user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
result = Token.retrieve(request, 'unknown hash') |
||||
|
||||
assert result is None |
||||
|
||||
|
||||
def test_registration_token_retrieve_wrong_subject(dbsession): # noqa: F811 |
||||
''' test 'retrieve()' class method returns None if wrong subject used ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
result = Token.retrieve( |
||||
request, |
||||
token.hash, |
||||
subject=TokenSubject.RESET_PASSWORD |
||||
) |
||||
|
||||
assert result is None |
||||
|
||||
|
||||
def test_registration_token_expired_raises_exception(dbsession): # noqa: F811 |
||||
''' test 'retrieve()' class method raises exception if token is expired ''' |
||||
from ordr.models.account import Role, Token, TokenSubject, TokenExpired |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.REGISTRATION) |
||||
token.expires = datetime.utcnow() - timedelta(weeks=1) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
with pytest.raises(TokenExpired): |
||||
Token.retrieve(request, token.hash) |
||||
|
||||
dbsession.flush() |
||||
assert dbsession.query(Token).count() == 0 |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
import pytest |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'value,expected', [ |
||||
(None, None), |
||||
([1, 2, 3], '[1, 2, 3]'), |
||||
({'a': 1, 'b': 2}, '{"a": 1, "b": 2}'), |
||||
] |
||||
) |
||||
def test_json_encoder_bind(value, expected): |
||||
''' test encoding json ''' |
||||
from ordr.models.meta import JsonEncoder |
||||
encoder = JsonEncoder() |
||||
assert encoder.process_bind_param(value, None) == expected |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'value,expected', [ |
||||
(None, None), |
||||
('[1, 2, 3]', [1, 2, 3]), |
||||
('{"a": 1, "b":2}', {'a': 1, 'b': 2}), |
||||
] |
||||
) |
||||
def test_json_encoder_result(value, expected): |
||||
''' test decoding json ''' |
||||
from ordr.models.meta import JsonEncoder |
||||
encoder = JsonEncoder() |
||||
assert encoder.process_result_value(value, None) == expected |
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [None, [1, 2, 3], {'a': 1, 'b': 2}]) |
||||
def test_json_encoder_bind_and_result(value): |
||||
''' encoding and later decoding json should provide not change value ''' |
||||
from ordr.models.meta import JsonEncoder |
||||
encoder = JsonEncoder() |
||||
result = encoder.process_bind_param(value, None) |
||||
assert encoder.process_result_value(result, None) == value |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
''' test package for resources ''' |
@ -0,0 +1,323 @@
@@ -0,0 +1,323 @@
|
||||
''' Tests for the account resources ''' |
||||
|
||||
import pytest |
||||
|
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
from .. import app_config, dbsession, get_example_user # noqa: F401 |
||||
|
||||
|
||||
def test_registration_token_acl(): |
||||
''' test access controll list for RegistrationTokenResource ''' |
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
from ordr.resources.account import RegistrationTokenResource |
||||
|
||||
parent = DummyResource(request='request') |
||||
resource = RegistrationTokenResource('name', parent) |
||||
|
||||
assert resource.__acl__() == [(Allow, Everyone, 'register'), DENY_ALL] |
||||
|
||||
|
||||
def test_registration_acl(): |
||||
''' test access controll list for RegistrationResource ''' |
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
from ordr.resources.account import RegistrationResource |
||||
|
||||
parent = DummyResource(request='request') |
||||
resource = RegistrationResource('a name', parent) |
||||
|
||||
assert resource.__acl__() == [(Allow, Everyone, 'register'), DENY_ALL] |
||||
|
||||
|
||||
def test_registration_get_registration_form(): |
||||
''' test 'get_registration_form()' method of RegistrationResource ''' |
||||
from ordr.resources.account import RegistrationResource |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = RegistrationResource('a name', 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' |
||||
|
||||
|
||||
def test_registration_getitem_found(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method returns child resource ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import ( |
||||
RegistrationResource, |
||||
RegistrationTokenResource |
||||
) |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = RegistrationResource('a name', parent) |
||||
result = resource[token.hash] |
||||
|
||||
assert isinstance(result, RegistrationTokenResource) |
||||
assert result.__name__ == token.hash |
||||
assert result.__parent__ == resource |
||||
assert result.model == token |
||||
|
||||
|
||||
def test_registration_getitem_not_found(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method raises KeyError ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import RegistrationResource |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = RegistrationResource('a name', parent) |
||||
|
||||
with pytest.raises(KeyError): |
||||
resource['unknown hash'] |
||||
|
||||
|
||||
def test_password_reset_token_acl(): |
||||
''' test access controll list for PasswordResetTokenResource ''' |
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
|
||||
parent = DummyResource(request='request') |
||||
resource = PasswordResetTokenResource('name', parent) |
||||
|
||||
assert resource.__acl__() == [(Allow, Everyone, 'reset'), DENY_ALL] |
||||
|
||||
|
||||
def test_password_reset_token_get_reset_form(): |
||||
''' test the setup of the password reset form''' |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = PasswordResetTokenResource('some name', parent) |
||||
form = resource.get_reset_form() |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert len(form.buttons) == 2 |
||||
assert form.buttons[0].title == 'Set New Password' |
||||
assert form.buttons[1].title == 'Cancel' |
||||
|
||||
|
||||
def test_password_reset_acl(): |
||||
''' test access controll list for PasswordResetResource ''' |
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
from ordr.resources.account import PasswordResetResource |
||||
|
||||
parent = DummyResource(request='request') |
||||
resource = PasswordResetResource('a name', parent) |
||||
|
||||
assert resource.__acl__() == [(Allow, Everyone, 'reset'), DENY_ALL] |
||||
|
||||
|
||||
def test_password_reset_getitem_found(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method returns child resource ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import ( |
||||
PasswordResetResource, |
||||
PasswordResetTokenResource |
||||
) |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = PasswordResetResource('a name', parent) |
||||
result = resource[token.hash] |
||||
|
||||
assert isinstance(result, PasswordResetTokenResource) |
||||
assert result.__name__ == token.hash |
||||
assert result.__parent__ == resource |
||||
assert result.model == token |
||||
|
||||
|
||||
def test_password_reset_getitem_not_found(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method raises KeyError ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import PasswordResetResource |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
user.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = PasswordResetResource('a name', parent) |
||||
|
||||
with pytest.raises(KeyError): |
||||
resource['unknown hash'] |
||||
|
||||
|
||||
def test_change_email_token_acl(dbsession): # noqa: F811 |
||||
''' test access controll list for PasswordResetTokenResource ''' |
||||
from pyramid.security import Allow, DENY_ALL |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
from ordr.resources.account import ChangeEmailTokenResource |
||||
|
||||
request = DummyRequest() |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
user.issue_token(request, TokenSubject.CHANGE_EMAIL) |
||||
dbsession.flush() |
||||
token = dbsession.query(Token).first() |
||||
|
||||
parent = DummyResource(request='request') |
||||
resource = ChangeEmailTokenResource('name', parent, model=token) |
||||
|
||||
assert resource.__acl__() == [(Allow, 'user:3', 'edit'), DENY_ALL] |
||||
|
||||
|
||||
def test_account_resource_set_model_from_request(): |
||||
''' test access controll list for PasswordResetResource ''' |
||||
from ordr.resources.account import AccountResource |
||||
|
||||
request = DummyRequest(user='Amy McDonald') |
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('a name', parent) |
||||
|
||||
assert resource.model == 'Amy McDonald' |
||||
|
||||
|
||||
def test_account_resource_acl(): |
||||
''' test access controll list for PasswordResetResource ''' |
||||
from pyramid.security import ( |
||||
Allow, |
||||
Everyone, |
||||
Authenticated, |
||||
DENY_ALL |
||||
) |
||||
from ordr.resources.account import AccountResource |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('a name', parent) |
||||
|
||||
assert resource.__acl__() == [ |
||||
(Allow, Everyone, 'view'), |
||||
(Allow, Everyone, 'login'), |
||||
(Allow, Everyone, 'logout'), |
||||
(Allow, Everyone, 'register'), |
||||
(Allow, Everyone, 'reset'), |
||||
(Allow, Authenticated, 'edit'), |
||||
DENY_ALL |
||||
] |
||||
|
||||
|
||||
@pytest.mark.parametrize('key', ['register', 'forgot']) # noqa: F811 |
||||
def test_account_resource_getitem_static(dbsession, key): |
||||
''' test '__getitem__()' method returns static resources ''' |
||||
from ordr.resources.account import ( |
||||
AccountResource, |
||||
PasswordResetResource, |
||||
RegistrationResource |
||||
) |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('some name', parent) |
||||
result = resource[key] |
||||
|
||||
if key == 'register': |
||||
assert isinstance(result, RegistrationResource) |
||||
elif key == 'forgot': |
||||
assert isinstance(result, PasswordResetResource) |
||||
|
||||
|
||||
def test_account_resource_getitem_token(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method returns child resource ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import ( |
||||
AccountResource, |
||||
ChangeEmailTokenResource |
||||
) |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
token = user.issue_token(request, TokenSubject.CHANGE_EMAIL) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('a name', parent) |
||||
result = resource[token.hash] |
||||
|
||||
assert isinstance(result, ChangeEmailTokenResource) |
||||
assert result.__name__ == token.hash |
||||
assert result.__parent__ == resource |
||||
assert result.model == token |
||||
|
||||
|
||||
def test_account_resource_getitem_not_found(dbsession): # noqa: F811 |
||||
''' test '__getitem__()' method raises KeyError ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import AccountResource |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.NEW) |
||||
user.issue_token(request, TokenSubject.CHANGE_EMAIL) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('a name', parent) |
||||
|
||||
with pytest.raises(KeyError): |
||||
resource['unknown hash'] |
||||
|
||||
|
||||
def test_account_resource_get_settings_form(): |
||||
''' test the setup of the settings form''' |
||||
from ordr.resources.account import AccountResource |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('some name', parent) |
||||
form = resource.get_settings_form() |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert len(form.buttons) == 2 |
||||
assert form.buttons[0].title == 'Change Settings' |
||||
assert form.buttons[1].title == 'Cancel' |
||||
|
||||
|
||||
def test_account_resource_get_password_form(): |
||||
''' test the setup of the change password form''' |
||||
from ordr.resources.account import AccountResource |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = AccountResource('some name', parent) |
||||
form = resource.get_password_form() |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert len(form.buttons) == 2 |
||||
assert form.buttons[0].title == 'Change Password' |
||||
assert form.buttons[1].title == 'Cancel' |
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
''' Tests for the root resource ''' |
||||
|
||||
import pytest |
||||
|
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
|
||||
def test_base_child_init(): |
||||
''' test initilization of BaseChildResource ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
|
||||
parent = DummyResource(request='some request') |
||||
resource = BaseChildResource(name='a name', parent=parent) |
||||
|
||||
assert resource.__name__ == 'a name' |
||||
assert resource.__parent__ == parent |
||||
assert resource.request == 'some request' |
||||
|
||||
|
||||
def test_base_child_acl(): |
||||
''' test access controll list of BaseChildResource ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
|
||||
parent = DummyResource(request='some request') |
||||
resource = BaseChildResource(name='a name', parent=parent) |
||||
|
||||
with pytest.raises(NotImplementedError): |
||||
resource.__acl__() |
||||
|
||||
|
||||
def test_base_child_prepare_form(): |
||||
''' test '_prepare_form()' method of BaseChildResource ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
from ordr.schemas.account import RegistrationSchema |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = BaseChildResource('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(): |
||||
''' test '_prepare_form()' method sets correct url ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
from ordr.schemas.account import RegistrationSchema |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = BaseChildResource('a name', parent) |
||||
form = resource._prepare_form(RegistrationSchema, action='/foo') |
||||
|
||||
assert form.action == '/foo' |
||||
|
||||
|
||||
def test_base_child_prepare_form_settings(): |
||||
''' test '_prepare_form()' method uses additional settings ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
from ordr.schemas.account import RegistrationSchema |
||||
import deform |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = BaseChildResource('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) |
||||
|
||||
|
||||
def test_base_child_prepare_form_prefill(): |
||||
''' test '_prepare_form()' method can prefill a form ''' |
||||
from ordr.resources.helpers import BaseChildResource |
||||
from ordr.schemas.account import RegistrationSchema |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
resource = BaseChildResource('a name', parent) |
||||
prefill = { |
||||
'first_name': 'John', |
||||
'last_name': 'Doe', |
||||
'email': 'johndoe@example.com' |
||||
} |
||||
form = resource._prepare_form(RegistrationSchema, prefill=prefill) |
||||
|
||||
assert form['first_name'].cstruct == 'John' |
||||
assert form['last_name'].cstruct == 'Doe' |
||||
assert form['email'].cstruct == 'johndoe@example.com' |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
''' Tests for the root resource ''' |
||||
|
||||
import pytest |
||||
|
||||
from ordr.resources.account import AccountResource |
||||
|
||||
|
||||
def test_root_init(): |
||||
''' test RootResource initialization ''' |
||||
from ordr.resources import RootResource |
||||
root = RootResource('request') |
||||
assert root.__name__ is None |
||||
assert root.__parent__ is None |
||||
assert root.request == 'request' |
||||
|
||||
|
||||
def test_root_acl(): |
||||
''' test access controll list for RootResource ''' |
||||
from pyramid.security import Allow, Everyone, DENY_ALL |
||||
from ordr.resources import RootResource |
||||
root = RootResource(None) |
||||
assert root.__acl__() == [(Allow, Everyone, 'view'), DENY_ALL] |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'key,resource_class', [ |
||||
('account', AccountResource) |
||||
] |
||||
) |
||||
def test_root_getitem(key, resource_class): |
||||
''' test '__getitem__()' method of RootResource ''' |
||||
from ordr.resources import RootResource |
||||
|
||||
root = RootResource(None) |
||||
child = root[key] |
||||
|
||||
assert isinstance(child, resource_class) |
||||
assert child.__name__ == key |
||||
assert child.__parent__ == root |
||||
assert child.request == root.request |
||||
|
||||
|
||||
def test_root_getitem_raises_error(): |
||||
''' test '__getitem__()' method raises KeyError ''' |
||||
from ordr.resources import RootResource |
||||
root = RootResource(None) |
||||
with pytest.raises(KeyError): |
||||
root['unknown child name'] |
@ -0,0 +1,28 @@
@@ -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, action='/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 @@
@@ -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.validators 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.validators 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.validators 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.validators 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.validators 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.validators 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.validators 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.validators 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.validators 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.validators 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,132 @@
@@ -0,0 +1,132 @@
|
||||
import pytest |
||||
|
||||
from pyramid.testing import DummyRequest |
||||
|
||||
from . import app_config, dbsession, get_example_user # noqa: F401 |
||||
|
||||
|
||||
def test_crypt_context_to_settings(): |
||||
''' test the transformation of .ini styles from pyramid to passlib ''' |
||||
from ordr.security import crypt_context_settings_to_string |
||||
|
||||
settings = { |
||||
'no_prefix': 'should not appear', |
||||
'prefix.something': 'left unchanged', |
||||
'prefix.schemes': 'adjust list', |
||||
'prefix.depreceated': 'do, not, adjust, this, list' |
||||
} |
||||
result = crypt_context_settings_to_string(settings, 'prefix.') |
||||
expected_lines = { |
||||
'[passlib]', |
||||
'something = left unchanged', |
||||
'schemes = adjust,list', |
||||
'depreceated = do, not, adjust, this, list', |
||||
} |
||||
|
||||
assert set(result.split('\n')) == expected_lines |
||||
|
||||
|
||||
def test_authentication_policy_authenticated_user_id_no_user(): |
||||
''' test 'authenticated_userid()' returns None if no user is logged in ''' |
||||
from ordr.security import AuthenticationPolicy |
||||
|
||||
ap = AuthenticationPolicy('') |
||||
request = DummyRequest(user=None) |
||||
|
||||
assert ap.authenticated_userid(request) is None |
||||
|
||||
|
||||
def test_authentication_policy_authenticated_user_id_with_user(): |
||||
''' test 'authenticated_userid()' returns id if user is logged in ''' |
||||
from ordr.security import AuthenticationPolicy |
||||
from ordr.models import User |
||||
|
||||
ap = AuthenticationPolicy('') |
||||
request = DummyRequest(user=User(id=123)) |
||||
|
||||
assert ap.authenticated_userid(request) == 123 |
||||
|
||||
|
||||
def test_authentication_policy_effective_principals_no_user(): |
||||
''' test 'effective_principals()' if not user is logged in ''' |
||||
from ordr.security import AuthenticationPolicy |
||||
from pyramid.security import Everyone |
||||
|
||||
request = DummyRequest(user=None) |
||||
ap = AuthenticationPolicy('') |
||||
result = ap.effective_principals(request) |
||||
|
||||
assert result == [Everyone] |
||||
|
||||
|
||||
def test_authentication_policy_effective_principals_with_user(): |
||||
''' test 'effective_principals()' if user is logged in ''' |
||||
from ordr.security import AuthenticationPolicy |
||||
from ordr.models import User, Role |
||||
from pyramid.security import Authenticated, Everyone |
||||
|
||||
ap = AuthenticationPolicy('') |
||||
user = User(id=123, role=Role.PURCHASER) |
||||
request = DummyRequest(user=user) |
||||
result = ap.effective_principals(request) |
||||
expected = [ |
||||
Everyone, |
||||
Authenticated, |
||||
'user:123', |
||||
'role:purchaser', |
||||
'role:user' |
||||
] |
||||
|
||||
assert result == expected |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'uauid,role_name', [ |
||||
(3, 'USER'), |
||||
(4, 'PURCHASER'), |
||||
(5, 'ADMIN'), |
||||
] |
||||
) |
||||
def test_get_user_returns_user(dbsession, uauid, role_name): |
||||
''' test 'get_user()' returns active user ''' |
||||
from ordr.security import get_user |
||||
from ordr.models import Role |
||||
|
||||
# this is a dirty hack, but DummyRequest does not accept setting an |
||||
# unauthenticated_userid |
||||
from pyramid.testing import DummyResource |
||||
request = DummyResource(unauthenticated_userid=uauid, dbsession=dbsession) |
||||
|
||||
user_role = Role[role_name] |
||||
user = get_example_user(user_role) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
assert get_user(request) == user |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'uauid,role_name', [ |
||||
(1, 'UNVALIDATED'), |
||||
(2, 'NEW'), |
||||
(6, 'INACTIVE'), |
||||
(2, 'USER'), |
||||
(None, 'USER'), |
||||
] |
||||
) |
||||
def test_get_user_returns_none(dbsession, uauid, role_name): |
||||
''' test 'get_user()' returns None for an inactive user ''' |
||||
from ordr.security import get_user |
||||
from ordr.models import Role |
||||
|
||||
# this is a dirty hack, but DummyRequest does not accept setting an |
||||
# unauthenticated_userid |
||||
from pyramid.testing import DummyResource |
||||
request = DummyResource(unauthenticated_userid=uauid, dbsession=dbsession) |
||||
|
||||
user_role = Role[role_name] |
||||
user = get_example_user(user_role) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
assert get_user(request) is None |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest |
||||
|
||||
from ... import ( # noqa: F401 |
||||
app_config, |
||||
dbsession, |
||||
get_example_user, |
||||
get_post_request |
||||
) |
||||
|
||||
|
||||
# test for account resource root |
||||
|
||||
def test_account_redirect(dbsession): # noqa: F811 |
||||
''' redirect on root of account resource ''' |
||||
from ordr.views.account import account |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
result = account(None, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
@ -0,0 +1,241 @@
@@ -0,0 +1,241 @@
|
||||
import deform |
||||
import pytest |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
from ... import ( # noqa: F401 |
||||
app_config, |
||||
dbsession, |
||||
get_example_user, |
||||
get_post_request |
||||
) |
||||
|
||||
|
||||
def test_forgotten_password_form(): |
||||
''' test the view for the forgotten password form ''' |
||||
from ordr.resources.account import PasswordResetResource |
||||
from ordr.views.account import forgotten_password_form |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetResource(name=None, parent=parent) |
||||
result = forgotten_password_form(context, None) |
||||
|
||||
assert result == {'formerror': False} |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'identifier', |
||||
['TerryGilliam', 'gilliam@example.com', 'Gilliam@Example.com'] |
||||
) |
||||
def test_forgotten_password_processing_ok(dbsession, identifier): |
||||
''' test the processing of the forgotten password form ''' |
||||
from ordr.models.account import Role, TokenSubject |
||||
from ordr.resources.account import PasswordResetResource |
||||
from ordr.views.account import ( |
||||
forgotten_password_form_processing |
||||
) |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
post_data = { |
||||
'identifier': identifier, |
||||
'send_mail': 'send_mail', |
||||
} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetResource(name=None, parent=parent) |
||||
result = forgotten_password_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//verify' |
||||
|
||||
# a token should be created |
||||
token = user.tokens[0] |
||||
assert token.subject == TokenSubject.RESET_PASSWORD |
||||
|
||||
# a verification email should be sent |
||||
# this is tested in the functional test since request.registry.notify |
||||
# doesn't know about event subscribers in the unittest |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'identifier', |
||||
['', 'GrahamChapman', 'unknown@example.com'] |
||||
) |
||||
def test_forgotten_password_processing_not_ok(dbsession, identifier): |
||||
''' test error processing of the forgotten password form ''' |
||||
from ordr.models.account import Role, Token |
||||
from ordr.resources.account import PasswordResetResource |
||||
from ordr.views.account import ( |
||||
forgotten_password_form_processing |
||||
) |
||||
|
||||
user = get_example_user(Role.UNVALIDATED) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
|
||||
post_data = { |
||||
'identifier': identifier, |
||||
'send_mail': 'send_mail', |
||||
} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetResource(name=None, parent=parent) |
||||
result = forgotten_password_form_processing(context, request) |
||||
|
||||
assert result == {'formerror': True} |
||||
assert dbsession.query(Token).count() == 0 |
||||
|
||||
|
||||
def test_forgotten_password_processing_cancel(dbsession): # noqa: F811 |
||||
''' test the canceling of the forgotten password form ''' |
||||
from ordr.models.account import Token |
||||
from ordr.resources.account import PasswordResetResource |
||||
from ordr.views.account import ( |
||||
forgotten_password_form_processing |
||||
) |
||||
|
||||
post_data = { |
||||
'identifier': 'TerryGilliam', |
||||
'cancel': 'cancel', |
||||
} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetResource(name=None, parent=parent) |
||||
result = forgotten_password_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
||||
assert dbsession.query(Token).count() == 0 |
||||
|
||||
|
||||
def test_forgotten_password_verify_email(): |
||||
''' test the message view for check your email ''' |
||||
from ordr.views.account import forgotten_password_verify_email |
||||
result = forgotten_password_verify_email(None, None) |
||||
assert result == {} |
||||
|
||||
|
||||
def test_forgotten_password_completed(): |
||||
''' test the view for a completed reset process ''' |
||||
from ordr.views.account import forgotten_password_completed |
||||
result = forgotten_password_completed(None, None) |
||||
assert result == {} |
||||
|
||||
|
||||
def test_reset_password_form(): |
||||
''' test reset password form view ''' |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
from ordr.schemas.account import ResetPasswordSchema |
||||
from ordr.views.account import reset_password_form |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetTokenResource(name=None, parent=parent) |
||||
result = reset_password_form(context, None) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, ResetPasswordSchema) |
||||
|
||||
|
||||
def test_reset_password_form_processing_valid(dbsession): # noqa: F811 |
||||
''' test reset password form processing ''' |
||||
from ordr.models.account import User, Role, Token, TokenSubject |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
from ordr.views.account import reset_password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'Lost in La Mancha', |
||||
'password-confirm': 'Lost in La Mancha', |
||||
'__end__': 'password:mapping', |
||||
'change': 'Set New Password' |
||||
} |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
user.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
dbsession.flush() |
||||
token = dbsession.query(Token).first() |
||||
|
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetTokenResource(name=None, parent=parent, model=token) |
||||
result = reset_password_form_processing(context, request) |
||||
|
||||
# return value of function call |
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com/completed' |
||||
|
||||
# password of the user should be updated |
||||
user = dbsession.query(User).filter_by(username='TerryGilliam').first() |
||||
assert user.check_password('Lost in La Mancha') |
||||
|
||||
token_count = dbsession.query(Token).count() |
||||
assert token_count == 0 |
||||
|
||||
|
||||
def test_reset_password_form_processing_invalid_data(dbsession): # noqa: F811 |
||||
''' test reset password form processing ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
from ordr.schemas.account import ResetPasswordSchema |
||||
from ordr.views.account import reset_password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'does not match', |
||||
'password-confirm': 'the confirmation', |
||||
'__end__': 'password:mapping', |
||||
'change': 'Set New Password' |
||||
} |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
user.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
dbsession.flush() |
||||
token = dbsession.query(Token).first() |
||||
|
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetTokenResource(name=None, parent=parent, model=token) |
||||
result = reset_password_form_processing(context, request) |
||||
|
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, ResetPasswordSchema) |
||||
|
||||
|
||||
def test_reset_password_form_processing_cancel(dbsession): # noqa: F811 |
||||
''' test reset password form processing ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
from ordr.resources.account import PasswordResetTokenResource |
||||
from ordr.views.account import reset_password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'Lost in La Mancha', |
||||
'password-confirm': 'Lost in La Mancha', |
||||
'__end__': 'password:mapping', |
||||
'cancel': 'Cancel' |
||||
} |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
user.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||
dbsession.flush() |
||||
token = dbsession.query(Token).first() |
||||
|
||||
parent = DummyResource(request=request) |
||||
context = PasswordResetTokenResource(name=None, parent=parent, model=token) |
||||
result = reset_password_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
import pytest |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
from ordr.models.account import Role |
||||
|
||||
from ... import ( # noqa: F401 |
||||
app_config, |
||||
dbsession, |
||||
get_example_user, |
||||
get_post_request |
||||
) |
||||
|
||||
|
||||
def test_login(): |
||||
''' test the view for the login form ''' |
||||
from ordr.views.account import login |
||||
|
||||
context = DummyResource(nav_active=None) |
||||
result = login(context, None) |
||||
|
||||
assert result == {'loginerror': False} |
||||
assert context.nav_active == 'welcome' |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'role', [Role.USER, Role.PURCHASER, Role.ADMIN] |
||||
) |
||||
def test_check_login_ok(dbsession, role): |
||||
''' test the processing of the login form with valid credentials ''' |
||||
from ordr.views.account import check_login |
||||
|
||||
user = get_example_user(role) |
||||
dbsession.add(user) |
||||
post_data = {'username': user.username, 'password': user.first_name} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
context = DummyResource(nav_active=None) |
||||
result = check_login(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'role', [Role.UNVALIDATED, Role.NEW, Role.INACTIVE] |
||||
) |
||||
def test_check_login_not_activated(dbsession, role): |
||||
''' test the processing of the login form with an inactive user ''' |
||||
from ordr.views.account import check_login |
||||
|
||||
user = get_example_user(role) |
||||
dbsession.add(user) |
||||
post_data = {'username': user.username, 'password': user.first_name} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
context = DummyResource(nav_active=None) |
||||
result = check_login(context, request) |
||||
|
||||
assert result == {'loginerror': True} |
||||
assert context.nav_active == 'welcome' |
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: F811 |
||||
'username,password', [ |
||||
('', ''), |
||||
('TerryGilliam', ''), |
||||
('', 'Terry'), |
||||
('TerryGilliam', 'wrong password'), |
||||
('wrong username', 'Terry'), |
||||
] |
||||
) |
||||
def test_check_login_invalid_credentials(dbsession, username, password): |
||||
''' test the processing of the login form with invalid credentials ''' |
||||
from ordr.views.account import check_login |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
post_data = {'username': username, 'password': password} |
||||
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||
context = DummyResource(nav_active=None) |
||||
result = check_login(context, request) |
||||
|
||||
assert result == {'loginerror': True} |
||||
assert context.nav_active == 'welcome' |
||||
|
||||
|
||||
def test_logout(): |
||||
''' test the logout view ''' |
||||
from ordr.views.account import logout |
||||
|
||||
request = DummyRequest() |
||||
result = logout(None, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
import deform |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
from ... import ( # noqa: F401 |
||||
app_config, |
||||
dbsession, |
||||
get_example_user, |
||||
get_post_request |
||||
) |
||||
|
||||
|
||||
REGISTRATION_FORM_DATA = { |
||||
'username': 'AmyMcDonald', |
||||
'first_name': 'Amy', |
||||
'last_name': 'McDonald', |
||||
'email': 'amy.mcdonald@example.com', |
||||
'__start__': 'password:mapping', |
||||
'password': 'Make Amy McDonald A Rich Girl Fund', |
||||
'password-confirm': 'Make Amy McDonald A Rich Girl Fund', |
||||
'__end__': 'password:mapping', |
||||
'create': 'create account' |
||||
} |
||||
|
||||
|
||||
def test_registration_form(): |
||||
''' test the view for the registration form ''' |
||||
from ordr.resources.account import RegistrationResource |
||||
from ordr.schemas.account import RegistrationSchema |
||||
from ordr.views.account import registration_form |
||||
|
||||
request = DummyRequest() |
||||
parent = DummyResource(request=request) |
||||
context = RegistrationResource(name=None, parent=parent) |
||||
result = registration_form(context, None) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, RegistrationSchema) |
||||
|
||||
|
||||
def test_registration_form_valid(dbsession): # noqa: F811 |
||||
''' test processing the registration form with valid data ''' |
||||
from ordr.models.account import User, Role, TokenSubject |
||||
from ordr.resources.account import RegistrationResource |
||||
from ordr.views.account import registration_form_processing |
||||
|
||||
data = REGISTRATION_FORM_DATA.copy() |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
parent = DummyResource(request=request) |
||||
context = RegistrationResource(name=None, parent=parent) |
||||
result = registration_form_processing(context, request) |
||||
|
||||
# return value of function call |
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//verify' |
||||
|
||||
# user should be added to database |
||||
user = dbsession.query(User).first() |
||||
assert user.username == data['username'] |
||||
assert user.first_name == data['first_name'] |
||||
assert user.last_name == data['last_name'] |
||||
assert user.email == data['email'] |
||||
assert user.check_password(data['password']) |
||||
assert user.role == Role.UNVALIDATED |
||||
|
||||
# a token should be created |
||||
token = user.tokens[0] |
||||
assert token.subject == TokenSubject.REGISTRATION |
||||
|
||||
# a verification email should be sent |
||||
# this is tested in the functional test since request.registry.notify |
||||
# doesn't know about event subscribers in the unittest |
||||
|
||||
|
||||
def test_registration_form_invalid(dbsession): # noqa: F811 |
||||
''' test processing registration form with invalid data ''' |
||||
from ordr.views.account import registration_form_processing |
||||
from ordr.resources.account import RegistrationResource |
||||
|
||||
data = REGISTRATION_FORM_DATA.copy() |
||||
data['email'] = 'not an email address' |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
parent = DummyResource(request=request) |
||||
context = RegistrationResource(name=None, parent=parent) |
||||
result = registration_form_processing(context, request) |
||||
|
||||
assert result['form'].error is not None |
||||
|
||||
|
||||
def test_registration_form_no_create_button(dbsession): # noqa: F811 |
||||
''' test processing registration form, create button not clicked ''' |
||||
from ordr.views.account import registration_form_processing |
||||
from ordr.resources.account import RegistrationResource |
||||
|
||||
data = REGISTRATION_FORM_DATA.copy() |
||||
data.pop('create') |
||||
request = get_post_request(data, dbsession=dbsession) |
||||
parent = DummyResource(request=request) |
||||
context = RegistrationResource(name=None, parent=parent) |
||||
result = registration_form_processing(context, request) |
||||
|
||||
assert result.location == 'http://example.com//' |
||||
|
||||
|
||||
def test_registration_verify_email(): |
||||
''' test the view displaying that a verifcation email has been sent ''' |
||||
from ordr.views.account import registration_verify_email |
||||
result = registration_verify_email(None, None) |
||||
assert result == {} |
||||
|
||||
|
||||
def test_registration_completed(dbsession): # noqa: F811 |
||||
''' test the view for the completed registration process ''' |
||||
from ordr.models.account import User, Role, Token, TokenSubject |
||||
from ordr.views.account import registration_completed |
||||
|
||||
request = DummyRequest(dbsession=dbsession) |
||||
user = get_example_user(Role.UNVALIDATED) |
||||
user.issue_token(request, TokenSubject.REGISTRATION) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
token = user.tokens[0] |
||||
context = DummyResource(model=token) |
||||
result = registration_completed(context, request) |
||||
|
||||
assert result == {} |
||||
assert user.role == Role.NEW |
||||
assert dbsession.query(Token).count() == 0 |
||||
assert dbsession.query(User).count() == 1 |
@ -0,0 +1,300 @@
@@ -0,0 +1,300 @@
|
||||
import deform |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest, DummyResource |
||||
|
||||
from ... import ( # noqa: F401 |
||||
app_config, |
||||
dbsession, |
||||
get_example_user, |
||||
get_post_request |
||||
) |
||||
|
||||
|
||||
def test_settings_form(): |
||||
''' tests for displaying the settings form ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.schemas.account import SettingsSchema |
||||
from ordr.views.account import settings_form |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = DummyRequest(user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
result = settings_form(context, request) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, SettingsSchema) |
||||
|
||||
|
||||
def test_settings_form_processing_valid_data(dbsession): # noqa: F811 |
||||
''' tests for processing the settings form |
||||
|
||||
The data is valid, but no email change requested |
||||
''' |
||||
from ordr.models.account import Role, Token, User |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.views.account import settings_form_processing |
||||
|
||||
data = { |
||||
'username': 'TerryG', |
||||
'first_name': 'Amy', |
||||
'last_name': 'McDonald', |
||||
'email': 'gilliam@example.com', |
||||
'confirmation': 'Terry', |
||||
'change': 'Change Settings' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
request = get_post_request(data=data, dbsession=dbsession, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
request.context = context |
||||
result = settings_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
||||
|
||||
account = dbsession.query(User).first() |
||||
assert account.username == 'TerryGilliam' |
||||
assert account.first_name == 'Amy' |
||||
assert account.last_name == 'McDonald' |
||||
assert account.email == 'gilliam@example.com' |
||||
assert dbsession.query(Token).count() == 0 |
||||
|
||||
|
||||
def test_settings_form_processing_mail_change(dbsession): # noqa: F811 |
||||
''' tests for processing the settings form |
||||
|
||||
The data is valid and an email change is requested |
||||
''' |
||||
from ordr.models.account import Role, Token, TokenSubject, User |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.views.account import settings_form_processing |
||||
|
||||
data = { |
||||
'username': 'TerryG', |
||||
'first_name': 'Amy', |
||||
'last_name': 'McDonald', |
||||
'email': 'amy@example.com', |
||||
'confirmation': 'Terry', |
||||
'change': 'Change Settings' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
request = get_post_request(data=data, dbsession=dbsession, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
request.context = context |
||||
result = settings_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//verify' |
||||
|
||||
account = dbsession.query(User).first() |
||||
assert account.username == 'TerryGilliam' |
||||
assert account.first_name == 'Amy' |
||||
assert account.last_name == 'McDonald' |
||||
assert account.email == 'gilliam@example.com' |
||||
|
||||
token = dbsession.query(Token).first() |
||||
assert token.subject == TokenSubject.CHANGE_EMAIL |
||||
assert token.payload == {'email': 'amy@example.com'} |
||||
|
||||
# a verification email should be sent |
||||
# this is tested in the functional test since request.registry.notify |
||||
# doesn't know about event subscribers in the unittest |
||||
|
||||
|
||||
def test_settings_form_processing_invalid_data(dbsession): # noqa: F811 |
||||
''' tests for processing the settings form with invalid data ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.schemas.account import SettingsSchema |
||||
from ordr.views.account import settings_form_processing |
||||
|
||||
data = { |
||||
'username': 'TerryG', |
||||
'first_name': 'Amy', |
||||
'last_name': 'McDonald', |
||||
'email': 'this is not an email address', |
||||
'confirmation': 'Terry', |
||||
'change': 'Change Settings' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
request = get_post_request(data=data, dbsession=dbsession, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
request.context = context |
||||
result = settings_form_processing(context, request) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, SettingsSchema) |
||||
|
||||
|
||||
def test_settings_form_processing_cancel(dbsession): # noqa: F811 |
||||
''' tests for processing the settings form with invalid data ''' |
||||
from ordr.models.account import Role, User |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.views.account import settings_form_processing |
||||
|
||||
data = { |
||||
'username': 'TerryG', |
||||
'first_name': 'Amy', |
||||
'last_name': 'McDonald', |
||||
'email': 'this is not an email address', |
||||
'confirmation': 'Terry', |
||||
'cancel': 'cancel' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
dbsession.add(user) |
||||
request = get_post_request(data=data, dbsession=dbsession, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
request.context = context |
||||
result = settings_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
||||
|
||||
account = dbsession.query(User).first() |
||||
assert account.first_name == 'Terry' |
||||
|
||||
|
||||
def test_verify_email_change(dbsession): # noqa: F811 |
||||
''' tests for processing the change password form ''' |
||||
from ordr.models.account import Role, Token, TokenSubject |
||||
from ordr.views.account import verify_email_change |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = DummyRequest(dbsession=dbsession, user=user) |
||||
|
||||
user.issue_token( |
||||
request, |
||||
TokenSubject.CHANGE_EMAIL, |
||||
{'email': 'amy@example.com'} |
||||
) |
||||
dbsession.add(user) |
||||
dbsession.flush() |
||||
token = dbsession.query(Token).first() |
||||
context = DummyResource(model=token) |
||||
|
||||
result = verify_email_change(context, request) |
||||
assert result == {} |
||||
assert user.email == 'amy@example.com' |
||||
assert dbsession.query(Token).count() == 0 |
||||
|
||||
|
||||
def test_password_form(): |
||||
''' tests for displaying the change password form ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.schemas.account import ChangePasswordSchema |
||||
from ordr.views.account import password_form |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = DummyRequest(user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
result = password_form(context, request) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, ChangePasswordSchema) |
||||
|
||||
|
||||
def test_password_form_processing_valid(dbsession): # noqa: F811 |
||||
''' tests for processing the change password form ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.views.account import password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'Make Amy McDonald A Rich Girl Fund', |
||||
'password-confirm': 'Make Amy McDonald A Rich Girl Fund', |
||||
'__end__': 'password:mapping', |
||||
'confirmation': 'Terry', |
||||
'change': 'Change Password' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = get_post_request(data=data, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
result = password_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//changed' |
||||
assert not user.check_password('Terry') |
||||
assert user.check_password('Make Amy McDonald A Rich Girl Fund') |
||||
|
||||
|
||||
def test_password_form_processing_invalid(dbsession): # noqa: F811 |
||||
''' tests for processing the change password form ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.schemas.account import ChangePasswordSchema |
||||
from ordr.views.account import password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'Make Amy McDonald A Rich Girl Fund', |
||||
'password-confirm': 'Make Amy McDonald A Rich Girl Fund', |
||||
'__end__': 'password:mapping', |
||||
'confirmation': 'not the right password for confirmation', |
||||
'change': 'Change Password' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = get_post_request(data=data, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
result = password_form_processing(context, request) |
||||
form = result['form'] |
||||
|
||||
assert isinstance(form, deform.Form) |
||||
assert isinstance(form.schema, ChangePasswordSchema) |
||||
assert user.check_password('Terry') |
||||
|
||||
|
||||
def test_password_form_processing_cancel(dbsession): # noqa: F811 |
||||
''' tests canceling the change password form ''' |
||||
from ordr.models.account import Role |
||||
from ordr.resources.account import AccountResource |
||||
from ordr.views.account import password_form_processing |
||||
|
||||
data = { |
||||
'__start__': 'password:mapping', |
||||
'password': 'Make Amy McDonald A Rich Girl Fund', |
||||
'password-confirm': 'Make Amy McDonald A Rich Girl Fund', |
||||
'__end__': 'password:mapping', |
||||
'confirmation': 'Terry', |
||||
'cancel': 'cancel' |
||||
} |
||||
|
||||
user = get_example_user(Role.USER) |
||||
request = get_post_request(data=data, user=user) |
||||
parent = DummyResource(request=request) |
||||
context = AccountResource(None, parent) |
||||
result = password_form_processing(context, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == 'http://example.com//' |
||||
assert user.check_password('Terry') |
||||
|
||||
|
||||
def test_password_changed(): |
||||
''' show password has changed message ''' |
||||
from ordr.views.account import password_changed |
||||
result = password_changed(None, None) |
||||
assert result == {} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
from pyramid.testing import DummyRequest |
||||
|
||||
|
||||
def test_404(): |
||||
''' test the file not found view ''' |
||||
from ordr.views.errors import notfound_view |
||||
|
||||
request = DummyRequest() |
||||
result = notfound_view(None, request) |
||||
|
||||
assert result == {} |
||||
assert request.response.status == '404 Not Found' |
||||
|
||||
|
||||
def test_token_expired(): |
||||
''' test the token expired found view ''' |
||||
from ordr.views.errors import token_expired |
||||
|
||||
request = DummyRequest() |
||||
result = token_expired(None, request) |
||||
|
||||
assert result == {} |
||||
assert request.response.status == '410 Gone' |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
import pytest |
||||
|
||||
from pyramid.httpexceptions import HTTPFound |
||||
from pyramid.testing import DummyRequest |
||||
|
||||
|
||||
from .. import app_config, dbsession, get_example_user # noqa: F401 |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
'user,location', |
||||
[(None, '/account/login'), ('someone', '/orders')] |
||||
) |
||||
def test_welcome(user, location): |
||||
''' test redirects on web root ''' |
||||
from ordr.views.pages import welcome |
||||
|
||||
request = DummyRequest(user=user) |
||||
result = welcome(None, request) |
||||
|
||||
assert isinstance(result, HTTPFound) |
||||
assert result.location == f'http://example.com/{location}' |
||||
|
||||
|
||||
def test_faq(): |
||||
''' test the view for the faq page ''' |
||||
from ordr.views.pages import faq |
||||
result = faq(None, None) |
||||
assert result == {} |
Reference in new issue