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,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 @@ |
|||||||
|
.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 @@ |
|||||||
|
[[source]] |
||||||
|
|
||||||
|
url = "https://pypi.python.org/simple" |
||||||
|
verify_ssl = true |
||||||
|
name = "pypi" |
||||||
|
|
||||||
|
|
||||||
|
[packages] |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[dev-packages] |
||||||
|
|
@ -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 @@ |
|||||||
|
### |
||||||
|
# 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 @@ |
|||||||
|
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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
$(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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
### |
||||||
|
# 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 @@ |
|||||||
|
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 @@ |
|||||||
|
[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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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,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 @@ |
|||||||
|
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,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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
''' 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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