diff --git a/learning_journal/routes.py b/learning_journal/routes.py index 9e890bf..101c2e4 100644 --- a/learning_journal/routes.py +++ b/learning_journal/routes.py @@ -7,3 +7,5 @@ def includeme(config): config.add_route("update", "/journal/{id:\d+}/edit-entry") config.add_route("login", "/login") config.add_route("logout", "/logout") + config.add_route("delete", "/delete/{id:\d+}") + config.add_route("tweet", "/tweet/{id:\d+}") diff --git a/learning_journal/static/app.js b/learning_journal/static/app.js new file mode 100644 index 0000000..4920d9c --- /dev/null +++ b/learning_journal/static/app.js @@ -0,0 +1,25 @@ +$(document).ready(function(){ + $("#homeSubmitButton").on("click", function(e){ + e.preventDefault() + entry = $(this).parent().serializeArray() + $.ajax({ + method: 'POST', + url: '/journal/new-entry', + data: { + "csrf_token": entry[0]["value"], + "title": entry[1]["value"], + "body": entry[2]["value"] + }, + success: function(result){ + new_id = parseInt($(".entryListItem a").first().attr('href').split('/')[4]) + 1 + $(".entryListItem").first().prepend( + "
  • " + + "

    " + entry[1]["value"] + "

    " + + "

    Created " + Date.now() + "

    " + + "
    " + + "
  • " + ) + } + }); + }); +}); diff --git a/learning_journal/static/assets/awesome.png b/learning_journal/static/assets/awesome.png new file mode 100644 index 0000000..5dd9a9e Binary files /dev/null and b/learning_journal/static/assets/awesome.png differ diff --git a/learning_journal/static/base.css b/learning_journal/static/base.css index 7ac7a6e..ee17947 100644 --- a/learning_journal/static/base.css +++ b/learning_journal/static/base.css @@ -17,3 +17,8 @@ h2{ h3{ font-size:1.25em; } + +body, main{ + margin: 0px; + width: 100%; +} \ No newline at end of file diff --git a/learning_journal/static/layout.css b/learning_journal/static/layout.css index 34df568..57816a2 100644 --- a/learning_journal/static/layout.css +++ b/learning_journal/static/layout.css @@ -8,11 +8,28 @@ header a{ font-family: Share Tech Mono; color: white; line-height: 40px; - padding-left: 8px; + margin-left: 12px; +} + +header a:hover { + border-bottom: 2px solid white; } main { width: 90%; height: 100%; margin: auto; +} + +.entryEditList li{ + display: inline; + margin-left: 12px; +} + +.entryEditList a{ + color: black; +} + +.entryEditList a:hover{ + border-bottom: 2px solid black; } \ No newline at end of file diff --git a/learning_journal/static/module.css b/learning_journal/static/module.css index f01d472..da39266 100644 --- a/learning_journal/static/module.css +++ b/learning_journal/static/module.css @@ -6,6 +6,12 @@ background-color: lightgrey; } +#entryListWrapper ul{ + list-style: none; + margin: 0px; + padding-left: 2px; +} + #entryDisplayWrapper{ width: 80%; margin: auto; @@ -69,4 +75,11 @@ form #submitButton{ #pageTitle{ text-align: center; +} + +header img { + float: right; + height: 100px; + width: 100px; + padding-right: 10px; } \ No newline at end of file diff --git a/learning_journal/static/reset.css b/learning_journal/static/reset.css deleted file mode 100644 index bba3cdf..0000000 --- a/learning_journal/static/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* 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/entry.jinja2 b/learning_journal/templates/entry.jinja2 index ea6eaa8..19efaf6 100644 --- a/learning_journal/templates/entry.jinja2 +++ b/learning_journal/templates/entry.jinja2 @@ -7,9 +7,14 @@

    by Ted Callahan

    - {{ entry.body|e }} + {{ entry.body|safe }}
    {% if request.authenticated_userid %} - EDIT + {% endif %} + {% endblock %} diff --git a/learning_journal/templates/form.jinja2 b/learning_journal/templates/form.jinja2 index 16b322f..ccfc890 100644 --- a/learning_journal/templates/form.jinja2 +++ b/learning_journal/templates/form.jinja2 @@ -5,6 +5,7 @@
    +

    diff --git a/learning_journal/templates/layout.jinja2 b/learning_journal/templates/layout.jinja2 index 3a3e44b..f2b6de7 100644 --- a/learning_journal/templates/layout.jinja2 +++ b/learning_journal/templates/layout.jinja2 @@ -6,10 +6,10 @@ - +

    @@ -20,9 +20,15 @@ {% else %} Login {% endif %} + awesome
    {% block body %}{% endblock %}
    + + + \ No newline at end of file diff --git a/learning_journal/templates/list.jinja2 b/learning_journal/templates/list.jinja2 index 3ae8b2b..9698472 100644 --- a/learning_journal/templates/list.jinja2 +++ b/learning_journal/templates/list.jinja2 @@ -13,6 +13,16 @@ {% else %}

    This Journal is Empty. =(

    {% endif %} + {% if request.authenticated_userid %} + + +

    Title:

    + +

    Body:

    + + +
    + {% endif %} {% endblock %} \ No newline at end of file diff --git a/learning_journal/templates/login.jinja2 b/learning_journal/templates/login.jinja2 index e887bb3..1fd554d 100644 --- a/learning_journal/templates/login.jinja2 +++ b/learning_journal/templates/login.jinja2 @@ -2,7 +2,6 @@ {% block body %}

    Login

    -
    diff --git a/learning_journal/templates/mytemplate.jinja2 b/learning_journal/templates/mytemplate.jinja2 deleted file mode 100644 index ade7f2c..0000000 --- a/learning_journal/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/learning_journal/tests.py b/learning_journal/tests.py index 2954ecc..7cdd7ed 100644 --- a/learning_journal/tests.py +++ b/learning_journal/tests.py @@ -84,8 +84,18 @@ def add_models(dummy_request): ] +@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_entries_are_added(db_session): """New expenses get added to the database.""" for entry in ENTRIES: @@ -127,14 +137,14 @@ def test_create_view_updates_db_on_post(db_session, dummy_request): query = db_session.query(Entries).all() assert query[0].title == "Some Title." - assert query[0].body == "Some Body." + assert query[0].body[3:-4] == "Some Body." # ======== FUNCTIONAL TESTS =========== -@pytest.fixture -def testapp(): +@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 @@ -157,6 +167,7 @@ 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() @@ -167,14 +178,19 @@ def main(global_config, **settings): engine = SessionFactory().bind Base.metadata.create_all(bind=engine) - with transaction.manager: - dbsession = get_tm_session(SessionFactory, transaction.manager) - dbsession.query(Entries).delete() + def tear_down(): + Base.metadata.drop_all(bind=engine) + + request.addfinalizer(tear_down) + + # with transaction.manager: + # dbsession = get_tm_session(SessionFactory, transaction.manager) + # dbsession.query(Entries).delete() return testapp -@pytest.fixture +@pytest.fixture(scope="session") def fill_the_db(testapp): """Fill the database with some model instances. @@ -189,6 +205,17 @@ def fill_the_db(testapp): row = Entries(title=entry["title"], creation_date=entry["creation_date"], body=entry["body"]) dbsession.add(row) + 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_list(testapp): """The home page has a list in the html.""" @@ -197,29 +224,25 @@ def test_home_route_has_list(testapp): 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.""" + """Without data the home page has an empty list.""" response = testapp.get('/', status=200) html = response.html - assert len(html.find_all("ul")) == 1 + assert len(html.find_all("li")) == 0 -def test_create_entry_route_has_form(testapp): +def test_create_entry_route_forbidden(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 + response = testapp.get('/journal/new-entry', status=403) + assert "Forbidden" in response.text + assert response.status_code == 403 + +# ================= LOGGED IN ==================== -def test_create_view_post_redirects(testapp): +def test_create_view_post_redirects(set_auth_credentials, testapp): """Test that a post request redirects to home.""" + response = testapp.post("/login", params={"username": "testme", "password": "foobar"}) post_params = { 'title': 'Some Title.', 'body': 'Some Body.' @@ -230,6 +253,12 @@ def test_create_view_post_redirects(testapp): assert len(full_response.html.find_all(id='entryListWrapper')) == 1 +def test_entry_route_not_found(testapp): + """Test that requesting a non-existing entry returns a 404.""" + response = testapp.get('/journal/99999999', status=404) + assert response.status_code == 404 + + def test_create_view_adds_to_db(testapp): """Test that a post method to create view updates the db.""" post_params = { @@ -241,6 +270,13 @@ def test_create_view_adds_to_db(testapp): assert full_response.html.find(class_='entryListItem').a.text == post_params["title"] +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_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) diff --git a/learning_journal/tweeting.py b/learning_journal/tweeting.py new file mode 100644 index 0000000..8c32848 --- /dev/null +++ b/learning_journal/tweeting.py @@ -0,0 +1,14 @@ +"""This module configures a twitter API object to be used to send tweets.""" + +import twitter +import os + + +def include_me(): + """Create a twitter api object for use in tweeting posts.""" + twitter_api = twitter.Api( + consumer_key=os.environ.get("TWITTER_CONSUMER_KEY", None), + consumer_secret=os.environ.get("TWITTER_SECRET", None), + access_token_key=os.environ.get("TWITTER_ACCESS_TOKEN", None), + access_token_secret=os.environ.get("TWITTER_ACCESS_TOKEN_SECRET", None) + ) diff --git a/learning_journal/views/default.py b/learning_journal/views/default.py index 81e4bae..4c35eeb 100644 --- a/learning_journal/views/default.py +++ b/learning_journal/views/default.py @@ -9,6 +9,10 @@ from learning_journal.security import check_credentials from pyramid.security import remember, forget +import markdown +import twitter +import os + @view_config(route_name='login', renderer='../templates/login.jinja2', permission="guest") def login_view(request): @@ -41,8 +45,10 @@ def home_view(request): @view_config(route_name='create', renderer='../templates/create_entry.jinja2', permission='author') def create_view(request): if request.method == "POST": + md = markdown.Markdown() entry = request.POST - row = Entries(title=entry["title"], creation_date=date.today(), body=entry["body"]) + entry_html = md.convert(entry["body"]) + row = Entries(title=entry["title"], creation_date=date.today(), body=entry_html) request.dbsession.add(row) return HTTPFound(request.route_url("home")) return {} @@ -71,6 +77,32 @@ def update_view(request): return {"entry": entry} +@view_config(route_name="delete") +def delete_view(request): + """Handle deleting a post.""" + the_id = int(request.matchdict["id"]) + if request.authenticated_userid: + entry = request.dbsession.query(Entries).get(the_id) + request.dbsession.delete(entry) + return HTTPFound(request.route_url("home")) + return HTTPFound(request.route_url("home")) + + +@view_config(route_name="tweet") +def tweet_view(request): + """Handle tweeting the title of a post with link to post.""" + the_id = request.matchdict["id"] + twitter_api = twitter.Api( + consumer_key=os.environ.get("TWITTER_CONSUMER_KEY", None), + consumer_secret=os.environ.get("TWITTER_SECRET", None), + access_token_key=os.environ.get("TWITTER_ACCESS_TOKEN", None), + access_token_secret=os.environ.get("TWITTER_ACCESS_TOKEN_SECRET", None) + ) + post_url = "https://tedsbetterlearningjournal.herokuapp.com/journal/" + the_id + title = request.dbsession.query(Entries).get(the_id).title + twitter_api.PostUpdate(title + '\n' + post_url) + + return HTTPFound(request.route_url("home")) db_err_msg = """\ Pyramid is having a problem using your SQL database. The problem diff --git a/requirements.txt b/requirements.txt index 6be9d43..b0a3f1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,25 @@ appnope==0.1.0 +beautifulsoup4==4.5.1 +coverage==4.2 decorator==4.0.10 +future==0.16.0 ipython==5.1.0 ipython-genutils==0.1.0 Jinja2==2.8 Mako==1.0.6 +Markdown==2.6.7 MarkupSafe==0.23 +numpy==1.11.3 +oauthlib==2.0.1 +passlib==1.7.0 PasteDeploy==1.5.2 pexpect==4.2.1 pickleshare==0.7.4 +pluggy==0.4.0 prompt-toolkit==1.0.9 +psycopg2==2.6.2 ptyprocess==0.5.1 +py==1.4.32 Pygments==2.1.3 pyramid==1.7.3 pyramid-debugtoolbar==3.0.5 @@ -17,17 +27,26 @@ pyramid-ipython==0.2 pyramid-jinja2==2.7 pyramid-mako==1.0.2 pyramid-tm==1.1.1 +pytest==3.0.5 +pytest-cov==2.4.0 +python-twitter==3.2 repoze.lru==0.6 +requests==2.12.4 +requests-oauthlib==0.7.0 +scipy==0.18.1 simplegeneric==0.8.1 six==1.10.0 SQLAlchemy==1.1.4 +tox==2.5.0 traitlets==4.3.1 transaction==2.0.3 translationstring==1.3 venusian==1.0 +virtualenv==15.1.0 waitress==1.0.1 wcwidth==0.1.7 WebOb==1.7.0 +WebTest==2.0.24 zope.deprecation==4.2.0 zope.interface==4.3.3 zope.sqlalchemy==0.7.7 diff --git a/setup.py b/setup.py index 356ad27..809a1d5 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,8 @@ 'ipython', 'psycopg2', 'passlib', + 'python-twitter', + 'markdown' ] tests_require = [