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:
- create new branch
- commit changes to branch & push them to origin
- rebase regularly onto
master
- force push to overwrite
origin/new-branch
- 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¶
- When your branch is ready to be reviewed by others you can create a pull request.
- Make sure you have latest
master
branch changes rebased into your working branch - Push your branch up to the remote with
git push origin new-branch
- Create a pull request via the Github interface wtih
master
as the base branch. - 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.
- Make sure your
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
are exported or prepended to the deploy command. - Run the following from the app directory
fab -A staging deploy
- SSH into
staging.pentameter.org
- Go to app directory
/home/ubuntu/pentameter
- 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 fromme/
endpoints."poems"
section will showis_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