diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..594b033 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = learning_journal +omit = learning_journal/test* diff --git a/.gitignore b/.gitignore index 72364f9..f64f3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[cod] *$py.class +.DS_Store + # C extensions *.so @@ -23,6 +25,7 @@ var/ *.egg-info/ .installed.cfg *.egg +*.sqlite # PyInstaller # Usually these files are written by a python script from a template diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..35a34f3 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5bf30ca --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include learning_journal *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..e645050 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./run diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..dc32eb8 --- /dev/null +++ b/README.txt @@ -0,0 +1,14 @@ +learning_journal README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_learning_journal_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/development.ini b/development.ini new file mode 100644 index 0000000..30ddcd7 --- /dev/null +++ b/development.ini @@ -0,0 +1,71 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + +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/learning_journal.sqlite +; sqlalchemy.url = postgres://CCallahanIV@localhost:5432/learning_journal + +# 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, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_learning_journal] +level = DEBUG +handlers = +qualname = learning_journal + +[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/learning_journal/__init__.py b/learning_journal/__init__.py new file mode 100644 index 0000000..c8ee430 --- /dev/null +++ b/learning_journal/__init__.py @@ -0,0 +1,15 @@ +import os +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + settings["sqlalchemy.url"] = os.environ["DATABASE_URL"] + 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/learning_journal/models/__init__.py b/learning_journal/models/__init__.py new file mode 100644 index 0000000..b7f2212 --- /dev/null +++ b/learning_journal/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 Entries # 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('learning_journal.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/learning_journal/models/meta.py b/learning_journal/models/meta.py new file mode 100644 index 0000000..0682247 --- /dev/null +++ b/learning_journal/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/learning_journal/models/mymodel.py b/learning_journal/models/mymodel.py new file mode 100644 index 0000000..13a93cc --- /dev/null +++ b/learning_journal/models/mymodel.py @@ -0,0 +1,17 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Unicode, + Date +) + +from .meta import Base + + +class Entries(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode) + creation_date = Column(Date) + body = Column(Unicode) diff --git a/learning_journal/routes.py b/learning_journal/routes.py new file mode 100644 index 0000000..9e890bf --- /dev/null +++ b/learning_journal/routes.py @@ -0,0 +1,9 @@ +def includeme(config): + """Routes for learning journal web app.""" + config.add_static_view(name='static', path='static', cache_max_age=3600) + config.add_route("home", "/") + config.add_route("detail", "/journal/{id:\d+}") + config.add_route("create", "/journal/new-entry") + config.add_route("update", "/journal/{id:\d+}/edit-entry") + config.add_route("login", "/login") + config.add_route("logout", "/logout") diff --git a/learning_journal/scripts/__init__.py b/learning_journal/scripts/__init__.py new file mode 100644 index 0000000..5bb534f --- /dev/null +++ b/learning_journal/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/learning_journal/scripts/initializedb.py b/learning_journal/scripts/initializedb.py new file mode 100644 index 0000000..8bba1d7 --- /dev/null +++ b/learning_journal/scripts/initializedb.py @@ -0,0 +1,58 @@ +import os +import sys +import transaction +from datetime import datetime + +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 Entries + + +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) + settings["sqlalchemy.url"] = os.environ["DATABASE_URL"] + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + ENTRIES = [ + {"title": "There's value in stepping back.", "creation_date": datetime.strptime("December 14, 2016", "%B %d, %Y"), "body": """Sometimes principles from our past help with our present. Before starting the double linked list today, I committed to two things: Starting with the tests. Designing before coding. I actually wrote some tests first, just sort of stream of consciousness but things quickly got messy. After flailing for a bit, I stepped back and mapped out the function of each method and made a list of everything I would want test about it. From that, I was able to write a fairly comprehensive testing suite that should require little refactoring to really test all functions of the Double Linked List. Joey and I had some frustrating issues to work out with the server assignment. For whatever reason, since yesterday, our message sending on both the client and server sides decided to start escaping all the \r and \n characters. It had us and the TA's stumped for... far too long. We ended up changing our search parameters in our receiving while loops so that things could work again. Now I'm worried that I'll wake up tomorrow and it won't work. Or it just won't work whomever grades our assignment. SUCH IS LIFE."""}, + {"title": "Revenge is Best SERVERED Cold", "creation_date": datetime.strptime("December 15, 2016", "%B %d, %Y"), "body": """The second you feel like youre starting to climb youre way out of the avalanche of work and information theres always some weird and random server error to pull you right back in. Im struggling a lot to find ways to refactor and clean up the server code. I feel like were working with a patchwork of try/except statements, teetering towers of if/else logic, and precarious while loops. Id like to see an example of a server done RIGHT. But I want to earn that - I want to finish my own server the hard way. Hey, urllib2, sup?"""}, + {"title": "Whiteboarding", "creation_date": datetime.strptime("December 16, 2016", "%B %d, %Y"), "body": "Its fun, its hard, it makes your brain bend in ways you never thought possible. Really lucky I was partnered with Joey. He had the essential, Aha! moment that led to the solution. Glad I was able to contribute though. I worry that in the future as I prep for interviews, I'll won't be able to rely on flexible thinking to solve new problems. I'll likely have to plan ahead and try to expose myself to as many problem scenarios as possible in order to succeed. So it goes, more studying ahead. The server assignment really started as a hacky mess. Lots of patchwork try/except blocks and grabbing specific errors in desperation. I think today we really nailed it down into something I can be somewhat proud of. Looking forward to the opportunity to go back in time and refactor things this weekend. Also looking forward to beer."}, + {"title": "Recent Awesome Things We've Learned", "creation_date": datetime.strptime("December 19, 2016", "%B %d, %Y"), "body": "

It feels like we're moving beyond the introductory part of Python knowledge and are really starting to get into the meat n' potatoes of development and CS concepts. For the first time in a while, I'm starting to feel somewhat empowered as a dev-in-development. Not to say I know everything there is, but I know enough to now to actually make something with it, and more importantly, I'm starting to have some idea of what I don't know.

Despite the time crunch and the fast pace, I've enjoyed the class to this point. BUT, I am really excited for what is to come.

Of course, the second I begin to think this, the rug will be pulled out from underneath once again.

Looking forward to building stuff.

"}, + {"title": "Tunnels, Lights, Progress, etc.", "creation_date": datetime.strptime("December 20, 2016", "%B %d, %Y"), "body": """Two quick things, the binary heap was initially a pain in the ass. However, I feel really proud of the implementation I have. Hooray for max OR min. Finally, I'm really excited for the group project. I feel a strong personal connection to the purpose of this app and I'm excited to put our newfound skills and knowledge to good use."""}, + {"title": "They Said It Would Happen", "creation_date": datetime.strptime("December 21, 2016", "%B %d, %Y"), "body": "Unfortunately, I think I'm enjoying too much tinkering around with data structures. We have fallen far behind on the journal stuff. If we didn't have next week off, I'm not sure how it would all get done. Getting tough to balance home and school life. But that's the fun part, right? The challenge! BRING IT ON, COWBOY UP."}, + {"title": "First Time Entering On the New Site", "creation_date": datetime.strptime("December 22, 2016", "%B %d, %Y"), "body": "This is pretty exciting. This is the first time I'll actually be writing my journal entry in the form I created! Of course it won't persist until I upgrade it to Postgres, BUT STILL. Pretty cool. Getting the chance to work from an emptier plate today really helped alleviate some pressure. I'm very much looking forward to next week for the opportunity to go back through past assignments with a fine tooth comb and improve things, first by learning from earlier mistakes, and second by getting some points back. Hopefully, time permitting, I'll be able to work a bit on the twitter app. Going back to Javascript after three intensive weeks of Python should be a trip."} + ] + + # with transaction.manager: + # dbsession = get_tm_session(session_factory, transaction.manager) + + # for entry in ENTRIES: + # row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + # dbsession.add(row) diff --git a/learning_journal/security.py b/learning_journal/security.py new file mode 100644 index 0000000..82e5a0d --- /dev/null +++ b/learning_journal/security.py @@ -0,0 +1,43 @@ +import os +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import Allow, Everyone, 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 + + __acl__ = [ + (Allow, Authenticated, 'author'), + (Allow, Everyone, 'guest') + ] + + +def check_credentials(username, password): + """Return True if correct username and password, else False.""" + if username and password: + # proceed to check credentials + if username == os.environ["AUTH_USERNAME"]: + return pwd_context.verify(password, os.environ["AUTH_PASSWORD"]) + return False + + +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) + # Begin CSRF security configurations. + session_secret = os.environ.get('SESSION_SECRET', 'superdupersecret') + session_factory = SignedCookieSessionFactory(session_secret) + config.set_session_factory(session_factory) + config.set_default_csrf_options(require_csrf=True) diff --git a/learning_journal/static/base.css b/learning_journal/static/base.css new file mode 100644 index 0000000..7ac7a6e --- /dev/null +++ b/learning_journal/static/base.css @@ -0,0 +1,19 @@ +a { + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6{ + font-family: Share Tech Mono; +} + +h1{ + font-size: 2.0em; +} + +h2{ + font-size: 1.5em; +} + +h3{ + font-size:1.25em; +} diff --git a/learning_journal/static/layout.css b/learning_journal/static/layout.css new file mode 100644 index 0000000..34df568 --- /dev/null +++ b/learning_journal/static/layout.css @@ -0,0 +1,18 @@ +header { + width: 100%; + height: 80px; + background-color: black; +} + +header a{ + font-family: Share Tech Mono; + color: white; + line-height: 40px; + padding-left: 8px; +} + +main { + width: 90%; + height: 100%; + margin: auto; +} \ No newline at end of file diff --git a/learning_journal/static/module.css b/learning_journal/static/module.css new file mode 100644 index 0000000..f01d472 --- /dev/null +++ b/learning_journal/static/module.css @@ -0,0 +1,72 @@ +#entryListWrapper{ + width: 80%; + margin: auto; + min-width: 320px; + height: 100%; + background-color: lightgrey; +} + +#entryDisplayWrapper{ + width: 80%; + margin: auto; + display: block; + margin-bottom: 10px; +} + +#entryTitleWrapper{ + width: 100%; + background: lightgrey; + margin-bottom: 20px; +} + +#entryWrapper p{ + margin-bottom: 20px; +} + +.date{ + font-size: 0.8em; + font-color: grey; + font-style: italic; +} + +.entryListItem{ + margin-bottom: 10px; +} + +/*BEGIN FORM STYLES*/ + +form{ + display: block; + width: 100%; +} + +form #entryTitle{ + display: block; + width: 90%; + margin: auto; + line-height: 2.0em; + margin-bottom: 20px; + font-size: 1.5em; +} + +form #entryBody{ + display: block; + width: 90%; + margin: auto; + margin-bottom: 20px; + height: 400px; +/* box-sizing: border-box; + resize: none;*/ +} + +form #submitButton{ + display: block; + width: 60%; + height: 40px; + margin: auto; + font-size: 1.5em; +} + +#pageTitle{ + text-align: center; +} \ No newline at end of file diff --git a/learning_journal/static/reset.css b/learning_journal/static/reset.css new file mode 100644 index 0000000..bba3cdf --- /dev/null +++ b/learning_journal/static/reset.css @@ -0,0 +1,48 @@ +/* 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; +} + diff --git a/learning_journal/templates/404.jinja2 b/learning_journal/templates/404.jinja2 new file mode 100644 index 0000000..1917f83 --- /dev/null +++ b/learning_journal/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/learning_journal/templates/create_entry.jinja2 b/learning_journal/templates/create_entry.jinja2 new file mode 100644 index 0000000..7ab26b3 --- /dev/null +++ b/learning_journal/templates/create_entry.jinja2 @@ -0,0 +1,11 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+ +

Title:

+ +

Body:

+ + +
+{% endblock %} \ No newline at end of file diff --git a/learning_journal/templates/edit_entry.jinja2 b/learning_journal/templates/edit_entry.jinja2 new file mode 100644 index 0000000..7ffec93 --- /dev/null +++ b/learning_journal/templates/edit_entry.jinja2 @@ -0,0 +1,11 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+ +

Title:

+ +

Body:

+ + +
+{% endblock %} diff --git a/learning_journal/templates/entry.jinja2 b/learning_journal/templates/entry.jinja2 new file mode 100644 index 0000000..ea6eaa8 --- /dev/null +++ b/learning_journal/templates/entry.jinja2 @@ -0,0 +1,15 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+
+

{{ entry.title }}

+

{{ entry.creation_date }}

+

by Ted Callahan

+
+
+ {{ entry.body|e }} +
+ {% if request.authenticated_userid %} + EDIT + {% endif %} +{% endblock %} diff --git a/learning_journal/templates/form.jinja2 b/learning_journal/templates/form.jinja2 new file mode 100644 index 0000000..16b322f --- /dev/null +++ b/learning_journal/templates/form.jinja2 @@ -0,0 +1,19 @@ + + + + + + +
+

+ + +

+

+ + +

+ +
+ + \ No newline at end of file diff --git a/learning_journal/templates/layout.jinja2 b/learning_journal/templates/layout.jinja2 new file mode 100644 index 0000000..3a3e44b --- /dev/null +++ b/learning_journal/templates/layout.jinja2 @@ -0,0 +1,28 @@ + + + + + Ted's Learning Journal + + + + + + + + + +
+ Home + {% if request.authenticated_userid %} + New Entry + Logout + {% else %} + Login + {% endif %} +
+
+ {% block body %}{% endblock %} +
+ + \ No newline at end of file diff --git a/learning_journal/templates/list.jinja2 b/learning_journal/templates/list.jinja2 new file mode 100644 index 0000000..3ae8b2b --- /dev/null +++ b/learning_journal/templates/list.jinja2 @@ -0,0 +1,18 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+
    + {% if entries %} + {% for entry in entries|reverse %} +
  • +

    {{ entry.title }}

    +

    Created {{ entry.creation_date }}

    +
    +
  • + {% endfor %} + {% else %} +

    This Journal is Empty. =(

    + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/learning_journal/templates/login.jinja2 b/learning_journal/templates/login.jinja2 new file mode 100644 index 0000000..e887bb3 --- /dev/null +++ b/learning_journal/templates/login.jinja2 @@ -0,0 +1,18 @@ +{% extends "layout.jinja2" %} +{% block body %} +

Login

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/learning_journal/templates/mytemplate.jinja2 b/learning_journal/templates/mytemplate.jinja2 new file mode 100644 index 0000000..ade7f2c --- /dev/null +++ b/learning_journal/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/learning_journal/tests.py b/learning_journal/tests.py new file mode 100644 index 0000000..2954ecc --- /dev/null +++ b/learning_journal/tests.py @@ -0,0 +1,269 @@ +"""A short testing suite for the expense tracker.""" + + +import pytest +import transaction + +from pyramid import testing + +from learning_journal.models import Entries, get_tm_session +from learning_journal.models.meta import Base + +from datetime import datetime + + +@pytest.fixture(scope="session") +def configuration(request): + """Set up a Configurator instance. + + 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. + + This configuration will persist for the entire duration of your PyTest run. + """ + settings = {'sqlalchemy.url': 'postgres://CCallahanIV@localhost:5432/test_lj'} + config = testing.setUp(settings=settings) + config.include('learning_journal.models') + + def teardown(): + testing.tearDown() + + request.addfinalizer(teardown) + return config + + +@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() + Entries.__table__.drop(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. + + """ + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + dummy_request.dbsession.add(row) + +ENTRIES = [ + {"title": "There's value in stepping back.", "creation_date": datetime.strptime("December 14, 2016", "%B %d, %Y"), "body": """Sometimes principles from our past help with our present. Before starting the double linked list today, I committed to two things: Starting with the tests. Designing before coding. I actually wrote some tests first, just sort of stream of consciousness but things quickly got messy. After flailing for a bit, I stepped back and mapped out the function of each method and made a list of everything I would want test about it. From that, I was able to write a fairly comprehensive testing suite that should require little refactoring to really test all functions of the Double Linked List. Joey and I had some frustrating issues to work out with the server assignment. For whatever reason, since yesterday, our message sending on both the client and server sides decided to start escaping all the \r and \n characters. It had us and the TA's stumped for... far too long. We ended up changing our search parameters in our receiving while loops so that things could work again. Now I'm worried that I'll wake up tomorrow and it won't work. Or it just won't work whomever grades our assignment. SUCH IS LIFE."""}, + {"title": "Revenge is Best SERVERED Cold", "creation_date": datetime.strptime("December 15, 2016", "%B %d, %Y"), "body": """The second you feel like youre starting to climb youre way out of the avalanche of work and information theres always some weird and random server error to pull you right back in. Im struggling a lot to find ways to refactor and clean up the server code. I feel like were working with a patchwork of try/except statements, teetering towers of if/else logic, and precarious while loops. Id like to see an example of a server done RIGHT. But I want to earn that - I want to finish my own server the hard way. Hey, urllib2, sup?"""}, + {"title": "Whiteboarding", "creation_date": datetime.strptime("December 16, 2016", "%B %d, %Y"), "body": "Its fun, its hard, it makes your brain bend in ways you never thought possible. Really lucky I was partnered with Joey. He had the essential, Aha! moment that led to the solution. Glad I was able to contribute though. I worry that in the future as I prep for interviews, I'll won't be able to rely on flexible thinking to solve new problems. I'll likely have to plan ahead and try to expose myself to as many problem scenarios as possible in order to succeed. So it goes, more studying ahead. The server assignment really started as a hacky mess. Lots of patchwork try/except blocks and grabbing specific errors in desperation. I think today we really nailed it down into something I can be somewhat proud of. Looking forward to the opportunity to go back in time and refactor things this weekend. Also looking forward to beer."}, + {"title": "Recent Awesome Things We've Learned", "creation_date": datetime.strptime("December 19, 2016", "%B %d, %Y"), "body": "
  • Big(O) Notation
  • Linked-List based Data Structures
  • Web App Deployment Frameworks (Pyramid)

It feels like we're moving beyond the introductory part of Python knowledge and are really starting to get into the meat n' potatoes of development and CS concepts. For the first time in a while, I'm starting to feel somewhat empowered as a dev-in-development. Not to say I know everything there is, but I know enough to now to actually make something with it, and more importantly, I'm starting to have some idea of what I don't know.

Despite the time crunch and the fast pace, I've enjoyed the class to this point. BUT, I am really excited for what is to come.

Of course, the second I begin to think this, the rug will be pulled out from underneath once again.

Looking forward to building stuff.

"}, + {"title": "Tunnels, Lights, Progress, etc.", "creation_date": datetime.strptime("December 20, 2016", "%B %d, %Y"), "body": """Two quick things, the binary heap was initially a pain in the ass. However, I feel really proud of the implementation I have. Hooray for max OR min. Finally, I'm really excited for the group project. I feel a strong personal connection to the purpose of this app and I'm excited to put our newfound skills and knowledge to good use."""}, + {"title": "They Said It Would Happen", "creation_date": datetime.strptime("December 21, 2016", "%B %d, %Y"), "body": "Unfortunately, I think I'm enjoying too much tinkering around with data structures. We have fallen far behind on the journal stuff. If we didn't have next week off, I'm not sure how it would all get done. Getting tough to balance home and school life. But that's the fun part, right? The challenge! BRING IT ON, COWBOY UP."} +] + + +# ======== UNIT TESTS ========== + +def test_new_entries_are_added(db_session): + """New expenses get added to the database.""" + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + db_session.add(row) + query = db_session.query(Entries).all() + assert len(query) == len(ENTRIES) + + +def test_home_view_returns_empty_when_empty(dummy_request): + """Test that the home view returns no objects in the expenses iterable.""" + from learning_journal.views.default import home_view + result = home_view(dummy_request) + assert len(result["entries"]) == 0 + + +def test_home_view_returns_objects_when_exist(dummy_request, add_models): + """Test that the home view does return objects when the DB is populated.""" + from learning_journal.views.default import home_view + result = home_view(dummy_request) + assert len(result["entries"]) == 6 + + +def test_create_view_returns_empty_dict_on_get(dummy_request): + """Test that the create_view method returns an empty dict on a get request.""" + from learning_journal.views.default import create_view + result = create_view(dummy_request) + assert result == {} + + +def test_create_view_updates_db_on_post(db_session, dummy_request): + """Test that the create_view method updates the DB with a POST request.""" + from learning_journal.views.default import create_view + dummy_request.method = "POST" + dummy_request.POST["title"] = "Some Title." + dummy_request.POST["body"] = "Some Body." + with pytest.raises(Exception): + create_view(dummy_request) + + query = db_session.query(Entries).all() + assert query[0].title == "Some Title." + assert query[0].body == "Some Body." + + +# ======== FUNCTIONAL TESTS =========== + + +@pytest.fixture +def testapp(): + """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 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() + + app = main({}, **{'sqlalchemy.url': 'postgres://CCallahanIV@localhost:5432/test_lj'}) + testapp = TestApp(app) + + SessionFactory = app.registry["dbsession_factory"] + engine = SessionFactory().bind + Base.metadata.create_all(bind=engine) + + with transaction.manager: + dbsession = get_tm_session(SessionFactory, transaction.manager) + dbsession.query(Entries).delete() + + return testapp + + +@pytest.fixture +def fill_the_db(testapp): + """Fill the database with some model instances. + + 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) + if len(dbsession.query(Entries).all()) == 0: + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + dbsession.add(row) + + +def test_home_route_has_list(testapp): + """The home page has a list in the html.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("ul")) == 1 + + +def test_home_route_with_data_has_filled_list(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("li")) == 6 + + +def test_home_route_has_list2(testapp): + """Without data the home page only has a list.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("ul")) == 1 + + +def test_create_entry_route_has_form(testapp): + """Test that the "create" route loads a page with a form.""" + response = testapp.get('/journal/new-entry', status=200) + html = response.html + assert len(html.find_all("form")) == 1 + + +def test_create_view_post_redirects(testapp): + """Test that a post request redirects to home.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/new-entry', post_params, status=302) + full_response = response.follow() + assert response.text[0:3] == '302' + assert len(full_response.html.find_all(id='entryListWrapper')) == 1 + + +def test_create_view_adds_to_db(testapp): + """Test that a post method to create view updates the db.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/new-entry', post_params, status=302) + full_response = response.follow() + assert full_response.html.find(class_='entryListItem').a.text == post_params["title"] + + +def test_update_route_has_populated_form(testapp, fill_the_db): + """Test the update view has a populated form.""" + response = testapp.get('/journal/1/edit-entry', status=200) + title = response.html.form.input["value"] + body = response.html.form.textarea.contents[0] + assert title == ENTRIES[0]["title"] + assert body == ENTRIES[0]["body"] + + +def test_update_view_post_redirects_changes_title(testapp): + """Test the update view redirects on a post request and changes title.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/2/edit-entry', post_params, status=302) + full_response = response.follow() + assert response.text[0:3] == '302' + assert full_response.html.find_all(href='http://localhost/journal/2')[0].text == post_params["title"] + + +def test_detail_route_loads_proper_entry(testapp, fill_the_db): + """Test that the detail route loads the proper entry.""" + response = testapp.get('/journal/2', status=200) + title = response.html.find_all(class_='articleTitle')[0].contents[0] + assert title == ENTRIES[1]["title"] diff --git a/learning_journal/tests/tests.py b/learning_journal/tests/tests.py new file mode 100644 index 0000000..60541e0 --- /dev/null +++ b/learning_journal/tests/tests.py @@ -0,0 +1,263 @@ +"""A short testing suite for the expense tracker.""" + + +import pytest +import transaction + +from pyramid import testing + +from learning_journal.models import Entries, get_tm_session +from learning_journal.models.meta import Base + +from datetime import datetime + + +@pytest.fixture(scope="session") +def configuration(request): + """Set up a Configurator instance. + + 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. + + This configuration will persist for the entire duration of your PyTest run. + """ + settings = {'sqlalchemy.url': 'postgres://CCallahanIV@localhost:5432/test_lj'} + config = testing.setUp(settings=settings) + config.include('learning_journal.models') + + def teardown(): + testing.tearDown() + + request.addfinalizer(teardown) + return config + + +@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() + + 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. + + """ + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + dummy_request.dbsession.add(row) + +ENTRIES = [ + {"title": "There's value in stepping back.", "creation_date": datetime.strptime("December 14, 2016", "%B %d, %Y"), "body": """Sometimes principles from our past help with our present. Before starting the double linked list today, I committed to two things: Starting with the tests. Designing before coding. I actually wrote some tests first, just sort of stream of consciousness but things quickly got messy. After flailing for a bit, I stepped back and mapped out the function of each method and made a list of everything I would want test about it. From that, I was able to write a fairly comprehensive testing suite that should require little refactoring to really test all functions of the Double Linked List. Joey and I had some frustrating issues to work out with the server assignment. For whatever reason, since yesterday, our message sending on both the client and server sides decided to start escaping all the \r and \n characters. It had us and the TA's stumped for... far too long. We ended up changing our search parameters in our receiving while loops so that things could work again. Now I'm worried that I'll wake up tomorrow and it won't work. Or it just won't work whomever grades our assignment. SUCH IS LIFE."""}, + {"title": "Revenge is Best SERVERED Cold", "creation_date": datetime.strptime("December 15, 2016", "%B %d, %Y"), "body": """The second you feel like youre starting to climb youre way out of the avalanche of work and information theres always some weird and random server error to pull you right back in. Im struggling a lot to find ways to refactor and clean up the server code. I feel like were working with a patchwork of try/except statements, teetering towers of if/else logic, and precarious while loops. Id like to see an example of a server done RIGHT. But I want to earn that - I want to finish my own server the hard way. Hey, urllib2, sup?"""}, + {"title": "Whiteboarding", "creation_date": datetime.strptime("December 16, 2016", "%B %d, %Y"), "body": "Its fun, its hard, it makes your brain bend in ways you never thought possible. Really lucky I was partnered with Joey. He had the essential, Aha! moment that led to the solution. Glad I was able to contribute though. I worry that in the future as I prep for interviews, I'll won't be able to rely on flexible thinking to solve new problems. I'll likely have to plan ahead and try to expose myself to as many problem scenarios as possible in order to succeed. So it goes, more studying ahead. The server assignment really started as a hacky mess. Lots of patchwork try/except blocks and grabbing specific errors in desperation. I think today we really nailed it down into something I can be somewhat proud of. Looking forward to the opportunity to go back in time and refactor things this weekend. Also looking forward to beer."}, + {"title": "Recent Awesome Things We've Learned", "creation_date": datetime.strptime("December 19, 2016", "%B %d, %Y"), "body": "
  • Big(O) Notation
  • Linked-List based Data Structures
  • Web App Deployment Frameworks (Pyramid)

It feels like we're moving beyond the introductory part of Python knowledge and are really starting to get into the meat n' potatoes of development and CS concepts. For the first time in a while, I'm starting to feel somewhat empowered as a dev-in-development. Not to say I know everything there is, but I know enough to now to actually make something with it, and more importantly, I'm starting to have some idea of what I don't know.

Despite the time crunch and the fast pace, I've enjoyed the class to this point. BUT, I am really excited for what is to come.

Of course, the second I begin to think this, the rug will be pulled out from underneath once again.

Looking forward to building stuff.

"}, + {"title": "Tunnels, Lights, Progress, etc.", "creation_date": datetime.strptime("December 20, 2016", "%B %d, %Y"), "body": """Two quick things, the binary heap was initially a pain in the ass. However, I feel really proud of the implementation I have. Hooray for max OR min. Finally, I'm really excited for the group project. I feel a strong personal connection to the purpose of this app and I'm excited to put our newfound skills and knowledge to good use."""}, + {"title": "They Said It Would Happen", "creation_date": datetime.strptime("December 21, 2016", "%B %d, %Y"), "body": "Unfortunately, I think I'm enjoying too much tinkering around with data structures. We have fallen far behind on the journal stuff. If we didn't have next week off, I'm not sure how it would all get done. Getting tough to balance home and school life. But that's the fun part, right? The challenge! BRING IT ON, COWBOY UP."} +] + + +# ======== UNIT TESTS ========== + +def test_new_entries_are_added(db_session): + """New expenses get added to the database.""" + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + db_session.add(row) + query = db_session.query(Entries).all() + assert len(query) == len(ENTRIES) + + +def test_home_view_returns_empty_when_empty(dummy_request): + """Test that the home view returns no objects in the expenses iterable.""" + from learning_journal.views.default import home_view + result = home_view(dummy_request) + assert len(result["entries"]) == 0 + + +def test_home_view_returns_objects_when_exist(dummy_request, add_models): + """Test that the home view does return objects when the DB is populated.""" + from learning_journal.views.default import home_view + result = home_view(dummy_request) + assert len(result["entries"]) == 6 + + +def test_create_view_returns_empty_dict_on_get(dummy_request): + """Test that the create_view method returns an empty dict on a get request.""" + from learning_journal.views.default import create_view + result = create_view(dummy_request) + assert result == {} + + +def test_create_view_updates_db_on_post(db_session, dummy_request): + """Test that the create_view method updates the DB with a POST request.""" + from learning_journal.views.default import create_view + dummy_request.method = "POST" + dummy_request.POST["title"] = "Some Title." + dummy_request.POST["body"] = "Some Body." + with pytest.raises(Exception): + create_view(dummy_request) + + query = db_session.query(Entries).all() + assert query[0].title == "Some Title." + assert query[0].body == "Some Body." + + +# ======== FUNCTIONAL TESTS =========== + + +@pytest.fixture +def testapp(): + """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 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() + + app = main({}, **{'sqlalchemy.url': 'postgres://CCallahanIV@localhost:5432/test_lj'}) + testapp = TestApp(app) + + SessionFactory = app.registry["dbsession_factory"] + engine = SessionFactory().bind + Base.metadata.create_all(bind=engine) + + return testapp + + +@pytest.fixture +def fill_the_db(testapp): + """Fill the database with some model instances. + + 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) + for entry in ENTRIES: + row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) + dbsession.add(row) + + +def test_home_route_has_list(testapp): + """The home page has a table in the html.""" + response = testapp.get('/', status=200) + html = response.html + assert len(html.find_all("ul")) == 1 + + +def test_home_route_with_data_has_filled_list(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("li")) == 6 + + +def test_home_route_has_list2(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("ul")) == 1 + + +def test_create_entry_route_has_form(testapp): + """Test that the "create" route loads a page with a form.""" + response = testapp.get('/journal/new-entry', status=200) + html = response.html + assert len(html.find_all("form")) == 1 + + +def test_create_view_post_redirects(testapp): + """Test that a post request redirects to home.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/new-entry', post_params, status=302) + full_response = response.follow() + assert response.text[0:3] == '302' + assert len(full_response.html.find_all(id='entryListWrapper')) == 1 + + +def test_create_view_adds_to_db(testapp): + """Test that a post method to create view updates the db.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/new-entry', post_params, status=302) + full_response = response.follow() + assert full_response.html.find(class_='entryListItem').a.text == post_params["title"] + + +def test_update_route_has_populated_form(testapp, fill_the_db): + """Test the update view has a populated form.""" + response = testapp.get('/journal/1/edit-entry', status=200) + title = response.html.form.input["value"] + body = response.html.form.textarea.contents[0] + assert title == ENTRIES[0]["title"] + assert body == ENTRIES[0]["body"] + + +def test_update_view_post_redirects_changes_title(testapp, fill_the_db): + """Test the update view redirects on a post request and changes title.""" + post_params = { + 'title': 'Some Title.', + 'body': 'Some Body.' + } + response = testapp.post('/journal/2/edit-entry', post_params, status=302) + full_response = response.follow() + assert response.text[0:3] == '302' + assert full_response.html.find_all(href='http://localhost/journal/2')[0].text == post_params["title"] + + +def test_detail_route_loads_proper_entry(testapp, fill_the_db): + """Test that the detail route loads the proper entry.""" + response = testapp.get('/journal/2', status=200) + title = response.html.find_all(class_='articleTitle')[0].contents[0] + assert title == ENTRIES[1]["title"] diff --git a/learning_journal/views/.slugignore b/learning_journal/views/.slugignore new file mode 100644 index 0000000..85b19f8 --- /dev/null +++ b/learning_journal/views/.slugignore @@ -0,0 +1 @@ +/learning_journal/tests \ No newline at end of file diff --git a/learning_journal/views/__init__.py b/learning_journal/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning_journal/views/default.py b/learning_journal/views/default.py new file mode 100644 index 0000000..81e4bae --- /dev/null +++ b/learning_journal/views/default.py @@ -0,0 +1,89 @@ +from pyramid.response import Response +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound +from datetime import date + +from sqlalchemy.exc import DBAPIError + +from learning_journal.models import Entries +from learning_journal.security import check_credentials +from pyramid.security import remember, forget + + +@view_config(route_name='login', renderer='../templates/login.jinja2', permission="guest") +def login_view(request): + """Handle the login view.""" + if request.method == 'POST': + username = request.params.get('username', '') + password = request.params.get('password', '') + if check_credentials(username, password): + headers = remember(request, username) + return HTTPFound(location=request.route_url('home'), headers=headers) + return {} + + +@view_config(route_name='logout') +def logout_view(request): + """Handle logging the user out.""" + auth_head = forget(request) + return HTTPFound(request.route_url("home"), headers=auth_head) + + +@view_config(route_name='home', renderer='../templates/list.jinja2', permission="guest") +def home_view(request): + try: + query = request.dbsession.query(Entries).all() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status=500) + return {'entries': query} + + +@view_config(route_name='create', renderer='../templates/create_entry.jinja2', permission='author') +def create_view(request): + if request.method == "POST": + entry = request.POST + row = Entries(title=entry["title"], creation_date=date.today(), body=entry["body"]) + request.dbsession.add(row) + return HTTPFound(request.route_url("home")) + return {} + + +@view_config(route_name="detail", renderer="../templates/entry.jinja2", permission='guest') +def detail_view(request): + """Handle the detail view for a specific journal entry.""" + the_id = int(request.matchdict["id"]) + entry = request.dbsession.query(Entries).get(the_id) + return {"entry": entry} + + +@view_config(route_name="update", renderer="../templates/edit_entry.jinja2", permission='author') +def update_view(request): + """Handle the view for updating a new entry.""" + the_id = int(request.matchdict["id"]) + if request.method == "POST": + entry = request.POST + query = request.dbsession.query(Entries).get(the_id) + query.title = entry["title"] + query.body = entry["body"] + request.dbsession.flush() + return HTTPFound(request.route_url("home")) + entry = request.dbsession.query(Entries).get(the_id) + return {"entry": entry} + + + +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_learning_journal_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/learning_journal/views/notfound.py b/learning_journal/views/notfound.py new file mode 100644 index 0000000..69d6e28 --- /dev/null +++ b/learning_journal/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/production.ini b/production.ini new file mode 100644 index 0000000..8b20de8 --- /dev/null +++ b/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:learning_journal + +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/learning_journal.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, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_learning_journal] +level = WARN +handlers = +qualname = learning_journal + +[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/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b769f12 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = learning_journal +python_files = *.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6be9d43 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,33 @@ +appnope==0.1.0 +decorator==4.0.10 +ipython==5.1.0 +ipython-genutils==0.1.0 +Jinja2==2.8 +Mako==1.0.6 +MarkupSafe==0.23 +PasteDeploy==1.5.2 +pexpect==4.2.1 +pickleshare==0.7.4 +prompt-toolkit==1.0.9 +ptyprocess==0.5.1 +Pygments==2.1.3 +pyramid==1.7.3 +pyramid-debugtoolbar==3.0.5 +pyramid-ipython==0.2 +pyramid-jinja2==2.7 +pyramid-mako==1.0.2 +pyramid-tm==1.1.1 +repoze.lru==0.6 +simplegeneric==0.8.1 +six==1.10.0 +SQLAlchemy==1.1.4 +traitlets==4.3.1 +transaction==2.0.3 +translationstring==1.3 +venusian==1.0 +waitress==1.0.1 +wcwidth==0.1.7 +WebOb==1.7.0 +zope.deprecation==4.2.0 +zope.interface==4.3.3 +zope.sqlalchemy==0.7.7 diff --git a/run b/run new file mode 100755 index 0000000..20ed4a0 --- /dev/null +++ b/run @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +python setup.py develop +initialize_db production.ini +python runapp.py \ No newline at end of file diff --git a/runapp.py b/runapp.py new file mode 100644 index 0000000..e5df1c6 --- /dev/null +++ b/runapp.py @@ -0,0 +1,10 @@ +import os + +from paste.deploy import loadapp +from waitress import serve + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app = loadapp('config:production.ini', relative_to='.') + + serve(app, host="0.0.0.0", port=port) \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..356ad27 --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +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', + 'pyramid_ipython', + 'ipython', + 'psycopg2', + 'passlib', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + 'tox', + ] + +setup(name='learning_journal', + version='0.0', + description='learning_journal', + 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 = learning_journal:main + [console_scripts] + initialize_db = learning_journal.scripts.initializedb:main + """, + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7dc4fbd --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py27, py35 + +[testenv] +commands = py.test learning_journal/tests.py +deps = + pytest + webtest \ No newline at end of file