Consuming Tumblr blog from your website using Spree

Consuming Tumblr blog from your website using Spree

Consuming Tumblr blog from your website using Ruby on Rails (Spree), Backbone and Tumblr Api

You need to have an account on tumblr of course and generate the application to be able to consume the tumblr api:
http://www.tumblr.com/oauth/apps

About the default callback you can choose any url for example: http:://localhost:3000

Then generate the application oauth in the following url:
https://api.tumblr.com/console/calls/user/info

Add the following gem to your Gemfile:

gem 'tumblr_client'


Now from console you can do some tests before starting working with the project files:

token_params = {
 consumer_key: 'some test',
 consumer_secret: 'some test',
 oauth_token: 'token',
 oauth_token_secret: 'oauthtoken'
}

client = Tumblr::Client.new(token_params)

client.info

client.posts("your-tumblr-url.tumblr.com", limit: 10, offset: 2)

And now let’s start working with our project files

Gemfile


gem 'tumblr_client'

app/assets/javascripts/my_project.js.coffee:


window.MyProject =
  Models: {}
  Collections: {}
  Views: {}
  Routers: {}
  Router: null
  PostPerPage: 8
  OrderNumber: null
  initialize: ->
    @Router = new MyProject.Routers.MainRouter()
    Backbone.history.start()
    @

MyProject.InitializeBlog = ->
  blog = new MyProject.Models.Blog()
  blog.fetch
    success: (model) ->
      collection = new MyProject.Collections.PostsCollection(
        blog.get('posts')
      )
      view = new MyProject.Views.BlogView
                                   model: blog
                                   collection: collection
                                   el: "#blog-content"
                                   currentPage: 1

      view.render()
    error: ->
      $("#loading").html 'The tumblr blog is not available.'

$(document).ready ->
  MyProject.initialize()

app/assets/javascripts/helpers/post_types.js.coffee:


Handlebars.registerHelper 'PostType', (post)->
  post_date = post.date.replace(' GMT', '')
  postDate = $.format.date(post_date, 'MMM dd,yyyy')
  linkURL = post.post_url

  htmlContent = "<div class='post #{post.type}-post'>"
  switch post.type
    when "audio"
      audioTitle = "AUDIO:"
      imgSRC = (if post.album_art then "<img src=' #{post.album_art} '/>" else " ")
      htmlContent += "<h2> #{audioTitle} </h2><p class='post-date'> #{postDate} </p> #{imgSRC} #{post.player} #{post.caption} <a href=' #{linkURL} '>Go to tumblr post...</a>"

    when "text"
      htmlContent += "<h2> #{post.title} </h2> <p class='post-date'> #{postDate} </p> #{post.body} <a href=' #{linkURL} '>Go to tumblr post...</a>"

    when "photo"
      photos = post.photos
      if photos.length > 1
        htmlContent = "<div class='post photo-post multi-photo'>"
      else
        htmlContent = "<div class='post photo-post'>"

      for photo in photos
        photoSizeURL = (size for size in photo.alt_sizes when 400 <= size.width <= 500)[0]
        photoSizeURL ||= photo.original_size

        htmlContent += "<a href=' #{photo.original_size.url} ' target='_blank' title=' #{photo.caption} '><img src=' #{photoSizeURL.url} '/></a>"

        unless photo.caption is ""
          htmlContent += "<figcaption> #{photo.caption} </figcaption>"

      htmlContent += "<p class='post-date'> #{postDate} </p> #{post.caption} <a href=' #{linkURL} '>Go to tumblr post...</a></div>"

    when "quote"
      htmlContent += "<p class='post-date'> #{postDate} </p><q class='quote-text'> #{post.text} </q><p class='quote-author'> &#8212; #{post.source} </p><a href=' #{linkURL} '>Go to tumblr post...</a>"

    when "video"
      htmlContent += "<p class='post-date'> #{postDate} </p>"
      htmlContent += "#{post.player[2].embed_code} #{post.caption} <a href=' #{linkURL} '>Go to tumblr post...</a></div>"

    when "link"
      description = post.description or ""
      htmlContent += "<p class='post-date'> #{postDate} </p> <a href=' #{post.url} '> #{post.title} </a> #{description} <a href=' #{linkURL} '>Go to tumblr post...</a>"

    when "chat"
      htmlContent += "<p class='post-date'> #{postDate} </p>"

      for dialogue in post.dialogue
        htmlContent += "<span class='chat-post-name'> #{dialogue.name} </span><p class='chat-post-phrase'> #{dialogue.phrase} </p>"

      htmlContent += "<a href=' #{linkURL} '>Go to tumblr post...</a>"

  return new Handlebars.SafeString(htmlContent)

app/assets/javascripts/collections/posts_collection.js.coffee:

class MyProject.Collections.PostsCollection extends Backbone.Collection
  url: 'api/blog'

app/assets/javascripts/models/blog.js.coffee:

class MyProject.Models.Blog extends Backbone.Model
  urlRoot: 'api/blog'

app/assets/javascripts/models/post.js.coffee:


class MyProject.Models.Post extends Backbone.Model
  urlRoot: 'api/blog'

app/assets/javascripts/views/blog/post_view.js.coffee:


class MyProject.Views.PostView extends Backbone.View
  template: HandlebarsTemplates['blog/post_view']

  tagName: 'article'

  render: ->
    $(@el).html(@template( @model.toJSON() )).fadeIn()
    @

app/assets/javascripts/views/blog/blog_view.js.coffee:


class MyProject.Views.BlogView extends Backbone.View
  template: HandlebarsTemplates['blog/blog_view']

  initialize: (options) ->
    @collection.on 'reset', @addAllPosts, @
    @currentPage = options.currentPage
    @totalPosts = parseInt(@model.get('total_posts'))
    $(window).bind "scroll", (ev) =>
      @checkScroll()

  render: ->
    $(@el).html(@template( @model.toJSON() ))
    @addAllPosts()
    @

  addAllPosts: ->
    @collection.each @addPost, @

  checkScroll: ->
    isLastPage = @isTheLastPage()
    @removeLoadingResults()
    windowValue = ($(window).innerHeight() + $(window).scrollTop() + 400)
    @getMorePosts() if windowValue >= $("body").height() and not isLastPage

  getMorePosts: ->
    next_page =  (@currentPage + 1) * MyProject.PostPerPage
    @model.fetch
      data:
        page: next_page
      success: (model) =>
        @currentPage = @currentPage + 1
        @collection.reset(@model.get('posts'))

      error: ->
        alert "a ocurrido un error favor de intentarlo mas tarde..."

      async: false

  removeLoadingResults: ->
    page = (@currentPage + 1) * MyProject.PostPerPage
    if @totalPosts <= page       @$el.siblings('#loading').remove()   isTheLastPage: ->
    currentPage = @currentPage * MyProject.PostPerPage
    @totalPosts <= currentPage   addPost: (post) ->
    currentView = new MyProject.Views.PostView
                                        model: post

    @$("#posts-list").append currentView.render().el

app/assets/templates/blog/blog_view.hbs.haml


%h1
Hello from the content blog

%hr
#posts-list

app/assets/templates/blog/post_view.hbs.haml:


{{PostType this}}
%hr

app/controllers/spree/blog_controller.rb:

module Spree
  class BlogController < Spree::StoreController

    def index
    end
  end
end

app/controllers/spree/blog_entries_controller.rb:


module Spree
  class BlogEntriesController < Spree::StoreController     def index       blog = ENV['TUMBLR_BLOG_URL']       posts = TumblrApiService.get_posts page_params, blog       if posts['total_posts'] > 0
        render json: posts, status: :ok
      else
        render json: [], status: :unprocessable_entity
      end
    end

    private

    def page_params
      (params[:page].to_i || 1 ) - 1
    end
  end
end

app/services/tumblr_api_service.rb:

class TumblrApiService
  def self.get_posts page, blog
    client = Tumblr::Client.new
    client.posts blog, limit:8, offset: page
  end
end

config/initializers/tumblr.rb:


Tumblr.configure do |config|
  config.consumer_key = ENV['CONSUMER_KEY']
  config.consumer_secret = ENV['CONSUMER_SECRET']
  config.oauth_token = ENV['ACCESS_TOKEN']
  config.oauth_token_secret = ENV['ACCESS_TOKEN_SECRET']
end

config/routes.rb:


get 'blog' => 'spree/blog#index'
get 'api/blog' => 'spree/blog_entries#index'

vendor/assets/javascripts/spree/frontend/all.js:

//= require_tree ../../lib

app/views/spree/blog/index.html.haml:


%h1
  Welcome to the blog
#blog-content
#loading
  = image_tag 'loading.gif'

:javascript
  $(function() {
    MyProject.InitializeBlog()
  });

Remember to add a loading image in:

app/assets/images/loading.gif

test suite:

Gemfile

gem 'vcr'
gem 'webmock'

features/support/webmock.rb:


require 'webmock/cucumber'
WebMock.disable_net_connect!(:allow_localhost => true)

spec/controllers/spree/blog_entries_controller_spec.rb:

require 'spec_helper'

describe Spree::BlogEntriesController do

  describe '#index' do
    context "when the query is successfull" do
      specify do
        VCR.use_cassette 'tumblr_api_controller' do
          get :index
          expect(response).to be_success
          expect(JSON(response.body)['blog']['name']).to eq 'yourblogname'
          expect(JSON(response.body)['posts'].count).to eq 7
        end
      end
    end

    context "when the query fails" do
      let(:posts) do
        {
          'total_posts' => 0
        }
      end

      before do
        TumblrApiService.should_receive(:get_posts)
                        .and_return(posts)
      end

      specify do
        get :index
        expect(response.status).to eq(422)
        expect(response.body).to eq('[]')
      end
    end
  end
end

spec/services/tumblr_api_service_spec.rb:

require 'spec_helper'

describe TumblrApiService do

  describe '.get_posts' do
    let(:page){ 1 }
    let(:blog_name){ ENV['TUMBLR_BLOG_URL'] }

    specify do
      VCR.use_cassette 'tumblr_api' do
        result = TumblrApiService.get_posts page, blog_name
        expect(result['blog']['name']).to eq 'yourblogname'
        expect(result['blog']['posts']).to eq 2
        expect(result['posts'].count).to eq 1
      end
    end
  end
end

spec/spec_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
require 'webmock/rspec'

# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

require 'spree/testing_support/factories'

# Checks for pending migrations before tests are run.
# If you are not using ActiveRecord, you can remove this line.
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)

RSpec.configure do |config|
  # ## Mock Framework
  #
  # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
  #
  # config.mock_with :mocha
  # config.mock_with :flexmock
  # config.mock_with :rr

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  config.include FactoryGirl::Syntax::Methods

  config.include Devise::TestHelpers, :type => :controller

  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  #     --seed 1234
  config.order = "random"
end

spec/support/vcr.rb:

require 'vcr'

VCR.configure do |config|
  config.hook_into :webmock
  config.ignore_localhost = true
  config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
  config.configure_rspec_metadata!
  config.preserve_exact_body_bytes { true }
  config.allow_http_connections_when_no_cassette = true
end
No Comments

Post A Comment