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
Related
I have a simple form on a website where a user enters a mailing address.
I have a service which can validate this address and return various responses, either Success, Suspect, or Invalid, as well as return the full and most complete zip code for that address. If the response is "Success", then I will save it to the db. If the response is "Suspect", then I will update the zip field and ask them to confirm. If the response is "Invalid" then I will return an error message asking them to contact us in person.
I'm trying to set up my rails create action such that it makes a call to my service (for example http:/addresssValidator.com) and I want to inform the user if they have a valid address or not, and update the zip with the suggested zip code.
Looking for address validation in rails however, seems to only give me APIs for using the built in error and validation system in rails and not how to return my own custom results to the form.
How can I do this?
Below is my code:
def create
#valid = validate_address
#address = Address.new(address_params)
if #valid
if #address.save
redirect_to "/survey/success"
else
p #address.errors
respond_to do |format|
format.json {render json: #address.errors}
format.html {render "/survey/failure"}
end
end
else
##//Display errors
end
end
def validate_address
#api_key = "my_api_key"
HTTParty.post("http://addressValidator.com",
{
:body => [
"StreetAddress" => params[:address][:address_line_2] + " " + params[:address][:address_line_1],
"City" => params[:address][:city],
"PostalCode" => params[:address][:zip],
"State" => params[:address][:state],
"CountryCode" => params[:address][:country],
"Locale" => "en" ].to_json,
:APIKey => #api_key ,
:headers => { 'Content-Type' => 'application/json', 'Accept' => 'application/json'}
})
return false;
##//return actual results from validator
end
If you want to add a custom error to a specific field:
#address.errors[:FIELD_NAME] << "Custom Error Message"
that way the error is added to the instance that you have, and will be shown in the form itself as the other errors appear.
if you just want to display an error, you can add it to the flash[:error] like this:
flash[:error] = "Invalid address"
if you want the error to move to the next method (like if you will use redirect after it), or:
flash.now[:error] = "Invalid address"
if you want the error to be available only in current action.
as #illusionist said in comment.
and in the html code, check if it exists, and if so, print it:
<% if flash[:error] %>
<%= flash[:error] %>
<% end %>
in both ways, you can add the error from #valid.
If you want to store the new zip, add #address.zip = #valid.zip. That way it will show it in the field in the form.
Models do the work, forms are stupid
In Rails you add errors to your models. To add an error you would do something like this:
if #address.country == "Sealand"
#address.errors[:country] << ["we don't ship there"]
end
In Rails forms are just are just simple form-builders bound to a model which create HTML. They don't actually have a role in validation besides displaying errors.
Custom validations
Rails lets you create custom validator classes and HTTParty is made to extend classes.
Lets mosh them together:
class AddressValidator < ActiveModel::Validator
include HTTParty
base_uri 'addressValidator.com'
format :json
def validate(address)
response = self.post('/',
# Note that I am just guessing here.
body: {
"StreetAddress" => address.street_address,
"City" => address.city,
"PostalCode" => address.zip,
"State" => address.state,
"CountryCode" => address.country
}
)
if response.success?
# bind the errors to the model
unless response["StreetAddress"] == "valid"
record.errors[:street_address] << response["StreetAddress"]
end
else
e = response.response
logger.warn "Failed to remotely validate Address. #{ e.message }"
address.errors[:base] << "Failed to remotely validate!"
end
end
end
Of course you will need to adapt this to the actual response from the API and your model. This is just a starting point demonstrating the coarse strokes.
So how would you use it?
def Address < ActiveRecord::Base
validates_with AddressValidator
# ...
end
http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations
http://blog.teamtreehouse.com/its-time-to-httparty
I'm currently using the Full Calendar gem for Rails, and all is working perfectly. However I would like to dynamically display calendar events based on the :id in the address bar via routes in Ruby on Rails.
Here's my index.json.jbuilder file
json.array!(#appointments.all.where(:user_id => params[:id])) do |appointment|
json.extract! appointment, :id, :user_id, :business_title, :address
json.user appointment.user_id
json.title appointment.business_title
json.start appointment.starts_at
json.className 'boobs'
json.end appointment.starts_at + 30.minutes
json.url appointment_url(appointment, format: :html)
end
This works when the params[:id] is replaced with an existing user id from my database. The url thats generating this is localhost:3000/c/2, so this should display all the events that have been created/or belong to the user with the ID of 2. Here's my Routes:
get 'calendar/:id' => 'users#calendar'
Inside your calendar action, write:
#id = params[:id]
Then in your jbuilder file, write:
json.array!(#appointments.all.where(:user_id => #id)) do |appointment|
I guess that means, you have this in your calendar action:
#appointments = Appointment
...which makes me wonder: why aren't you doing the following in your calendar action:
#appointments = Appointment.all.where(:user_id => params[:id])
...and then in your jbuilder file:
json.array! #appointments do |appointment|
...
...
end
Response to comment:
Suppose you set up your controller like this:
class UsersController < ApplicationController
def calendar
#user_id = params[:user_id] #route: get 'users/calendar/:user_id', to: 'users#calendar'
#By default, render()'s calendar.html.erb
end
def feed
#appointments = Appointment.all.where(user_id: #user_id)
#For the request: http://localhost:3000/users/feed.json, by
#default render()'s /app/views/users/feed.json.jbuilder
end
end
..then you request your calendar() action like this:
http://localhost:3000/users/calendar/2
The problem is: once your rails app is done responding to a request, all the variables inside an action are destroyed, so #user_id, which was assigned the value of params[:user_id], i.e. 2, will be destroyed. As a result, when your calendar sends a request to the feed() action here:
<script>
$(document).ready(function() {
// page is now ready, initialize the calendar...
$('#calendar').fullCalendar({
events: '/users/feed.json' //****HERE****
})
});
</script>
...you won't be able to access #user_id inside the feed() action.
And, because the calendar was never given the user_id, the calendar cannot include it in the url when the calendar sends a request to /users/feed.json.
The stateless nature of the web has been a thorn in the side of developers for a long time, so they have invented ways to make data persist between requests using hidden form fields, cookies, sessions, local storage, databases, etc.
Rail's provides a very simple sessions mechanism, which you can use like this:
#route: get 'users/calendar/:user_id', to: 'users#calendar'
def calendar
session[:user_id] = params[:user_id] #Store the value in the session hash.
end
def feed
#appointments = Appointment.all.where(user_id: session[:user_id]) #Retrieve the value from the session hash.
end
However, you should read about the potential problems when using sessions.
Finally, as far as I can tell your jbuilder page responds with a lot of data that is irrelevant. According to the fullcalendar docs, an Event only requires a :title and the start time :starts_at. But, your jbuilder file provides additional data that is not even used by an Event:
:id
:user_id
:address
And, you actually provide the :user_id data twice, once here:
json.extract! appointment, :id, :user_id, :business_title, :address
and the second time aliased under a different name, user:
json.user appointment.user_id
It looks to me like, the following is all you need:
json.array! #appointments do |appointment|
json.title appointment.business_title
json.start appointment.starts_at
json.className 'boobs'
json.end appointment.starts_at + 30.minutes
json.url appointment_url(appointment, format: :html)
end
...which produces json like this;
[
{"title":"XYZ",
"start":"2014-12-25T12:30:00.000Z",
"className":"boobs",
"end":"2014-12-25T13:00:00.000Z",
"url":"http://localhost:3000/appointments/1.html"},
{"title":"ABC",
"start":"2015-01-25T09:30:00.000Z",
"className":"boobs",
"end":"2015-01-25T10:00:00.000Z",
"url":"http://localhost:3000/appointments/2.html"},
{"title":"TTT",
"start":"2015-01-03T14:30:00.000Z",
"className":"boobs",
"end":"2015-01-03T15:00:00.000Z",
"url":"http://localhost:3000/appointments/3.html"},
]
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.
this newbie here is smacking his head with webservices over Rails.
Perhaps someone could ease my pain?
I've created a simple rails app, and generated the scaffold MyRecords. Then I'm trying to create a record over irb with the code below :
testWS.rb
require 'HTTParty'
class MyRecordCreate
include HTTParty
base_uri 'localhost:3000'
def initialize(u, p)
#auth = {:username => u, :password => p}
end
def post(text)
options = { :body => { name:text} }
self.class.post('/my_records', options)
end
end
response = HTTParty.get("http://localhost:3000/my_records/new.json")
print response
record = MyRecordCreate.new("","").post("test remote record")
print record
With the code above, I managed to create a record. the thing is that my Record (which only has the column "name") is created with an empty name!
Any suggestions on this one?
I'm longing to slice this despair piece by piece.
Thank you for your contribute.
Try adding these two lines to your HTTParty class:
format :json
headers "Accept" => "application/json"
These tell httparty and the remote service to which it connects to send and receive JSON. For your example (with .json at the end of the URL) it isn't necessary to add the second line, but I find it is good practice and keep it anyway.
The next problem is that Rails expects your uploaded data to be inside the top level name of your object. So, for your example, the options line should look something like:
options = { :body => { :person => { :name => text } } }
Replace person with the name of the model that you are attempting to create.
I would like to connect to an API using Ruby On Rails 2.3.8 and HTTParty gem.
My model is the following:
class Onnion < ActiveRecord::Base
require 'net/http'
include HTTParty
base_uri 'http://myapiurl.com'
digest_auth 'user', 'password'
disable_rails_query_string_format
def self.create_rma(order)
put('/orders/rma', :query => {:action => 'put', :data => {:api_key => 'user', :onnion_order_id => order.id, :customer_rma => order.saving.profile.user.id, :comments => ''}})
end
end
What I would like to do is to call a method of the API called Put, with certain parameters grouped within data parameter.
After executing this method I'm getting a 401 Unauthorized error message.
What am I doing wrong? This is the first time I'm trying to do something like this.
What version of HTTParty are you using, and have you tried using the very latest version from Github? There were some fixes to do with digest auth security a little while ago in version 0.7.3.
If that doesn't work it could be that the server you're attempting to talk to isn't following protocol correctly. I've had this happen before, had to monkey patch HTTParty to get it to login correctly. I'll put the patch I used here in-case it works for you...
module Net
module HTTPHeader
class DigestAuthenticator
# use NC = 1 instead of 0
def authorization_header
#cnonce = md5(random)
header = [%Q(Digest username="#{#username}"),
%Q(realm="#{#response['realm']}"),
%Q(nonce="#{#response['nonce']}"),
%Q(uri="#{#path}"),
%Q(response="#{request_digest}")]
[%Q(cnonce="#{#cnonce}"),
%Q(opaque="#{#response['opaque']}"),
%Q(qop="#{#response['qop']}"),
%Q(nc="1")].each { |field| header << field } if qop_present?
header
end
private
def request_digest
a = [md5(a1), #response['nonce'], md5(a2)]
a.insert(2, "1", #cnonce, #response['qop']) if qop_present?
md5(a.join(":"))
end
end
end
end
module HTTParty
class Request
def setup_digest_auth
# issue a get instead of a head request
res = http.get(uri.request_uri, options[:headers]||{})
if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
#raw_request.digest_auth(username, password, res)
end
end
end
end
The changes made were to send NC 1 and not NC 0, and also to do a GET request, rather than a HEAD request in setup_digest_auth