summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-02-14 17:47:48 -0600
committerMichael Merickel <michael@merickel.org>2016-02-14 17:47:48 -0600
commit00b2c691f88fcf42dfc81222aed939833f7f1f05 (patch)
treeae8970a9147d8b4be83c1e7cb9f5f71220a168a2 /docs/tutorials/wiki2
parentda5ebc28c38ea32ad99389b5bc23e2f847af8047 (diff)
downloadpyramid-00b2c691f88fcf42dfc81222aed939833f7f1f05.tar.gz
pyramid-00b2c691f88fcf42dfc81222aed939833f7f1f05.tar.bz2
pyramid-00b2c691f88fcf42dfc81222aed939833f7f1f05.zip
implement the authentication example code
Diffstat (limited to 'docs/tutorials/wiki2')
-rw-r--r--docs/tutorials/wiki2/src/authentication/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/authentication/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authentication/README.txt14
-rw-r--r--docs/tutorials/wiki2/src/authentication/development.ini73
-rw-r--r--docs/tutorials/wiki2/src/authentication/production.ini62
-rw-r--r--docs/tutorials/wiki2/src/authentication/setup.py49
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/__init__.py13
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/routes.py8
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/security.py28
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja264
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/tests.py65
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py44
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py76
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py7
29 files changed, 933 insertions, 0 deletions
diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
new file mode 100644
index 000000000..42cd299b5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
new file mode 100644
index 000000000..68f430110
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -0,0 +1,14 @@
+tutorial README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/python setup.py develop
+
+- $VENV/bin/initialize_tutorial_db development.ini
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini
new file mode 100644
index 000000000..f3079727e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/development.ini
@@ -0,0 +1,73 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_tm
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+auth.secret = seekrit
+
+# 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
+host = 127.0.0.1
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[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
diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini
new file mode 100644
index 000000000..686dba48a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/production.ini
@@ -0,0 +1,62 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+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/tutorial.sqlite
+
+auth.secret = real-seekrit
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[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
diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py
new file mode 100644
index 000000000..c342c1aba
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/setup.py
@@ -0,0 +1,49 @@
+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 = [
+ 'bcrypt',
+ 'docutils',
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ ]
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ 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 wsgi bfg pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = tutorial:main
+ [console_scripts]
+ initialize_tutorial_db = tutorial.scripts.initializedb:main
+ """,
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
new file mode 100644
index 000000000..f5c033b8b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
@@ -0,0 +1,13 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+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 .page import Page # flake8: noqa
+from .user import 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):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ 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
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/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)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
new file mode 100644
index 000000000..6fb32a1b2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
new file mode 100644
index 000000000..cb747244f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
@@ -0,0 +1,8 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
new file mode 100644
index 000000000..f3c0a6fef
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
@@ -0,0 +1,57 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+from ..models import Page, 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)
+
+ 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)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
new file mode 100644
index 000000000..24035c8b9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
@@ -0,0 +1,28 @@
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css
new file mode 100644
index 000000000..0d25de5b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css
@@ -0,0 +1 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..44d14304e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!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">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+
+ <!-- Bootstrap core CSS -->
+ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ <div class="content">
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
+ <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
+ </body>
+</html>
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
new file mode 100644
index 000000000..c54945c28
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
@@ -0,0 +1,65 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
+
+ def tearDown(self):
+ from .models.meta import Base
+
+ testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
new file mode 100644
index 000000000..d3db34132
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
@@ -0,0 +1,44 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
new file mode 100644
index 000000000..55aa74d04
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -0,0 +1,76 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPForbidden,
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2')
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound('No such page')
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=pagename)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
+def edit_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ user = request.user
+ if user is None or (user.role != 'editor' and page.creator != user):
+ raise HTTPForbidden
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=pagename),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
+def add_page(request):
+ user = request.user
+ if user is None or user.role not in ('editor', 'basic'):
+ raise HTTPForbidden
+ pagename = request.matchdict['pagename']
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}