From 16954417a18d2fed478253d3fdaff76888fbc785 Mon Sep 17 00:00:00 2001 From: Ted Callahan Date: Wed, 21 Dec 2016 19:50:53 -0800 Subject: [PATCH 001/144] Updated README for project description. --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3848fee..1045fe6 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -# CF401-Project-1---PyListener \ No newline at end of file +# CF401-Project-1: PyListener + +This is the repository for the CF 401 Project 1. + +Members of this project are: + * Ted Callahan + * Maelle Vance + * Rick Valenzuela + +The purpose of this project is to create a visual communication tool in the form of a web based app intended for use on mobile platforms to assist users with communication disabilities. + +Our Github Working Scheme will be comprised as follows: + - master branch: deployment + - staging branch: staging/testing + - feature branches: by feature, using descriptive names for branch naming. + - extensive readme + +Project Management, ticketing, issue tracking, etc. will be handled via Waffle. From 0d4db3e345c7e1d863422f72bb6ac116e00f516e Mon Sep 17 00:00:00 2001 From: Ted Callahan Date: Wed, 21 Dec 2016 19:51:12 -0800 Subject: [PATCH 002/144] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1045fe6..586e1ea 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,5 @@ Our Github Working Scheme will be comprised as follows: - master branch: deployment - staging branch: staging/testing - feature branches: by feature, using descriptive names for branch naming. - - extensive readme Project Management, ticketing, issue tracking, etc. will be handled via Waffle. From afe84896906ad64a13ed95523542ce8df6ee7963 Mon Sep 17 00:00:00 2001 From: Ted Callahan Date: Tue, 3 Jan 2017 20:52:36 -0800 Subject: [PATCH 003/144] Added user stories. --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 586e1ea..92d1124 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CF401-Project-1: PyListener -This is the repository for the CF 401 Project 1. +This is the repository for the CF 401 Python Project 1. Members of this project are: * Ted Callahan @@ -15,3 +15,20 @@ Our Github Working Scheme will be comprised as follows: - feature branches: by feature, using descriptive names for branch naming. Project Management, ticketing, issue tracking, etc. will be handled via Waffle. + +# User Stories + +As a... +* ...child with Apraxia I want a simple communication tool to help me communicate with my parents and other adults I commonly interact with (e.g. teachers). + +* ...stroke victim with Apraxia I want a simple communication tool to help me communicate with caregivers and family by means of written text, email messages, or SMS. + +* ...parent or guardian of a child with Apraxia, I want a communication tool I can configure to enable my child to better communicate their needs, wants, and feelings to me directly or remotely. + +* ...family member of an adult with Apraxia, I want a communication tool I can configure to enable my loved one to communicate their needs, wants, and feelings to me directly or remotely. + +* ...professional caregiver of an adult with Apraxia, I want a communication tool I can configure to enable my patient to communicate their medical and personal needs and wants to me directly or remotely. + +* ...developer, I want to develop a web based application that utilizes remote communication (e.g. SMS and email). + +* ...developer, I want to develop a web based application that utilizes a common web based architecture to create a meaningful user experience. From ebc2ceda694d264c3fc3ce755310c8a588055bb5 Mon Sep 17 00:00:00 2001 From: Rick Valenzuela Date: Thu, 5 Jan 2017 08:27:45 -0800 Subject: [PATCH 004/144] Update README.md --- README.md | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 92d1124..8a91924 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,45 @@ Project Management, ticketing, issue tracking, etc. will be handled via Waffle. # User Stories -As a... -* ...child with Apraxia I want a simple communication tool to help me communicate with my parents and other adults I commonly interact with (e.g. teachers). +As a child with Apraxia I want a simple communication tool to help me communicate with my parents and other adults I commonly interact with (e.g. teachers). -* ...stroke victim with Apraxia I want a simple communication tool to help me communicate with caregivers and family by means of written text, email messages, or SMS. +Task estimations: +* build pages for viewing People, Categories, and Attributes, as well as communication choices. Estimated relative effort (scale of 1-10): 3 -* ...parent or guardian of a child with Apraxia, I want a communication tool I can configure to enable my child to better communicate their needs, wants, and feelings to me directly or remotely. +* Pass data from one page to next, persisting user choices to build the complete message to be conveyed. Estimated effort: 4 -* ...family member of an adult with Apraxia, I want a communication tool I can configure to enable my loved one to communicate their needs, wants, and feelings to me directly or remotely. +* Send SMS: 1 +* Send email. Estimated effort: 2 +* Print to screen: 1 -* ...professional caregiver of an adult with Apraxia, I want a communication tool I can configure to enable my patient to communicate their medical and personal needs and wants to me directly or remotely. +As a stroke victim with Apraxia I want a simple communication tool to help me communicate with caregivers and family by means of written text, email messages, or SMS. -* ...developer, I want to develop a web based application that utilizes remote communication (e.g. SMS and email). +Task estimations: same as above -* ...developer, I want to develop a web based application that utilizes a common web based architecture to create a meaningful user experience. +As a parent or guardian of a child with Apraxia, I want a communication tool I can configure to enable my child to better communicate their needs, wants, and feelings to me directly or remotely. + +Task estimations: As above plus: +Initial setup: +* user name, superuser name. Effort: 3 +* Address Book: people who will be contacted. Effort: 4 +* managing and customization options (e.g., categories and attributes). Effort: 4 +* instituting basic default options. Effort: 2 +* photo uploads, for user and address book info and custom categories/attributes. Effort: ∞ + +As a family member of an adult with Apraxia, I want a communication tool I can configure to enable my loved one to communicate their needs, wants, and feelings to me directly or remotely. + +Task estimations: As above. + +As a professional caregiver of an adult with Apraxia, I want a communication tool I can configure to enable my patient to communicate their medical and personal needs and wants to me directly or remotely. + +Task estimations: As above. + +As a developer, I want to develop a web-based application that utilizes remote communication (e.g. SMS and email). + +Task estimations: noted above: +* Send SMS: 1 +* Send email. Estimated effort: 2 + +As a developer, I want to develop a web-based application that utilizes a common web-based architecture to create a meaningful user experience. + +* Task estimation: All of the above From 3848ac1837f61dd3e92f38b00d7e9c85eb8c3493 Mon Sep 17 00:00:00 2001 From: Ted Callahan Date: Mon, 9 Jan 2017 08:50:32 -0800 Subject: [PATCH 005/144] Initial commit, scaffold created. --- pylistener/.coveragerc | 3 + pylistener/CHANGES.txt | 4 + pylistener/MANIFEST.in | 2 + pylistener/README.txt | 14 ++ pylistener/development.ini | 70 ++++++++ pylistener/production.ini | 60 +++++++ pylistener/pylistener/__init__.py | 12 ++ pylistener/pylistener/models/__init__.py | 73 +++++++++ pylistener/pylistener/models/meta.py | 16 ++ pylistener/pylistener/models/mymodel.py | 18 ++ pylistener/pylistener/routes.py | 3 + pylistener/pylistener/scripts/__init__.py | 1 + pylistener/pylistener/scripts/initializedb.py | 45 +++++ .../pylistener/static/pyramid-16x16.png | Bin 0 -> 1319 bytes pylistener/pylistener/static/pyramid.png | Bin 0 -> 12901 bytes pylistener/pylistener/static/theme.css | 154 ++++++++++++++++++ pylistener/pylistener/templates/404.jinja2 | 8 + pylistener/pylistener/templates/layout.jinja2 | 66 ++++++++ .../pylistener/templates/mytemplate.jinja2 | 8 + pylistener/pylistener/tests.py | 65 ++++++++ pylistener/pylistener/views/__init__.py | 0 pylistener/pylistener/views/default.py | 33 ++++ pylistener/pylistener/views/notfound.py | 7 + pylistener/pytest.ini | 3 + pylistener/setup.py | 55 +++++++ 25 files changed, 720 insertions(+) create mode 100644 pylistener/.coveragerc create mode 100644 pylistener/CHANGES.txt create mode 100644 pylistener/MANIFEST.in create mode 100644 pylistener/README.txt create mode 100644 pylistener/development.ini create mode 100644 pylistener/production.ini create mode 100644 pylistener/pylistener/__init__.py create mode 100644 pylistener/pylistener/models/__init__.py create mode 100644 pylistener/pylistener/models/meta.py create mode 100644 pylistener/pylistener/models/mymodel.py create mode 100644 pylistener/pylistener/routes.py create mode 100644 pylistener/pylistener/scripts/__init__.py create mode 100644 pylistener/pylistener/scripts/initializedb.py create mode 100644 pylistener/pylistener/static/pyramid-16x16.png create mode 100644 pylistener/pylistener/static/pyramid.png create mode 100644 pylistener/pylistener/static/theme.css create mode 100644 pylistener/pylistener/templates/404.jinja2 create mode 100644 pylistener/pylistener/templates/layout.jinja2 create mode 100644 pylistener/pylistener/templates/mytemplate.jinja2 create mode 100644 pylistener/pylistener/tests.py create mode 100644 pylistener/pylistener/views/__init__.py create mode 100644 pylistener/pylistener/views/default.py create mode 100644 pylistener/pylistener/views/notfound.py create mode 100644 pylistener/pytest.ini create mode 100644 pylistener/setup.py diff --git a/pylistener/.coveragerc b/pylistener/.coveragerc new file mode 100644 index 0000000..04d1057 --- /dev/null +++ b/pylistener/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = pylistener +omit = pylistener/test* diff --git a/pylistener/CHANGES.txt b/pylistener/CHANGES.txt new file mode 100644 index 0000000..35a34f3 --- /dev/null +++ b/pylistener/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/pylistener/MANIFEST.in b/pylistener/MANIFEST.in new file mode 100644 index 0000000..7224bcf --- /dev/null +++ b/pylistener/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include pylistener *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/pylistener/README.txt b/pylistener/README.txt new file mode 100644 index 0000000..4c04cac --- /dev/null +++ b/pylistener/README.txt @@ -0,0 +1,14 @@ +pylistener README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_pylistener_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/pylistener/development.ini b/pylistener/development.ini new file mode 100644 index 0000000..7fdcc43 --- /dev/null +++ b/pylistener/development.ini @@ -0,0 +1,70 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:pylistener + +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 + +sqlalchemy.url = sqlite:///%(here)s/pylistener.sqlite + +# 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/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, pylistener, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_pylistener] +level = DEBUG +handlers = +qualname = pylistener + +[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/pylistener/production.ini b/pylistener/production.ini new file mode 100644 index 0000000..0c879bf --- /dev/null +++ b/pylistener/production.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:pylistener + +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/pylistener.sqlite + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, pylistener, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_pylistener] +level = WARN +handlers = +qualname = pylistener + +[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/pylistener/pylistener/__init__.py b/pylistener/pylistener/__init__.py new file mode 100644 index 0000000..4dab448 --- /dev/null +++ b/pylistener/pylistener/__init__.py @@ -0,0 +1,12 @@ +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.scan() + return config.make_wsgi_app() diff --git a/pylistener/pylistener/models/__init__.py b/pylistener/pylistener/models/__init__.py new file mode 100644 index 0000000..3e721ed --- /dev/null +++ b/pylistener/pylistener/models/__init__.py @@ -0,0 +1,73 @@ +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 .mymodel import MyModel # 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('pylistener.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/pylistener/pylistener/models/meta.py b/pylistener/pylistener/models/meta.py new file mode 100644 index 0000000..0682247 --- /dev/null +++ b/pylistener/pylistener/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.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) diff --git a/pylistener/pylistener/models/mymodel.py b/pylistener/pylistener/models/mymodel.py new file mode 100644 index 0000000..d65a01a --- /dev/null +++ b/pylistener/pylistener/models/mymodel.py @@ -0,0 +1,18 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + +from .meta import Base + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/pylistener/pylistener/routes.py b/pylistener/pylistener/routes.py new file mode 100644 index 0000000..25504ad --- /dev/null +++ b/pylistener/pylistener/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/pylistener/pylistener/scripts/__init__.py b/pylistener/pylistener/scripts/__init__.py new file mode 100644 index 0000000..5bb534f --- /dev/null +++ b/pylistener/pylistener/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/pylistener/pylistener/scripts/initializedb.py b/pylistener/pylistener/scripts/initializedb.py new file mode 100644 index 0000000..7307ecc --- /dev/null +++ b/pylistener/pylistener/scripts/initializedb.py @@ -0,0 +1,45 @@ +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 MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [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) + + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/pylistener/pylistener/static/pyramid-16x16.png b/pylistener/pylistener/static/pyramid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<% zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+! z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$ zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#& zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog? z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3 zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#b�i5=5IxreNAbVsKKM4p@aIXvt)VjX~ zLcj$&PM%O%3~m8hs_+6jp*DiMh>#*THuP7Kuo(0>$o&*`2|it5S+0m8|22g(K^uZ@ z;6o1l6qp_E8Ol2dBLz5X2wDO(`F*c>PlO=RH?}G2hLZu0*R!%E-GVEC+T4e?MR);V z_^jU-j{q4)fSwlDL?FBr6^_xQgu)=RiX|@qmWrjtpcW9eMoGpx>_EeXqAIZV+wop(eQ& zddcwQJrU|q&zm1a_C786I&8KaRWQwHi;?Yq$Niu!>Pxo{x^?XH0JL7G3nMSGE+k(f zUy_Yz(!p+;7({Its{k~zBrv5lr7AiB!al-t5Jn%nl7ESUGkGw&`+$zo+uAQnLLE{> z)bjDzQo)pX%9L+Y8~jzJEXj4L`Kdd};zxK*BpmUzAbJW_l-Xc?DzrF3#ROVvYz1i| zG2!p>JkqTYcZj=4p)#n%c22V_r7crip;Odb+M8J-{$29VK@ZtjSD$_LrTfkfWNmFpri8%bWfq{-bz; zG=eUIHw0<~$?St1Z_;ejM$&fE_SuIT%(amlVYGL(_Z#(C5>wBQegW7bxl~NRGd`Qh@8sO z+`6hk+hoHeiq)PuHG4Tn`%qrZs+LxT_(Bd(Ki{xdzI*yTJu-iUW<)0L8m>OWDT4~* zF$1aATP;{kn}(yBhyLY(G%H_1)}q=hRjbx3!NSzR4{{?Yj)v46H5je}8Uyq(_rMi`oz6O&CSQn6^7ABOjKl`T{3!jW>_L33Rec#ReVI^tJu7RoS3IrvY1S=CWBV} zj(DVYB)Etlmy{64lhVbp^w-RqOvv`h52Wogrgu6?^(V`Yjk~2|lT|VLy;=@*B!r~I z8|W`#Sbe3tvQ^jmt**N;i}CFtk8%5h^!rhlx_72eu`tO&bwSgj$pgA!#!^*MI8xg{ z1);{xPj&iN{yU`!F$wu^-<3|6j#~sZ+%?P!QyGTW(CfbAr|D$wXU}I5X&beeKU2fX zgG|TD(mH9GwWoafEqfywNtsR+sD)f_S-1XC!ZdqS=^Mu0^-kK3?HKXM&yhzT4l@qd zPanHneg{AGa-3PAR(@Wn(phPhch&7}+q&sGjVCclej0Ri%*4SHsn< zivG#tyrZ`6kG}f8qNkFVv6B*?B?^c7qCd^QpIhWA;Y#4_i;5ep-F6tVd)~Ye@x&@W zRD74;dI!Tz#&h{&=#KO}3x)5yd$@PmAOxpk0jGthtmnp|-)tuF z1Tmvv`is|f*M_*fh^p4)UD+SflPZC8Hjg7w~i z(0ycHzisp0{qmAY2ps|UaK_Z-`J%VVf9SpbJPluprYHE#gZtV1+4y8Tj|NGBE~`wi z@_GJl(X6!d`Xp!3V6r~+V{~wf2=hzgeYHYA>}2UAy?BH8kwm4$WaNG1nn&&R*Nd^p z+GwW(`UQ`^uUfv~m z>;IhlXnZ{sdw8O7r;wN(CFtsf_;lq)ZDY2#@hj-(BO9-l&+9uSqP?V+699mW^=F3y zq-Ed(05Fsms+!K4an_%9V_D}HiKIYqFDouet3gNdDqgq~Fhlhumg^ihwjqz23(aGJ`+0c#A)`{X@o%~NfqNYy9ju!UL z7IwDaKm8gS*?n^6Cnx`7=s&-I`RQz7_P>^Fo&FuxYkS4<|x%%;|+Hm0`DPOm)H|7z|vxBnsje@?m?+W*VgUrGE|YPxyZ`@-LQ%osGStsgu(yO@QOyl)q#D)Ytr9GXh*} z|0et${3k)d(c(2y!#{rg$EUwz|J2v|ZwCGj{*CY_^}LD}Zl>0nq86_S{VNJK78X9{ z|0?+>Q^d~N&QZnQ(Ae~kXMa)t2K`g}FFRWQr=7n^{>C&h=5_jHWNB*b{I~1%de#0K z{lbPHng0g!G5=R>zSpt9D`#h7VdgGs=xi#$#=^?Z$im9V@=leFg_nhumy?^1`5!ue z^Wcv}#L?8y+0Ieb&dyrkuP|)>G{NtfUNiMi`M;@r%zx_WZ*}#rqWuefty%%3SLXlR z0R)f+r_;Fr0E#pzQ6W_~sMAcu7M!n%L-`n@_FrK&I5Ctcs4eqYZOO14!psI+MDrsT(i5x*g|To^~3a zG(P=0{jmS|yL#iajCX&owCt=*MaK8c$v*)0zil)1J#>d*NO7`^HAY{0d&~02KKt_%Xg6cfO#nv8b)Ua-40z&0(uXf(uOcr&ku?L3B3P?hlUS z>VhrlISxGTaoh|P?^iWWhV%lXOrhv(^;xDR%1buy`MO;#8IECW(wBj%OM06l;o&Dg z_t{hIHs-|9QteQX6_vQ46z;7-9BzVR&x{29(n4cJ4FDWf=&N)~)lGI=E7`02B6go) z=iiKw-6unW%A7Be3W)ESUXn($gAMbhoKh88cjXAs*zc36EQ}lgSmAairK0&GdX3ZA zwh(VLk^Kt7kZ%j{M3uh4)DPeep>H;(#PQ7%c$oa4rY89lnqJwZvz`woVWhWTw+Yt4 zK4Z?lOIC7fdL{7TL>YLd1VYhMkf)>>sG7<6sCi@j;dDj?-g`#>pw+-!OoAG9N4i|~ zeV<%)x8PqKB+Fotdk_^bJG$@J!o42!876Z5@8~BdYV^iQA4M~MF4<$54r~0Ff_Q1&*4xzmrX1KOYSRpELIw>?DZ)mm zFK_GBShr5fn}g46mJx_Pas|Y>PqDsQE4&b>`1w%aGo;@X@s;Nl*lk5$5MGxo&fZa~ z?)Y9Rmi-z7&KQGc_gS>J^@R5LdoGtFY8iZj&|=_6q0RQ1g;q89e5r$5_4S0&a)DQ` z7*pE~UrO~c)K@vEAM(}0siTPqLN|~uSWfh>==;JS=?6%x67xnVLg0SX0;dw)(QYaD z!fQ;uWtNbQKGxM3j!x(;)P87Q6_D+-sl;lR%V{3cV~^olt7GJBzJR;b=~9(!Rb>Wf zO@=*jvZGFZCSDq<2iV0<23iTJvt#ydEoHlX#ZxXcgrYkL-o%LE_o$6FtCN9 zlcTA@S;BN?S9l{x?dSoWnVOEvAClv9$$|Pd-7H34>`>0(90|@(*RN^71(;U|IgZZ) zZug2l923m((p(=P&l2{WO{6xPmUIw_2oyDR68pd+CusR0wZ5CG&93)<%Lx8~uxYBm ze-G zNw<06f}q@gVA8Qb(}fP=Zu`?e&__018nWFT8S_5OBn%=uSYRS5z!Bb0v0gC5z?M+* z_hR*MbOQQ+cgez@-}LxHbl-C%xo<)%N|8DmlT8`Ch}#1YLRj4BQiMS>rL+&$SErCb zds6l{MUU?;ZrUiKy`0766{bKXSIGo^Ur-X?36(qO3!!T2I~D=KRMED9r}@R{l2M(Nv?cylDF1VI^29xSqn9TeS38h9L$J4&L>Ec<7Uza28R zX>DFt@0RIAo;Z~C(DO?P2CgmMFc7o>af=(<_XSJ7XHM%!_z8lwM47LPb15t<5}EP5 z(mBmYkczz4-Y%%-gr%#24WEK|C<>&1R1{AK*K5Ez1`cC0Dki|i<&CI^$DQ{58q!G1 zPkF)4^*3=x|5^040+&pK3i-8JQXY?kV@V~fF8-~4m7G1M^$kG_!tL0U*FBzY5Ku26 zH+A2H_I;>)FHp=JtWv9jOA~xBFp&B-K?yxJ_m9=}9v>~|dgrdW<2OmV=$Qe3usLq( zLW6Q?4BnLGv923wp)FSzT=P4)zN}C*){RW?sYu>A#ATVSzWl9_XRhmuA0o;OD7 zLxy8cz=xcz4KN*P)($VF2fn0LjQ~pO@)};rCNAwaQ9Olf)P#9+`_O$hLg<%%bMP47 zSgn!5??!W#2%1kVL8EGD-Kmf-L2nP*GpusVngEF=P8VpK5!C(M5ual@B5=GPporHm z=t{(2cJ{dK73HYOa@-jpVoMmZ@KsX#8t(7s2bzMWOw}%oFQ{3_Y;e>?kl;tZ6SSLO zmcn?H9Wl^ru#<={zr-R7C#&^*-!wLmsgvRU#*0Ulnv1C#@Tu4Sf-_WxH&KaiVUmUZ zR5X7Q9J8~eocTMC^+S$XGXTdBFQi;5u< zuUs!xDLz2~RK=mVMtBYMpijukBfad#z-48x%E81X&rY9`$0U%1^mg^? z>TX2JO+)D9AXLYF{F&M<9#$!4+xse7j^4^-9YedAJ4L^F-PJ{;L=WaM z;CK&Bt-!AJK9kVQfq1dGvtVdg#zaF|VRwZD9#FJ8iXkihP* zM@V+&9q|$&`z|z3lYcs6E*-+uM>Hcf>=|$57C!!~BW_baR7XD@psemUQ5y%kr>yd1 zm8R927JKP-M5?1q?ZnPx~YXH0ZFCq=^@gs+bs?4^}Op`zA_CBxkPj6Az z;MI_gpZMYpTd<|@Mb}Fa{lJ|9ss`EgWag+hOWFkr`ZK4QWV<~AlI>!wr0lSSbV~5M?-~y9)8}?rjttq? z?^rV9;r(VVgQc|x4RH8HiBL$Xx&jpLq#W=yr1G14m#KFleBO;R`i+iqmIV?i2Qg|H z!YA+}qi!5KM-5*Ct%AhX7L=b!Fk;WZUfQA=B?fYSdi{{^&I)6!1IbRk93xWB*ioWC z-?tV_L00i(^fhn8M_lA)Ko!YJ()=M8^^mw{;%=H}o12}4K5crtox2ij!9g_=0Xe3< zID(mRnOuw1VR-}i9O*)uJe?#bf3vhkA@etj43hODQnYmrv0UzhO`^lFJ-1}P;Ac?$ zhiHPOZ>h5;`B~^>#-nzw0Fl9#U{xfJP*TACY!(t=8uUk?VW*nFi3j;vW| zxKM;M)HFBQDik}!miY#WErQKTN!Q*z?wcS*9tmabvs|u;E>15Q2dUyGN-b@LD@g*q zxa4N5vkh>HdiAekp$y&A`I`z15?W+r#REcsM!bqP%YJ80Y%MQ7@{cJM z${FeHRrCiGz;e}b{EqW5)pyhjnT+AUs*_%3>c*71W<-mp=jrq=n-|I;DRkz0NMxgPIsuX!Y zR7vp%sdh$oStk(aqcqV?4H9HKi(0<1#1S=HfO_w@=65S|gGcS03Fw+|mgy%zoO{()m0} z>pAmc+1M0RbgWr&WvFv|yn%jBRqVrr_U;2-)vrD~xJ6k6;)b#u<|!;Kc*Tw_WsJMh zh#POWb=0nym|w~>*-(?kSXUNZJJ@{-n~a>(h&!cg!s(_6AI94!uX?&F-##~?~` z-~KI!D`FZ@9lPFp_&LWAt5Ug{V(<6!o2?iv1fuQ-p)HK6;`KD^3gqU5==q~=+u<_{}p9aZZg%uQAoDv{B!5p!x? z?Yz%z9yZ?P(tX@%>`f?*m$JrC*0rn*Sn7>p}n{_>4w}@QdRjgr$IbLT@0B z0Oc`npAp|qbd?1666a>DtY=5;QPnFpX+E7#RCSOyY}y_At!bo}rNiExdV($9kFsJ# z5(^aR!wwzNf+!vZO%`?N+MBYG_=G{CA*6n@*p>QRKVC>-UYv`gn*zqWSE)qvor{J0Y39m3U+bmeAu*LrMJx-`c@_H+Md^&Uao z*%a0orrd6}m9!vAH36E1zL;zOUHC1E`^ECUZDZ_0A~E17Dw>4q33h5q)O+fK&PwW| zpPZl0($)I-E}bki!H0=GZ7=few9+nw7V;7D0u*BJZ#PPu` zlA(UtVA`W21{#bu8Yk`(qWRel%T%s*TEls6i1R98B+yaoxK}IMIrP@NMsRqyf1?yM zVnSn2At^0mnDY#-4Qpjg$drVpRBz3Rs?#6U&N_mc-!h?S-62gqi14bK}%x=*@5ABC$@^)0|}3t=BxoOn@pl_}$J#d;E@;1%Ww zwT%8|h*_+45u3nzqKub-PLF!;LA-(HA_s1gQ+7MFcviUpD8hPx%s0Zb1__vV)25W5 zS8{t2u3;1^9m!;F=SKmH&O9g)-TOhZF0c~7o7DNtsbdX0Tn;$h`NWy72*Mr#gHQS# zxa;YG>k!+`V6gK%<|AxR0=w-BYvnhxV_;6*wa{Yk+{0;B$x3X}7Ts=)lDo|UGy%$3 ze(nY-aNI$*KmL+r5rTu;W1F^>jJy^sKai!dL>Y>OxAuj4P4bm5R=V>w`O3fgBeEjm zd~78>4=abld8DYhhvcp(qB}w@gIuMGrdY2^bMsi?&x!AEHF{hc zg+DNk`;;y^5U@qHYv=l>ylIkSKv`8XcdBRg;rpD%iSyg|$79xK4KALACso;aG|J4{ zM=o}BWcpcJ_KWUFN?+hr{n&QgXhF{Bw41yMii;`_47xsc=oiVa{2v8tGY5O%13zW5 zMwu0SaukgGf@_5T!7pGgvWky^G(b8|<3Q8c?2Uxz;%QICsI2NeBU(~c(>lU$tH6(C z!?)hYYB0_>NwsFik*o@lw6(twlS4|#5OmRiv&TD9A6z@RJWNVz&4kkC_)Ex;=+C8% zhHW9oet8E%$zeE0Lx)l| z=wi>IIXK<#^2zteGFqb+r$okBf*p0SN|+e-y7uB{@}g!jRmHwcAD#4Zq9gfyip+yb zg$p2O6nybR$|(m`%9&h-k;~b0WPC*SWDv0 z1&u*-B!%bl&r%mubHAmK3X&*R)ku>#Sn8&-cW zK2P^rNE3`Q0+iXeUh@BCMUQms&JW=t9&VE&B=j{C_Mt z$mL|laTbhQgi+&2b%Qj`DcB8l_!W28*z<;=$}o~q$YZ5ib#+OCo=#EfVrgPSawAWd z%WVsIlIueGb^Y3(soWx^r_q}!j>S~mK`W5qoTfHg!!)Hpx3H`rxF;vV&q!5gyXfe} z0w`eJ`B@bPHZN8O{n}7ct|M7Y)O;n&H{BRtSwk_lvB@p=mUnMu$2+*_ZDam<*g2lV)D*CN+d5a+`ovbJ*R=J07&JhgO=jzjd`-bU(l369g@NNrgdz_k zKoFKRlMxmvxJ37=Bl>ZIpqmbal3z$ZE^S5i&CR0o139PaX1W0jev36_8BKBDBWBgY z+%U+5sdYYFRpUB=UK}(m(IhK;P9vq0l@!jGw&92`OV>^M^F>373n5EmP=f%-E$UZMe?68(8}P<^m%x{pAz(AgGxC!4Ig1;|%a<*AzsN%*Uyc2owx7h3Fy+Jzq=sO^hwGFuo^~ zc{n)Li1;l8scyJNbWB?Usx}BfKosHBm}Jx1lq&Bi_P^1ZB6Q=hU%}Lr@(6cSFhF3B zQL_L=1zoL|{=*JeurG~%BX~^>5)_xqCEUEh>~aQBbQ%)&Ts2gu(hZp+EKRf>%`opE z*rmwaJt+>Mnv1~lc@PIm0c7WFc2l$3h0%AxBnqzgoF!)#MxJ8YmbTp&<@5YFFG3`n zRK)lKO;%E~Gd#h`ByiHOT05kr608$S{`H=nP8or#9#R1(yyX(?t)HXo<_Q?L9UaSu z%VWW5L6SQ5+_`r{T}8_(05a^QeSC0DhQ=4=d@hDHk$`fyPl6_l6ZA%!QeyK~R$5X<@EBPs z6mr#>@yPjP8Aes{u7xe2wZqgBavJ&=D0JT;%Au~&D1+|+O|2bK=&FF;;1CCNjvM6Y2rA1#PB>ldCu^Ct^?5 zjC#04phl(9-(H_Sde@MxP!O|LS0mcfLz5Vu1n6OmnD)X=iqC@>x(L#NvCQ-qo09hM zf0;W)QMG_V`AHR1FjhM=`sw$yR-1(C=)?~$j|$*5_7@oysinfSvsF@)5mlV-Jwt?` zO4MsO^fJy-0uS1*DCG15#`uDLhk5*WF${jJCX$_&vpLZij=-9%Qx{$B1AFBF6n&B9 zvc%BGkJ?v>?M=E|^in=s&8#&xM7x$6#DrASZ_h2tJ!)$teX+Y?t<#m^3PNsCCZSvl zDeIF`JQ2BJt(EA7qaK%&SuoL58S%<(wlzfRf3n|t z!#tm7n;`Nka^>_JNY&)=i1Q9d*Jj&#$i}O2Ib|LL}gk)s3 zZAKLWg#nkq>onjIOpnBUE3gGjyq5YPf|9!OI>C>E9Df}8=GFeSJv{kU1@|1{tDB~c9FCD4zW$$KV*<%O!``!3xt zBX2aSMkfYlXYO$QF!(&hV%3;zU!%?Vk(GI!MolHcs-63phqvybJKhd*jeJ%PyIz=F zxl+8=`GMP2u;N(R)O=P0)A;8^Q&6e<29GZHmKI=`GBo}gxcCa&Um`VpXjpa1J;gdm zBE_l24tcG0WX;7R(gt5Sau=sIsJau`N+Y1}V#T*WntsL4$^nCO2u+=xDKOFBsl&q+c*?onp_Ig$2xVgMqVTsR zA~SQu=?fQ((O*cZ6rfBN(b8z7z5Ob zv;^yp+3g+pGEyAGPyJ)Bz@&f##wb~8Fkugu` zSVE>qA?d=z3+dg3yv`Sxd4dc`a_J^$^43+TWMmub-`(I^rrOr@-gO2y=o#XyoE=$`zXVKEqG0;D@1eH2^^P95q-@m z?VTX;b!IM1ni4IdgWjXef=wpy^j>w@BQc$JKFrzU!2I|uQwI&U9a=5?#{Z;}D|nE> zIP4KYBYTc?FZ}@HV5e62A+-3=mfm&Ctgb8v4w`Ja3mA8_fF3n<9?8vMC z64R^afvmMB*F>Q;Q&+5DK3s2q8H8DB1hM`ukk+R+J^{8I>558d4%IB-C8ca4y`8_?B3 z>Dl+_wKI%`l{_G_My6lTc|h#N>N=pIC(K{@Q!qEhZlw8EAD^> zZT8CnaH6nr&{z*|R^kiBQ+F~Xc#k)Zy@qx=9X;1owIH&e11>e87B%t5{HdoXS2-to zr#8ZScpc~&xA-Rzj|7=3lm?zNxvYzQFKs}`i7PHh*@@1f#4bY=*JAGO$B`KswU6qk z4pv|D(EcXX?%Kgk61Rp87zrFONG1qP5O2PD5;%um4G&L#&VdIksYvht7=x@A%-0}) z#M0klAu-FByr+33Z>(rhsl9r{hKy(dGIb0NO@Upvu zF|hY1SW4V1-iLMxtBVWRX{0+t#+3TzYB|i!p-tu;%KcE(?OVhTZAcDp3R2|PDiGg} zmQ!0&b4?b@50a{16wL8ddl(&o4ZNGif}F!QvwPq4tjrRx^QQg=y*}#STZ+-TgYO3N z>n8i!y?A01eJmuz7c+$#v1s_y%&Dw0zk9lwW>rQ>9Mj!Qi}%{U!*_vy4|JDp04i(n zH?m{#z{^5aANtNPZ6C!y^sYAVmnHpn$N+j6S;F=gHKtH^yZ|^Au9_5R*O_?Qj;zem zxaZs0$F~cllF440*kOy9khe)tt|^BDNJ8Tcu{-DD>$IZ_Kc|8u4!r56T3aoqK?q06 zi?@8kkW6&x!?7cg@NoQyCY%D##F^$x2} zmJs7q`ZtUjtlKHFFz4p;aWhHjF6Um@rktsn?oI1D*-Hd!(Df5N>jAUh#W*oc<&Bi7 zHo%dei>%VPt3hk6MxNi_Es7TtGMdasl<|~f(O$o~A?FarcW*)Fe*vwclnHDZ?}+SP zgYR&kn8RYb9O9><=RQuodIuwAN)ulS;SGy)PX}i^~1UYf0k$b`JnqicQgxToaq?rolg6VA7$>(o?TbE z6jEI3BBqxbUgL{6t@896dly!z7dXPugQXkT$}T@PWqm_3(f}$Agk`G%;50L{=*mi| zTE(qgB%svciozkc)B +

Pyramid Alchemy scaffold

+

404 Page Not Found

+ +{% endblock content %} diff --git a/pylistener/pylistener/templates/layout.jinja2 b/pylistener/pylistener/templates/layout.jinja2 new file mode 100644 index 0000000..81276ea --- /dev/null +++ b/pylistener/pylistener/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/pylistener/pylistener/templates/mytemplate.jinja2 b/pylistener/pylistener/templates/mytemplate.jinja2 new file mode 100644 index 0000000..ade7f2c --- /dev/null +++ b/pylistener/pylistener/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.3.

+
+{% endblock content %} diff --git a/pylistener/pylistener/tests.py b/pylistener/pylistener/tests.py new file mode 100644 index 0000000..599abc3 --- /dev/null +++ b/pylistener/pylistener/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.meta 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'], 'pylistener') + + +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/pylistener/pylistener/views/__init__.py b/pylistener/pylistener/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pylistener/pylistener/views/default.py b/pylistener/pylistener/views/default.py new file mode 100644 index 0000000..5e6c5b2 --- /dev/null +++ b/pylistener/pylistener/views/default.py @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status=500) + return {'one': one, 'project': 'pylistener'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_pylistener_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/pylistener/pylistener/views/notfound.py b/pylistener/pylistener/views/notfound.py new file mode 100644 index 0000000..69d6e28 --- /dev/null +++ b/pylistener/pylistener/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 {} diff --git a/pylistener/pytest.ini b/pylistener/pytest.ini new file mode 100644 index 0000000..df48dc0 --- /dev/null +++ b/pylistener/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = pylistener +python_files = *.py diff --git a/pylistener/setup.py b/pylistener/setup.py new file mode 100644 index 0000000..7c1611f --- /dev/null +++ b/pylistener/setup.py @@ -0,0 +1,55 @@ +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 = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='pylistener', + version='0.0', + description='pylistener', + 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, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = pylistener:main + [console_scripts] + initialize_pylistener_db = pylistener.scripts.initializedb:main + """, + ) From e35da7591e189e26daf6ac5b16c99fb386e4e69b Mon Sep 17 00:00:00 2001 From: Ted Callahan Date: Mon, 9 Jan 2017 12:54:39 -0800 Subject: [PATCH 006/144] Initial Commit of roughed out structure. Added routes.py, security.py, .css files, jinja2 templates, and updated views and setup.py --- pylistener/pylistener/__init__.py | 1 + pylistener/pylistener/routes.py | 7 + pylistener/pylistener/security.py | 40 ++ pylistener/pylistener/static/base.css | 0 pylistener/pylistener/static/layout.css | 0 pylistener/pylistener/static/model.css | 0 pylistener/pylistener/static/reset.css | 47 ++ .../pylistener/templates/display.jinja2 | 0 pylistener/pylistener/templates/layout.jinja2 | 50 +- pylistener/pylistener/templates/login.jinja2 | 4 + pylistener/pylistener/templates/main.jinja2 | 4 + pylistener/pylistener/templates/manage.jinja2 | 4 + .../pylistener/templates/mytemplate.jinja2 | 8 - .../pylistener/templates/register.jinja2 | 4 + pylistener/pylistener/tests.py | 520 ++++++++++++++++-- pylistener/pylistener/views/default.py | 46 +- pylistener/setup.py | 9 +- 17 files changed, 646 insertions(+), 98 deletions(-) create mode 100644 pylistener/pylistener/security.py create mode 100644 pylistener/pylistener/static/base.css create mode 100644 pylistener/pylistener/static/layout.css create mode 100644 pylistener/pylistener/static/model.css create mode 100644 pylistener/pylistener/static/reset.css create mode 100644 pylistener/pylistener/templates/display.jinja2 create mode 100644 pylistener/pylistener/templates/login.jinja2 create mode 100644 pylistener/pylistener/templates/main.jinja2 create mode 100644 pylistener/pylistener/templates/manage.jinja2 delete mode 100644 pylistener/pylistener/templates/mytemplate.jinja2 create mode 100644 pylistener/pylistener/templates/register.jinja2 diff --git a/pylistener/pylistener/__init__.py b/pylistener/pylistener/__init__.py index 4dab448..f5c033b 100644 --- a/pylistener/pylistener/__init__.py +++ b/pylistener/pylistener/__init__.py @@ -8,5 +8,6 @@ def main(global_config, **settings): config.include('pyramid_jinja2') config.include('.models') config.include('.routes') + config.include('.security') config.scan() return config.make_wsgi_app() diff --git a/pylistener/pylistener/routes.py b/pylistener/pylistener/routes.py index 25504ad..9a6706d 100644 --- a/pylistener/pylistener/routes.py +++ b/pylistener/pylistener/routes.py @@ -1,3 +1,10 @@ def includeme(config): config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('manage', '/manage') + config.add_route('register', '/register') + config.add_route('categories', '/categories') + config.add_route('attributes', '/attributes') + config.add_route('display', '/display') diff --git a/pylistener/pylistener/security.py b/pylistener/pylistener/security.py new file mode 100644 index 0000000..c392426 --- /dev/null +++ b/pylistener/pylistener/security.py @@ -0,0 +1,40 @@ +import os +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import Allow, Authenticated +from pyramid.session import SignedCookieSessionFactory + +from passlib.apps import custom_app_context as pwd_context + + +class NewRoot(object): + def __init__(self, request): + self.request = request + + """TODO: create second level of authentication?""" + __acl__ = [ + (Allow, Authenticated, 'guardian'), + ] + + +def check_credentials(request): + """Return True if correct username and password, else False.""" + pass + + +def includeme(config): + """Pyramid security configuration.""" + auth_secret = os.environ.get("AUTH_SECRET", "potato") + authn_policy = AuthTktAuthenticationPolicy( + secret=auth_secret, + hashalg="sha512" + ) + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.set_root_factory(NewRoot) + # Session stuff for CSRF Protection + session_secret = os.environ.get("SESSION_SECRET", "itsaseekrit") + session_factory = SignedCookieSessionFactory(session_secret) + config.set_session_factory(session_factory) + config.set_default_csrf_options(require_csrf=True) diff --git a/pylistener/pylistener/static/base.css b/pylistener/pylistener/static/base.css new file mode 100644 index 0000000..e69de29 diff --git a/pylistener/pylistener/static/layout.css b/pylistener/pylistener/static/layout.css new file mode 100644 index 0000000..e69de29 diff --git a/pylistener/pylistener/static/model.css b/pylistener/pylistener/static/model.css new file mode 100644 index 0000000..e69de29 diff --git a/pylistener/pylistener/static/reset.css b/pylistener/pylistener/static/reset.css new file mode 100644 index 0000000..daebc53 --- /dev/null +++ b/pylistener/pylistener/static/reset.css @@ -0,0 +1,47 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/pylistener/pylistener/templates/display.jinja2 b/pylistener/pylistener/templates/display.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/pylistener/pylistener/templates/layout.jinja2 b/pylistener/pylistener/templates/layout.jinja2 index 81276ea..59fe96e 100644 --- a/pylistener/pylistener/templates/layout.jinja2 +++ b/pylistener/pylistener/templates/layout.jinja2 @@ -5,17 +5,16 @@ - + - Alchemy Scaffold for The Pyramid Web Framework - - - + PyListener - - + + + + - diff --git a/pylistener/pylistener/templates/login.jinja2 b/pylistener/pylistener/templates/login.jinja2 new file mode 100644 index 0000000..ea882a0 --- /dev/null +++ b/pylistener/pylistener/templates/login.jinja2 @@ -0,0 +1,4 @@ +{% extends 'layout.jinja2' %} +{% block body %} + +{% endblock %} \ No newline at end of file diff --git a/pylistener/pylistener/templates/main.jinja2 b/pylistener/pylistener/templates/main.jinja2 new file mode 100644 index 0000000..ea882a0 --- /dev/null +++ b/pylistener/pylistener/templates/main.jinja2 @@ -0,0 +1,4 @@ +{% extends 'layout.jinja2' %} +{% block body %} + +{% endblock %} \ No newline at end of file diff --git a/pylistener/pylistener/templates/manage.jinja2 b/pylistener/pylistener/templates/manage.jinja2 new file mode 100644 index 0000000..ea882a0 --- /dev/null +++ b/pylistener/pylistener/templates/manage.jinja2 @@ -0,0 +1,4 @@ +{% extends 'layout.jinja2' %} +{% block body %} + +{% endblock %} \ No newline at end of file diff --git a/pylistener/pylistener/templates/mytemplate.jinja2 b/pylistener/pylistener/templates/mytemplate.jinja2 deleted file mode 100644 index ade7f2c..0000000 --- a/pylistener/pylistener/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.3.

-
-{% endblock content %} diff --git a/pylistener/pylistener/templates/register.jinja2 b/pylistener/pylistener/templates/register.jinja2 new file mode 100644 index 0000000..ea882a0 --- /dev/null +++ b/pylistener/pylistener/templates/register.jinja2 @@ -0,0 +1,4 @@ +{% extends 'layout.jinja2' %} +{% block body %} + +{% endblock %} \ No newline at end of file diff --git a/pylistener/pylistener/tests.py b/pylistener/pylistener/tests.py index 599abc3..9adf9f0 100644 --- a/pylistener/pylistener/tests.py +++ b/pylistener/pylistener/tests.py @@ -1,65 +1,499 @@ -import unittest +"""A short testing suite for the expense tracker.""" + + +import pytest import transaction from pyramid import testing +from expense_tracker.models import Expense, get_tm_session +from expense_tracker.models.meta import Base -def dummy_request(dbsession): - return testing.DummyRequest(dbsession=dbsession) +@pytest.fixture(scope="session") +def configuration(request): + """Set up a Configurator instance. -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() + This Configurator instance sets up a pointer to the location of the + database. + It also includes the models from your app's model package. + Finally it tears everything down, including the in-memory SQLite database. - from .models import ( - get_engine, - get_session_factory, - get_tm_session, - ) + This configuration will persist for the entire duration of your PyTest run. + """ + settings = { + 'sqlalchemy.url': 'postgres:///test_expenses'} + config = testing.setUp(settings=settings) + config.include('pylistener.models') + config.include('pylistener.routes') - self.engine = get_engine(settings) - session_factory = get_session_factory(self.engine) + def teardown(): + testing.tearDown() - self.session = get_tm_session(session_factory, transaction.manager) + request.addfinalizer(teardown) + return config - def init_database(self): - from .models.meta import Base - Base.metadata.create_all(self.engine) - def tearDown(self): - from .models.meta import Base +@pytest.fixture +def db_session(configuration, request): + """Create a session for interacting with the test database. + + This uses the dbsession_factory on the configurator instance to create a + new database session. It binds that session to the available engine + and returns a new session for every call of the dummy_request object. + """ + SessionFactory = configuration.registry['dbsession_factory'] + session = SessionFactory() + engine = session.bind + Base.metadata.create_all(engine) + + def teardown(): + session.transaction.rollback() + Base.metadata.drop_all(engine) + + request.addfinalizer(teardown) + return session + + +@pytest.fixture +def dummy_request(db_session): + """Instantiate a fake HTTP Request, complete with a database session. + + This is a function-level fixture, so every new request will have a + new database session. + """ + return testing.DummyRequest(dbsession=db_session) + + +@pytest.fixture +def add_models(dummy_request): + """Add a bunch of model instances to the database. + + Every test that includes this fixture will add new random expenses. + """ + dummy_request.dbsession.add_all(EXPENSES) + + +@pytest.fixture +def set_auth_credentials(): + """Make a username/password combo for testing.""" + import os + from passlib.apps import custom_app_context as pwd_context + + os.environ["AUTH_USERNAME"] = "testme" + os.environ["AUTH_PASSWORD"] = pwd_context.hash("foobar") + + +# ======== UNIT TESTS ========== + +def test_new_expenses_are_added(db_session): + """New expenses get added to the database.""" + db_session.add_all(EXPENSES) + query = db_session.query(Expense).all() + assert len(query) == len(EXPENSES) + + +def test_list_view_returns_empty_when_empty(dummy_request): + """Test that the list view returns no objects in the expenses iterable.""" + from .views.default import list_view + result = list_view(dummy_request) + assert len(result["expenses"]) == 0 + + +def test_list_view_returns_objects_when_exist(dummy_request, add_models): + """Test that the list view does return objects when the DB is populated.""" + from .views.default import list_view + result = list_view(dummy_request) + assert len(result["expenses"]) == 100 + + +def test_list_view_with_categories(dummy_request, add_models): + """Test that the list view does return objects when the DB is populated.""" + from .views.default import list_view + + dummy_request.method = "POST" + dummy_request.POST["category"] = "utilities" + result = list_view(dummy_request) + assert "utilities" in result.location + + +def test_detail_view_contains_individual_expense_details(db_session, dummy_request, add_models): + """Test that the detail view actually returns individual expense info.""" + from .views.default import detail_view + dummy_request.matchdict["id"] = 12 + expense = db_session.query(Expense).get(12) + result = detail_view(dummy_request) + assert result["expense"] == expense + + +def test_create_view_get_request_is_normal(dummy_request): + """The create view should return an empty dict.""" + from .views.default import create_view + result = create_view(dummy_request) + assert result == {} + + +def test_create_view_post_request_adds_new_db_item(db_session, dummy_request): + """Posting to the create view adds an item.""" + from .views.default import create_view + + dummy_request.method = "POST" + dummy_request.POST["item"] = "test item" + dummy_request.POST["amount"] = "1234.56" + dummy_request.POST["paid_to"] = "test recipient" + dummy_request.POST["category"] = "rent" + dummy_request.POST["description"] = "test description" + create_view(dummy_request) + new_expense = db_session.query(Expense).first() + latest = new_expense + assert latest.item == "test item" + + +def test_create_view_post_request_adds_new_db_items(db_session, dummy_request): + """Posting to the create view twice adds another new item.""" + from .views.default import create_view + + dummy_request.method = "POST" + dummy_request.POST["item"] = "test item" + dummy_request.POST["amount"] = "1234.56" + dummy_request.POST["paid_to"] = "test recipient" + dummy_request.POST["category"] = "rent" + dummy_request.POST["description"] = "test description" + create_view(dummy_request) + + dummy_request.method = "POST" + dummy_request.POST["item"] = "test item2" + dummy_request.POST["amount"] = "834.00" + dummy_request.POST["paid_to"] = "test recipient2" + dummy_request.POST["category"] = "food" + dummy_request.POST["description"] = "test description2" + create_view(dummy_request) + + new_expenses = db_session.query(Expense).all() + latest = new_expenses[-1] + assert latest.item == "test item2" + + +def test_edit_view_returns_expense_info(db_session, dummy_request, add_models): + """GET request to the edit view contains expense item info.""" + from .views.default import edit_view + dummy_request.matchdict["id"] = 2 + result = edit_view(dummy_request) + expense = db_session.query(Expense).get(2) + assert result["data"]["item"] == expense.item + + +def test_edit_view_edits_expense_info(db_session, dummy_request, add_models): + """POST request to the edit view edits expense item info.""" + from .views.default import edit_view + dummy_request.matchdict["id"] = 2 + dummy_request.POST["item"] = "test item" + dummy_request.POST["amount"] = "1234.56" + dummy_request.POST["paid_to"] = "test recipient" + dummy_request.POST["category"] = "rent" + dummy_request.POST["description"] = "test description" + edit_view(dummy_request) + expense = db_session.query(Expense).get(2) + assert expense.item == "test item" + + +def test_edit_view_redirects_after_edit(dummy_request, add_models): + """POST request redirects.""" + from .views.default import edit_view + from pyramid.httpexceptions import HTTPFound + dummy_request.matchdict["id"] = 2 + dummy_request.POST["item"] = "test item" + dummy_request.POST["amount"] = "1234.56" + dummy_request.POST["paid_to"] = "test recipient" + dummy_request.POST["category"] = "rent" + dummy_request.POST["description"] = "test description" + result = edit_view(dummy_request) + assert isinstance(result, HTTPFound) + + +def test_category_view_shows_only_one_category(db_session, dummy_request, add_models): + """GET request only shows one category.""" + from .views.default import category_view + dummy_request.matchdict["cat"] = "utilities" + result = category_view(dummy_request) + query = db_session.query(Expense).filter(Expense.category == "utilities") + expenses = query.all() + assert result["expenses"] == expenses + + +def test_category_view_with_new_category(dummy_request, add_models): + """Test that the list view does return objects when the DB is populated.""" + from .views.default import category_view + from pyramid.httpexceptions import HTTPFound + dummy_request.method = "POST" + dummy_request.matchdict["cat"] = "rent" + dummy_request.POST["category"] = "utilities" + result = category_view(dummy_request) + assert isinstance(result, HTTPFound) + + +def test_login_view_get_request(dummy_request): + """Test that you can see the login view.""" + from .views.default import login_view + result = login_view(dummy_request) + assert result == {} + + +def test_login_view_good_credentials(dummy_request, set_auth_credentials): + """Test that when given good credentials login can be successful.""" + from .views.default import login_view + from pyramid.httpexceptions import HTTPFound + dummy_request.POST["username"] = "testme" + dummy_request.POST["password"] = "foobar" + result = login_view(dummy_request) + assert isinstance(result, HTTPFound) + + +def test_login_view_bad_credentials(dummy_request, set_auth_credentials): + """Test that when given bad credentials login doesn't happen.""" + from .views.default import login_view + dummy_request.POST["username"] = "testme" + dummy_request.POST["password"] = "badpass" + result = login_view(dummy_request) + assert result == {} + + +def test_logout_view_redirects(dummy_request): + """When logging out you get redirected to the home page.""" + from .views.default import logout_view + from pyramid.httpexceptions import HTTPFound + result = logout_view(dummy_request) + assert isinstance(result, HTTPFound) + + +def test_delete_view_removes_an_item(db_session, dummy_request, add_models): + """Delete view removes an item.""" + from .views.default import delete_view + expense = db_session.query(Expense).get(2) + dummy_request.matchdict["id"] = expense.id + delete_view(dummy_request) + assert expense not in db_session.query(Expense).all() + + +def test_delete_view_redirects(dummy_request, add_models): + """When logging out you get redirected to the home page.""" + from .views.default import delete_view + from pyramid.httpexceptions import HTTPFound + dummy_request.matchdict["id"] = 2 + result = delete_view(dummy_request) + assert isinstance(result, HTTPFound) + + +def test_api_list_contains_list_of_dicts(dummy_request, add_models): + """When using the list view for the API, get back dictionaries.""" + from .views.default import api_list_view + result = api_list_view(dummy_request) + assert isinstance(result[0], dict) + + +def test_api_list_contains_all_expenses(dummy_request, add_models): + """When using the list view for the API, get back dictionaries.""" + from .views.default import api_list_view + result = api_list_view(dummy_request) + expense_dicts = [expense.to_json() for expense in EXPENSES] + for item in expense_dicts: + assert item in result + +# ======== FUNCTIONAL TESTS =========== + + +@pytest.fixture(scope="session") +def testapp(request): + """Create an instance of webtests TestApp for testing routes. + + With the alchemy scaffold we need to add to our test application the + setting for a database to be used for the models. + We have to then set up the database by starting a database session. + Finally we have to create all of the necessary tables that our app + normally uses to function. + + The scope of the fixture is function-level, so every test will get a new + test application. + """ + from webtest import TestApp + from expense_tracker import main + + app = main({}, **{"sqlalchemy.url": 'postgres:///test_expenses'}) + testapp = TestApp(app) + + SessionFactory = app.registry["dbsession_factory"] + engine = SessionFactory().bind + Base.metadata.create_all(bind=engine) + + def tearDown(): + Base.metadata.drop_all(bind=engine) + + request.addfinalizer(tearDown) + + return testapp + + +@pytest.fixture(scope="session") +def fill_the_db(testapp): + """Fill the database with some model instances and return the session. + + Start a database session with the transaction manager and add all of the + expenses. This will be done anew for every test. + """ + + SessionFactory = testapp.app.registry["dbsession_factory"] + with transaction.manager: + dbsession = get_tm_session(SessionFactory, transaction.manager) + dbsession.add_all(new_expenses) + + return dbsession + + +@pytest.fixture +def new_session(testapp): + """Return a session for inspecting the database.""" + SessionFactory = testapp.app.registry["dbsession_factory"] + with transaction.manager: + dbsession = get_tm_session(SessionFactory, transaction.manager) + return dbsession + + +def test_home_route_has_table(testapp): + """The home page has a table in the html.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("table")) == 1 + + +def test_home_route_has_table2(testapp): + """Without data the home page only has the header row in its table.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("tr")) == 1 + + +def test_detail_route_is_not_found(testapp): + """Without data there's no detail page.""" + response = testapp.get('/expense/4', status=404) + assert response.status_code == 404 + +# ========= WITH DATA IN THE DB ======== + + +def test_home_route_with_data_has_filled_table(testapp, fill_the_db): + """When there's data in the database, the home page has some rows.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("tr")) == 101 + + +def test_login_route_can_be_seen(testapp): + """Can send a GET request to the login route and see three input fields.""" + response = testapp.get("/login", status=200) + html = response.html + assert len(html.find_all("input")) == 3 + + +def test_detail_route_has_details(testapp, new_session): + """Can send a GET request to a detail route and see item info.""" + response = testapp.get("/expense/4") + expense = new_session.query(Expense).get(4) + assert expense.item in response.text + +# ======== TESTING WITH SECURITY ========== + + +def test_create_route_is_forbidden(testapp): + """Any old user trying to create a new expense sees the forbidden view.""" + response = testapp.get("/new-expense") + assert "can't do that" in response.text + + +def test_edit_route_is_forbidden(testapp): + """Any old user trying to create a new expense sees the forbidden view.""" + response = testapp.get("/expense/4/edit") + assert "can't do that" in response.text + + +def test_delete_route_is_forbidden(testapp): + """Any old user trying to delete an expense sees the forbidden view.""" + response = testapp.get("/delete/4") + assert "can't do that" in response.text + + +def test_login_with_bad_credentials(set_auth_credentials, testapp): + """Bad credentials remain unauthenticated.""" + response = testapp.post("/login", params={ + "username": "testme", + "password": "bad password" + }) + response = testapp.get("/new-expense") + assert "can't do that" in response.text + + +def test_login_with_no_credentials(set_auth_credentials, testapp): + """No credential login remains unauthenticated.""" + response = testapp.post("/login", params={ + "username": "", + "password": "" + }) + response = testapp.get("/new-expense") + assert "can't do that" in response.text + +# ======== TESTING WITH SECURITY | APP IS AUTHENTICATED ========== - testing.tearDown() - transaction.abort() - Base.metadata.drop_all(self.engine) +def test_auth_app_can_see_create_route(set_auth_credentials, testapp): + """A logged-in user should be able to access the create view.""" + response = testapp.post("/login", params={ + "username": "testme", + "password": "foobar" + }) + response = testapp.get("/new-expense") + assert response.status_code == 200 -class TestMyViewSuccessCondition(BaseTest): - def setUp(self): - super(TestMyViewSuccessCondition, self).setUp() - self.init_database() +def test_auth_app_can_create_expense(testapp): + """A logged-in user can post a new expense.""" + response = testapp.get("/new-expense") + token = response.html.find("input", {"type": "hidden"}).attrs["value"] + testapp.post("/new-expense", params={ + "csrf_token": token, + "item": "another item", + "amount": "2743.88", + "paid_to": "another person", + "category": "another thing", + "description": "another item" + }) + response = testapp.get("/") + assert "another item" in response.text - from .models import MyModel - model = MyModel(name='one', value=55) - self.session.add(model) +def test_auth_app_can_edit_expense(testapp): + """A logged-in user can edit an existing expense.""" + response = testapp.get("/expense/4/edit") + token = response.html.find("input", {"type": "hidden"}).attrs["value"] + testapp.post("/expense/4/edit", params={ + "csrf_token": token, + "item": "an edited expense", + "amount": "0.00", + "paid_to": "no one", + "category": "who cares", + "description": "it was edited" + }) + response = testapp.get("/expense/4") + assert "an edited expense" in response.text - 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'], 'pylistener') +def test_auth_app_can_delete_expense(testapp): + """A logged-in user can delete an existing expense.""" + response = testapp.get("/delete/4") + response = testapp.get('/expense/4', status=404) + assert response.status_code == 404 -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) +def test_logged_out_user_can_no_longer_create(testapp): + """A user that has logged out can't create expenses.""" + testapp.get("/logout") + response = testapp.get("/new-expense") + assert "can't do that" in response.text diff --git a/pylistener/pylistener/views/default.py b/pylistener/pylistener/views/default.py index 5e6c5b2..f11261d 100644 --- a/pylistener/pylistener/views/default.py +++ b/pylistener/pylistener/views/default.py @@ -6,8 +6,8 @@ from ..models import MyModel -@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') -def my_view(request): +@view_config(route_name='home', renderer='../templates/main.jinja2') +def home_view(request): try: query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() @@ -16,6 +16,48 @@ def my_view(request): return {'one': one, 'project': 'pylistener'} +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login_view(request): + '''Handle the login route.''' + pass + + +@view_config(route_name='logout') +def logout_view(request): + '''Handle the logout route.''' + pass + + +@view_config(route_name='manage', renderer='../templates/manage.jinja2') +def manage_view(request): + '''Handle the manage route.''' + pass + + +@view_config(route_name='register', renderer='../templates/register.jinja2') +def register_view(request): + '''Handle the register route.''' + pass + + +@view_config(route_name='categories', renderer='../templates/main.jinja2') +def categories_view(request): + '''Handle the categories route.''' + pass + + +@view_config(route_name='attributes', renderer='../templates/main.jinja2') +def attributes_view(request): + '''Handle the attributes route.''' + pass + + +@view_config(route_name='display', renderer='../templates/display.jinja2') +def display_view(request): + '''Handle the display route.''' + pass + + db_err_msg = """\ Pyramid is having a problem using your SQL database. The problem might be caused by one of the following things: diff --git a/pylistener/setup.py b/pylistener/setup.py index 7c1611f..c400bb2 100644 --- a/pylistener/setup.py +++ b/pylistener/setup.py @@ -27,7 +27,8 @@ setup(name='pylistener', version='0.0', - description='pylistener', + description='''A simple tool designed to enable people with Apraxia + to communicate.''', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", @@ -35,9 +36,9 @@ "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], - author='', + author='Maelle Vance, Rick Valenzuela, Ted Callahan', author_email='', - url='', + url='https://pylistener.herokuapp.com', keywords='web wsgi bfg pylons pyramid', packages=find_packages(), include_package_data=True, @@ -50,6 +51,6 @@ [paste.app_factory] main = pylistener:main [console_scripts] - initialize_pylistener_db = pylistener.scripts.initializedb:main + initialize_db = pylistener.scripts.initializedb:main """, ) From fd35e27b4baf5f598e6fa607012e23c9140e8030 Mon Sep 17 00:00:00 2001 From: Maelle Vance Date: Mon, 9 Jan 2017 15:39:24 -0800 Subject: [PATCH 007/144] changed manage route for username specific, added login, logout and register views --- pylistener/pylistener/routes.py | 2 +- pylistener/pylistener/views/default.py | 48 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/pylistener/pylistener/routes.py b/pylistener/pylistener/routes.py index 9a6706d..e5a8361 100644 --- a/pylistener/pylistener/routes.py +++ b/pylistener/pylistener/routes.py @@ -3,7 +3,7 @@ def includeme(config): config.add_route('home', '/') config.add_route('login', '/login') config.add_route('logout', '/logout') - config.add_route('manage', '/manage') + config.add_route('manage', '/manage/{id:\w+}') config.add_route('register', '/register') config.add_route('categories', '/categories') config.add_route('attributes', '/attributes') diff --git a/pylistener/pylistener/views/default.py b/pylistener/pylistener/views/default.py index f11261d..0d60fbc 100644 --- a/pylistener/pylistener/views/default.py +++ b/pylistener/pylistener/views/default.py @@ -3,7 +3,11 @@ from sqlalchemy.exc import DBAPIError -from ..models import MyModel +from pyramid.httpexceptions import HTTPFound + +from pylistener.models import User +from pylistener.security import check_credentials +from pyramid.security import remember, forget @view_config(route_name='home', renderer='../templates/main.jinja2') @@ -18,14 +22,31 @@ def home_view(request): @view_config(route_name='login', renderer='../templates/login.jinja2') def login_view(request): - '''Handle the login route.''' - pass - - -@view_config(route_name='logout') + if request.POST: + query = request.dbsession.query(User) + username = request.POST["username"] + password = request.POST["password"] + real_password = None + for user in query.all(): + if user.username == username: + real_password = user.hashed_password + break + if real_password: + if check_credentials(password, real_password): + auth_head = remember(request, username) + return HTTPFound( + location=request.route_url("home"), + headers=auth_head + ) + + return {} + + +@view_config(route_name='logout', permission="manage") def logout_view(request): '''Handle the logout route.''' - pass + auth_head = forget(request) + return HTTPFound(location=request.route_url("home"), headers=auth_head) @view_config(route_name='manage', renderer='../templates/manage.jinja2') @@ -37,7 +58,18 @@ def manage_view(request): @view_config(route_name='register', renderer='../templates/register.jinja2') def register_view(request): '''Handle the register route.''' - pass + if request.POST: + username = request.POST["username"] + password = request.POST["password"] + email = request.POST["email"] + new_user = User( + username=username, + hashed_password=password.pwd_context.hash(password), + email=email + ) + request.dbsession.add(new_user) + return HTTPFound(location=request.route_url('manage', id=new_user.username)) + return {} @view_config(route_name='categories', renderer='../templates/main.jinja2') From 343de25c184cf9c74c2265632012ef8f9d20caba Mon Sep 17 00:00:00 2001 From: Rick Valenzuela Date: Mon, 9 Jan 2017 15:54:55 -0800 Subject: [PATCH 008/144] add auth policies, credential check, CSRF protection --- pylistener/pylistener/security.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pylistener/pylistener/security.py b/pylistener/pylistener/security.py index c392426..10aea9c 100644 --- a/pylistener/pylistener/security.py +++ b/pylistener/pylistener/security.py @@ -5,6 +5,7 @@ from pyramid.session import SignedCookieSessionFactory from passlib.apps import custom_app_context as pwd_context +from .models import User class NewRoot(object): @@ -13,18 +14,18 @@ def __init__(self, request): """TODO: create second level of authentication?""" __acl__ = [ - (Allow, Authenticated, 'guardian'), + (Allow, Authenticated, 'manage'), ] -def check_credentials(request): - """Return True if correct username and password, else False.""" - pass +def check_credentials(input_password, real_password): + """Return True if correct password, else False.""" + return pwd_context.verify(input_password, real_password) def includeme(config): """Pyramid security configuration.""" - auth_secret = os.environ.get("AUTH_SECRET", "potato") + auth_secret = request.dbsession.query(User).get("AUTH_SECRET", "potato") authn_policy = AuthTktAuthenticationPolicy( secret=auth_secret, hashalg="sha512" From 6ca11e5dd0b25ad15296f9dadeb59538e344bb87 Mon Sep 17 00:00:00 2001 From: Maelle Vance Date: Mon, 9 Jan 2017 16:03:40 -0800 Subject: [PATCH 009/144] added csrf token field --- pylistener/pylistener/templates/login.jinja2 | 9 ++++++++- pylistener/pylistener/templates/register.jinja2 | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pylistener/pylistener/templates/login.jinja2 b/pylistener/pylistener/templates/login.jinja2 index ea882a0..19879f2 100644 --- a/pylistener/pylistener/templates/login.jinja2 +++ b/pylistener/pylistener/templates/login.jinja2 @@ -1,4 +1,11 @@ {% extends 'layout.jinja2' %} {% block body %} - +
+ + + + + + +
{% endblock %} \ No newline at end of file diff --git a/pylistener/pylistener/templates/register.jinja2 b/pylistener/pylistener/templates/register.jinja2 index ea882a0..1c355f9 100644 --- a/pylistener/pylistener/templates/register.jinja2 +++ b/pylistener/pylistener/templates/register.jinja2 @@ -1,4 +1,13 @@ {% extends 'layout.jinja2' %} {% block body %} - +
+ + + + + + + + +
{% endblock %} \ No newline at end of file From 1e1e947166bccc6345ad3688d2b2c03a4716e164 Mon Sep 17 00:00:00 2001 From: Maelle Vance Date: Mon, 9 Jan 2017 16:14:24 -0800 Subject: [PATCH 010/144] template for manage page --- pylistener/pylistener/templates/manage.jinja2 | 35 +++++++++++++++++++ pylistener/setup.py | 1 + 2 files changed, 36 insertions(+) diff --git a/pylistener/pylistener/templates/manage.jinja2 b/pylistener/pylistener/templates/manage.jinja2 index ea882a0..4b2cb38 100644 --- a/pylistener/pylistener/templates/manage.jinja2 +++ b/pylistener/pylistener/templates/manage.jinja2 @@ -1,4 +1,39 @@ {% extends 'layout.jinja2' %} {% block body %} +
+ + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + +
{% endblock %} \ No newline at end of file diff --git a/pylistener/setup.py b/pylistener/setup.py index c400bb2..91590b2 100644 --- a/pylistener/setup.py +++ b/pylistener/setup.py @@ -17,6 +17,7 @@ 'transaction', 'zope.sqlalchemy', 'waitress', + 'passlib' ] tests_require = [ From 57c7538acebe13852eac310e1ad54db9658e3ab1 Mon Sep 17 00:00:00 2001 From: Rick Valenzuela Date: Mon, 9 Jan 2017 17:05:32 -0800 Subject: [PATCH 011/144] basic main template --- pylistener/pylistener/templates/main.jinja2 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pylistener/pylistener/templates/main.jinja2 b/pylistener/pylistener/templates/main.jinja2 index ea882a0..a1fa307 100644 --- a/pylistener/pylistener/templates/main.jinja2 +++ b/pylistener/pylistener/templates/main.jinja2 @@ -1,4 +1,14 @@ {% extends 'layout.jinja2' %} {% block body %} +{% if request.authenticated_userid %} +