Welcome to Pentameter’s documentation!

Pentameter.org documentation for working with the backend and frontend codebases in order to develop and maintain the code to run the site.

Continue to Foreword, or Installation, or the Quickstart.

Project Guide

Getting the project set up and general guidance on how to work with the code from development to deploying.

Foreword

Poetry for poetry sake. You’ve been warned.

Installation

Required tools to install for development purposes. Continue to Quickstart if you already have installed all tools.

  • Make sure python is installed at a version >=2.7
  • Make sure Node.js is installed at a version >=5.10
  • Install Vagrant at version >=1.8 to manage virtual machines
  • Install VirtualBox at version >=5.0 to manage virtual machines
  • Pull the code down ptbrodie/pentameter
  • From the code directory install/provision virtual machine with vagrant up
  • Wait for everything to download and install
  • Jump to Quickstart to get the app up and running

Quickstart

Make sure you have everything installed. See Installation instructions to verify you have installed all required tools first.

Python Flask App

Getting the Flask app set up locally.

  • From within the repo, run vagrant ssh
  • Once in the VM go to flask app directory cd pentameter/
  • Install latest requirements with pip install --upgrade -r dev-requirements.txt
  • Initialize the database with python init.py
  • Run the app with python run.py
  • Should see app at 55.55.55.10

React Front End

  • SSH into VM vagrant ssh
  • Once with VM to go front end app directory cd pentameter/app
  • Make sure npm dependencies are installed npm install
  • Build the app with npm run build
  • Start/run app with npm start

Config

Configuration for the app requires the environment is set up with variables that tell the app where to look for services, what credentials to use, or general settings to use:

# Define the application directory
import os
BASE_DIR = os.path.abspath(os.path.dirname(__file__))

PENTAMETER_ENV = os.getenv("PENTAMETER_ENV", "development")
PENTAMETER_HOME = os.getenv("PENTAMETER_HOME", os.getenv("PYTHONPATH", os.getcwd()))

SERVER_URL = os.getenv("SERVER_URL", "http://55.55.55.10")
S3_BUCKET_NAME = None
if PENTAMETER_ENV == "production":
    SERVER_URL = "https://pentameter.org"
    S3_BUCKET_NAME = "assets.pentameter.org"
if PENTAMETER_ENV == "staging":
    S3_BUCKET_NAME = "assets.staging.pentameter.org"
    SERVER_URL = "http://staging.pentameter.org"
if PENTAMETER_ENV == "development":
    S3_BUCKET_NAME = "assets.dev.pentameter.org"

# Database
POSTGRESQL_DATABASE_HOST = os.getenv("PENTAMETER_DBHOST", "127.0.0.1")
POSTGRESQL_DATABASE_NAME = os.getenv("PENTAMETER_DBNAME", "pentameter")
POSTGRESQL_DATABASE_USER = os.getenv("PENTAMETER_DBUSER", "pentameter")
POSTGRESQL_DATABASE_PWD = os.getenv("PENTAMETER_DBPWD", "pentameter")
SQLALCHEMY_DATABASE_URI = "postgresql://%s:%s@%s/%s" % \
                          (POSTGRESQL_DATABASE_USER,
                           POSTGRESQL_DATABASE_PWD,
                           POSTGRESQL_DATABASE_HOST,
                           POSTGRESQL_DATABASE_NAME)

# Cache
REDIS_HOST = os.getenv("PENTAMETER_REDISHOST", "127.0.0.1")
REDIS_PORT = os.getenv("PENTAMETER_REDISPORT", 6379)
REDIS_PWD = os.getenv("PENTAMETER_REDISPWD")
REDIS_TIMEOUT = 3

# EC2
DEFAULT_EC2_REGION = "us-west-1"

# Application threads. A common general assumption is
# using 2 per available processor cores - to handle
# incoming requests using one and performing background
# operations using the other.
THREADS_PER_PAGE = 1

# Enable protection agains *Cross-site Request Forgery (CSRF)*
CSRF_ENABLED = True

# Use a secure, unique and absolutely secret key for
# signing the data.
CSRF_SESSION_KEY = os.getenv("CSRF_SESSION_KEY", "sosecret")

PENTAMETER_SECRET_KEY = os.getenv("PENTAMETER_SECRET_KEY", "CHANGE_ME_BRO")
PENTAMETER_PASSWORD_SALT = os.getenv("PENTAMETER_PASSWORD_SALT", "CHANGE_ME_PLEASE")

ALGOLIA_APPLICATION_ID = os.getenv("ALGOLIA_APPLICATION_ID", "No Algolia ID Found")
ALGOLIA_API_KEY = os.getenv("ALGOLIA_API_KEY", "No Algolia Key Found")
MANDRILL_API_KEY = os.getenv("MANDRILL_API_KEY", "No Mandrill Key Found.")
AWS_KEY = os.getenv("AWS_KEY", "No AWS Key Found.")
AWS_SECRET = os.getenv("AWS_SECRET", "No AWS Secret Found.")

POEM_SIMILARITY_THRESHOLD = 0.9

PENTAMETER_TRANSACTIONAL_EMAIL = "email@pentameter.org"
PENTAMETER_NOTIFY_EMAIL = "messenger@pentameter.org"
PENTAMETER_RECOMMEND_EMAIL = "prophet@pentameter.org"

STATIC_FOLDER = os.path.join(PENTAMETER_HOME, "pentameter/static")

Testing

When developing tests should be added and maintained with feature development as well as fixing bugs or refactoring code.

  • Run backend tests with py.test from app directory

Gitflow

Outlines a reasonable git flow when doing development with the team.

Workflow

We use a branching workflow with rebasing to keep the commit log uniform across all our branches:

  1. create new branch
  2. commit changes to branch & push them to origin
  3. rebase regularly onto master
  4. force push to overwrite origin/new-branch
  5. open pull request

Feature Branches

Working with creating new branches for features or fixes:

master~$ git pull -r
master~$ git checkout -b new-branch
new-branch~$ # do what you need to make changes to new-branch
new-branch~$ git add -A    # alternatively you can add specific files
new-branch~$ git commit -m "a description of some changes to my branch"
new-branch~$ git push -u origin new-branch   # further changes can be pushed with just 'git push'

Rebasing

Keep commits small and rebase often so that conflicts will be easier to resolve:

new-branch~$ git fetch
new-branch~$ git rebase origin/master
new-branch~$ # resolve conflicts during rebase
new-branch~$ git push origin new-branch --force   # force push to overwrite origin with rebased version

Pull Requests

  1. When your branch is ready to be reviewed by others you can create a pull request.
  2. Make sure you have latest master branch changes rebased into your working branch
  3. Push your branch up to the remote with git push origin new-branch
  4. Create a pull request via the Github interface wtih master as the base branch.
  5. Fill out details on what was changed, any setup needed to review/test code, and assign appropriate labels and a person to review.

Deploying

Staging

Currently deploying to staging is a multi-part process.

  1. Make sure your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are exported or prepended to the deploy command.
  2. Run the following from the app directory fab -A staging deploy
  3. SSH into staging.pentameter.org
  4. Go to app directory /home/ubuntu/pentameter
  5. Build and upload assets with fab staging_assets

Production

TBD

API Reference

Information on the backend API that is used by the front end.

API

Overview

All endpoints are prepended with /api/ and will respond with JSON back with everything under a top level data key:

{
    "data": {
        ...
    }
}

Response Types

Author Response

All author responses take the form:

{
   "data" : {
        "status_code": "<200 404 403 etc>",
        "message": "<message>",
        "author": {
            "firstname": "firstname",
            "lastname": "lastname",
            "username": "username",
            "fullname": "firstname lastname",
            "email": "email@email.com",
            "bio": "this is a bio.",
            "birth_year": "1900",
            "death_year": "1984",
            "profile_banner_url": "http://assets.pentameter.org/img/banner_url.jpg",
            "profile_photo_url": "http://assets.pentameter.org/img/photo_url.jpg",
            "poems_count": "2",
            "poems": [
               { "is_published": "true", "id": "poem_id_1","title": "Poem Title 1", "body": "body with<br>html", "etc": "etc" },
               { "is_published": "false", "id": "poem_id_2","title": "Poem Title 2", "body": "body with<br>html 2", "etc": "etc" }
            ],
            "collections_count": "1",
            "collections": [
               { "is_public": "true", "id": "collection_id_1", "etc": "etc" }
            ]
        }
   }
}
  • "author" section will be an empty object if no author found.
  • "email" and other personal data will only be shown from me/ endpoints.
  • "poems" section will show is_published if and only if the owner of the poem is making the request.
List of Author

A list of authors response will take the form:

{
   "data" : {
        "status_code": "<200 404 403 etc>",
        "message": "<message>",
        "num_authors": 3
        "authors": [
            {
                "id": "asdf-asdf-asdf",
                "firstname": "Renat",
                "lastname": "etc"
            },
            {
                "id": "qwer-qwer-qwer",
                "firstname": "Lusha",
                "etc": "etc"
            }
            {
                "id": "qwer-qwer-qwer",
                "firstname": "Dan",
                "etc": "etc"
            }
        ]
   }
  • No personal data will be returned in author objects.
Collection Response

All collection responses will take the form:

{
  "data": {
    "id": "53aa8968-077b-4781-a134-7e70afc79d7b",
    "message": "Collection",
    "status_code": 200
    "collection": {
      "author": "048d0a75-023c-4705-a483-9eff8cb7ed8f",
      "create_date": "2016-05-10T23:54:31.956597",
      "description": "The baz collection",
      "id": "53aa8968-077b-4781-a134-7e70afc79d7b",
      "is_public": null,
      "modify_date": "2016-05-10T23:55:14.267262",
      "poems": [],
      "tags": "temp, for example",
      "title": "baz"
    },
  }
}
  • "collection" field will be empty if nothing was found.
List of Collections

A list of collections response will take the form:

{
  "data": {
    "message": "List of Collections",
    "status_code": 200
    "num_collections": 3
    "collections": [
        {
            "id": "collection-id",
            "title": "collection-title",
            "author": "author-id",
            "poems": [
                { "id": "asdf", "title": "You know", "etc": "etc"},
                { "id": "asdf", "title": "etc"}
            ]
        },
        {
            "id": "collection-id",
            "title": "collection-title",
            "author": "author-id",
            "poems": [
                { "id": "asdf", "title": "You know", "etc": "etc"},
                { "id": "asdf", "title": "etc"}
            ]
        },
        {
            "id": "collection-id",
            "title": "collection-title",
            "author": "author-id",
            "poems": [
                { "id": "asdf", "title": "You know", "etc": "etc"},
                { "id": "asdf", "title": "etc"}
            ]
        }
    ]
  }
}
Poem Response

All poem responses will take the form:

{
   "data" : {
        "status_code": "<200 404 403 etc>",
        "message": "<message>",
        "poem": {
            "id": "asdf-asdf-adsf",
            "title": "Poem Title",
            "title_id": "poem-title",
            "year": 1999,
            "read_count": 12,
            "tags": ["birds", "sky", "venture capital"],
            "body": "<p>an html line of a poem. <br/><p>another line.",
            "author": {
                "firstname": "firstname",
                "lastname": "lastname",
                "username": "username",
                "fullname": "firstname lastname",
                "email": "email@email.com",
                "bio": "this is a bio.",
                "birth_year": "1900",
                "death_year": "1984",
                "profile_banner_url": "http://assets.pentameter.org/img/banner_url.jpg",
                "profile_photo_url": "http://assets.pentameter.org/img/photo_url.jpg",
                "poems": [
                    "poem-id-1",
                    "poem-id-2",
                    "poem-id-3"
                ]
            }
        }
   }
}
  • "poem" section will be an empty object if no poem found.
  • An additional share_key will be present in responses from the /api/me/poem/ endpoints. - This key can be used to retrieve poems using the /api/poem/private/<share_key> endpoint.
List of Poems

A list of poems response will take the form:

{
   "data" : {
        "status_code": "<200 404 403 etc>",
        "message": "<message>",
        "num_poems": 2
        "poems": [
            {
                "id": "asdf-asdf-asdf",
                "title": "The Knight",
                "etc": "etc"
            },
            {
                "id": "qwer-qwer-qwer",
                "title": "The Rook",
                "etc": "etc"
            }
        ]
   }
}

Endpoints

.._quickref_api:

Quickref
auth

POST /api/auth/login Logs a user in.

POST /api/auth/signup Registers a new user. Returns author.

GET /api/auth/logout Logs a user out.

POST /api/auth/forgot-password Sends ‘forgot password’ email.

POST /api/auth/reset-password Resets a password, linked from forgot password email.

GET /api/auth/email-confirmation Sends ‘email confirmation’ email.

POST /api/auth/verify-email Verifies user’s email, linked from email-confirmation email

author

GET /api/author/<author_id> Get author.

GET /api/author/<author_id>/collection Get all collections by author. Returns list of collections.

GET /api/author/<author_id>/collection/<collection_id> Get one collection by author. Returns collection.

GET /api/author/<author_id>/poem Get all poems by author. Returns list of poems.

GET /api/author/<author_id>/poem/<poem_id> Get all poems by author. Returns poem.

Future: GET /api/author/<author_id>/follower Get all followers for author. Returns list of authors.

Future: PUT /api/author/<author_id>/follower Follow author.

Future: DELETE /api/author/<author_id>/follower Unfollow author.

Future: GET /api/author/list/<list_id> Get list of authors (e.g. featured). Returns list of authors.

collection

GET /api/collection/<collection_id> Get collection. Returns collection.

GET /api/collection/<collection_id>/poem Get all poems in collection. Returns list of poems.

GET /api/collection/<collection_id>/poem/<poem_id> Get poem in collection. Returns poem.

Future: PUT /api/collection/<collection_id>/follower Follow collection.

Future: DELETE /api/collection/<collection_id>/follower Unfollow collection.

Future: GET /api/collection/list/<list_id> Get a list of collections (e.g. xmas). Returns List of collections.

me

GET /api/me Get current user. Returns Author

PUT /api/me Update current user. Returns Author

POST /api/me/photo Upload photos for user. Returns Author.

DELETE /api/me Delete current user. Returns Author

POST /api/me/poem Create a poem. Returns poem.

GET /api/me/poem Get all current user’s poems. Returns list of poems.

GET /api/me/poem/<poem_id> Get one poem by current user. Returns poem.

PUT /api/me/poem/<poem_id> Update poem. Returns poem.

DELETE /api/me/poem/<poem_id> Delete poem. Returns poem.

GET /api/me/collection Get all current user’s collections. Returns list of Collections

POST /api/me/collection Create a new collection. Returns collection.

PUT /api/me/collection/<collection_id> Update a collection. Returns Collection.

DELETE /api/me/collection/<collection_id> Update a collection. Returns collection.

GET /api/me/collection/<collection_id>/poem Get all poems in collection. Returns list of poems

PUT /api/me/collection/<collection_id>/poem Add poem to collection. Returns collection.

DELETE /api/me/collection/<collection_id>/poem Remove poem from collection. Returns collection.

poem

GET /api/poem/<poem_id> Get poem by id. Returns Poem.

Future: PUT /api/poem/<poem_id>/metric Increment a metric for the given poem. (e.g. read, clicked, shared)

Future: PUT /api/poem/<poem_id>/like Like a poem.

Future: DELETE /api/poem/<poem_id/like Unlike a poem.

Auth

All auth endpoints begin with the prefix /api/auth

login

POST /api/auth/login

Logs in an existing author:

Returns: Author

Method: POST
Content-Type: application/json
Body:
{
    "email": "email@email.com",
    "password": "password"
}
signup

POST /api/auth/signup

Registers and creates a new author:

Returns: Author

Method: POST
Content-Type: application/json
Body:
{
    "email": "email@email.com",
    "firstname": "firstname",
    "lastname": "lastname",
    "password": "password"
}
logout

GET /api/auth/logout

Logs an author out if they are logged in:

Returns: Author

Method: GET
Content-Type: N/A
Body: Empty
forgot password

POST /api/auth/forgot-password

Triggers a “forgot password” email to be sent to a specified email address:

Returns: Author

Method: POST
Content-Type: application/json
Body:
{
    "email": "email@email.com",
}
reset password

POST /api/auth/reset-password

Used to reset a password from a temporary sign-in link that was sent to the author through the forgot password endpoint:

Returns: Author

Method: POST
Content-Type: application/json
Body:
{
    "token": "<Forgot Password Token>",
    "new_password": "password",
    "confirmed_password": "password"
}
verify email

POST /api/auth/verify-email

Used to verify a user’s email after they have been sent a token URL to their email:

Returns: Author

Method: POST
Content-Type: application/json
Body:
{
    "token": "<verify email token>"
}
email confirmation

GET /api/auth/email-confirmation

Used to send an email to the email address the author signed up with with a temporary link that can be used to confirm that the email is real and is controlled by the author:

Returns: Author

Method: GET
Content-Type: N/A
Body: Empty
Author
  • Pentameter users are called authors.
  • Pentameter author endpoints are used to manage author-centered activity.
  • All author endpoints begin with the prefix /api/author.
Get author by ID

GET /api/author/<author_id>

Retrieve an author by system id:

Returns: Author Response

Method: GET
Get one collection by author

GET /api/author/<author_id>/collection/<collection_id>

Retrieve the specified collection by the specified author:

Returns: Collection Response

Method: GET
Get collections by author

GET /api/author/<author_id>/collection

Retrieve the specified author’s public collections:

Returns: List of Collections Response

Method: GET
Get one poem by author

GET /api/author/<author_id>/poem/<poem_id>

Retrieve the specified poem by the specified author:

Returns: Poem Response

Method: GET
Get poems by author

GET /api/author/<author_id>/poem

Retrieve the specified author’s published poems:

Returns: List of Poems Response

Method: GET
Get author’s followers

GET /api/author/<author_id>/follower

Retrieve a list of followers for the given author:

Returns: List of Authors Response

Method: GET
follow

PUT /api/author/<author_id>/follower

Follow the given author:

Returns: Author Response

Method: PUT
Unfollow

DELETE /api/author/<author_id>/follower

Unfollow the given author:

Returns: Author Response

Method: DELETE
Get list of authors

GET /api/author/list/<list_id>

Get a list of authors (e.g. featured or top or by genre):

Returns: List of Authors Response

Method: GET
Collection

All endpoints require an authenticated user.

get

GET /api/collection/<collection_id>

Used to get a public collection by id:

Returns: Collection Response

Method: GET
get many poems in collection

GET /api/collection/<collection_id>/poem?author_id=<author_id>

Used to get a subset of poems from the specified collection:

Returns: List of Poems Response

Method: GET
URL Params:
    - author_id: an author's id
    - tags: future?

NOTE: No URL params returns all poems in collection

get one poem in collection

GET /api/collection/<collection_id>/poem/<poem_id>

Used to get the specified poem in the given collection:

Returns: Poem Response

Method: GET
follow

PUT /api/collection/<collection_id>/follower

Used to make the current user follow the collection:

Returns: Collection Response

Method: PUT
Login: Required
unfollow

DELETE /api/collection/<collection_id>/follower

Used to remove the current user from the collection’s followers:

Returns: Collection Response

Method: DELETE
Login: Required
get followers

GET /api/collection/<collection_id>/follower

Used to get a list of the collection’s followers:

Returns: List of Authors response

Method: GET
Me
  • Login is required for all endpoints.
  • Me endpoints begin with /api/me
get

GET /api/me

Used to get the current user’s public and personal data:

Returns: Author Response

Method: GET
Content-Type: application/json
update

PUT /api/me

Used to update current user’s settings:

Returns: Author Response

Method: PUT
Content-Type: application/json
Body:
{
    "firstname": "Lindsay",
    "lastname": "Lohan",
    "email": "lindsay@lohan.com",
    "username": "FreakyFriday",
    "bio": "This is lindsay's bio.",
    "password": "freaky-friday"
}

NOTE: all parameters optional

photo

POST /api/me/photo`

Used to upload profile photos for the author:

Response: Author Response

Method: POST
Content-Type: multipart/form-data
Body: Files keyed by name, e.g.:
{
    'profile_banner_photo': ('mybanner.jpg', <data>, 'jpg'),
    'profile_photo': ('myphoto.png', <data>, 'png)
}
delete

DELETE /api/me

Used to delete the current user:

Returns: Author Response

Method: DELETE
Body: Empty
create poem

POST /api/me/poem

Used to create a new poem with the current user as the author:

Returns: Poem Response

Method: POST
Content-Type: application/json
Body: Empty
get all poems

GET /api/me/poem

Used to retrieve all poems by the current user, public and private:

Returns: List of Poems Response

Method: GET
get poem

GET /api/me/poem/<poem_id>

Used to get the specified poem:

Returns: Poem Response

Method: GET
update poem

PUT /api/me/poem/<poem_id>

Used to update the specified poem:

Returns: Poem Response

Method: POST
Content-Type: application/json
Body: {
    "tags": ['kittens', 'space', 'lusha'],
    "title": "Kittens in Space",
    "body": "A kitten in space<br>Lusha the astronaut"
    "is_published": true
}

NOTE: is_published is optional. Set to true to publish. Set to false to unpublish.

delete poem

DELETE /api/me/poem/<poem_id>

Delete the specified poem:

Returns: Poem Response

Method: DELETE
Body: Empty
get all collections

GET /api/me/collection

Used to get a list of all of the current user’s collections, public or private:

Returns: List of Collections Response

Method: GET
create new collection

POST /api/me/collection

Used to create a new collection for the current user:

Returns: Collection Response

Method: POST
Content-Type: application/json
Body: {
    "title": "New Collection",
    "tags": ["a", "cool", "collection"],
    "description": "this collection is cool.",
    "is_public": true
}

NOTE: All arguments optional.

update collection

PUT /api/me/collection/<collection_id>

Used to update an existing collection owned by the current user:

Returns: Collection

Method: PUT
Content-Type: multipart/form-data
Body: {
    image: <an image>
    title: "title"
    tags: ["some", "tags"]
    description: "a description"
    is_public: false
}
delete collection

DELETE /api/me/collection/<collection_id>

Used to delete an existing collection owned by the current user:

Returns: Collection Response

Method: DELETE
get poems in collection

GET /api/me/collection/<collection_id>/poem

Used to get all poems in the given collection owned by the current user:

Returns: List of poems Response

Method: GET
add poem to collection

PUT /api/me/collection/<collection_id>/poem/<poem_id>

Used to add a poem to the given collection:

Returns: Collection Response

Method: PUT
Content-Type: application/json
Body: {
    "is_hidden": true
}
remove poem from collection

DELETE /api/me/collections/<collection_id>/poem/<poem_id>

Used to delete a poem from the specified collection:

Returns: Collection Response

Method: DELETE
Poem

Pentameter poem endpoints are used to manage poem-centered activity.

get

GET /api/poem/<poem_id>

Returns a poem if it exists and is published:

Returns: Poem Response

Method: GET
Content-Type: application/json
Body: empty
increment metric

PUT /api/poem/<poem_id>/metric

Used to increment a metric for the given poem:

Returns: Poem Response

Method: PUT
Content-Type: application/json
Body: {
    "metric": "read"
}
like

PUT /api/poem/<poem_id>/like

Current user like the specified poem:

Returns: Poem Response

Method: PUT
Login: Required
unlike

DELETE /api/poem/<poem_id>/like

Current user like the specified poem:

Returns: Poem Response

Method: PUT
Login: Required
get likes

GET /api/poem/<poem_id>/like

Used to get a list of the authors who have liked this poem:

Returns: List of Authors Response

Method: GET

Frontend Reference

Information on setting up and developing the frontend React application which uses the API to display the interface on web and mobile.

Front End

Additional Notes

Legalese and general information

Changelog

The history of changes for the project for various releases or versions.

License

Pentameter license for use of code and intellectual property.