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" %> + + + <% 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