diff --git a/.gitignore b/.gitignore
index b8567d8..4ee08f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,9 @@
!/log/.keep
/tmp
/coverage
+
+# Ignore .env always
+.env
+
+# Ignore all VCR cassettes
+/spec/support/vcr_cassettes
diff --git a/Gemfile b/Gemfile
index f09efb6..c2c818f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,16 +3,18 @@ source 'https://rubygems.org'
gem 'dotenv-rails', :groups => [:development, :test]
# Auth
gem 'omniauth'
-
+gem 'omniauth-instagram'
+gem "omniauth-github"
# reading APIs
gem 'httparty'
-
+# Twitter API
+gem 'twitter'
+# Instagram API
+gem 'instagram'
# styling
gem 'bootstrap-sass'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.3'
-# Use sqlite3 as the database for Active Record
-gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
@@ -20,8 +22,6 @@ gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
-# gem 'therubyracer', platforms: :ruby
-
# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
@@ -30,7 +30,6 @@ gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc
-
# Use ActiveModel has_secure_password
gem 'bcrypt'
@@ -40,10 +39,9 @@ gem 'bcrypt'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
-# group :production do
-# gem "pg"
-# end
-
+group :test do
+ gem 'webmock'
+end
group :development, :test do
# troubleshooting
@@ -55,6 +53,7 @@ group :development, :test do
gem 'rspec-rails'
gem 'simplecov', require: false
gem 'factory_girl_rails', "~> 4.0"
+ gem 'vcr'
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
@@ -64,4 +63,13 @@ group :development, :test do
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
+
+ # Use sqlite3 as the database for Active Record
+ gem 'sqlite3'
+ # for inspecting route usage
+ # gem 'traceroute'
+end
+
+group :production do
+ gem 'pg'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 13f60b1..d393004 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -36,6 +36,7 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
+ addressable (2.3.8)
arel (6.0.2)
autoprefixer-rails (5.2.1.1)
execjs
@@ -50,6 +51,7 @@ GEM
bootstrap-sass (3.3.5.1)
autoprefixer-rails (>= 5.0.0.1)
sass (>= 3.3.0)
+ buftok (0.2.0)
builder (3.2.2)
byebug (5.0.0)
columnize (= 0.9.0)
@@ -62,6 +64,8 @@ GEM
execjs
coffee-script-source (1.9.1.1)
columnize (0.9.0)
+ crack (0.4.2)
+ safe_yaml (~> 1.0.0)
debug_inspector (0.0.2)
diff-lcs (1.2.5)
docile (1.1.5)
@@ -69,6 +73,7 @@ GEM
dotenv-rails (2.0.2)
dotenv (= 2.0.2)
railties (~> 4.0)
+ equalizer (0.0.11)
erubis (2.7.0)
execjs (2.5.2)
factory_girl (4.5.0)
@@ -76,13 +81,25 @@ GEM
factory_girl_rails (4.5.0)
factory_girl (~> 4.5.0)
railties (>= 3.0.0)
+ faraday (0.9.1)
+ multipart-post (>= 1.2, < 3)
+ faraday_middleware (0.9.2)
+ faraday (>= 0.7.4, < 0.10)
globalid (0.3.5)
activesupport (>= 4.1.0)
hashie (3.4.2)
+ http (0.6.4)
+ http_parser.rb (~> 0.6.0)
+ http_parser.rb (0.6.0)
httparty (0.13.5)
json (~> 1.8)
multi_xml (>= 0.5.2)
i18n (0.7.0)
+ instagram (1.1.5)
+ faraday (>= 0.7, < 0.10)
+ faraday_middleware (>= 0.8, < 0.10)
+ hashie (>= 0.4.0)
+ multi_json (~> 1.0, >= 1.0.3)
jbuilder (2.3.1)
activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2)
@@ -91,21 +108,42 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.3)
+ jwt (1.5.1)
loofah (2.0.2)
nokogiri (>= 1.5.9)
mail (2.6.3)
mime-types (>= 1.16, < 3)
+ memoizable (0.4.2)
+ thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
mime-types (2.6.1)
mini_portile (0.6.2)
minitest (5.7.0)
multi_json (1.11.2)
multi_xml (0.5.5)
+ multipart-post (2.0.0)
+ naught (1.0.0)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
+ oauth2 (1.0.0)
+ faraday (>= 0.8, < 0.10)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (~> 1.2)
omniauth (1.2.2)
hashie (>= 1.2, < 4)
rack (~> 1.0)
+ omniauth-github (1.1.2)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-instagram (1.0.1)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.0)
+ omniauth-oauth2 (1.3.1)
+ oauth2 (~> 1.0)
+ omniauth (~> 1.2)
+ pg (0.18.2)
pry (0.10.1)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -158,6 +196,7 @@ GEM
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
+ safe_yaml (1.0.4)
sass (3.4.16)
sass-rails (5.0.3)
railties (>= 4.0.0, < 5.0)
@@ -168,6 +207,7 @@ GEM
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
+ simple_oauth (0.3.1)
simplecov (0.10.0)
docile (~> 1.1.0)
json (~> 1.8)
@@ -187,16 +227,31 @@ GEM
tilt (1.4.1)
turbolinks (2.5.3)
coffee-rails
+ twitter (5.14.0)
+ addressable (~> 2.3)
+ buftok (~> 0.2.0)
+ equalizer (~> 0.0.9)
+ faraday (~> 0.9.0)
+ http (~> 0.6.0)
+ http_parser.rb (~> 0.6.0)
+ json (~> 1.8)
+ memoizable (~> 0.4.0)
+ naught (~> 1.0)
+ simple_oauth (~> 0.3.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.7.1)
execjs (>= 0.3.0)
json (>= 1.8.0)
+ vcr (2.9.3)
web-console (2.2.1)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
+ webmock (1.21.0)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
PLATFORMS
ruby
@@ -211,9 +266,13 @@ DEPENDENCIES
dotenv-rails
factory_girl_rails (~> 4.0)
httparty
+ instagram
jbuilder (~> 2.0)
jquery-rails
omniauth
+ omniauth-github
+ omniauth-instagram
+ pg
pry-rails
rails (= 4.2.3)
rspec-rails
@@ -223,8 +282,11 @@ DEPENDENCIES
spring
sqlite3
turbolinks
+ twitter
uglifier (>= 1.3.0)
+ vcr
web-console (~> 2.0)
+ webmock
BUNDLED WITH
1.10.6
diff --git a/README.md b/README.md
index 1aed32d..bcb2406 100644
--- a/README.md
+++ b/README.md
@@ -62,25 +62,25 @@ You and your team will build a feed aggregator service that allows users to logi
### User Stories
# As a logged out user
-- I can sign up
+x I can sign up
- with OAuth developer
- - with instagram
-- I can log in
+ x with instagram
+x I can log in
- API CALL (OAuth)
# As a logged in user
-- I can sign out
+x I can sign out
-- I can see a chronological feed of posts from users you choose to follow
- - Instagram
- - Twitter
+x I can see a chronological feed of posts from users you choose to follow
+ x Instagram
+ x Twitter
-- I can search Instagram users
- - and see list of results
- - and follow a user
-- I can search Twitter users
- - and see list of results
- - and follow a user
+x I can search Instagram users
+ x and see list of results
+ x and follow a user
+x I can search Twitter users
+ x and see list of results
+ x and follow a user
### Notes on structure
@@ -105,8 +105,8 @@ My Subscriptions
Added Fun (Nice to Have)
- live stream updating
-- be able to unsubscribe
- - from that point on, no new posts
+x be able to unsubscribe
+ x from that point on, no new posts
- Feed
- be able to select which source to see
- pagination?
diff --git a/app/assets/.DS_Store b/app/assets/.DS_Store
new file mode 100644
index 0000000..09a387f
Binary files /dev/null and b/app/assets/.DS_Store differ
diff --git a/app/assets/images/.DS_Store b/app/assets/images/.DS_Store
new file mode 100644
index 0000000..5008ddf
Binary files /dev/null and b/app/assets/images/.DS_Store differ
diff --git a/app/assets/images/Coffee.jpg b/app/assets/images/Coffee.jpg
new file mode 100644
index 0000000..08efc25
Binary files /dev/null and b/app/assets/images/Coffee.jpg differ
diff --git a/app/assets/images/Newspaper.jpg b/app/assets/images/Newspaper.jpg
new file mode 100644
index 0000000..2f0e803
Binary files /dev/null and b/app/assets/images/Newspaper.jpg differ
diff --git a/app/assets/images/arnold.jpg b/app/assets/images/arnold.jpg
new file mode 100644
index 0000000..75d78c2
Binary files /dev/null and b/app/assets/images/arnold.jpg differ
diff --git a/app/assets/images/coffee_table.jpg b/app/assets/images/coffee_table.jpg
new file mode 100644
index 0000000..d839e6f
Binary files /dev/null and b/app/assets/images/coffee_table.jpg differ
diff --git a/app/assets/images/gosling.jpg b/app/assets/images/gosling.jpg
new file mode 100644
index 0000000..6f924da
Binary files /dev/null and b/app/assets/images/gosling.jpg differ
diff --git a/app/assets/images/insta.png b/app/assets/images/insta.png
new file mode 100644
index 0000000..edc63f5
Binary files /dev/null and b/app/assets/images/insta.png differ
diff --git a/app/assets/images/tweet.png b/app/assets/images/tweet.png
new file mode 100644
index 0000000..816fdb6
Binary files /dev/null and b/app/assets/images/tweet.png differ
diff --git a/app/assets/images/will-i-am.jpg b/app/assets/images/will-i-am.jpg
new file mode 100644
index 0000000..ebb5ca5
Binary files /dev/null and b/app/assets/images/will-i-am.jpg differ
diff --git a/app/assets/javascripts/followees.coffee b/app/assets/javascripts/followees.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/followees.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/home.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/posts.coffee b/app/assets/javascripts/posts.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/posts.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/subscriptions.coffee b/app/assets/javascripts/subscriptions.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/subscriptions.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 435c578..dd87aa5 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -16,3 +16,73 @@
@import "bootstrap-sprockets";
@import "bootstrap";
+
+.panel-change {
+ opacity: 0.8;
+}
+
+.header {
+ margin: 5px;
+ color: black;
+}
+
+.header-img {
+ background: image-url("Newspaper.jpg") repeat;
+ background-size: contain;
+ opacity: 0.8;
+ padding: 10px;
+}
+
+.button {
+ margin: 10px;
+}
+
+.center {
+ text-align: center;
+}
+
+.center-posts {
+ text-align: center;
+ padding-left: auto;
+ padding-right: auto;
+}
+
+.news-text, h1 {
+ font-family: Copperplate, "Copperplate Gothic Light", fantasy;
+ font-weight: bold;
+}
+
+.search-results,
+.twitter-results {
+ list-style-type: none;
+}
+
+.link-to-top {
+ color: white;
+ text-align: center;
+}
+
+.twitter-results img {
+ list-style-type: none;
+ border-radius: 10px;
+ border: 2px solid #3366CC;
+ padding: 2px;
+ width: 60px;
+ height: 60px;
+ margin-bottom: 5px;
+}
+
+.bkgd {
+ background: image-url("Coffee.jpg");
+ background-size: 100%;
+}
+
+body {
+ background: image-url("coffee_table.jpg");
+ background-size: 100%;
+}
+
+.search-results form,
+.twitter-results form {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/followees.scss b/app/assets/stylesheets/followees.scss
new file mode 100644
index 0000000..707b601
--- /dev/null
+++ b/app/assets/stylesheets/followees.scss
@@ -0,0 +1,8 @@
+// Place all the styles related to the followees controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+
+#flash {
+ color: red;
+ text-align: center;
+}
diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss
new file mode 100644
index 0000000..79adafc
--- /dev/null
+++ b/app/assets/stylesheets/home.scss
@@ -0,0 +1,29 @@
+// Place all the styles related to the home controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+.sign-in-box {
+ padding-bottom: 20px;
+ padding-top: 10px;
+ margin-top: 250px;
+ margin-left: 150px;
+ height: 200px;
+ width: 500px;
+ background-color: white;
+ display: block;
+ text-align: center;
+ opacity: 0.9;
+ border-radius: 10px
+}
+
+.sign-in {
+ width: 100%;
+ text-align: center;
+ border-bottom: 0.5px solid #000;
+ line-height: 0.1em;
+ margin: 10px 0 20px;
+}
+
+.sign-line {
+ background:#fff;
+ padding:0 10px;
+}
diff --git a/app/assets/stylesheets/posts.scss b/app/assets/stylesheets/posts.scss
new file mode 100644
index 0000000..1a7e153
--- /dev/null
+++ b/app/assets/stylesheets/posts.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the posts controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/subscriptions.scss b/app/assets/stylesheets/subscriptions.scss
new file mode 100644
index 0000000..bbed55d
--- /dev/null
+++ b/app/assets/stylesheets/subscriptions.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the subscriptions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d83690e..0edb46f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,5 +1,40 @@
+# include these gems to work with social media APIs
+require 'twitter'
+require 'instagram'
+
+# include the below classes to keep api logic modular
+require 'twitter_api'
+require 'instagram_api'
+require 'instagram_mapper'
+require 'twitter_mapper'
+require 'api_helper'
+
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+
+ before_action :current_user
+ before_action :require_signin
+
+ TWITTER = "twitter"
+ INSTAGRAM = "instagram"
+
+ private
+
+ Instagram.configure do |config|
+ config.client_id = ENV["INSTAGRAM_CLIENT_ID"]
+ config.client_secret = ENV["INSTAGRAM_CLIENT_SECRET"]
+ end
+
+ def current_user
+ @current_user ||= User.find_by(id: session[:user_id])
+ end
+
+ def require_signin
+ unless @current_user
+ # flash[:errors] = MESSAGES[:not_signed_in]
+ redirect_to signin_path
+ end
+ end
end
diff --git a/app/controllers/followees_controller.rb b/app/controllers/followees_controller.rb
new file mode 100644
index 0000000..838bd4a
--- /dev/null
+++ b/app/controllers/followees_controller.rb
@@ -0,0 +1,44 @@
+class FolloweesController < ApplicationController
+ USER_COUNT = 3
+
+ include ActionView::Helpers::OutputSafetyHelper
+
+ def search; end # this renders the search page
+
+ def instagram_users_redirect
+ if params[:user].present?
+ redirect_to search_results_path(params[:source], params[:user])
+ else
+ flash[:errors] = "Please enter a username to search."
+ redirect_to search_path
+ end
+ end
+
+ def twitter_users_redirect
+ if params[:user].present?
+ redirect_to search_results_path(params[:source], params[:user])
+ else
+ flash[:errors] = "Please enter a username to search."
+ redirect_to search_path
+ end
+ end
+
+ # this displays results on the search page
+ def search_results
+ @query = params[:user]
+ processed_query = sanitize(@query)
+ @source = params[:source]
+ @results = ApiHelper.user_search(processed_query, USER_COUNT, @source)
+ if @results.count == 0
+ flash[:notice] = "No users matching '#{@query}'."
+ end
+ render 'search'
+ end
+
+ private
+
+ def sanitize(input)
+ input.delete!("#")
+ input.gsub(" ", "%20")
+ end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
new file mode 100644
index 0000000..300106c
--- /dev/null
+++ b/app/controllers/home_controller.rb
@@ -0,0 +1,33 @@
+class HomeController < ApplicationController
+ skip_before_action :require_signin, only: [:signin, :fire]
+
+ include ActionView::Helpers::OutputSafetyHelper
+
+ def signin; end
+
+ def newsfeed
+ subscriptions = @current_user.subscriptions.active
+ ApiHelper.process_new_posts(subscriptions)
+
+ if subscriptions.count == 0
+ flash[:errors] = "You have no subscriptions! Search users to subscribe to."
+ else
+ @show_posts = []
+ subscriptions.each do |s|
+ posts_per_sub = []
+ all_sub_posts = s.posts
+ sub_posts = s.rev_posts
+ if sub_posts.count == 0
+ posts_per_sub << all_sub_posts.last
+ end
+ @show_posts << posts_per_sub
+ end
+ end
+ if @show_posts
+ @show_posts.flatten!
+ @show_posts.sort_by! { |p| p.native_created_at }
+ @show_posts.reverse!
+ end
+ end
+
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index ea87b3d..56e34ce 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,15 +1,17 @@
class SessionsController < ApplicationController
- skip_before_filter :verify_authenticity_token, :only => :create
+ skip_before_action :verify_authenticity_token, only: [:create]
+ skip_before_action :require_signin
def create
- @user = User.find_or_create_from_auth_hash(auth_hash)
+ @user = User.find_or_create_from_omniauth(auth_hash)
session[:user_id] = @user.id
- # raise
- redirect_to '/'
+
+ redirect_to root_path
end
- def home
- @current_user = User.find(session[:user_id])
+ def destroy
+ reset_session
+ redirect_to signin_path
end
private
diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
new file mode 100644
index 0000000..9fdc5a5
--- /dev/null
+++ b/app/controllers/subscriptions_controller.rb
@@ -0,0 +1,46 @@
+class SubscriptionsController < ApplicationController
+ def create
+ followee = Followee.find_or_create_by(followee_params)
+ Subscription.find_or_create_subscription(@current_user, followee)
+
+ redirect_to :back, notice: "You have successfully subscribed to '@#{followee.handle}'!"
+
+ rescue
+ redirect_to root_path
+ end
+
+ def index
+ active_subscriptions = @current_user.subscriptions.active
+ @followees = find_followees(active_subscriptions)
+ end
+
+ def find_followees(subscriptions)
+ @followees = []
+ subscriptions.map { |subscription| subscription.followee }
+ end
+
+ def unsubscribe
+ # adds current time to unsubscribe_date
+ subscription = Subscription.find(params[:id])
+ subscription.update!(unsubscribe_date: Time.now)
+ redirect_to :back, notice: "You have successfully unsubscribed from @#{subscription.followee.handle}."
+
+ rescue
+ redirect_to root_path
+ end
+
+ private
+
+ def sub_params
+ params.require(:subscriptions).permit(:id, :user_id, :followee_id)
+ end
+
+ def followee_params
+ { source: params[:source],
+ username: params[:username],
+ id: params[:id],
+ picture: params[:profile_picture],
+ avatar_url: params[:avatar_url]
+ }
+ end
+end
diff --git a/app/helpers/followees_helper.rb b/app/helpers/followees_helper.rb
new file mode 100644
index 0000000..a6d878a
--- /dev/null
+++ b/app/helpers/followees_helper.rb
@@ -0,0 +1,24 @@
+module FolloweesHelper
+ def private_user?(user_id)
+ InstagramApi.new.private_user?(user_id)
+ end
+
+ def currently_following?(native_id)
+ followee = Followee.find_by(native_id: native_id)
+
+ if followee
+ @current_user.subscriptions.active_for_this_followee(followee.id).empty? ? false : true
+ else
+ return false
+ end
+ end
+
+ def find_subscription_id(native_id)
+ followee = find_followee(native_id)
+ subscription_id_to_unsubscribe = @current_user.subscriptions.active_for_this_followee(followee.id).first
+ end
+
+ def find_followee(native_id)
+ Followee.find_by(native_id: native_id)
+ end
+end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
new file mode 100644
index 0000000..23de56a
--- /dev/null
+++ b/app/helpers/home_helper.rb
@@ -0,0 +1,2 @@
+module HomeHelper
+end
diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb
new file mode 100644
index 0000000..a7b8cec
--- /dev/null
+++ b/app/helpers/posts_helper.rb
@@ -0,0 +1,2 @@
+module PostsHelper
+end
diff --git a/app/helpers/subscriptions_helper.rb b/app/helpers/subscriptions_helper.rb
new file mode 100644
index 0000000..9ace045
--- /dev/null
+++ b/app/helpers/subscriptions_helper.rb
@@ -0,0 +1,2 @@
+module SubscriptionsHelper
+end
diff --git a/app/models/followee.rb b/app/models/followee.rb
new file mode 100644
index 0000000..37b8b9b
--- /dev/null
+++ b/app/models/followee.rb
@@ -0,0 +1,25 @@
+class Followee < ActiveRecord::Base
+# ASSOCIATIONS ----------------------------------
+ has_many :posts
+ has_many :subscriptions
+ has_many :users, through: :subscriptions
+
+# VALIDATIONS -----------------------------------
+ validates :handle, :source, :native_id, presence: true
+
+# SCOPES ----------------------------------------
+
+ def self.find_or_create_by(followee_hash)
+ followee = self.find_or_initialize_by(native_id: followee_hash[:id])
+ followee.source = followee_hash[:source]
+ followee.handle = followee_hash[:username]
+ followee.avatar_url = followee_hash[:picture] || followee_hash[:avatar_url]
+
+ followee.save ? followee : false
+ end
+
+ def self.update_last_post_id!(posts, followee, source)
+ new_last_post_id = (source == ApplicationController::TWITTER) ? posts.first.id : posts.first["id"]
+ followee.update!(last_post_id: new_last_post_id)
+ end
+end
diff --git a/app/models/post.rb b/app/models/post.rb
new file mode 100644
index 0000000..038c838
--- /dev/null
+++ b/app/models/post.rb
@@ -0,0 +1,21 @@
+class Post < ActiveRecord::Base
+ # Validations ----------------------------------------------------------------
+ validates :followee_id, :source, :native_created_at, :native_id, presence: true
+
+ # Associations ---------------------------------------------------------------
+ belongs_to :followee
+
+ def self.create_posts_and_update_followee(posts, followee, source)
+ if posts && posts.count > 0
+ create_from_API(posts, followee, source)
+ Followee.update_last_post_id!(posts, followee, source)
+ end
+ end
+
+ def self.create_from_API(posts, followee, source)
+ posts.each do |post|
+ post_hash = ApiHelper.post_params(post, followee, source)
+ create(post_hash)
+ end
+ end
+end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
new file mode 100644
index 0000000..e2d407e
--- /dev/null
+++ b/app/models/subscription.rb
@@ -0,0 +1,33 @@
+class Subscription < ActiveRecord::Base
+ # Associations ------------------------
+ belongs_to :user
+ belongs_to :followee
+ has_many :posts, through: :followee
+
+ # Validations -------------------------
+ validates :user_id, :followee_id, presence: true
+
+ # Scopes ------------------------------
+ scope :active, -> { where(unsubscribe_date: nil) }
+ scope :active_for_this_followee, -> (followee_id) { where( { followee_id: followee_id, unsubscribe_date: nil}) }
+
+ def self.find_or_create_subscription(current_user, followee)
+ subscription = self.find_or_initialize_by(
+ user_id: current_user.id,
+ followee_id: followee.id
+ )
+
+ if !subscription.unsubscribe_date.nil?
+ # if user previously subscribed to followee
+ subscription = Subscription.create!(user_id: current_user.id, followee_id: followee.id)
+ end
+ subscription.save
+
+ return subscription
+ end
+
+ def rev_posts
+ posts.where("native_created_at >= ?", self.created_at)
+ end
+
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 90710a8..1f169d7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,13 +1,27 @@
class User < ActiveRecord::Base
-
- def self.find_or_create_from_auth_hash(auth_hash)
+ # Associations ----------------------------------------------------
+ has_many :subscriptions
+ has_many :followees, through: :subscriptions
+
+ # Validations ------------------------------------------------------
+ validates :uid, :provider, presence: true
+
+ # OmniAuth ---------------------------------------------------------
+ def self.find_or_create_from_omniauth(auth_hash)
uid = auth_hash["uid"]
provider = auth_hash["provider"]
user = User.where(uid: uid, provider: provider).first_or_initialize
user.email = auth_hash["info"]["email"]
- user.name = auth_hash["info"]["name"]
+
+ if provider == "developer"
+ user.name = auth_hash["info"]["name"]
+ else
+ user.name = auth_hash["info"]["nickname"]
+ end
return user.save ? user : nil
end
+
+ # Scopes ----------------------------------------------------------
end
diff --git a/app/views/followees/search.html.erb b/app/views/followees/search.html.erb
new file mode 100644
index 0000000..3651e04
--- /dev/null
+++ b/app/views/followees/search.html.erb
@@ -0,0 +1,91 @@
+
+ Search
+ <% unless flash[:notice] %>
+ <%= render partial: "shared/errors" %>
+ <% end %>
+ Search Twitter Users:
+ <%= form_tag twitter_users_redirect_path, options: {class: "form-inline", role:"form"} do %>
+
+ <%= label_tag :user %>
+ <%= text_field_tag :user %>
+ <%= hidden_field_tag :source, "twitter" %>
+
+ <%= submit_tag 'Look up Twitter Users', class: "btn btn-default" %>
+
+ <% end %>
+
+ Search Instagram Users:
+ <%= form_tag instagram_users_redirect_path, options: {class: "form-inline", role: "form"} do %>
+
+ <%= label_tag :user %>
+ <%= text_field_tag :user %>
+ <%= hidden_field_tag :source, "instagram" %>
+
+ <%= submit_tag "Look up Instagram Users", class: "btn btn-default" %>
+
+ <% end %>
+
+ <% if action_name == "search_results" %>
+
+
+
<%= @source.capitalize %> results for "<%= @query %>"
+
+
+ <%= render partial: "shared/errors" %>
+
+ <% if params[:source] == "instagram" %>
+
+ <% @results.each do |user| %>
+ -
+ <%= image_tag(user["profile_picture"], class: "img-thumbnail", size: "50x50") %>
+ @<%= user["username"] %>
+
+
+ <% if private_user?(user["id"]) %>
+ <%= link_to "Private User", root_path, class: "btn btn-default disabled"%>
+
+
+ <% elsif currently_following?(user["id"]) %>
+ <%= link_to "Unsubscribe", unsubscribe_path(find_subscription_id(user["id"])), method: :put, class: "btn btn-danger" %>
+
+
+ <% else %>
+ <%= form_tag subscriptions_path(user) do %>
+ <%= hidden_field_tag :source, "instagram" %>
+ <%= submit_tag "Subscribe", class: "btn btn-success btn-sm" %>
+ <% end %>
+ <% end %>
+
+ <% end %>
+
+
+ <% elsif params[:source] == "twitter" %>
+
+ <% end %>
+
+ <%= link_to "Back to top", "#top", class: "link-to-top" %>
+ <% end %>
+
+
diff --git a/app/views/home/fire.html.erb b/app/views/home/fire.html.erb
new file mode 100644
index 0000000..b90e963
--- /dev/null
+++ b/app/views/home/fire.html.erb
@@ -0,0 +1,5 @@
+
Nope.
+
+via GIPHY
+
+<%= link_to 'Back to FeedMe sign-in', root_path, class: "btn btn-primary" %>
diff --git a/app/views/home/newsfeed.html.erb b/app/views/home/newsfeed.html.erb
new file mode 100644
index 0000000..07d5ed8
--- /dev/null
+++ b/app/views/home/newsfeed.html.erb
@@ -0,0 +1,13 @@
+NewsFeed
+
+<%= render partial: "shared/errors" %>
+<%= link_to 'Search Users', search_path, class: "btn btn-default" %>
+<%= link_to 'Update Feed', root_path, method: :get, class: "btn btn-success" %>
+
+<% if @show_posts %>
+ <%= render partial: "shared/update" %>
+<% end %>
+
+
+
+<%= link_to "Back to top", "#top", class: "link-to-top" %>
diff --git a/app/views/home/signin.html.erb b/app/views/home/signin.html.erb
new file mode 100644
index 0000000..70eb3b1
--- /dev/null
+++ b/app/views/home/signin.html.erb
@@ -0,0 +1,14 @@
+
+
+
Sign in to FeedMe
+
+ <% unless Rails.env.production? %>
+ <%= link_to 'Sign in (dev strategy)', '/auth/developer', class: "btn btn-default" %>
+
+ OR
+ <% end %>
+
+ <%= link_to 'Sign in w/ Instagram', '/auth/instagram', class: "btn btn-primary" %>
+ <%= link_to 'Sign in w/ GitHub', '/auth/github', class: "btn btn-primary" %>
+
+
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
new file mode 100644
index 0000000..5a06ff0
--- /dev/null
+++ b/app/views/layouts/_header.html.erb
@@ -0,0 +1,21 @@
+<% unless current_page?("/signin") %>
+
+<% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index b323c2a..61eb1f1 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,14 +1,33 @@
- C3ProjectsSeemore
+ FeedMe!
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
-
-
-<%= yield %>
+
+
+
+
+
+ <%= render 'layouts/header' %>
+ <%= yield %>
+
diff --git a/app/views/sessions/home.html.erb b/app/views/sessions/home.html.erb
deleted file mode 100644
index aab4758..0000000
--- a/app/views/sessions/home.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-K Bye
-
-Logged in user is: <%= @current_user.name %>
diff --git a/app/views/shared/_errors.html.erb b/app/views/shared/_errors.html.erb
new file mode 100644
index 0000000..21742e2
--- /dev/null
+++ b/app/views/shared/_errors.html.erb
@@ -0,0 +1,6 @@
+<% if !flash.empty? %>
+
+ <%= flash[:errors] %>
+ <%= flash[:notice] %>
+
+<% end %>
diff --git a/app/views/shared/_update.html.erb b/app/views/shared/_update.html.erb
new file mode 100644
index 0000000..9760b4a
--- /dev/null
+++ b/app/views/shared/_update.html.erb
@@ -0,0 +1,5 @@
+
+<% @show_posts.each do |p| %>
+
+ <%= raw p.embed_html %>
+ <% end %>
diff --git a/app/views/subscriptions/index.html.erb b/app/views/subscriptions/index.html.erb
new file mode 100644
index 0000000..70cfa6e
--- /dev/null
+++ b/app/views/subscriptions/index.html.erb
@@ -0,0 +1,31 @@
+
+
+
<%= @current_user.name.capitalize %>'s Subscriptions
+
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 7219a5e..bb18b99 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -1,5 +1,11 @@
Rails.application.config.middleware.use OmniAuth::Builder do
- provider :developer,
- fields: [:name, :email],
- uid_field: :email
+ if !Rails.env.production?
+ provider :developer,
+ fields: [:name, :email],
+ uid_field: :email
+ end
+
+ provider :instagram, ENV['INSTAGRAM_ID'], ENV['INSTAGRAM_SECRET']
+ provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
+
end
diff --git a/config/routes.rb b/config/routes.rb
index fe0c40f..69e40e8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,29 @@
Rails.application.routes.draw do
- root 'sessions#home'
- match "auth/:provider/callback", to: "sessions#create", via: [:get, :post]
+ # Newsfeed
+ root 'home#newsfeed'
+
+ # Signing in and out
+ get '/signin', to: 'home#signin', as: 'signin'
+ delete 'signout', to: 'sessions#destroy', as: 'signout'
+
+ # OmniAuth callback
+ match '/auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
+
+ if Rails.env.production?
+ get '/auth/developer', to: 'home#fire'
+ end
+
+ # Search page
+ get '/search', to: 'followees#search', as: 'search'
+
+ # Redirect (checks if search field is filled in)
+ post '/instagram_users', to: 'followees#instagram_users_redirect', as: 'instagram_users_redirect'
+ post '/twitter_users', to: 'followees#twitter_users_redirect', as: 'twitter_users_redirect'
+
+ # Search Results
+ get '/search/:source/:user', to: 'followees#search_results', as: 'search_results'
+
+ # Subscriptions
+ resources :subscriptions, only: [:create, :index]
+ put '/unsubscribe/:id', to: 'subscriptions#unsubscribe', as: 'unsubscribe'
end
diff --git a/db/followees.csv b/db/followees.csv
new file mode 100644
index 0000000..e0606a5
--- /dev/null
+++ b/db/followees.csv
@@ -0,0 +1,7 @@
+handle,source,avatar_url,native_id,last_post_id
+beyonce,twitter,http://pbs.twimg.com/profile_images/582395896593170432/pw_jGziR_normal.jpg,31239408,369486010280706048
+rihanna,twitter,http://pbs.twimg.com/profile_images/582747937958076418/ZrNhtrD2_normal.jpg,79293791,
+NICKIMINAJ,twitter,http://pbs.twimg.com/profile_images/628666793222082561/CcerTIf6_normal.jpg,35787166,630811487406288897
+beyonce,instagram,https://igcdn-photos-g-a.akamaihd.net/hphotos-ak-xft1/t51.2885-19/11098624_1632794343609174_1724291661_a.jpg,247944034,
+badgalriri,instagram,https://igcdn-photos-c-a.akamaihd.net/hphotos-ak-xaf1/t51.2885-19/11032926_1049846535031474_260957621_a.jpg,25945306,
+nickiminaj,instagram,https://igcdn-photos-d-a.akamaihd.net/hphotos-ak-xfa1/t51.2885-19/s150x150/11820626_1619202511669867_2070537136_a.jpg,451573056,
diff --git a/db/migrate/20150805185922_create_followees.rb b/db/migrate/20150805185922_create_followees.rb
new file mode 100644
index 0000000..a6c2576
--- /dev/null
+++ b/db/migrate/20150805185922_create_followees.rb
@@ -0,0 +1,11 @@
+class CreateFollowees < ActiveRecord::Migration
+ def change
+ create_table :followees do |t|
+ t.string :handle
+ t.string :source
+ t.string :avatar_url
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20150806202309_create_subscriptions.rb b/db/migrate/20150806202309_create_subscriptions.rb
new file mode 100644
index 0000000..6bbd399
--- /dev/null
+++ b/db/migrate/20150806202309_create_subscriptions.rb
@@ -0,0 +1,11 @@
+class CreateSubscriptions < ActiveRecord::Migration
+ def change
+ create_table :subscriptions do |t|
+ t.references :user, index: true, foreign_key: true
+ t.references :followee, index: true, foreign_key: true
+ t.datetime :unsubscribe_date
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20150806223126_create_posts.rb b/db/migrate/20150806223126_create_posts.rb
new file mode 100644
index 0000000..9a0e361
--- /dev/null
+++ b/db/migrate/20150806223126_create_posts.rb
@@ -0,0 +1,13 @@
+class CreatePosts < ActiveRecord::Migration
+ def change
+ create_table :posts do |t|
+ t.integer :followee_id
+ t.string :source
+ t.string :native_created_at
+ t.string :native_id
+ t.string :embed_html
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20150807000808_add_native_id_to_followees.rb b/db/migrate/20150807000808_add_native_id_to_followees.rb
new file mode 100644
index 0000000..9cb0d01
--- /dev/null
+++ b/db/migrate/20150807000808_add_native_id_to_followees.rb
@@ -0,0 +1,5 @@
+class AddNativeIdToFollowees < ActiveRecord::Migration
+ def change
+ add_column :followees, :native_id, :string
+ end
+end
diff --git a/db/migrate/20150807191512_add_last_post_id_to_followees.rb b/db/migrate/20150807191512_add_last_post_id_to_followees.rb
new file mode 100644
index 0000000..6a1a7c8
--- /dev/null
+++ b/db/migrate/20150807191512_add_last_post_id_to_followees.rb
@@ -0,0 +1,5 @@
+class AddLastPostIdToFollowees < ActiveRecord::Migration
+ def change
+ add_column :followees, :last_post_id, :string
+ end
+end
diff --git a/db/posts.csv b/db/posts.csv
new file mode 100644
index 0000000..c0d6cef
--- /dev/null
+++ b/db/posts.csv
@@ -0,0 +1,3 @@
+followee_id,source,native_created_at,native_id,embed_html
+1,twitter,"2015,8,8",31239408,"8-8"
+2,instagram,"2015,5,5",25945306,"5-5"
diff --git a/db/schema.rb b/db/schema.rb
index 4a719d0..c783f8e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,38 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150805173151) do
+ActiveRecord::Schema.define(version: 20150807191512) do
+
+ create_table "followees", force: :cascade do |t|
+ t.string "handle"
+ t.string "source"
+ t.string "avatar_url"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "native_id"
+ t.string "last_post_id"
+ end
+
+ create_table "posts", force: :cascade do |t|
+ t.integer "followee_id"
+ t.string "source"
+ t.string "native_created_at"
+ t.string "native_id"
+ t.string "embed_html"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "subscriptions", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "followee_id"
+ t.datetime "unsubscribe_date"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "subscriptions", ["followee_id"], name: "index_subscriptions_on_followee_id"
+ add_index "subscriptions", ["user_id"], name: "index_subscriptions_on_user_id"
create_table "users", force: :cascade do |t|
t.string "name"
diff --git a/db/seeds.rb b/db/seeds.rb
index 4edb1e8..d155baf 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,7 +1,76 @@
-# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
-#
-# Examples:
-#
-# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
-# Mayor.create(name: 'Emanuel', city: cities.first)
+require 'csv'
+
+CSV.foreach("db/followees.csv", headers: true) do |row|
+ Followee.create(
+ handle: row[0],
+ source: row[1],
+ avatar_url: row[2],
+ native_id: row[3],
+ last_post_id: row[4]
+ )
+end
+
+# followees = [
+# { handle: "beyonce",
+# source: "twitter",
+# avatar_url: "http://pbs.twimg.com/profile_images/582395896593170432/pw_jGziR_normal.jpg",
+# native_id: 31239408,
+# last_post_id: "369486010280706048"},
+# { handle: "rihanna",
+# source: "twitter",
+# avatar_url: "http://pbs.twimg.com/profile_images/582747937958076418/ZrNhtrD2_normal.jpg",
+# native_id: 79293791,
+# last_post_id: ""},
+# { handle: "NICKIMINAJ",
+# source: "twitter",
+# avatar_url: "http://pbs.twimg.com/profile_images/628666793222082561/CcerTIf6_normal.jpg",
+# native_id: 35787166,
+# last_post_id: "630811487406288897"},
+# { handle: "beyonce",
+# source: "instagram",
+# avatar_url: "https://igcdn-photos-g-a.akamaihd.net/hphotos-ak-xft1/t51.2885-19/11098624_1632794343609174_1724291661_a.jpg",
+# native_id: 247944034,
+# last_post_id: ""},
+# { handle: "badgalriri",
+# source: "instagram",
+# avatar_url: "https://igcdn-photos-c-a.akamaihd.net/hphotos-ak-xaf1/t51.2885-19/11032926_1049846535031474_260957621_a.jpg",
+# native_id: 25945306,
+# last_post_id: ""} ,
+# { handle: "nickiminaj",
+# source: "instagram",
+# avatar_url: "https://igcdn-photos-d-a.akamaihd.net/hphotos-ak-xfa1/t51.2885-19/s150x150/11820626_1619202511669867_2070537136_a.jpg",
+# native_id: 451573056,
+# last_post_id: ""}
+# ]
+
+# followees.each do |followee|
+# Followee.create(followee)
+# end
+
+CSV.foreach("db/subscriptions.csv", headers: true) do |row|
+ Subscription.create(
+ user_id: row[0],
+ followee_id: row[1],
+ unsubscribe_date: row[2]
+ )
+end
+
+CSV.foreach("db/users.csv", headers: true) do |row|
+ User.create(
+ name: row[0],
+ email: row[1],
+ avatar_url: row[2],
+ uid: row[3],
+ provider: row[4]
+ )
+end
+
+CSV.foreach("db/posts.csv", headers: true) do |row|
+ Post.create(
+ followee_id: row[0],
+ source: row[1],
+ native_created_at: row[2],
+ native_id: row[3],
+ embed_html: row[4]
+ )
+end
diff --git a/db/subscriptions.csv b/db/subscriptions.csv
new file mode 100644
index 0000000..27a0928
--- /dev/null
+++ b/db/subscriptions.csv
@@ -0,0 +1,9 @@
+user_id,followee_id,unsubscribe_date
+1,1,Datetime.new(2015,5,3)
+1,2,Datetime.new(2015,3,5)
+1,3,
+1,4,
+2,1,Datetime.new(2014,9,10)
+2,4,Datetime.new(2015,2,1)
+2,5,
+2,2,
diff --git a/db/users.csv b/db/users.csv
new file mode 100644
index 0000000..e3f2ccd
--- /dev/null
+++ b/db/users.csv
@@ -0,0 +1,3 @@
+name,email,avatar_url,uid,provider
+Jack Attack,attack@email.com,,1234,instagram
+Mary Mary,mnm@email.com,,5678,instagram
diff --git a/lib/api_helper.rb b/lib/api_helper.rb
new file mode 100644
index 0000000..e807464
--- /dev/null
+++ b/lib/api_helper.rb
@@ -0,0 +1,45 @@
+class ApiHelper
+ def self.user_search(query, count, source)
+ case source
+ when ApplicationController::INSTAGRAM
+ InstagramApi.new.user_search(query, count)
+ when ApplicationController::TWITTER
+ TwitterApi.new.user_search(query, count)
+ end
+ end
+
+ def self.process_new_posts(subscriptions)
+ subscriptions.each do |subscription|
+ followee = subscription.followee
+ source = followee.source
+ posts = get_posts_from_API(followee)
+
+ Post.create_posts_and_update_followee(posts, followee, source)
+ end
+ end
+
+ def self.get_posts_from_API(followee)
+ last_post_id = followee.last_post_id
+ source = followee.source
+
+ case source
+ when ApplicationController::TWITTER
+ id = followee.native_id.to_i
+ posts = TwitterApi.new.get_posts(id, last_post_id)
+ when ApplicationController::INSTAGRAM
+ id = followee.native_id
+ posts = InstagramMapper.get_posts(id, last_post_id)
+ end
+
+ posts
+ end
+
+ def self.post_params(post, followee, source)
+ case source
+ when ApplicationController::TWITTER
+ TwitterMapper.format_params(post, followee)
+ when ApplicationController::INSTAGRAM
+ InstagramMapper.format_params(post, followee)
+ end
+ end
+end
diff --git a/lib/instagram_api.rb b/lib/instagram_api.rb
new file mode 100644
index 0000000..1b33cfb
--- /dev/null
+++ b/lib/instagram_api.rb
@@ -0,0 +1,46 @@
+class InstagramApi
+ INSTA_URI = "https://api.instagram.com/v1/users/search?q="
+ INSTA_USER_POSTS_URI = "https://api.instagram.com/v1/users/"
+ INSTA_OEMBED_URI = "http://api.instagram.com/oembed?omitscript=false&url="
+ INSTA_USER_DETAILS_URI = "https://api.instagram.com/v1/users/"
+
+ # the number of posts to get
+ # the first time you update your feed after following someone
+ FIRST_POSTS = "1"
+
+ def user_search(query, count)
+ response = HTTParty.get(INSTA_URI + query + "&count=" + count.to_s + "&access_token=#{ENV["INSTAGRAM_ACCESS_TOKEN"]}")
+
+ return response["data"]
+ end
+
+ def private_user?(user_id)
+ response = HTTParty.get(INSTA_USER_DETAILS_URI + "#{user_id}" + "/relationship?access_token=#{ENV["INSTAGRAM_ACCESS_TOKEN"]}")
+ privacy_boolean = response["data"]["target_user_is_private"]
+
+ return privacy_boolean
+ end
+
+ def embed_html_with_js(link)
+ response = HTTParty.get(INSTA_OEMBED_URI + link)
+ return response["html"]
+ end
+
+ def query_API_for_posts(id, last_post_id)
+ number_of_posts_query = assign_number_of_posts(last_post_id)
+
+ response = HTTParty.get(
+ INSTA_USER_POSTS_URI + id +
+ "/media/recent/?" + number_of_posts_query +
+ "&access_token=" + ENV["INSTAGRAM_ACCESS_TOKEN"]
+ )
+ posts = response["data"]
+ end
+
+ private
+ ### we're leaving this method here because it is called by #get_posts above
+ ### and we don't want to create a dependency in this class on the InstagramMapper class
+ def assign_number_of_posts(last_post_id)
+ last_post_id ? "min_id=#{last_post_id}" : "count=#{FIRST_POSTS}"
+ end
+end
diff --git a/lib/instagram_mapper.rb b/lib/instagram_mapper.rb
new file mode 100644
index 0000000..d916ac8
--- /dev/null
+++ b/lib/instagram_mapper.rb
@@ -0,0 +1,32 @@
+class InstagramMapper
+ def self.get_posts(id, last_post_id)
+ posts = InstagramApi.new.query_API_for_posts(id, last_post_id)
+
+ remove_duplicate_post(posts, last_post_id)
+ end
+
+ # If there is a last_post_id,
+ # the instagram API will retrieve the last_post again,
+ # so we want to exclude the last post in the collection.
+ # Otherwise return the whole collection (for newly followed users).
+ def self.remove_duplicate_post(posts, last_post_id)
+ last_post_id ? posts[0..-2] : posts
+ end
+
+ def self.format_params(post, followee)
+ post_hash = {}
+ link = post["link"]
+
+ post_hash[:native_id] = post["id"]
+ post_hash[:native_created_at] = convert_instagram_time(post["created_time"])
+ post_hash[:followee_id] = followee.id
+ post_hash[:source] = followee.source
+ post_hash[:embed_html] = InstagramApi.new.embed_html_with_js(link)
+
+ return post_hash
+ end
+
+ def self.convert_instagram_time(time)
+ Time.at(time.to_i)
+ end
+end
diff --git a/lib/twitter_api.rb b/lib/twitter_api.rb
new file mode 100644
index 0000000..a330194
--- /dev/null
+++ b/lib/twitter_api.rb
@@ -0,0 +1,35 @@
+class TwitterApi
+ attr_reader :client
+
+ # the number of posts to get
+ # the first time you update your feed after following someone
+ FIRST_POSTS = 1
+
+ def initialize
+ @client = Twitter::REST::Client.new do |config|
+ config.consumer_key = ENV['TWITTER_CONSUMER_KEY']
+ config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET']
+ config.access_token = ENV['TWITTER_ACCESS_TOKEN']
+ config.access_token_secret = ENV['TWITTER_ACCESS_SECRET']
+ end
+ end
+
+ def user_search(user, count=3)
+ client.user_search(user, options = {count: count})
+ end
+
+ def embed_html_without_js(tweet_id)
+ client.oembed(tweet_id, { omit_script: true })[:html]
+ end
+
+ def get_posts(id, last_post_id)
+ client.user_timeline(id, timeline_options(last_post_id))
+ end
+
+ private
+ ### we're leaving this method here because it is called by #get_posts above
+ ### and we don't want to create a dependency in this class on the TwitterMapper class
+ def timeline_options(last_post_id)
+ last_post_id ? { since_id: last_post_id.to_i } : { count: FIRST_POSTS }
+ end
+end
diff --git a/lib/twitter_mapper.rb b/lib/twitter_mapper.rb
new file mode 100644
index 0000000..d9d3368
--- /dev/null
+++ b/lib/twitter_mapper.rb
@@ -0,0 +1,12 @@
+class TwitterMapper
+ def self.format_params(post, followee)
+ post_hash = {}
+ post_hash[:native_id] = post.id
+ post_hash[:native_created_at] = post.created_at
+ post_hash[:followee_id] = followee.id
+ post_hash[:source] = followee.source
+ post_hash[:embed_html] = TwitterApi.new.embed_html_without_js(post.id)
+
+ return post_hash
+ end
+end
diff --git a/spec/controllers/followees_controller_spec.rb b/spec/controllers/followees_controller_spec.rb
new file mode 100644
index 0000000..5255a83
--- /dev/null
+++ b/spec/controllers/followees_controller_spec.rb
@@ -0,0 +1,103 @@
+require 'rails_helper'
+require 'support/vcr_setup'
+
+RSpec.describe FolloweesController, type: :controller do
+ let(:user) { create :user }
+
+ before :each do
+ session[:user_id] = user.id
+ end
+
+ describe "GET #search" do
+ before :each do
+ get :search
+ end
+
+ it "returns http success" do
+ expect(response).to have_http_status(:success)
+ end
+
+ it "renders the search template" do
+ expect(response).to render_template("search")
+ end
+ end
+
+ describe "POST #twitter_users_redirect" do
+ context "search term entered" do
+
+ before :each do
+ response = post :twitter_users_redirect, user: "pig", source: "twitter"
+ end
+
+ it "redirects to search_results_path if params[:user]" do
+ expect(response).to redirect_to(search_results_path("twitter", "pig"))
+ end
+
+ # negative test
+ it "doesn't redirect to other search term" do
+ expect(response).to_not redirect_to(search_results_path("twitter", "cow"))
+ end
+
+ # negative test
+ it "doesn't redirect to instagram search" do
+ expect(response).to_not redirect_to(search_results_path("instagram", "pig"))
+ end
+ end
+
+ context "no search term entered" do
+ it "without a user it redirects to search_path" do
+ post :twitter_users_redirect, source: "twitter", user: nil
+ expect(response).to redirect_to(search_path)
+ end
+ end
+ end
+
+ describe "POST #instagram_users_redirect" do
+ context "search term entered" do
+ before :each do
+ response = post :instagram_users_redirect, user: "obama", source: "instagram"
+ end
+
+ it "redirects to search_results_path if params[:user]" do
+ expect(response).to redirect_to(search_results_path("instagram", "obama"))
+ end
+
+ # negative test
+ it "doesn't redirect to other search term" do
+ expect(response).to_not redirect_to(search_results_path("instagram", "bush"))
+ end
+
+ # negative test
+ it "doesn't redirect to twitter search" do
+ expect(response).to_not redirect_to(search_results_path("twitter", "obama"))
+ end
+ end
+
+ context "no search term entered" do
+ it "without a user it redirects to search_path" do
+ post :instagram_users_redirect, source: "instagram", user: nil
+ expect(response).to redirect_to(search_path)
+ end
+ end
+ end
+
+ describe "GET #search_results" do
+ context "twitter search" do
+ it "renders the search template" do
+ VCR.use_cassette 'controller/followees_controller/search_results_twitter' do
+ response = get :search_results, source: "twitter", user: "cow"
+ expect(response).to render_template("search")
+ end
+ end
+ end
+
+ context "instagram search" do
+ it "renders the search template" do
+ VCR.use_cassette 'controller/followees_controller/search_results_instagram' do
+ response = get :search_results, source: "instagram", user: "pig"
+ expect(response).to render_template("search")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
new file mode 100644
index 0000000..851701c
--- /dev/null
+++ b/spec/controllers/home_controller_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+require 'support/vcr_setup'
+
+RSpec.describe HomeController, type: :controller do
+ let!(:user) { create :user }
+ before :each do
+ session[:user_id] = user.id
+ end
+
+ describe "GET #newsfeed" do
+
+ context "user has no subscription" do
+
+ before :each do
+ get :newsfeed
+ end
+
+ it "returns http success" do
+ expect(response).to have_http_status(200)
+ end
+
+ it "should display a flash error" do
+ expect(flash[:errors]).to include("You have no subscriptions! Search users to subscribe to.")
+ end
+ end
+
+ context "updating feed for a new followee" do
+ let(:followee_twitter) { create :followee, last_post_id: nil }
+ let(:followee_instagram) { create :followee, handle: "badgalriri", source: "instagram", native_id: "25945306", last_post_id: nil }
+
+ it "adds posts to the database" do
+ VCR.use_cassette 'controller/home_controller/get_newsfeed_last_post_id_absent' do
+ create :subscription, followee_id: followee_twitter.id, user_id: user.id
+ create :subscription, followee_id: followee_instagram.id, user_id: user.id
+
+ get :newsfeed
+ expect(Post.count).to eq 2
+ end
+ end
+ end
+
+ context "updating feed for an existing followee" do
+ let(:followee_twitter) { create :followee, handle: "rhomieux", native_id: "11583102", last_post_id: "630946246803361792"} # 3 back
+ let(:followee_instagram) { create :followee, handle: "barrackobama", native_id: "10206720", last_post_id: "914665339329635845_10206720", source: "instagram" } # 3 back
+
+ before(:each) do
+ VCR.use_cassette 'controller/home_controller/get_newsfeed_last_post_id_present' do
+ create :subscription, followee_id: followee_twitter.id, user_id: user.id
+ create :subscription, followee_id: followee_instagram.id, user_id: user.id
+
+ get :newsfeed
+ end
+ end
+
+ it "adds 2 Instagram posts to the database" do
+ expect(Post.where(source: "instagram").count).to eq 2
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 003bede..12dbe5f 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -1,5 +1,86 @@
require 'rails_helper'
RSpec.describe SessionsController, type: :controller do
+ describe "GET #create" do
+ context "when using instagram authorization" do
+ context "is successful" do
+ before { request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:instagram] }
+ it "redirects to home page" do
+ get :create, provider: :instagram
+ expect(response).to redirect_to root_path
+ end
+
+ it "creates a user" do
+ expect { get :create, provider: :instagram }.to change(User, :count).by(1)
+ end
+
+ it "assigns the @user variable" do
+ get :create, provider: :instagram
+ expect(assigns(:user)).to be_an_instance_of User
+ end
+
+ it "assigns the session[:user_id]" do
+ get :create, provider: :instagram
+ expect(session[:user_id]).to eq assigns(:user).id
+ end
+ end # context is successful
+
+ context "when the user has already signed up" do
+ before(:each) do
+ request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:instagram]
+ end
+ let!(:petunia) { create :user }
+
+ it "doesn't create another user" do
+ get :create, provider: :instagram
+ expect(User.count).to eq(1)
+ end
+
+ it "assigns the session[:user_id]" do
+ get :create, provider: :instagram
+ expect(session[:user_id]).to eq petunia.id
+ end
+ end #context
+
+ # context "fails on instagram" do
+ # before { request.env["omniauth.auth"] = :invalid_credential }
+ #
+ # it "redirect to home with flash error" do
+ # get :create, provider: :instagram
+ # expect(response).to redirect_to root_path
+ # expect(flash[:notice]).to include "Failed to authenticate"
+ # end
+ # end # context
+ #
+ # context "when failing to save the user" do
+ # before {
+ # request.env["omniauth.auth"] = {"uid" => "1234", "info" => {}}
+ # }
+ #
+ # it "redirect to home with flash error" do
+ # get :create, provider: :instagram
+ # expect(response).to redirect_to root_path
+ # expect(flash[:notice]).to include "Failed to save the user"
+ # end
+ # end # context
+ end # using intsagram auth
+ end #create describe
+
+ describe "DELETE #destroy" do
+ let(:user) { create :user }
+
+ before :each do
+ session[:user_id] = user.id
+ delete :destroy
+ end
+
+ it "removes the current_user's id from sessions" do
+ expect(session[:user_id]).to be_nil
+ end
+
+ it "redirects to the signin page" do
+ expect(response).to redirect_to signin_path
+ end
+ end
end
diff --git a/spec/controllers/subscriptions_controller_spec.rb b/spec/controllers/subscriptions_controller_spec.rb
new file mode 100644
index 0000000..850fa27
--- /dev/null
+++ b/spec/controllers/subscriptions_controller_spec.rb
@@ -0,0 +1,69 @@
+require 'rails_helper'
+
+RSpec.describe SubscriptionsController, type: :controller do
+ describe "POST #create" do
+ let(:zynthia) { create :user }
+
+ before :each do
+ session[:user_id] = zynthia.id
+ post :create, source: "twitter", username: "beyonce", id: "123456", picture: ""
+ end
+
+ it "creates a new subscription" do
+ expect(Subscription.count).to eq(1)
+ end
+ end # create
+
+ describe "PUT #unsubscribe" do
+ let!(:user) { create :user }
+ let!(:followee) { create :followee, id: 100 }
+ let!(:subscription) { create :subscription }
+
+ before :each do
+ session[:user_id] = user.id
+ put :unsubscribe, :id => subscription.id
+ subscription.reload
+ end
+
+ it "adds unsubscribe_date" do
+ expect(subscription.unsubscribe_date).to_not be_nil
+ end
+
+ it "redirects to the root_path" do
+ # for purpose of testing
+ expect(response).to redirect_to root_path
+ end
+ end
+
+ describe "GET #index" do
+ let(:user) { create :user }
+
+ before :each do
+ session[:user_id] = user.id
+ @followee1 = create :followee
+ @followee2 = create :followee
+ create :subscription, followee_id: @followee1.id, user_id: user.id
+ create :subscription, followee_id: @followee2.id, user_id: user.id
+ get :index
+ end
+
+ it "renders index template" do
+ expect(response).to render_template "index"
+ end
+
+ it "returns an array of followees user's actively following" do
+ expect(assigns[:followees].count).to eq(2)
+ expect(assigns[:followees].first).to eq(@followee1)
+ expect(assigns[:followees].last).to eq(@followee2)
+ end
+
+ it "returns only active subscription followees" do
+ followee3 = create :followee
+ create :subscription, followee_id: followee3.id, user_id: user.id, unsubscribe_date: "Sat, 01 Aug 2015 22:12:25 UTC +00:00"
+
+ expect(assigns[:followees].count).to eq(2)
+ expect(assigns[:followees].first).to eq(@followee1)
+ expect(assigns[:followees].last).to eq(@followee2)
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index e69de29..5766e03 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -0,0 +1,29 @@
+FactoryGirl.define do
+
+ factory :user do
+ provider 'instagram'
+ uid 'e@mail.com'
+ name 'Zynthia'
+ end
+
+ factory :followee do
+ handle "beyonce"
+ source "twitter"
+ avatar_url "http://pbs.twimg.com/profile_images/582395896593170432/pw_jGziR_normal.jpg"
+ native_id 31239408
+ last_post_id "369486010280706048"
+ end
+
+ factory :subscription do
+ user_id 12
+ followee_id 100
+ end
+
+ factory :post do
+ followee_id 1
+ source "instagram"
+ native_created_at "2015-06-01"
+ native_id "www.instagram.com/blah"
+ embed_html "blah"
+ end
+end
diff --git a/spec/helpers/followees_helper_spec.rb b/spec/helpers/followees_helper_spec.rb
new file mode 100644
index 0000000..44b1378
--- /dev/null
+++ b/spec/helpers/followees_helper_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the FolloweesHelper. For example:
+#
+# describe FolloweesHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe FolloweesHelper, type: :helper do
+end
diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb
new file mode 100644
index 0000000..82578e5
--- /dev/null
+++ b/spec/helpers/home_helper_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the HomeHelper. For example:
+#
+# describe HomeHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe HomeHelper, type: :helper do
+end
diff --git a/spec/helpers/posts_helper_spec.rb b/spec/helpers/posts_helper_spec.rb
new file mode 100644
index 0000000..2a15f35
--- /dev/null
+++ b/spec/helpers/posts_helper_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the PostsHelper. For example:
+#
+# describe PostsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe PostsHelper, type: :helper do
+end
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
index 9484198..38c13fc 100644
--- a/spec/helpers/sessions_helper_spec.rb
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -11,5 +11,4 @@
# end
# end
RSpec.describe SessionsHelper, type: :helper do
- pending "add some examples to (or delete) #{__FILE__}"
end
diff --git a/spec/helpers/subscriptions_helper_spec.rb b/spec/helpers/subscriptions_helper_spec.rb
new file mode 100644
index 0000000..0c49930
--- /dev/null
+++ b/spec/helpers/subscriptions_helper_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the SubscriptionsHelper. For example:
+#
+# describe SubscriptionsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe SubscriptionsHelper, type: :helper do
+end
diff --git a/spec/instagram_api_spec.rb b/spec/instagram_api_spec.rb
new file mode 100644
index 0000000..f789bda
--- /dev/null
+++ b/spec/instagram_api_spec.rb
@@ -0,0 +1,85 @@
+require 'rails_helper'
+require 'instagram_api'
+require 'support/vcr_setup'
+
+RSpec.describe InstagramApi do
+ let(:instagram_api) { InstagramApi.new }
+
+ # results.count will equal 4 if the search term is "rihanna"
+ # not sure why -- is this a known instagram bug?
+ describe "#user_search" do
+ it "returns up to a specified number of users" do
+ VCR.use_cassette 'instagram_api/user_search' do
+ results = instagram_api.user_search("beyonce", 3)
+ expect(results.count).to be <= 3
+ end
+ end
+ end
+
+ describe "#private_user?" do
+ it "returns a truthy value for private user" do
+ VCR.use_cassette 'instagram_api/private_user' do
+ private_user_id = "184517457"
+ result = instagram_api.private_user?(private_user_id)
+ expect(result).to be_truthy
+ end
+ end
+
+ it "returns a falsy value for non-private user" do
+ VCR.use_cassette 'instagram_api/non_private_user' do
+ non_private_user_id = "9266829"
+ result = instagram_api.private_user?(non_private_user_id)
+ expect(result).to be_falsy
+ end
+ end
+ end
+
+ describe "#embed_html_with_js" do
+ let(:instagram_link) { "http://instagr.am/p/D/" }
+
+ before :each do
+ VCR.use_cassette 'instagram_api/embed_html_with_js' do
+ @response = instagram_api.embed_html_with_js(instagram_link)
+ end
+ end
+
+ it "returns a blockquote" do
+ expect(@response[1..10]).to eq "blockquote"
+ end
+
+ it "returns a script" do
+ expect(@response[-7..-2]).to eq "script"
+ end
+ end
+
+ describe "#query_API_for_posts" do
+ context "last_post_id present" do
+ let(:followee) { create :followee, handle: "barrackobama", native_id: "10206720", last_post_id: "914665339329635845_10206720", source: "instagram" } # 3 back
+
+ # the last post id was >= 3 back for the followee
+ it "returns posts when posts have been made since last_post_id" do
+ VCR.use_cassette 'instagram_api/query_API_for_posts_with_last_post_id' do
+ id = followee.native_id
+ last_post_id = followee.last_post_id
+ response = instagram_api.query_API_for_posts(id, last_post_id)
+
+ expect(response.count).to eq 3
+ end
+ end
+ end
+
+ context "last_post_id absent" do
+ let(:followee) { create :followee, handle: "barrackobama", native_id: "10206720", last_post_id: nil, source: "instagram" }
+
+ it "returns 1 post" do
+ VCR.use_cassette 'instagram_api/query_API_for_posts_without_last_post_id' do
+ id = followee.native_id
+ last_post_id = followee.last_post_id
+ response = instagram_api.query_API_for_posts(id, last_post_id)
+
+ expect(response.count).to eq 1
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/followee_spec.rb b/spec/models/followee_spec.rb
new file mode 100644
index 0000000..9848536
--- /dev/null
+++ b/spec/models/followee_spec.rb
@@ -0,0 +1,40 @@
+require 'rails_helper'
+
+RSpec.describe Followee, type: :model do
+ describe "model validations" do
+ context "valid" do
+ let (:followee) { create :followee }
+
+ it "requires a handle and source" do
+ expect(followee).to be_valid
+ end
+ end
+
+ context "invalid" do
+ let (:invalid_followee) { build :followee, handle: nil, source: nil, native_id: nil }
+
+ it "requires a handle and source" do
+ expect(invalid_followee).to be_invalid
+ expect(invalid_followee.errors.keys).to include(:handle, :source, :native_id)
+ end
+ end
+ end
+
+ describe "assocations" do
+ before :each do
+ @followee = Followee.create(handle: "samiam", source: "twitter")
+ end
+
+ it "responds to 'posts' method call" do
+ expect(@followee).to respond_to(:posts)
+ end
+
+ it "responds to 'subscriptions' method call" do
+ expect(@followee).to respond_to(:subscriptions)
+ end
+
+ it "responds to 'users' method call" do
+ expect(@followee).to respond_to(:users)
+ end
+ end
+end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
new file mode 100644
index 0000000..57fbd2f
--- /dev/null
+++ b/spec/models/post_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+RSpec.describe Post, type: :model do
+ describe "validations" do
+ let(:post) { build :post }
+
+ it "requires a followee_id" do
+ post.followee_id = nil
+ post.valid?
+ expect(post.errors.keys).to include(:followee_id)
+ end
+
+ it "requires a source" do
+ post.source = nil
+ post.valid?
+ expect(post.errors.keys).to include(:source)
+ end
+
+ it "requires a native_created_at" do
+ post.native_created_at = nil
+ post.valid?
+ expect(post.errors.keys).to include(:native_created_at)
+ end
+
+ it "requires a native_created_at" do
+ post.native_id = nil
+ post.valid?
+ expect(post.errors.keys).to include(:native_id)
+ end
+ end
+
+ describe "assocations" do
+ let(:post) { create :post }
+
+ it "responds to 'followee' method call" do
+ expect(post).to respond_to(:followee)
+ end
+ end
+end
diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb
new file mode 100644
index 0000000..97b3c7b
--- /dev/null
+++ b/spec/models/subscription_spec.rb
@@ -0,0 +1,61 @@
+require 'rails_helper'
+
+RSpec.describe Subscription, type: :model do
+ describe "validations" do
+ let(:space) { create :subscription }
+
+ context "user_id and followee_id are required" do
+ it "subscription is valid" do
+ expect(space).to be_valid
+ end
+
+ it "subscription invalid if missing user_id" do
+ space.user_id = nil
+ expect(space).to be_invalid
+ end
+
+ it "subscription invalid if missing followee_id" do
+ space.followee_id = nil
+ expect(space).to be_invalid
+ end
+ end # context
+ end #describe
+
+ describe ".find_or_create_subscription" do
+ let!(:zynthia) { create :user, id: 1 }
+ let!(:beyonce) { create :followee, id: 200, handle: "beyonce", source: "instagram" }
+ let!(:sub2) { create :subscription, user_id: zynthia.id, followee_id: beyonce.id, unsubscribe_date: "Sat, 02 Aug 2015 22:12:25 UTC +00:00" }
+
+ it "creates valid subscription to existing followee & user" do
+ natgeo = Subscription.find_or_create_subscription(zynthia, beyonce)
+ expect(natgeo).to be_valid
+ end
+
+ it "makes new subscription from previously unsubscribed followee" do
+ # re-subscribe
+ natgeo = Subscription.find_or_create_subscription(zynthia, beyonce)
+
+ expect(Subscription.count).to eq(2)
+ expect(Subscription.first).to eq(sub2)
+ expect(Subscription.last).to eq(natgeo)
+ end
+ end
+
+ describe "scopes" do
+
+ context ":active scope" do
+ let(:sub) { create :subscription }
+ let(:sub2) { create :subscription, unsubscribe_date: "Sat, 02 Aug 2015 22:12:25 UTC +00:00" }
+ let(:sub3) { create :subscription, unsubscribe_date: "Sat, 01 Aug 2015 22:12:25 UTC +00:00" }
+
+ it "selects subscriptions that do not have an unsubscribe_date" do
+ sub.reload
+ sub2.reload
+ sub3.reload
+
+ expect(Subscription.count).to eq 3
+ expect(Subscription.active.count).to eq 1
+ end
+ end
+ end
+end # Rspec
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 47a31bb..09b7f6d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,5 +1,70 @@
require 'rails_helper'
RSpec.describe User, type: :model do
- pending "add some examples to (or delete) #{__FILE__}"
+ let(:zyn) { create :user }
+
+ describe "validations" do
+ it "is valid" do
+ expect(zyn).to be_valid
+ end
+
+ it "requires a uid" do
+ zyn.uid = nil
+ expect(zyn).to be_invalid
+ end
+
+ it "requires a provider" do
+ zyn.provider = nil
+ expect(zyn).to be_invalid
+ end
+ end # validations
+
+ describe ".find_or_create_from_omniauth" do
+ context "when user has already been created" do
+ it "finds the correct user" do
+ maebe = create :user, uid: "123", provider: "instagram"
+ user = User.find_or_create_from_omniauth({
+ "uid" => "123",
+ "provider" => "instagram",
+ "info" => {}
+ })
+
+ expect(user).to eq(maebe)
+ end
+ end # context
+
+ context "when user has already been created" do
+ let(:petunia) {
+ User.find_or_create_from_omniauth(
+ OmniAuth.config.mock_auth[:instagram]
+ )
+ # mock_auth is like a result of an authentication from instagram
+ }
+
+ it "user auth is valid" do
+ expect(petunia).to be_valid
+ end
+ end # context
+
+ context "when omniauth is invalid" do
+ it "nil uid will return nil" do
+ user = User.find_or_create_from_omniauth({
+ "uid" => nil,
+ "provider" => "instagram",
+ "info" => {}
+ })
+ expect(user).to be_nil
+ end
+
+ it "nil provider will return nil" do
+ user = User.find_or_create_from_omniauth({
+ "uid" => "123",
+ "provider" => nil,
+ "info" => {}
+ })
+ expect(user).to be_nil
+ end
+ end # context
+
+ end # describe
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2b013ac..968a4fa 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,5 +1,6 @@
require 'factory_girl'
require 'simplecov'
+# require 'vcr_setup'
SimpleCov.start 'rails'
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
@@ -21,6 +22,36 @@
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
+ config.before(:suite) do
+ # Once you have enabled test mode, all requests
+ # to OmniAuth will be short circuited
+ # to use the mock authentication hash.
+ # A request to /auth/provider will redirect
+ # immediately to /auth/provider/callback.
+
+ OmniAuth.config.test_mode = true
+
+ # The mock_auth configuration allows you to
+ # set per-provider (or default) authentication
+ # hashes to return during testing.
+
+ OmniAuth.config.mock_auth[:instagram] = OmniAuth::AuthHash.new({
+ # used in place of the sessions controller, so you have access to the auth_hash
+ :provider => "instagram",
+ :uid => "e@mail.com",
+ :info => {
+ email: "e@mail.com",
+ name: "Zynthia"}
+ })
+
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new({
+ provider: 'github',
+ uid: '123545',
+ info: {
+ email: "a@b.com", nickname: "Ada"
+ }
+ })
+ end
config.include FactoryGirl::Syntax::Methods
# rspec-expectations config goes here. You can use an alternate
diff --git a/spec/support/vcr_setup.rb b/spec/support/vcr_setup.rb
new file mode 100644
index 0000000..4bc0a90
--- /dev/null
+++ b/spec/support/vcr_setup.rb
@@ -0,0 +1,17 @@
+require 'vcr'
+
+VCR.configure do |c|
+ c.hook_into :webmock
+ c.cassette_library_dir = 'spec/support/vcr_cassettes'
+ # c.configure_rspec_metadata!
+ # c.allow_http_connections_when_no_cassette = true if real_requests
+ # c.default_cassette_options = {:record => :new_episodes}
+end
+
+# real_requests = ENV['REAL_REQUESTS']
+
+# RSpec.configure do |config|
+# config.before(:each) do
+# VCR.eject_cassette
+# end if real_requests
+# end
diff --git a/spec/twitter_api_spec.rb b/spec/twitter_api_spec.rb
new file mode 100644
index 0000000..18edcd3
--- /dev/null
+++ b/spec/twitter_api_spec.rb
@@ -0,0 +1,61 @@
+require 'rails_helper'
+require 'twitter_api'
+require 'support/vcr_setup'
+
+RSpec.describe TwitterApi do
+ let(:twitter_api) { TwitterApi.new }
+
+ describe "#user_search" do
+ it "returns up to a specified number of users" do
+ VCR.use_cassette 'twitter_api/user_search' do
+ results = twitter_api.user_search("rihanna", 3)
+ expect(results.count).to be <= 3
+ end
+ end
+ end
+
+ describe "#embed_html_without_js" do
+ let(:tweet_id) { "630946246803361792" }
+
+ before :each do
+ VCR.use_cassette 'twitter_api/embed_html_without_js' do
+ @result = twitter_api.embed_html_without_js(tweet_id)
+ end
+ end
+
+ it "returns a blockquote" do
+ expect(@result[1..10]).to eq "blockquote"
+ end
+
+ it "does not return a script" do
+ expect(@result[-7..-2]).to_not eq "script"
+ end
+
+ end
+
+ # def get_posts(id, last_post_id)
+ # client.user_timeline(id, timeline_options(last_post_id))
+ # end
+
+ ###### this spec is currently broken
+ # describe "#get_posts" do
+ # context "last_post_id present" do
+ # let(:followee) { create :followee }
+
+ # it "gets a collection of posts for a followee" do
+ # VCR.use_cassette 'twitter_api/get_posts_last_post_id_present' do
+ # # binding.pry
+ # id = followee.id
+ # last_post_id = followee.last_post_id
+ # @posts = twitter_api.get_posts(id, last_post_id)
+ # expect(@posts.count).to be > 0
+ # end
+ # end
+ # end
+
+ # context "last_post_id absent" do
+ # it "gets 1 post for a followee" do
+ # end
+ # end
+ # end
+end