[SOLVED] EECS485 P3: An Instagram clone

$25

File Name: EECS485_P3:_An_Instagram_clone.zip
File Size: 282.6 KB

SKU: [Solved] EECS485 P3: Client-side Dynamic Pages Category: Tag:
5/5 - (1 vote)

Introduction

An Instagram clone implemented with client-side dynamic pages. This is the third of an EECS 485 three project sequence: a static site generator from templates, server-side dynamic pages, and client-side dynamic pages.

Build an application using client-side dynamic pages and a REST API. Reuse server-side code from project 2, refactoring portions of it into a REST API. Write a client application in JavaScript that runs in the browser and makes AJAX calls to the REST API.

The learning goals of this project are client-side dynamic pages, JavaScript programming, asynchronous programming (AJAX), and REST APIs. Youll also gain more practice with the command line.

This spec will walk you through several parts:

  1. Setup
  2. REST API Specification
  3. Client-side Insta485 specification
  4. Testing
  5. Deploy to AWS
  6. Submitting and grading
  7. FAQ

Setup

Group registration

Register your group on the Autograder.

AWS account and instance

You will use Amazon Web Services (AWS) to deploy your project. AWS account setup may take up to 24 hours, so get started now. Create an account, launch and configure the instance. Dont deploy yet. AWS Tutorial.

Project folder

Create a folder for this project (instructions). Your folder location might be different.

WARNING: avoid file paths containing spaces in the project. Spaces can cause problems with the local tool installations.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

Version control

Set up version control using the Version control tutorial.

Be sure to check out the Version control for a team tutorial.

After youre done, you should have a local repository with a clean status and your local repository should be connected to a remote GitLab repository.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ git status

On branch master

Your branch is up-to-date with origin/master.

nothing to commit, working tree clean

$ git remote -v

origin https://gitlab.eecs.umich.edu/awdeorio/p3-insta485-clientside.git (fetch) origin https://gitlab.eecs.umich.edu/awdeorio/p3-insta485-clientside.git (push)

You should have a .gitignore file (instructions).

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ head .gitignore

This is a sample .gitignore file thats useful for EECS 485 projects.

Python virtual environment

Create a Python virtual environment using the Project 1 Python Virtual Environment Tutorial.

Check that you have a Python virtual environment, and that its activated (remember source env/bin/activate ).

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ ls -d env env

$ echo $VIRTUAL_ENV

/Users/awdeorio/src/eecs485/p3-insta485-clientside/env

Install utilities

Linux and Windows 10 Subsystem for Linux

$ sudo apt-get install sqlite3 curl httpie

MacOS

$ brew install sqlite3 curl httpie coreutils

Starter files

Download and unpack the starter files.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ wget https://eecs485staff.github.io/p3-insta485-clientside/starter_files.tar.gz $ tar -xvzf starter_files.tar.gz

Move the starter files to your project directory and remove the original starter_files/ directory and tarball.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ mv starter_files/* .

$ mv starter_files/.eslintrc.js .

$ rm -rf starter_files starter_files.tar.gz You should see these files.

$ tree matchdirs -I env|__pycache__

.

VERSION

bin

insta485test-html

package-lock.json

package.json

setup.py

sql

uploads

e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg tests

util.py

webpack.config.js

Heres a brief description of each of the starter files.

VERSION Version of the starter files
bin/insta485test-html Script to test HTML5 compliance
package-lock.json JavaScript packages with dependencies
package.json JavaScript packages
setup.py Insta485 python package configuration
sql/uploads/ Sample image uploads
tests/ Public unit tests
webpack.config.js JavaScript bundler config

Before making any changes to the clean starter files, its a good idea to make a commit to your Git repository.

Copy project 2 code

Youll reuse much of your code from project 2. Copy these files and directories from project 2 to project 3:

Scripts bin/insta485db bin/insta485run bin/insta485test

Server-side version of Insta485

insta485/

Database SQL files sql/schema.sql sql/data.sql Do not copy:

Virtual environment files env/

Python package files insta485.egg-info/ Your directory should now look like this:

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside $ tree matchdirs -I env|__pycache__

.

bin

insta485db

insta485run

insta485test

insta485test-html

insta485

__init__.py

api

__init__.py

config.py

model.py

static

css

style.css

images

logo.png

js

bundle.js templates

index.html

views

__init__.py

package-lock.json

package.json

setup.py

sql

data.sql

schema.sql uploads

e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg tests

util.py

webpack.config.js

Use pip to install the insta485 package.

$ pip install -e .

Run your project 2 code and make sure it still works by navigating to http://localhost:8000/.

$ ./bin/insta485db reset

$ ./bin/insta485run

Commit these changes and push to your Git repository.

REST API

The Flask REST API Tutorial will show you how to create a small REST API with Python/Flask.

Run the Flask development server.

$ ./bin/insta485run

Navigate to http://localhost:8000/api/v1/p/1/likes/. You should see this JSON response:

{

logname_likes_this: 1,

likes_count: 3,

postid: 1,

url: /api/v1/p/1/likes/

}

Commit these changes and push to your Git repository.

REST API tools

The REST API Tools Tutorial will show you how to use curl and HTTPie (the http command) to test a REST API from the command line.

You should now be able to log in and make a REST API call via the command line. Login using HTTPie to fill out the login form. This will save a file called session.json containing a cookie set by the server.

$ http

session=./session.json

form POST

http://localhost:8000/accounts/login/ username=awdeorio password=password submit=login HTTP/1.0 302 FOUND

$ http

session=./session.json

http://localhost:8000/api/v1/p/1/likes/ HTTP/1.0 200 OK

{

likes_count: 3,

logname_likes_this: 1,

postid: 1,

url: /api/v1/p/1/likes/

}

React/JS

The React/JS Tutorial will get you starter with a hello world React app and development toolchain.

After completing the tutorial, you have local JavaScript libraries and tools installed. Your versions may be different.

$ ls -d node_modules node_modules

$ echo $VIRTUAL_ENV

/Users/awdeorio/src/eecs485/p3-insta485-clientside/env

$ which npm

/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/npm

$ npm version

6.5.0

$ which node

/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/node

$ node version v11.9.0

More tools written in JavaScript were installed via npm .

$ ./node_modules/.bin/webpack version

3.6.0

$ ./node_modules/.bin/eslint version v4.8.0

You can check the style of your code using eslint .

$ ./node_modules/.bin/eslint ext jsx insta485/js/

Build the front end using webpack and then start a Flask development server.

$ ./node_modules/.bin/webpack

$ ./bin/insta485run

Browse to http://localhost:8000/ where you should see the test Likes React Component.

Commit these changes and push to your Git repository.

End-to-end testing

The End-to-end Testing Tutorial describes how to test a website implemented with client-side dynamic pages.

After completing the tutorial, you should have Google Chrome and Chrome Driver installed. The first part of version should match ( 77 in this example). While your versions should match, they might be different than this example.

$ google-chrome version # macOS

$ google-chrome-stable version # WSL/Linux

Google Chrome 77.0.3865.90

$ chromedriver version

ChromeDriver 77.0.3865.40

Run an end-to-end test provided with the starter files.

$ pytest -v tests/test_index.py::TestIndex::test_react_load

::TestIndex::test_react_load PASSED [100%]

=========================== 1 passed in 2.47 seconds ===========================

Install script

Installing the tool chain requires a lot of steps! Write a bash script bin/insta485install to install your app. Dont forget to check for shell script pitfalls.

Remember the shebang

#!/bin/bash

Stop on errors, print commands

set -Eeuo pipefail set -x

Create a Python virtual environment

python3 -m venv env

Activate Python virtual environment, avoiding some bad bash habits in the auto-generated activate script.

set +u

source env/bin/activate set -u

Install nodeenv

pip install nodeenv

Install JavaScript virtual environment

nodeenv python-virtualenv

Deactivate and reactivate the Python virtual environment

set +u deactivate

source env/bin/activate set -u

Install the latest Chromedriver. Automatically download either the macOS or Linux version.

mkdir -p ${VIRTUAL_ENV}/tmp pushd ${VIRTUAL_ENV}/tmp

CHROMEDRIVER_VERSION=`curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE` CHROMEDRIVER_ARCH=linux64

if [ `uname -s` = Darwin ]; then CHROMEDRIVER_ARCH=mac64 fi wget https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_ unzip chromedriver_${CHROMEDRIVER_ARCH}.zip mv chromedriver ${VIRTUAL_ENV}/bin/ popd

pip install -e .

Install front end

npm install .

Remember to add bin/insta485install to your Git repo and push.

WARNING Do not commit automatically generated or binary files to your Git repo! They can cause problems when running the code base on other computers, e.g., on AWS or a group members machine. These should all be in your .gitignore .

env/ insta485.egg-info node_modules __pycache__ bundle.js tmp cookies.txt session.json var

Fresh install

These instructions are useful for a group member installing the toolchain after checking out a fresh copy of the code.

Check out a fresh copy of the code and change directory.

$ git clone <your git URL here>

$ cd p3-insta485-clientside/

If you run into trouble with packages or dependencies, you can delete these automatically generated files.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ rm -rf env/ node_modules/ insta485.egg-info/ insta485/static/js/bundle.js Run the installer created during the setup tutorial.

$ ./bin/insta485install

Activate the newly created virtual environment.

$ source env/bin/activate

Thats it!

Database

Use the same database schema and starter data as in the Project 2 Database instructions.

After copying data.sql and schema.sql from project 2, your sql/ directory should look like this.

$ tree sql sql

data.sql

schema.sql uploads

e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg

Database management shell script

Reuse your same database management shell script ( insta485db ) from project 2. Your script should already support these subcommands:

$ insta485db create

$ insta485db destroy

$ insta485db reset

$ insta485db dump

Add the insta485db random subcommand, which will generate 100 posts in the database each with owner awdeorio and a random photo (selected from the starter photos).

Here is a bash snippet that adds 100 posts to the database each with owner awdeorio and a random photo. Note: you will not need to modify this bash snippet, but you will need to add the random subcommand to your bash script.

SHUF=shuf

# If shuf is not on this machine, try to use gshuf instead if ! type shuf 2> /dev/null; then SHUF=gshuf fi

DB_FILENAME=var/insta485.sqlite3

FILENAMES=122a7d27ca1d7420a1072f695d9290fad4501a41.jpg ad7790405c539894d25ab8dcf0b79eed3341e109.jpg 9887e06812ef434d291e4936417d125cd594b38a.jpg 2ec7cf8ae158b3b1f40065abfb33e81143707842.jpg for i in `seq 1 100`; do

# echo $FILENAMES print string

# shuf -n1 select one random line from multiline input

# awk {$1=$1;print} trim leading and trailing whitespace

# Use ${SHUF} instead of shuf

FILENAME=`echo $FILENAMES | ${SHUF} -n1 | awk {$1=$1;print}` OWNER=awdeorio

sqlite3 -echo -batch ${DB_FILENAME} INSERT INTO posts(filename, owner) VALUES(${FILENA done

MacOS: the insta485db random code above using the shuf (or gshuf ) command-line utility, which not installed by default. Install the coreutils package, which includes gshuf .

$ brew install coreutils

REST API Specification

This section describes the REST API implemented by the server. It implements the functionality needed to implement the main insta485 page. You might find this tutorial on REST APIs using Python/Flask helpful.

The following table describes each REST API method.

HTTP Method Example URL Action
GET /api/v1/ Return API resource URLs
GET /api/v1/p/ Return 10 newest posts
GET /api/v1/p/?size=N Return N newest posts
GET /api/v1/p/?page=N Return Nth page of posts
HTTP Method Example URL Action
GET /api/v1/p/<postid>/ Return post metadata: URL, username, etc.
GET /api/v1/p/<postid>/comments/ Return comments for one post
POST /api/v1/p/<postid>/comments/ Create comment
GET /api/v1/p/<postid>/likes/ Return number of likes
POST /api/v1/p/<postid>/likes/ Create like
DELETE /api/v1/p/<postid>/likes/ Delete like

Reminder: all the following examples require a logged in user. Heres how to log in and save session cookies using curl :

$ curl

request POST

cookie-jar cookies.txt

form username=awdeorio

form password=password

form submit=login

http://localhost:8000/accounts/login/

GET /api/v1/

Return a list of services available. The output should look exactly like this example. Note that curl -b is the same as curl cookie .

$ curl -b cookies.txt http://localhost:8000/api/v1/

{

posts: /api/v1/p/,

url: /api/v1/

}

GET /api/v1/p/

Return the 10 newest posts. The posts should meet the following criteria: each post is made by a user which the logged in user follows or the post is made by the logged in user. The URL of the next page of posts is returned in next . Note that postid is an int, not a string.

$ curl -b cookies.txt http://localhost:8000/api/v1/p/

{

next: ,

results: [

{

postid: 3,

url: /api/v1/p/3/

},

{

postid: 2,

url: /api/v1/p/2/

},

{

postid: 1,

url: /api/v1/p/1/

}

],

url: /api/v1/p/

}

Request a specific number of results with ?size=N .

$ curl -b cookies.txt http://localhost:8000/api/v1/p/?size=1

{

next: /api/v1/p/?size=1&page=1,

results: [

{

postid: 3,

url: /api/v1/p/3/

}

],

url: /api/v1/p/

}

Request a specific page of results with ?page=N .

$ curl -b cookies.txt http://localhost:8000/api/v1/p/?page=1

{

next: ,

results: [],

url: /api/v1/p/

}

Put size and page together.

$ curl -b cookies.txt http://localhost:8000/api/v1/p/?size=1&page=1

{

next: /api/v1/p/?size=1&page=2,

results: [

{

postid: 2,

url: /api/v1/p/2/

}

],

url: /api/v1/p/

}

Both size and page must be non-negative integers. Hint: let Flask coerce to the integer type in a query string like this: flask.request.args.get(size, default=<some number>, type=int) .

$ curl -b cookies.txt http://localhost:8000/api/v1/p/?page=-1

{

message: Bad Request,

status_code: 400

}

$ curl -b cookies.txt http://localhost:8000/api/v1/p/?size=-1

{

message: Bad Request,

status_code: 400

}

HINT: Use an SQL query with LIMIT and OFFSET , which you can compute from the page and size parameters. NOTE: age should not be returned as humanreadble from API.

Pro-tip: Returning the newest posts can be tricky due to the fact that all the posts are generated at nearly the same instant. If you tried to order by timestamp, this could potentially cause ties. Instead of using timestamp, use the fact that post ID is auto incremented in the order of creation to get the correct order.

GET /api/v1/p/<postid>/

Return the details for one post. Example:

$ curl -b cookies.txt http://localhost:8000/api/v1/p/3/

{

age: 2017-09-28 04:33:28,

img_url: /uploads/9887e06812ef434d291e4936417d125cd594b38a.jpg,

owner: awdeorio, owner_img_url: /uploads/e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg,

owner_show_url: /u/awdeorio/,

post_show_url: /p/3/,

url: /api/v1/p/3/

}

HINT: <postid> must be an integer. Let Flask enforce the integer type in a URL like this:

@insta485.app.route(/api/v1/p/<int:postid_url_slug>/, methods=[GET]) def get_post(postid_url_slug):

GET /api/v1/p/<postid>/comments/

Return a list of comments for one post. Example:

$ curl -b cookies.txt http://localhost:8000/api/v1/p/3/comments/

{

comments: [

{

commentid: 1,

owner: awdeorio, owner_show_url: /u/awdeorio/,

postid: 3, text: #chickensofinstagram

},

{

commentid: 2,

owner: jflinn, owner_show_url: /u/jflinn/,

postid: 3,

text: I <3 chickens

},

{

commentid: 3,

owner: michjc, owner_show_url: /u/michjc/,

postid: 3,

text: Cute overload!

}

],

url: /api/v1/p/3/comments/

}

POST /api/v1/p/<postid>/comments/

Add one comment to a post. Include the ID of the new comment in the return data. Return 201 on success.

HINT: sqlite3 provides a special function to retrieve the ID of the most recently inserted item:

SELECT last_insert_rowid() .

$ curl -ib cookies.txt

header Content-Type: application/json request POST

data {text:Comment sent from curl} http://localhost:8000/api/v1/p/3/comments/

HTTP/1.0 201 CREATED

Content-Type: application/json

Content-Length: 135

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:38:06 GMT

{

commentid: 8,

owner: awdeorio,

owner_show_url: /u/awdeorio/,

postid: 3,

text: Comment sent from curl

}

The new comment appears in the list now.

$ curl -ib cookies.txt http://localhost:8000/api/v1/p/3/comments/

HTTP/1.0 200 OK

Content-Type: application/json

Content-Length: 655

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:38:19 GMT

{

comments: [

{

commentid: 1,

owner: awdeorio,

owner_show_url: /u/awdeorio/,

postid: 3, text: #chickensofinstagram

},

{

commentid: 2,

owner: jflinn, owner_show_url: /u/jflinn/,

postid: 3,

text: I <3 chickens

},

{

commentid: 3,

owner: michjc, owner_show_url: /u/michjc/,

postid: 3,

text: Cute overload!

},

{

commentid: 8,

owner: awdeorio, owner_show_url: /u/awdeorio/,

postid: 3,

text: Comment sent from curl

}

],

url: /api/v1/p/3/comments/

}

GET /api/v1/p/<postid>/likes/

Return the number of likes for one post. Also include whether the logged in user like this ( 1 ) or not ( 0 ). Example:

$ curl -b cookies.txt http://localhost:8000/api/v1/p/3/likes/

{

logname_likes_this: 1,

likes_count: 1,

postid: 3,

url: /api/v1/p/3/likes/

}

DELETE /api/v1/p/<postid>/likes/

Delete one like. Return 204 on sucess.

$ curl -ib cookies.txt

header Content-Type: application/json

request DELETE data {}

http://localhost:8000/api/v1/p/3/likes/

HTTP/1.0 204 NO CONTENT

Content-Type: application/json

Content-Length: 0

Server: Werkzeug/0.12.2 Python/3.6.1 Date: Wed, 28 Jun 2017 17:48:03 GMT Its OK to delete a like twice.

$ curl -ib cookies.txt

header Content-Type: application/json

request DELETE data {}

http://localhost:8000/api/v1/p/3/likes/

HTTP/1.0 204 NO CONTENT

Content-Type: application/json

Content-Length: 0

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:50:28 GMT

$ curl -ib cookies.txt

header Content-Type: application/json

request DELETE data {}

http://localhost:8000/api/v1/p/3/likes/

HTTP/1.0 204 NO CONTENT

Content-Type: application/json

Content-Length: 0

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:50:28 GMT

POST /api/v1/p/<postid>/likes/

Create one like. Return 201 on success. Example:

$ curl -ib cookies.txt

header Content-Type: application/json

request POST data {}

http://localhost:8000/api/v1/p/3/likes/

HTTP/1.0 201 CREATED

Content-Type: application/json

Content-Length: 44

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:51:30 GMT

{

logname: awdeorio,

postid: 3 }

If the like already exists, return the same data with a 409 error and JSON description.

$ curl -ib cookies.txt

header Content-Type: application/json

request POST data {}

http://localhost:8000/api/v1/p/3/likes/

HTTP/1.0 409 CONFLICT

Content-Type: application/json

Content-Length: 44

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 17:57:59 GMT

{

logname: awdeorio,

message: Conflict,

postid: 3, status_code: 409

}

HTTP Response codes

The Flask documentation has a helpful section on implementing API exceptions. Errors returned by the REST API should take the form:

{

message: <describe the problem here>,

status_code: <int goes here>

}

All routes require a login. Return 403 if user is not logged in.

$ curl -i http://localhost:8000/api/v1/ # didnt send cookies

HTTP/1.0 403 FORBIDDEN

Content-Type: application/json

Content-Length: 52

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 20:12:26 GMT

{

message: Forbidden,

status_code: 403

}

Note that requests to user-facing pages should still return HTML. For example, if the user isnt logged in, the / redirects to /accounts/login/ .

$ curl -i http://localhost:8000/

HTTP/1.0 302 FOUND

Content-Type: text/html; charset=utf-8

Content-Length: 239

Location: http://localhost:8000/accounts/login/

Server: Werkzeug/0.12.2 Python/3.6.1

Date: Wed, 28 Jun 2017 20:13:04 GMT

<!DOCTYPE HTML PUBLIC -//W3C//DTD HTML 3.2 Final//EN>

<title>Redirecting</title>

<h1>Redirecting</h1>

<p>You should be redirected automatically to target URL: <a href=/accounts/login/>/accou

Post IDs that are out of range should return a 404 error.

$ curl -b cookies.txt http://localhost:8000/api/v1/p/1000/

{

message: Not Found,

status_code: 404

}

$ curl -b cookies.txt http://localhost:8000/api/v1/p/1000/comments/ {

message: Not Found,

status_code: 404

}

$ curl -b cookies.txt http://localhost:8000/api/v1/p/1000/likes/ {

message: Not Found,

status_code: 404

}

Checking output style

All returned JSON should conform to standard formatting. Whitespace doesnt matter. You can check it using jsonlint . Example:

$ curl -b cookies.txt http://localhost:8000/api/v1/p/ | jsonlint

% Total % Received % Xferd Average Speed Time Time Time Current

Dload Upload Total Spent Left Speed

100 222 100 222 0 0 24960 0 :: :: :: 27750

{

next: ,

results: [

{

postid: 3,

url: /api/v1/p/3/

},

{

postid: 2,

url: /api/v1/p/2/

},

{

postid: 1,

url: /api/v1/p/1/

}

],

url: /api/v1/p/ }

At this point you should be able to run the following public autograder tests on your code:

pytest -v tests/test_rest_api_query.py tests/test_rest_api_responses.py tests/test_rest_ap

============================================== 10 passed in 18.59 seconds ================

Client-side Insta485 Specification

This project includes the same pages as project 2. The only modified page is the index / . All other user-facing pages are identical to project 2. This section applies only to the main page.

The main page displays the same feed of posts as in project 2. In this project, posts will be rendered client-side by JavaScript code. All posts, including comments, likes, photo, data about the user who posted, etc. must be generated from JavaScript. Continue to use server-side rendering (AKA HTML templates) for the navigation bar at the top of the page.

Pro-tip: start with a React/JS mock-up and hard coded data. Gradually add features, like retrieving data from the REST API, one at a time. See the Thinking in React docs for a good example.

Pro-tip: Commit and push features to your git repo, one at a time. Follow the Version control for a team work flow.

HTML Modifications from Project 2

In order for the autograder to correctly navigate the insta485 site, youll need to add a few HTML tags to your HTML forms. This section only applies to the main page; all other pages and their HTML elements may be left as they were in project 2.

The comment form must contain the id attribute comment-form. You may use this HTML form code. Feel free to style it and add other HTML attributes.

<form id=comment-form>

<input type=text value=/>

</form>

The like button must contain the class name attribute like-unlike-button. You may use this HTML code. Feel free to style it and add other HTML attributes.

<button className=like-unlike-button>

FIXME-button-text-here

</button>

Main page HTML

The navigation bar should be rendered server-side, just like project 2. Include a link to / in the upper left hand corner. If not logged in, redirect to /accounts/login/ . If logged in, include a link to /explore/ and /u/<user_url_slug>/ in the upper right hand corner.

Heres an outline of the rendered HTML for the main page. Notice that there is no feed content. Rather, there is an entry point for JavaScript to add the feed content.

<body>

<! Plain old HTML and jinja2 nav bar goes here >

<div id=reactEntry>

Loading

</div>

<! Load JavaScript >

<script type=text/javascript src={{ url_for(static, filename=js/bundle.js) }}></

</body>

Note: Rendered html text should be in the appropriate tags.

// good render(){

<p>{this.state.some_bool ? Hello : Goodbye}</p>

}

// bad render(){

{this.state.some_bool ? Hello : Goodbye} }

Human readable timestamps

The API call GET /api/v1/p/<postid> returns the age of a post in the format Year-Month-Day Hour:Minutes:Seconds .

Your React code should convert this timestamp into human readable form e.g a few seconds ago . You should only use the moment.js library, which is already included for you in package.json, to achieve this. Using any other libraries, including react-moment, will cause you to fail tests the Autograder.

Response time

The main page should load without errors (exceptions), even when the REST API takes a long time to respond. Keep in mind that the render() method may be called asynchronously, and may be called multiple times.

Put another way, render() will likely be called by the React framework before any AJAX data arrives. The page should still render without errors.

Likes and comments update

Likes and comments added by the logged in user should appear immediately on the user interface without a page reload.

Comments are added by pressing the enter ( return ) key. The user interface shall not contain any comment submit button. The React docs on forms are very helpful for this feature.

Likes demo video.

Comments update demo video.

Infinite scroll

Scrolling to the bottom of the page causes additional posts to be loaded and displayed. Load and display the next 10 posts as specified by the next parameter of the most recent API call to /api/v1/p/ . Do not reload the page.

Infinite scroll demo video.

We recommend the React Infinite Scroll Component, which is already included in package.json .

Note: if infinite scroll has been triggered and more than 10 posts are present on the main page, reloading the page should only display the 10 most recent posts (including any new posts made before the reload).

Pro-tip to test this feature, you can use insta485db random .

(Note that in some visual demos, we use the same pictures multiple times, which might make you think that infinite scroll should at some point cycle back to the start. Thats NOT how it should work. Infinite scroll should keep scrolling until there are no more pictures available.)

Browser history

Dont break the back button. Heres an example:

  1. awdeorio loads / , which displays 10 posts.
  2. awdeorio scrolls, triggering the infinite scroll mechanism. Now the page contains 20 posts.
  3. jflinn adds a new post, which updates the database.
  4. awdeorio clicks on a post to view the post details at /p/<postid>/ .
  5. awdeorio clicks the back button on his browser, returning to / .
  6. The exact same 20 posts from step 2 are loaded. jflinns new post is not included.
  7. awdeorio refreshes the page. the 10 most recent posts are shown including jflinns new post.

As you might notice in the previous example, we do not specify whether the comments and / or likes on the 20 posts (step 6) are updated after returning to the index page using the back button. This is intentional. It is the students choice whether to update the comments and likes of each post after returning to the main page using the back button.

Heres an example of two accceptable scenarios depending on the students choice:

Scenario 1:

  1. awdeorio loads / , which displays 10 posts (post ids 1-10).
  2. awdeorio scrolls, triggering the infinite scroll mechanism. Now the page contains 20 posts (post ids 1-20).
  3. jflinn adds a new post, which updates the database.
  4. jflinn likes post id 20 (displayed to awdeorio in step 2), which updates the database.
  5. awdeorio clicks on a post to view the post details at /p/5/ .
  6. awdeorio clicks the back button on his browser, returning to / .
  7. The exact same 20 posts from step 2 are loaded. jflinns new post is not included but his like on post id 20 is shown.
  8. awdeorio refreshes the page. the 10 most recent posts are shown including jflinns new post.

Scenario 2:

  1. awdeorio loads / , which displays 10 posts (post ids 1-10).
  2. awdeorio scrolls, triggering the infinite scroll mechanism. Now the page contains 20 posts (post ids 1-20).
  3. jflinn adds a new post, which updates the database.
  4. jflinn likes post id 20 (displayed to awdeorio in step 2), which updates the database.
  5. awdeorio clicks on a post to view the post details at /p/5/ .
  6. awdeorio clicks the back button on his browser, returning to / .
  7. The exact same 20 posts from step 2 are loaded. jflinns new post is not included and his like on post id 20 is not shown.
  8. awdeorio refreshes the page. the 10 most recent posts are shown including jflinns new post.

The same example could be given to illustrate the students freedom by taking scenario 1 and 2 and replacing jflinns like in step 4 with a comment instead. In this case, the student could choose whether or not to display the comment in step 7.

The Mozilla documentation on the history API will be helpful.

Hint: this requires very few code modifications! Use the History API to manipulate browser history and use the PerformanceNavigationTiming API to check how the user is navigating to and from a page. Dont use other libraries for this feature (they only make it harder). Do not modify the URL.

REST API calls and logging

Were going to grade your REST API by inspecting the server logs. Well also be checking that your client-side javascript is making the correct API calls by inspecting the server logs. Loading the main page with the default database configuration, while logged in as awdeorio should yield the following logs. Note that the order of likes and comments doesnt matter.

127.0.0.1 [06/Jul/2017 12:02:09] GET / HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /static/js/bundle.js HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/3/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/3/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/3/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/2/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/2/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/2/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/1/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/1/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /api/v1/p/1/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:02:09] GET /uploads/e1a7c5c32973862ee15173b0259e3efdb6a391a

127.0.0.1 [06/Jul/2017 12:02:09] GET /uploads/9887e06812ef434d291e4936417d125cd594b38

127.0.0.1 [06/Jul/2017 12:02:09] GET /uploads/505083b8b56c97429a728b68f31b0b2a089e511

127.0.0.1 [06/Jul/2017 12:02:09] GET /uploads/ad7790405c539894d25ab8dcf0b79eed3341e10

127.0.0.1 [06/Jul/2017 12:02:09] GET /uploads/122a7d27ca1d7420a1072f695d9290fad4501a4

Press the like button a couple of times:

127.0.0.1 [06/Jul/2017 12:03:08] DELETE /api/v1/p/3/likes/ HTTP/1.1 204

127.0.0.1 [06/Jul/2017 12:03:09] POST /api/v1/p/3/likes/ HTTP/1.1 201 Add a comment:

127.0.0.1 [06/Jul/2017 12:03:27] POST /api/v1/p/3/comments/ HTTP/1.1 201

An example of infinite scroll. First, we load the main page from a database populated with 100 random posts.

127.0.0.1 [06/Jul/2017 12:04:59] GET / HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /static/js/bundle.js HTTP/1.1 200 127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/104/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/104/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/104/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/103/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/103/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/103/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/101/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/101/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/101/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/100/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/100/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/100/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/99/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/99/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/99/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/98/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/98/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/98/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/96/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/96/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/96/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/95/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/95/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/95/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/94/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/94/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/94/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/93/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/93/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /api/v1/p/93/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/505083b8b56c97429a728b68f31b0b2a089e511

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/e1a7c5c32973862ee15173b0259e3efdb6a391a

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/ad7790405c539894d25ab8dcf0b79eed3341e10

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/5ecde7677b83304132cb2871516ea50032ff7a4

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/122a7d27ca1d7420a1072f695d9290fad4501a4

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/2ec7cf8ae158b3b1f40065abfb33e8114370784

127.0.0.1 [06/Jul/2017 12:04:59] GET /uploads/9887e06812ef434d291e4936417d125cd594b38

Scroll to the bottom and infinite scroll is triggered.

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/?size=10&page=1 HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/91/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/91/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/91/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/90/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/90/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/90/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/89/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/89/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/89/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/87/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/87/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/87/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/85/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/85/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/85/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/84/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/84/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/84/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/83/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/83/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/83/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/81/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/81/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/81/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/80/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/80/comments/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/80/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/79/likes/ HTTP/1.1 200

127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/79/comments/ HTTP/1.1 200 127.0.0.1 [06/Jul/2017 12:05:18] GET /api/v1/p/79/ HTTP/1.1 200

Code style

As in project 2, all HTML should be W3C compliant, as reported by html5validator . Python code should be contain no errors or warnings from pycodestyle , pydocstyle , and pylint . Use pylint disable=cyclic-import .

All JSON returned by the REST API should be jsonlint clean.

All JavaScript source code should conform to the AirBnB javascript coding standard. Use eslint to test it. Refer to the setup / eslint tutorial.

You may only use JavaScript libraries that are contained in package.json from the starter files and the built-in Web APIs.

You must use the fetch API for AJAX calls.

Can I disable any code style checks?

Do not disable any code style check from any python code style tool ( pycodestyle , pydocstyle , pylint ), besides the three exceptions specified in the Project 2 spec

Additionally, do not disable any eslint checks in your .jsx files. There are no exceptions to this rule.

Testing

Make sure that youve completed the End-to-end testing tutorial.

Several unit tests are published with the starter files. Make sure youve copied the tests directory. Note that your files may be slightly different.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ ls tests/

73ab33bd357c3fd42292487b825880958c595655.jpg test_base_rest_api.py config.py test_index.py insta485_logging.py test_rest_api_responses.py insta485_logging_slow.py test_slow_server_index.py test_base_live.py util.py

Rebuild your javascript bundles by running webpack and then run the tests.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-clientside

$ ./node_modules/.bin/webpack

Hash: 94d02a55e475959ef08d

Version: webpack 2.6.1

Time: 4910ms

[. Truncated output ]

$ pytest -v

Note: if you get deprecation warnings from third party libraries, check out the pytest tutorial deprecation warnings to suppress them.

insta485test

Add JavaScript style checking you insta485test script from project 2. In addition to the tests run in project 2, insta485test should run eslint on all files within the insta485/js/ directory. Refer back to the eslint instructions for direction on how to do this.

$ ./bin/insta485test

Deploy to AWS

You should have already created an AWS account and instance (instructions). Resume the Project 2 AWS Tutorial Deploy a web app .

After you have deployed your site, download the main page along with a log. Do this from your local machine.

$ pwd

/Users/awdeorio/src/eecs485/p3-insta485-serverside

$ curl

request POST

cookie-jar cookies.txt

form username=awdeorio

form password=password

form submit=login

http://<Public DNS (IPv4)>/accounts/login/

$ curl -v -b cookies.txt http://<Public DNS (IPv4)>/ > deployed_index.html 2> deployed_i

$ curl -v -b cookies.txt http://<Public DNS (IPv4)>/static/js/bundle.js > deployed_bundl

Be sure to verify that the output in deployed_index.log and deployed_bundle_js.log doesnt include errors like Couldnt connect to server. If it does contain an error like this, it means curl couldnt successfully connect with your flask app.

Also be sure to verify that the output in deployed_index.html looks like the index.html file you coded while deployed_bundle_js.js contains Javascript code.

Submitting and grading

One team member should register your group on the autograder using the create new invitation feature.

Submit a tarball to the autograder, which is linked from https://eecs485.org. Include the disable-copyfile flag only on macOS.

$ tar

disable-copyfile

exclude *__pycache__* -czvf submit.tar.gz bin insta485 package-lock.json package.json setup.py sql webpack.config.js deployed_index.html deployed_index.log deployed_bundle_js.js deployed_bundle_js.log

The autograder will run pip install -e YOUR_SOLUTION and cd YOUR_SOLUTION && npm install . . The exact library versions in starter_files/setup.py and package.json are cached on the autograder, so be sure not to add extra library dependencies to either one.

FAQ

My JavaScript code doesnt work. What do I do?

  1. Make sure its eslint Instructions here.
  2. Make sure its free from exceptions by checking the developer console for exception messages
  3. Try the React Developer tools Chrome extension
  4. Check your assumptions about when React methods are called. This is called the React component lifecycle. Add log() messages to each React method ( constructor() , render() , etc.).

How do I use the fetch API?

Refer to the Fetch API documentation for a brief tutorial on how to use fetch.

Dont forget credentials: include ! Make sure you arent using host:port in your client fetch URL.

Can I use jQuery ?

Do not use jQuery for this project. Doing so is likely to cause issues with the Autograder.

The only external libraries that are needed for this project are already included for you in starter_files/package.json .

Can I use XMLHttpRequest ?

Do not use XMLHttpRequest to make HTTP requests. Instead use the fetch API detailed above How do I make a button toggle?

Toggle buttons are useful for the like/like button. See this example for more information.

Do trailing slashes in URLs matter to Flask?

Yes. Use them with the route decorator your REST API. See the Unique URLs / Redirection Behavior section in the Flask quickstart. Heres a good example:

@insta485.app.route(/u/<username_url_slug>/, methods=[GET, POST])

Can we use console.log() ?

Yes. Ideally you should only log in the case of an error.

eslint Error is missing in props validation

Youll probably encounter this error while running eslint :

$ eslint ext jsx insta485/js/

24:38 error url is missing in props validation react/prop-types

With prop-types , youll get a nice error in the console when a type property is violated at run time. For example,

Warning: Failed propType: The prop url is marked as required in CommentInput , but its value is undefined . Check the render method of Comments .

More on the prop-types library: https://www.npmjs.com/package/prop-types.

How do I append to an array of mutable state in a React Component?

Be really careful when both reading and writing this.state.MY_VARIABLE . Heres how.

this.setState(prevState => ({

posts: prevState.posts.concat(data.results), }));

Reconciliation and keys

When using a collection of React components, they need to have unique key properties. This enables the fast shadow DOM to real DOM update performed by react. More info here:

https://facebook.github.io/react/docs/reconciliation.html#keys https://facebook.github.io/react/docs/lists-and-keys.html#keys

How to fix pylint Similar lines in 2 files

The REST API shares some code in common with portions of insta485s static pages that havent been modified. For example, both the REST API and the static /u/<postid>/ read the comments and likes from the database. This could lead to pylint detecting copy paste errors.

************* Module insta485.views.user

R: 1, 0: Similar lines in 2 files

==insta485.api.comments:30

==insta485.views.post:42

A nice way to resolve this problem is by adding helper functions to your model . The canonical way to solve this problem is an Object Relational Model (ORM), but were simplifying in this project.

Reviews

There are no reviews yet.

Only logged in customers who have purchased this product may leave a review.

Shopping Cart
[SOLVED] EECS485 P3: An Instagram clone
$25