How to make a PUT request from a Rails model to controller? - ruby-on-rails

I want to update model via PUT request in Rails app. What would be the best way to do that?
Basically I have:
def method
...
#relation = Relation.find(34)
#relation.name = "new_name"
#relation.save
end
This gives me errors in SQLite ("cannot start a transaction within a transaction").
Switching to put/post should I guess save the problem.. What would be the right way to do it?

So after some time, I actually found the solution. Here is the code for the Resque worker, that updates the Relation model via PUT. Using this method I don't get SQLite busy exception errors.
class VideoCollector
def self.perform(rel_id)
#relation = Relation.find_by_id(rel_id)
#url = Rails.application.routes.url_helpers.relation_url(#relation)
#uri = URI(#url)
#body ={"collected" => "true"}.to_json
request = Net::HTTP::Put.new(#uri.path, initheader = {'Content-Type' =>'application/json'})
request.body = #body
response = Net::HTTP.new(#uri.host, #uri.port).start {|http| http.request(request) }
end
end
Maybe that will be useful to someone.

Related

Why are records not being stored to the db when testing with VCR?

I have an interactor foo with method perform on it which makes an API call, and records the response through ActiveRecord. It works fine. I have a spec for it that triggers the method with FactoryBot data, then checks the db for the expected records. This also works fine. However, when I wrap the call in a VCR.use_cassette, the call is made (and resulting cassette created), but the db records don't seem to be written. What am I missing?
spec looks like this:
it 'should do a thing' do
bar = FactoryGirl.create(:bar)
VCR.use_cassette('foo/cassette') do
MyInteractor.perform(bar)
end
record = BarRecord.find_by(bar_id: bar.id)
expect(record.status.to_sym).to be(:success)
end
perform method looks roughly like this:
def perform(bar)
uri = URI.parse("<url>")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.basic_auth Settings.username, Settings.password
req['Accept'] = 'Application/json'
req.body = post_params.to_json
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
res = https.request(req)
record = BarRecord.new(bar_id: bar.id)
record.status = JSON.parse(res.body)["status"]
record.save!
record
end
BarRecord should be created by the call to perform. Without VCR it is.
For the curious, there was in fact nothing wrong with VCR. I was storing items with a fixed timestamp, and then reading them with a dynamic one that didn't overlap. Full pebkac.

How to properly use Sidekiq to process background tasks in Rails

So, i've generated a rails app using https://github.com/Shopify/shopify_app - and for the most part have the app working as intended - it's goal is to get product quantities from an external stock management API, and then update the variant quantities in Shopify with the latest quantities from that stock management system.
My problem is that the initial POST request to the external API responds with a large number of products - this takes upwards of 15 seconds sometimes. In addition to this, another portion of my app then takes this response, and for every product in the response that also exists in Shopify, it will make a PUT request to Shopify to update the variant quantities. As with the initial request, this also takes upwards of 10-15 seconds.
My problem is that i'm hosting the app on Heroku, and as a result i've hit their 30 second request timeout limit. As a result I need to use a background worker to offset at least one of the requests above (perhaps both) to a worker queue. I've gone with the widely recommended Sidekiq gem - https://github.com/mperham/sidekiq - which is easy enough to set up.
My problem is that I don't know how to get the results from the finished Sidekiq worker job, and then use that again within the Controller - I also don't know if this is best practice (i'm a little new to Rails/App development).
I've included my controller (prior to breaking it down into workers) that currently runs the app below - I guess I just need some advice - am I doing this correctly - should some of this logic be inside a Model, and if so how would that model then communicate with the Controller, and then how would Sidekiq then fit into all of it.
Appreciate any advice or assistance, thanks.
class StockManagementController < ShopifyApp::AuthenticatedController
require 'uri'
require 'net/http'
require 'json'
require 'nokogiri'
require 'open-uri'
require 'rexml/document'
def new
#token = StockManagementController.new
end
def get_token
url = URI('https://external.api.endpoint/api/v1/AuthToken')
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
#HEROKU_ENV_USERNAME = ENV['HEROKU_ENV_USERNAME']
#HEROKU_ENV_PASSWORD = ENV['HEROKU_ENV_PASSWORD']
request = Net::HTTP::Post.new(url)
request['content-type'] = 'application/x-www-form-urlencoded'
request['cache-control'] = 'no-cache'
request.body = 'username=' + #HEROKU_ENV_USERNAME + '&password=' + #HEROKU_ENV_PASSWORD + '&grant_type=password'
response = http.request(request)
responseJSON = JSON.parse(response.read_body)
session[:accessToken] = responseJSON['access_token']
if session[:accessToken]
flash[:notice] = 'StockManagement token generation was successful.'
redirect_to '/StockManagement/product_quantity'
else
flash[:alert] = 'StockManagement token generation was unsuccessful.'
end
end
def product_quantity
REXML::Document.entity_expansion_text_limit = 1_000_000
#theToken = session[:accessToken]
if #theToken
url = URI('https://external.api.endpoint/api/v1/ProductQuantity')
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(url)
request['authorization'] = 'bearer ' + #theToken + ''
request['content-type'] = 'application/xml'
request['cache-control'] = 'no-cache'
response = http.request(request)
responseBody = response.read_body
finalResponse = Hash.from_xml(responseBody).to_json
resultQuantity = JSON.parse finalResponse
#connectionType = resultQuantity['AutomatorResponse']['Type']
#successResponse = resultQuantity['AutomatorResponse']['Success']
#errorResponse = resultQuantity['AutomatorResponse']['ErrorMsg']
productQuantityResponse = resultQuantity['AutomatorResponse']['ResponseString']
xmlResponse = Hash.from_xml(productQuantityResponse).to_json
jsonResponse = JSON.parse xmlResponse
#fullResponse = jsonResponse['StockManagement']['Company']['InventoryQuantitiesByLocation']['InventoryQuantity']
# This hash is used to store the final list of items that we need in order to display the item's we've synced, and to show the number of items we've sycned successfully.
#finalList = Hash.new
# This array is used to contain the available products - this is used later on as a way of only rendering
#availableProducts = Array.new
# Here we get all of the variant data from Shopify.
#variants = ShopifyAPI::Variant.find(:all, params: {})
# For each peace of variant data, we push all of the available SKUs in the store to the #availableProducts Array for use later
#variants.each do |variant|
#availableProducts << variant.sku
end
#Our final list of products which will contain details from both the Stock Management company and Shopify - we will use this list to run api calls against each item
#finalProductList = Array.new
puts "Final product list has #{#fullResponse.length} items."
puts #fullResponse.inspect
# We look through every item in the response from Company
#fullResponse.each_with_index do |p, index|
# We get the Quantity and Product Code
#productQTY = p["QtyOnHand"].to_f.round
#productCode = p["Code"].upcase
# If the product code is found in the list of available products in the Shopify store...
if #availableProducts.include? #productCode
#variants.each do |variant|
if #productCode === variant.sku
if #productQTY != 0
#finalProductList << {
"sku" => variant.sku,
"inventory_quantity" => variant.inventory_quantity,
"old_inventory_quantity" => variant.old_inventory_quantity,
"id" => variant.id,
"company_sku" => #productCode,
"company_qty" => #productQTY
}
end
end
end
end
end
# If we get a successful response from StockManagement, proceed...
if #finalProductList
flash[:notice] = 'StockManagement product quantity check was successful.'
puts "Final product list has #{#finalProductList.length} items."
puts #finalProductList
#finalProductList.each do |item|
#productSKU = item["sku"]
#productInventoryQuantity = item["inventory_quantity"]
#productOldInventoryQuantity = item["old_inventory_quantity"]
#productID = item["id"]
#companySKU = item["company_sku"]
#companyQTY = item["company_qty"]
url = URI("https://example.myshopify.com/admin/variants/#{#productID}.json")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Put.new(url)
request["content-type"] = 'application/json'
request["authorization"] = 'Basic KJSHDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDF'
request["cache-control"] = 'no-cache'
request.body = "{\n\t\"variant\": {\n\t\t\"id\": #{#productID},\n\t\t\"inventory_quantity\": #{#companyQTY},\n\t\t\"old_inventory_quantity\": #{#productOldInventoryQuantity}\n\t}\n}"
# This is the line that actually runs the put request to update the quantity.
response = http.request(request)
# Finally, we populate the finalList has with response information.
#finalList[#companySKU] = ["","You had #{#productOldInventoryQuantity} in stock, now you have #{#companyQTY} in stock."]
end
else
# If the overall sync failed, we flash an alert.
flash[:alert] = 'Quantity synchronisation was unsuccessful.'
end
# Lastly we get the final number of items that were synchronised.
#synchronisedItems = #finalList.length
# We flash this notification, letting the user known how many products were successfully synchronised.
flash[:notice] = "#{#synchronisedItems} product quantities were synchronised successfully."
# We then pretty print this to the console for debugging purposes.
pp #finalList
else
flash[:alert] = #errorResponse
end
end
end
First of all, your product_quantity method is way too long. You should break it into smaller parts. 2nd, http.verify_mode = OpenSSL::SSL::VERIFY_NONE should not be done in production. The example you've provide along with your question are too complex and are therefore difficult to answer. It sounds like you need a basic understanding of design patterns and this is not a specific ruby question.
If your app needs to make realtime API calls inside of a controller this is a poor design. You don't want to keep requests of any kind waiting for more than a couple of seconds at most. You should consider WHY you need to make these requests in the first place. If it's data you need quick access to, you should write background jobs to scrape the data on a schedule and store it in your own database.
If a user of your app makes a request which needs to wait for the API's response, you could write a worker to handle fetching the API data and eventually send a response to the user's browser probably using actioncable.
For your constant definitions you probably should do this in an initializer wihich you would keep in my_app_root/config/initializers/constants.rb which get loaded into your app at runtime. You could just call them where need using te ENV[] syntax but if you prefer simpler constants drop the # since that naming convention in ruby is for instance objects.
#app_root/config/initializers/constants.rb
HEROKU_ENV_USERNAME = ENV['HEROKU_ENV_USERNAME']
HEROKU_ENV_PASSWORD = ENV['HEROKU_ENV_PASSWORD']

Rails app to check the status of a server

I want to achieve a problem, where we manually go and check a webapp/server if it is up/down. I want to build a rails app which can automate this task.
Consider my app url is: HostName:PORT/Route?Params (may or may not have port in url)
I checked 'net/http'
def check_status()
#url='host'
uri = URI(#url)
http = Net::HTTP.new(#url,port)
response = http.request_get('/<route>?<params>')
if response == Net::HTTPSuccess
#result='Running'
else
#result='Not Running'
end
end
I am facing error at ,
response = http.request_get('/<route>?<params>')
when the app is down throwing 'Failed to open TCP connection to URL' which is correct.
Can you guys help me find some new solution or how can I improve the above implementation?
Since it's working as intended and you just need to handle the error that's returned when the app is down, wrap it in a rescue block.
def check_status()
#url='host'
uri = URI(#url)
http = Net::HTTP.new(#url,port)
begin
response = http.request_get('/<route>?<params>')
rescue TheClassNameOfThisErrorWhenSiteIsDown
#result = 'Not Running'
end
if response == Net::HTTPSuccess
#result='Running'
else
#result='Not Running'
end
end
end
Just came across this old question. Net::HTTP methods get and head don't raise an exception. So use one of these instead.
def up?(site)
Net::HTTP.new(site).head('/').kind_of? Net::HTTPOK
end
up? 'www.google.com' #=> true

How to use delayed job in rails observer with faraday

With the app I'm building, we're using trumpia to schedule and send sms, and email messages to our users. When they sign up, I'm using an observer to trigger the call to trumpia, and I'm using faraday to actually connect with the trumpia API. My problem is that these calls are taking a lot of time, and I need to put them into the background. I also have a smaller set of commands that I need to run immediately, so I can't just delay the whole after_create.
I'm thinking of using delayed_job to accomplish this, but I have no idea how to do it with something as convoluted as my code is. How can I do this? I think I need to put the Faraday calls into a method, but I don't know how. -- Thanks
Here's my observer. I've left out the code that isn't necessary.
class UserObserver < ActiveRecord::Observer
def after_create(user)
#This first part should not be delayed
#user = user
kevin = User.find(1)
user.increment!(:renewcash, by = 5)
body = "Hi " + #user.name.split(' ', 2).first + ", ..."
kevin.send_message(#user, body, "First message", sanitize_text=true, attachment=nil)
# This whole second part needs to be delayed.
conn = Faraday.new(:url => 'http://api.trumpia.com') do |faraday|
faraday.request :multipart
faraday.response :logger
faraday.adapter Faraday.default_adapter
end
trumpia_body = {:list_name => ... .... }.to_json
response = conn.put do |req|
req.url '/rest/v1/XXXXXXXXX/subscription'
req.headers['X-Apikey'] = 'XXXXXXXXXXXXX'
req.headers['Content-Type'] = 'application/json'
req.body = trumpia_body
end
response2 = conn.get do |req2|
req2.url '/rest/v1/XXXXXXXX/subscription?row_size=1'
req2.headers['X-Apikey'] = 'XXXXXXXXXXX'
req2.headers['Content-Type'] = 'application/json'
end
end
I'm using rails 3.2.8. But I haven't install delayed job yet, and I'm open to a different queing gem.

Problems with MailChimp API in Ruby Error Code: -90

I am using the following code in my MailChimp Controller to submit simple newsletter data. When It is sent I receive the following error as a "Method is not exported by this server -90" I have attached my controller code below. I am using this controller for a simple newsletter signup form. (Name, Email)
class MailchimpController < ApplicationController
require "net/http"
require "uri"
def subscribe
if request.post?
mailchimp = {}
mailchimp['apikey'] = 'f72328d1de9cc76092casdfsd425e467b6641-us2'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
uri = URI.parse("http://us2.api.mailchimp.com/1.3/?method=listSubscribe")
response = Net::HTTP.post_form(uri, mailchimp)
mailchimp = ActiveSupport::JSON.decode(response.body)
if mailchimp['error']
render :text => mailchimp['error'] + "code:" + mailchimp['code'].to_s
elsif mailchimp == 'true'
render :text => 'ok'
else
render :text => 'error'
end
end
end
end
I highly recommend the Hominid gem: https://github.com/tatemae-consultancy/hominid
The problem is that Net::HTTP.post_form is not passing the "method" GET parameter. Not being a big ruby user, I'm not certain what the actual proper way to do that with Net::HTTP is, but this works:
require "net/http"
data="apikey=blahblahblah"
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=lists', data)
}
p response.body
That's the lists() method (for simplicity) and you'd have to build up (and urlencode your values!) your the full POST params rather than simply providing the hash.
Did you take a look at the many gems already available for ruby?
http://apidocs.mailchimp.com/downloads/#ruby
The bigger problem, and main reason I'm replying to this, is that your API Key is not obfuscated nearly well enough. Granted I'm used to working with them, but I was able to guess it very quickly. I would suggest immediately going and disabling that key in your account and then editing the post to actually have completely bogus data rather than anything close to the correct key. The list id on the other hand, doesn't matter at all.
You'll be able to use your hash if you convert it to json before passing it to Net::HTTP. The combined code would look something like:
mailchimp = {}
mailchimp['apikey'] = 'APIKEYAPIKEYAPIKEYAPIKEY'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=listSubscribe', mailchimp.to_json)
}

Resources