Viewing entries in
Ruby on Rails

Factual & Rails || Grabbing UPC Product Data

We're heads down creating the next significant iteration of Nutribu, our nutrition tracking app. It's nearly ready, and we're dead excited.

Nutribu is being re-imagined to make tracking what you eat, still a very manual process which is unavoidable despite the numerous tech gimmicks you may have seen to help automate the process. So, we're focussing making it simple, fun and dead easy to find what you're eating and capture it in order that you can track your nutrition against your goals.

One area that users really wanted, and isn't really a new feature as it's often touted as one of the best features of apps like MyFitnessPal, is the ability to scan a UPC product barcode and instantly return the product complete with nutritional information. I can tell you now, the new Nutribu will have that too. Yay!!!

Actually, now I'll show you how to implement such a feature into your own app, starting with the server side component of fetching and storing product information from a service like Factual.

Before you do anything, head over to Factual and grab an API key and secret.

1. There's a Gem for that, of course.

gem 'factual-api'

2. Write tests.

Note: We use VCR to optimise the testing of HTTP interactions.

products_controller_spec.rb
require 'spec_helper'

describe V1::ProductsController do
  
  include_context "signed up user"
  include_context "api token authentication"
  
  describe "show" do
    let(:product) { Product.new(name: "Stone Crop Body Lotion", brand: "Eminence", upc: "608866337447") }
    def get_product
      VCR.use_cassette('factualapi') do
        get "/v1/products/#{product.upc}.json", nil, token_auth
      end
    end
    it "returns status 200" do
      get_product
      expect(response.status).to eq(200)
    end

    it "contains the product info" do
      get_product
      body = JSON.parse(response.body)
      expect(body["name"]).to eq(product.name)
      
    end
    
    context "product does not exists in in-house db" do
      it "create a new product in db" do
        
      end
    end
  end
end

3. Generate Products Model

$rails g model Product name brand weight:integer upc

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.string :brand
      t.integer :weight
      t.string :upc
      
      t.timestamps
    end
  end
end

4. Generate a Model for Product Image

$rails g model Image image:attachment product:references
class Image < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.references :product
      t.attachment :image
    end
  end
end

5. Update Routes.rb

...

resources :products

...

6. Product Model and Validations

app/models/product.rb

class Product < ActiveRecord::Base
  validates_presence_of :name
  validates_presence_of :upc
  validates_uniqueness_of :upc
  has_many :images
end

7. Image Model & Validations

This one is somewhat more involved. We use Paperclip

#todo include explanation for this.

app/models/image.rb

class Image < ActiveRecord::Base
  
  has_attached_file :image
  belongs_to :product
  validates_attachment_content_type :image, :content_type => /\Aimage/
  
  def file_from_url(url)
    self.image = download_remote_file(url)
  end
 
  private
  
  def download_remote_file(url)
    # OpenURI extends Kernel.open to handle URLs as files
    io = open(url)
    
    # overrides Paperclip::Upfile#original_filename;
    # we are creating a singleton method on specific object ('io')
    def io.original_filename
      base_uri.path.split('/').last
    end
    
    io.original_filename.blank? ? nil : io
  end
end

8. The Product Controller

# Update an existing user's profile.
# Requires a valid API token.
class V1::ProductsController < V1::BaseController

  before_action :authenticate

  def show
    result = FetchProduct.perform({upc: params[:id]})
    if result.success?
      render json: result.product
    else
      render json: {error: {message: result.message}}, status: result.status
    end
  end

end


9. The FetchProduct Interactor

We use Interactors to add a layer of abstraction between our models and controllers - especially useful when performing complex interactions in a single request.

Basically, how we want this to work, is to initially check our own database to see if the product already exists, if not route the request to Factual to get the data, return and store it in our db, so the next request can just fetch it from our local store.

app / interactors / fetch_product.rb

class FetchProduct
  require 'factual'
  include Interactor
  attr_reader :status
  attr_reader :product
  
  def perform
    product = Product.find_by_upc(context[:upc])
    if product.present?
      @status = :found
      context[:product] = product
    else
      # could not find this product in our db, find it on factual db
      factual = Factual.new(ENV['FACTUAL_KEY'], ENV['FACTUAL_SECRET'])
      raw_data = factual.table("products-cpg").filters("upc" => upc).last
      
      # this should be put in a worker, then what will we return for the first time? not_found?
      if raw_data.present?
        # create product from raw data
        product = Product.create({name: raw_data["product_name"], brand: raw_data["brand"], upc: raw_data["upc"]})
        raw_data["image_urls"].each do |url|
          image = product.images.create()
          image.file_from_url(url)
        end
        @product = product
        @status = :found
      else
        context.fail! message: "could not find product info"
        @status = :not_found
      end
    end
  end
end


Reigniting an Old Flame || Fuelr

When I was 18 (I think, it was such a frickin' long time ago), I used to sit in office buildings for 12 hours straight, working site security to pay for my university fees (well, beer money). I was fascinated by the idea of offering prescribed physical activity for people with chronic conditions, and for the prevention of disease in at-risk populations. 

I had completed a module in my sports studies degree, on this very subject. The NHS had not long released the framework for quality assurance, for the prescription of exercise and exercise referral schemes.

I used to write business plan after business plan, how I would construct a company to offer such services. I would talk to GPs and practise managers. I would do cashflow forecasts and P&Ls. I would research the equipment needs, and create startup cost budgets. 

Then I did nothing about it. Until now. 14 years later.

Not only am I now working on a next-generation Social Care platform, which is all about re-enablement and self-management of conditions, but I'm also working on a health and wellness platform for tracking and improving your overall wellbeing, by improving your knowledge and behaviour through the use of smart technologies and connected devices. I am now positioned to build something that does what I wanted, only a decade ago.

But, this is not the same plan - such has been the journey and has lead me back to this point. My education, in particular over the past 4 - 5 years, has armed me with what I was missing back then.

- Technical skills and the ability to execute.

- The mental processes that force me to ship a product

- Confirmation that my passion for this space is unyielding.

- Knowledge of a global market opportunity, enabled through technology, for distribution and scaleable business models.

The original idea was nice. But it wasn't scaleable. It was a lifestyle business idea, like a hairdressers, or an activity centre owner, or a restaurant owner.

I was in San Francisco 2 years ago, when I saw the idea, realised, working and gaining traction. A US startup called Wello had pulled together nascent real time video streaming protocols, such as webRTC, and modern web application development techniques, to offer a marketplace for exercise professionals.

 

Go to the site, and you can take part in pilates, yoga, circuits, cross-training, strength training... you name it. Delivered by real experts, in real time, at any time, wherever you or they are.

Now, this really appeals to me, both conceptually and technically.

So, this thread of posts is going to follow my exploration into building a very similar service offering, with I think, some key differentiators. We'll tackle the main technical concepts, including:

  • Building a web services / API using Ruby on Rails
  • Building a native iOS application, specifically for iPad
  • Using webRTC and the different options available to make this easier
  • Creating an admin area for the business
  • Creating the web application front end views

 

 

 

Using Omniauth Facebook Profile Image with Carrierwave in Rails 4

Using Omniauth Facebook Profile Image with Carrierwave in Rails 4

I'm currently learning Ruby on Rails, as a 2014 resolution and I'm really pleased with my progress so far under the Bloc Curriculum. I will write more about the general learning experience, but this post is somewhat specific.

In my first full demo app, which I've lovingly branded Jello.io - a reddit clone complete with users, topics, posts, user roles & capabilities, voting, favouriting, etc. etc. The User Authentication part is handled by a popular gem, Devise.

Whilst Devise is excellent at a lot of things, it is also quite problematic in a number of ways, most of which I have not yet experienced. I look forward to rolling my own Auth functionality in future projects.

I have added the Carrierwave gem with the MiniMagic gem to support a User avatar upload feature. This all works very nicely using the internal system.

But, then I added Facebook connect for user signups, via the Omniauth gem and wanted grab the Facebook User Profile Image from the Facebook hash.

I tried a few options, overly complicated as usual, including:

Original method without profile image for Facebook in user.rb

...

mount_uploader :avatar, AvatarUploader
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
pass = Devise.friendly_token[0,20]
user = User.new(name: auth.extra.raw_info.name,
 provider: auth.provider,
 uid: auth.uid,
 email: auth.info.email,
 password: pass,
 password_confirmation: pass
)
user.skip_confirmation!
user.save
end
user
end

...

Added Avatar to the User.new arguments. Didn't work.

...

mount_uploader :avatar, AvatarUploader
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
pass = Devise.friendly_token[0,20]
user = User.new(name: auth.extra.raw_info.name,
 provider: auth.provider,
 uid: auth.uid,
 email: auth.info.email,
 avatar: auth.info.image,
 password: pass,
 password_confirmation: pass
)
user.skip_confirmation!
user.save
end
user
end

...

Then changed from using the method magic (which may or may not always work) to retrieve the image from the Facebook Hash, to explicitly selecting from the Hash which is much more reliable, so I'm told...

...

mount_uploader :avatar, AvatarUploader
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
pass = Devise.friendly_token[0,20]
user = User.new(name: auth.extra.raw_info.name,
 provider: auth.provider,
 uid: auth.uid,
 email: auth.info.email,
 avatar: auth[:info][:image],
 password: pass,
 password_confirmation: pass
)
user.skip_confirmation!
user.save
end
user
end

...

Then, realising that this is just passing us an image url, which still isn't quite sufficient, we need to open the url first before uploading. So trying:

...

mount_uploader :avatar, AvatarUploader
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
pass = Devise.friendly_token[0,20]
user = User.new(name: auth.extra.raw_info.name,
 provider: auth.provider,
 uid: auth.uid,
 email: auth.info.email,
 avatar: open(auth[:info][:image]),
 password: pass,
 password_confirmation: pass
)
user.skip_confirmation!
user.save
end
user
end

...

But, that's a bit of a dirty hack, and nonetheless, also didn't work. So, when all else fails, the lesson here is Go Back to the Documentation.

With a little more searching, the solution was actually very straightforward. By providing the remote_avatar_url key, which has some baked in magic for unpacking the url ready for your app to display the image.

...

mount_uploader :avatar, AvatarUploader
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
pass = Devise.friendly_token[0,20]
user = User.new(name: auth.extra.raw_info.name,
 provider: auth.provider,
 uid: auth.uid,
 email: auth.info.email,
 remote_avatar_url: auth[:info][:image],
 password: pass,
 password_confirmation: pass
)
user.skip_confirmation!
user.save
end
user
end

...

Phew.