Rails Dry up Find or Create - ruby-on-rails

I have the following method in my model which uses find_or_create_by to find or create a new product.
def self.save_prod(product)
Product.find_or_create_by_prod_id(product)
product_data = ItemData.get_product_data(product)
p.update_attributes(
:prod_id => product,
:upc => product_data[:upc],
:title => product_data[:title]
)
end
The ItemData.get_product_data() method is a module method which calls an API to fetch product data:
def self.get_product_data(product)
url_raw = URI.parse("http://www.api.com/v1/itemid=#{product}")
url = Net::HTTP.get_response(url_raw).body
#resp = JSON.parse(url)
#title = Sanitize.clean(#resp["serviceResult"]["itemName"]).strip
#upc = #resp["serviceResult"]["uPC"]
{:title => #title, :upc => #upc}
end
This works as expected, however I know it can be a LOT more efficient, by not calling the ItemData.get_product_data() method every time the save_prod() method is called. How can I add new product data without having to call the ItemData.get_product_data() if a product already exists.

Another way to doing it. This would return the Product object if it is already present otherwise it will create it from api and return the new object.
def self.save_prod(product)
Product.find_by_prod_id(product) || Product.create( ItemData.get_product_data(product) )
end
Modify the api call to return a hash with prod_id. Not sure why you are converting title and upc to class variables here. It could lead to problems if they are used extensively.
def self.get_product_data(product)
url_raw = URI.parse("http://www.api.com/v1/itemid=#{product}")
url = Net::HTTP.get_response(url_raw).body
#resp = JSON.parse(url)
#title = Sanitize.clean(#resp["serviceResult"]["itemName"]).strip
#upc = #resp["serviceResult"]["uPC"]
{:title => #title, :upc => #upc, :prod_id => product}
end

Instead of doing a find or create use find or initialize by . Change your code to following :
prod = find_or_initialize_by_prod_id(product)
if prod.new_record?
prod.save!
product_data = ItemData.get_product_data(product)
prod.update_attributes(
:prod_id => product,
:upc => product_data[:upc],
:title => product_data[:title]
)
end
by using find_or_initalize you can distinguish whether the record was created or found by using new_record method. If new you can save and make an API call and do whatever you want.

Related

Access Local variable in another method Ruby

I am looking to understand how I can access a variable set in method A, then use that variable in method B, and also a clean way of reusing the same part of code and then only changing the query
require 'google/api_client'
module GoogleAnalytics
class Analytic
SERVICE_ACCOUNT_EMAIL_ADDRESS = ENV['SERVICE_ACCOUNT_EMAIL_ADDRESS']
PATH_TO_KEY_FILE = ENV['PATH_TO_KEY_FILE']
PROFILE = ENV['ANALYTICS_PROFILE_ID']
def google_analytics_api
client = Google::APIClient.new(
application_name: "Example Application",
application_version: "1.0")
client.authorization = Signet::OAuth2::Client.new(
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:audience => 'https://accounts.google.com/o/oauth2/token',
:scope => 'https://www.googleapis.com/auth/analytics.readonly',
:issuer => SERVICE_ACCOUNT_EMAIL_ADDRESS,
:signing_key => Google::APIClient::KeyUtils.load_from_pkcs12(PATH_TO_KEY_FILE, 'notasecret')).tap { |auth| auth.fetch_access_token! }
api_method = client.discovered_api('analytics','v3').data.ga.get
# make queries
result = client.execute(:api_method => api_method, :parameters => {
'ids' => PROFILE,
'start-date' => Date.new(2014,1,1).to_s,
'end-date' => Date.today.to_s,
'dimensions' => 'ga:pagePath',
'metrics' => 'ga:pageviews',
'filters' => 'ga:pagePath==/'
})
end
end
end
So if i run the method google_analytics_api i get a set of results returned assigned to the variable result.
So what if i want another 2 separate methods that will return different sets of results, so
new users and bounce rates, that would be two separate calls changing the request params wouldnt it? would i have to repeat the whole method?
Is there a way to refactor this so that the authorization call can be wrapped up in its on method and all i change is the request params assigned to result ?
So something like this
def api_call
logic to make request
end
def new_users
api_call
# make queries
result = client.execute(:api_method => api_method, :parameters => {
'ids' => PROFILE,
'start-date' => Date.new(2014,1,1).to_s,
'end-date' => Date.today.to_s,
'dimensions' => 'ga:pagePath',
'metrics' => 'ga:newUsers',
'filters' => 'ga:pagePath==/'
})
end
One of the problems though will be having the local variables client and result available in the new_users method, what could these be changed to? an instance variable with an #? or a class variable with an ## ?
Your instincts are good - you don't want to repeat yourself, and there are better ways of structuring this code. But rather than sharing variables, you should think about small pieces, loosely joined. Write methods that do one thing well, and combine them together. For instance, we could write a get_client method that just returns a client for other methods to use:
protected
def get_client
client = Google::APIClient.new(
application_name: "Example Application",
application_version: "1.0")
client.authorization = Signet::OAuth2::Client.new(
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:audience => 'https://accounts.google.com/o/oauth2/token',
:scope => 'https://www.googleapis.com/auth/analytics.readonly',
:issuer => SERVICE_ACCOUNT_EMAIL_ADDRESS,
:signing_key => Google::APIClient::KeyUtils.load_from_pkcs12(PATH_TO_KEY_FILE, 'notasecret')).tap { |auth| auth.fetch_access_token! }
client
end
It's protected because external code - stuff outside your Analytic class - shouldn't work with it directly. They should use the methods we provide for them.
You could also provide a helper method for getting results from the API. I'm not familiar with the query API, but it looks like it's your metrics value that changes. So maybe something like this:
protected
def get_result(metrics)
client = self.get_client
api_method = client.discovered_api('analytics','v3').data.ga.get
result = client.execute(:api_method => api_method, :parameters => {
'ids' => PROFILE,
'start-date' => Date.new(2014,1,1).to_s,
'end-date' => Date.today.to_s,
'dimensions' => 'ga:pagePath',
'metrics' => metrics,
'filters' => 'ga:pagePath==/'
})
result
end
Now you can write simple methods that your external classes can use:
def new_users
get_result('ga:newUsers')
end
def total_visits
get_result('ga:pageViews')
end
If you can, try to return simple data from these methods. Maybe total_visits is going to return get_result('ga:pageViews')['totalsForAllResults']['ga:pageviews'] instead. Code outside your class shouldn't have to know about the GA data format to work with it. This is called encapsulation.
From talking on Skype, I think there are several things to look at
Init
Currently, you're using the google_analytics_api method every time you want to use the module. This is completely inefficient, and is partly why you have this issue now. Instead, I would create an init method, which will fire each time you initialize the object (and make GoogleAnalytics into a class of its own):
#lib/google_analytics.rb
Class GoogleAnalytics
def initialize
... google_analytics_api method here
end
end
This will allow you to treat your current module as a real Ruby object - like this:
#analytics = GoogleAnalytics.new #-> fires initialize method
This will give you the ability to call the object (which will pull the data from the API), and then split that data accordingly, for the different use cases you have.
Instance Methods
This leads me nicely onto the idea of instance methods
What you're referring to, and indeed what Alex P is referring to, is the idea of an instance method. This doubles as an attribute for an object, but essentially allows you to call a piece of functionality on an instance of a method.
So in Alex's example, you have:
def new_users
get_result('ga:newUsers')
end
This is just calling an instance method of your class:
GoogleAnalytics::Analytic.new_users
This will create an instance of the Analytic class, and then call the new_users method (which should be a class method). This method will then allow you to call instance methods on the newly initialized object, hence the get_result method call
--
What I'm proposing is to use instance methods after the object has been initialized, giving you acesss to the data defined with google_analytics_api
For example:
#app/controllers/analyics_controller.rb
Class AnalyticsController < ApplicationController
def index
#analytics = GoogleAnalytics.new
#new_users = #analytics.new_users
end
end
#lib/google_analytics.rb
Class GoogleAnalytics
def initialize
... google_analytics_api method here
end
def new_users
return [new_users_data]
end
end
The one caveat to this is whether this will work without the module. I think it should, but it's untested for me

Make API Request, puts response in RAILS 4.0.0

I have been using ruby to make API calls and operating strictly in the terminal for some time. I am now in the process of learning more about rails and trying to get out of my terminal. How can I, using rails 4.0, put a variable to the screen from an already existing .rb file? I am confused as to where I should write the API request to get the variable- Is it a controller, can I write it directly in a view, etc.
Sample idea:
#test.rb
call= "/api/v2/surveys/"
auth = {:username => "test", :password => "password"}
url = HTTParty.get("https://surveys.com#{call}",
:basic_auth => auth,
:headers => { 'ContentType' => 'application/json' } )
response = JSON.parse(url.body)
survey_ids = response["surveys"].map { |s| s["id"] }
survey_ids.each do |i|
puts i
end
That is a sample .rb script I already have. The difference is I would like for puts i to happen on a web app when a page is loaded instead of me running the script in my terminal. What would I use in rails to make that happen?
It depends entirely on how your application is going to be set up but here's a basic example:
Say you have a Survey model:
class Survey < ActiveRecord::Base
attr_accessible :survey_id
end
You can place your call for a list of surveys (I'm assuming that's what your code does) in the SurveysController:
class SurveysController < ApplicationController
def index
#surveys = Survey.all
end
def show
#survey = Survey.find(params[:id])
end
def pull_surveys
call= "/api/v2/surveys/"
auth = {:username => "test", :password => "password"}
url = HTTParty.get("https://surveys.com#{call}",
:basic_auth => auth,
:headers => { 'ContentType' => 'application/json' } )
response = JSON.parse(url.body)
survey_ids = response["surveys"].map { |s| s["id"] }
survey_ids.each do |i|
Survey.create(survey_id: i)
end
end
After calling the pull_surveys method, you'll actually have surveys your view can load so in your views for the Survey Model you can use #surveys or #survey (depending on which view you're in) and serve up whatever you want (e.g #survey.survey_id in show to show that specific survey's ID).
Note that you'll want to be careful about where you place your API call methods - I placed it in the controller for simplicity's sake but you may not want to do this.
There's lots of useful info in the rails guides to get you started: http://guides.rubyonrails.org/index.html

ruby on rails can i use something like Model.find(params[:id]).where()

Hi, I am still a student and I'm taking a software engineering course and we have this big project (web design) and we're using rails so my question is I have a table Users and a table Groups and another association table GroupUsers where it has two foreign keys user_id and group_id. Whenever a user creates or join an already created group his id and the group_id are added to GroupUsers table
module GroupUsersHelper
def join
#group_id = params[:id]
#user_id = params[:user_id]
#newuser= GroupUser.new(:group_id => #group_id, :user_id => #user_id)
#newuser.save
redirect_to(:controller => 'groups', :action => 'show', :id => #group_id)
end
end
Now I have to create method leave group where I'll have to destroy the record from GroupUsers, so I wrote this code also in GroupUsersHelper
def leave
#group_id = params[:group_id]
#user_id = params[:user_id]
#group_user_id = params[:group_user_id]
#newuser= GroupUser.find(#group_user_id).where(:group_id => #group_id, :user_id =>
#user_id)
#newuser.destroy
redirect_to(:controller => 'groups', :action => 'show', :id => params[:id])
end
but I get this error
ActiveRecord::RecordNotFound in GroupsController#leave
Couldn't find GroupUser without an ID
If you need more info about the code please let me know.
The other answers addressed your code example, but not the question in the title. You cannot use Model.find(params[:id]).where() in ActiveRecord (Rails' default ORM). Model.find returns a single record (an instance of your Model class) but Model.where returns a Model::ActiveRecord_Relation object (an array-like object). The .where method is only available on your Model class directly (i.e. Model.where) or on an ActiveRecord_Relation. If you are chaining methods, you need to make sure each method returns an ActiveRecord_Relation. For example:
Model.where("name = ?", "max")
.order(created_at: :desc)
.reorder(created_at: :asc)
.includes(:sent_comments)
Model.find(params[:id]) will generate such kind of query
*Lets take an example : GroupUser.find(params[:id]) will generate sql equivalent*
select * from group_users where id = params[:id]
If you want to add where condition do something like this
GroupUser.where("# id ={params[:id]} and someColumn = #{somevariable}")
You are not passing the groupuser_id in the params hash. Instead of storing it in the #group_id and #user_id, just try this:
#newuser= GroupUser.find_by_group_id_and_user_id(params[:group_id],params[:user_id])
Can you additionally post your params dump so we can figure out how to make a good redirect since i hence that will be a problem also.
Just do:
group = Group.find(params[:group_id])
user = User.find(params[:user_id])
group.users.delete(user)
group.save
This will remove the user from the users association, and will automatically destroy the GroupUser record when the group is saved. See the collection.delete method.
#max pleaner is right, you should not use Model.find().where().
It is incorrect, and if you think about it, you will see that it is redundant. To find the association you wish to destroy, you either need params[:group_id] and params[:user_id], or just params[:group_user_id].
If you have passed the params[:group_user_id] to the leave method, you already have all the information needed to find the association:
def leave
#newuser= GroupUser.find(params[:group_user_id])
#newuser.destroy
redirect_to(:controller => 'groups', :action => 'show', :id => params[:group_id])
end
Or alternatively, If you have passed the params[:group_id] and params[:user_id] to the leave method:
def leave
#newuser= GroupUser.find_by_group_id_and_user_id(params[:group_id], params[:user_id])
#newuser.destroy
redirect_to(:controller => 'groups', :action => 'show', :id => params[:group_id])
end
Also, there are a few other redundancies in your code that you will notice I omitted above. There is no need to assign each parameter to an instance variable before passing the parameter to the find() method:
#model_id = params[:model_id]
#model= Model.find(#model_id)
#can be simplified to:
#model = Model.find(params[:model_id])
Generally, you only want to create instances variables for a record or collection of records you will need to access in a view (such as a Group in the context of your application).

How to pass addition attributes or arguments to a Object.new in Controller

I have a simple
#redemption = Redemption.new(params[:redemption])
I need to pass :user_id => current_user.id into this new Redem
I have tried:
#redemption = Redemption.new(params[:redemption], :user_id => current_user.id)
this doesn't build the Redemption with a user_id...
params is a Hash, so if you want additional attributes in create, just add them to params, e.g.:
params[:redemption][:user_id] = current_user.id
#redemption = Redemption.new params[:redemption]
Or, if you want to add multiple attributes at once, use Hash#merge, e.g.:
redemption_params = params[:redemption].merge :user_id => current_user.id,
:some_attr => :foo
#redemption = Redemption.new redemption_params
In this particular case, though, you could probably use a shorter syntax, e.g.:
current_user.redemptions.create params[:redemption]

How to copy or clone model?

I have a model Book with attributes id, name, price. I have an instance of Book:
b1 = Book.new
b1.name = "Blah"
b1.price = 12.5
b1.save
I would like to copy b1, create another instance of the Product model. I'm tryid p1=b1.clone then p1.save but it didn't work. Any idea?
And my environment is:
Netbeans 6.9 RC2
JRuby 1.5.0
EDITED:
My TemporaryProduct model:
class Admin::TemporaryProduct < ActiveRecord::Base
def self.update_from_web_service(web_service_url)
response = HTTParty.get(web_service_url)
response["webServiceResult"]["product"].each do|element|
unless exists? :orignal_product_id => element['id']
create!(
:name => element['name'],
:price => element['price'],
:amount => element['amount'],
:description => element['description'],
:orignal_product_id => element['id'],
:image => element['image'],
:shop_account_number => element['shopAccountNumber'],
:unit => element['unit']
)
end
end
end
end
Product is create action:
def create
#temporary_products = Admin::TemporaryProduct.find_all_by_orignal_product_id(params[:product])
#product = Admin::Product.new(#temporary_products.attributes)
# #product = #temporary_products.clone
respond_to do |format|
format.html { redirect_to(admin_products_url, :notice => 'Admin::Product was successfully created.') }
end
end
I want to clone all b1's attributes to p1 model.
I think you want:
b2 = Book.create(b1.attributes)
Edit:
Given your create action above, I think what you want to do is change the line which starts #product to
#temporary_products.each {|tp| Admin::Product.create(tp.attributes)}
That will create a new Product object for each TemporaryProduct object, using the same attributes as the TemporaryProduct. If that's not what you want, let me know.
You can make duplicate record using dup in rails For Example,
b1 = Book.create(name: "example", price: 120)
b1.save
duplicate_record = b1.dup
duplicate_record.save!
or you can create first new record and then make a duplicate
Hope this is useful for you.
If by didn't work you mean that there is no new record in the database then you probably want to set the id of p1 to null before you save. If the clone has the same id as the original then it would appear to represent the same object.

Resources